Skip to content

[AI-1861] Post live subagent-stop from Codex collab child watchers - #526

Merged
alexeyzimarev merged 3 commits into
mainfrom
ai/codex-subagent-live-stop
Aug 11, 2026
Merged

[AI-1861] Post live subagent-stop from Codex collab child watchers#526
alexeyzimarev merged 3 commits into
mainfrom
ai/codex-subagent-live-stop

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Fixes AI-1861 (follow-up to AI-1839 / #515).

Problem

A Codex collab child's SubagentCompleted was written only by the parent's session-end teardown (CodexSubagentTeardown), so on a long-lived parent — a hosted reviewer running for hours across review rounds — every finished child's chat card spun for the parent's whole lifetime. Verified live on the diagnosed session: 8 SubagentStarted, 0 SubagentCompleted on the parent stream.

Codex fires no per-child stop hook, and the live signal audit (real 0.148 rollouts) showed:

  • sub_agent_activity kinds are only started/interacted — no completed kind
  • wait_agent outputs timed out in every observed call; list_agents status is authoritative but model-driven
  • the deterministic signal is the child rollout's own per-turn terminal event_msg.task_complete — in the file the child watcher already tails

Fix

New CodexSubagentTurnTracker (Cli.Core, pure) folds the child rollout's turn state: task_complete marks the turn completed; any response_item or task_started re-opens it (re-engagement); trailing event_msg noise (token_count etc.) leaves it alone. The codex child watcher's polling loop posts /hooks/subagent-stop once the turn is complete, no tool call is in flight, and an idle grace elapsed — KCAP_CODEX_SUBAGENT_IDLE_MINUTES, default 5m (sized to the observed 2–6 min same-round re-engagement gaps), 0 = immediate.

One-shot by design: the server dedupes lifecycle events per (session, agent) (AgentLifecycleDeterministicId), so the tracker latches after the first successful POST; a failed POST retries next tick. The parent-end teardown stays as the backstop and its duplicate stop dedupes server-side (doc comment updated — it claimed to be the ONLY finalizer).

Accepted trade-off: a child re-engaged after the grace window still streams into its subsession (appends aren't lifecycle-gated) but its card stays completed with duration frozen — mildly wrong in the far less annoying direction than spinning forever. The fuller reactivation-aware (generation-keyed) lifecycle is deliberately out of scope.

Tests

  • CodexSubagentTurnTrackerTests (15 tests): grace boundary, zero-grace immediate, pending-tool-call suppression, re-engagement re-opening (response_item / task_started), noise immunity, one-shot latching across a later task_complete, malformed-line safety, env parsing.
  • Full unit suite: the only failures (both on this branch and on a clean origin/main baseline worktree — 5 vs 12 of the same class) are the known-flaky AgentOrchestratorVendorTests timer races (Fix telemetry lane-overlap, leftover-token, and consent timer-race CI flakes #525 territory), untouched by this change.

Note for the kcap-server submodule bump: docs/CODEX_NORMALIZER.md's "Session end" section says child watchers are finalized by the parent's session-end synthesis — after this, that's the backstop, not the only path.

🤖 Generated with Claude Code

A Codex collab child's SubagentCompleted was written only by the parent's
session-end teardown (CodexSubagentTeardown), so on a long-lived parent —
a hosted reviewer running for hours across rounds — every finished child's
chat card spun for the parent's whole lifetime (verified live: 8
SubagentStarted, 0 SubagentCompleted on the diagnosed session). Codex fires
no per-child stop hook and sub_agent_activity has no completed kind; the
deterministic per-turn signal is the child rollout's own
event_msg.task_complete, which the child watcher already tails.

The new CodexSubagentTurnTracker folds that turn state (task_complete sets
completed; any response_item or task_started re-opens it — re-engagement;
trailing event_msg noise leaves it alone), and the child watcher's polling
loop posts /hooks/subagent-stop once the turn is complete, no tool call is
in flight, and an idle grace elapsed (KCAP_CODEX_SUBAGENT_IDLE_MINUTES,
default 5m, 0 = immediate — sized to absorb the observed 2-6 min same-round
re-engagement gaps). One-shot by design: the server dedupes lifecycle events
per (session, agent), so the tracker latches after the first successful POST
and the parent-end teardown remains the backstop, its duplicate stop deduped
the same way. A child re-engaged after the grace still streams into its
subsession (appends are not lifecycle-gated); its card stays completed with
duration frozen — accepted trade-off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 11, 2026

Copy link
Copy Markdown

AI-1861

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Post live subagent-stop from Codex collab child watchers

🐞 Bug fix ✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Post per-child /hooks/subagent-stop when a Codex child turn completes and idles
• Track child turn completion via rollout task_complete, with re-engagement reopening
• Add KCAP_CODEX_SUBAGENT_IDLE_MINUTES grace window and unit tests for edge cases
Diagram

graph TD
  E["KCAP_CODEX_SUBAGENT_IDLE_MINUTES"] --> W["WatchCommand (codex child)"]
  T[("Child rollout transcript")] --> W --> X["CodexSubagentTurnTracker"] --> D{"Complete + idle + no tools?"} --> P["POST /hooks/subagent-stop"] --> S(["Server lifecycle dedupe"])
  B["CodexSubagentTeardown (parent end)"] --> P
  subgraph Legend
    direction LR
    _proc["Process"] ~~~ _file[("File")] ~~~ _dec{"Decision"} ~~~ _api(["API/Service"])
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Derive completion from list_agents / status polling
  • ➕ Avoids parsing rollout lines and reduces coupling to log formats
  • ➕ Potentially simpler state model (authoritative status source)
  • ➖ Status is model-driven / not reliably deterministic per investigation notes
  • ➖ Adds another external dependency and failure mode to the polling loop
2. Generation-keyed lifecycle (support re-open after stop)
  • ➕ Correctly represents late re-engagements after the idle grace window
  • ➕ Prevents 'completed but still streaming' UI mismatch
  • ➖ Requires server lifecycle model changes (new identity semantics beyond (session, agent))
  • ➖ Much larger scope and higher risk than the targeted fix
3. Server-side auto-complete on task_complete ingestion
  • ➕ Centralizes completion logic; clients remain dumb
  • ➕ Consistent behavior across all watcher implementations
  • ➖ Requires server access to child rollout semantics and ingestion timing
  • ➖ Harder to tune grace/idle behavior close to the source

Recommendation: The chosen approach (child-side stop based on deterministic task_complete + idle grace + no pending tools) is the best scoped fix: it uses the only reliable per-child signal already present in the child rollout and avoids server model changes. If reactivation correctness becomes important, consider the generation-keyed lifecycle later; for now, the deduped one-shot stop plus parent teardown backstop provides a robust, low-risk improvement over 'spins forever'.

Files changed (5) +334 / -1

Enhancement (2) +116 / -0
CodexSubagentTurnTracker.csAdd pure turn-completion tracker for Codex child watchers +109/-0

Add pure turn-completion tracker for Codex child watchers

• Introduces CodexSubagentTurnTracker to infer per-turn completion from rollout JSON lines (task_complete vs task_started/response_item). Adds one-shot stop latching and environment-driven idle grace parsing via KCAP_CODEX_SUBAGENT_IDLE_MINUTES.

src/Capacitor.Cli.Core/CodexSubagentTurnTracker.cs

Models.csPersist Codex child turn-tracking state in WatchState +7/-0

Persist Codex child turn-tracking state in WatchState

• Extends WatchState with a CodexSubagentTurnTracker instance for codex child watchers (vendor==codex && agentId!=null). Documents why this state exists and that it is specific to Codex collab children.

src/Capacitor.Cli.Core/Models.cs

Bug fix (1) +71 / -0
WatchCommand.csPost /hooks/subagent-stop from Codex child watchers after idle grace +71/-0

Post /hooks/subagent-stop from Codex child watchers after idle grace

• Adds stop-grace resolution from KCAP_CODEX_SUBAGENT_IDLE_MINUTES and tracks child agent type lazily from the rollout header. In the polling loop, posts subagent-stop once the child turn is complete, idle grace elapsed, and no tool calls are pending; retries on failure and latches on success, with parent teardown remaining the backstop.

src/Capacitor.Cli/Commands/WatchCommand.cs

Tests (1) +143 / -0
CodexSubagentTurnTrackerTests.csAdd unit tests for CodexSubagentTurnTracker turn state and grace logic +143/-0

Add unit tests for CodexSubagentTurnTracker turn state and grace logic

• Adds a focused test suite covering grace boundary behavior, zero-grace immediate stop, pending-tool-call suppression, re-engagement reopening, noise immunity, latching after successful stop, malformed-line safety, and env parsing.

test/Capacitor.Cli.Tests.Unit/CodexSubagentTurnTrackerTests.cs

Documentation (1) +4 / -1
CodexSubagentTeardown.csClarify teardown is a backstop (child watchers now post live stop) +4/-1

Clarify teardown is a backstop (child watchers now post live stop)

• Updates the class summary to reflect that parent session-end teardown is no longer the only finalizer. References the new live child stop mechanism and server-side dedupe behavior.

src/Capacitor.Cli/Commands/CodexSubagentTeardown.cs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66430829af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1700 to +1702
if (vendor == "codex" && agentId is not null) {
foreach (var line in newLines) {
state.CodexSubagentTurn.Observe(line);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconstruct completion state when resuming

When a child watcher restarts after the batch containing task_complete was acknowledged but before the grace-period stop was posted, WatcherConnect resumes from the server watermark and DrainNewLines returns only later lines. Because the fresh tracker observes only newLines here, it never sees the already-acknowledged completion and therefore never posts the live stop; the child card remains in progress until the parent eventually ends. Reconstruct the tracker from the acknowledged prefix, or otherwise persist its completion state when resuming.

Useful? React with 👍 / 👎.

@qodo-code-review

qodo-code-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. AI-1861 in comments ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
New/updated comments include the Linear issue identifier AI-1861, which is disallowed in code
comments. This reduces portability and can leak internal tracking identifiers.
Code

src/Capacitor.Cli.Core/CodexSubagentTurnTracker.cs[R6-8]

+/// Turn-completion state a Codex collab CHILD watcher folds from its own rollout lines to
+/// decide when to post the LIVE <c>/hooks/subagent-stop</c> (AI-1861). Codex fires no
+/// per-child stop hook and <c>sub_agent_activity</c> carries no completed kind, so before
Evidence
PR Compliance ID 4 forbids Linear issue numbers in code comments. The cited code regions contain
AI-1861 within newly added/modified XML doc comments and inline comments.

CLAUDE.md: Do not include Linear issue numbers in code comments
src/Capacitor.Cli.Core/CodexSubagentTurnTracker.cs[6-8]
src/Capacitor.Cli/Commands/WatchCommand.cs[474-477]
src/Capacitor.Cli/Commands/CodexSubagentTeardown.cs[9-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Comments introduced/updated in this PR reference Linear issue IDs (e.g., `AI-1861`), which is prohibited. Replace these references with non-Linear wording (or a GitHub issue/PR reference if truly necessary).

## Issue Context
Compliance requires code comments to avoid Linear-specific identifiers.

## Fix Focus Areas
- src/Capacitor.Cli.Core/CodexSubagentTurnTracker.cs[6-13]
- src/Capacitor.Cli.Core/Models.cs[171-176]
- src/Capacitor.Cli/Commands/CodexSubagentTeardown.cs[7-12]
- src/Capacitor.Cli/Commands/WatchCommand.cs[474-476]
- test/Capacitor.Cli.Tests.Unit/CodexSubagentTurnTrackerTests.cs[5-11]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. KCAP_CODEX_SUBAGENT_IDLE_MINUTES not in README ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
This PR introduces a new user-facing environment variable (KCAP_CODEX_SUBAGENT_IDLE_MINUTES) that
changes Codex child watcher behavior, but README.md is not updated to document it. Users won’t
know how to configure the new idle-grace behavior.
Code

src/Capacitor.Cli/Commands/WatchCommand.cs[R474-477]

+        // AI-1861: idle grace between a Codex collab child's task_complete and its live
+        // subagent-stop POST (codex child watchers only — see the loop's stop check). The
+        // agent type is read lazily from the child rollout's own header, once.
+        var     codexSubagentStopGrace = CodexSubagentTurnTracker.ResolveStopGrace(Environment.GetEnvironmentVariable("KCAP_CODEX_SUBAGENT_IDLE_MINUTES"));
Evidence
PR Compliance ID 6 requires updating README.md for user-facing CLI surface changes. The watcher
now reads KCAP_CODEX_SUBAGENT_IDLE_MINUTES, but the README’s environment-variable table for
watcher tuning does not mention it.

CLAUDE.md: Update README.md in the same PR for any user-facing CLI surface changes
src/Capacitor.Cli/Commands/WatchCommand.cs[474-478]
README.md[1430-1436]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new user-facing configuration knob (`KCAP_CODEX_SUBAGENT_IDLE_MINUTES`) was added/used by the CLI, but the README was not updated.

## Issue Context
The README already documents related watcher tuning env vars (e.g., `KCAP_CODEX_IDLE_MINUTES`). This new variable should be listed alongside them with default and behavior.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/WatchCommand.cs[474-478]
- README.md[1430-1437]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Stop POST trips heartbeat ✓ Resolved 🐞 Bug ☼ Reliability
Description
The Codex child watcher awaits PostCodexSubagentStopAsync inside the 1Hz main loop; because it
uses PostWithRetryAsync with the default 30s retry budget, the loop can stop touching its
heartbeat for >20s and be killed/restarted as “stale.” This can cause unnecessary watcher churn
exactly at turn completion and can delay or prevent the intended stop POST from succeeding in a
stable way.
Code

src/Capacitor.Cli/Commands/WatchCommand.cs[R734-737]

+                    if (await PostCodexSubagentStopAsync(baseUrl, sessionId, agentId, codexChildAgentType, transcriptPath, cts.Token)) {
+                        state.CodexSubagentTurn.StopPosted = true;
+                        Log($"Codex subagent {agentId} ({codexChildAgentType}) turn complete + idle "
+                          + $"{codexSubagentStopGrace.TotalMinutes:F0}m; posted subagent-stop");
Evidence
The main loop touches heartbeat each iteration and the repo’s staleness policy marks watchers stale
after 20s; the new stop call is awaited in that same loop and uses an HTTP helper with a default 30s
retry budget, allowing the loop to stop touching heartbeat long enough to be killed as stale.

src/Capacitor.Cli/Commands/WatchCommand.cs[226-239]
src/Capacitor.Cli/Commands/WatchCommand.cs[659-666]
src/Capacitor.Cli/Commands/WatchCommand.cs[719-739]
src/Capacitor.Cli/WatcherManager.cs[314-333]
src/Capacitor.Cli.Core/WatcherHeartbeat.cs[27-36]
src/Capacitor.Cli.Core/HttpClientExtensions.cs[289-347]
src/Capacitor.Cli.Core/HttpClientExtensions.cs[452-519]
src/Capacitor.Cli/Commands/WatchCommand.cs[1088-1101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`WatchCommand` touches the watcher heartbeat once per loop iteration, and `WatcherManager.IsWatcherAlive` considers a watcher stale if it hasn’t touched within `WatcherHeartbeat.Threshold` (20s). The new Codex child stop path awaits an HTTP call (`PostWithRetryAsync`) with a default 30s retry budget, which can block the loop longer than the stale threshold.

### Issue Context
This affects only Codex *child* watchers (vendor == "codex" && agentId != null), and triggers exactly when the tracker decides it’s time to post `/hooks/subagent-stop`.

### Fix Focus Areas
- src/Capacitor.Cli/Commands/WatchCommand.cs[719-775]
- src/Capacitor.Cli/Commands/WatchCommand.cs[1088-1101]
- src/Capacitor.Cli.Core/HttpClientExtensions.cs[289-347]
- src/Capacitor.Cli.Core/WatcherHeartbeat.cs[27-36]

### Suggested fix
- Add an explicit short timeout for the stop POST so the main loop cannot be blocked beyond the heartbeat slice/threshold (e.g., pass `timeout: TimeSpan.FromSeconds(5)` to `PostWithRetryAsync`, or use `PostOnceAsync` with a small timeout).
- If you still want retries, keep them at the watch-loop level (next iterations) rather than a single 30s in-loop await.
- Optionally, wrap the stop post in a linked CTS with a tight deadline that covers *both* client creation/auth discovery and the POST.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Silent rapid stop retries ✓ Resolved 🐞 Bug ◔ Observability
Description
On any non-2xx response or exception, PostCodexSubagentStopAsync returns false without logging;
because StopPosted is only set on success, the watch loop retries the stop POST on every
subsequent eligible poll (with ~1s cadence). This can spam /hooks/subagent-stop during partial
outages/auth issues and provides little diagnostic signal about why stops aren’t being recorded.
Code

src/Capacitor.Cli/Commands/WatchCommand.cs[R1097-1100]

+            return resp.IsSuccessStatusCode;
+        } catch {
+            return false;
+        }
Evidence
The stop is attempted in the main loop when eligible; on failure, no state changes occur to prevent
repeated attempts, and the loop continues with a 1s delay. The stop helper swallows exceptions and
only returns a boolean success flag with no logging on failure paths.

src/Capacitor.Cli/Commands/WatchCommand.cs[719-739]
src/Capacitor.Cli/Commands/WatchCommand.cs[771-775]
src/Capacitor.Cli/Commands/WatchCommand.cs[1088-1101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new Codex child stop path retries on every eligible poll when the POST fails, but failures are silent (no status-code/body logging; exceptions are swallowed). This can create repeated requests and makes it hard to debug why stop events are not landing.

### Issue Context
- Eligibility remains true until `StopPosted` is set.
- The loop delays ~1 second between iterations.
- `PostWithRetryAsync` retries only transport/timeouts; non-success HTTP responses will return quickly and then be retried next loop.

### Fix Focus Areas
- src/Capacitor.Cli/Commands/WatchCommand.cs[719-775]
- src/Capacitor.Cli/Commands/WatchCommand.cs[1088-1101]

### Suggested fix
- Log failures at least once per attempt (include status code; optionally a small, sanitized/truncated response body).
- Add a simple backoff gate in state (e.g., `DateTimeOffset? LastStopAttemptAt` + min retry interval, or exponential backoff capped).
- Consider treating certain non-retryable statuses (e.g., 400) as a latch to avoid infinite retries until parent teardown.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
⚠️ Tickets: not configured — ticket URL found in PR but could not be fetched — check ticket provider credentials

Grey Divider

Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli.Core/CodexSubagentTurnTracker.cs
Comment thread src/Capacitor.Cli/Commands/WatchCommand.cs Outdated
Comment thread src/Capacitor.Cli/Commands/WatchCommand.cs
Comment thread src/Capacitor.Cli/Commands/WatchCommand.cs
… restart

Review fix: a restarted child watcher resumes from the server watermark
(WatcherConnect), so DrainNewLines never re-delivers already-acknowledged
lines — including the task_complete that should drive the live stop. A child
watcher dying between that ack and the grace-delayed stop POST left a fresh
tracker permanently disarmed, spinning the card until the parent-end
teardown. SeedCodexSubagentTurnState now folds the rollout's full on-disk
prefix through both the turn tracker and the pending-call set at child
watcher startup; the first drain re-observing the unacknowledged suffix is
harmless (both folds converge on the same last-state), and an unreadable
rollout degrades to the pre-seed behavior with the teardown as backstop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexeyzimarev

Copy link
Copy Markdown
Member Author

Addressed the reviewer finding about restart recovery in 7ddcb38: a restarted child watcher resumes from the server watermark (WatcherConnect), so the drain never re-delivers an already-acknowledged task_complete — a child watcher dying inside the [ack, stop-POST] window (≈ the grace period) left a fresh tracker permanently disarmed. SeedCodexSubagentTurnState now folds the rollout's full on-disk prefix through both the turn tracker and the pending-call set at child-watcher startup. Re-observing the unacknowledged suffix on the first drain is harmless (both folds converge on the same last-state); a missing/unreadable rollout degrades to the pre-seed behavior with the parent-end teardown as backstop. Covered by three new seed tests (file ending in task_complete → re-armed; file ending mid-turn with a pending call → disarmed; missing file → no-op).

…README

- Strip the Linear issue token from all new C# comments (repo rule + the
  lint-linear-ids CI check; references live in the PR/commit metadata).
- Bound one live child-stop POST attempt to 5s overall (linked CTS across
  auth client creation + the POST, and an explicit PostWithRetryAsync
  timeout): the main loop touches the watcher heartbeat once per iteration
  and awaits this call inline, so the previous default 30s retry budget
  could blow past the 20s staleness threshold and churn the watcher exactly
  at turn completion. Retries ride later loop iterations instead.
- Space failed stop attempts 60s apart (was: every 1s tick) and log each
  failure with the HTTP status / exception message. Deliberately no
  terminal give-up: an auth refresh can heal, and the parent-end teardown
  dedupes whatever never posts.
- Document KCAP_CODEX_SUBAGENT_IDLE_MINUTES in the README's Codex
  session-end tuning table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexeyzimarev
alexeyzimarev merged commit 5c39fb2 into main Aug 11, 2026
6 checks passed
@alexeyzimarev
alexeyzimarev deleted the ai/codex-subagent-live-stop branch August 11, 2026 12:06
alexeyzimarev added a commit that referenced this pull request Aug 11, 2026
)

* Feed the Codex watcher state trackers raw lines, not redacted ones

RedactLine replaces any line over 64 KiB with a placeholder carrying no
call_id and a type neither tracker recognises, and an oversized
function_call_output — a build log, a large file read, a verbose test run —
is routine rather than exceptional.

UpdateCodexPendingToolCalls therefore never removed the call_id, leaving
PendingCodexToolCalls non-empty for the life of the watcher. toolInFlight
was then permanently true and ShouldEndOnIdle could never fire. For Codex
that is not a degraded backstop: the desktop app's shared app-server never
exits per conversation, so the idle timeout is the ONLY per-conversation
session-end path. The session stays Active in the read model indefinitely
and the watcher never exits.

The same stranded call_id also permanently blocks the live subagent-stop
added in #526, which requires no tool call in flight — reinstating the
symptom it fixed, a finished child's chat card spinning for the parent's
whole lifetime.

CodexSubagentTurn.Observe read the same list and fails in the other
direction: it treats any response_item as the turn re-opening, so an
oversized one is not recognised and a child that re-engaged after
task_complete could be reported stopped while still working.

Both now read drainRead.Lines, matching what SeedCodexSubagentTurnState
already does when it folds this state from disk — the seed was correct and
only the live drain path was inconsistent. Neither tracker emits anything;
they read call_id and type only, so no unredacted content leaves the
process and redaction still governs everything sent to the server.

Same defect and same fix as the Claude tracker in #517.

Closes #528

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Gate the Codex tool tracker on vendor now that it reads raw lines

Both reviewers caught the same regression. The tracker's loop was never
gated on vendor — it ran for every watcher on the grounds that a non-Codex
line has no response_item and so is a cheap no-op. That held while it read
the redacted list, where any oversized line arrived as a ~90-byte
placeholder. Reading raw lines removes that bound, so a Claude or Gemini
watcher would JsonDocument.Parse a multi-megabyte tool output on every
drain to discover, again, that it is not a Codex record.

TracksCodexToolCalls mirrors TracksClaudeToolCalls and makes the decision
testable rather than an inline condition. Not gated on watcher role: a
collab child needs the tracking for ShouldPostSubagentStop.

Also trims the comments added in the previous commit, per the repo's
keep-comments-minimal rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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