Repository navigation
tasks: correct work lands your call because the checker's own call is cut at 30s and the window closes before a second model answers #941
Description
What happened
2026-09-11, acceptance drives on the Spark against dev@e67f1827b and
speed/task-start@d2589f21e. Real bin/aforge in tmux, every tier and role in the
profile pinned to deepseek/deepseek-v4-flash-0731 (a reasoning model), a fresh
AFORGE_HOME and a fresh throwaway git fixture per run, approval mode allow.
Work that was complete and correct landed your call. This is the whole of what
the checker did, from the node's own job log (logs/jobs/1.log):
audit: judging a clean restore of what the work wrote, in …/trees/1-check
audit: nothing this work declares or ran is a re-runnable check — judging from reading, within 1m0s
audit: verifying against the acceptance
audit · ls .
audit · bash git status --porcelain --untracked-files=all && git diff --cached --stat
audit · bash wc -l words.md
audit · bash grep -cE '^f[0-9]{2}\.txt .+' words.md
audit · read words.md
… (it read all twenty files, one by one)
audit · bash wc -m words.md
audit: one call ran 30s without answering and was abandoned
audit: no verdict — asking deepseek/deepseek-v4-pro-0813 instead
audit: verifying against the acceptance
audit · bash pwd
audit · bash cat words.md
…
audit: one call ran 29s without answering and was abandoned · the window closed before a second
The node's record (tasks.json) then reads:
state = "unverified"
report = "nobody could check it — one call ran 29s without answering and was abandoned · the window closed before a second\nwords.md written and verified at the root of the working copy, 20 lines …"
merge = "aborted"
Which is drawn to the person as your call · nobody could check it. The work was
right: words.md had all twenty lines, each file name and its secret word, and the
node's own two acceptance commands (wc -l words.md → 20, grep -cE '^f[0-9]{2}\.txt .+'
→ 20) both passed. The checker had already read the diff and all twenty files. What
failed is the checker's own model call, twice, at 30s and 29s.
Five of the thirty-four non-quick landings in the drive ended this way — three on
dev, two on the branch. internal/session/task_audit.go is byte-identical on
both, so this is not a regression; it is a standing defect that a reasoning model
makes ordinary. It is the same family as #886 (correct work lands your call), and
the same shape as #927 on the planner: a fixed window over a prompt that never told
the model how long it had.
The three numbers are not a coincidence:
1m0sisauditReadingDeadline=auditDeadline / 5. It is the window an
audit gets when the work declares no re-runnable check — and this node declared
none, because its two commands were written into the proposal'sacceptanceprose
rather than into itschecksfield. Nine of the ten audits in this drive's job
logs say the same line, so one minute is the common case, not the exception.30sis that window divided byauditCallShare(= 2): the most one call may
hold.29sis the second call's share of what was left. Both were cut mid-thought.
Replication
Deterministic (no model). Stand up a stub endpoint the way
internal/session/reasoning_wire_test.go and cmd/aforge/do_test.go already do,
point AFORGE_BASE_URL at it, and have it answer 200 and stream reasoning
deltas forever on the checker's request — a token a second, no content delta, no
finish — while the worker's own requests answer normally. Run a task to its landing
with no checks declared in the proposal, so the door falls to the reading window.
Config.auditWindow (the seam Agent.auditWindowFor reads, already moved by
internal/session/task_settle_test.go and checkmemo_test.go) shortens the minute
so the whole thing runs in seconds rather than one.
What a developer sees TODAY: two requests go out, each cut at exactly its share of
the window with decode stream: context deadline exceeded, and the node settles
unverified with the report line quoted above. Neither request body carries a
reasoning budget or a ceiling derived from the 30s it actually has — the checker is
handed a stopwatch it was never shown. The work itself is untouched and correct; its
branch is left unmerged (merge = "aborted").
Field (real models). make build, then drive bin/aforge in tmux with
roles.auditor (or every tier) pinned to deepseek/deepseek-v4-flash-0731 and ask
for a task whose proposal states its checks in prose rather than in checks —
"read these twenty files, one quick task each, then write words.md with one line per
file". Needs OPENROUTER_API_KEY; budget five minutes and a few cents. What makes
it fire is a reasoning model on the checker plus a 1m0s window: the two
shares are 30s and 29s, and the model's thinking outlives both. The control is the
same run with roles.auditor pinned to a non-reasoning model, or the same run with
the two commands declared in the proposal's checks field so the window is 5m0s
and each share is 2m30s — both land done.
Owner's forensics, may be gone: the drive's job logs, tasks.json records and
screen captures are on a Spark box under ~/af-ts-X-out/{c1/carryhook-dev, c1/carryhook-branch,c3/fan20-branch,c4/sizing-dev}. Nothing in them is needed —
every line is quoted above and the stub replication stands in for all of it.
Where
- The window and its shares.
auditDeadline,auditCallShare,
auditCallFloorShare,auditPace,newAuditPaceandauditPace.boundin
internal/session/task_audit.go;auditReadingDeadlineandauditDoor.windowin
internal/session/task_checks.go;Agent.auditWindowForin
internal/session/pending.go. - Where the bound becomes a timeout and nothing else.
Agent.auditOnceininternal/session/task_audit.go— search
audit: verifying against the acceptance. The bound reaches the model as
context.WithTimeout(ctx, bound)and as nothing else: no effort word derived from
it, no reasoning budget, nothing in the prompt that says how long the checker has. - How a cut check is scored, and it is BY DESIGN.
noVerdictin the same file
("Every one of them is a REFUSAL to call the work done — nothing merges on a
non-answer — and NONE of them is a refutation"), thencheckerStalled,
checkerWindowClosed,auditVerdict.andTheWindowClosed,secondAuditOutcomeand
Agent.auditNode. An unanswered check isauditUnverified, which is theyour calltier. The partial the checker had already established — it read the diff and
all twenty files, and its own log shows it ran both acceptance commands — is
thrown away with the stream. - The reasoning knob that is never derived from the window.
Agent.newAuditAgent
ininternal/session/task_audit.gosetsEffort: nodeRung/
EffortRole: effort.RoleWorker/DefaultEffort: inherited— a LADDER RUNG, not a
budget sized to 30 seconds.effortRequestForin
internal/provider/effortladder.goturns a rung intoxhighReasoningTokens
(32,000) ormaxReasoningTokens(64,000) andreasoningForin
internal/provider/wire.goputs it on the wire; neither is ever asked what this
call's own ceiling is. This is 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's third bullet, on the checker instead of the
planner. - The role the checker's call runs on, and it is not a judge's.
Agent.laneRoleininternal/session/phasenews.go: the auditor is built with
InTask: true, so its call islane.RoleLeafAttached/lane.RoleLeafUnattended
— a task node doing work — rather thanlane.RoleJudge, whose own doc names "a
gate reading an answer: the route judge, the checkpoint reader, the guardian".
lane.PlanFor(internal/lane/watch.go) therefore builds the plan from a leaf's
Role.Ceiling(10s attached / 30s unattended) andRole.GiveUp(90s / 270s,
internal/lane/roles.go) — both written for a worker, neither of them this
check's 30 seconds. Nothing auxiliary blocks the person: one reading beside the work, never in front of it #871 made exactly this move forcheckpointSketchWindow; the
checker never got it. - Why the window was one minute and not five.
auditDoorForin
internal/session/task_checks.gofillsdoor.checksfrom the verification
contract'schecksfield (TaskNode.verification,runnableChecks) only.
Commands stated in the proposal'sacceptanceprose are not checks, so the door is
empty and the window is the reading deadline. - The unattended road, for completeness.
takenAsItStandsin
internal/session/task_audit.goandAgent.landFinishedin
internal/session/task_run.go: with nobody watching, the same non-answer lands
taken as it stands: …, and the run is unattended.
The fix
A window is not a contract with a reasoning model — it is a stopwatch the checker
was never shown — so the check's budget is derived from the check's own window
(the share auditPace.bound just computed, less head-room for the verdict sentence,
against the lane's measured reasoning rate) and sent on the request the way
internal/provider/effortladder.go already knows how to send one, with the same
figure stated in the auditor's prompt so the bound is in the words as well as on the
wire; the checker's call moves onto lane.RoleJudge so the controller's ceiling and
give-up are a gate's and not a worker's; a call that is cut keeps what it had
established — the files it read, the declared checks it ran and their exit codes —
and the retry is asked only for the word, with thinking switched off, rather than
starting the whole investigation again from nothing; and when the window really does
close, the landing says the check was cut, not that the work needs the person:
your call · the check was cut before it could answer — one call ran 30s without answering and was abandoned · the window closed before a second, with the partial
findings under it, so a person reads a fact about the checker rather than a verdict
about their work. (The one-minute reading window is not itself the bug and is not
widened here; a check with nothing to run genuinely has nothing slow to do. What is
wrong is that nobody told the model so.)
Acceptance
- e2e: through the real door —
go test -tags e2e -run TestTaskStatesE2E -count=1 -timeout 40m ./internal/e2e/— against the forever-thinking stub at
AFORGE_BASE_URLwith a shortConfig.auditWindow: the task landsdone, exit
code 0, the node'stasks.jsonstateis notunverified, and the screen never
drawsnobody could check it. The stub asserts that the checker's request body
carried a reasoning budget no larger than its own share of the window, and that the
call after the cut carried the findings the first call had already made with
thinking switched off. - e2e: the control, same door and same suite: a stub whose checker answers
promptly is checked in one call, with no second ask and no budget-driven change to
the verdict; and a stub that never answers either call still landsyour call,
now readingyour call · the check was cut before it could answer, with the
partial findings on the card — so the honest non-answer survives and is spelled as
a fact about the checker. - Unit: the budget is derived and never a constant — a call given a 30s share
on a lane measured fast is given a larger reasoning budget than the same call on a
lane measured slow, and both leave room for the verdict sentence under the model's
window; and a structural law that every path reaching the auditor's model goes
through the one door that attaches it (ago/astlaw in the shape
internal/iconlawuses, so it is on the pull-request gate the day it lands). - Unit:
Agent.newAuditAgent's call is made onlane.RoleJudge, pinned so that
a later edit toAgent.laneRolecannot quietly put the checker back on a worker's
patience. - The manual page that covers this quotes the new wording
(internal/manual/chat/how-tasks-run.md, which today states
nobody could check it — one call ran 2m30s without answering and was abandoned · the window closed before a second, andinternal/manual/chat/tasks.md's
nobody could check itrow), and the change entry'sinvalidatesnames what people
believed before: that a check that ran out of time is a reason to look at your work,
and that the checker's call is bounded by a window the model is told about.
Sibling: #927 is the same shape on the planner — a reasoning model thinking past
a fixed wall, with everything it thought thrown away and no budget ever put on the
wire. #886 is the same symptom from the other cause (a declared check run against
the wrong copy), and its fix is the worked example of what "correct work must not land
your call" means here.
Drafted with CodeAF · reviewed and owned by the author
Activity
tasks: a check that runs out of time is told its window, keeps what i…
tasks: a check that runs out of time is told its window, keeps what i…
tasks: the checker's window, proved end to end and pinned mid-tool (#941
tasks: a check that runs out of time is told its window, keeps what i…
tasks: the checker's window, proved end to end and pinned mid-tool (#941
tasks: a check that runs out of time is told its window, keeps what i…
tasks: the checker's window, proved end to end and pinned mid-tool (#941
tasks: a check that runs out of time is told its window, keeps what i…
tasks: the checker's window, proved end to end and pinned mid-tool (#941
tasks: a check that runs out of time is told its window, keeps what i…