Skip to content

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

Closed

Description

@santoshkumarradha

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, longer commandWall). Change entry: docs/changes/unreleased/940-reasoning-past-the-wall.md.

Still open


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 handed nil.
  • 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 is commandStrikeLimit, 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. DefaultCallWall and
    walled.CompleteWithMessages in internal/provider/pool/wall.go; it is armed
    on the structuring slots at chatClient.WithCallWall / planClient.WithCallWall
    in cmd/aforge/chat.go and cmd/aforge/wake.go. On expiry it returns
    nil, ErrCallWall — search the model stopped answering.
  • Where the partial answer dies. internal/provider/client.go, the streamed
    read's decodeErr branch — search decode stream. The accumulated response
    is written into the call-log row and then return 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, applyBounded and stalledReceipt in
    internal/resident/watchdog_command.go.
  • The open-ended calls. GroundWith in internal/plan/ground.go,
    spineWithProgress in internal/plan/spine.go, FanOutWith in
    internal/plan/fanout.go, Acceptance in internal/plan/accept.go, and the
    compiler's own call in internal/head/compiler.go (search WithCallTag,
    "compile"). Not one of them sets a reasoning knob — grep
    internal/plan/ for WithEffortRung, WithReasoningEffort or
    WithConfiguredEffortRung and it is empty. What they do get is
    Config.Context in internal/config/config.go, which sends
    WithConfiguredReasoningEffort(ctx, c.Reasoning) — and DefaultReasoning is
    provider.EffortNone. So the default planning request carries no effort word
    and no budget.
  • The knob that exists and is never reached from here. effortRequestFor in
    internal/provider/effortladder.go already translates a rung into a
    max_tokens thinking budget (xhighReasoningTokens, maxReasoningTokens),
    and reasoningFor in internal/provider/wire.go is the one place it becomes a
    request field.
  • The measured rate a budget could be derived from. Client.streamPace /
    Client.streamGap / Client.streamWall in internal/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.reviewDivision in
    internal/session/task_divide.go (the sizing review, measured at 219 s on the
    owner's laptop, where any failure is unanswered("unreached: ...")) and
    checkpointSketchWindow in internal/session/checkpoint.go, whose 30-second
    window failed four times a day at 30,001 ms before Nothing auxiliary blocks the person: one reading beside the work, never in front of it #871 moved it onto a
    lane.RoleJudge patience. 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:

  1. 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.go already 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.
  2. 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 becomes ErrCallWall. Today a four-minute burn produces
    output: None and is billed in full.
  3. A prompt that asks for a short answer says how much thinking it is worth.
    groundPrompt already 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 --json door against the forever-thinking
    stub endpoint at AFORGE_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
    reasoning budget, and that after the wall it received a second request on
    the same lineage carrying the reasoning it had already emitted. The log never
    contains I 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
    with stalledReceipt and 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.go goes through the one door that attaches the budget
    — a go/ast law in the shape internal/iconlaw uses, so it is on the
    pull-request gate the day it lands.
  • The manual page that covers planning and aforge do quotes the new wording,
    and the change entry's invalidates names 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

  1. added
    bugSomething the code does that it should not
    sev:seriousWrong or missing behaviour a person meets in ordinary use
    area:providerRouting, lanes, refusals, hedging, what a call costs
    area:headlesscodeaf do and codeaf exec, runs with no screen
    on Sep 11, 2026
  2. AbirAbbas commented on Sep 16, 2026

    @AbirAbbas
    Collaborator

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:headlesscodeaf do and codeaf exec, runs with no screenarea:providerRouting, lanes, refusals, hedging, what a call costsbugSomething the code does that it should notsev:seriousWrong or missing behaviour a person meets in ordinary use

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions