Skip to content

refactor(agent-status): one Claude transcript watch per pane, read before every event - #24075

Closed
brennanb2025 wants to merge 5 commits into
brennanb2025/claude-idle-ctrl-c-subagentsfrom
brennanb2025/claude-transcript-watch
Closed

brennanb2025 wants to merge 5 commits into
brennanb2025/claude-idle-ctrl-c-subagentsfrom
brennanb2025/claude-transcript-watch

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​982 $\color{#cf222e}{\Huge{\mathbf{−}}}$​171 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​811
Prod 17 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​574 $\color{#cf222e}{\Huge{\mathbf{−}}}$​227 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​347

ELI5

Claude Code does some things without telling Orca through a hook. Examples: you answer a permission dialog with No or press Esc, or you kill a background task from /tasks. The only record of those is a line Claude writes to its own session log (its "transcript"). #22722 already reads that log for one reason: an idle Ctrl+C that kills background subagents.

Two planned fixes need to read the same log for other reasons. Without a shared reader, each fix would bolt its own copy onto #22722's single-purpose reader, in the same file.

This PR turns #22722's reader into that shared reader, once: one "transcript watch" per Claude pane. Nothing you can see changes. The one difference is theoretical: if something in a Claude pane printed Orca's OSC 9999 terminal status, the watch would restore the subagents that repaint drops within a second instead of at the next hook. Nothing Orca ships emits OSC 9999 for Claude, and none of 32 real Claude terminal captures contains one.

The plan (this PR is step 1 of 3):

  1. This PR, on fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722: the shared watch machinery, with fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722's one reason to watch.
  2. R1b, on this PR: a background shell counts as live work from the tool call that starts it, and ends on Claude's own queue-operation log row (fixes "done while a shell still runs" after Ctrl+C, and a /tasks kill that leaves the pane on Monitoring).
  3. R2b, on this PR and fix(agent-status): derive the Claude permission wait from outstanding prompts #22042: answering a Claude permission with No or Esc ends Orca's "needs you" wait when Claude logs its interrupt notice. It merges only after R1b is on main.

A follow-up, F1, then makes Claude's own interrupt notice end every cancelled turn on the host that runs Claude (STA-2413), which retires the temporary pieces listed under Why.

What Changed

Before (#22722): a single-purpose watch. It armed only while the pane had a working background agent, read the log only on a one-second timer, and published a row only when it found an agents_killed line. It had its own copy of the "shell running" stamp and a Claude-only special case in the generic transcript-poll code.

After: one watch per pane, built as a small list of reasons to watch.

  • Reasons are a list (claude-transcript-watch.ts). Each entry says when it holds (re-derived from the listener's records, never stored), which raw lines it needs (a substring test), and what fact it applies. This PR ships fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722's reason only: agent-child-working, which ends when the pane has no working agent child (SubagentStop, an agents_killed line, the Stop inventory, SessionStart, pane teardown). R1b and R2b each add one entry.
  • One cursor per pane. A change of reasons never resets it. It is created or repointed only from a row the host accepted, never from an event still being processed, so a refused event cannot move it. When the accepted row names a new transcript (/clear, /resume, a fork), the cursor repoints at that file's end and keeps its reasons, because a reason can outlive the session (a background shell survives /clear, per a 2.1.285 capture). It disarms only after a read that applied no fact with no reason left, and at pane teardown or server/relay stop.
  • Catch-up before every Claude event. On the desktop it runs at the ingest boundary, before the accept-or-roll-back snapshot of the shell and cron evidence, so an event the store then refuses cannot roll back a fact the cursor has already passed. The relay has no rollback step, so there the catch-up simply runs first. The catch-up only reads an existing cursor.
  • Incremental reads: one stat, then only the new bytes, capped at 1 MiB per read and 256 KiB per line. Lines are pre-filtered by the watched reasons' substrings before JSON.parse, so Claude's ~55 KB per-turn snapshot rows never reach the parser. Panes with no reason pay nothing.
  • The tick (one second after the pane's last event, then every second while armed) reads the cursor, rebuilds the row from the listener's records with the hooks' own builder, and publishes it when the stored row does not show it. It compares state, working mode, interrupted, the main agent's state and outcome, subagents, the shell stamp, and the card while the row waits. A fact read inside an event, rather than by a tick, is restated on the next tick with the fact's attribution (SubagentStop and the retired child's id, as fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722 published).
  • The tick against the cancel latch. After Orca infers a Ctrl+C, the store's cancel latch holds late hooks of that turn, but the listener's record has already taken them in and says Working. The tick would restate that record every second, and the latch would accept it once its 15 s window passed: Working over an idle Claude. So a tick row that carries no new fact is marked as a restatement, and the desktop's latch holds it like the late hook it repeats. The marker is stripped when the store takes the row: it is never stored, persisted, or sent over the relay. Rows that carry a fact are not marked, so a killed child is still retired under a held cancel.
  • Why not write the cancel into Claude's record instead (this PR's first version did): that turned a Ctrl+C Claude ignored, for example one that only clears a typed draft while Claude works, into a final "Interrupted" instead of "Done" when the turn ended with its own Stop, on every local Claude pane. The listener's record now keeps the evidence of the held hooks, as on fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722.
  • A row still waiting on a prompt keeps the hook that raised it when the tick restates it, so a kill read just before a main-agent permission prompt no longer lets a sibling call clear the card.
  • A pane moved to another key (detached to a new tab) keeps its watch ticking under the new key, if it already had one; a move never arms a new watch.
  • One stamp function (claudeRunningNonAgentTask in claude-roster-state.ts) for the shell-running flag, used by the listener and the watch. fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722's second copy is deleted. R1b re-points this one function at its task record.
  • Deleted: fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722's single-purpose names, the Claude arm of the generic transcript poll, and its anchor special case. transcript-poll-policy.ts and server-state.ts are back to main's shape.
  • Fixtures: fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722's kind: 'transcript' record now carries lines: the raw scrubbed lines Claude appended since the previous transcript record, in file order. The four idle Ctrl+C fixtures are converted; only field names changed, plus one record whose line repeated an earlier scan and now lists none. No new capture lines were committed.

Why

Deliberate choices, and what is temporary:

Linked Issue

Closes none. Part of STA-3344 and STA-3259.

Visual Proof

N/A. This PR is meant to change nothing you can see; the writer census and its tests below show that, not screenshots.

Testing

Typecheck tc-gate pnpm tc:node and tc:cli at fe3a427c8d6: pass. oxlint and oxfmt on every touched file: pass. Vitest on 53 named files (every src/main/agent-hooks/server*.test.ts, the relay's hook-server tests, every agent-hook-listener* test): 665 passed.

New tests, built from the committed 2.1.280 idle Ctrl+C capture (hook bodies plus its one agents_killed line), with hand-built fields or timing where each test says:

  • Catch-up before the event: the next hook's own row already shows the killed child retired (desktop and relay).
  • A fact read inside an event that produced no row (hand-built auto PostCompact) is published by the next tick.
  • Inferred Ctrl+C with a working background agent, a late main-agent hook the latch holds, then 20 s past the window: still cancelled after a tick.
  • A Ctrl+C Claude ignored, then the turn's own Stop: a plain done, with the watch armed and on a pane it never armed.
  • On an SSH pane: the desktop infers a cancel, the relay ticks past the window and forwards nothing, and a child hook keeps the cancel.
  • A kill read just before a main-agent permission prompt: the card survives a sibling call finishing.
  • A pane moved to another key keeps ticking; a pane restored from disk and moved before any hook arms nothing.
  • A refused event naming another transcript does not move the cursor; an accepted nested claude -p does (pinned).
  • /clear: the cursor repoints and keeps its reasons, then disarms only after the next read.
  • An armed tick while main's permission hold keeps a card, and an inferred question answer: the row is unchanged.
  • Every "row unchanged" assertion first waits for a tick to read past an appended line, so a slow machine fails the test instead of passing it without a tick.

Unit tests (hand-built): arming, never resetting on a reason change, the session-change repoint, reading for a reason that ended just before the read, the pre-filter never parsing a 56 KB line, the restatement marker, the waiting-row attribution, a dismissed row, and a row another host or agent owns.

Ablations (one mechanism reverted, named file run, then restored):

  • Latch does not hold restatements: the late-hook cancel test fails.
  • Cancel written back into the record: both Ctrl+C-ignored tests fail.
  • Waiting row takes the fact's attribution: both permission-card tests fail.
  • No watch resume after a move, or resume without a prior cursor: the matching moved-pane test fails.
  • Relay forced to restate every tick: the SSH cancel test fails.
  • Ticks slowed to 60 s: 11 of 13 desktop watch tests fail, including every "unchanged" test.
  • Drop the desktop or relay catch-up; tick publishes only what it read itself; arm from the incoming event; filter by the reasons holding now only; drop the cursor on a session change; no pre-filter: each fails its named test.

Regression guards (#22722's tests; they pass): server-claude-idle-ctrl-c-captures.test.ts, server-relayed-claude-cancel.test.ts, the renamed unit test, and agent-hook-listener-relay-dependency.test.ts (now lists the two watch modules the relay imports).

Platforms: macOS unit and server tests only. Not run: Linux, Windows, a live SSH relay, or a real Claude. No agent CLI was launched.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Review

Census of every writer of the stored status row, and whether it can disagree with the watch:

  • Hook ingress, desktop and relay: rows are built from the same records, so they agree by construction.
  • Cancel latch: holds the tick's restatements; re-folds child rows and fact rows under the cancel. Tests: late-hook cancel, Ctrl+C ignored, SSH cancel.
  • Permission hold: the tick re-offers a working row, the hold keeps the card, nothing is written. Test: row unchanged after a tick.
  • Inferred Ctrl+C and inferred question answer: update the record before they publish. Tests as above.
  • OSC terminal status: the one writer whose row the watch changes (the theoretical difference in the ELI5). Pinned by restates the children an OSC repaint dropped….
  • User dismissal: never restated; a fact still publishes as its missing hook would, as fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722 did.
  • Row deletion with no teardown: the tick stops and keeps the cursor; the next event resumes it.
  • Pane teardown, PTY exit, dead-pane reconcile, tab close, authority fences: clear the cursor with the records.
  • Pane-key transfer: moves the cursor with the records and resumes its tick at the new key.
  • Restored-row reaping and hydration: no cursor exists at startup; the watch arms only from a live accepted row.
  • Relayed rows on the desktop, structured-session rows, and spool replays: never armed on the desktop.
  • Relay cache: equals what the relay forwarded; the relay's stop clears every cursor.

Merging with other open PRs: #23521 and #23947 change the relay's hook ingest. Whichever lands second must route relay hooks through the retry scheduler's normalizeHook, not normalizeHookPayload directly, or the relay catch-up silently disappears. catches up on the relay before it normalizes the next hook fails if it is missed.

Other things a reviewer should know:

  • Nested claude -p (temporary): it inherits the pane key and its rows are accepted, so the cursor follows the nested session's file and outer agents_killed lines written meanwhile are missed. The killed child lingers until the next inventory: the safe direction, and fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722's behaviour today.
  • Wire (remote-wire-compatibility.md): no field, opcode, enum value or capability changes; the restatement marker never leaves the host. The relay now restates a row in more situations, always in shapes it already publishes.
  • SSH: the relay watches on the machine that runs Claude. Loss of contact produces no fact: the desktop keeps its last row, and silence is never taken as exit (ssh-execution-boundary.md).
  • WSL and Windows: fix(agent-status): reflect Claude's idle-prompt Ctrl+C killing background subagents #22722's path rule is kept. Windows main never watches a drive-less path (a WSL guest's own relay does).
  • Folder workspaces: nothing depends on git.
  • Cost: only armed panes pay, one stat per event and per second plus the appended bytes. Nothing is persisted.
  • Not verified: a live run against real Claude, or on Linux, Windows or SSH. Whether Claude ignores a mid-turn Ctrl+C that clears a draft is inferred from a captured idle-prompt draft Ctrl+C, not captured mid-turn.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…fore every event

Generalizes the idle Ctrl+C `agents_killed` watch into the machinery two planned
fixes need: a list of reasons to watch (one today, a working agent child), one
cursor per pane armed or repointed only from an accepted row, a catch-up before
every Claude event (before the desktop's accept-or-roll-back snapshot; the relay
has no such step), and a one-second tick that restates the row rebuilt from the
listener's records whenever the stored row does not show it.

The cancel latch now re-marks a local Claude pane's record cancelled when it
holds a late main-agent hook, as it already does for Codex, so the tick has no
stale record to restate once the latch window ends.

One stamp function for the shell fact; the watch's second copy is gone. The
idle Ctrl+C capture fixtures' transcript records carry raw `lines`.
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Refactors how the agent hook system reads session transcripts.

The PR appears safe to merge; no outstanding finding or new actionable issue was established.

Summary

This PR replaces the single-purpose Claude agents_killed poll with a per-pane transcript watch that catches up before hooks and restates record-derived status on a tick. Changes since the previous review preserve an active waiting hook when another child is retired and prevent pane transfer from arming a watch without a live cursor.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Accepted Claude row] --> B[Arm or retain pane cursor]
  B --> C[Catch up before next hook]
  B --> D[One-second tick]
  C --> E[Update listener records]
  D --> E
  E --> F[Rebuild and, if needed, publish status]
Loading

Reviews (3) · Last reviewed commit: "fix(agent-status): a moved pane resumes ..."

…r its new key

Moving a pane to another key carried its transcript cursor along but left no tick
scheduled for the new key, so an idle Ctrl+C that killed a background agent stayed
invisible until the pane's next hook.
…ents instead of re-marking the record

When Orca inferred a Ctrl+C that Claude did not act on (one that only cleared a
typed draft), the latch held the turn's later tool hooks and the old fix wrote
the latch's cancel into the listener's record. The turn's own plain Stop then
ended as Interrupted instead of Done, on every local Claude pane.

The listener's record now keeps the held hooks' evidence, as before the watch.
A tick row that carries no transcript fact is tagged `restatesRecords` (stripped
at the store's entry, never stored, persisted or sent over the relay) and the
latch holds it, so a tick still cannot reopen a held cancel after the window.
Rows carrying a fact are untagged and still re-fold under the latch.

A still-waiting row the tick restates now keeps the hook that raised its wait
unless the fact retired that wait's own child, so a kill read just before a
main-agent permission prompt no longer lets a sibling call clear the card.

The watch tests that assert nothing changed now first wait for a tick to read
past an appended line, so a starved runner fails them instead of passing them.

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

✅ No new issues found.

Reviewed changes

This run reviewed the full PR at bf54046: #22722's single-purpose Claude agents_killed poll is replaced by a per-pane transcript watch that reads before every event and on a tick.

  • Shared watch machinery (providers/claude-transcript-watch.ts, claude-transcript-watch-ticks.ts): one cursor per pane armed at the file's end, repointed only from an accepted row on a session change, reasons re-derived from records rather than stored, incremental reads with a substring pre-filter, and a tick that rebuilds the row from the listener's records and publishes it only when the stored row does not already show it.
  • Catch-up ordering: desktop runs it at the ingest boundary before the shell/cron accept-or-roll-back snapshot (server-ingest-normalization.ts); the relay has no rollback step and simply runs it first (agent-hook-result-retry-scheduler.ts).
  • W1: the desktop re-marks the local listener record cancelled when the cancel latch holds a late main-agent working Claude hook, preserving the published verdict's clock (server-status-update.ts, claude-roster-state.ts), so the tick has nothing stale to restate.
  • Single shell stamp: claudeRunningNonAgentTask is now the one function used by both the listener and the watch; #22722's second copy is gone.
  • Cleanup: old module, its test, the generic-poll anchor special case, and the Claude arm of transcript-poll-policy.ts are deleted; fixtures change agents_killed_records to lines.

I traced the stated writer census (OSC repaint, cancel latch, permission hold, inferred answer, user dismissal, alias transfer, teardown, relay cache) against the watch's publish/disarm paths, and the cursor/read spec (arm-at-end, repoint, carry, prefix-skip, reason grace). I could not construct a sequence where the tick produces a wrong final stored status, republishes indefinitely, misses a retirement it should apply, or double-applies a fact. Two independent specialist passes over the watch-vs-other-writers and cursor/read lenses reached the same conclusion.

ℹ️ Verification breadth

The PR body is candid that Linux, Windows, a live SSH relay, and a real Claude were not exercised, and that the OSC-reachability claim rests only on Orca's own HTTP-hook Claude integration. The relay catch-up path (agent-hook-result-retry-scheduler.ts) is the highest-value unverified surface, since the execution host owns the watch there per ssh-execution-boundary.md. No change requested — flagging it as the remaining confidence gap rather than a defect.

Technical details
# Verification gaps carried forward

## Unverified surfaces
- `src/relay/agent-hook-result-retry-scheduler.ts` — catch-up + watch ownership on the SSH/WSL relay (no live relay run).
- `src/relay/agent-hook-server.ts` — `syncClaudeTranscriptWatch` placement after `forward`.
- Platform matrix: Linux, Windows, and the `claudeTranscriptWatchPath` Windows drive-path rule (unit-tested in isolation only).
- OSC reachability: the census counts the OSC terminal-status writer as the one writer the watch changes; the test hand-drives `ingestTerminalStatus`, and the PR notes real Claude reports by HTTP hooks.

## Pre-existing behavior retained (not introduced here)
- `readJsonlCursor`'s >1 MiB `skippedPrefix` path can silently drop a matching line; the old poll used the same reader and filter, so this is inherited rather than a regression.
- Pane-key alias transfer moves the cursor via `movePaneCacheState` but does not reschedule the tick; the watch resumes on the pane's next event (same as #22722).

## Open questions for the human
- Is the stacked base (`brennanb2025/claude-idle-ctrl-c-subagents`) the intended integration target, i.e. is this landing only after #22722?

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

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

✅ No new issues found.

Reviewed changes (incremental, since the prior review at bf54046)

Two fix commits landed; both clean.

  • The cancel latch holds the watch's restatements instead of re-marking the record (7636014dfc). The desktop no longer calls markClaudeLeadTurnInterrupted on a held late main-agent hook. A tick row that carries no fact is now tagged restatesRecords: true (ClaudeTranscriptRow), stripped in applyNormalizedStatus, and resolveCancelVerdictLatch returns HOLD for it once its early-apply checks pass. Result: after a late main-agent hook the latch refused, the listener record keeps that hook's evidence (the turn ends as its own plain done), and the tick cannot reopen the cancel. The flag is desktop-only and the relay strips it before applyEvent, so nothing changes on the wire.
  • Waiting cards keep their hook (7636014dfc). A tick's waiting row now keeps the card's hookEventName/toolUseId unless the fact retired that wait's own child, so a sibling finishing no longer rewrites the card; a fact retiring another child preserves the prompt.
  • A moved pane's watch resumes (093e4781c2). transferPaneAuthority calls the new resumeClaudeTranscriptWatch(transferredStatus) after clearing the old key, so the tick continues under the new key rather than going dormant until the next event. Covered by a pane moved to another key.
  • Test timing hardened: afterTick appends a non-matching line and waits for the cursor offset to reach EOF, so the "nothing changed" assertions provably run after a tick instead of racing one. Ablations and the new tests (moved-key, plain-done settle for an armed and an unarmed pane, fact-retires-another-child) pin the behavior.

Re-checked the redesign against the writer census and the cursor/read path. The key question was whether the new latch hold could swallow a legitimate transition: it cannot, because incoming.payload.mainAgent?.state === 'done' and opensNewTurn are evaluated before the hold, so a real Stop or a new prompt still applies. On a relay the tick compares against the relay's own cache (built from the same records), so it does not restate a plain working row — which is why the desktop-only flag is sufficient. No new issues found.

Technical details
# Increment review notes (bf54046..7636014)

## Verified
- `resolveCancelVerdictLatch` order: early apply on `!previous`, not latched, agent-type mismatch, `restatesAnotherPrompt`, `incoming.mainAgent.state === 'done'`, `opensNewTurn`; only then `restatesRecords → HOLD`. So a settled main agent and a new turn escape the hold. (`server-cancel-verdict-latch.ts:100-114`)
- `applyNormalizedStatus` destructures `restatesRecords` before `canWriteLegacyStatusRow` and before the stored row, so the flag is never persisted or emitted. (`server-status-update.ts:30,159-161`)
- Relay strips `restatesRecords` in the watch publish, so `applyEvent` / `buildRelayHookEnvelope` never carry it. (`relay/agent-hook-result-retry-scheduler.ts:64`)
- Relay tick self-restatement is bounded: `observeClaudeTranscript` compares against `lastStatusByPaneKey` (the relay's cache, produced by its own records), so an unchanged record yields no row.
- `resumeClaudeTranscriptWatch` is on the inheritance chain (`StatusRetries` → `AuthorityEvidence` → `AuthorityAliases`) and runs after `clearTranscriptPoll(previousOwnerPaneKey)` with the row already moved/re-admitted under `toPaneKey`.
- Removed `markClaudeLeadTurnInterrupted` `verdict` param has no remaining callers passing it.

## Not raised
- `unpublished` is dropped on a same-event repoint, and `settleClaudeTranscriptWatch`'s one-more-read grace keys on `read !== undefined` rather than the `unpublished` attribution. Both are narrow; no reachable visible miss could be constructed (a repointing event's own accepted row is rebuilt from the already-retired records).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…iting row keeps its hook

Moving a pane now keeps ticking only a transcript cursor an accepted row
armed; a row restored from disk and moved before its first hook arms nothing.

A row the tick restates while it still waits keeps the hook that raised the
wait. The extra check against the retired child never decided anything: a
fact that retires the wait's owner has already ended the wait.

Adds a relay regression test: after the desktop infers a cancel the relay
never learns of, relay ticks past the latch window forward nothing and the
desktop keeps the cancel through the child's next hook.

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

✅ No new issues found.

Reviewed changes (incremental, since the prior review at 7636014)

Three changes landed in fe3a427c8d; all clean.

  • A moved pane only resumes a watch it had. resumeClaudeTranscriptWatch now calls sync only when a cursor already exists under the moved-to key, so a row and roster hydrated from disk after a restart (no cursor exists at startup) can no longer arm a watch. An ablation confirms the new restart test fails without the guard (expected true to be false).
  • The tick's waiting-row hook rule is simplified. The attribution.toolAgentId === current.toolAgentId exclusion is gone; it was unreachable because the only fact, applyAgentsKilled, clears the wait when it retires the wait's owner (clearClaudePendingWaitForAgent), and stateBeforeWait is never waiting. Behavior is unchanged, and the store doc is updated to match.
  • Relay regression test. A new test proves the desktop's inferred cancel survives relay ticks while the relay's own records still say its main agent is working, with a hand-built afterRelayTick helper plus a RelayAgentHookServer._getStateForTests() accessor.

I verified the guard's ordering (movePaneCacheState moves the cursor before the resume check, and clearTranscriptPoll(previousOwnerPaneKey) cannot delete the moved cursor), that the removed keepsWait branch was dead, and that the new tests can fail. All 14 and 17 tests in the two touched test files pass.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing this intermediate shared-reader branch and rebuilding the reader with its current consumers. The permission and background-work fixes remain tracked; this closure would not claim those gaps are fixed.

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