Skip to content

Harden home connector observability and snapshot handling#269

Merged
kentcdodds merged 7 commits into
mainfrom
cursor/home-connector-sentry-issues-5e68
Apr 27, 2026
Merged

Harden home connector observability and snapshot handling#269
kentcdodds merged 7 commits into
mainfrom
cursor/home-connector-sentry-issues-5e68

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • initialize home-connector Sentry from the real package entrypoint and Docker entrypoint
  • add graceful shutdown/fatal-path flushing for the Node home connector process
  • harden websocket message handling and worker-side home connector snapshot invalidation/reporting
  • add focused node tests for the new Sentry helper behavior and session disconnect handling

Testing

  • npm run test -- --run packages/home-connector/src/sentry.node.test.ts packages/worker/src/home/session.node.test.ts packages/worker/src/mcp/capabilities/meta/meta-get-home-connector-status.node.test.ts
  • npm run typecheck
  • live smoke check against npm run dev:
    • curl http://127.0.0.1:3742/health
    • curl http://127.0.0.1:4040/health
    • curl http://127.0.0.1:3742/home/connectors/default/snapshot (returned a populated live tool snapshot including bond_shade_set_position)

Closes #268

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Sentry now initializes at process startup; added breadcrumb helper and truncated socket payload previews for richer telemetry.
    • New Sentry close helper to support orderly flush/close on shutdown.
    • Centralized session reporting and session-key summarization for safer diagnostics.
  • Bug Fixes

    • Improved graceful shutdown with cleanup and signal-specific exit codes; better capture for uncaught exceptions/unhandled rejections.
    • More robust websocket lifecycle: telemetry for reconnects/acks, hardened message handling, and snapshot endpoint returns null when no active connector.
  • Tests

    • Expanded tests covering Sentry helpers, shutdown, websocket/session behavior, and error paths.

@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds unconditional Sentry bootstrap to Home Connector startup (including Docker), installs coordinated graceful-shutdown and process-level error handlers that flush/close Sentry, enriches websocket transport with breadcrumbs and error capture, and hardens worker session state handling and tests to clear persisted connection state on disconnects or snapshot failures.

Changes

Cohort / File(s) Summary
Entrypoint & Docker
packages/home-connector/Dockerfile, packages/home-connector/index.ts
Ensure src/sentry-init.ts runs at process start (Docker and local), making Sentry initialization unconditional across runs.
Server shutdown & handlers
packages/home-connector/server/index.ts
Install one-time SIGINT/SIGTERM graceful-shutdown handlers and uncaughtException/unhandledRejection handlers that stop the connector worker, close the HTTP server (5s watchdog), flush/close Sentry, and exit with signal- or error-specific codes.
Sentry helpers & tests
packages/home-connector/src/sentry.ts, packages/home-connector/src/sentry.node.test.ts
Add addHomeConnectorSentryBreadcrumb(...) and closeHomeConnectorSentry(timeout) with enablement guards; update tests to mock @sentry/node and assert no-op behavior when disabled and correct calls when enabled.
Websocket transport
packages/home-connector/src/transport/worker-connector.ts
Wrap websocket message handler in try/catch; add socket context & payload-preview helpers; emit breadcrumbs (reconnect, hello, ack, list_changed, etc.); capture exceptions with socket/attempt context and update lastSyncAt/lastError handling.
Worker session & tests
packages/worker/src/home/session.ts, packages/worker/src/home/session.node.test.ts
Centralize session Sentry messages, summarize session keys, clear persisted connectedAt/tools when no active sockets or on snapshot failures, stop pending JSON‑RPC requests on disconnect, and add tests for snapshot/null behavior and close semantics.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Process as Process
    participant Server as Server
    participant HTTP as HTTP_Server
    participant Sentry as Sentry

    Process->>Server: Start (node --import ./src/sentry-init.ts)
    Server->>HTTP: Start listening
    Note over Server: Register SIGINT/SIGTERM and fatal handlers
    Process->>Server: SIGTERM / uncaughtException / unhandledRejection
    Server->>Server: Run shutdown routine
    Server->>Server: Stop connector worker
    Server->>HTTP: Close HTTP server (5s watchdog)
    Server->>Sentry: Flush/Close (timeout)
    Sentry-->>Server: Complete or timeout
    Server->>Process: Exit with code
Loading
sequenceDiagram
    autonumber
    participant Socket as WebSocket
    participant Connector as Connector_Handler
    participant Dispatch as JSON_RPC_Dispatch
    participant Sentry as Sentry

    Socket->>Connector: message (raw data)
    Connector->>Connector: try { parse + handle }
    alt Parse & Dispatch Success
        Connector->>Dispatch: Dispatch JSON-RPC
        Dispatch-->>Connector: Result
        Connector->>Sentry: add breadcrumb (ack/hello/list_changed) with socket context
    else Parse or Handler Failure
        Connector->>Connector: set lastError / update state
        Connector->>Sentry: capture exception with socket context + payload preview
        Connector->>Socket: optional error response / close
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Add home connector MCP bridge and Roku package #31 — Modifies the same Home Connector entrypoints and runtime handlers (index/Dockerfile/server/sentry/transport/session) to add Sentry bootstrap, graceful shutdown, and improved transport/session telemetry.

Poem

🐰 I nudged the start to wake Sentry's light,

Breadcrumbs now hop when sockets fight.
On close I tidy tools and state,
Flush the trail before it's late.
Hooray — the rabbit guards the night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and concisely summarizes the main changes: improving observability and snapshot handling for the home connector through Sentry integration, shutdown handling, and websocket hardening.
Linked Issues check ✅ Passed The PR comprehensively addresses all six coding objectives from issue #268: Sentry initialization in Docker [Dockerfile], graceful shutdown/fatal-path flushing [server/index.ts], websocket message handler wrapping [worker-connector.ts], capability refresh instrumentation [worker-connector.ts, session.ts], worker snapshot hardening [session.ts], and targeted error capture with Sentry helpers [sentry.ts, session.ts].
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing issue #268's recommendations: Sentry initialization, shutdown handlers, websocket instrumentation, snapshot handling, and Sentry helper functions. No unrelated refactoring, dependencies, or feature additions outside the stated objectives.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/home-connector-sentry-issues-5e68

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@kentcdodds kentcdodds marked this pull request as ready for review April 26, 2026 13:37
@github-actions

github-actions Bot commented Apr 26, 2026

Copy link
Copy Markdown

🔎 Preview deployed: https://kody-pr-269.kentcdodds.workers.dev

