Skip to content

fix(slackbot): keep streamed answer when a plan block is finalized (Codex answers dropped)#397

Open
jalagrange wants to merge 1 commit into
paradigmxyz:mainfrom
jalagrange:fix/slackbot-codex-answer-dropped
Open

fix(slackbot): keep streamed answer when a plan block is finalized (Codex answers dropped)#397
jalagrange wants to merge 1 commit into
paradigmxyz:mainfrom
jalagrange:fix/slackbot-codex-answer-dropped

Conversation

@jalagrange
Copy link
Copy Markdown

Problem

When the Codex harness produces a final answer, the Slack reply shows only the "Thinking" widget and no answer text. The agent completes successfully and the answer is streamed, but it never appears in the closed message. The Claude Code harness is unaffected.

Concretely, the bot message ends up with blocks [rich_text, plan] (header + Thinking) and the answer is gone.

Root cause

Slack streaming is startStreamappendStream (chunks) → stopStream (final blocks). The stopStream blocks replace the streamed message body; they do not append to the accumulated chunks.

In closeTextStream() (services/slackbot/src/slack/agent-session.ts), the answer markdown was only included in the final blocks when it was not streamed live:

...(!streamedTextLive && answerMarkdown ? renderMarkdownBlocks(answerMarkdown) : [])

That is correct only when stopStream is called with no blocks (the accumulated stream chunks then stand on their own). But plan-emitting harnesses (Codex always renders a "Thinking" plan block) cause stopStream to be called with a plan block, which replaces the streamed body and drops the live-streamed answer.

Claude Code does not emit a plan block, so stopStream is called with no blocks and its streamed answer survives — which is why only Codex is affected.

Fix

When we emit any composed blocks (e.g. a plan block), also re-include the answer markdown, since those blocks replace the streamed chunks. Only omit the answer blocks when sending no blocks at all and the answer was already streamed live.

const planBlocks = tasks.length && !segment.planStarted ? [planBlock(...)] : []
const includeAnswerBlocks =
  Boolean(answerMarkdown) && (planBlocks.length > 0 || !streamedTextLive)

Verification

Verified end-to-end on a live deployment. A Codex Slack reply that previously rendered [rich_text, plan] (no answer) now renders [rich_text, plan, rich_text, table] with the answer table present.

All existing agent-session and codex-session tests pass (45 across the two suites). The existing "does not duplicate live plan or streamed answer markdown on finalize" test still passes — its scenario has the plan already streamed (planStarted=true), so no plan block is emitted at finalize and the answer is correctly left as the streamed chunks.

I attempted a focused unit regression test, but the exact trigger state (a plan/Thinking block emitted at finalize while the answer streamed live) depends on reasoning-summary publish ordering that was non-trivial to reproduce synthetically at the unit level. Happy to add one with guidance on the preferred test shape.

Slack streaming works as startStream -> appendStream (chunks) ->
stopStream (final blocks). The stopStream `blocks` REPLACE the streamed
message body rather than appending to it. In closeTextStream() the answer
markdown was only included in the final blocks when it was NOT streamed
live (`!streamedTextLive`), on the assumption the accumulated stream
chunks would stand. That holds only when no blocks are sent.

When a plan block is emitted in the final layout (plan-emitting harnesses
like Codex always render a "Thinking" plan), stopStream is called with
those blocks, which replace the streamed body and drop the live-streamed
answer. The user is left with a Thinking widget and no answer. Claude
Code is unaffected because it does not emit a plan block, so stopStream is
called with no blocks and the streamed answer survives.

Fix: when we emit any composed blocks (e.g. a plan block), also re-include
the answer markdown, since those blocks replace the streamed chunks. Only
omit the answer blocks when sending no blocks at all and the answer was
already streamed live.

Verified end-to-end on a live deployment: a Codex Slack reply that
previously rendered blocks [rich_text, plan] (no answer) now renders
[rich_text, plan, rich_text, table] with the answer present. All existing
agent-session and codex-session tests pass.
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