Grok is fine. opencode's integration with our terminal layer is not. Every failure below is
on our side of the line and fixable without changing opencode.
What happened, 2026-08-29
One workspace, three concurrent builders: spir-146 (claude), air-180 (codex),
bugfix-189 (opencode/grok-4.6). Identical architect messaging to all three.
|
claude |
codex |
opencode |
| Messages delivered on first attempt |
all |
all |
none |
| Longest hold |
— |
— |
12m, never delivered |
| Sessions lost |
0 |
0 |
2 |
| Context recovered on restart |
n/a |
n/a |
none — no conversation resume |
Holds classified no-composer-marker, so the render gate could not bound a composer at all.
Tower log, one instance:
16:22:09 Message held (busy): architect → builder-bugfix-189
16:23:10 [mailbox] ESCALATED (held 60s) — visibility only, not delivered
16:25:10 [mailbox] STARVATION notice (1 held ~180s)
16:25:35 Interrupt (ESC) sent — manual, by the architect
16:25:36 delivered
Consequence, not just annoyance. An 8-minute hold meant PR #192 opened missing a fix that had
been requested before it opened, costing a full extra review round. The same one-line change,
after switching that builder to another harness, was delivered, made and verified in 3 minutes.
Four fixes, ours to make
1. Re-measure the opencode gate profile (most likely root cause)
OPENCODE_PROFILE in gate-profiles.ts anchors on literal box-drawing:
rulePattern: /^\s*╹▀{5,}/,
bodyPattern: /^\s*┃/,
topEdgePattern: /^\s*$/,
minContentRows: 3,
Its comments say these were measured across seven states, so this was right when written. Those
patterns are exact glyphs from a specific opencode release, and opencode has shipped since.
no-composer-marker is precisely what a changed rule glyph or chrome row produces.
Hypothesis, not verified — I have not captured current frames. First task is the capture, not
the patch: dump real frames from a live opencode builder in idle, mid-turn and dialog states and
diff them against these four patterns. If they still match, the cause is elsewhere and this issue
needs re-diagnosing rather than a profile edit.
A profile that drifts silently is the deeper problem: add a test that fails when a captured
fixture stops matching, so the next opencode release breaks CI instead of breaking delivery.
2. Never send Ctrl+C to a harness that exits on it
afx send --interrupt sends Ctrl+C unconditionally. claude and codex treat it as "pause";
opencode treats it as "quit" — verified, session gone 30s after delivery. That is what killed
one of the two sessions, and the operator has no way to know from the flag's name.
Add a per-harness interrupt capability (interruptSignal: 'esc' | 'ctrl-c'). On a harness that
exits, --interrupt should refuse with a named reason or downgrade to ESC — never send a signal
recorded as fatal for that app.
3. Deliver held mail through the filesystem when the screen cannot be read
Today, mail the gate cannot verify waits indefinitely. But a message that cannot be typed can
still be written: drop held messages into .builders/<id>/codev/state/inbox/ and have the
builder protocol prompt check it. Harness-independent, survives restarts, and bypasses screen
recognition entirely — the gate stops being a single point of failure for every instruction.
This is the durable fix. #1 makes opencode work; #3 makes the next unmeasured TUI work.
4. Warm restart for harnesses with no conversation resume
afx spawn <id> --resume on opencode prints "This harness does not support conversation resume;
starting a fresh session." Both times, the replacement had no idea work was already committed, and
only a hand-written architect briefing stopped it starting over.
We cannot give opencode conversation resume. We can stop the restart being a blank slate: the
builder's own narrative log already exists at codev/state/<id>_thread.md. Inject it as opening
context on a no-resume restart, alongside git log for the branch. Turns total context loss into
a warm start.
Suggested order
2 first — smallest, and it is the one that destroyed work. 1 next — the capture is a
diagnostic, cheap, and either confirms drift or redirects this issue. 4 — makes the failure
survivable. 3 — the real fix, and worth doing whether or not 1 succeeds.
What is not the problem
Grok's output. The fix it wrote under all this (PR #192) was approved 3/3 by CMAP and held up on
my own read: an allowlisted scrub, re-applied per worker, with tests that assert their own reach.
The model did its job; it just could not hear us.
Related: #190 (the starvation notice promises a recovery that cannot fire for a busy agent).
Grok is fine.
opencode's integration with our terminal layer is not. Every failure below ison our side of the line and fixable without changing opencode.
What happened, 2026-08-29
One workspace, three concurrent builders:
spir-146(claude),air-180(codex),bugfix-189(opencode/grok-4.6). Identical architect messaging to all three.Holds classified
no-composer-marker, so the render gate could not bound a composer at all.Tower log, one instance:
Consequence, not just annoyance. An 8-minute hold meant PR #192 opened missing a fix that had
been requested before it opened, costing a full extra review round. The same one-line change,
after switching that builder to another harness, was delivered, made and verified in 3 minutes.
Four fixes, ours to make
1. Re-measure the opencode gate profile (most likely root cause)
OPENCODE_PROFILEingate-profiles.tsanchors on literal box-drawing:Its comments say these were measured across seven states, so this was right when written. Those
patterns are exact glyphs from a specific opencode release, and opencode has shipped since.
no-composer-markeris precisely what a changed rule glyph or chrome row produces.Hypothesis, not verified — I have not captured current frames. First task is the capture, not
the patch: dump real frames from a live opencode builder in idle, mid-turn and dialog states and
diff them against these four patterns. If they still match, the cause is elsewhere and this issue
needs re-diagnosing rather than a profile edit.
A profile that drifts silently is the deeper problem: add a test that fails when a captured
fixture stops matching, so the next opencode release breaks CI instead of breaking delivery.
2. Never send Ctrl+C to a harness that exits on it
afx send --interruptsends Ctrl+C unconditionally. claude and codex treat it as "pause";opencode treats it as "quit" — verified, session gone 30s after delivery. That is what killed
one of the two sessions, and the operator has no way to know from the flag's name.
Add a per-harness interrupt capability (
interruptSignal: 'esc' | 'ctrl-c'). On a harness thatexits,
--interruptshould refuse with a named reason or downgrade to ESC — never send a signalrecorded as fatal for that app.
3. Deliver held mail through the filesystem when the screen cannot be read
Today, mail the gate cannot verify waits indefinitely. But a message that cannot be typed can
still be written: drop held messages into
.builders/<id>/codev/state/inbox/and have thebuilder protocol prompt check it. Harness-independent, survives restarts, and bypasses screen
recognition entirely — the gate stops being a single point of failure for every instruction.
This is the durable fix. #1 makes opencode work; #3 makes the next unmeasured TUI work.
4. Warm restart for harnesses with no conversation resume
afx spawn <id> --resumeon opencode prints "This harness does not support conversation resume;starting a fresh session." Both times, the replacement had no idea work was already committed, and
only a hand-written architect briefing stopped it starting over.
We cannot give opencode conversation resume. We can stop the restart being a blank slate: the
builder's own narrative log already exists at
codev/state/<id>_thread.md. Inject it as openingcontext on a no-resume restart, alongside
git logfor the branch. Turns total context loss intoa warm start.
Suggested order
2 first — smallest, and it is the one that destroyed work. 1 next — the capture is a
diagnostic, cheap, and either confirms drift or redirects this issue. 4 — makes the failure
survivable. 3 — the real fix, and worth doing whether or not 1 succeeds.
What is not the problem
Grok's output. The fix it wrote under all this (PR #192) was approved 3/3 by CMAP and held up on
my own read: an allowlisted scrub, re-applied per worker, with tests that assert their own reach.
The model did its job; it just could not hear us.
Related: #190 (the starvation notice promises a recovery that cannot fire for a busy agent).