Worker: kody-pr-269
D1: kody-pr-269-db
KV: kody-pr-269-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (5)
packages/worker/src/home/session.ts (1)

271-317: Use the new captureSessionMessage helper here for consistency.

Both rejection paths (handleHello) duplicate the tag/level pattern that was just centralized in captureSessionMessage. Routing them through the helper keeps tagging consistent and shrinks the surface area.

♻️ Proposed refactor
 		if (ingressSessionKey && ingressSessionKey !== expectedSessionKey) {
-			Sentry.captureMessage(
+			this.captureSessionMessage(
 				'Remote connector session rejected hello (session key mismatch).',
 				{
 					level: 'error',
-					tags: {
-						service: 'worker',
-						worker_component: 'home-connector-session',
-					},
 					extra: {
 						connectorId: canonicalInstanceId,
 						declaredKind,
 						ingressSessionKey,
 						expectedSessionKey,
 					},
 				},
 			)
@@
 		if (!expectedSecret || message.sharedSecret !== expectedSecret) {
-			Sentry.captureMessage(
+			this.captureSessionMessage(
 				'Home connector session rejected websocket hello.',
 				{
 					level: 'error',
-					tags: {
-						service: 'worker',
-						worker_component: 'home-connector-session',
-					},
 					extra: {
 						connectorId: canonicalInstanceId,
 						declaredKind,
 						hasExpectedSecret: Boolean(expectedSecret),
 					},
 				},
 			)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/home/session.ts` around lines 271 - 317, Replace the two
direct Sentry.captureMessage calls inside the handleHello logic with the
centralized helper captureSessionMessage to ensure consistent tagging/leveling;
call captureSessionMessage with the original descriptive message string ('Remote
connector session rejected hello (session key mismatch).' and 'Home connector
session rejected websocket hello.'), and pass the same extra objects
(connectorId: canonicalInstanceId, declaredKind,
ingressSessionKey/expectedSessionKey for the first, and connectorId:
canonicalInstanceId, declaredKind, hasExpectedSecret: Boolean(expectedSecret)
for the second) so all other behavior (ws.send, ws.close, return) remains
unchanged.
packages/home-connector/src/transport/worker-connector.ts (2)

432-462: Cap or omit rawMessage in the Sentry payload.

String(event.data) can be large (full JSON-RPC tool result payloads) and may include device-state that you don't want to retain in Sentry. Consider truncating to a few hundred chars and/or only including a fingerprint.

-				extra: {
-					rawMessage: String(event.data),
-				},
+				extra: {
+					rawMessage: String(event.data).slice(0, 500),
+					rawMessageLength: String(event.data).length,
+				},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/home-connector/src/transport/worker-connector.ts` around lines 432 -
462, The Sentry payload currently includes rawMessage: String(event.data) which
may be very large or contain sensitive device state; in
captureHomeConnectorException (near updateConnectionState and
createSocketEventContext usage) truncate or omit the raw message before sending
to Sentry — for example, replace rawMessage with a capped string (e.g. first
~200–500 chars plus an indicator like "...[truncated]") or remove it and instead
include a short fingerprint/metadata (length, type, and truncated preview) so
the Sentry extra does not retain full JSON-RPC payloads or sensitive device
data.

377-430: Drop redundant type guards inside their switch cases.

Within case 'server.ack': the discriminant is already 'server.ack', so isAckMessage(value) is always true. Same for isJsonRpcEnvelope(value) inside case 'connector.jsonrpc':. They're dead checks that obscure the actual condition (socket?.readyState === WebSocket.OPEN).

♻️ Proposed refactor
-						if (isAckMessage(value) && socket?.readyState === WebSocket.OPEN) {
+						if (socket?.readyState === WebSocket.OPEN) {
 							captureHomeConnectorMessage(
 								'Sending home connector tools changed notification.',
@@
 					case 'connector.jsonrpc': {
 						const message = value.message
-						if (isJsonRpcEnvelope(value) && isJsonRpcResponse(message)) {
+						if (isJsonRpcResponse(message)) {
 							updateConnectionState(input.state, {
@@
 						if (
-							isJsonRpcEnvelope(value) &&
 							isJsonRpcRequest(message) &&
 							socket?.readyState === WebSocket.OPEN
 						) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/home-connector/src/transport/worker-connector.ts` around lines 377 -
430, In the switch handling for 'server.ack' and 'connector.jsonrpc', remove the
redundant discriminant checks (calls to isAckMessage(value) inside case
'server.ack' and isJsonRpcEnvelope(value) inside case 'connector.jsonrpc') and
rely on the switch discriminant; keep the real runtime guards such as
socket?.readyState === WebSocket.OPEN and the JSON-RPC specific checks
(isJsonRpcRequest / isJsonRpcResponse) that determine behavior. Specifically,
edit the blocks around the captureHomeConnectorMessage/socket.send using
stringifyHomeConnectorMessage/createToolsChangedNotification in the 'server.ack'
branch to drop isAckMessage(value) and only check socket state, and in the
'connector.jsonrpc' branch drop isJsonRpcEnvelope(value) while still using
isJsonRpcResponse(message) and isJsonRpcRequest(message) to decide when to call
updateConnectionState and handleJsonRpcRequest and to send responses.
packages/home-connector/server/index.ts (2)

22-69: closeSentry parameter is always true — consider inlining.

Every call site (signal handlers, uncaughtException, unhandledRejection) passes true. The flush branch is unreachable from here; the only flushHomeConnectorSentry call lives in the outer startup catch (Line 131) and bypasses shutdown entirely. Drop the parameter to reduce indirection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/home-connector/server/index.ts` around lines 22 - 69, The shutdown
function always receives closeSentry=true, so remove the closeSentry parameter
and make shutdown always call await closeHomeConnectorSentry(); update the
shutdown implementation (function shutdown(reason: string)) to delete the
conditional branch that calls flushHomeConnectorSentry and instead always await
closeHomeConnectorSentry(), and update all call sites (the process.once handlers
for 'SIGINT'/'SIGTERM', 'uncaughtException', 'unhandledRejection' that currently
call shutdown(..., true)) to call shutdown(reason) and keep existing
.finally(process.exit(...)) logic; leave the separate startup catch that uses
flushHomeConnectorSentry intact so flushing remains explicit there.

47-69: Capture both reason and promise for unhandled rejections.

The unhandledRejection callback receives (reason, promise). Including the promise reference (or at least a marker) in the Sentry context makes it easier to correlate the rejection with the originating call site, especially when the same error type can come from multiple async chains.

-	process.once('unhandledRejection', (reason) => {
+	process.once('unhandledRejection', (reason, promise) => {
 		captureHomeConnectorException(reason, {
 			tags: {
 				area: 'process',
 				process_event: 'unhandledRejection',
 			},
+			extra: {
+				promise: String(promise),
+			},
 		})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/home-connector/server/index.ts` around lines 47 - 69, The
unhandledRejection handler currently only captures the reason; update the
listener signature in the process.once('unhandledRejection', ...) callback to
accept (reason, promise) and include the promise reference in the Sentry capture
call (captureHomeConnectorException) — e.g., add the promise to the event
context as an extra field or tag so it’s logged alongside reason and
process_event; keep the shutdown('unhandledRejection', true) call unchanged but
ensure any typing for the listener reflects the two-argument signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/home-connector/server/index.ts`:
- Around line 22-37: The shutdown function can hang because input.server.close()
waits for all connections to drain; update shutdown (and keep
input.connector.workerConnector.stop usage) to add a watchdog that proactively
terminates idle/hung connections: first attempt graceful close via
input.server.close(), then if it doesn't finish within a short timeout (e.g.,
few seconds of orchestrator grace) call input.server.closeAllConnections() or
iterate and destroy sockets to forcibly terminate connections, then proceed to
flush/close Sentry by calling closeHomeConnectorSentry/flushHomeConnectorSentry
as before; also simplify the always-true closeSentry parameter by removing it
and using a constant or inlining the chosen Sentry-close behavior.

In `@packages/worker/src/home/session.ts`:
- Around line 363-379: The catch block in the refreshToolsSnapshot sequence
rethrows the error (in the try/catch around this.refreshToolsSnapshot), which
causes an unhandled rejection because handleJsonRpcMessage (called from
handleWebSocketMessage and invoked as void in webSocketMessage) doesn't await
errors; instead of rethrowing from the catch, stop propagation by returning
after calling this.clearConnectionState(), this.captureSessionMessage(...), and
await this.persistState(); modify the catch to remove "throw error" and simply
return so the error is logged/persisted but not bubbled to
handleJsonRpcMessage/handleWebSocketMessage.

---

Nitpick comments:
In `@packages/home-connector/server/index.ts`:
- Around line 22-69: The shutdown function always receives closeSentry=true, so
remove the closeSentry parameter and make shutdown always call await
closeHomeConnectorSentry(); update the shutdown implementation (function
shutdown(reason: string)) to delete the conditional branch that calls
flushHomeConnectorSentry and instead always await closeHomeConnectorSentry(),
and update all call sites (the process.once handlers for 'SIGINT'/'SIGTERM',
'uncaughtException', 'unhandledRejection' that currently call shutdown(...,
true)) to call shutdown(reason) and keep existing .finally(process.exit(...))
logic; leave the separate startup catch that uses flushHomeConnectorSentry
intact so flushing remains explicit there.
- Around line 47-69: The unhandledRejection handler currently only captures the
reason; update the listener signature in the process.once('unhandledRejection',
...) callback to accept (reason, promise) and include the promise reference in
the Sentry capture call (captureHomeConnectorException) — e.g., add the promise
to the event context as an extra field or tag so it’s logged alongside reason
and process_event; keep the shutdown('unhandledRejection', true) call unchanged
but ensure any typing for the listener reflects the two-argument signature.

In `@packages/home-connector/src/transport/worker-connector.ts`:
- Around line 432-462: The Sentry payload currently includes rawMessage:
String(event.data) which may be very large or contain sensitive device state; in
captureHomeConnectorException (near updateConnectionState and
createSocketEventContext usage) truncate or omit the raw message before sending
to Sentry — for example, replace rawMessage with a capped string (e.g. first
~200–500 chars plus an indicator like "...[truncated]") or remove it and instead
include a short fingerprint/metadata (length, type, and truncated preview) so
the Sentry extra does not retain full JSON-RPC payloads or sensitive device
data.
- Around line 377-430: In the switch handling for 'server.ack' and
'connector.jsonrpc', remove the redundant discriminant checks (calls to
isAckMessage(value) inside case 'server.ack' and isJsonRpcEnvelope(value) inside
case 'connector.jsonrpc') and rely on the switch discriminant; keep the real
runtime guards such as socket?.readyState === WebSocket.OPEN and the JSON-RPC
specific checks (isJsonRpcRequest / isJsonRpcResponse) that determine behavior.
Specifically, edit the blocks around the captureHomeConnectorMessage/socket.send
using stringifyHomeConnectorMessage/createToolsChangedNotification in the
'server.ack' branch to drop isAckMessage(value) and only check socket state, and
in the 'connector.jsonrpc' branch drop isJsonRpcEnvelope(value) while still
using isJsonRpcResponse(message) and isJsonRpcRequest(message) to decide when to
call updateConnectionState and handleJsonRpcRequest and to send responses.

In `@packages/worker/src/home/session.ts`:
- Around line 271-317: Replace the two direct Sentry.captureMessage calls inside
the handleHello logic with the centralized helper captureSessionMessage to
ensure consistent tagging/leveling; call captureSessionMessage with the original
descriptive message string ('Remote connector session rejected hello (session
key mismatch).' and 'Home connector session rejected websocket hello.'), and
pass the same extra objects (connectorId: canonicalInstanceId, declaredKind,
ingressSessionKey/expectedSessionKey for the first, and connectorId:
canonicalInstanceId, declaredKind, hasExpectedSecret: Boolean(expectedSecret)
for the second) so all other behavior (ws.send, ws.close, return) remains
unchanged.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dbd42e5b-31c0-4358-bb7d-36105b7519e3

📥 Commits

Reviewing files that changed from the base of the PR and between 55c8e05 and 31c3e93.

📒 Files selected for processing (8)
  • packages/home-connector/Dockerfile
  • packages/home-connector/index.ts
  • packages/home-connector/server/index.ts
  • packages/home-connector/src/sentry.node.test.ts
  • packages/home-connector/src/sentry.ts
  • packages/home-connector/src/transport/worker-connector.ts
  • packages/worker/src/home/session.node.test.ts
  • packages/worker/src/home/session.ts

Comment thread packages/home-connector/server/index.ts Outdated
Comment thread packages/home-connector/server/index.ts
Comment thread packages/home-connector/src/transport/worker-connector.ts
Comment thread packages/worker/src/home/session.ts Outdated
Comment thread packages/worker/src/home/session.ts
Comment thread packages/home-connector/src/transport/worker-connector.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (4)
packages/home-connector/src/transport/worker-connector.ts (2)

364-414: Ack telemetry loses the "reconnects-before-recovery" signal.

consecutiveReconnects is reset to 0 on line 366 before being passed into createSocketEventContext on lines 386 and 402. That means websocket.ack and tools.list_changed.sent always report consecutiveReconnects: 0, which removes one of the most useful diagnostic signals (how many failed attempts preceded a successful ack).

♻️ Proposed fix — preserve the pre-reset value for telemetry
 					case 'server.ack':
 						hasReportedSocketIssue = false
+						const reconnectsBeforeAck = consecutiveReconnects
 						consecutiveReconnects = 0
 						updateConnectionState(input.state, {
 							connected: true,
 							lastSyncAt: new Date().toISOString(),
 							lastError: null,
 						})
 						console.info(
 							`Home connector websocket acknowledged connectorId=${value.connectorId}`,
 						)
 						captureHomeConnectorMessage(
 							'Home connector websocket acknowledged.',
 							{
 								level: 'info',
 								tags: {
 									connector_event: 'websocket.ack',
 								},
 								extra: {
 									...createSocketEventContext({
 										config: input.config,
 										connectionAttempt,
-										consecutiveReconnects,
+										consecutiveReconnects: reconnectsBeforeAck,
 									}),
 									acknowledgedConnectorId: value.connectorId,
 								},
 							},
 						)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/home-connector/src/transport/worker-connector.ts` around lines 364 -
414, In the server.ack handler preserve the pre-reset consecutiveReconnects
value for telemetry: store the current consecutiveReconnects into a temp (e.g.
prevConsecutiveReconnects) before setting consecutiveReconnects = 0, then pass
that temp into createSocketEventContext calls used by
captureHomeConnectorMessage (the 'websocket.ack' and 'tools.list_changed.sent'
events) so telemetry reflects how many reconnects preceded recovery; reset
consecutiveReconnects after capturing/logging using the preserved value.

246-262: Consider breadcrumbs (or sampling) for info-level lifecycle events.

Each successful connect now emits ≥4 info-level captureMessage events (websocket.connecting, websocket.open, websocket.hello_sent, websocket.ack) plus tools.list_changed.sent, and every flap adds a websocket.reconnect_scheduled. On a flaky network this becomes an unbounded stream of billable Sentry events with no actionable signal.

Two options to keep observability without the noise:

  • Switch the info-level emissions to Sentry.addBreadcrumb({...}) so they ride along with subsequent error events for free.
  • Or keep captureMessage but gate via Sentry.withScope + a short-window dedupe / sample rate so reconnect storms don't multiply.

Errors/warnings (server.error, websocket.message_error, websocket.close, websocket.error) should remain as full captures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/home-connector/src/transport/worker-connector.ts` around lines 246 -
262, The info-level lifecycle emission using captureHomeConnectorMessage (e.g.,
the 'Scheduling home connector websocket reconnect.' call that builds context
via createSocketEventContext and includes reconnectDelayMs) is generating
high-volume Sentry events; change these non-actionable info emissions to
Sentry.addBreadcrumb(...) instead of capture/exception-level captures (or
alternatively wrap them in Sentry.withScope and apply a short-window
dedupe/sample before calling capture) so reconnect storms don't create billable
events—leave error/warn captures (server.error, websocket.message_error,
websocket.close, websocket.error) as full captureHomeConnectorMessage calls
unchanged.
packages/worker/src/home/session.ts (1)

62-77: LGTM — Sentry tagging consolidation is a clean improvement.

The captureSessionMessage helper standardizes service/worker_component tagging across the four prior inline Sentry.captureMessage sites and makes the call sites considerably more readable. One micro-nit: the extra is conditionally spread, which is fine, but Sentry accepts an empty extra object equivalently — you could drop the conditional with no behavioral change. Optional, ignore if you prefer the explicit form.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/home/session.ts` around lines 62 - 77, In
captureSessionMessage, simplify the Sentry options by always passing an extra
object instead of conditionally spreading it: ensure the Sentry.captureMessage
call within captureSessionMessage (the helper that sets level, tags, and extra)
uses a guaranteed extra field (e.g., extra: input.extra ?? {}) so the
conditional spread can be removed; update the captureSessionMessage
implementation accordingly.
packages/home-connector/server/index.ts (1)

58-68: Fatal-path handlers should flush Sentry synchronously, not run full async shutdown.

Node.js official guidance states that uncaughtException signals an undefined process state—synchronous cleanup only, then exit immediately. The current code runs the full graceful path: closeServerWithWatchdog() waits up to 5 seconds, then closeHomeConnectorSentry() waits up to 2 seconds, for a 7-second window before exit. If the exception originated from corrupted internal state, that delay can cause additional harm rather than clean recovery.

Replace shutdown() with flushHomeConnectorSentry() for both uncaughtException and unhandledRejection handlers:

Suggested adjustment
 	process.once('uncaughtException', (error) => {
 		captureHomeConnectorException(error, {
 			tags: {
 				area: 'process',
 				process_event: 'uncaughtException',
 			},
 		})
-		void shutdown('uncaughtException').finally(() => {
-			process.exit(1)
-		})
+		void flushHomeConnectorSentry().finally(() => process.exit(1))
 	})
 	process.once('unhandledRejection', (reason, promise) => {
 		captureHomeConnectorException(reason, {
 			tags: {
 				area: 'process',
 				process_event: 'unhandledRejection',
 			},
 			extra: {
 				promise: String(promise),
 			},
 		})
-		void shutdown('unhandledRejection').finally(() => {
-			process.exit(1)
-		})
+		void flushHomeConnectorSentry().finally(() => process.exit(1))
 	})

The signal handlers (SIGINT, SIGTERM) are correct as-is—the process state is clean there, so the full graceful shutdown is appropriate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/home-connector/server/index.ts` around lines 58 - 68, The
uncaughtException and unhandledRejection handlers should not call the async
graceful shutdown; instead replace the async shutdown('...') invocation with a
synchronous Sentry flush by calling flushHomeConnectorSentry() and then
immediately exit (process.exit(1)); update the handlers that currently call
shutdown(...) to call flushHomeConnectorSentry() and then process.exit(1) (refer
to the existing process.once('uncaughtException', ...) and
process.once('unhandledRejection', ...) handlers and the shutdown() call to
locate the change), leaving SIGINT/SIGTERM handlers and the graceful shutdown
flow unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/home-connector/server/index.ts`:
- Around line 70-83: The current unhandledRejection handler passes
extra.promise: String(promise) which only yields "[object Promise]"; update the
handler in the process.once('unhandledRejection', ...) block that calls
captureHomeConnectorException and shutdown: remove the extra.promise field
(since reason is already captured) or replace it with actionable data such as
capturing String(reason) when reason is not an Error (e.g., set extra.reason =
typeof reason === 'object' ? undefined : String(reason)); ensure
captureHomeConnectorException(...) still receives useful context and keep
shutdown('unhandledRejection') behavior unchanged.

In `@packages/worker/src/home/session.ts`:
- Around line 355-371: On refreshToolsSnapshot() failure inside the try/catch,
do not call clearConnectionState(); instead remove or clear only the tools
snapshot in state (e.g., reset this.stateSnapshot.persisted.tools or equivalent)
and then call this.captureSessionMessage(...) and await this.persistState() so
the transient RPC error doesn't zero connectedAt; keep clearConnectionState()
reserved for the websocket close handler and leave connectedAt intact so
handleHello() / notifications/tools/list_changed semantics remain correct.

---

Nitpick comments:
In `@packages/home-connector/server/index.ts`:
- Around line 58-68: The uncaughtException and unhandledRejection handlers
should not call the async graceful shutdown; instead replace the async
shutdown('...') invocation with a synchronous Sentry flush by calling
flushHomeConnectorSentry() and then immediately exit (process.exit(1)); update
the handlers that currently call shutdown(...) to call
flushHomeConnectorSentry() and then process.exit(1) (refer to the existing
process.once('uncaughtException', ...) and process.once('unhandledRejection',
...) handlers and the shutdown() call to locate the change), leaving
SIGINT/SIGTERM handlers and the graceful shutdown flow unchanged.

In `@packages/home-connector/src/transport/worker-connector.ts`:
- Around line 364-414: In the server.ack handler preserve the pre-reset
consecutiveReconnects value for telemetry: store the current
consecutiveReconnects into a temp (e.g. prevConsecutiveReconnects) before
setting consecutiveReconnects = 0, then pass that temp into
createSocketEventContext calls used by captureHomeConnectorMessage (the
'websocket.ack' and 'tools.list_changed.sent' events) so telemetry reflects how
many reconnects preceded recovery; reset consecutiveReconnects after
capturing/logging using the preserved value.
- Around line 246-262: The info-level lifecycle emission using
captureHomeConnectorMessage (e.g., the 'Scheduling home connector websocket
reconnect.' call that builds context via createSocketEventContext and includes
reconnectDelayMs) is generating high-volume Sentry events; change these
non-actionable info emissions to Sentry.addBreadcrumb(...) instead of
capture/exception-level captures (or alternatively wrap them in Sentry.withScope
and apply a short-window dedupe/sample before calling capture) so reconnect
storms don't create billable events—leave error/warn captures (server.error,
websocket.message_error, websocket.close, websocket.error) as full
captureHomeConnectorMessage calls unchanged.

In `@packages/worker/src/home/session.ts`:
- Around line 62-77: In captureSessionMessage, simplify the Sentry options by
always passing an extra object instead of conditionally spreading it: ensure the
Sentry.captureMessage call within captureSessionMessage (the helper that sets
level, tags, and extra) uses a guaranteed extra field (e.g., extra: input.extra
?? {}) so the conditional spread can be removed; update the
captureSessionMessage implementation accordingly.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c63c357e-0f43-498e-8491-c9417838bcee

📥 Commits

Reviewing files that changed from the base of the PR and between 31c3e93 and 0f6f79e.

📒 Files selected for processing (3)
  • packages/home-connector/server/index.ts
  • packages/home-connector/src/transport/worker-connector.ts
  • packages/worker/src/home/session.ts

Comment thread packages/home-connector/server/index.ts
Comment thread packages/worker/src/home/session.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (3)
packages/worker/src/home/session.ts (3)

270-282: ⚠️ Potential issue | 🟠 Major

Avoid forwarding raw session keys to Sentry.

ingressSessionKey and expectedSessionKey are auth-bearing tokens used to admit a connector to this DO; landing them verbatim in Sentry extra exposes them to anyone with project access and to any downstream PII pipeline. Even with rotation, anyone reading the issue can replay the key against the worker before rotation completes.

Prefer a non-reversible fingerprint (and/or a boolean for the comparison result):

🛡️ Proposed redaction
 			this.captureSessionMessage(
 				'Remote connector session rejected hello (session key mismatch).',
 				{
 					level: 'error',
 					extra: {
 						connectorId: canonicalInstanceId,
 						declaredKind,
-						ingressSessionKey,
-						expectedSessionKey,
+						ingressSessionKeyPresent: Boolean(ingressSessionKey),
+						sessionKeyMatches: ingressSessionKey === expectedSessionKey,
 					},
 				},
 			)

A short hashed prefix (e.g. first 8 chars of sha256(key)) is also fine if you need to correlate with logs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/home/session.ts` around lines 270 - 282, The code
currently sends raw auth-bearing tokens ingressSessionKey and expectedSessionKey
in the captureSessionMessage extra payload; instead, compute non-reversible
fingerprints (e.g. SHA-256 and use a short prefix like first 8 chars) for both
keys and send those fingerprint fields plus a boolean sessionKeyMatch (false)
rather than the raw values, and update the captureSessionMessage call site that
builds the extra object (referencing ingressSessionKey, expectedSessionKey, and
captureSessionMessage) to include ingressSessionKeyFingerprint,
expectedSessionKeyFingerprint, and sessionKeyMatch: false instead of the
original raw tokens.

124-257: ⚠️ Potential issue | 🟡 Minor

Websocket handler is not wrapped end-to-end yet.

PR objective for #268 explicitly calls for capturing “unexpected async exceptions with connector id/attempt metadata” in the websocket handler. Today, only two paths are protected here:

  • the parseHomeConnectorMessage failure (Lines 226–243), and
  • the refreshToolsSnapshot() failure inside handleJsonRpcMessage (Lines 355–371).

Anything else thrown from handleHello (e.g. persistState() storage failure, ws.send after socket teardown) or handleHeartbeat becomes an unhandled rejection because webSocketMessage (Line 128) dispatches with void this.handleWebSocketMessage(...). Those will not surface to Sentry from this site.

Consider wrapping the dispatcher tail in a single try/catch so all post-parse failures are reported with consistent metadata:

♻️ Proposed fix
-		switch (parsed.type) {
-			case 'connector.hello':
-				await this.handleHello(ws, parsed)
-				return
-			case 'connector.heartbeat':
-				await this.handleHeartbeat()
-				return
-			case 'connector.jsonrpc':
-				await this.handleJsonRpcMessage(parsed.message)
-				return
-		}
+		try {
+			switch (parsed.type) {
+				case 'connector.hello':
+					await this.handleHello(ws, parsed)
+					return
+				case 'connector.heartbeat':
+					await this.handleHeartbeat()
+					return
+				case 'connector.jsonrpc':
+					await this.handleJsonRpcMessage(parsed.message)
+					return
+			}
+		} catch (error) {
+			this.captureSessionMessage(
+				'Home connector session message handler threw.',
+				{
+					level: 'error',
+					extra: {
+						connectorId: this.stateSnapshot.persisted.connectorId,
+						messageType: parsed.type,
+						error: error instanceof Error ? error.message : String(error),
+					},
+				},
+			)
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/home/session.ts` around lines 124 - 257,
handleWebSocketMessage currently only catches parse errors and a specific
refreshToolsSnapshot path, so exceptions thrown by handleHello, handleHeartbeat,
handleJsonRpcMessage, persistState, or ws.send become unhandled; wrap the
dispatch/switch block in handleWebSocketMessage (the code after
parseHomeConnectorMessage) in a single try/catch that catches any error, calls
this.captureSessionMessage with a clear message and extra metadata (include
this.stateSnapshot.persisted.connectorId and any attempt/session identifiers),
include the error message and stack, attempt to send a server.error via ws.send
if the socket is open, then perform cleanup
(clearConnectionState()/persistState() as appropriate) and return to avoid
unhandled rejections; reference the methods handleHello, handleHeartbeat,
handleJsonRpcMessage, captureSessionMessage, persistState, clearConnectionState,
and webSocketMessage when locating the code to change.

167-182: ⚠️ Potential issue | 🟡 Minor

Reconnect path leaves tools empty until the connector sends tools/list_changed.

clearConnectionState() (line 59) empties this.stateSnapshot.tools = [] on every disconnect via webSocketClose. When handleHello reconnects (lines 320–335), it restores connectorId/connectorKind/connectedAt/lastSeenAt but does not call refreshToolsSnapshot(), leaving tools empty. They only repopulate when the connector sends a notifications/tools/list_changed notification (line 353–356).

During the reconnect window, getSnapshot() will return a valid snapshot with tools: []. Downstream, home/index.ts short-circuits on snapshot.tools.length === 0, but home/status.ts only checks if (!snapshot) — so the status surface briefly shows a connected connector with zero tools.

Two options worth considering:

  1. Call void this.refreshToolsSnapshot() (with the same try/catch + Sentry handling) at the end of handleHello after the ack, so a freshly re-handshaked socket is immediately re-populated.
  2. Or persist tools across disconnects and only clear on hello if the connector identity changed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/home/session.ts` around lines 167 - 182, Reconnection
currently clears this.stateSnapshot.tools in clearConnectionState and never
repopulates it in handleHello, so getSnapshot returns tools: [] until the
connector sends a tools/list_changed notification; fix by invoking
refreshToolsSnapshot() at the end of handleHello (after the ack and within the
same try/catch + Sentry handling used elsewhere) so tools are reloaded
immediately upon reconnection; alternatively, modify clearConnectionState to
preserve stateSnapshot.tools across disconnects and only clear them in
handleHello if connector identity changed—use the functions
clearConnectionState, handleHello, refreshToolsSnapshot and the
stateSnapshot.tools field to locate where to apply the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/home-connector/src/transport/worker-connector.ts`:
- Around line 365-418: Wrap the entire case 'server.ack' body in its own block
(change to case 'server.ack': { ... }) so the const
previousConsecutiveReconnects is lexically scoped to that case and cannot be
accessed by sibling cases; update the block to include the existing references
to previousConsecutiveReconnects, consecutiveReconnects, socket,
updateConnectionState and addHomeConnectorSentryBreadcrumb exactly as they are
now, then close the block and keep the final return; this mirrors the case
'connector.jsonrpc' style and will satisfy the Biome noSwitchDeclarations rule.
- Around line 247-263: All six calls to addHomeConnectorSentryBreadcrumb use the
wrong signature (two args) — change each to pass a single object with keys {
message, category, level?, data? }; set message to the string currently passed,
derive category from the connector_event value (e.g.,
'websocket.reconnect_scheduled'), and merge the current tags and extra payload
(including the result of createSocketEventContext(...) and reconnectDelayMs or
other extras) into the data field; keep level if present. Update call sites near
addHomeConnectorSentryBreadcrumb and any uses of createSocketEventContext to
ensure the combined map is passed as data.

---

Outside diff comments:
In `@packages/worker/src/home/session.ts`:
- Around line 270-282: The code currently sends raw auth-bearing tokens
ingressSessionKey and expectedSessionKey in the captureSessionMessage extra
payload; instead, compute non-reversible fingerprints (e.g. SHA-256 and use a
short prefix like first 8 chars) for both keys and send those fingerprint fields
plus a boolean sessionKeyMatch (false) rather than the raw values, and update
the captureSessionMessage call site that builds the extra object (referencing
ingressSessionKey, expectedSessionKey, and captureSessionMessage) to include
ingressSessionKeyFingerprint, expectedSessionKeyFingerprint, and
sessionKeyMatch: false instead of the original raw tokens.
- Around line 124-257: handleWebSocketMessage currently only catches parse
errors and a specific refreshToolsSnapshot path, so exceptions thrown by
handleHello, handleHeartbeat, handleJsonRpcMessage, persistState, or ws.send
become unhandled; wrap the dispatch/switch block in handleWebSocketMessage (the
code after parseHomeConnectorMessage) in a single try/catch that catches any
error, calls this.captureSessionMessage with a clear message and extra metadata
(include this.stateSnapshot.persisted.connectorId and any attempt/session
identifiers), include the error message and stack, attempt to send a
server.error via ws.send if the socket is open, then perform cleanup
(clearConnectionState()/persistState() as appropriate) and return to avoid
unhandled rejections; reference the methods handleHello, handleHeartbeat,
handleJsonRpcMessage, captureSessionMessage, persistState, clearConnectionState,
and webSocketMessage when locating the code to change.
- Around line 167-182: Reconnection currently clears this.stateSnapshot.tools in
clearConnectionState and never repopulates it in handleHello, so getSnapshot
returns tools: [] until the connector sends a tools/list_changed notification;
fix by invoking refreshToolsSnapshot() at the end of handleHello (after the ack
and within the same try/catch + Sentry handling used elsewhere) so tools are
reloaded immediately upon reconnection; alternatively, modify
clearConnectionState to preserve stateSnapshot.tools across disconnects and only
clear them in handleHello if connector identity changed—use the functions
clearConnectionState, handleHello, refreshToolsSnapshot and the
stateSnapshot.tools field to locate where to apply the change.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3680c6df-5eee-4b59-97d7-9941e3906c56

📥 Commits

Reviewing files that changed from the base of the PR and between 0f6f79e and 221bd9c.

📒 Files selected for processing (5)
  • packages/home-connector/server/index.ts
  • packages/home-connector/src/sentry.node.test.ts
  • packages/home-connector/src/sentry.ts
  • packages/home-connector/src/transport/worker-connector.ts
  • packages/worker/src/home/session.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/home-connector/src/sentry.ts

Comment thread packages/home-connector/src/transport/worker-connector.ts Outdated
Comment thread packages/home-connector/src/transport/worker-connector.ts Outdated
cursoragent and others added 4 commits April 27, 2026 05:05
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor cursor Bot force-pushed the cursor/home-connector-sentry-issues-5e68 branch from 221bd9c to eab74f8 Compare April 27, 2026 05:05
Comment thread packages/home-connector/src/transport/worker-connector.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
packages/worker/src/home/session.ts (1)

141-161: ⚠️ Potential issue | 🟡 Minor

Reject in-flight pendingRequests on websocket close.

webSocketClose clears connection state but leaves the pendingRequests Map intact. Any RPC promise outstanding at the moment of disconnect (e.g., a tools/call initiated from /rpc/tools-call at lines 107–119, or a tools/list from refreshToolsSnapshot) will now hang for the full rpcTimeoutMs (15s) before its setTimeout fires — there’s no way for a response to arrive once the socket is gone. The associated HTTP caller blocks for that duration and the timer keeps the DO from sleeping.

Reject pending requests immediately on close so callers fail fast:

🛡️ Suggested fix
 	webSocketClose(
 		_ws: WebSocket,
 		code: number,
 		reason: string,
 		wasClean: boolean,
 	): void {
 		this.stateSnapshot.persisted.lastSeenAt = new Date().toISOString()
 		this.clearConnectionState()
+		for (const [id, pending] of this.pendingRequests) {
+			clearTimeout(pending.timeout)
+			pending.reject(
+				new Error(
+					`Home connector websocket closed before RPC ${id} responded.`,
+				),
+			)
+		}
+		this.pendingRequests.clear()
 		const closeMessage = `Home connector session websocket closed code=${code} wasClean=${wasClean}${reason ? ` reason=${reason}` : ''}`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/home/session.ts` around lines 141 - 161, The
webSocketClose handler currently clears connection state but leaves the
pendingRequests Map unresolved causing RPC callers to hang; update
webSocketClose to iterate over this.pendingRequests (or the Map used for
in-flight RPCs), call each request's reject callback (or reject Promise) with an
Error describing the websocket close (include code and reason), clear the Map,
then continue with clearConnectionState() and persistState() as before so
callers fail fast and resources are released; reference the webSocketClose
method and the pendingRequests Map when making this change.
🧹 Nitpick comments (2)
packages/home-connector/server/index.ts (2)

70-95: promiseConstructor is essentially always 'Promise' — drop it.

The runtime always invokes the unhandledRejection listener with an actual Promise instance, so the new promiseConstructor extra resolves to the literal string 'Promise' for all practical purposes. It’s the same low-signal field the past review flagged, just spelled differently. Either remove it or replace with something actionable.

♻️ Suggested cleanup
 		captureHomeConnectorException(reason, {
 			tags: {
 				area: 'process',
 				process_event: 'unhandledRejection',
 			},
 			extra: {
 				...(typeof reason === 'string' ||
 				typeof reason === 'number' ||
 				typeof reason === 'boolean'
 					? { reason: String(reason) }
 					: {}),
-				promiseConstructor:
-					promise &&
-					typeof promise === 'object' &&
-					'constructor' in promise &&
-					typeof promise.constructor === 'function' &&
-					promise.constructor.name
-						? promise.constructor.name
-						: 'Promise',
 			},
 		})

If you do want something useful for grouping, consider reasonType: typeof reason or reasonName: reason instanceof Error ? reason.name : undefined — both add diagnostic value normalizeError doesn’t already cover.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/home-connector/server/index.ts` around lines 70 - 95, The extra
field promiseConstructor added in the unhandledRejection handler is low-signal
and effectively always "Promise"; update the process.once('unhandledRejection',
...) handler in this file by removing the promiseConstructor entry from the
extra payload passed to captureHomeConnectorException, or replace it with a
useful diagnostic such as reasonType: typeof reason or reasonName: reason
instanceof Error ? reason.name : undefined; keep the rest of the
captureHomeConnectorException call and the flushHomeConnectorSentry().finally(()
=> process.exit(1)) logic unchanged.

58-68: Minor: flushHomeConnectorSentry vs closeHomeConnectorSentry divergence.

The signal path uses closeHomeConnectorSentry() while the fatal uncaughtException / unhandledRejection paths use flushHomeConnectorSentry(). That's defensible — flush is the right primitive when you're about to process.exit(1) immediately and don't need the client to refuse further events — just flagging it so the inconsistency is intentional rather than incidental.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/home-connector/server/index.ts` around lines 58 - 68, The handlers
use two different shutdown primitives (flushHomeConnectorSentry vs
closeHomeConnectorSentry); make the divergence explicit and consistent: keep
using flushHomeConnectorSentry() in fatal handlers
(captureHomeConnectorException in uncaughtException/unhandledRejection) because
we immediately call process.exit(1), and keep using closeHomeConnectorSentry()
in signal handlers where we want the client to stop accepting events; add a
short inline comment near each handler (uncaughtException/unhandledRejection and
the signal handlers) explaining why flush vs close is chosen so the intent is
clear to future readers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/worker/src/home/session.ts`:
- Around line 141-161: The webSocketClose handler currently clears connection
state but leaves the pendingRequests Map unresolved causing RPC callers to hang;
update webSocketClose to iterate over this.pendingRequests (or the Map used for
in-flight RPCs), call each request's reject callback (or reject Promise) with an
Error describing the websocket close (include code and reason), clear the Map,
then continue with clearConnectionState() and persistState() as before so
callers fail fast and resources are released; reference the webSocketClose
method and the pendingRequests Map when making this change.

---

Nitpick comments:
In `@packages/home-connector/server/index.ts`:
- Around line 70-95: The extra field promiseConstructor added in the
unhandledRejection handler is low-signal and effectively always "Promise";
update the process.once('unhandledRejection', ...) handler in this file by
removing the promiseConstructor entry from the extra payload passed to
captureHomeConnectorException, or replace it with a useful diagnostic such as
reasonType: typeof reason or reasonName: reason instanceof Error ? reason.name :
undefined; keep the rest of the captureHomeConnectorException call and the
flushHomeConnectorSentry().finally(() => process.exit(1)) logic unchanged.
- Around line 58-68: The handlers use two different shutdown primitives
(flushHomeConnectorSentry vs closeHomeConnectorSentry); make the divergence
explicit and consistent: keep using flushHomeConnectorSentry() in fatal handlers
(captureHomeConnectorException in uncaughtException/unhandledRejection) because
we immediately call process.exit(1), and keep using closeHomeConnectorSentry()
in signal handlers where we want the client to stop accepting events; add a
short inline comment near each handler (uncaughtException/unhandledRejection and
the signal handlers) explaining why flush vs close is chosen so the intent is
clear to future readers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8df026c1-a7ef-4290-bbb4-ad963684cead

📥 Commits

Reviewing files that changed from the base of the PR and between 221bd9c and b742734.

📒 Files selected for processing (8)
  • packages/home-connector/Dockerfile
  • packages/home-connector/index.ts
  • packages/home-connector/server/index.ts
  • packages/home-connector/src/sentry.node.test.ts
  • packages/home-connector/src/sentry.ts
  • packages/home-connector/src/transport/worker-connector.ts
  • packages/worker/src/home/session.node.test.ts
  • packages/worker/src/home/session.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/home-connector/index.ts
  • packages/home-connector/Dockerfile
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/home-connector/src/sentry.node.test.ts

Comment thread packages/home-connector/index.ts Outdated
Comment thread packages/worker/src/home/session.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/worker/src/home/session.node.test.ts (1)

91-171: Solid coverage of the new socket-presence branching.

Tests 2 and 3 nicely exercise both the "last socket closing" and "stale socket among many" paths through webSocketClose. Two small enhancements you could consider in follow-up tests (no need to block on these):

  • Assert that pending RPC requests are rejected when the last socket closes (e.g., issue an /rpc/tools-list POST first, then close the socket and assert the response is a 502 with the before RPC response reason). This would lock in the new rejectPendingRequests contract at lines 161–163 of session.ts.
  • Assert that test 3 (stale-socket close) still emits the captureSessionMessage warning, since the close-warning capture at lines 167–175 of session.ts is unconditional even when other sockets remain.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/home/session.node.test.ts` around lines 91 - 171, Add
assertions to the two tests to cover the additional behaviors: in the "websocket
close clears..." test, open a pending RPC (e.g., POST /rpc/tools-list) before
closing the last socket and assert that the pending request is rejected with a
502 and reason "before RPC response" to exercise
HomeConnectorSession.rejectPendingRequests via webSocketClose; in the "stale
websocket close preserves..." test, assert that captureMessageMock was called
with the websocket-close warning (the captureSessionMessage call inside
HomeConnectorSession.webSocketClose) even though other sockets remain; locate
these by referring to HomeConnectorSession.webSocketClose and the
rejectPendingRequests/captureSessionMessage behavior in session.ts.
packages/worker/src/home/session.ts (1)

250-270: Symmetry: wrap ws.send in the parse-error path with the same try/catch as the dispatch path.

The dispatch error path at lines 296–305 defensively wraps ws.send in an inner try/catch with the comment "Ignore send failures while we're already handling a websocket error." The parse-error branch here doesn't, so if the socket is already half-closed when an invalid payload arrives, ws.send can throw and surface as an unhandled rejection (since webSocketMessage at line 146 fire-and-forgets via void). Low-impact edge case but trivial to harden for consistency.

♻️ Suggested adjustment
-			ws.send(
-				stringifyHomeConnectorMessage({
-					type: 'server.error',
-					message: error instanceof Error ? error.message : String(error),
-				}),
-			)
+			try {
+				ws.send(
+					stringifyHomeConnectorMessage({
+						type: 'server.error',
+						message: error instanceof Error ? error.message : String(error),
+					}),
+				)
+			} catch {
+				// Ignore send failures while we're already handling a websocket error.
+			}
 			return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/home/session.ts` around lines 250 - 270, The parse-error
branch that calls ws.send after parseHomeConnectorMessage throws should wrap
that ws.send in the same defensive try/catch used in the dispatch path (see the
inner try/catch around ws.send in the dispatch/error handling) to ignore send
failures while already handling a websocket error; update the block right after
captureSessionMessage so the stringifyHomeConnectorMessage(...) send is
performed inside a try { ws.send(...) } catch { /* Ignore send failures while
we're already handling a websocket error. */ } to avoid unhandled rejections
from webSocketMessage fire-and-forget callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/worker/src/home/session.node.test.ts`:
- Around line 91-171: Add assertions to the two tests to cover the additional
behaviors: in the "websocket close clears..." test, open a pending RPC (e.g.,
POST /rpc/tools-list) before closing the last socket and assert that the pending
request is rejected with a 502 and reason "before RPC response" to exercise
HomeConnectorSession.rejectPendingRequests via webSocketClose; in the "stale
websocket close preserves..." test, assert that captureMessageMock was called
with the websocket-close warning (the captureSessionMessage call inside
HomeConnectorSession.webSocketClose) even though other sockets remain; locate
these by referring to HomeConnectorSession.webSocketClose and the
rejectPendingRequests/captureSessionMessage behavior in session.ts.

In `@packages/worker/src/home/session.ts`:
- Around line 250-270: The parse-error branch that calls ws.send after
parseHomeConnectorMessage throws should wrap that ws.send in the same defensive
try/catch used in the dispatch path (see the inner try/catch around ws.send in
the dispatch/error handling) to ignore send failures while already handling a
websocket error; update the block right after captureSessionMessage so the
stringifyHomeConnectorMessage(...) send is performed inside a try { ws.send(...)
} catch { /* Ignore send failures while we're already handling a websocket
error. */ } to avoid unhandled rejections from webSocketMessage fire-and-forget
callers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d78027d-37af-49c7-af28-83fd0c390e56

📥 Commits

Reviewing files that changed from the base of the PR and between b742734 and b17f6c7.

📒 Files selected for processing (3)
  • packages/home-connector/index.ts
  • packages/worker/src/home/session.node.test.ts
  • packages/worker/src/home/session.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/home-connector/index.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b17f6c7. Configure here.

Comment thread packages/home-connector/src/transport/worker-connector.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds kentcdodds merged commit ec9a1d0 into main Apr 27, 2026
9 checks passed
@kentcdodds kentcdodds deleted the cursor/home-connector-sentry-issues-5e68 branch April 27, 2026 15:59
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.

Home connector observability gaps can hide disconnects and tool-snapshot failures

2 participants