Skip to content

feat: add WebSocket reconnection with exponential backoff (#57)#115

Open
harika880 wants to merge 1 commit into
sreerevanth:mainfrom
harika880:feat/ws-reconnection-57
Open

feat: add WebSocket reconnection with exponential backoff (#57)#115
harika880 wants to merge 1 commit into
sreerevanth:mainfrom
harika880:feat/ws-reconnection-57

Conversation

@harika880
Copy link
Copy Markdown

@harika880 harika880 commented May 31, 2026

Closes #57

Changes

  • Added useWebSocket hook with exponential backoff reconnection (1s → 2s → 4s, max 30s)
  • Live Feed badge shows Reconnecting… with elapsed time when disconnected
  • On reconnect, badge returns to streaming and event list is preserved
  • Max attempts configurable via NEXT_PUBLIC_WS_MAX_ATTEMPTS env variable
  • 10 Jest tests covering all reconnection state transitions

Summary by CodeRabbit

  • New Features

    • Enhanced WebSocket reconnection with visual status indicators, including elapsed time display during reconnection attempts and exponential backoff timing
  • Tests

    • Added comprehensive test coverage for reconnection logic, backoff delay calculations, and connection state transitions
  • Chores

    • Updated testing framework configuration and development dependencies to support automated testing

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Implements WebSocket reconnection with exponential backoff for the frontend live event feed. Adds a custom React hook managing socket lifecycle, state transitions, and reconnection scheduling; provides Jest test coverage for backoff logic and state machine; integrates hook into the dashboard page with status badges showing streaming, reconnecting with elapsed time, or failed.

Changes

WebSocket Reconnection and Live Feed

Layer / File(s) Summary
WebSocket reconnection types and utilities
frontend/lib/wsReconnect.ts
Defines LiveFeedStatus state union ('connecting' | 'streaming' | 'reconnecting' | 'failed'), backoff timing constants (1s base, 30s max), and core functions: wsBackoffDelayMs() computes exponential delays, parseMaxReconnectAttempts() reads configurable max attempts from environment, and nextLiveFeedStatus() implements state machine transitions for WebSocket lifecycle events.
Custom React hook for WebSocket lifecycle
frontend/lib/useLiveEventSocket.ts
Implements useLiveEventSocket hook that creates WebSocket via createEventSocket, dispatches incoming events to onEvent callback, tracks reconnecting state with elapsed seconds, schedules reconnection via exponential backoff when socket closes, increments attempt counter until parseMaxReconnectAttempts() limit, and transitions to failed on exhaustion; handles cleanup via timer cancellation and socket closure on unmount.
Jest test infrastructure and configuration
frontend/jest.config.js, frontend/package.json
Adds jest.config.js using next/jest preset with jsdom environment and __tests__/**/*.test.ts?(x) file glob; updates package.json with Jest, Testing Library, and type definitions; adds test npm script running jest.
WebSocket reconnection utilities test suite
frontend/__tests__/wsReconnect.test.ts
Jest test suite verifying exponential backoff (1s → 2s → ... → 30s cap), status transitions from connectingstreaming/reconnectingstreaming/failed, and parseMaxReconnectAttempts() environment parsing (default 8, override via NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS env var).
Dashboard page integration with new hook and UI
frontend/pages/index.tsx
Replaces direct WebSocket state management with useLiveEventSocket hook; adds liveFeedBadge() helper rendering status-aware badges (streaming, reconnecting with elapsed seconds, failed, default connecting); updates LiveEventFeed component signature to accept reconnectElapsedSec prop; wires hook callbacks to prepend live events (capped at 200) and trigger refreshSummary()/refreshSessions().
Broaden node_modules ignore patterns
.gitignore
Updates ignore rules to use recursive **/node_modules/ pattern and explicitly adds agentwatch-landing/node_modules/ entry.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • sreerevanth/AgentWatch#68: Adds initial wsStatus state handling to drive LiveEventFeed UI; this PR extends that foundation with reconnection logic, backoff scheduling, and elapsed-time tracking.

Suggested labels

enhancement, frontend, level: intermediate

Poem

A rabbit hops through broken wires,
With backoff timers climbing higher—
Reconnecting, seconds tick,
Till the socket's back online, real quick! 🐰✨
When all nine lives run their course,
A graceful fail shows network loss. 🌐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main feature added: WebSocket reconnection with exponential backoff.
Linked Issues check ✅ Passed All acceptance criteria from issue #57 are met: exponential backoff reconnection (1s, 2s, 4s, max 30s), reconnecting badge with elapsed time, preserved event list on reconnect, configurable max attempts, and Jest tests for state transitions.
Out of Scope Changes check ✅ Passed All changes are scoped to WebSocket reconnection implementation: new utility modules (wsReconnect.ts), hook (useLiveEventSocket.ts), tests, Jest configuration, and integration into index.tsx. However, .gitignore updates are minimal configuration changes supporting test infrastructure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

🧪 PR Test Results

Check Result
Tests (pytest tests/) ✅ success
Lint (ruff check .) ❌ failure
Coverage (agentwatch) 71.88%

Python 3.12 · commit 7ab41b4

@harika880
Copy link
Copy Markdown
Author

"The ruff lint failure is unrelated to this PR — it's a pre-existing issue in the Python backend files, not in the frontend WebSocket changes."

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
agentwatch/frontend/hooks/useWebSocket.ts (2)

101-105: 💤 Low value

Consider bounded message retention for long-lived connections.

The messages array grows unboundedly as events arrive. For long-running sessions with high message volume, this could lead to memory pressure. Consider:

  • Adding an optional maxMessages option to cap the array size
  • Letting consumers clear messages via a returned clearMessages callback

This is low priority if sessions are short or consumers handle cleanup externally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/frontend/hooks/useWebSocket.ts` around lines 101 - 105, The
messages state in useWebSocket (ws.onmessage handler using setMessages) grows
unbounded; add an optional maxMessages parameter to the useWebSocket hook and
enforce it in the setMessages updater (e.g., when appending new { raw,
receivedAt } drop oldest entries to keep length <= maxMessages), and expose a
clearMessages callback from the hook that resets messages to an empty array so
consumers can clear history; keep existing onMessageRef behavior unchanged and
make maxMessages optional with sensible default (e.g., undefined meaning
unlimited).

134-145: 💤 Low value

State not reset when URL changes.

When url changes, the effect reconnects to the new endpoint, but messages, status, elapsedSecs, and attemptCount retain their previous values. Messages from the old URL will persist in the array. For this PR's single-endpoint use case this is likely fine, but for broader hook reusability consider resetting state when the URL changes.

Proposed fix to reset state on URL change
   useEffect(() => {
     stoppedRef.current = false;
+    setMessages([]);
+    setStatus('connecting');
+    setElapsedSecs(0);
+    setAttemptCount(0);
+    attemptRef.current = 0;
     connect();

     return () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/frontend/hooks/useWebSocket.ts` around lines 134 - 145, When the
url changes the hook reconnects but doesn't clear prior connection state; update
the effect that runs on [url] (the useEffect that calls connect and manages
stoppedRef/wsRef) to reset the hook state before reconnecting by calling
setMessages([]), setStatus(/* initial status value used in the hook */),
setElapsedSecs(0), and setAttemptCount(0), and also clear any elapsedTimerRef
and retryTimerRef; keep stoppedRef.current assignments and existing
wsRef.current?.close() behavior, then call connect() so the new URL starts with
a clean state.
agentwatch/frontend/hooks/__tests__/useWebSocket.test.ts (1)

47-48: 💤 Low value

Consider type-safe WebSocket mock assignment.

The @ts-ignore directive suppresses type checking when assigning MockWebSocket to global.WebSocket. While common in tests, this bypasses verification that the mock fully implements the WebSocket interface.

♻️ Type-safe alternative
-  // `@ts-ignore`
-  global.WebSocket = MockWebSocket;
+  global.WebSocket = MockWebSocket as unknown as typeof WebSocket;

This maintains the assertion while making the type cast explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/frontend/hooks/__tests__/useWebSocket.test.ts` around lines 47 -
48, Replace the // `@ts-ignore` assignment by doing an explicit, type-safe cast
when assigning MockWebSocket to global.WebSocket: ensure you cast global to a
type that permits overwriting WebSocket (e.g., cast global to unknown/any or to
an interface with WebSocket) and then assign MockWebSocket, referencing the
MockWebSocket symbol and the global.WebSocket target so TypeScript no longer
needs to be suppressed.
agentwatch/frontend/jest.config.json (1)

1-1: 💤 Low value

Remove trailing whitespace.

Line 1 has trailing whitespace after the closing brace.

✨ Proposed fix
-{"preset":"ts-jest","testEnvironment":"jsdom"} 
+{"preset":"ts-jest","testEnvironment":"jsdom"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/frontend/jest.config.json` at line 1, The JSON line
{"preset":"ts-jest","testEnvironment":"jsdom"} in the Jest config contains
trailing whitespace after the closing brace; edit the jest config so the line
ends immediately after the brace (no extra spaces or tabs), ensuring the file
contains {"preset":"ts-jest","testEnvironment":"jsdom"} with no trailing
whitespace.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@agentwatch/frontend/hooks/__tests__/useWebSocket.test.ts`:
- Around line 47-48: Replace the // `@ts-ignore` assignment by doing an explicit,
type-safe cast when assigning MockWebSocket to global.WebSocket: ensure you cast
global to a type that permits overwriting WebSocket (e.g., cast global to
unknown/any or to an interface with WebSocket) and then assign MockWebSocket,
referencing the MockWebSocket symbol and the global.WebSocket target so
TypeScript no longer needs to be suppressed.

In `@agentwatch/frontend/hooks/useWebSocket.ts`:
- Around line 101-105: The messages state in useWebSocket (ws.onmessage handler
using setMessages) grows unbounded; add an optional maxMessages parameter to the
useWebSocket hook and enforce it in the setMessages updater (e.g., when
appending new { raw, receivedAt } drop oldest entries to keep length <=
maxMessages), and expose a clearMessages callback from the hook that resets
messages to an empty array so consumers can clear history; keep existing
onMessageRef behavior unchanged and make maxMessages optional with sensible
default (e.g., undefined meaning unlimited).
- Around line 134-145: When the url changes the hook reconnects but doesn't
clear prior connection state; update the effect that runs on [url] (the
useEffect that calls connect and manages stoppedRef/wsRef) to reset the hook
state before reconnecting by calling setMessages([]), setStatus(/* initial
status value used in the hook */), setElapsedSecs(0), and setAttemptCount(0),
and also clear any elapsedTimerRef and retryTimerRef; keep stoppedRef.current
assignments and existing wsRef.current?.close() behavior, then call connect() so
the new URL starts with a clean state.

In `@agentwatch/frontend/jest.config.json`:
- Line 1: The JSON line {"preset":"ts-jest","testEnvironment":"jsdom"} in the
Jest config contains trailing whitespace after the closing brace; edit the jest
config so the line ends immediately after the brace (no extra spaces or tabs),
ensuring the file contains {"preset":"ts-jest","testEnvironment":"jsdom"} with
no trailing whitespace.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f6e4f03-bbf8-4af2-af22-8cbe8125dc94

📥 Commits

Reviewing files that changed from the base of the PR and between 7ab41b4 and 2569ede.

⛔ Files ignored due to path filters (297)
  • agentwatch/frontend/node_modules/.bin/baseline-browser-mapping is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/baseline-browser-mapping.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/baseline-browser-mapping.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/browserslist is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/browserslist.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/browserslist.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/esparse is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/esparse.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/esparse.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/esvalidate is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/esvalidate.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/esvalidate.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/glob is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/glob.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/glob.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/handlebars is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/handlebars.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/handlebars.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/import-local-fixture is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/import-local-fixture.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/import-local-fixture.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/jest is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/jest.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/jest.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/js-yaml is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/js-yaml.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/js-yaml.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/jsesc is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/jsesc.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/jsesc.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/json5 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/json5.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/json5.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/lz-string is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/lz-string.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/lz-string.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/napi-postinstall is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/napi-postinstall.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/napi-postinstall.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/node-which is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/node-which.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/node-which.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/parser is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/parser.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/parser.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/semver is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/semver.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/semver.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/tldts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/tldts.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/tldts.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/ts-jest is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/ts-jest.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/ts-jest.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/tsc is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/tsc.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/tsc.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/tsserver is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/tsserver.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/tsserver.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/uglifyjs is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/uglifyjs.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/uglifyjs.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/update-browserslist-db is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/update-browserslist-db.cmd is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.bin/update-browserslist-db.ps1 is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/.package-lock.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/LICENSE is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/README.md is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/node_modules/lru-cache/LICENSE is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/node_modules/lru-cache/README.md is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/node_modules/lru-cache/package.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/package.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/index.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/cache.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/color.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/common.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/constant.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/convert.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/css-calc.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/css-gradient.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/css-var.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/relative-color.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/resolve.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/typedef.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@asamuzakjp/css-color/src/js/util.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/code-frame/LICENSE is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/code-frame/README.md is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/code-frame/lib/index.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/code-frame/lib/index.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/code-frame/package.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/LICENSE is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/README.md is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/corejs2-built-ins.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/corejs3-shipped-proposals.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/data/corejs2-built-ins.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/data/corejs3-shipped-proposals.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/data/native-modules.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/data/overlapping-plugins.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/data/plugin-bugfixes.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/data/plugins.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/native-modules.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/overlapping-plugins.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/package.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/plugin-bugfixes.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/compat-data/plugins.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/LICENSE is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/README.md is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/cache-contexts.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/cache-contexts.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/caching.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/caching.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/config-chain.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/config-chain.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/config-descriptors.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/config-descriptors.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/configuration.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/configuration.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/import.cjs is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/import.cjs.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/index-browser.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/index-browser.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/index.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/index.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/module-types.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/module-types.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/package.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/package.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/plugins.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/plugins.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/types.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/types.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/utils.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/files/utils.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/full.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/full.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/helpers/config-api.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/helpers/config-api.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/helpers/deep-array.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/helpers/deep-array.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/helpers/environment.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/helpers/environment.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/index.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/index.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/item.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/item.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/partial.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/partial.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/pattern-to-regex.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/pattern-to-regex.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/plugin.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/plugin.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/printer.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/printer.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/resolve-targets-browser.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/resolve-targets-browser.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/resolve-targets.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/resolve-targets.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/util.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/util.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/validation/option-assertions.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/validation/option-assertions.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/validation/options.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/validation/options.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/validation/plugins.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/validation/plugins.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/config/validation/removed.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/config/validation/removed.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/errors/config-error.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/errors/config-error.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/gensync-utils/async.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/gensync-utils/async.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/gensync-utils/fs.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/gensync-utils/fs.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/gensync-utils/functional.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/gensync-utils/functional.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/index.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/index.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/parse.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/parse.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/parser/index.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/parser/index.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/tools/build-external-helpers.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/tools/build-external-helpers.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transform-ast.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transform-ast.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transform-file-browser.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transform-file-browser.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transform-file.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transform-file.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transform.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transform.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/file/file.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/file/file.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/file/generate.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/file/generate.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/file/merge-map.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/file/merge-map.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/index.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/index.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/normalize-file.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/normalize-file.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/normalize-opts.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/normalize-opts.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/plugin-pass.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/plugin-pass.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/read-input-source-map-file-browser.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/read-input-source-map-file-browser.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/read-input-source-map-file.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/read-input-source-map-file.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/util/clone-deep.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/transformation/util/clone-deep.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/lib/vendor/import-meta-resolve.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/lib/vendor/import-meta-resolve.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/core/package.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/src/config/files/index-browser.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/src/config/files/index.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/src/config/resolve-targets-browser.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/src/config/resolve-targets.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/src/transform-file-browser.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/src/transform-file.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/src/transformation/read-input-source-map-file-browser.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/core/src/transformation/read-input-source-map-file.ts is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/LICENSE is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/README.md is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/buffer.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/buffer.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/base.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/base.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/classes.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/classes.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/deprecated.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/deprecated.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/expressions.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/expressions.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/flow.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/flow.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/index.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/index.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/jsx.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/jsx.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/methods.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/methods.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/modules.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/modules.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/statements.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/statements.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/template-literals.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/template-literals.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/types.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/types.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/typescript.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/generators/typescript.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/index.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/index.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/node/index.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/node/index.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/node/parentheses.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/node/parentheses.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/nodes.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/nodes.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/printer.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/printer.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/source-map.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/source-map.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/lib/token-map.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/generator/lib/token-map.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/generator/package.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/LICENSE is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/README.md is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/debug.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/debug.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/filter-items.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/filter-items.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/index.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/index.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/options.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/options.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/pretty.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/pretty.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/targets.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/targets.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/utils.js is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/lib/utils.js.map is excluded by !**/node_modules/**, !**/*.map
  • agentwatch/frontend/node_modules/@babel/helper-compilation-targets/package.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-globals/LICENSE is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-globals/README.md is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-globals/data/browser-upper.json is excluded by !**/node_modules/**
  • agentwatch/frontend/node_modules/@babel/helper-globals/data/builtin-lower.json is excluded by !**/node_modules/**
📒 Files selected for processing (3)
  • agentwatch/frontend/hooks/__tests__/useWebSocket.test.ts
  • agentwatch/frontend/hooks/useWebSocket.ts
  • agentwatch/frontend/jest.config.json

@sreerevanth
Copy link
Copy Markdown
Owner

@harika880 Thanks for the contribution.

Before I review the implementation, the PR appears to contain over 1.2 million lines across 3000+ files, which is far larger than expected for a WebSocket reconnection feature.

Could you check whether build artifacts, node_modules, .next, coverage files, or other generated files were accidentally committed?

Please clean up the branch so the PR only contains the files related to the WebSocket reconnection implementation. Once that's done I'll review the actual changes.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agentwatch/api/server.py`:
- Around line 51-70: The _Limiter class currently uses an unbounded defaultdict
_buckets in check which will leak memory; replace _buckets with a bounded TTL
cache or add pruning logic: either use cachetools.TTLCache(maxsize=<reasonable>,
ttl=60) (import cachetools and initialize _buckets = TTLCache(...)) or, if you
prefer a manual approach inside _Limiter.check, iterate and delete keys whose
stored "start" is older than the window before accessing the caller IP; keep the
existing semantics of check (update b["count"], b["start"], set
request.state.rl_limit and rl_remaining, and raise the same HTTPException) and
ensure the cache/prune is used wherever _buckets is referenced.
- Line 399: The _limiter.check call is dereferencing request.client.host which
can be None; change both occurrences where _limiter.check(...
request.client.host+':r', RATE_READ, request) is used to safely compute a
client_host string first (e.g. client_host = (request.client.host if
request.client else request.headers.get('x-forwarded-for') or 'unknown')) and
then call _limiter.check(client_host + ':r', RATE_READ, request); apply this
same change for the GET handler and the POST /api/v1/sessions handler (the two
places calling _limiter.check) so you no longer assume request.client is
non-null.
- Around line 439-442: The ingest_event handler accepts Request but never
enforces rate limiting; call the limiter check at the start of ingest_event by
invoking _limiter.check(request, RATE_WRITE) (or the project's equivalent
RATE_WRITE constant) before publishing the event and handle a rate-limit failure
by raising an HTTPException(429) or returning the same error path your other
endpoints use; update the ingest_event function (and import
RATE_WRITE/HTTPException if needed) so the check runs prior to
get_event_bus().publish(event).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 008ede00-9e2c-443d-8953-62362bcb5cbf

📥 Commits

Reviewing files that changed from the base of the PR and between 2569ede and 3730e9d.

📒 Files selected for processing (2)
  • .gitignore
  • agentwatch/api/server.py

Comment thread agentwatch/api/server.py Outdated
Comment on lines +51 to +70
class _Limiter:
def __init__(self):
self._buckets = defaultdict(lambda: {"count": 0, "start": 0.0})
def check(self, ip, limit, request):
now = time.time()
b = self._buckets[ip]
if now - b["start"] > 60:
b["count"] = 0
b["start"] = now
b["count"] += 1
request.state.rl_limit = limit
request.state.rl_remaining = max(0, limit - b["count"])
if b["count"] > limit:
raise HTTPException(
status_code=429,
detail="rate_limit_exceeded",
headers={"X-RateLimit-Limit": str(limit),
"X-RateLimit-Remaining": "0",
"Retry-After": "60"},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Unbounded memory growth in rate limiter.

_buckets is never pruned. Each unique IP+suffix key persists indefinitely. In production with many clients, this will leak memory over time.

Consider adding periodic cleanup of stale entries (e.g., entries older than the window) or using a bounded cache like cachetools.TTLCache.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/api/server.py` around lines 51 - 70, The _Limiter class currently
uses an unbounded defaultdict _buckets in check which will leak memory; replace
_buckets with a bounded TTL cache or add pruning logic: either use
cachetools.TTLCache(maxsize=<reasonable>, ttl=60) (import cachetools and
initialize _buckets = TTLCache(...)) or, if you prefer a manual approach inside
_Limiter.check, iterate and delete keys whose stored "start" is older than the
window before accessing the caller IP; keep the existing semantics of check
(update b["count"], b["start"], set request.state.rl_limit and rl_remaining, and
raise the same HTTPException) and ensure the cache/prune is used wherever
_buckets is referenced.

Comment thread agentwatch/api/server.py Outdated
status: str | None = Query(default=None),
since_hours: int | None = Query(default=None),
) -> SessionListResponse:
_limiter.check(request.client.host+':r', RATE_READ, request)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

request.client can be None, causing AttributeError.

When behind certain reverse proxies or in test environments, request.client may be None. Accessing .host directly will crash the server.

Proposed fix
-    _limiter.check(request.client.host+':r', RATE_READ, request)
+    client_ip = request.client.host if request.client else "unknown"
+    _limiter.check(client_ip + ':r', RATE_READ, request)

Apply the same fix at line 413 for POST /api/v1/sessions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_limiter.check(request.client.host+':r', RATE_READ, request)
client_ip = request.client.host if request.client else "unknown"
_limiter.check(client_ip + ':r', RATE_READ, request)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/api/server.py` at line 399, The _limiter.check call is
dereferencing request.client.host which can be None; change both occurrences
where _limiter.check(... request.client.host+':r', RATE_READ, request) is used
to safely compute a client_host string first (e.g. client_host =
(request.client.host if request.client else
request.headers.get('x-forwarded-for') or 'unknown')) and then call
_limiter.check(client_host + ':r', RATE_READ, request); apply this same change
for the GET handler and the POST /api/v1/sessions handler (the two places
calling _limiter.check) so you no longer assume request.client is non-null.

Comment thread agentwatch/api/server.py
Comment on lines 439 to 442
@app.post("/api/v1/events")
async def ingest_event(event: AgentEvent) -> dict[str, Any]:
async def ingest_event(request: Request, event: AgentEvent) -> dict[str, Any]:
await get_event_bus().publish(event)
return {"status": "accepted", "event_id": event.event_id}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

POST /api/v1/events accepts Request but doesn't enforce rate limiting.

The handler signature was updated to accept Request, but _limiter.check() is not called. This write endpoint seems like a candidate for RATE_WRITE limiting. Was the check intended here?

,

Proposed fix if rate limiting is intended
 `@app.post`("/api/v1/events")
 async def ingest_event(request: Request, event: AgentEvent) -> dict[str, Any]:
+    client_ip = request.client.host if request.client else "unknown"
+    _limiter.check(client_ip + ':w', RATE_WRITE, request)
     await get_event_bus().publish(event)
     return {"status": "accepted", "event_id": event.event_id}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/api/server.py` around lines 439 - 442, The ingest_event handler
accepts Request but never enforces rate limiting; call the limiter check at the
start of ingest_event by invoking _limiter.check(request, RATE_WRITE) (or the
project's equivalent RATE_WRITE constant) before publishing the event and handle
a rate-limit failure by raising an HTTPException(429) or returning the same
error path your other endpoints use; update the ingest_event function (and
import RATE_WRITE/HTTPException if needed) so the check runs prior to
get_event_bus().publish(event).

Copy link
Copy Markdown
Owner

@sreerevanth sreerevanth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. The WebSocket reconnection work looks useful, but this PR currently includes unrelated backend rate-limiting changes and several development/debug scripts.

Before merge, please:

Resolve the merge conflicts
Address the rate-limiter review comments
Remove unrelated debug/fix scripts from the PR
Keep the PR focused on the WebSocket reconnection feature

@harika880 harika880 force-pushed the feat/ws-reconnection-57 branch from 3730e9d to 967641b Compare June 2, 2026 06:41
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
agentwatch/api/server.py (1)

432-443: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

/health still bypasses the new auth boundary.

This endpoint remains public while returning DB connectivity plus trace, event-bus, safety, and cost stats. That exposes the same class of operational detail the new protected /api/v1/system/status endpoint is meant to gate. If /health must stay probe-friendly, keep it to a minimal liveness payload and move the detailed fields behind an authenticated route.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/api/server.py` around lines 432 - 443, The /health endpoint
(function health) currently exposes operational details (_db_session_factory
connectivity, _collector.get_stats(), get_event_bus().stats(),
_safety_engine.stats(), _cost_tracker.stats()) without authentication; reduce
/health to a minimal liveness payload (e.g., "status" and "timestamp" only) and
remove those detailed fields, then surface the detailed diagnostics behind the
authenticated system-status route (e.g., reuse or call the authenticated
/api/v1/system/status handler) or create a new auth-protected endpoint that
returns the DB, traces, event bus, safety, and cost stats.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agentwatch/api/server.py`:
- Around line 174-176: The production flag logic is too strict: _IS_PROD only
matches the exact string "production" so values like "prod" or whitespace-padded
values bypass fail-closed auth; update the environment normalization and check
used by the startup guard and request-time misconfiguration check by computing
_ENV = (os.getenv("AGENTWATCH_ENV") or os.getenv("ENVIRONMENT") or
"development").strip().lower() and then set _IS_PROD using a broader test such
as _ENV.startswith("prod") (or membership in {"production","prod"}) so
trimmed/shortened variants are treated as production for the fail-closed auth
paths referenced by _IS_PROD.
- Around line 402-416: The current CORS setup constructs _cors_origins and
silently defaults to ["*"] when CORS_ALLOWED_ORIGINS is unset; change this to
fail closed by (a) defaulting _cors_origins to an empty list ([]) instead of
["*"] and setting _cors_credentials accordingly, and (b) validate at startup and
raise an explicit error if CORS_ALLOWED_ORIGINS is empty while running in
production (or when a PRODUCTION/ENV flag indicates non-dev), so the
app.add_middleware(CORSMiddleware, allow_origins=_cors_origins,
allow_credentials=_cors_credentials, ...) will never be initialized with a
wildcard in prod; update the logic around _raw_cors_origins, _cors_origins, and
_cors_credentials to implement these checks and raise a RuntimeError (or
similar) when the env is unsafe.

---

Outside diff comments:
In `@agentwatch/api/server.py`:
- Around line 432-443: The /health endpoint (function health) currently exposes
operational details (_db_session_factory connectivity, _collector.get_stats(),
get_event_bus().stats(), _safety_engine.stats(), _cost_tracker.stats()) without
authentication; reduce /health to a minimal liveness payload (e.g., "status" and
"timestamp" only) and remove those detailed fields, then surface the detailed
diagnostics behind the authenticated system-status route (e.g., reuse or call
the authenticated /api/v1/system/status handler) or create a new auth-protected
endpoint that returns the DB, traces, event bus, safety, and cost stats.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 322ff03a-9bf3-41d9-ba9b-780cebb904a6

📥 Commits

Reviewing files that changed from the base of the PR and between 3730e9d and 967641b.

📒 Files selected for processing (1)
  • agentwatch/api/server.py

Comment thread agentwatch/api/server.py
Comment on lines +174 to +176
# Environment detection for fail-closed logic
_ENV = os.getenv("AGENTWATCH_ENV") or os.getenv("ENVIRONMENT") or "development"
_IS_PROD = _ENV.lower() == "production"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Broaden the production check before using it for fail-closed auth.

_IS_PROD only flips for the exact string "production". A deployment using values like prod or whitespace-padded env values will skip both the startup guard and the request-time misconfiguration check, leaving the API effectively open when AGENTWATCH_API_KEY is unset.

Suggested fix
-_ENV = os.getenv("AGENTWATCH_ENV") or os.getenv("ENVIRONMENT") or "development"
-_IS_PROD = _ENV.lower() == "production"
+_ENV = (os.getenv("AGENTWATCH_ENV") or os.getenv("ENVIRONMENT") or "development").strip()
+_IS_PROD = _ENV.lower() in {"prod", "production"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/api/server.py` around lines 174 - 176, The production flag logic
is too strict: _IS_PROD only matches the exact string "production" so values
like "prod" or whitespace-padded values bypass fail-closed auth; update the
environment normalization and check used by the startup guard and request-time
misconfiguration check by computing _ENV = (os.getenv("AGENTWATCH_ENV") or
os.getenv("ENVIRONMENT") or "development").strip().lower() and then set _IS_PROD
using a broader test such as _ENV.startswith("prod") (or membership in
{"production","prod"}) so trimmed/shortened variants are treated as production
for the fail-closed auth paths referenced by _IS_PROD.

Comment thread agentwatch/api/server.py
Comment on lines +402 to 416
_raw_cors_origins = os.getenv("CORS_ALLOWED_ORIGINS", "")
_cors_origins: list[str] = (
[o.strip() for o in _raw_cors_origins.split(",") if o.strip()]
if _raw_cors_origins.strip()
else ["*"]
)
_cors_credentials = _cors_origins != ["*"]

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_origins=_cors_origins,
allow_credentials=_cors_credentials,
allow_methods=["*"],
allow_headers=["*"],
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

FastAPI 0.115 CORSMiddleware wildcard allow_origins custom headers X-Api-Key allow_credentials false browser preflight behavior

💡 Result:

When you configure CORSMiddleware in FastAPI with allow_origins=[""] and allow_credentials=False, the browser's CORS preflight behavior for requests containing custom headers (like X-Api-Key) is governed by the CORS specification and how FastAPI handles the preflight OPTIONS request [1][2]. 1. Preflight Trigger: When a browser detects a cross-origin request that includes a custom header such as X-Api-Key, it is classified as a non-simple request [1][3]. The browser automatically sends a preflight OPTIONS request to the server before the actual request to verify that the server allows the specific method and headers [1][4]. 2. Middleware Response: FastAPI's CORSMiddleware intercepts this OPTIONS request [1][2]. Because allow_origins=[""] is set and allow_credentials=False, the middleware is configured to permit all origins and does not require credentialed support [1][5]. 3. Handling X-Api-Key: - For the preflight to succeed, the middleware must be configured to allow the custom header. By default, CORSMiddleware does not automatically allow every custom header you might send [1]. - You must explicitly add 'x-api-key' (or whatever your header name is) to the allow_headers list in your CORSMiddleware configuration [1][6]. - If you use allow_headers=[""], the wildcard will effectively authorize any custom header, including X-Api-Key, in the preflight response [1][2]. 4. Browser Behavior: - If CORSMiddleware is configured with allow_headers=[""] or explicitly includes 'x-api-key', it will respond to the OPTIONS request with Access-Control-Allow-Headers: x-api-key (or equivalent) [7][1]. - The browser, seeing this confirmation, will proceed to send the actual request [4]. - Because allow_credentials is set to False, the browser will not send cookies or HTTP authentication headers, but it will send the custom X-Api-Key header as it is now explicitly permitted by the preflight response [1][6]. In summary, setting allow_credentials=False allows you to use a wildcard () for allowed_origins [1][2]. To ensure preflight succeeds for requests with custom headers like X-Api-Key, you must ensure that either the header is explicitly listed in allow_headers or that allow_headers=[""] is used [1][6].

Citations:


Fail closed for CORS when CORS_ALLOWED_ORIGINS is unset in production

In agentwatch/api/server.py (lines 402-416), _cors_origins falls back to allow_origins=["*"] whenever CORS_ALLOWED_ORIGINS is empty/unset, with allow_headers=["*"] still permitting preflighted custom headers from browsers. This silently widens cross-origin access and undercuts the intended hardening. Require an explicit allowlist in production (e.g., fail at startup or default to allow_origins=[]).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/api/server.py` around lines 402 - 416, The current CORS setup
constructs _cors_origins and silently defaults to ["*"] when
CORS_ALLOWED_ORIGINS is unset; change this to fail closed by (a) defaulting
_cors_origins to an empty list ([]) instead of ["*"] and setting
_cors_credentials accordingly, and (b) validate at startup and raise an explicit
error if CORS_ALLOWED_ORIGINS is empty while running in production (or when a
PRODUCTION/ENV flag indicates non-dev), so the
app.add_middleware(CORSMiddleware, allow_origins=_cors_origins,
allow_credentials=_cors_credentials, ...) will never be initialized with a
wildcard in prod; update the logic around _raw_cors_origins, _cors_origins, and
_cors_credentials to implement these checks and raise a RuntimeError (or
similar) when the env is unsafe.

@sreerevanth sreerevanth self-requested a review June 2, 2026 07:07
- Reconnect Live Feed with 1s/2s/4s backoff capped at 30s
- Show Reconnecting badge with elapsed time; failed state after max attempts
- Preserve event list across reconnects; configurable NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS
- Add Jest tests for backoff and status transitions
- Ignore node_modules globally in git

Co-authored-by: Cursor <cursoragent@cursor.com>
@harika880 harika880 force-pushed the feat/ws-reconnection-57 branch from 967641b to d49f0e9 Compare June 3, 2026 08:05
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
frontend/__tests__/wsReconnect.test.ts (1)

42-50: ⚡ Quick win

Add coverage for invalid env values fallback.

parseMaxReconnectAttempts() has validation branches for invalid/non-positive values, but this suite currently doesn’t assert them ('0', '-1', 'abc'8).

Suggested test additions
 describe('parseMaxReconnectAttempts', () => {
@@
   it('reads env override', () => {
     process.env.NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS = '3'
     expect(parseMaxReconnectAttempts()).toBe(3)
   })
+
+  it('falls back to 8 for invalid values', () => {
+    process.env.NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS = '0'
+    expect(parseMaxReconnectAttempts()).toBe(8)
+
+    process.env.NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS = '-1'
+    expect(parseMaxReconnectAttempts()).toBe(8)
+
+    process.env.NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS = 'abc'
+    expect(parseMaxReconnectAttempts()).toBe(8)
+  })
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/__tests__/wsReconnect.test.ts` around lines 42 - 50, Add tests in
wsReconnect.test.ts covering invalid NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS
values so parseMaxReconnectAttempts() falls back to the default (8).
Specifically: for values '0', '-1', and 'abc' set
process.env.NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS to each, call
parseMaxReconnectAttempts(), and assert it returns 8; also ensure you cleanup or
delete the env between cases (similar to existing tests) so each case is
isolated. Use the existing test names/patterns around
parseMaxReconnectAttempts() to locate where to add these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/__tests__/wsReconnect.test.ts`:
- Around line 32-33: The test is setting and restoring the wrong environment
variable (NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS) so it doesn't match the runtime
contract (NEXT_PUBLIC_WS_MAX_ATTEMPTS); update all references in the test (the
assignments that save original, set, and restore process.env entries) to use
NEXT_PUBLIC_WS_MAX_ATTEMPTS instead of NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS so
the test reflects the PR objective and will pick up the correct runtime config.

In `@frontend/lib/useLiveEventSocket.ts`:
- Around line 51-87: connect() is clearing timers every attempt which stops
elapsedTimer and because disconnectedAt is only set once it prevents
startElapsedTicker from restarting; modify the onclose/connect flow so the
elapsed ticker is restarted on each reconnect attempt: after clearTimers() in
connect() (or at start of onclose before setting disconnectedAt) ensure
startElapsedTicker() is called whenever status will become 'reconnecting' (use
nextLiveFeedStatus result) or when reconnectTimer is scheduled, and ensure
disconnectedAt is set to Date.now() each time you start the ticker (and reset to
null in onopen as already present) so reconnectElapsedSec continues updating
across multiple attempts; update handlers referring to connect, clearTimers,
onclose, disconnectedAt, startElapsedTicker, and onopen accordingly.

---

Nitpick comments:
In `@frontend/__tests__/wsReconnect.test.ts`:
- Around line 42-50: Add tests in wsReconnect.test.ts covering invalid
NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS values so parseMaxReconnectAttempts()
falls back to the default (8). Specifically: for values '0', '-1', and 'abc' set
process.env.NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS to each, call
parseMaxReconnectAttempts(), and assert it returns 8; also ensure you cleanup or
delete the env between cases (similar to existing tests) so each case is
isolated. Use the existing test names/patterns around
parseMaxReconnectAttempts() to locate where to add these assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b203d906-1973-4ef8-8992-c283b7d23f5b

📥 Commits

Reviewing files that changed from the base of the PR and between 967641b and d49f0e9.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • .gitignore
  • frontend/__tests__/wsReconnect.test.ts
  • frontend/jest.config.js
  • frontend/lib/useLiveEventSocket.ts
  • frontend/lib/wsReconnect.ts
  • frontend/package.json
  • frontend/pages/index.tsx
💤 Files with no reviewable changes (2)
  • frontend/package.json
  • frontend/pages/index.tsx
✅ Files skipped from review due to trivial changes (2)
  • frontend/jest.config.js
  • .gitignore

Comment on lines +32 to +33
const original = process.env.NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Env var contract in tests does not match the PR objective.

These tests lock in NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS, but the objective specifies NEXT_PUBLIC_WS_MAX_ATTEMPTS. If deployment follows the documented objective, runtime config won’t apply.

Also applies to: 43-49

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/__tests__/wsReconnect.test.ts` around lines 32 - 33, The test is
setting and restoring the wrong environment variable
(NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS) so it doesn't match the runtime contract
(NEXT_PUBLIC_WS_MAX_ATTEMPTS); update all references in the test (the
assignments that save original, set, and restore process.env entries) to use
NEXT_PUBLIC_WS_MAX_ATTEMPTS instead of NEXT_PUBLIC_WS_MAX_RECONNECT_ATTEMPTS so
the test reflects the PR objective and will pick up the correct runtime config.

Comment on lines +51 to +87
const connect = () => {
if (cancelled) return
clearTimers()
socket?.close()
socket = createEventSocket((event) => {
onEventRef.current(event)
refreshRef.current()
})

socket.onopen = () => {
attempt = 0
disconnectedAt = null
setReconnectElapsedSec(0)
setStatus('streaming')
}

socket.onclose = () => {
if (cancelled) return
setStatus((prev) => {
const next = nextLiveFeedStatus(prev, 'close')
if (next === 'reconnecting' && disconnectedAt === null) {
disconnectedAt = Date.now()
startElapsedTicker()
}
return next
})

attempt += 1
if (attempt > maxAttempts) {
clearTimers()
setStatus('failed')
return
}

const delay = wsBackoffDelayMs(attempt - 1)
reconnectTimer = setTimeout(connect, delay)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Elapsed-time ticker freezes after the first reconnect attempt.

connect() calls clearTimers() on every attempt, which clears elapsedTimer. The ticker is only (re)started in onclose under the guard disconnectedAt === null (Line 71). After the first close, disconnectedAt becomes non-null and stays so until a successful onopen, so the ticker is never restarted on subsequent reconnect attempts. As a result reconnectElapsedSec stops updating after roughly the first second, breaking the "Reconnecting… (elapsed)" requirement during longer outages.

🐛 Proposed fix — restart the ticker on each reconnect attempt
     const connect = () => {
       if (cancelled) return
       clearTimers()
+      if (disconnectedAt !== null) startElapsedTicker()
       socket?.close()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/lib/useLiveEventSocket.ts` around lines 51 - 87, connect() is
clearing timers every attempt which stops elapsedTimer and because
disconnectedAt is only set once it prevents startElapsedTicker from restarting;
modify the onclose/connect flow so the elapsed ticker is restarted on each
reconnect attempt: after clearTimers() in connect() (or at start of onclose
before setting disconnectedAt) ensure startElapsedTicker() is called whenever
status will become 'reconnecting' (use nextLiveFeedStatus result) or when
reconnectTimer is scheduled, and ensure disconnectedAt is set to Date.now() each
time you start the ticker (and reset to null in onopen as already present) so
reconnectElapsedSec continues updating across multiple attempts; update handlers
referring to connect, clearTimers, onclose, disconnectedAt, startElapsedTicker,
and onopen accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add WebSocket reconnection logic to frontend

2 participants