Repository navigation
a reasoning model thinks past the wall and everything it thought is thrown away: 'I couldn't get this planned — the model stopped answering twice' #927
Description
Status (checked 2026-09-12)
Landed
- Thinking wall + salvage + honest wording: PR #940 (
provider.WithThinkingWall, wall salvage ask-for-answer,pool.CauseInWords/the model thought past its time, longercommandWall). Change entry:docs/changes/unreleased/940-reasoning-past-the-wall.md.
Still open
- Fix feat: ship canonical headless harness contract #3 from this issue: grounding / spine / fan-out prompts state how much thinking the question is worth (prompt-depth). Named as still owed in the A reasoning model that thinks past the wall keeps what it thought (#927) #940 PR body and change entry.
What happened
aforge do on a long issue body, 2026-09-11 15:26–15:47 EDT, Spark. The tree it
ran on carries internal/provider/pool/wall.go, internal/resident/watchdog_command.go
and all of internal/plan/ unchanged from dev@e67f1827b.
aforge do "<the text of a go/ast law issue — a long body>" \
--model z-ai/glm-5.3 --timeout 60m
Twenty minutes, fifteen model calls, about $0.76 of recorded spend plus two
four-minute burns whose rows could not price them. What the person got:
I couldn't get this planned — the model stopped answering twice. Say 'try again' to requeue it.
The machines were fine. Every call's TTFT was 227–717 ms across three lanes
(Friendli, AkashML, Wafer). The model was fine too — it was thinking, hard, and
it was cut off mid-thought and everything it had thought was thrown away. From
the run's own call log:
15:26:40 compile Friendli ttft 554ms 225072ms finish stop reasoning 47242 $0.195
15:30:25 ground Friendli ttft 481ms 240000ms error "decode stream: context deadline exceeded"
15:34:28 ground Wafer ttft 284ms 2293ms finish stop reasoning 16 $0.006
15:35:18 spine Friendli ttft 327ms 52974ms finish stop reasoning 10936
15:35:57 spine AkashML ttft 580ms 91986ms finish stop reasoning 15109
15:36:40 spine AkashML ttft 546ms 134875ms error "decode stream: context deadline exceeded"
15:36:41 compile Friendli ttft 697ms 160385ms finish stop reasoning 38478 $0.160
15:39:21 ground Friendli ttft 227ms 240000ms error "decode stream: context deadline exceeded"
15:43:30 ground Friendli ttft 297ms 8492ms finish stop reasoning 1088
15:44:48 spine Friendli ttft 531ms 86635ms finish stop reasoning 16202
15:46:00 spine Friendli ttft 287ms 159041ms finish stop reasoning 30775
15:46:06 spine AkashML ttft 717ms 164663ms finish stop reasoning 24054
15:46:15 fanout AkashML ttft 388ms 26011ms error "decode stream: context deadline exceeded"
15:46:15 fanout AkashML ttft 504ms 26011ms error "decode stream: context deadline exceeded"
Three numbers in that log are not coincidences, and together they are the whole bug:
- 240000ms, twice. That is
pool.DefaultCallWall— four minutes — to the
millisecond. The grounding call thought for four minutes and was cut with
output: None. Nothing was salvaged: the partial stream is dropped on the
floor and the caller is handednil. - 134875ms and 26011ms. Both land exactly ten minutes after their command
started (15:26:40 → 15:36:40, 15:36:41 → 15:46:41). That is
resident.commandWall. Two strikes iscommandStrikeLimit, and the second
one is the sentence the person read. - 47242 and 38478 reasoning tokens on
compile, for a compiled goal of a
few dozen visible tokens. Nothing on the wire ever told the model how long it
was allowed to think, because no planning call sends a reasoning budget or an
effort word at all.
The control is in the same log. At 15:34:28 the same grounding prompt, on
the same model, on a second lane, finished in 2,293 ms with 16 reasoning
tokens. This is not a slow lane and not a slow model — it is an open-ended
prompt against a fixed wall, and which side of the wall the model lands on is
luck. Five other glm-5.3 lanes with shorter briefs the same afternoon planned
without trouble, so the length of the brief is what pushes the thinking past
four minutes.
The comment on commandWall says ten minutes is "roughly twice the worst honest
case and nowhere near any observed one". This run cleared it twice in a row on a
request nobody would call dishonest.
Replication
Deterministic (no model). Stand up a stub OpenRouter endpoint, point
AFORGE_BASE_URL at it, and have it answer 200 and stream reasoning deltas
forever — a token every second, no content delta, no finish. Run
aforge do "<any request>" --timeout 60m against it. Both walls are settable for
a test, so this runs in seconds rather than twenty minutes:
pool.Client.WithCallWall (internal/provider/pool/wall.go) and the
commandWall field on resident.Reconciler (internal/resident/resident.go,
the constant is in internal/resident/watchdog_command.go).
What a developer sees TODAY: the structuring calls end at exactly the call wall
with decode stream: context deadline exceeded and no completion tokens on
the row — every reasoning token the stub sent is discarded, because
internal/provider/client.go returns nil, false, decodeErr and never offers
the accumulated response to anyone. The command wall then strikes twice and
the run prints stalledReceipt verbatim. At no point is the stub asked to stop
thinking and answer, and at no point was it told a budget: a --json receipt
and the call log both show a request body with no reasoning object on it.
Field (real models). The command above with --model z-ai/glm-5.3 and a
long request body — the text of a real issue, not a sentence. Needs
OPENROUTER_API_KEY; budget twenty minutes and about a dollar. What makes it
fire is brief length: the same model with a one-paragraph brief plans fine, and
the second lane's 2,293 ms grounding above is the control inside the failing run
itself.
Owner's forensics, may be gone: the run's private store and call log were kept
on a Spark box (AFORGE_HOME=/tmp/afl898, run aforge-do-976678615). Nothing in
them is needed — the rows are quoted above and the stub replication stands in
for all of it.
Where
- The four-minute wall and what it throws away.
DefaultCallWalland
walled.CompleteWithMessagesininternal/provider/pool/wall.go; it is armed
on the structuring slots atchatClient.WithCallWall/planClient.WithCallWall
incmd/aforge/chat.goandcmd/aforge/wake.go. On expiry it returns
nil, ErrCallWall— searchthe model stopped answering. - Where the partial answer dies.
internal/provider/client.go, the streamed
read'sdecodeErrbranch — searchdecode stream. The accumulatedresponse
is written into the call-log row and thenreturn nil, false, decodeErr
discards it. There is no salvage on this path and no second ask. - The ten-minute wall and the sentence.
commandWall,commandStrikeLimit,
stalled,applyBoundedandstalledReceiptin
internal/resident/watchdog_command.go. - The open-ended calls.
GroundWithininternal/plan/ground.go,
spineWithProgressininternal/plan/spine.go,FanOutWithin
internal/plan/fanout.go,Acceptanceininternal/plan/accept.go, and the
compiler's own call ininternal/head/compiler.go(searchWithCallTag,
"compile"). Not one of them sets a reasoning knob — grep
internal/plan/forWithEffortRung,WithReasoningEffortor
WithConfiguredEffortRungand it is empty. What they do get is
Config.Contextininternal/config/config.go, which sends
WithConfiguredReasoningEffort(ctx, c.Reasoning)— andDefaultReasoningis
provider.EffortNone. So the default planning request carries no effort word
and no budget. - The knob that exists and is never reached from here.
effortRequestForin
internal/provider/effortladder.goalready translates a rung into a
max_tokensthinking budget (xhighReasoningTokens,maxReasoningTokens),
andreasoningForininternal/provider/wire.gois the one place it becomes a
request field. - The measured rate a budget could be derived from.
Client.streamPace/
Client.streamGap/Client.streamWallininternal/provider/velocity.go,
already fed per model-and-lane. The run above measured ~210 tok/s of reasoning
on Friendli before the wall cut it. - The same family on the chat side.
Agent.reviewDivisionin
internal/session/task_divide.go(the sizing review, measured at 219 s on the
owner's laptop, where any failure isunanswered("unreached: ...")) and
checkpointSketchWindowininternal/session/checkpoint.go, whose 30-second
window failed four times a day at30,001 msbefore Nothing auxiliary blocks the person: one reading beside the work, never in front of it #871 moved it onto a
lane.RoleJudgepatience. Both are the same shape: a fixed window over a
prompt that never told the model how long it had.
The fix
A ceiling on its own is not a contract with a reasoning model — it is a stopwatch
the model was never shown. Three things, and they are one change:
- Every planning call tells the model how long it may think. A reasoning
budget is derived where the ceiling is known and the rate is measured: the
call's own wall, the lane's measured reasoning rate
(internal/provider/velocity.go), and enough head-room left for the answer.
internal/provider/effortladder.goalready knows how to put a budget on the
wire; what is missing is that the planning path never asks for one. A model
given four minutes and 210 tok/s should be told it has room for roughly forty
thousand reasoning tokens, not left to discover the wall by hitting it. - A wall asks for the answer, it does not throw the thinking away. When a
structuring call reaches its wall with reasoning on the wire and no answer,
the reasoned-so-far is kept and the model is asked once more for the answer it
has already worked out — a short second ask, on the same lane, with the
thinking it just did in front of it and thinking switched off. Only a call
that fails that too becomesErrCallWall. Today a four-minute burn produces
output: Noneand is billed in full. - A prompt that asks for a short answer says how much thinking it is worth.
groundPromptalready bounds its answer — "At most six settled points",
"one line each" — and then the same call spends four minutes and 47k tokens
reaching it. The sizing, grounding, spine and fan-out prompts state the depth
the question warrants in the same sentence that states the shape of the answer,
so the bound is in the prompt as well as on the wire.
And commandWall's comment is no longer true: whatever number it lands on after
this, it is written from what has been measured on reasoning models rather than
from "twice the worst honest case".
Acceptance
- e2e: through the real
aforge do --jsondoor against the forever-thinking
stub endpoint atAFORGE_BASE_URL, with a short call wall and command wall set
for the test: the run plans, exit code 0, and the receipt's plan is
non-empty. The stub asserts that the request body it received carried a
reasoningbudget, and that after the wall it received a second request on
the same lineage carrying the reasoning it had already emitted. The log never
containsI couldn't get this planned — the model stopped answering twice. - e2e: the control, same door: a stub that answers promptly plans exactly as
it does today — one request, no second ask, no budget-driven change to the
plan — and a stub that thinks forever and refuses the second ask still ends
withstalledReceiptand a non-zero exit, so the honest failure survives. - Unit: the budget is derived, not a constant — a test pins that a call
with a four-minute wall on a lane measured at 210 tok/s is given a larger
budget than the same call on a lane measured at 20 tok/s, and that both leave
room for the answer under the model's window. - Unit (structural): every call made from
internal/plan/and
internal/head/compiler.gogoes through the one door that attaches the budget
— ago/astlaw in the shapeinternal/iconlawuses, so it is on the
pull-request gate the day it lands. - The manual page that covers planning and
aforge doquotes the new wording,
and the change entry'sinvalidatesnames what people believed before: that a
planning call with no effort configured sends no reasoning knob and that a call
that outlives its wall is simply lost.
Drafted with CodeAF · reviewed and owned by the author
Activity
A reasoning model that thinks past the wall keeps what it thought (#927)
A reasoning model that thinks past the wall keeps what it thought (#927)
The wall told is the wall that binds, and the door proves it (#927 re…
A reasoning model that thinks past the wall keeps what it thought (#927)
A reasoning model that thinks past the wall keeps what it thought (#927)
The wall told is the wall that binds, and the door proves it (#927 re…
A thought is kept whichever clock cut it, and the cause is said once (#…
A reasoning model that thinks past the wall keeps what it thought (#927)
The wall told is the wall that binds, and the door proves it (#927 re…
A thought is kept whichever clock cut it, and the cause is said once (#…
A reasoning model that thinks past the wall keeps what it thought (#927…
AbirAbbas commentedon Sep 16, 2026
Closing as completed. #940 fixed what a person felt: the reasoning budget is derived from the wall, a model that thinks past it is asked to salvage rather than thrown away, the command wall is bounded, and the "stopped answering twice" wording is gone. The one item left, a sentence in the grounding, spine, sizing and fan-out prompts saying how much reasoning the short answer warrants, is prompt tuning, and #940's body says so. Reopen if a planning call still loses everything it thought.