Skip to content

feat: interceptor diagnose — debugging snapshot for agent diagnosis#105

Open
trillium wants to merge 3 commits into
Hacker-Valley-Media:mainfrom
trillium:feat/diagnose-command
Open

feat: interceptor diagnose — debugging snapshot for agent diagnosis#105
trillium wants to merge 3 commits into
Hacker-Valley-Media:mainfrom
trillium:feat/diagnose-command

Conversation

@trillium

@trillium trillium commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

feat: interceptor diagnose — debugging snapshot for agent diagnosis

Hey there, I ran into a problem where 2 interceptor processes were running and went down a troubleshooting rabbit hole 🐇🕳️. Thought this could potentially happent o someone else.

What

Adds interceptor diagnose — a single command that surfaces everything needed to understand why Interceptor isn't working, including a new lock file mechanism and binary mismatch detection that catches the specific failure mode that started this rabbit hole.

The incident this solves

The root cause was a manifest/binary split: the NMH manifest pointed to ~/Projects/interceptor/daemon/interceptor-daemon (dev build) but the running daemon on the socket was /opt/homebrew/bin/interceptor-daemon (Homebrew install). Chrome spawned the manifest binary when the extension connected; the CLI talked to the socket daemon. Two different processes, zero visibility into each other — interceptor tabs timed out for 15 seconds with no useful signal about why.

interceptor diagnose now catches this immediately:

daemon:    running  (pid 12345, /opt/homebrew/bin/interceptor-daemon)
⚠ binary mismatch (chrome):
    socket daemon: /opt/homebrew/bin/interceptor-daemon
    NMH manifest:  /Users/trillium/Projects/interceptor/daemon/interceptor-daemon
    Chrome will spawn the manifest binary; CLI talks to the socket binary.
    Fix: run 'interceptor init' or update the NMH manifest to match.
extension: disconnected  (extension not responding)
monitor:   no sessions

Why diagnose instead of status

status is a pre-flight check — it tells you if the daemon is running and the bridge is installed. diagnose is a post-failure tool — it tells you what's actually connected right now and why commands might be failing. They answer different questions.

Full output (healthy single-browser setup)

daemon:    running  (pid 12345, /opt/homebrew/bin/interceptor-daemon)
extension: connected
tab 3:     https://example.com  "Example Domain"
elements:  24 interactive
monitor:   no sessions

Dual-browser setup (Chrome + Brave):

daemon:    running  (pid 12345, /opt/homebrew/bin/interceptor-daemon)
context chrome-ctx-1:
  extension: connected
  tab 3:     https://example.com  "Example Domain"
  elements:  24 interactive
context brave-ctx-2:
  extension: disconnected  (extension not responding)
  tab:       no active interceptor-group tab
monitor:   no sessions

Daemon not running:

daemon:    not running  — open Chrome with the Interceptor extension, then run 'interceptor init'
monitor:   no sessions

--json available for all of the above.

Changes

New: interceptor diagnose (cli/commands/diagnose.ts)

  • Reads local daemon status + lock file (no daemon needed for this part)
  • Compares lock file execPath against every installed NMH manifest — flags mismatches
  • If daemon is running: enumerates all browser contexts via contexts, probes each in parallel (2 s cap per probe)
  • Reads monitor session state from disk
  • --context <id> to scope to a single context; --json for structured output

New: lock file (daemon/lifecycle.ts, shared/platform.ts, daemon/index.ts)

  • Daemon writes /tmp/interceptor.lock on startup: { pid, version, execPath, startedAt, socketPath, wsPort, mode }
  • Cleared on SIGINT/SIGTERM/SIGHUP
  • checkLockFileDuplicate() exits early with a clear error when a second live daemon is detected at startup — prevents the silent fork
  • LOCK_PATH exported from shared/platform.ts, overridable via $INTERCEPTOR_LOCK_PATH

Updated: cli/index.ts, cli/help.ts

  • diagnose wired into routing, added to NO_DAEMON set (never auto-spawns)
  • Help entries for diagnose, diagnose --context <id>, diagnose --json

