Repository navigation
fix(agent-hooks): follow Claude sessions continued into a daemon job (adopts #26569) - #33605
Conversation
…24117) When Claude Code forks a pane's session into a daemon job (`/background`, or an interrupt followed by a new prompt), the job keeps running in the same pane but its hooks are declined since #15304. The pane's row kept the old providerSession, so mobile native chat showed the old transcript while Stop still interrupted the live job. The old transcript records `continued-in` with the new session id. On each presence recheck the execution host's hook server (and the relay for SSH) reads the tail of the row's transcript, verifies the fork beside it carries the new id, and rewrites only the row's providerSession as a replay, keeping state and evidence age. A fork another pane already reports is left alone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`/clear` inside a job starts a new transcript without writing `continued-in`, so the chain stopped at the job's first transcript. The job's state.json keeps its first sessionId and names the current one in resumeSessionId and linkScanPath; follow that when the new transcript sits beside the old one and carries the new id. It is checked before the size memo because the old transcript stops growing after `/clear`. Reported by Sangkwun on #26569. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… evidence Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rs88gCErtCoZLvrC7fTCXm
The job's state.json stays under the job's first session id, so once the pane's row moved to a /clear'd session the next /clear was never found. Remember which job each followed session belongs to.
Walkthrough
Priority: Merge Risk:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments: 1
src/shared/agent-hook-listener/claude-session-continuation.ts: 1
217-217:Trivial · Performance & Scalability · Heavy lift Move the synchronous continuation checks off the presence paths.
ClaudeSessionContinuationTracker.resolveperforms bounded but synchronous filesystem work. Main-server and relay presence checks call it before their asynchronous process probes. Cached relay replays also invoke this path once per replayable pane. A slow filesystem can block the relevant event loop during these checks.Convert the complete tracker path, including
reverse-file-region-scan.ts, to asynchronous filesystem APIs. Preserve the current fail-closed error handling. Retain the row-identity check after asynchronous resolution before applying a rebind.

