Skip to content

fix(agent-hooks): follow Claude sessions continued into a daemon job (adopts #26569) - #33605

Merged
nwparker merged 7 commits into
mainfrom
nwparker/agint-claude-background-job-following
Oct 25, 2026
Merged

nwparker merged 7 commits into
mainfrom
nwparker/agint-claude-background-job-following

Conversation

@nwparker

@nwparker nwparker commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​600 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​600
Prod 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​394 $\color{#cf222e}{\Huge{\mathbf{−}}}$​11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​383

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-in note 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 /clear inside a job.

Root cause

  • Job workers run under Claude's shared daemon and inherit whichever pane started it, so the hook script exits when CLAUDE_JOB_DIR is set (src/main/claude/hook-script.ts, if [ -n "$CLAUDE_JOB_DIR" ]). That rule stays.
  • Nothing else tells Orca the pane is now showing the job's session.
  • Upstream (Claude Code 2.1.310, installed binary): the job is launched as --session-id <new> --fork-session --resume <old>.jsonl (seen in ps and in daemon/roster.json dispatch.launch), the old transcript gets {"type":"continued-in","sessionId":<old>,"continuedInSessionId":<new>}, and jobs/<new8>/state.json carries sessionId, resumeSessionId and linkScanPath (resumeSessionId/linkScanPath move to the new transcript after /clear).

What changed

  • From fix(agent-hooks): follow Claude sessions continued into a daemon job (#24117) #26569: on the existing presence recheck (non-decorative title change or finished command), the execution host follows continued-in (and the job's state.json after /clear) from the row's own transcript and rewrites only the row's providerSession, 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.
  • Review fixes on top:
    • Followed sessions keep 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).
    • A continued-in pointer 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 user don'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, resumeSessionId and the linkScanPath basename must have Claude's UUID shape (same regex Claude uses in ZL), so ids like ../../x can't escape the project or jobs/ directory.
  • New on top: state.json stays under the job's first session id, so once the row had followed one /clear, a second /clear was 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 AgentHookServer with the real generated claude-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/messages SSE server. Steps: claude, prompt, ←, Enter, prompt again, then a presence recheck.

  • main: the job's SessionStart/UserPromptSubmit/Stop hooks fire with CLAUDE_JOB_DIR set and are dropped. The row stays on the old session (0ea221da…) after the recheck.
  • This branch: the recheck moves the row to the fork (d7c64a32… → 8ad56c95…, with the job's transcript path). State is unchanged.
  • /clear twice 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.
  • After the review fixes, I re-ran the real CLI flow (fork, then /clear) and resolved the real transcripts with the tracker: both followed and kept cwd. 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

  • Only which session the pane shows is fixed. Live working/done state for the job still comes only from OSC titles. A rebound row is written as a replay to keep its evidence clock, and the stored row keeps isReplay until 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 behind applyNormalizedStatus in both the main server and the WSL relay, so it's left for a follow-up.
  • /background followed by claude attach <id> is not covered. The pane's claude exits, 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.
  • If Claude respawns a job under a new 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.
  • The /clear job 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.
  • Cost per recheck for Claude rows: one state.json read attempt and one stat. The transcript tail is re-read only when its size changes (1 MB on the first scan, then appended bytes plus 64 KB).
  • No wire or schema changes, so mixed client/server versions are unaffected. An older host just doesn't follow.

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

mtsr404 and others added 5 commits October 25, 2026 11:17
…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.
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Walkthrough

The changes add a tracker that resolves Claude session continuations from transcript records and job state. The main server and WSL relay use the tracker to rebind pane rows before presence checks. Rebound rows carry the resolved provider session, and subsequent presence observations clear replay status. Tests cover continuation resolution, rebinding, transcript validation, and presence updates.

Priority: Normal

Merge Risk: Moderate · up to a87f8

An active Claude pane can be associated with the wrong session after an XML-prefixed prompt. Correct prompt detection before merging; synchronous presence checks also warrant follow-up.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 25.93% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: following Claude sessions continued into daemon jobs.
Description check Passed The description is detailed and covers the user impact, root cause, implementation, safeguards, testing, limitations, and linked issue references. It omits some template headings and checklist confirm…
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.
Full details: Description check

Explanation

The description is detailed and covers the user impact, root cause, implementation, safeguards, testing, limitations, and linked issue references. It omits some template headings and checklist confirmations, but the required technical information is mostly complete.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@coderabbitai coderabbitai 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.

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.resolve performs 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
Commits

Reviewing files that changed from the base of the PR and between 76029bf and d0e7b15.

Files selected for processing: 7
  • src/main/agent-hooks/server-claude-session-continuation.test.ts
  • src/main/agent-hooks/server/server-agent-presence.ts
  • src/shared/agent-hook-listener/claude-session-continuation.test.ts
  • src/shared/agent-hook-listener/claude-session-continuation.ts
  • src/wsl-guest/agent-hook-server-claude-session-continuation.test.ts
  • src/wsl-guest/agent-hook-server.ts
  • src/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.

@coderabbitai coderabbitai 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.

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
Commits

Reviewing files that changed from the base of the PR and between fc7d605 and a87f8f3.

Files selected for processing: 2
  • src/shared/agent-hook-listener/claude-session-continuation.test.ts
  • src/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/

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.

Major · Functional Correctness · Heavy lift

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.

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.

2 participants