Notes for reviewer

  • Lock file execPath uses process.execPath — the actual binary path, not argv[0], so symlinks resolve correctly
  • Binary mismatch check is macOS-only paths currently (Chrome + Brave NMH dirs); Windows path can follow
  • Pre-existing TS errors in extension/src/inject-net.ts and test/screenshot-minimized-preflight.test.ts are not introduced by this PR

Add 'interceptor diagnose' — a new command that collapses the 4-5
follow-up calls an agent normally issues after a failure into one.

Output:
  daemon:    running  (pid 12345)
  extension: connected
  tab 3:     https://example.com  "Example Domain"
  elements:  24 interactive
  monitor:   no sessions

Works without a daemon (reports local state), surfaces richer context
when daemon + extension are reachable. Probes are capped at 2 s each
and run in parallel so the command stays fast even on a heavy page.

--json emits a structured DiagnoseSnapshot for programmatic use.
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a diagnose CLI command that gathers daemon status, probes extension reachability and per-context tab/a11y data with timeouts, loads persisted monitor session counts, and prints either structured JSON or human-readable diagnostics. Also introduces a JSON lock file with duplicate-instance detection and lifecycle wiring, and exposes lockPath in platform config.

Changes

Diagnose CLI Command

Layer / File(s) Summary
Snapshot data model and timeout helper
cli/commands/diagnose.ts
DiagnoseSnapshot/ContextProbe and NMH path mapping added; probeWithTimeout wraps probes with a 2s timeout and null fallback.
Binary-mismatch detection
cli/commands/diagnose.ts
Parses browser NMH manifest JSON to extract native-messaging binary paths and compares them to the daemon lock-file execPath to produce BinaryMismatch entries.
Context probing implementation
cli/commands/diagnose.ts
probeContext concurrently calls tab_list and get_a11y_tree (via probeWithTimeout), determines extension reachability and failure reasons, selects active/first tab, and counts interactive element IDs from the accessibility tree.
Diagnose command implementation & output
cli/commands/diagnose.ts
runDiagnoseCommand(jsonMode, contextId?) reads readStatusSnapshot(), optionally reads lock data, enumerates contexts (or uses provided contextId) when daemon is running, probes contexts, loads monitor session counts via listSessions() with error fallback, and emits JSON or formatted text.
CLI routing and help integration
cli/index.ts, cli/help.ts
Adds help entries, imports/wires runDiagnoseCommand, defines DIAGNOSE_CMDS, adds to NO_DAEMON and ALL_KNOWN_CMDS, and dispatches diagnose in main().

Daemon lock-file and lifecycle

Layer / File(s) Summary
Platform lockPath
shared/platform.ts
Adds lockPath to PlatformConfig, reads INTERCEPTOR_LOCK_PATH, and exports LOCK_PATH.
Lock-file handlers & duplicate check
daemon/lifecycle.ts
Adds LockFileData, readLockFile/writeLockFile/clearLockFile, expands LifecycleDeps with lockPath, clears lock file during cleanup, and implements checkLockFileDuplicate that returns error-duplicate when another live pid owns the lock.
Daemon startup wiring
daemon/index.ts
Propagates LOCK_PATH into lifecycle deps, performs early duplicate-instance check before bootstrap decision, writes runtime lock file after boot with PID/execPath/VERSION/etc., and registers signal handlers to clear the lock file on exit.
Tests: lifecycle deps
test/daemon-lifecycle.test.ts
Adds lockPath to mocked LifecycleDeps in test helper makeDeps.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Hacker-Valley-Media/Interceptor#61: Both PRs implement CLI status-style snapshots that probe the interceptor extension via tab_list reachability checks and integrate those probes into status/JSON rendering.

Poem

🐰 I hopped through locks and probed some tabs,
I counted trees and fixed up drab labs.
JSON neat or text that sings,
A rabbit's snapshot — small bright things.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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
Title check ✅ Passed The title accurately describes the main change: adding a new interceptor diagnose command that provides a debugging snapshot for agent diagnosis, which aligns with the core functionality introduced across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

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

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.

