Keep a service reply that a tool result was recorded after - #1164
Merged
Conversation
A service turn whose final answer was followed by a tool result reported no
reply at all. The poll endpoint returned `ready` with `reply: null`, and a
waiting caller blocked until its 30-minute timeout — while the answer sat in
the transcript the whole time.
`latest_assistant_reply` walks the transcript backwards and stops at the first
tool event, which is right for the case it was written for: narration that
precedes a tool call is commentary, not the answer. But it also stops on a
tool result that *trails* the answer, and harnesses do not guarantee a tool's
result is recorded before the assistant text it produced. Codex flushes the
reply tool's result afterwards, so the walk hit the result immediately and
never reached the answer:
tool_use construct_service_reply
message assistant "CHARLIE"
tool_result call_MXn6... <- walk stopped here, reply = None
Skip trailing tool results until collection has actually begun. Once some
answer text is in hand, a tool event is again the boundary that ends it, so a
turn that stops inside a tool call still reports no reply rather than serving
pre-tool narration as the answer.
Observed on a live interactive codex service session; both shapes are pinned
by tests.
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.
Follow-up to #1162. Found while verifying that PR against a live interactive codex service.
Problem
A service turn whose final answer is followed by a tool result reports no reply at all:
GET /svc/<name>/sessions/<id>) returnsready: truewithreply: nullwait_for_final(headless services) blocks until its 30-minute timeout…while the answer is sitting in the transcript the whole time.
latest_assistant_replywalks the transcript backwards and stops at the first tool event. That's correct for the case it was written for — narration preceding a tool call is commentary, not the answer. But it also stops on a tool result that trails the answer, and harnesses don't guarantee a tool's result is recorded before the assistant text it produced. Codex flushes the reply tool's result afterwards, so the walk hit the result immediately and never reached the answer:That transcript is from a real session, not a constructed one.
Fix
Skip trailing tool results until collection has actually begun. Once some answer text is in hand, a tool event is again the boundary that ends it — so a turn that stops inside a tool call still reports no reply, rather than serving pre-tool narration as the answer.
Scope
The interactive reply path (
construct_service_reply→record_reply→ channel adapter) never went through this function, so Slack pushes were unaffected. What was affected:ready: true,reply: nullwait_for_final, headless servicesTests
Two new cases, both pinned to real transcript shapes:
reply_survives_a_tool_result_recorded_after_the_answer— the bug. Verified it fails onmain(left: None, right: Some("CHARLIE")) and passes with the fix.a_turn_that_ends_inside_a_tool_call_reports_no_reply— guards the boundary the skip must not cross.cargo test --workspace: 51 test binaries, 0 failures.Binary
Touches
crates/daemon→ the relevant binary isconstruct.