Review info
-
Run configuration-
- Source mode: Auto
- Repository: Unavailable
- Branch: Unavailable
- File: No configuration file from this repository applied
- Commit: Unavailable
- Layers: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
742adbeb-9ca1-4586-b944-d9595675ee89
Files selected for processing: 7-
src/main/agent-hooks/server-claude-session-continuation.test.tssrc/main/agent-hooks/server/server-agent-presence.tssrc/shared/agent-hook-listener/claude-session-continuation.test.tssrc/shared/agent-hook-listener/claude-session-continuation.tssrc/wsl-guest/agent-hook-server-claude-session-continuation.test.tssrc/wsl-guest/agent-hook-server.tssrc/wsl-guest/relay-agent-presence.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.
-
…uuid ids when following a Claude job - Carry providerSession.cwd into followed sessions so resume keeps the folder (#33527). - Like Claude Code's own reader, drop a continued-in pointer that a later prompt or finished reply overtook, and remember a pending pointer so an unchanged transcript is not re-read. - Require Claude's session-id shape for continuedInSessionId, resumeSessionId and the linkScanPath basename so ids cannot escape the project or jobs directory.
…llowing a Claude job - Tagged text and interrupt markers are not new prompts, as in Claude Code's reader. - Match "sessionId":"<id>" in the fork's head so a huge first record cannot block following.
There was a problem hiding this comment.
Actionable comments posted: 1

Review info
-
Run configuration-
- Source mode: Auto
- Repository: Unavailable
- Branch: Unavailable
- File: No configuration file from this repository applied
- Commit: Unavailable
- Layers: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
7203f4c2-2444-4edb-b889-b3202b7cd228
Files selected for processing: 2-
src/shared/agent-hook-listener/claude-session-continuation.test.tssrc/shared/agent-hook-listener/claude-session-continuation.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.
-
| const MAX_REMEMBERED_ENTRIES = 256 | ||
| // Claude Code accepts only this shape for continuedInSessionId; it also keeps ids out of path joins. | ||
| // Claude Code skips tagged text (commands, reminders) and interrupt markers when finding prompts. | ||
| const NOT_A_PROMPT = /^\s*<[a-z][\w-]*[\s>]|^\[Request interrupted by user/ |
There was a problem hiding this comment.
Do not exclude every XML-prefixed user prompt.
If a user sends <context>please revise this</context> after a continued-in pointer, NOT_A_PROMPT treats the prompt as tagged system text. Before an assistant reply completes, the scan can follow the stale pointer and bind the pane to the fork instead of the active old session. Restrict this exclusion to known Claude-generated markers, or use record metadata to distinguish them from user prompts.
ELI5
Claude Code can move the conversation in an Orca terminal into a background "job" while that same terminal keeps showing it (press ← for the agent view, then Enter). Orca deliberately ignores job hooks (#15304), so it never learns about the new conversation: the pane's row, mobile chat and native chat stay frozen on the old session. Claude writes a
continued-innote into the old transcript saying where the conversation went. Orca now follows that note.This adopts @mtsr404's #26569 (their 4 commits are kept as-is, rebased onto current
main) and adds one fix on top for repeated/clearinside a job.Root cause
CLAUDE_JOB_DIRis set (src/main/claude/hook-script.ts,if [ -n "$CLAUDE_JOB_DIR" ]). That rule stays.--session-id <new> --fork-session --resume <old>.jsonl(seen inpsand indaemon/roster.jsondispatch.launch), the old transcript gets{"type":"continued-in","sessionId":<old>,"continuedInSessionId":<new>}, andjobs/<new8>/state.jsoncarriessessionId,resumeSessionIdandlinkScanPath(resumeSessionId/linkScanPathmove to the new transcript after/clear).What changed
continued-in(and the job'sstate.jsonafter/clear) from the row's own transcript and rewrites only the row'sproviderSession, as a replay. State and evidence age don't change. Main,orcad(SSH) and the WSL guest relay all do this. Nothing comes from the worker's environment.providerSession.cwd, so resume still starts in the session's folder (fix(agents): resume an agent from the folder it ran in, not the pane's start folder #33527).continued-inpointer is ignored once a later user prompt or a finished, non-error assistant reply appears after it in the old transcript. This closely follows Claude Code 2.1.310's own reader (QKn): tool results, tagged text such as<command-name>, and[Request interrupted by userdon't count as prompts, so a pane that kept using the old session doesn't flip-flop. The new transcript is accepted when its first 256 KB contain"sessionId":"<id>", so a huge first record can't block following. A pointer whose fork isn't on disk yet is remembered, so an unchanged transcript isn't re-read on every recheck.continuedInSessionId,resumeSessionIdand thelinkScanPathbasename must have Claude's UUID shape (same regex Claude uses inZL), so ids like../../xcan't escape the project orjobs/directory.state.jsonstays under the job's first session id, so once the row had followed one/clear, a second/clearwas never found. The tracker now remembers which job each followed session belongs to.Verification (real Claude Code 2.1.310 + fake Anthropic server)
I ran the real
AgentHookServerwith the real generatedclaude-hook.sh, plus pinned Claude Code 2.1.310 in tmux. Everything was isolated (HOME,CLAUDE_CONFIG_DIR,TMPDIR,env -i) and talked to a local fake/v1/messagesSSE server. Steps:claude, prompt, ←, Enter, prompt again, then a presence recheck.main: the job'sSessionStart/UserPromptSubmit/Stophooks fire withCLAUDE_JOB_DIRset and are dropped. The row stays on the old session (0ea221da…) after the recheck.d7c64a32… → 8ad56c95…, with the job's transcript path). State is unchanged./cleartwice inside the job, rechecking after each: the row follows both (2eacedc1 → fd8b2579 → 213172da). A fresh tracker without the new memo stops after the first/clear; the extended unit test fails without the fix./clear) and resolved the real transcripts with the tracker: both followed and keptcwd. In that flow the old transcript ends with the pointer, so the overtake check doesn't fire.pnpm tc,pnpm test src/main/agent-hooks src/wsl-guest src/shared/agent-hook-listener(2378 passed),pnpm run check:code-quality:changed.Limits / risk
isReplayuntil the next real hook or presence change. Until then, Orca won't infer an Esc interrupt for it from terminal output. Clearing it means writing the stored row behindapplyNormalizedStatusin both the main server and the WSL relay, so it's left for a follow-up./backgroundfollowed byclaude attach <id>is not covered. The pane'sclaudeexits, Orca keeps only an identity row, and the attach client fires no hooks. That needs the issue's direction 1 (attach argv to the job), so this PR only refs [Bug]: Claude session reopened in its pane after /background loses its Orca status and mobile chat #24117.sessionId, Orca stops following at the last session it found. This fails safe: the row keeps a valid older session and nothing wrong is attributed./clearjob memo lives in memory. After an Orca restart, a row already on a/clear'd session follows again only once the job's first session is rediscovered.state.jsonread attempt and onestat. The transcript tail is re-read only when its size changes (1 MB on the first scan, then appended bytes plus 64 KB).Refs #24117
Supersedes #26569, #9237
Co-authored-by: mtsr404 10844682+mtsr404@users.noreply.github.com
Co-authored-by: dimakrest 12894495+dimakrest@users.noreply.github.com