@trillium trillium marked this pull request as ready for review June 12, 2026 17:38

@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: 1

🤖 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 `@cli/commands/diagnose.ts`:
- Around line 24-29: The probeWithTimeout function schedules a timeout that is
never cleared, leaving the timer running and causing artificial tail latency;
modify probeWithTimeout to store the timeout id from setTimeout and clear it
(clearTimeout) as soon as either fn() resolves or rejects so the timer doesn't
keep the process alive—update the Promise.race wrapper around fn() and the
timeout to ensure the timeout is cleared when fn wins (and also clear the
timeout if the timeout path runs), adjusting types for the timeout id
(NodeJS.Timeout vs number) if needed; locate and change the probeWithTimeout
function to implement this cleanup.
🪄 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

Run ID: b446d9ff-ec71-4480-a557-9034f81b8f46

📥 Commits

Reviewing files that changed from the base of the PR and between 86e7eb6 and 6b9afd9.

📒 Files selected for processing (3)
  • cli/commands/diagnose.ts
  • cli/help.ts
  • cli/index.ts

Comment thread cli/commands/diagnose.ts
trillium added 2 commits June 12, 2026 10:48
Two fixes:

1. probeWithTimeout: clear the timer in finally{} so it doesn't keep
   the process alive after fn() resolves. CodeRabbit catch on PR Hacker-Valley-Media#105.

2. Context-awareness (brain-svjg): without --context, enumerate ALL
   connected browser contexts via the 'contexts' daemon action and
   probe each one in parallel. In a dual-browser setup (Chrome + Brave)
   both contexts appear side-by-side — the silent mismatch that made
   the previous version misleading is now visible.

   With --context <id>: probe only that context (for targeted use).

   Single 'default' context: output format unchanged (flat, no header).
   Multiple or named contexts: indented under 'context <id>:' blocks.

   --json output promotes tab/extension/elements into a 'contexts'
   array so callers can diff contexts programmatically.
Lock file (daemon/lifecycle.ts, shared/platform.ts, daemon/index.ts):
- New LockFileData type: pid, version, execPath, startedAt, socketPath, wsPort, mode
- writeLockFile() / readLockFile() / clearLockFile() helpers
- checkLockFileDuplicate() exits early when a live duplicate daemon is found
- Daemon writes lock on startup, clears on SIGINT/SIGTERM/SIGHUP
- LOCK_PATH exported from shared/platform.ts ($INTERCEPTOR_LOCK_PATH override)

Binary mismatch detection (cli/commands/diagnose.ts):
- Reads lock file to get the execPath of the daemon that owns the socket
- Reads each browser's NMH manifest to get the path Chrome/Brave will spawn
- When they differ, surfaces a prominent warning immediately after the daemon line

This is the exact signal that was missing in the incident where 'interceptor
tabs' timed out despite Chrome being open: the socket daemon was the Homebrew
binary but the NMH manifest pointed to the dev binary, so the extension and
CLI were talking to different processes.

Output when mismatch detected:
  daemon:    running  (pid 12345, /opt/homebrew/bin/interceptor-daemon)
  ⚠ binary mismatch (chrome):
      socket daemon: /opt/homebrew/bin/interceptor-daemon
      NMH manifest:  /Users/trillium/Projects/interceptor/daemon/interceptor-daemon
      Chrome will spawn the manifest binary; CLI talks to the socket binary.
      Fix: run 'interceptor init' or update the NMH manifest to match.

@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: 3

🧹 Nitpick comments (3)
daemon/lifecycle.ts (1)

30-36: 💤 Low value

Consider adding minimal schema validation to readLockFile.

The function parses JSON and type-asserts it as LockFileData without validating that required fields exist. If the lock file is corrupted or contains valid JSON with a different shape, consumers (like cli/commands/diagnose.ts accessing lock.execPath, lock.startedAt) could receive undefined for expected fields.

Given the lock file is written by this same codebase and read shortly after, the risk is low. However, a minimal check could improve robustness.

