Fix interactive service session message delivery - #1162
Merged
Conversation
An interactive service session dropped the first channel message and left every later one typed but unsubmitted, so a Slack thread produced a session that never answered. Both failures came from writing the message into the session's PTY with `send_input`, which is not a valid way to talk to an agent TUI: - The opening message was written immediately after spawn, while codex was still drawing its splash screen and had not attached its input handler. The terminal echoed it in canonical mode and discarded it the moment the harness switched to raw mode. The session's own pty.log opens with the echoed text, then a clean redraw with an empty composer. - Later messages were written as `text` + LF. A terminal's Enter key sends CR; in raw mode crossterm reads LF as Ctrl+J, which codex binds to "insert a newline". The message was typed into the composer, line by line, and left sitting there. The daemon already solved both problems for Playbook Run forks. Route service deliveries through the same machinery instead of a second, weaker path: - The first delivery becomes the session's seed prompt, carried in session.start as structured data (spec 0046), so the adapter starts its first turn natively and there is no PTY to race. - Later deliveries go through a new `deliver_user_text`, which frames the message the way the target harness accepts a submitted turn — bracketed paste plus a gated Enter for an agent TUI, CR-terminated write for a line editor, structured input for a headless harness. `deliver_user_text` records the delivery as a user turn only for harnesses that do not mirror their own; codex reports it from its rollout, so recording it again showed the caller's message twice. Renames the delivery classifier off its `playbook_` prefix — it is now the shared answer to "how does this harness take a submitted turn", not a playbook detail. Verified end to end against a live interactive codex service over the HTTP channel: first delivery and follow-up each started a turn and returned through construct_service_reply.
This was referenced Aug 2, 2026
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.
Reported against the
slack-constructservice: the first Slack message in a thread never showed up in the interactive session, and a second message from the same thread got typed into the session's prompt but never submitted.Root causes
Both come from delivering channel messages by writing into the session's PTY with
send_input, which is not a valid way to talk to an agent TUI.First message — lost to a boot race. The opening message was written immediately after
create()returned, while codex was still drawing its splash screen and had not attached its input handler. The bytes sat in the tty's canonical-mode input buffer, got echoed back, and were discarded when codex switched the terminal to raw mode. The reported session's ownpty.logshows it plainly — it opens with the raw echoed message text, before codex'sESC[?2004hsetup, then redraws with an empty composer:Follow-up message — typed but never submitted.
run_ptyterminatesInputwith LF. A terminal's Enter key sends CR; crossterm explicitly maps LF to Ctrl+J in raw mode (crossterm#371), and codex binds Ctrl+J to "insert a newline". So the message was typed into the composer, one line per embedded newline, and left there — exactly what the tail of that session'spty.logshows.Fix
The daemon already solved both problems for Playbook Run forks (
wait_for_fork_ready, bracketed-paste + gated Enter). This routes service deliveries through that machinery instead of maintaining a second, weaker path.session.startas structured data per spec 0046, so the adapter starts its first turn natively and there is no PTY to race.deliver_user_text, which frames the message the way the target harness accepts a submitted turn: bracketed paste + gated Enter for an agent TUI, CR-terminated write for a line editor, structured input for a headless harness.deliver_user_textrecords the delivery as a user turn only for harnesses that don't mirror their own. Codex reports it from its rollout, so recording it again showed the caller's message twice.playbook_prefix — it's now the shared answer to "how does this harness take a submitted turn".New spec
0177-service-deliveries-reach-sessions-as-native-input.mdrecords the rule.Verification
End-to-end against a live interactive codex service over the HTTP channel (same ingress code path Slack uses — the channel is transport-only):
session_key: thread-B)CHARLIEviaconstruct_service_reply06:57:29, repliedDELTAat06:57:36The follow-up's user message appears exactly once in the transcript. The new session's
pty.logopens with codex's own setup sequence — no echoed message text.cargo test --workspace: 51 test binaries, 0 failures.Binary
Touches
crates/daemon→ the relevant binary isconstruct(the daemon lives in it).