Skip to content

strip ANSI from headless agent output so a slow run stops looking wedged - #6118

Merged
atomantic merged 1 commit into
mainfrom
cos/task-mtlz1w6b/agent-c98ccfe6
Sep 3, 2026
Merged

strip ANSI from headless agent output so a slow run stops looking wedged#6118
atomantic merged 1 commit into
mainfrom
cos/task-mtlz1w6b/agent-c98ccfe6

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

A public-review Stage 3 agent looked stuck. It wasn't — the OpenCode child was nine minutes into prefilling a large prompt on a local 27B model. The card gave no way to tell, for two reasons, both fixed here.

  • Headless agent output is decolored. opencode run wraps its status line in bare ANSI resets. Those bytes reached output.txt and the live tail verbatim, and the browser drops only the ESC — so the only thing on the card was [stderr] [0m. Both headless spawners now strip per stream via the shared createStreamingAnsiStripper, ahead of the fallback-signal detector and the codex stderr formatter (each matches provider prose that an SGR pair would otherwise split).
  • cos-runner gets the same fix. Its handlers are a twin of spawnDirectly's, so the identical bug reproduced whenever a task was dispatched with useRunner — fixing only one spawner would have left half the dispatch matrix broken.
  • The card explains a missing shell. A public-review stage is forced headless even when configured onto a TUI provider (spawnHeadless = publicReview || !isTui), but agent.metadata is a hand-picked projection that carried no trace of that posture — so the card was indistinguishable from one whose PTY failed to attach. publicReviewPosture is now projected, and the card shows a "No shell" chip explaining it.

Scoped deliberately: an ordinary headless CLI agent gets no chip, and neither does a TUI run still waiting on its session id — every healthy TUI spawn passes through that state, so a chip there would put the "looks wedged" noise straight back.

A stream-json stdout is NDJSON with no raw ESC, so the busiest stdout stream skips the scan. A chunk that decolors to nothing is dropped rather than emitting a blank [stderr] line per progress redraw; a whitespace-only stdout chunk is kept, since a blank line is legitimate formatting when it isn't wearing an [stderr] tag.

The prompt size that made the run slow in the first place is filed separately as #6117.

Test plan

  • server/services/agentCliSpawning.test.js — 5 new behavioral tests against the real handlers: escapes stripped from both streams, a sequence split across two chunks reassembled, a control-only chunk dropped instead of tagged blank, a colors-only chunk still counted as run output (proof of liveness), and a fallback signal detected through embedded color codes.
  • server/cos-runner/index.test.js — 4 parity tests pinning the runner twin (the module binds a socket server at load and can't be imported, matching that file's existing source-inspection convention).
  • client/src/components/cos/tabs/AgentCard.test.jsx — 4 tests: the chip appears with its explanation for a public-review agent, stays absent for an ordinary headless CLI agent and for a TUI run mid-attach, and never displaces the real "Open Shell" link.
  • server/services/agentLifecycle.test.js — the posture is projected onto the agent record.
  • Full server and client suites pass. Reviewed with agy (Gemini 3.8 Flash, medium effort): no findings.

A public-review Stage 3 run reported as stuck. It was not: the OpenCode child
was nine minutes into prefilling a ~100K-token prompt on a local 27B model. The
card gave no way to tell, for two reasons, both fixed here.

`opencode run` wraps its status line in bare SGR resets. Those bytes reached
output.txt and the live tail verbatim, and the browser drops only the ESC — so
the only thing on the card was `[stderr] [0m`. Both headless spawners now
decolor per stream through the shared `createStreamingAnsiStripper`, before the
fallback-signal detector and the codex stderr formatter (each matches provider
prose that an SGR pair would otherwise split). The runner in `cos-runner` is a
twin of `spawnDirectly`'s handlers and had the identical bug, so it gets the
same treatment; a stream-json stdout is NDJSON with no raw ESC and skips the
scan. A chunk that decolors to nothing is dropped instead of emitting a blank
`[stderr]` line per progress redraw.

Second, the card had no "Open Shell" link and said nothing about why. A
public-review stage is forced headless even when configured onto a TUI provider
(`spawnHeadless = publicReview || !isTui`), but `agent.metadata` is a
hand-picked projection that carried no trace of that posture, so the card was
indistinguishable from one whose PTY failed to attach. `publicReviewPosture` is
now projected and the card shows a "No shell" chip explaining it. Scoped to that
case only: an ordinary headless CLI agent gets no chip, and neither does a TUI
run still waiting on its session id — every healthy TUI spawn passes through
that state, so a chip there would put the noise straight back.

The prompt size that made the run slow in the first place is #6117.
@atomantic
atomantic merged commit bf795b5 into main Sep 3, 2026
7 checks passed
@atomantic
atomantic deleted the cos/task-mtlz1w6b/agent-c98ccfe6 branch September 3, 2026 20:57
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