♻️ Optional: Add minimal field validation
 export function readLockFile(lockPath: string): LockFileData | null {
   try {
-    return JSON.parse(readFileSync(lockPath, "utf-8")) as LockFileData
+    const data = JSON.parse(readFileSync(lockPath, "utf-8"))
+    if (typeof data?.pid !== "number" || typeof data?.execPath !== "string") {
+      return null
+    }
+    return data as LockFileData
   } catch {
     return null
   }
 }
🤖 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 `@daemon/lifecycle.ts` around lines 30 - 36, readLockFile currently JSON-parses
and type-asserts to LockFileData without verifying required fields, which can
yield undefined for consumers (e.g., cli/commands/diagnose.ts reading
lock.execPath or lock.startedAt); modify readLockFile to perform minimal schema
validation after parsing: ensure the resulting object is non-null, has the
expected keys (at least execPath and startedAt) and proper primitive types
(string/number), and return null if validation fails; reference the readLockFile
function and LockFileData type and update callers to handle a null result safely
if not already.
cli/commands/diagnose.ts (2)

73-80: 💤 Low value

Consider runtime validation of manifest structure.

The type assertion as { path?: string } doesn't verify the actual JSON shape at runtime. If the manifest has unexpected structure (e.g., path is not a string), the function silently returns null rather than reporting the issue.

For a diagnostic tool where surfacing configuration problems is valuable, consider validating the parsed JSON:

♻️ Optional: Add runtime validation
 function readNmhManifestPath(manifestFile: string): string | null {
   try {
-    const manifest = JSON.parse(readFileSync(manifestFile, "utf-8")) as { path?: string }
-    return manifest.path ?? null
+    const manifest = JSON.parse(readFileSync(manifestFile, "utf-8"))
+    if (typeof manifest === "object" && manifest !== null && typeof manifest.path === "string") {
+      return manifest.path
+    }
+    return null
   } catch {
     return null
   }
 }
🤖 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 `@cli/commands/diagnose.ts` around lines 73 - 80, The function
readNmhManifestPath currently asserts the JSON shape but doesn't validate it;
update it to perform runtime validation after parsing: parse manifest into an
unknown, assert it's an object, check that manifest.path exists and is of type
string (e.g. typeof manifest.path === "string"), and if not, surface a clear
diagnostic (throw or return an Error/diagnostic message) rather than silently
returning null; update error handling in readNmhManifestPath to differentiate
parse/file errors from invalid manifest shape and include the offending
value/type in the message so callers can report configuration problems.

191-199: 💤 Low value

Consider clarifying which binary is authoritative in the mismatch message.

The fix suggestion "run 'interceptor init' or update the NMH manifest to match" doesn't indicate which binary should be considered correct. Users may not know whether to align the manifest to the running daemon or vice versa.

Consider making the guidance more specific, e.g., "interceptor init will update the manifest to use the CLI's current binary" or explicitly state which path is expected.

🤖 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 `@cli/commands/diagnose.ts` around lines 191 - 199, The binary mismatch
messaging (in the loop over snap.binaryMismatches) should explicitly state which
binary is authoritative and what 'interceptor init' does: update the NMH
manifest to match the CLI/daemon binary. Update the lines.push for the Fix
and/or surrounding text to mention that the socket daemon path (m.runningPath)
is the expected/authoritative binary, show both paths (m.runningPath and
m.manifestPath) as you're already doing, and change the Fix line to say
something like "'interceptor init' will update the NMH manifest to use the CLI's
current/socket binary (m.runningPath) or run the opposite if you intentionally
want the manifest binary to be authoritative." Ensure you edit the messages
constructed in the for loop over snap.binaryMismatches so users know which path
to align.
🤖 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 `@cli/commands/diagnose.ts`:
- Around line 30-33: NMH_PATHS currently hardcodes macOS paths causing
detectBinaryMismatches to silently fail on Linux/Windows; update the code to
detect platform (process.platform) and build NMH paths using os.homedir() for
POSIX and process.env.APPDATA or process.env.USERPROFILE for Windows, providing
correct per-platform paths (macOS: Library/Application Support/..., Linux:
~/.config/.../NativeMessagingHosts, Windows:
%APPDATA%\...\NativeMessagingHosts), then ensure detectBinaryMismatches reads
from these platform-specific NMH_PATHS and gracefully handles missing env vars
by falling back to os.homedir() or throwing a clear error/log via the same
function names (NMH_PATHS, detectBinaryMismatches).

