fix(slack): drop working pill on codex out-of-credits warning#82
Conversation
When codex returns an out-of-credits sentinel (task_complete with last_agent_message=null + has_credits=false), the bridge correctly surfaces the warning at the channel root — but then immediately sets the WORKING pill on it. The pill refresh loop re-applies every 30s, so the channel keeps showing "is working…" against a state that's already finished. Operators see a stuck "Processing…" indicator that contradicts the warning right above it. Add a `terminal?: boolean` flag on SlackPost. Set it on the out-of-credits post in format.ts. In bridge.ts, when a `kind: "text"` post arrives with `terminal: true`: still clear the previous anchor's pill, still post the text as the new root, but skip the setStatus(WORKING_LABEL) call — no work is happening, no pill should advertise that it is. Could be extended to normal codex task_complete later (which also leaves a stale pill on the last agent_message anchor), but keeping the scope to the regression I can see in the screenshot for now. Claude-runtime parity is separate — Claude has its own end-of-turn signaling.
|
Extended in 048e59d — same The earlier commit only marked codex's out-of-credits sentinel; a screenshot caught the same UX bug on the Claude side after a normal end-of-turn: pill stays lit on the final answer, refresh loop keeps re-applying it, and on the next user message Slack overlays its default localized "Finding answers..." indicator on top. Changes:
One acknowledged gap: codex normal completion ( |
…turn) After PR #82 only the codex out-of-credits sentinel set `terminal: true`, so the bridge always re-attached the "is working…" pill after every non-terminal text post. For Claude that meant the pill stayed up forever after the agent posted its final message and ended the turn — exactly what you see in the screenshot where the agent's review is posted but the channel still shows a pill below. When a transcript-line message carries `stop_reason: "end_turn"` (or `stop_sequence` / `refusal`), find the last text post we just emitted from that message and mark it terminal. The bridge already honours that flag by skipping the pill re-attach. `tool_use` / `max_tokens` / `pause_turn` stay non-terminal — the agent is still working. If a turn ends with no text post (e.g. the last block was a tool call), we leave the pill alone — that's the existing behaviour; rare in practice, and dropping a pill mid-tool would be more confusing than the two-minute Slack TTL it relies on otherwise.
Summary
When codex hits its credit ceiling, the rollout emits `task_complete` with `last_agent_message=null` + `has_credits=false`. The bridge already surfaces a clear warning at the channel root:
But then it immediately sets the "is working…" pill on that anchor. The refresh loop re-applies every 30s, so the channel keeps showing a working indicator against a state that's already finished. Operators get a mixed signal — warning above, "Processing…" below.
This PR adds a `terminal?: boolean` flag on `SlackPost` and sets it on the out-of-credits emission. In `bridge.ts`, when a `kind: "text"` post arrives with `terminal: true`, the previous anchor's pill still gets cleared and the warning still posts as the new root — but the `setStatus(WORKING_LABEL)` call is skipped, so nothing claims work is in progress.
Why a flag, not a text match
Coupling the bridge to specific warning text would be fragile (the wording lives in `format.ts`, the pill logic lives in `bridge.ts`, and we'd silently drift). A flag on the post shape makes the intent explicit at the emission site and gives us a clean extension point for any future terminal sentinels (normal task_complete with output, Claude end-of-turn, etc.).
Test plan