Skip to content

feat(native-chat): Claude sessions write their subagents into the host status store - #22536

Open
brennanb2025 wants to merge 13 commits into
mainfrom
brennanb2025/c3-claude-producer
Open

brennanb2025 wants to merge 13 commits into
mainfrom
brennanb2025/c3-claude-producer

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1454 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1453
Prod 24 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1478 $\color{#cf222e}{\Huge{\mathbf{−}}}$​143 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1335

ELI5

When a Claude chat starts a subagent or a background shell, Orca now keeps one record for it on the machine running the chat. The record says what kind of work it is, whether it is still running, what tool it is using right now, how it ended (finished, failed, stopped, or "ended, reason not reported"), and when. Nothing on screen reads these records yet, so this PR changes nothing you can see. The switch is #22614, at the top of this stack (this PR → #22553, the Codex producer → #22565, the shared row → #22614), which points the sidebar and the chat strip at these records and deletes the legacy background-task path this PR deliberately left byte-identical.

Supersedes #21279. #22521 (the child-record contract) has merged, so this PR's base is now main.

Merge order

What Changed

The path from a Claude frame to a record, hop by hop

  1. Claude adapter emit (claude-structured-session-adapter.ts). The background-task tracker observes the frame as before. The journal translator writes the frame's rows. The legacy background-task state is republished (so the parent's own status row lands first). Only after all of that, drainClaudeChildWork (claude-child-work-evidence.ts) collects the evidence this frame produced and calls the new onChildWorkEvidence(sessionId, evidence) dependency. A close clears the tracker outside emit, so closeSession and releaseAcquisition drain it afterwards too.
  2. Runtime (structured-agent-session-runtime.ts → structured-claude-runtime-adapter.ts) routes it to host.publishChildWorkEvidence.
  3. Host → client delivery → status feed (publishChildWork) looks up the session's provider.
  4. Status ownership (structured-agent-session-status-ownership.ts) forwards it only under the subject the session's parent row actually landed under. If that publish threw, it forwards nothing.
  5. Status sink (orcad-entry.ts, main-process-runtime-service.ts): publishChildWork → agentHookServer.ingestStructuredChildWork.
  6. Ingest (new server-ingest-structured-children.ts, now in the server class chain) refuses a subject whose parent row the store does not hold. Otherwise it runs the reducer against the host's canonical store.
  7. Reducer (new agent-status-child-work-reconciliation.ts, plus …-evidence-admission.ts and …-evidence-resolution.ts) turns each edge into feat(agent-status): child work records say what the child is doing, how it ended, and when #22521's admission calls (announce / adopt / resume). Records live in memory only and are never persisted or transmitted.

What the producer writes, and from which frame

Claude frame Evidence edge Record effect
task_started (any kind the tracker keeps) live (task id + spawn call, kind, residency from is_backgrounded, name/agent type, description) created live, fence {spawn call, 1}, aliases task_id + tool_use_id
task_updated with live content live kind, residency and labels updated (reclassification goes through adopt)
task_progress (foreground or background) live with operation {toolName: last_tool_name, basis: 'reported'}, summary as last message, usage as tokens operation, last message and tokens updated
a foreground child's own tool_use (frames carrying its parent_tool_use_id) operation {toolName, input: hook-lane preview, basis: 'open'} "Bash: npm test", the same preview a CLI row shows
that call's tool_result operation: null open operation cleared (a reported one stays)
a foreground spawn call's tool_result ended (succeeded, or failed when is_error), result text as last message settled with that outcome
task_notification ended (completed→succeeded, failed→failed, killed/stopped→cancelled, unreadable→unknown), summary, usage settled, or an unknown ending refined to the reported outcome
terminal task_updated ended with the same mapping; patch.error as last message same
background_tasks_changed inventory of the listed background tasks listed children are live (a settled one listed again is resumed, generation + 1). A live background child the list omits settles unknown. Foreground children are untouched
result, or a new turn starting turn-ended live foreground children settle unknown; background ones are untouched
task_started under a new spawn call for a task that already ended live with the new spawn call resumed: same childWorkId, generation + 1, the prior run's outcome kept in previousInvocations. The new run's own progress and spawn result keep reaching the record before any roster lists it
session ended / close session-ended the session's records are removed

Owner (parentChildWorkId). A child-launched shell or a nested agent is owned by the agent whose own traffic made the launching call. The journal translator gains a read-only childToolOwner(toolUseId) query that answers through the same tool-origin registry and linkage that stamp journal rows.

The eviction-before-outcome case. Claude drops a finished task from background_tasks_changed before its task_notification arrives. The roster omission settles the child unknown right away, with no deferral and no timer. The outcome frame then refines it to the reported outcome under #22521's monotone-refinement rule, keeping the first settledAt. Admission alone decides a second ending: an unknown one keeps the definite outcome and lands its last message and tokens, and a different definite one is refused as stale-invocation, which the ingest counts as expected (nothing logged).

The tracker (legacy row unchanged)

ClaudeBackgroundTaskTracker queues an evidence edge at each decision it already makes. The edges are stamped with the host clock when drained, so the tracker's own clock sequence is unchanged. Its published state, settledTasks and fingerprints are byte-for-byte what they were: every existing tracker test passes unmodified. background_tasks_changed handling moved into claude-background-task-aggregate-roster.ts unchanged, to stay under the line limit. The evidence path differs from the legacy row in four deliberate ways:

  • It keeps a foreground child's task_progress. The legacy row still keeps usage only for background tasks.
  • It records a foreground spawn call's result as the child's ending. The legacy row still holds the child until result.
  • It reports outcomes in the outcome vocabulary. The legacy row still maps failed→blocked and killed/stopped→idle.
  • It reports a restart under a new spawn call. The legacy row still waits for the roster to list the task again. Until then the run is held in a separate map (claude-background-task-restarts.ts) that the legacy row never reads. That keeps the run's progress and its foreground spawn result flowing to the record. A roster listing hands the run, and its spawn call, back to the live map.

Bounds

At most 256 live children per session (#21279's bound, which is the tracker's). At most 32 settled children per session: the oldest are removed first, never one that owns live work. Labels go to admission raw: admission owns label clean-up (the one-line fold, the length bounds, dropping a malformed label).

Ported from #21279 (pin 0565294472)

  • The subject-gated ingest that refuses an unknown parent.
  • Children riding the address the parent landed under.
  • Producer-owned-only changes.
  • Ambiguity rejection.
  • Bounded ingestion.
  • Retired alias bindings fencing a finished lifetime, with the id reusable at a higher generation.
  • A monotonic per-child clock.
  • The null-vs-undefined rule, now applied as: an empty inventory is authoritative "no live background work", and a frame with no inventory is no evidence.

Tests for each are included.

Dropped:

  • the legacy-projection module;
  • the parity/agreement gates, which are void on main;
  • the lossy DTO decoder as the evidence source;
  • the constant invocationId;
  • the stop-capability facts, since nothing consumes them yet.

Why

A child is materialised three times today: the tracker's DTO, a renderer-side conversion, and the hook roster. Each hop loses a fact. This PR gives the host the one record per child that #22614 (the reader switch at the top of this stack) will make every surface read. The record is fed from provider frames, not from the lossy summary, so it keeps what the DTO drops: foreground progress, the tool a child is running, and failed as distinct from blocked.

Settlement that no single child names (an inventory omission, a turn boundary) is decided in the reducer from the residency the record stores. Every provider lane therefore shares one rule.

Alternatives considered:

  • Snapshot reconciliation of the whole roster per frame, as feat(agent-status): host-owned child producer for structured sessions #21279 did. It heals itself, but it rewrites every child per frame and needs a settled-history copy in the decoder. Edges carry the full descriptive state instead, so an edge the host could not admit is healed by that child's next one.
  • Deferring the roster-omission settle until the outcome frame arrives. Rejected: there is no timer by decision, so a lost outcome frame would strand the child live for ever.

Linked Issue

None — part of the structured chat status/orchestration program.

Visual Proof

N/A. Nothing reads the records yet: the sidebar and the chat strip keep their current sources until #22614 (the reader switch at the top of this stack). A test asserts that ingesting child work leaves every published status row identical and fires no status-change listener.

Testing

  • pnpm tc:node, pnpm tc:web and pnpm tc:cli all exit 0.
  • New tests: 35 new tests plus 3 new assertions in existing ones.
    • agent-status-child-work-reconciliation.test.ts (16): the reducer.
    • claude-child-work-evidence.test.ts (8): the tracker's evidence, including a restarted run's progress, its roster hand-back, and a restarted foreground run's own spawn result.
    • claude-structured-child-work-producer.test.ts (3): the real adapter end to end, including a run-count parity test. On a new spawn call, the record's invocation.generation equals the attempt the journal roster stamps on that run's child rows (1 then 2). The same test checks that the restarted run's progress and failed spawn result reach the record while the legacy row stays unchanged.
    • server-ingest-structured-children.test.ts (5): subject-gated ingest on a real AgentHookServer.
    • structured-agent-session-status-feed-child-work.test.ts (2): the feed hop.
    • structured-agent-session-status-ownership.test.ts (+1): the landed-address gate, including a publish that threw.
    • claude-structured-session-integration.test.ts (+1): a real session over agentSession.* hands its subagent to the status sink under the session's own address.
    • orca-runtime-structured-status-sink-wiring.test.ts (+2 assertions): both entry points wire the sink to the ingest.
  • Shadow check. A 17-frame script runs through the real adapter: foreground agent, its own Bash call and background shell, foreground progress, spawn result, background agent, result, roster eviction before the outcome, notification, kill, and auto-resume. At every frame it asserts that the parent state folded from the records equals the one folded from today's backgroundTasks, and that raw liveness is equal whenever the main agent is done. Deleting the inventory-omission settle turns it red at frame 11: the records would read working, while today reads working/monitoring.
  • Admission ordering. For a task_started frame, every journal row and the legacy republish precede the evidence delivery. Moving the delivery ahead of the journal turns this red.
  • Ablations, each an exactly-once edit asserted by the script, run at the final head and restored from HEAD:
Deleted (or reverted) Red
Foreground progress: the old backgrounded gate restored 2: foreground progress reaches the record; frame script
Notification ending 2: outcome vocabulary; frame script
failed outcome mapping 2: outcome vocabulary; frame script
Spawn-result ending 2: spawn result ends a foreground child; frame script
unknown → reported refinement 2: outcome after roster eviction; frame script
Inventory residency gate 1: only background children settle on omission
Inventory omission settle 4, including the frame script's parent-state shadow check (frame 11)
Turn-end settle 1: open foreground children settle unknown at turn end
New-spawn-call resume 1: resume keeps the id, generation + 1
Inventory revival 2: revival; frame script (auto-resume)
Session-end removal 3: session end; producer-owned; per-session isolation
Close-path drain 1: frame script (records gone after closeSession)
Owner enrichment 1: frame script (child-launched shell's owner)
Label folding 1: raw multi-line labels admitted as one line
Settled cap 1: bounded settled history, owners of live work kept
Producer-id ownership check 1: another producer's record is untouched
Ambiguity rejection 1: a handle two records answer to is refused
Live ingestion bound 1: the 257th live child is refused
Ingest parent gate 1: unknown parent returns null
Ownership landed gate 1: no child work under an address whose publish threw
Previous-run fence 1: a late frame from the first run neither ends nor restarts the second
Child's own tool call decode 1: frame script (Bash: npm test open operation)
Operation edge dispatch 2: open operation by any handle; frame script
Open-operation close 3: open vs reported operation lifetimes; frame script
Delivery moved ahead of the journal (reordered, not deleted) 1: admission ordering
Restarted-run progress lookup 2: restarted run's progress; run-count parity test
Restarted-run spawn-result lookup 2: restarted foreground run's own spawn result; run-count parity test
Roster hand-back of the restart's spawn call 1: roster listing carries the new spawn call
Restart evidence 2: restart under a new spawn call; run-count parity test

After the restart fix, the ablations covering the touched paths were re-run at the new head. They are: foreground progress gate, notification ending, failed mapping, spawn-result ending, inventory omission, new-spawn resume (also reddens the parity test), inventory revival, close drain, owner enrichment, previous-run fence, child-operation decode, and the four above. All are red.

  • Regression. Every test file the changed modules reach: src/main/claude, src/main/native-chat/agent-session-wire, src/main/agent-hooks, src/main/orcad, src/shared/agent-status*, src/shared/structured-agent-session*, and the structured/agent-session/Claude runtime tests. 482 files, 4,514 tests: 4,489 passed, 1 expected-fail, 17 skipped, 7 failed. 5 of the 7 are the two real-Claude-binary files (claude-structured-real-cli, claude-tui-resume-real-binary), which fail identically (provider close unproven, timeouts) with the tree reverted to the base head. The other 2 (lease-renewer production interval, refusal-retry host oracle) are timing tests that pass when rerun alone (8/8). After the restart fix: the Claude, agent-session-wire, agent-hooks and child-work suites plus the Claude runtime integration were re-run (339 files, 3,336 tests, real-binary files excluded). 3,320 passed. The single failure is the same refusal-retry 90-second timeout, which passes alone.

  • pnpm run check:code-quality:changed: 0 new findings. oxlint on all changed files: clean, and it flagged max-lines on two of them during development. pnpm run audit:anti-slop: exit 0. pnpm-lock.yaml is absent from the branch range, and there are no new docs.

  • Platforms: tests ran on macOS; the code is platform-independent (host-clock stamps, no paths, no shell).

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Not verified:

  • No Electron run. Nothing renders the records yet, and feat(native-chat): the chat strip and the sidebar read the host's child records #22614 (the reader switch at the top of this stack) carries the $electron check.
  • No live Claude SDK capture. Frame shapes come from the SDK type definitions (v0.3.251) and the repo's existing fixtures. In particular, that task_notification.tool_use_id names the current run is unverified. I key endings on the task id alone for that reason.
  • Waiting and blocked children are not produced. How a child's permission request is attributed in the SDK lane is untraced. task_updated.patch.error on a live child is not mapped to blocked. Both read working. Producing them belongs to this producer lane, not to the reader switch (feat(native-chat): the chat strip and the sidebar read the host's child records #22614), and is deferred to a follow-up PR at the top of the stack rather than this head: the SDK's routing of a subagent's permission request through the parent session's callback is unverified, so the state will be produced only once a captured transcript proves the attribution (the query it would use, childToolOwner(toolUseId), already ships here). Until then the child row reads working while the pane-level permission prompt — an untouched, separate path — still surfaces. This is not a regression at the switch: the legacy vocabulary has no waiting state, and its "blocked" is the mislabeled terminal failed, which the records correctly report as failed. The gap is missing parity with the CLI hook lane, whose children do show waiting.
  • A backgrounded child's own shell is not attributed to it. Such a child sends no traffic, so the shell counts at the parent level (never invented onto a child).
  • The published observedAt/firstObservedAt use the adapter's host clock at drain, not the tracker's startedAt. The two differ by microseconds. feat(native-chat): the chat strip and the sidebar read the host's child records #22614's legacy projection from views will carry the record's value.
  • Relay typecheck and mobile were not run. No relay or mobile module imports these files.
  • Per-frame cost is one store mutation per edge. With a large fan-out this means one mutation per task_progress per child, plus one turn-ended edge per turn. I did not measure it (plan risk 10); feat(native-chat): the chat strip and the sidebar read the host's child records #22614 must measure the publish rate before it publishes records.

Decisions and deviations from the plan:

  • Invocation generation. It counts runs. It goes up when a spawn call differs from the current run's registered spawn alias, which is the same event that advances the journal roster's attempt. It also goes up when an authoritative roster lists a settled child again: Claude's auto-resume carries no new spawn call, and the journal roster never sees roster frames. The counter is not read from the journal roster, because that roster lives inside the journal translator and tracks agent kinds only (shells and monitors have no attempt). invocationId is the first run's spawn call, or the task id when a roster names the child before its spawn call is known. The first spawn call seen after that joins the run rather than opening a new one.
  • The task_progress.description activity sentence ("Running Bash") is not stored. It restates the tool, the contract has no activity field, and operation.input is the tool-argument preview.
  • The tracker's foreground task_progress gate is removed for the evidence path only. Removing it from the legacy row would make the chat strip start showing token counts on foreground rows, which is a visible change this PR must not make. feat(native-chat): the chat strip and the sidebar read the host's child records #22614 deletes the legacy row.
  • A result frame from a nested child also sweeps foreground work in the tracker today. The evidence mirrors the tracker's decision so the parent state stays identical.
  • The 32-settled cap is enforced here so memory stays bounded now. Removing settled children when the next turn starts is product retention, left to feat(native-chat): the chat strip and the sidebar read the host's child records #22614.

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.

Notes

  • Since feat(agent-status): child work records say what the child is doing, how it ended, and when #22521 merged:
    • The base is now main.
    • Admission owns label clean-up; the evidence side no longer folds or cuts names, descriptions or agent types.
    • The reducer's "a reported ending latches" check is removed, so admission is the one owner of the ending rule. That check returned before admission for any record already settled with a definite outcome, which dropped a late unknown ending's last message and tokens. Two new tests go through the real Claude adapter and the host's own ingest: a child settled succeeded then given an unclassified task_notification keeps succeeded and records its summary and tokens; a conflicting failed notification keeps succeeded, is refused as stale-invocation, and logs nothing. Putting the latch back turns both red. Deleting admission's refine-only outcome rule now turns the first red too, where before only admission's own tests caught it.
    • The every-hop parity script no longer passes interrupted (removed from the fold input by fix(agent-status): a cancel never hides live work #22476).
  • Wire / mixed versions: no wire change, no new opcode, no new published field. The records stay in the host's in-memory store, which has no production reader and is never persisted. Restart forgets them, per the decision that child records live in memory.
  • SSH / WSL / folder workspaces: evidence is decoded where the Claude session runs (the execution host) and ingested in the same process (desktop main or orcad), under the parent's full execution scope. The ingest test uses an SSH host and a folder workspace.
  • Mobile: nothing reads the records; subagents is unchanged.
  • Boundary with the main-agent status lane: server-ingest-structured.ts, structured-agent-session-agent-status.ts, the fold and the renderer bridge are not touched.

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)

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

Important

The reducer drops a task_updated-driven resume. The tracker revives a settled background child on an edge-only task_updated patch and queues a plain live edge, but the reducer treats it as late evidence and leaves the record settled — the legacy row and the record disagree on the same frame. It is conditional (only reachable before the first background_tasks_changed), so I have flagged it rather than blocked, but C5 will read these records and this reintroduces exactly the stale-settled class of bug this stack exists to remove.

Reviewed changes

  • Claude producer — claude-child-work-evidence.ts decodes task frames into live / ended / inventory / turn-ended / session-ended / restart edges; the tracker queues one at each decision it already makes and drains them after the journal and the legacy republish. The journal translator gains read-only childToolOwner/childActivity.
  • Host delivery — a subject-gated ingestStructuredChildWork on a new server class link, reached through the feed's publishChildWork and the ownership landed gate from both main-process-runtime-service.ts and orcad-entry.ts.
  • Reducer — reconcileAgentChildWorkEvidence folds the edges into the canonical store through C2 admission, with 256-live / 32-settled caps and per-producer ownership.
  • Tests — 8 files; the producer test shadow-checks the parent state folded from records against the legacy backgroundTasks DTO frame by frame.

ℹ️ The unproven-exit window has no child-record consequence

session-ended is only ever produced from backgroundTasks.clear(), which the close ladder reaches only after connection.close() proves the child gone. On an unexpected exit whose tree proof fails, no ended event is emitted, so no edge reaches the store and the run's children stay live under the session's subject. If a replacement is then acquired under the same session id, those records outlive the dead provider session until the replacement ends and removes them with its own session-ended. This may be the intended reading of "loss of contact is never evidence of process death", but C5 will render these records, so it is worth an explicit decision plus a test.

Technical details
# Children of an unproven-exit session stay live across a same-id reacquire

## Affected sites
- `src/main/claude/claude-structured-session-adapter.ts:130-161` — `settleUnexpectedExit` emits the `ended` event (and therefore `session-ended`) only after `close()` returns true.
- `src/main/claude/claude-structured-session-close.ts:100-115` — `finalizeClaudePublishedSession` calls `backgroundTasks.clear()` only when `connectionClosed === true`.
- `src/main/claude/claude-structured-session-recovery.test.ts:402-418` — pins that an unproven exit produces no `ended` event.

## Required outcome
A decision (and a test) on what the child records of an unproven-exit session should read once a replacement is acquired under the same session id: remain `live` until the replacement ends, or settle to `unknown`/be removed when the old provider session is superseded.

## Open questions for the human
- Is a stale-`live` record under a reacquired session id acceptable until the replacement ends, or should the host settle the old subject's children when the provider session is proven gone?

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

counted(ctx, handle.id, result, 'admitted')
return
}
if (existing.membership === 'settled') {

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.

The tracker revives a settled background child on an edge-only task_updated live patch (observeTaskUpdated → upsert, which claude-background-task-resume.test.ts:77-91 pins as required) and queues a plain live edge, but that edge carries no runId (retention drops toolUseId), so agentChildWorkRunVerdict returns current and this branch drops it as "late evidence". The record stays settled while the same frame leaves the legacy row live, contradicting the shadow-check invariant the PR relies on.

Technical details
# Edge-only task_updated revival isn't mirrored into the record

## Affected sites
- `src/shared/agent-status-child-work-evidence-admission.ts:203` — a plain `live` edge on a settled record returns early as late evidence.
- `src/main/claude/claude-background-task-tracker.ts:239-248`, `:307` — `observeTaskUpdated` revives a retained task via `upsert`, queueing `pendingClaudeTaskLive` with no `runId`.
- `src/main/claude/claude-background-task-resume.test.ts:77-91` — pins the tracker's edge-only resume as required behaviour.

## Required outcome
A `task_updated` frame whose live patch revives a settled child leaves the record live, exactly as it leaves the legacy row live.

## Suggested approach
Either queue a resume-style edge (a distinct generation, like `pendingClaudeRestart`) when `upsert` revives an id absent from `this.tasks`, or treat a `live` edge on a settled record as a resume (generation + 1) here instead of dropping it. Do not regress the genuine late-edge case the early return also guards.

## Open question
The producer gate `!this.aggregateRosterObserved` in `observeTaskUpdated` means this is only reachable before the first `background_tasks_changed`. If the team considers it unreachable in practice, say so explicitly rather than leaving the record path silently divergent.

const existing = resolution.child
const request = { parent: ctx.parent, provider: ctx.provider }
const aliases = agentChildWorkHandleAliases(handle)
if (!existing) {

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.

This is the only check of STRUCTURED_CHILD_WORK_MAX_LIVE, so the advertised 256-live bound holds only for brand-new children. The resume branch below (a new spawn call, or an inventory re-listing a settled child) moves a settled record back to live with no capacity check, so a session can run past 256 live records — e.g. settle 32 of 256 live children, admit 32 fresh ids, then resume the settled 32 under new spawn calls. The provider trackers never carry those resumed runs, so nothing else bounds the live count. Computing the live count before both branches, rather than only the create path, keeps the documented bound honest.

@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 in this incremental delta. The restarted-run fix is correct and its tests pass, but two findings from the previous review remain open and unaddressed, so this is not an approval.

Reviewed changes

Only fc7fd5d8ab ("a Claude child restarted under a new spawn call keeps reporting to its record") landed since the prior pullfrog review — 6 files.

  • New ClaudeTaskRestarts store — a task that ended under one spawn call and starts again under another is held in a bounded (256-entry) map, with take/get/clear wired into the tracker's finish, clear, roster replacement, task_progress, and spawn-result lookups.
  • Restarted-run evidence — task_progress resolves a restarted run through this.restarts.get(id), and a foreground restart's own spawn tool_result resolves through restarts.tasks, so that run's operation, usage, and ending reach the record before any roster lists it.
  • Roster hand-back — replaceClaudeAggregateRoster takes the restarted task so a listing resumes it under the new spawn call instead of the stale settled one.
  • Tests — two tracker tests (restart progress before a roster; a restarted foreground run ends on its own spawn result) and a producer run-count parity test asserting the record's invocation.generation matches the journal roster's attempt.

I traced the restart path through task_progress, its own spawn result, repeated restarts, roster hand-back, and turn boundaries; the record tracks the legacy row as the PR body documents (the record hears the restart at once, the legacy row waits for a roster — a deliberate divergence). The two earlier inline findings live in code this delta does not touch and remain open.

Pullfrog  | Fix it ➔ | 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 critical issues. The new commit is a retention refactor; one edge case is worth a decision inline. The two findings from the earlier review are in code this commit does not touch and remain open.

Reviewed changes

Only eada18c2 landed since the prior pullfrog review (the branch was replayed onto a newer c2-child-record-contract, so the earlier commits are unchanged in content).

  • Retention moved into admission — liveFields and settleAgentChildWork now send only the facts the edge observed, delegating labels, model, owner, residency, last message and monotonic tokens to retainedFacts. Verified equivalent on the announce, adopt and resume paths, each of which passes the stored record as prior.
  • Token source narrowed — claudeTaskObservation now reads only facts.totalTokens, dropping the tracked task's task.totalTokens fallback.

Verified locally: claude-child-work-evidence (8), agent-status-child-work-reconciliation (16) and claude-structured-child-work-producer (3) all pass.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

// The published row names a task's type as both its name and its agent type.
...(task.name ? { name: task.name, agentType: task.name } : {}),
...(task.description ? { description: task.description } : {}),
...(facts.totalTokens !== undefined ? { totalTokens: facts.totalTokens } : {}),

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.

With the task.totalTokens fallback gone, a child's token count survives only while its record does — retainedFacts can retain from a prior record but cannot recreate one. The 32-settled cap in trimSettled can evict a settled record while the tracker still holds the task, so a child re-listed by a roster or restarted under a new spawn call is admitted proven-new with no prior and loses the count (these edges pass no facts). The tracked task still carries it here, so the legacy row and the record diverge until the next task_progress. Worth a decision: accept the gap, or carry the task's count on those edges.

Technical details
# An evicted-then-revived child loses its token count

## Affected sites
- `src/main/claude/claude-child-work-evidence.ts:80` — only `facts.totalTokens` is read now; the inventory, `upsert` and restart edges pass no `facts`.
- `src/shared/agent-status-child-work-reconciliation.ts:129-148` — `trimSettled` removes settled records past 32, leaving the tracker's task in `ClaudeSettledBackgroundTasks` / `ClaudeTaskRestarts`.
- `src/shared/agent-status-child-work-evidence-admission.ts:156-177` — a handle whose record is gone is admitted `proven-new`, calling `liveFields(..., null, null)`, so no prior exists to retain from.

## Required outcome
A revived child carries the same token count the legacy row does. Either keep the tracked task's `totalTokens` on the inventory / restart edge, or record a decision that the count may read absent until the next `task_progress`.

## Open question for the human
Is losing the count acceptable here, given #22614 renders tokens from these records?

@brennanb2025
brennanb2025 force-pushed the brennanb2025/c2-child-record-contract branch from 6de7f20 to 41d814b Compare September 24, 2026 23:22
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c3-claude-producer branch from eada18c to 503eb83 Compare September 24, 2026 23:28
…bscriptions as they are

subscribeStatus and subscribeTurnCompletions wrapped client delivery's bound
methods in forwarding lambdas; they are now the same members, the way
waitForSendSettlement already is. The host is at its size limit, and the
next channel it hands out needs the line.
…t status store

The Claude background-task tracker queues child-work evidence at each decision it
already makes (start, update, progress, terminal frame, roster replacement, turn
end, session end), plus the two facts its legacy row ignores: a foreground child's
progress and a foreground spawn call's result. The adapter drains that evidence
after the journal handled the frame and the parent row was republished, and the
host folds it into one record per child in its canonical store.

Nothing reads the records yet; the strip and sidebar keep their current sources.
…child record

The adapter delivers evidence after the frame's journal rows and the parent's
republished row; the frame script keeps the parent state today reads while the
records add outcome and activity; the runtime hands the evidence to the status sink
under the session's own address; both entry points wire the sink to the ingest.
…its record says it is doing

A child's tool traffic reaches the parent stream only for a foreground child. Read
after the journal handled the frame, the child's newest call still awaiting a result
becomes its open operation, previewed the way a hook-reported row previews its own
tool; the result closes it. The open call is derived from the journal's own
bookkeeping, not held a second time.
…ps reporting to its record

A task that ended and starts again stays hidden from the legacy row until a roster
lists it, so the tracker held no run for it: the new run's progress reached nothing
and a foreground re-run's own spawn result settled nothing. The run is now held
beside the live map, where the legacy row never reads it, until a roster hands it
back or it ends. A parity test pins the record's run count to the journal roster's
attempt on a new spawn call, the one event both count.
…ontract get their own homes

The translator's child-tool queries move into claude-child-tool-queries.ts and its contract
type into claude-journal-translator-contract.ts. Brings the translator back under the size
limit.
…e's facts

Admission now keeps what a child's record already knows: labels, model,
owner, residency, the last message within an invocation, and a token count
that never shrinks. The evidence side copied all of those forward itself, a
second owner of the same rule. It now sends only what this edge observed,
and a task's token count comes from the frame that reported it.
…labels

Admission now folds provider text to one line and drops a malformed fact
instead of refusing the record, so the evidence side no longer folds labels
itself. The description keeps admission's longer bound.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c3-claude-producer branch from 503eb83 to fa71175 Compare September 25, 2026 04:50
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/c2-child-record-contract to main September 25, 2026 04:50
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The change adds shared child-work evidence contracts and reconciliation for structured sessions. Claude task tracking now produces evidence for live work, progress, outcomes, inventories, and lifecycle boundaries. The structured-session runtime forwards that evidence to ingestion, which reconciles it only when the parent row exists. Tests cover reconciliation rules, Claude event handling, publication paths, and session isolation.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to bf827

Long sessions may incur slower child-work updates, and a delayed Claude notification can put an older run’s details on a newer child record. The impact is bounded, but these concerns warrant a fix or explicit acceptance before merge.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to bf827

Child-work records are gated on an existing parent session, but a failed status-store write can permanently lose an update. That matters most for completion and cleanup records. No verified security finding was supplied, and the current change does not add a reader for these records.

Retained concerns

  • Medium · reliability · inferred: A failed host publication consumes the queued evidence without retry; losing a terminal or cleanup edge can leave a child-work record live or absent rather than reflecting its final state.
Security review details

Security Blast Radius

  • inferred — The demonstrated write path is host-local and parent-scoped. The reviewed test ranges do not establish a new externally reachable entrypoint; exposure of stored child details to later readers was not established.

Trust Boundaries and Controls

  • observed — Publication uses the session's stored subject and provider; ingestion checks the structured parent, and handle resolution rejects ambiguous matches.

Resilience and Maintainability Implications

  • inferred — Synchronous forwarding supports producer-order delivery in the observed path, but it does not recover evidence lost when a sink fails after the producer drains its queue.

Hardening Proposals

  • proposed — Give failed child-work publications a bounded retry or replay path, particularly for terminal and session-ended evidence.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.71% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 62 functions across 32 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the main change: Claude sessions write subagent records to the host status store.
Description check ✅ Passed The description is comprehensive and covers the user impact, implementation flow, rationale, testing, limitations, platform considerations, and checklist. The Linked Issue section states "None" despit…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

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.

🧹 Nitpick comments (1)
src/shared/agent-status-child-work-evidence-resolution.ts (1)

155-157: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Avoid full alias scans for child lookups.

getAliasesForChild scans every stored alias before filtering by childWorkId. The lookup at src/shared/agent-status-child-work-evidence-resolution.ts:155-157 therefore has linear cost in the alias count. If this path is hot, maintain an index keyed by childWorkId.

The alias-retention claim is not valid: removing a child also removes its aliases, and AGENT_STATUS_STORE_LIMITS.aliases bounds the total alias count.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: b2049632-a4c3-4743-961e-35f553080726

📥 Commits

Reviewing files that changed from the base of the PR and between f559c05 and fa71175.

📒 Files selected for processing (32)
  • src/main/agent-hooks/server-ingest-structured-children.test.ts
  • src/main/agent-hooks/server/server-ingest-remote.ts
  • src/main/agent-hooks/server/server-ingest-structured-children.ts
  • src/main/claude/claude-background-task-aggregate-roster.ts
  • src/main/claude/claude-background-task-restarts.ts
  • src/main/claude/claude-background-task-tracker.ts
  • src/main/claude/claude-child-tool-queries.ts
  • src/main/claude/claude-child-work-evidence.test.ts
  • src/main/claude/claude-child-work-evidence.ts
  • src/main/claude/claude-journal-translator-contract.ts
  • src/main/claude/claude-settled-background-tasks.ts
  • src/main/claude/claude-structured-child-work-producer.test.ts
  • src/main/claude/claude-structured-journal-translation.ts
  • src/main/claude/claude-structured-session-adapter.ts
  • src/main/claude/claude-structured-session-state.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-client-delivery.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child-work.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-ownership.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-ownership.ts
  • src/main/orcad/orcad-entry.ts
  • src/main/runtime/claude-structured-session-integration.test.ts
  • src/main/runtime/orca-runtime-structured-status-sink-wiring.test.ts
  • src/main/runtime/structured-agent-session-runtime.ts
  • src/main/runtime/structured-claude-runtime-adapter.ts
  • src/main/startup/main-process-runtime-service.ts
  • src/shared/agent-status-child-work-evidence-admission.ts
  • src/shared/agent-status-child-work-evidence-resolution.ts
  • src/shared/agent-status-child-work-evidence.ts
  • src/shared/agent-status-child-work-reconciliation.test.ts
  • src/shared/agent-status-child-work-reconciliation.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

@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 in this incremental delta. The one new commit is a behavior-preserving refactor; the three earlier findings remain open, so this is not an approval.

Reviewed changes

Only fa71175a68 ("Claude child evidence hands admission its raw labels") landed since the prior pullfrog review — 2 files, +6/-27.

  • Label folding moved into admission — liveFields now passes child.name/description/agentType through raw; parseObservationFacts folds them with normalizeChildWorkText (control chars and line separators to spaces, trimmed) before the codec sees them, so the producer no longer owns that rule.
  • Description keeps admission's longer bound — the removed childWorkLabel capped every label at 512; description now uses AGENT_CHILD_WORK_DESCRIPTION_MAX_LENGTH (8,000), while name/agentType stay at 512.
  • Dead helper removed — childWorkLabel, its normalizeOptionalField import, and the label-bound constants are gone.
  • Test renamed — the reconciliation test now states that admission owns the folding.

This also closes a latent edge the old helper left open: childWorkLabel folded only C0/DEL, so a label containing U+0085/U+2028/U+2029 would have failed the codec's isChildWorkText check and refused the whole record. Delegating to normalizeChildWorkText folds those too, so the value always satisfies the codec. Verified pnpm test on the reconciliation (16) and evidence (8) files and pnpm tc:node, all green.

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

…nding

The reconciliation returned before admission whenever a record had already
settled with a definite outcome. That dropped the evidence an `unknown` ending
carries (its last message and tokens), which admission's refine-only rule keeps,
so that rule never ran for the structured producers.

The latch goes. Admission keeps the definite outcome, lands the late evidence,
and refuses a conflicting definite ending as `stale-invocation`, which the host
ingest already counts as the fence doing its job, not a fault.

Pinned through the real Claude producer and the host's own ingest.

@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 in this incremental delta. The new commit is a small behavioral fix whose tests can fail; the three earlier findings remain open, so this is not an approval.

Reviewed changes

Only bf82728df6 ("admission alone decides a settled child's second ending") landed since the prior pullfrog review — 2 files.

  • Second-ending ownership moved into admission — applyEnded no longer returns early once a settled child has a definite outcome. Every current ending now reaches settleAgentChildWork, and admission's conflictsWithSettled decides: an unknown ending keeps the stored definite outcome while still landing its last message and monotonic tokens, and a conflicting definite ending is refused as stale-invocation.
  • Tests — the producer harness can now route evidence through a real AgentHookServer. A settled foreground child is sent an unclassified task_notification (asserts its summary and usage land while the outcome stays succeeded) and a conflicting failed notification (asserts settled: 0, a stale-invocation refusal, and no console.warn/error).

I traced the removed latch through settleAgentChildWork → updateExistingAgentChildWork → conflictsWithSettled and mergeObservationFacts: the new comment matches the code, server-ingest-structured-children.ts already treats stale-invocation as an expected fence refusal, and both new tests would fail under the old early return. The three earlier inline findings (agent-status-child-work-evidence-admission.ts revive/cap and the claude-child-work-evidence.ts token gap) sit in files this commit does not touch and remain open.

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

@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: c506d7e3-3703-41b4-88cf-f4ab9b02e077

📥 Commits

Reviewing files that changed from the base of the PR and between fa71175 and bf82728.

📒 Files selected for processing (2)
  • src/main/claude/claude-structured-child-work-producer.test.ts
  • src/shared/agent-status-child-work-reconciliation.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.

Comment on lines +52 to +59
if (agentChildWorkRunVerdict(ctx, existing, edge.handle.runId) !== 'current') {
return
}
// Admission owns what a second ending may change: an `unknown` one keeps a definite outcome and
// lands its evidence; a conflicting definite one is refused as `stale-invocation`.
settleAgentChildWork(ctx, existing, edge.outcome, edge.observedAt, {
...(edge.lastMessage !== undefined ? { lastMessage: edge.lastMessage } : {}),
...(edge.totalTokens !== undefined ? { totalTokens: edge.totalTokens } : {})

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 '130,235p' src/main/claude/claude-background-task-tracker.ts
sed -n '185,230p' src/shared/agent-status-child-work-evidence-admission.ts
sed -n '240,270p' src/main/claude/claude-child-work-evidence.test.ts

Repository: stablyai/orca

Length of output: 6991


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- tracker symbols and notification construction ---'
rg -n -C 8 'pendingClaudeNotification|restarts|terminalTaskIds|task_notification|observeSystemFrame|finish\(|spawnResult|runId' src/main/claude/claude-background-task-tracker.ts src/main/claude src/shared
printf '%s\n' '--- reconciliation implementation and classifier ---'
rg -n -C 12 'agentChildWorkRunVerdict|runId|settleAgentChildWork|reconcileAgentChildWorkEvidence|task_notification|unknown|current|stale-invocation' src/shared/agent-status-child-work-reconciliation.ts src/shared/agent-status-child-work-evidence-admission.ts
printf '%s\n' '--- relevant tests ---'
rg -n -C 12 'status-less|notification|restarted|restart|runId|toolu_fg|agent-fg|lastMessage|totalTokens' src/main/claude/claude-child-work-evidence.test.ts src/main/claude src/shared --glob '*test*'

Repository: stablyai/orca

Length of output: 45659


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- tracker core ---'
sed -n '1,125p' src/main/claude/claude-background-task-tracker.ts
sed -n '175,325p' src/main/claude/claude-background-task-tracker.ts
printf '%s\n' '--- restart implementation ---'
cat -n src/main/claude/claude-background-task-restarts.ts
printf '%s\n' '--- notification producer ---'
rg -n -C 20 'function pendingClaudeNotification|pendingClaudeNotification|runId|invocation' src/main/claude/claude-child-work-evidence.ts
printf '%s\n' '--- reconciliation implementation ---'
rg -n -C 18 'agentChildWorkRunVerdict|reconcileAgentChildWorkEvidence|settleAgentChildWork|runId' src/shared/agent-status-child-work-reconciliation.ts
printf '%s\n' '--- producer restart test ---'
sed -n '330,430p' src/main/claude/claude-structured-child-work-producer.test.ts

Repository: stablyai/orca

Length of output: 29223


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- run verdict and admission ---'
rg -n -C 25 'function agentChildWorkRunVerdict|agentChildWorkRunVerdict|lifetime|stale-invocation|outcome|lastMessage|totalTokens' src/shared/agent-status-child-work-evidence-admission.ts
printf '%s\n' '--- second-ending tests ---'
sed -n '400,560p' src/main/claude/claude-structured-child-work-producer.test.ts
printf '%s\n' '--- notification and run-id tests ---'
rg -n -C 18 'task_notification|pendingClaudeNotification|runId.*undefined|undefined.*current|second ending|stale-invocation|lastMessage.*totalTokens' src/main/claude/*test.ts src/shared/*test.ts

Repository: stablyai/orca

Length of output: 45663


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- spawn result and handle construction ---'
sed -n '90,220p' src/main/claude/claude-child-work-evidence.ts
printf '%s\n' '--- tracker ordering tests ---'
rg -n -C 20 'task_started|task_notification|tool_use_id|delayed|late|notification' src/main/claude/claude-background-task-tracker.test.ts src/main/claude/claude-child-work-evidence.test.ts src/main/claude/claude-structured-child-work-producer.test.ts
printf '%s\n' '--- changed reconciliation diff ---'
git diff 58ba75b5a5fc762e64073b51e481f1832242aa09 bf82728df66ed17e1b0ebab9ae3f43de42ef7a80 -- src/shared/agent-status-child-work-reconciliation.ts src/shared/agent-status-child-work-evidence-admission.ts src/main/claude/claude-child-work-evidence.ts src/main/claude/claude-background-task-tracker.ts
printf '%s\n' '--- notification payload/tool-use fields ---'
rg -n -C 8 \"task_notification|tool_use_id|task_id.*summary|summary.*usage\" src/main/claude src/shared --glob '*.ts' --glob '*.json'

Repository: stablyai/orca

Length of output: 40402


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- notification payloads with tool-use fields ---'
rg -n -C 5 'task_notification' src/main/claude src/shared --glob '*.ts' | rg -n -C 3 'task_notification|tool_use_id|summary|usage'
printf '%s\n' '--- tracker update path ---'
sed -n '140,215p' src/main/claude/claude-background-task-tracker.ts
printf '%s\n' '--- task-id/run-id helpers ---'
rg -n -C 10 'function taskId|function taskAliasId|const taskId|const taskAliasId' src/main/claude/claude-background-task-frames.ts

Repository: stablyai/orca

Length of output: 41867


🏁 Script executed:

#!/bin/bash
set -e
sed -n '1,70p' src/main/claude/claude-child-work-evidence.ts
sed -n '136,150p' src/main/claude/claude-child-work-evidence.ts

Repository: stablyai/orca

Length of output: 3540


Preserve the notification run ID.

Real Claude notifications can include tool_use_id, but pendingClaudeNotification drops it. A delayed notification from toolu_old is then classified as current and can overwrite the newer run's lastMessage and totalTokens.

Suggested fix
-import { record, taskText, taskUsageTotalTokens } from './claude-background-task-frames'
+import { record, taskAliasId, taskText, taskUsageTotalTokens } from './claude-background-task-frames'
...
   return pendingClaudeTaskEnded(id, claudeChildWorkOutcome(message.status), {
     lastMessage: taskText(message.summary),
-    totalTokens: taskUsageTotalTokens(message)
+    totalTokens: taskUsageTotalTokens(message),
+    toolUseId: taskAliasId(message.tool_use_id)
   })

Notifications without tool_use_id remain unclassified and retain the existing same-run behavior.

This branch has not been deployed

No deployments
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