In `@daemon/index.ts`:
- Around line 514-516: Remove the early signal handlers that call
clearLockFile(LOCK_PATH) followed by process.exit(0) and instead invoke
clearLockFile(LOCK_PATH) from inside gracefulShutdown(); locate and update the
gracefulShutdown function to perform lock cleanup (call
clearLockFile(LOCK_PATH)) as part of its cleanup sequence and let
gracefulShutdown control process.exit timing, and ensure SIGINT/SIGTERM (and
optionally SIGHUP) are hooked to call gracefulShutdown() so pending requests,
servers, PID/socket cleanup and the lock file all run in the same shutdown flow.

In `@daemon/lifecycle.ts`:
- Around line 113-120: clearDaemonRuntimeFiles now calls
deps.unlinkSync(deps.lockPath) in addition to socketPath and pidPath, so the
test expecting deps.unlinked toEqual [deps.socketPath, deps.pidPath] will fail;
update the test assertion that checks deps.unlinked to include deps.lockPath
(i.e. expect deps.unlinked toEqual [deps.socketPath, deps.pidPath,
deps.lockPath]) so it matches the behavior of clearDaemonRuntimeFiles (which
references unlinkSync and lockPath).

---

Nitpick comments:
In `@cli/commands/diagnose.ts`:
- Around line 73-80: The function readNmhManifestPath currently asserts the JSON
shape but doesn't validate it; update it to perform runtime validation after
parsing: parse manifest into an unknown, assert it's an object, check that
manifest.path exists and is of type string (e.g. typeof manifest.path ===
"string"), and if not, surface a clear diagnostic (throw or return an
Error/diagnostic message) rather than silently returning null; update error
handling in readNmhManifestPath to differentiate parse/file errors from invalid
manifest shape and include the offending value/type in the message so callers
can report configuration problems.
- Around line 191-199: The binary mismatch messaging (in the loop over
snap.binaryMismatches) should explicitly state which binary is authoritative and
what 'interceptor init' does: update the NMH manifest to match the CLI/daemon
binary. Update the lines.push for the Fix and/or surrounding text to mention
that the socket daemon path (m.runningPath) is the expected/authoritative
binary, show both paths (m.runningPath and m.manifestPath) as you're already
doing, and change the Fix line to say something like "'interceptor init' will
update the NMH manifest to use the CLI's current/socket binary (m.runningPath)
or run the opposite if you intentionally want the manifest binary to be
authoritative." Ensure you edit the messages constructed in the for loop over
snap.binaryMismatches so users know which path to align.

In `@daemon/lifecycle.ts`:
- Around line 30-36: readLockFile currently JSON-parses and type-asserts to
LockFileData without verifying required fields, which can yield undefined for
consumers (e.g., cli/commands/diagnose.ts reading lock.execPath or
lock.startedAt); modify readLockFile to perform minimal schema validation after
parsing: ensure the resulting object is non-null, has the expected keys (at
least execPath and startedAt) and proper primitive types (string/number), and
return null if validation fails; reference the readLockFile function and
LockFileData type and update callers to handle a null result safely if not
already.
🪄 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

Run ID: 1c14bac4-58cf-4b11-aad4-517a6823afc3

📥 Commits

Reviewing files that changed from the base of the PR and between 383d9a7 and e65c300.

📒 Files selected for processing (5)
  • cli/commands/diagnose.ts
  • daemon/index.ts
  • daemon/lifecycle.ts
  • shared/platform.ts
  • test/daemon-lifecycle.test.ts

