Skip to content

fix(rendering): keep Thinking tasks in_progress until the reasoning item seals#527

Open
Zygimantass wants to merge 6 commits into
mainfrom
fix/slack-thinking-task-flicker
Open

fix(rendering): keep Thinking tasks in_progress until the reasoning item seals#527
Zygimantass wants to merge 6 commits into
mainfrom
fix/slack-thinking-task-flicker

Conversation

@Zygimantass

Copy link
Copy Markdown
Member

Problem

Slack threads flip-flop between "Thinking", "command execution", and "Thinking completed" while a turn is running.

Root cause in the Codex App Server mapper (packages/rendering/src/codex-app-server.ts): every reasoning event created a brand-new Thinking task (fresh counter id per delta), and whole-block reasoning events were marked complete immediately — so the Slack plan card's current-activity line churned between Thinking cards and the running command.

Fix

  • Accumulate reasoning deltas into one Thinking task per reasoning item, keyed by the protocol's stable itemId, staying in_progress while the block streams.
  • Flip to complete only when the item seals (item.completed for that reasoning item) or the execution actually finishes (flush via completeOpenTasks). A command starting mid-thought no longer flips it.
  • changedActivityTaskUpdates previously emitted a task's details exactly once; it now re-emits when the details text changes so the accumulating thinking text reaches Slack (task details are replace-semantics downstream; command details are static so their behavior is unchanged).
  • Codex multi-section summaries (summaryIndex changes) get a paragraph break within the one task.

Events without an itemId (whole-block reasoning passthrough) keep the old behavior — those blocks are already sealed on arrival, so complete is correct.

Testing

  • packages/rendering: 20/20 tests pass, including two new tests covering delta accumulation, mid-thought command interleaving, sealing, and summary-section breaks; tsc --noEmit clean.
  • services/slackbotv2 (only consumer of @centaur/rendering): 55/55 tests pass.

Note: the slackbot image needs a redeploy for this to take effect in production.

Zygimantass and others added 2 commits June 12, 2026 15:16
…tem seals

Each reasoning event previously created a brand-new Thinking task (fresh
counter id per delta) and whole-block events were marked complete
immediately, so the Slack plan card cycled between "Thinking",
"Thinking completed", and the running command.

- accumulate reasoning deltas into one task keyed by the protocol's
  stable itemId, staying in_progress while the block streams
- flip to complete only on item.completed for that reasoning item or at
  flush when the execution actually finishes
- re-emit task details when they grow (details are replace-semantics
  downstream) so the accumulating thinking text reaches Slack
- insert a paragraph break between Codex summary sections (summaryIndex
  changes) within the one task

Amp-Thread-ID: https://ampcode.com/threads/T-019ebbcb-9016-753d-85ba-fbbb7e94dbe9
Co-authored-by: Amp <amp@ampcode.com>
…g recovered state

The startup-recovery test asserted the persisted thread state immediately
after the chat.stopStream call, racing the state write that happens just
after the stream stops. Sibling recovery tests already wait for
renderObligation === null; do the same here.

Amp-Thread-ID: https://ampcode.com/threads/T-019ebbcb-9016-753d-85ba-fbbb7e94dbe9
Co-authored-by: Amp <amp@ampcode.com>
Zygimantass and others added 4 commits June 12, 2026 17:17
…ity starts

Slack renders the most recent task chunk as the plan card header. Marking
a Thinking task complete the moment its reasoning/commentary item seals
makes "Thinking completed" the header until the next activity emits a
chunk — which reads as if the agent is done while it is still working.

Keep sealed Thinking tasks in_progress and flip them to complete only
when the next activity (command, file change, tool use, new reasoning
block, or commentary item) produces its first update, ordered before that
update in the same batch, or when the run flushes. On failure, sealed
thinking flips to complete instead of error since those blocks finished
fine.

Amp-Thread-ID: https://ampcode.com/threads/T-019ebbcb-9016-753d-85ba-fbbb7e94dbe9
Co-authored-by: Amp <amp@ampcode.com>
…r shows 'Thinking completed' mid-turn

Slack computes the plan card header from task statuses: it shows the
current in_progress task and falls back to 'Thinking completed' whenever
no task is in progress — even mid-turn. Replaying a production thread
showed turns with zero reasoning events where every gap between commands
left the plan all-complete, so the header read 'Thinking completed' while
the agent was still working.

Replace the event-based sealed-thinking release with a presentation-level
hold in changedActivityTaskUpdates: when an update batch would leave no
task in progress mid-turn, present the most recent finished task as still
in_progress. Its true status is emitted with the next activity's batch
(ordered before the new task) or at the final flush, so per-card statuses
are only deferred across the silent gaps. Sealed thinking now completes
truthfully in state and relies on the same hold.

Amp-Thread-ID: https://ampcode.com/threads/T-019ebbcb-9016-753d-85ba-fbbb7e94dbe9
Co-authored-by: Amp <amp@ampcode.com>
Instead of holding the last finished task in_progress through think-gaps
(which made quick commands look long-running), complete tasks truthfully
and open a synthetic title-only 'Thinking' task whenever nothing is in
progress mid-turn. Slack's plan header then reads 'Thinking' during gaps
and the task completes as soon as real activity resumes, or at the final
flush.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant