Skip to content

Running-tasks strip and per-subagent Stop from the shared subagent tracker (Grok, Claude Stop) - #27585

Closed
brennanb2025 wants to merge 5 commits into
mainfrom
brennanb2025/subagent-tracker-pr2
Closed

brennanb2025 wants to merge 5 commits into
mainfrom
brennanb2025/subagent-tracker-pr2

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

When an agent in native chat starts helper agents ("subagents"), Orca shows them in two places: a row in the chat that lists them, and the running-tasks strip above the message box (also the sidebar), where each running helper has its own Stop button. PR #27228 put the chat rows for Claude, Codex and Grok on one shared subagent tracker. This PR starts feeding the strip and Stop from that same tracker, so the strip no longer loses helpers the chat row has no room for, and a helper you stop reads "stopped" in the chat as soon as the agent confirms the Stop.

What Changed

The problems.

  • Grok: the 65th helper in a turn could not be seen or stopped. A chat row lists at most 64 helpers. Grok's strip was built by reading that saved row back, and the tracker dropped every helper past 64 outright. So a 65th helper got no row entry, no strip entry and no Stop button. The row for helpers started outside any turn is kept for the whole chat, so for that row the limit counted every helper over the chat's whole life.
  • A stopped helper kept saying "working" in the chat. After you pressed Stop on one helper and the agent confirmed it, the chat row did not change until the helper sent its own final report. For Claude that report sometimes never comes: Claude confirms a Stop for a task it has already forgotten without sending anything else. The row then said "working" forever, while the strip (already updated by Claude's own code) said it was stopped.

What you see now.

  • Grok: helpers past the 64th appear in the strip and the sidebar and have their own Stop (up to 256 more per chat). Each chat row still lists its first 64 helpers (the row for helpers started outside any turn: its first 64 over the whole chat). That limit only keeps the saved chat small; it no longer decides what you can see running or stop.
  • Grok and Claude: when the agent confirms a per-helper Stop, the helper reads "stopped" right away in its row entry and in the strip, and its Stop button goes away. Before, Grok's row and strip both kept saying "working" until Grok's own final report, and Claude's row could say "working" forever. If the helper had in fact finished just before the Stop reached it, its own final report still corrects it to "completed" (or "failed"), as long as the agent is still running. Nothing but that report changes it, except the agent starting the same helper again. After the agent restarts, "stopped" is kept as final.
  • Rows are written in the same format as before and list the same helpers, with one exception: a helper past a row's limit that the agent resumes in a later turn keeps its one entry and stays off the rows (it is still in the strip with Stop). Before, such a helper was invisible during its first run and was listed only once resumed.

The mechanism. All in src/main/native-chat/subagent-tracker/:

  • The tracker keeps every helper. A row lists the first 64 helpers it admits. Later helpers are still tracked, with the same rules, the same turn and session handling, and the same Stop; they just have no row entry. Once one of them reports its own ending, the tracker lets it go and remembers it ended, so a late or repeated report cannot bring it back. One that only lost contact or was stopped stays until room is needed, so its own report can still correct it. Helpers past the row limit are capped at 256 per chat, the size of the app's own record of running work.
  • The strip is projected from the tracker (subagent-child-work.ts). After a row write is accepted, the tracker reports each helper whose state, name, run, token count or Stop status changed. Grok publishes those changes only when the row carrying them reaches the chat's saved history, as it already did, so the strip never runs ahead of the chat. When only unlisted helpers changed, the unchanged row is written again to carry the update in order.
  • An acknowledged Stop is reported to the tracker (stopAcknowledged) the same way for Grok and Claude. Each agent still makes its own Stop request to its own provider. The "stopped" it records is provisional: only the stopped run's own final report can replace it, which matches how the host's records already treated an acknowledged Stop. If the journal is busy and refuses that row write, Claude writes it again once the journal catches up.

Why

The tracker already decides what each helper's row says. Building the strip from the same entries removes the second copy of that state, which is where Grok's limit came from. Building it from the saved row would keep tying what you can stop to what fits in the row. The common pattern keeps one record per helper that the chat reads, and does not limit running helpers by count. This PR follows it, with these differences:

  • Intended: the saved chat row still lists at most 64 helpers per turn, because it is rewritten on every change and kept forever. Helpers past it are still tracked, shown in the strip and stoppable.
  • Intended: helpers past the row limit are capped at 256 per chat, the limit the app's record of running work already has. Past that, the app would refuse them anyway.
  • Intended: a helper reads "stopped" as soon as the agent confirms the Stop, instead of waiting for the helper's own final report as the common pattern does. Claude confirms a Stop for a task it has already forgotten and then sends nothing more, so waiting left the row saying "working" forever. The reading is provisional: the helper's own final report, which can arrive after the confirmation even though it was written first, still replaces it.
  • Intended: a helper the app's record of running work turned away because it already held 256 running items is shown once it next reports anything (progress, tokens or its ending), as for every other kind of background work. Before, Grok re-sent every listed helper on each row write; re-sending every running helper on every write would cost hundreds of updates per change once a row is past its limit.
  • Temporary: Claude's and Codex's strips are still built by each agent's own code, and Codex's chat row still waits for the helper's own report after a Stop. Follow-up PR (stacked on this one): build Claude's and Codex's helper entries in the strip from the tracker too, and record Codex's acknowledged Stop in it. For Codex this changes nothing you can see today: its Stop is only answered once the helper's turn has ended, and its row and strip already agree at every step (see the tests). The follow-up also moves Claude's strip onto the row's rules (when a helper in the foreground counts as finished, token counts in Claude's row, and the "stopped" reading after Orca closes Claude).

Considered and rejected: moving all three agents in one PR. Claude's change alters when its strip shows a helper as finished, which needs its own before and after check; doing both at once would hide which part caused a regression.

Linked Issue

N/A (follow-up to #27228, which listed this as its temporary item).

Visual Proof

Hidden test builds of Orca, before = main (3c1141f), after = this PR. Both images use a stand-in Grok (a small proxy in front of the real Grok that turns one prompt into 66 background helpers and answers their Stop like Grok; a helper's own end report arrives 45 s after its Stop). Real Grok, Claude and Codex could not show these: none can start 66 helpers on demand, and each sent its own stop report within a fraction of a second, so before and after look the same with them (see Testing). Every screenshot was checked by hand against the time of the action it shows.

1. 66 helpers in one turn (stand-in Grok). Before, the running-tasks strip lists 64 and helpers 65 and 66 have no entry and no Stop. After, it lists all 66, each with Stop. The chat row lists 64 on both.

Before After
Before: strip shows 64 agents, ends at Helper 64 After: strip shows 66 agents, Helper 65 and 66 with Stop

2. One second after pressing Stop on Helper 10 (stand-in Grok confirms the Stop at once). Before, the row still says "working" and the strip still lists it (it changes only when the helper's own end arrives, 45 s later). After, the row says "stopped" ("63 working +1 stopped") and it has left the strip; its siblings keep running.

Before (+1.3 s) After (+1.3 s)
Before: Helper 10 still working one second after Stop After: Helper 10 reads stopped one second after Stop

Testing

  • First commit: golden tests recorded on unchanged code, one per agent, pinning the strip's records, the Stop targets and the chat row for: one helper, two with the same name, a helper outliving its turn, Claude foreground helpers, an acknowledged per-helper Stop (with no later report, a "stopped" report, and an earlier "completed" report), the agent exiting, and 66 helpers in one turn. The snapshot diff in this PR is exactly the list above: Grok's 65th and 66th helpers appear with Stop; Claude's and Grok's rows, and Grok's strip, read "stopped" on the acknowledgement.
  • New tracker tests (subagent-tracker-child-work.test.ts): helpers past the row limit (including one that lost contact or was stopped, then reported its own ending), the 256 cap and making room in it, refused writes and their retry, restarts, earlier-process helpers never shown as running, and the provisional Stop.
  • Existing subagent, child-work, background-task and Stop suites for Claude, Codex and Grok pass (135 files).
  • Real Claude CLI suite on the final head: passes. orca-ci-checks: passes apart from its local bun typecheck runner, which fails here on a tooling error ("Script not found check"); tsc for node, web and CLI passes through the normal queue.
  • Live QA on hidden test builds, before and after, with the real Claude (2.1.280), Codex (0.159.0) and Grok (1.0.50), signed in as usual: one helper, two helpers, a helper outliving its turn, parent Stop, per-helper Stop, strip and sidebar against the row, and quit and reopen. Every row looked the same on both builds, with no regressions. For per-helper Stop, each real agent sent the helper's own stop report within about 0.4 s, so the "stopped on confirmation" change has no visible window with them; it shows only when the report lags (image 2, stand-in). The 66-helper case used the labelled stand-in (image 1). Real agent config files were hashed before and after; the only changes were Grok's own sessions, logs and caches, plus one Claude settings file changed while no QA process was running.

Notes

  • SSH and remote hosts: the tracker runs where the agent runs, as before. Nothing new crosses the wire; the strip records are the same shape.
  • Windows and Linux: no platform-specific code.
  • Mobile: reads the same rows and records.
  • Performance: each change compares one short fingerprint per helper in the rows it rewrote. Counting helpers past the row limit only runs when a row is full. For Grok, a change to a helper past the row limit writes its turn's unchanged row again, so the update reaches the app in order; that costs the same as a change to a listed helper already does.
  • Stop button: a Grok helper whose id is short but happens to end in "…~" and a number is now stoppable; before, such ids were mistaken for ones the row had shortened.

…e, Codex and Grok subagents

Recorded on unchanged code so the move onto the shared subagent tracker shows each
strip or Stop change it makes as a snapshot diff.
… shared subagent tracker

The tracker keeps every subagent, listing only the first 64 of a turn in its row, and
projects each change for the running-tasks strip after the row write that made it is
accepted. Grok's strip, sidebar and Stop now come from that projection instead of the
saved row, so a 65th subagent is shown and stoppable. An acknowledged per-subagent Stop
is reported to the tracker for Grok and Claude as a provisional stopped that only the
stopped run's own ending can replace.
A subagent past a row's limit is let go only once it reports its own ending, so a later
verdict still corrects lost contact or an acknowledged Stop; the cap makes room from settled
ones. Claude binds an acknowledged Stop to the run current when the CLI acknowledges it, and a
subagent row the journal refused is written again once it drains.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary

The problem. Grok's running-tasks strip, sidebar and per-subagent Stop were built by reading the saved chat row back, and a row lists at most 64 subagents, so a 65th Grok subagent could not be seen or stopped (for the row outside any turn, over the whole chat). Separately, after a per-subagent Stop the agent confirmed, the chat row kept saying "working" until the subagent's own final report, which Claude sometimes never sends.

What changes for the user.

  • Grok: subagents past the 64th appear in the strip and sidebar with their own Stop (up to 256 more per chat). Rows still list their first 64.
  • Grok and Claude: a stopped subagent reads "stopped" right away in its row and the strip; its own final report still corrects it while the agent runs.
  • Claude's and Codex's strips are unchanged in this PR (stacked follow-up).

Review loop.

  • Design: an architecture challenge before implementation recommended this split and a provisional (not final) acknowledged Stop; both adopted.
  • Round 1, three reviewers (tracker rules / Grok path / Claude path + scope + precedent): no P0-P2. Fixed: a subagent past the row limit was let go on any ending, so its own later report could not correct lost contact or an acknowledged Stop; Claude bound the Stop to the run current when it was sent instead of when it was confirmed; a Stop row the journal refused under load was never written again; PR text corrections.
  • Refuted with evidence: re-sending every running subagent on each row write (to re-admit one the app turned away at its 256 limit) made a 66-subagent fan-out send 2,211 updates; the app's own rule already re-admits it on its next report.
  • Round 2 (changes since round 1): one latent finding from the round-1 fix (making room at the 256 cap could let go of a subagent whose ending the strip had not seen yet), fixed with a test.
  • Readiness checklist on the final diff: PASS, no P0/P1. One P2 left as is and stated in the PR: if Claude restarts before a stopped subagent's own report, "stopped" is kept as final (the provisional marker is not saved).

Deferred. Claude's tracker does not report re-keyed or removed subagents to the strip (no effect until the follow-up moves Claude's strip onto the tracker; it must handle them first); subagents past the row limit show no "2" suffix in the strip; Grok rewrites an unchanged row to carry an update for a subagent past the limit (same cost as a listed one's update).

Verified. Golden tests recorded on unchanged code (strip records, Stop targets and rows per agent), with this PR's snapshot diff limited to the listed changes; new tracker tests; targeted suites (31 files); real Claude CLI suite on the final head; tsc node/web/cli; changed-code quality gate; CI on 830f0d2: 15 pass, 17 path-gated skips, 0 fail. Live QA ran the after build at 46b13e0; 830f0d2 adds one fix with no visible effect (covered by a test). Live QA on hidden test builds, before and after: real Claude, Codex and Grok for one and two helpers, a helper outliving its turn, parent Stop, per-helper Stop, strip/sidebar vs row, quit and reopen: all the same on both builds, no regressions. Each real agent sent its own stop report within ~0.4 s, so "stopped on confirmation" has no visible window with them. A labelled stand-in Grok showed both intended changes: 66 helpers all in the strip with Stop (64 before), and a stopped helper reading "stopped" ~1 s after the confirmation (before: ~45 s later, at its own end). A first stand-in run seemed to miss the second change; the stand-in had answered Stop in the wrong shape, which Orca rejects (reproduced in a test), and the corrected run passed.

Not verified. SSH/remote, Windows and Linux live runs (no platform or wire changes; source review only). Codex live behaviour is unchanged by design and was checked only as a no-difference row.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 12, 2026 03:42
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Walkthrough

The tracker now distinguishes children listed in journal rows from additional tracked children, with a bound on unlisted entries. It projects child-work changes when row writes are accepted and supports acknowledged Stops and refused-row retries. ACP events carry child-work changes for evidence conversion, and Claude wiring forwards Stop acknowledgements. Characterization tests cover ACP, Claude, and Codex behavior, including Stop responses, provider exits, and large child groups.


Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 830f0

If Claude restarts after acknowledging a helper Stop but before the helper reports its outcome, the helper may remain incorrectly marked stopped. This is a bounded issue that warrants a fix or explicit acceptance before merge.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 56.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 30 functions across 27 files. 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 clearly summarizes the main changes: tracker-backed running-task entries and per-subagent Stop handling for Grok and Claude.
Description check Passed The description is comprehensive and covers the user impact, mechanism, rationale, testing, visual proof, platform considerations, and known limitations. It does not provide an actual linked issue and…
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.

  • Fix all pre-merge checks with AI
✨ 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


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 12fbbc33-2ea2-43e6-869e-e8c89e80a2fd
📥 Commits

Reviewing files that changed from the base of the PR and between 8e536f9 and 830f0d2.

⛔ Files ignored due to path filters (4)
  • src/main/acp/__snapshots__/acp-subagent-child-work-characterization.test.ts.snap is excluded by !**/*.snap
  • src/main/acp/__snapshots__/acp-subagent-timeline-characterization.test.ts.snap is excluded by !**/*.snap
  • src/main/claude/__snapshots__/claude-subagent-child-work-characterization.test.ts.snap is excluded by !**/*.snap
  • src/main/codex/__snapshots__/codex-subagent-child-work-characterization.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (27)
  • src/main/acp/acp-structured-child-session-end.test.ts
  • src/main/acp/acp-structured-child-stop.ts
  • src/main/acp/acp-structured-child-work-admission.test.ts
  • src/main/acp/acp-structured-child-work.test.ts
  • src/main/acp/acp-subagent-child-work-characterization.test-fixture.ts
  • src/main/acp/acp-subagent-child-work-characterization.test.ts
  • src/main/acp/acp-subagent-child-work.ts
  • src/main/acp/acp-subagent-timeline-characterization.test.ts
  • src/main/acp/acp-subagent-timeline.ts
  • src/main/acp/acp-timeline-event.ts
  • src/main/acp/acp-timeline-translator.ts
  • src/main/claude/claude-journal-translator-contract.ts
  • src/main/claude/claude-structured-control-actions.ts
  • src/main/claude/claude-structured-journal-translation.ts
  • src/main/claude/claude-subagent-child-work-characterization.test.ts
  • src/main/claude/claude-subagent-roster.ts
  • src/main/codex/codex-subagent-child-work-characterization.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns-cancel.ts
  • src/main/native-chat/subagent-tracker/subagent-child-work-characterization.test-fixture.ts
  • src/main/native-chat/subagent-tracker/subagent-child-work.ts
  • src/main/native-chat/subagent-tracker/subagent-row-writes.ts
  • src/main/native-chat/subagent-tracker/subagent-tracker-child-work.test.ts
  • src/main/native-chat/subagent-tracker/subagent-tracker-entries.ts
  • src/main/native-chat/subagent-tracker/subagent-tracker-groups.ts
  • src/main/native-chat/subagent-tracker/subagent-tracker-types.ts
  • src/main/native-chat/subagent-tracker/subagent-tracker.ts
  • src/shared/native-chat-subagent-summary.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment on lines +134 to +159
/** The provider acknowledged a Stop of this child's `run` (null: whichever is current). It shows
* stopped at once; the acknowledgement can precede the run's own ending, which still replaces it,
* and nothing else can. An acknowledgement for a run since superseded changes nothing. */
stopAcknowledged(id: string, run: string | null = null): StructuredAgentSessionSinkAdmission {
const located = this.groups.locate(id)
if (!located || (run !== null && run !== located.tracked.run)) {
return ADMITTED
}
const stopped = acknowledgedStop(located.tracked, this.now())
if (!stopped) {
return ADMITTED
}
located.group.entries.set(id, stopped)
return this.write(located.group)
}

/** Rows a sink refused that no later report rewrote, written again once it drains. */
retryRefused(): StructuredAgentSessionSinkAdmission {
return this.write(...[...this.groups.values()].filter((group) => group.lastSerialized === null))
}

/** What changed for the strip since the last drain, in the order the rows were written. */
drainChildWork(): SubagentChildWorkChange[] {
return this.childWork.splice(0)
}

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '125,165p' src/main/native-chat/subagent-tracker/subagent-tracker.ts
sed -n '45,90p' src/main/native-chat/subagent-tracker/subagent-tracker-entries.ts
sed -n '180,220p' src/main/native-chat/subagent-tracker/subagent-tracker-groups.ts

Repository: stablyai/orca

Length of output: 5124


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- tracker files ---'
rg -n -F --glob 'src/main/native-chat/subagent-tracker/**' \
  -e 'stopAcknowledged' -e 'subagentGroupJournalBody' -e 'journaled' -e 'inherit(' -e 'report' src/main/native-chat/subagent-tracker
printf '%s\n' '--- Claude Stop bindings ---'
rg -n -F --glob 'src/**' \
  -e 'stopAcknowledged(' -e 'acknowledgedStop' -e 'Claude' src/main/native-chat | head -240
printf '%s\n' '--- journal serialization and types ---'
rg -n -F --glob 'src/main/native-chat/subagent-tracker/**' \
  -e 'function subagentGroupJournalBody' -e 'interface TrackedSubagent' -e 'type TrackedSubagent' -e 'NativeChatSubagentEntry' -e 'SubagentGroupJournal' -e 'claimGroup' -e 'report('
printf '%s\n' '--- changed diff ---'
git diff --unified=35 3c1141f23372d26abe9a66bd4074c61139b34175 -- \
  src/main/native-chat/subagent-tracker src/main/native-chat | sed -n '1,1200p'

Repository: stablyai/orca

Length of output: 42809


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- tracker files ---'
rg -n -F --glob 'src/main/native-chat/subagent-tracker/**' -e 'stopAcknowledged' -e 'subagentGroupJournalBody' -e 'journaled' -e 'inherit(' -e 'report' src/main/native-chat/subagent-tracker
printf '%s\n' '--- Claude Stop bindings ---'
rg -n -F --glob 'src/**' -e 'stopAcknowledged(' -e 'acknowledgedStop' src/main/native-chat
printf '%s\n' '--- journal serialization and types ---'
rg -n -F --glob 'src/main/native-chat/subagent-tracker/**' -e 'function subagentGroupJournalBody' -e 'interface TrackedSubagent' -e 'type TrackedSubagent' -e 'NativeChatSubagentEntry' -e 'SubagentGroupJournal' -e 'claimGroup' -e 'report('
printf '%s\n' '--- changed diff ---'
git diff --unified=35 3c1141f23372d26abe9a66bd4074c61139b34175 -- src/main/native-chat/subagent-tracker src/main/native-chat | sed -n '1,1200p'

Repository: stablyai/orca

Length of output: 43050


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- all tracker construction and stop calls ---'
rg -n -F --glob 'src/**' -e 'new SubagentTracker' -e '.stopAcknowledged(' -e 'stopAcknowledged(' -e 'acknowledgedStop' src
printf '%s\n' '--- journal row serialization ---'
sed -n '1,180p' src/main/native-chat/agent-session-journal/journal-subagent-group-body.ts
sed -n '1,90p' src/main/native-chat/subagent-tracker/subagent-row-writes.ts
printf '%s\n' '--- complete tracker report/revise/state path ---'
sed -n '55,115p' src/main/native-chat/subagent-tracker/subagent-tracker.ts
sed -n '185,380p' src/main/native-chat/subagent-tracker/subagent-tracker.ts
printf '%s\n' '--- Claude-specific surrounding matches ---'
rg -n -i --glob '*.{ts,tsx}' 'claude.*stop|stop.*claude|claude.*subagent|subagent.*claude' src/main src/shared | head -240

Repository: stablyai/orca

Length of output: 41167


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- Claude roster construction and stop ---'
sed -n '1,235p' src/main/claude/claude-subagent-roster.ts
printf '%s\n' '--- Claude journaled roster ---'
sed -n '1,280p' src/main/claude/claude-subagent-journaled-roster.ts
printf '%s\n' '--- Claude translation and control call sites ---'
sed -n '270,325p' src/main/claude/claude-structured-journal-translation.ts
sed -n '55,100p' src/main/claude/claude-structured-control-actions.ts
printf '%s\n' '--- restart and Stop-race test assertions ---'
rg -n -F --glob 'src/main/claude/*test.ts' -e 'stopped' -e 'restart' -e 'stopAcknowledged' -e 'before' src/main/claude/claude-subagent-resume-after-restart.test.ts src/main/claude/claude-stop-ack-race.test.ts src/main/claude/claude-subagent-child-work-characterization.test.ts
sed -n '1,280p' src/main/claude/claude-subagent-resume-after-restart.test.ts

Repository: stablyai/orca

Length of output: 31287


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- exact state transition and entry schema ---'
rg -n -F --glob 'src/shared/**' -e 'function canReplaceSubagentState' -e 'canReplaceSubagentState' -e 'isSubagentVerdict' -e 'isTerminalSubagentState' -e 'export type NativeChatSubagentEntry' -e 'type NativeChatSubagentEntry'
sed -n '1,180p' src/shared/native-chat-subagent-summary.ts
rg -n -F --glob 'src/shared/native-chat-types.ts' -e 'NativeChatSubagentEntry'
sed -n '1,120p' src/shared/native-chat-types.ts
printf '%s\n' '--- journaled group reader ---'
sed -n '1,260p' src/main/native-chat/subagent-tracker/journaled-subagent-groups.ts
printf '%s\n' '--- exact restart test blocks around stopped rows ---'
nl -ba src/main/claude/claude-subagent-resume-after-restart.test.ts | sed -n '350,500p'
printf '%s\n' '--- exact tracker hydration and stop transition ---'
nl -ba src/main/native-chat/subagent-tracker/subagent-tracker-entries.ts | sed -n '1,120p'
nl -ba src/main/native-chat/subagent-tracker/subagent-tracker-groups.ts | sed -n '175,220p'

Repository: stablyai/orca

Length of output: 32097


Persist the provisional Stop marker across hydration.

Claude records stopAcknowledged only in TrackedSubagent. The journal serializes tracked.entry, and hydration resets the flag to false. After a restart, stopped is therefore treated as a final verdict. canReplaceSubagentState then rejects the child’s later completed or failed report, so the row remains incorrectly stopped.

Suggested fix
diff --git a/src/shared/native-chat-types.ts b/src/shared/native-chat-types.ts
@@
 export type NativeChatSubagentEntry = {
+  /** True when `stopped` came from an acknowledged Stop and remains provisional. */
+  stopAcknowledged?: boolean
 }

diff --git a/src/main/native-chat/subagent-tracker/subagent-tracker-entries.ts b/src/main/native-chat/subagent-tracker/subagent-tracker-entries.ts
@@
 export function acknowledgedStop(tracked: TrackedSubagent, now: number): TrackedSubagent | null {
   const stopped = tracked.stopAcknowledged ? null : withSubagentState(tracked, 'stopped', now)
-  return stopped ? { ...stopped, stopAcknowledged: true } : null
+  return stopped
+    ? {
+        ...stopped,
+        entry: { ...stopped.entry, stopAcknowledged: true },
+        stopAcknowledged: true
+      }
+    : null
 }
@@
   if (tracked.stopAcknowledged) {
     // The run's own verdict confirms or corrects an acknowledged Stop, keeping when it ended;
     // nothing else may move it.
-    return isSubagentVerdict(state)
-      ? { ...tracked, stopAcknowledged: false, entry: { ...tracked.entry, state } }
-      : null
+    if (!isSubagentVerdict(state)) {
+      return null
+    }
+    const { stopAcknowledged: _provisional, ...entry } = tracked.entry
+    return { ...tracked, stopAcknowledged: false, entry: { ...entry, state } }
   }
diff --git a/src/main/native-chat/subagent-tracker/subagent-tracker-groups.ts b/src/main/native-chat/subagent-tracker/subagent-tracker-groups.ts
@@
-        stopAcknowledged: false
+        stopAcknowledged: entry.stopAcknowledged === true

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing without merging. Review found that saving subagents as one grouped row per turn, rewritten on every change, is what forces the per-row limit and most of the extra code here. The replacement saves one record per subagent, which removes those limits; it comes in two new PRs that will link back here.

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