feat(codex): republish todo_list revisions as they are ticked off - #473
Open
declan-scale wants to merge 3 commits into
Open
feat(codex): republish todo_list revisions as they are ticked off#473declan-scale wants to merge 3 commits into
declan-scale wants to merge 3 commits into
Conversation
Codex does not reopen its plan: one `todo_list` item is opened at the start of a turn and revised in place through `item.updated`, with `item.completed` only arriving at the very end. The tap dropped those updates, so a consumer saw the plan exactly twice — as first written (nothing done) and then, at end of turn, fully finished. Forward each revision as a `ToolResponseContent` under the item's existing `tool_call_id`. Consumers that key responses by call id (the GenAI Ops Hub chat does) now render the checklist filling in as the turn runs, and the last response received is still the final state. Scoped to `todo_list` via `_PROGRESSIVE_TOOL_ITEMS`: the other tool item types carry no intermediate state worth acting on, and republishing `command_execution` output on every chunk would be pure noise. Updates for an item that was never opened are ignored — there is no request for them to answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
declan-scale
force-pushed
the
declan-scale/codex-todo-list-updates
branch
from
July 28, 2026 14:08
078bfad to
da3b575
Compare
CI lint has been red on `next` since c67e177 added this file: - ruff I001: typing import block not sorted per the repo's isort config - pyright reportPrivateImportUsage: `tracer` is not re-exported from `ddtrace`, so it must be imported from `ddtrace.trace` The `except ImportError` guard still covers ddtrace being absent, and `ddtrace.trace.tracer` exists in the pinned ddtrace 4.10.1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…chine
mcp 2.0.0 renamed `McpError` to `MCPError` and split types out into a
separate `mcp_types` package. `mcp-server-time` and `mcp-server-fetch`
still import the 1.x name, so both now die at import:
ImportError: cannot import name 'McpError' from 'mcp.shared.exceptions'
... -> mcp.shared.exceptions.McpError: Connection closed
uvx builds each server its own isolated environment and resolves `mcp`
unpinned there, so the `mcp==1.23.0` this project installs does not apply.
With every server dead the deep research agent emits "Starting deep
research..." and then makes zero tool calls, so test_agent.py's
`uses_tool_requests` assertion fails (and the streaming test times out
waiting for the same content).
This is why the job passed at 03:48 today and failed at 14:08 with no
code change in between: mcp 2.0.0 landed in the gap.
Constraining uvx to mcp<2 restores all three servers. Verified by
completing an MCP initialize + tools/list handshake against the exact
argv this file now produces:
mcp-server-time tools=['get_current_time', 'convert_time']
openai-websearch-mcp tools=['openai_web_search']
mcp-server-fetch tools=['fetch']
The 30s test timeout is left alone; both tests passed in 21.1s total
when the servers worked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Codex does not reopen its plan. One
todo_listitem is opened at the start of a turn and revised in place throughitem.updated, withitem.completedonly arriving at the very end. The tap dropped those updates, so a consumer saw the plan exactly twice:Real trace from the golden agent (
item_1), request vs response of the same call:Nothing in between, though the agent ticked them off over ~7 minutes.
This forwards each revision as a
ToolResponseContentunder the item's existingtool_call_id. Consumers that key responses by call id (the GenAI Ops Hub chat does) now render the checklist filling in as the turn runs, and the last response received is still the final state.Scope
_PROGRESSIVE_TOOL_ITEMSis{"todo_list"}only. The other tool item types carry no intermediate state worth acting on, and republishingcommand_executionoutput on every chunk would be pure noise. Updates for an item that was never opened are ignored — there is no request for them to answer.tool_call_countstill counts the call once, not once per revision.Testing
pytest tests/lib/adk/test_codex_sync.py— 55 pass, 4 new (TestTodoListUpdates: republish per revision, call counted once, orphan update ignored, other tool items unaffected)pytest tests/lib/core/harness/test_harness_codex_sync.py— 9 pass./scripts/lint— ruff + pyright cleanPaired change
The GenAI Ops Hub side of this is scaleapi/scaleapi#153721's follow-up, which maps every codex tool name onto a real chat block. It already takes the newest response per call id, so it picks these up with no further change.
🤖 Generated with Claude Code
Greptile Summary
This PR fixes a gap in the Codex event-stream tap where
todo_listplan revisions were invisible to consumers: previously only the initial state and the final completed state were forwarded, with nothing in between even though Codex ticks items off over the course of a turn. Now eachitem.updatedfor atodo_listitem is republished as aToolResponseContentunder the sametool_call_id, so consumers (e.g. the GenAI Ops Hub chat) can render the checklist filling in progressively._codex_sync.py: Adds_PROGRESSIVE_TOOL_ITEMS = frozenset({"todo_list"})and a new branch in_handle_itemthat, foritem.updatedevents on progressive items, emits aStreamTaskMessageFull(ToolResponseContent)under the existingtool_call_id— guarded byitem_id in self._tool_openso orphan updates are silently dropped.tool_call_countis still incremented only once onitem.started.obs_ids.py: Updates the ddtrace import from the deprecatedddtrace.tracertoddtrace.trace.tracer(ddtrace v3 API path); the existingexcept ImportErrorguard ensures graceful degradation on older installs.performing_deep_research.py(example): Pinsmcp<2viauvx --withfor all MCP server invocations to avoid theMcpError→MCPErrorrename introduced in mcp 2.0.0.Confidence Score: 5/5
Safe to merge. The new progressive-update path is narrowly scoped to todo_list items, guarded by the _tool_open membership check, and covered by four targeted tests.
The change is purely additive — a new elif branch that only fires for todo_list item.updated events. The existing item.started / item.completed paths are untouched. The orphan-update guard and unchanged tool_call_count semantics both hold up under the new tests.
Files Needing Attention: No files require special attention.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Codex as Codex CLI participant Tap as _CodexStreamProcessor participant Consumer as Consumer Codex->>Tap: "item.started {id: item_1, type: todo_list}" Tap->>Consumer: "StreamTaskMessageStart(ToolRequestContent, tool_call_id=item_1)" Tap->>Consumer: StreamTaskMessageDone Note over Codex,Consumer: Agent ticks items off over many minutes Codex->>Tap: "item.updated {id: item_1, items: [true, false]}" Tap->>Consumer: "StreamTaskMessageFull(ToolResponseContent, tool_call_id=item_1)" Codex->>Tap: "item.updated {id: item_1, items: [true, true]}" Tap->>Consumer: "StreamTaskMessageFull(ToolResponseContent, tool_call_id=item_1)" Codex->>Tap: "item.completed {id: item_1, items: [true, true]}" Tap->>Consumer: "StreamTaskMessageFull(ToolResponseContent, tool_call_id=item_1)"Reviews (4): Last reviewed commit: "fix(tutorials): pin mcp<2 for uvx-spawne..." | Re-trigger Greptile