Repository navigation
Self-heal the transcript watcher from the permission-request hook - #210
Conversation
…t hook The PermissionRequest hook previously only recorded the permission event (or long-polled for the decision) and never checked whether the watcher was alive. Since permission requests fire frequently mid-session, add a best-effort, idempotent watcher self-heal there — the most frequent mid-session recovery point, complementing the existing notification/stop re-ensure. This recovers sessions stuck "active" when an abruptly-killed agent orphans its watcher (session-end never POSTed). Scoped to the main session: a present agent_id marks a subagent tool call whose watcher uses a distinct key + transcript and is ensured at subagent-start. Runs before the rendered/non-rendered split so both the interactive and daemon-hosted long-poll paths benefit. Best-effort: wrapped in try/catch, never throws into the hook path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR Summary by QodoSelf-heal transcript watcher on permission-request hook
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ccb4bbff24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // rendered/non-rendered split so both the interactive path and the daemon-hosted | ||
| // long-poll recover a dead or never-started watcher; idempotent, so a no-op when | ||
| // one is already running. | ||
| await TryEnsureWatcher(baseUrl, sessionId, node); |
There was a problem hiding this comment.
Respect exclusions before spawning the watcher
When a Claude session is in a configured excluded_paths/excluded_repos scope, ClaudeHookCommand dispatches permission-request to this handler before it reaches the profile exclusion checks later in that file, so this new self-heal can start a transcript watcher for a session that session-start intentionally skipped. That means the next permission prompt in an excluded project can begin uploading transcript data despite the user's exclusion settings; the self-heal needs to run only after applying the same exclusion/disabled-session gates or be guarded by them here.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in b6ed302. The permission-request dispatch runs before the excluded_repos/excluded_paths gates, so the self-heal now applies the same exclusion check first and only spawns the watcher when the session isn't excluded (selfHeal = !IsSessionExcludedAsync(...) passed into PermissionRequestCommand.Handle). The permission record/long-poll itself is left unchanged, since hosted agents need the decision regardless of exclusion. Extracted the gate into a shared ClaudeHookCommand.IsSessionExcludedAsync so both call sites can't drift, and added unit tests for it.
Code Review by Qodo
Context used 1.
|
…usions The permission-request dispatch in ClaudeHookCommand runs BEFORE the excluded_repos/excluded_paths gates, so the new watcher self-heal could start a transcript-uploading watcher for a project that session-start intentionally skips (data leak flagged by Codex). Apply the exclusion gate to the self-heal before delegating: skip spawning for excluded sessions while still handling the permission decision (hosted agents need the long-poll result regardless of exclusion). - Extract the repo/path exclusion check into a shared ClaudeHookCommand.IsSessionExcludedAsync used by both the main command path and the permission-request gate (no behavior change for other commands; removes duplication). - Thread selfHealWatcher into PermissionRequestCommand.Handle. - Make TryEnsureWatcher read optional fields via a safe extractor so a variant/non-string payload is treated as absent instead of throwing + logging in a frequently-firing hook (qodo). - Restore (not clobber) KCAP_WATCHER_DIR in the test fixture teardown (qodo). - Add unit tests for the exclusion gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
The PermissionRequest hook previously only recorded the permission event (or long-polled for the decision in rendered-agent mode) and never checked whether the transcript watcher was alive. Since permission requests fire frequently mid-session, this PR adds a best-effort, idempotent watcher self-heal there — the most frequent mid-session recovery point, complementing the existing
notification/stopre-ensure.This recovers sessions that get stuck
active(becausesession-endis never POSTed) when an abruptly-killed agent orphans its watcher or the watcher self-kills.Changes
PermissionRequestCommand.TryEnsureWatcher(...)calls the idempotentWatcherManager.EnsureWatcherRunning(PID-file liveness check, spawn only if dead). Wired intoHandlebefore the rendered/non-rendered split so both the interactive and daemon-hosted long-poll paths benefit.agent_idmarks a subagent tool call, whose watcher uses a distinct key ({sessionId}-{agentId}) + transcript and is ensured atsubagent-start. Mirrors hownotification/stoponly re-ensure the main watcher.Process.Start).Tests
New integration tests (mirror
WatcherLifecycleTests: ownKCAP_WATCHER_DIR, no Console capture):agent_idis presenttranscript_pathis missingAll pass; existing permission/watcher suites pass; AOT publish clean (no IL3050/IL2026 warnings).
No README change — internal self-heal, no user-facing CLI surface change.
Closes #209
AI-1085
🤖 Generated with Claude Code