Feed the Codex watcher state trackers raw lines, not redacted ones - #529
Conversation
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>
PR Summary by QodoFix Codex watcher tool tracking by using raw drain lines (not redacted)
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e1d10fda88
ℹ️ 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".
| foreach (var line in drainRead.Lines) { | ||
| UpdateCodexPendingToolCalls(state.PendingCodexToolCalls, line); |
There was a problem hiding this comment.
Restrict raw Codex parsing to Codex transcripts
When a non-Codex transcript contains an oversized line, such as a multi-megabyte Claude or Gemini build log, this unconditional loop now passes the entire raw payload to JsonDocument.Parse even though it cannot affect Codex state; previously newLines contained the tiny oversize placeholder. Because the drain imposes no maximum line size, this adds potentially large UTF-8/JSON allocations and parsing work to every vendor and can stall or exhaust the watcher on large dumps. Gate the raw-line loop on vendor == "codex" so only Codex transcripts pay this necessary cost.
Useful? React with 👍 / 👎.
Code Review by Qodo
1.
|
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>
|
Both fixed in 643204e. Raw parsing for all vendors (codex P2 / qodo 2) — confirmed, and a regression I introduced. Now gated by The Verbose comments (qodo 1) — fair, trimmed. Cut the production block at Verified after the change: |
Closes #528 (AI-1844 follow-up — the Codex counterpart of #517).
The bug
Two watcher state trackers read the redacted line list.
SecretRedactor.RedactLinereplaces anyline over 64 KiB with a placeholder carrying no
call_idand atypeneither tracker recognises,and an oversized
function_call_output— a build log, a large file read, a verbose test run — isroutine rather than exceptional.
SeedCodexSubagentTurnState(WatchCommand.cs:1170) already folds both trackers from the rolloutraw on disk. Only the live drain path was inconsistent — the seed had it right.
Why it matters more for Codex than it did for Claude
1. Sessions stuck
Activeforever. The strandedcall_idkeepsPendingCodexToolCallsnon-empty for the watcher's life, pinning
toolInFlighttrue soShouldEndOnIdlecan never fire.For Codex that isn't a degraded backstop — the desktop app's shared
codex app-servernever exitsper conversation, so the idle timeout is the only per-conversation session-end path. The session
stays
Activein the read model indefinitely and the watcher never exits.2. #526's live subagent-stop never posts.
ShouldPostSubagentStoprequires no tool call inflight, so the same stranded
call_idblocks it permanently — reinstating the exact symptom #526fixed (a finished child's chat card spinning for the parent's whole lifetime).
3. Premature subagent-stop.
Observetreats anyresponse_itemas the turn re-opening. Anoversized one isn't recognised, so a child that re-engaged after
task_completecan be reportedstopped while still working.
(1) and (2) are silent and permanent; (3) is a race.
The fix
Both trackers read
drainRead.Lines. Neither emits anything — they readcall_idandtypeonly —so no unredacted content leaves the process and redaction still governs everything sent to the
server. Identical to the Claude fix at
WatchCommand.cs:1896-1899.Testing
CodexToolTrackingSourceTestsmirrorsClaudeToolTrackingSourceTests, covering both directions:the
call_idis stranded when the redacted line is used and cleared when the raw one is, and aredacted
response_itemfails to re-open the turn while the raw one re-opens it. Payloads are sizedoff
SecretRedactor.MaxRedactableLineCharsrather than hard-coded, per the review of #517.Being straight about coverage: these tests pin the invariant, and they passed the moment they were
written. The defect is at a call site inside
RunWatch's localDrainNewLines, which closes overloop state and isn't reachable from a unit test — the same limitation as #517. What the tests
guarantee is that if
RedactLine's placeholder or either tracker changes shape, the reason thesecall sites must use raw lines is asserted rather than assumed.
CodexToolTrackingSourceTests2/2,CodexSubagentTurnTrackerTests18/18,UpdateCodexPendingToolCallsTests6/6,ClaudeToolTrackingSourceTests7/7,WatchCommandTests77/77.machine; integration 3 vs 2 on main, overlapping names (
SchemePresent_ProbeHitsAuthConfigfailson both). No failures in the affected area on either.
Not fixed here
UpdateCodexPendingToolCallshas no equivalent of #517's resume backfill for the session watcher,so a watcher reconnecting mid-tool starts with an empty pending set and could idle-end a live Codex
session after
KCAP_CODEX_IDLE_MINUTES. Pre-existing and independent of the redaction bug —SeedCodexSubagentTurnStatecovers only the collab-child path. Left out to keep this reviewable;noted in #528.
🤖 Generated with Claude Code