Comment thread cli/commands/diagnose.ts
Comment on lines +30 to +33
const NMH_PATHS: Record<string, string> = {
chrome: `${process.env.HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.interceptor.host.json`,
brave: `${process.env.HOME}/Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts/com.interceptor.host.json`,
}

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 | 🏗️ Heavy lift

Binary mismatch detection is macOS-only.

NMH_PATHS hardcodes macOS paths using process.env.HOME, which breaks on Linux and Windows:

  • HOME is undefined on Windows (use USERPROFILE or os.homedir())
  • Linux stores NMH manifests in ~/.config/google-chrome/NativeMessagingHosts/ and ~/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts/
  • Windows uses %APPDATA%\Google\Chrome\NativeMessagingHosts\ and %APPDATA%\BraveSoftware\Brave-Browser\NativeMessagingHosts\

detectBinaryMismatches will silently return an empty array on non-macOS platforms, hiding critical mismatches.

🔧 Proposed fix to add platform detection
+import { homedir, platform } from "node:os"
+import { join } from "node:path"
+
-const NMH_PATHS: Record<string, string> = {
-  chrome: `${process.env.HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.interceptor.host.json`,
-  brave:  `${process.env.HOME}/Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts/com.interceptor.host.json`,
+function getNmhPaths(): Record<string, string> {
+  const home = homedir()
+  const plat = platform()
+  
+  if (plat === "darwin") {
+    return {
+      chrome: join(home, "Library/Application Support/Google/Chrome/NativeMessagingHosts/com.interceptor.host.json"),
+      brave: join(home, "Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts/com.interceptor.host.json"),
+    }
+  } else if (plat === "linux") {
+    return {
+      chrome: join(home, ".config/google-chrome/NativeMessagingHosts/com.interceptor.host.json"),
+      brave: join(home, ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts/com.interceptor.host.json"),
+    }
+  } else if (plat === "win32") {
+    const appData = process.env.APPDATA || join(home, "AppData/Roaming")
+    return {
+      chrome: join(appData, "Google/Chrome/NativeMessagingHosts/com.interceptor.host.json"),
+      brave: join(appData, "BraveSoftware/Brave-Browser/NativeMessagingHosts/com.interceptor.host.json"),
+    }
+  }
+  return {}
 }
+
+const NMH_PATHS = getNmhPaths()

Then update line 85:

 function detectBinaryMismatches(lock: LockFileData | null): BinaryMismatch[] {
   if (!lock?.execPath) return []
   const mismatches: BinaryMismatch[] = []
-  for (const [browser, manifestFile] of Object.entries(NMH_PATHS)) {
+  const paths = getNmhPaths()
+  for (const [browser, manifestFile] of Object.entries(paths)) {
     if (!existsSync(manifestFile)) continue
🤖 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 `@cli/commands/diagnose.ts` around lines 30 - 33, NMH_PATHS currently hardcodes
macOS paths causing detectBinaryMismatches to silently fail on Linux/Windows;
update the code to detect platform (process.platform) and build NMH paths using
os.homedir() for POSIX and process.env.APPDATA or process.env.USERPROFILE for
Windows, providing correct per-platform paths (macOS: Library/Application
Support/..., Linux: ~/.config/.../NativeMessagingHosts, Windows:
%APPDATA%\...\NativeMessagingHosts), then ensure detectBinaryMismatches reads
from these platform-specific NMH_PATHS and gracefully handles missing env vars
by falling back to os.homedir() or throwing a clear error/log via the same
function names (NMH_PATHS, detectBinaryMismatches).

Comment thread daemon/index.ts
Comment on lines +514 to +516
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
process.on(sig, () => { clearLockFile(LOCK_PATH); process.exit(0) })
}

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

Signal handlers conflict with existing graceful shutdown logic.

These handlers call process.exit(0) immediately after clearing the lock file. However, lines 1192-1193 register gracefulShutdown() handlers for the same signals (SIGTERM, SIGINT). Since the handlers here are registered first and call process.exit(0), the graceful shutdown logic never runs:

  • Pending requests won't be drained (line 1170-1175)
  • Socket/WS servers won't be stopped (lines 1176-1180)
  • PID file and socket won't be cleaned up by gracefulShutdown (lines 1181-1182)

Integrate lock file cleanup into gracefulShutdown() instead.

🐛 Move lock file cleanup into gracefulShutdown

Remove the separate handlers:

-for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
-  process.on(sig, () => { clearLockFile(LOCK_PATH); process.exit(0) })
-}

And add lock file cleanup to gracefulShutdown:

 function gracefulShutdown(signal: string) {
   log(`${signal} received, draining ${pendingRequests.size} pending requests`)
   for (const [id, req] of pendingRequests) {
     clearTimeout(req.timer)
     socketWriteFramed(req.socket, JSON.stringify({ id, result: { success: false, error: "daemon shutting down" } }))
   }
   pendingRequests.clear()
   if (socketServer) {
     socketServer.stop(true)
     socketServer = null
   }
   if (wsServer) wsServer.stop(true)
   try { unlinkSync(SOCKET_PATH) } catch {}
   try { unlinkSync(PID_PATH) } catch {}
+  clearLockFile(LOCK_PATH)
   log("shutdown complete")
   process.exit(0)
 }

For SIGHUP, add a handler to gracefulShutdown calls if desired:

 process.on("SIGTERM", () => gracefulShutdown("SIGTERM"))
 process.on("SIGINT", () => gracefulShutdown("SIGINT"))
+process.on("SIGHUP", () => gracefulShutdown("SIGHUP"))
🤖 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 `@daemon/index.ts` around lines 514 - 516, Remove the early signal handlers
that call clearLockFile(LOCK_PATH) followed by process.exit(0) and instead
invoke clearLockFile(LOCK_PATH) from inside gracefulShutdown(); locate and
update the gracefulShutdown function to perform lock cleanup (call
clearLockFile(LOCK_PATH)) as part of its cleanup sequence and let
gracefulShutdown control process.exit timing, and ensure SIGINT/SIGTERM (and
optionally SIGHUP) are hooked to call gracefulShutdown() so pending requests,
servers, PID/socket cleanup and the lock file all run in the same shutdown flow.

Comment thread daemon/lifecycle.ts
Comment on lines +113 to +120
export function clearDaemonRuntimeFiles(deps: Pick<LifecycleDeps, "unlinkSync" | "pidPath" | "lockPath" | "socketPath" | "isWin" | "log">, reason: string): void {
deps.log(`clearing daemon runtime files: ${reason}`)
if (!deps.isWin) {
try { deps.unlinkSync(deps.socketPath) } catch {}
}
try { deps.unlinkSync(deps.pidPath) } catch {}
try { deps.unlinkSync(deps.lockPath) } catch {}
}

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

Test will fail: clearDaemonRuntimeFiles now unlinks three files but test expects two.

The test at test/daemon-lifecycle.test.ts:72 asserts:

expect(deps.unlinked).toEqual([deps.socketPath, deps.pidPath])

With the addition of unlinkSync(deps.lockPath) at line 119, the actual result will be [socketPath, pidPath, lockPath], causing the test to fail.

🐛 Update the test expectation

In test/daemon-lifecycle.test.ts, update line 72:

-    expect(deps.unlinked).toEqual([deps.socketPath, deps.pidPath])
+    expect(deps.unlinked).toEqual([deps.socketPath, deps.pidPath, deps.lockPath])
🤖 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 `@daemon/lifecycle.ts` around lines 113 - 120, clearDaemonRuntimeFiles now
calls deps.unlinkSync(deps.lockPath) in addition to socketPath and pidPath, so
the test expecting deps.unlinked toEqual [deps.socketPath, deps.pidPath] will
fail; update the test assertion that checks deps.unlinked to include
deps.lockPath (i.e. expect deps.unlinked toEqual [deps.socketPath, deps.pidPath,
deps.lockPath]) so it matches the behavior of clearDaemonRuntimeFiles (which
references unlinkSync and lockPath).

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.

1 participant