Skip to content

The turn decides, informed; promotion inherits; the ceiling is a runaway net - #923

Merged
santoshkumarradha merged 3 commits into
devfrom
feat/turn-promotes-itself
Sep 12, 2026
Merged

santoshkumarradha merged 3 commits into
devfrom
feat/turn-promotes-itself

Conversation

@santoshkumarradha

@santoshkumarradha santoshkumarradha commented Sep 11, 2026 •

Copy link
Copy Markdown
Member

DO NOT MERGE — the owner runs the test binary first (~/af-promote/bin/aforge, shasum in the lane report).

Owner's ruling, 2026-09-11 14:20 EDT: "we should not force a task via a hard turn count; we need to give the model enough in the prompt to choose; when it chooses it needs to weigh whether it is worth it for the task to re-read everything it has."

What was measured

A one-line read-only design question (conversation 7598fe73c5ce3a9a, 13:59–14:08). The chat read home-panel files for ten rounds in 48 s. The round-10 ceiling fired; a mark reader on a 4k-token account said split; a handoff writer spent 39 s and 41k input drawing a quick node; the worker opened on the raw sentence, four items and a CALLS THAT HAVE ALREADY RUN stub list. Its first six calls re-read the chat's own results (116 KB of stubs), then fourteen more files — 551k input tokens, 24% cached, 393 s, never ticked an item. Asked why, the chat stopped it and answered from its own earlier reading: "I over-read. I have what I need".

Three faults in one road: the decision made by the wrong party (a summary reader, not the model holding the context), on the wrong signal (a round count, which says nothing about progress), and the move throws the context away.

The shape

1. The turn decides, informed. The rungs below the last one (checkpointNotes = checkpointMarks - 1) call no reader and move nothing. They inject ONE note at the turn's next step, through the single ambient door a loop nudge already uses (enqueueAmbientNote) — never message[0], no timer, no model call:

[taking stock] 10 rounds so far, 9 files opened, 121 KB of results in front of
you. Three roads on, and the choice is yours: answer now from what you have;
carry the rest on in a room, with `quick_task` and `inherit` set, which opens on
this transcript as you are holding it; or hand out the parts you have not read
yet. A worker that does not inherit opens on a brief about your work instead of
on your work, and reads it all again. Nothing has been decided for you and
nothing has been moved.

(one line in the transcript; wrapped here, and the three figures are the turn's own.)

2. Promotion inherits. quick_task gains inherit (default false) — a field on #929's quickAsk, admitted through admitQuick, so the tool and the carry-on road ask for a promotion the same way: the worker is a read-only FORK of the caller's transcript, so its first request carries the caller's messages and tool results verbatim (prefix identical, cache hits) plus the brief as its opening message. One door — quickAsk gained a field and admitQuick is still the only way a quick node is born. Schema prose:

Hand it this conversation so it opens on everything you have already read. Off by default, and refused when your context will not fit its window

Bounded by a property, not a number: inheritFits refuses when the caller's context exceeds compactThresholdOf(workerWindow) — the build's own already-derived line for how far a conversation may grow — and the refusal names the road still open (hand out with a brief). The window read is the worker's, not the caller's: a task.model pin or the crew's worker seat hands tasks to a cheaper model every day, so the bound is asked below #929's own resolver, of the model the work will actually leave on (workerModelFor).

3. The carry-on road promotes. When the net fires, the carry-on goes through the SAME admission door as the tool, with the drawn items as its checklist and the context inherited. The brief road survives only where inheritance cannot go (the rung that fired because the context is too big), and there the brief now carries the compiled results themselves under WHAT THIS WORK ALREADY FOUND OUT rather than pointers at calls the worker must run again.

4. The runaway net. readMark survives only as the LAST rung, and no longer asks about rounds. turnHasRunAway reads ONE fact — the turn's context has passed the room it can still work in (compactThresholdOf(a.window)) — and the other roads to the same ceiling were already written and are not repeated in it: looped.go's nudge ceiling (handOverLoopingTurn), turnwall.go's wall (asked ahead of this ladder) and the meter's own disproved completion claim. Each a fact the build already had, with the derivation in prose on the constant. Its reading is a readBeside sidecar that may only INTERRUPT (sidecar_law_test.go reds an awaited reading on a road that carries the turn on). A turn reading ten distinct files in 48 s reaches none of these — pinned by TestATenRoundReadingTurnIsToldAndNotMoved.

5. The prompt, and it comes DOWN. inherit is a verb law now — one schema property, 72 bytes, registered at quick_task (class verb) rather than as a page sentence — and the road law quick_task's description said word for word after beltfacts.go had come to say it is deleted, giving back 145. Measured against dev 84016ad09 in a detached worktree of it: lean 31,341 → 31,267 and fixed 39,073 → 39,000 on a machine with ripgrep; 31,448 → 31,375 and 39,180 → 39,107 on one without, which is the shape CI measures. Budgets 31,500 and 48,000, and the note itself is in neither figure: it rides the event, at the boundary where it matters.

A net that fires once says so. The net measures a condition rather than a boundary, and a full context stays full — and three of handOverRunningTurn's endings LEAVE THE TURN RUNNING (no brief came back, this conversation is holding work of its own, the reader says nothing is left). Without a latch each of those was followed by a fresh mastermind call and an unconditional cutGeneration on every remaining step. checkpointMeter.netFiredAt records the firing and netRested holds the net off for checkpointPrice rounds of real work — ONE predicate for all four endings, in the unit the meter already used for a disproved claim, and pinned by TestADeclinedCeilingDoesNotReadTheWorkAgainEveryRound (3 readings with the latch over the same script that took 6 without).

Deleted

checkpointSplitNote, checkpointDecisionSplit, checkpointSeamMark, quickFromDrawing's bare-spec path, the sketch-split gate on the carry road, the stub-pointer section for an inherited worker, and every manual sentence saying a turn is moved at round ten.

Manual, same PR

chat/tasks.md rewritten — "An answer that runs long is told, and decides for itself", "When a reply is taken out of your hands", and a new section on inherit — plus three probes in chat_test.go (why did it stop reading and start a task, can it keep what it read, why did it not become a task) and the cross-reference in how-tasks-run.md.

Pinned end to end

inherit_test.go drives the real runner both ways. A promoted worker's FIRST REQUEST carries its caller's own tool result verbatim, opens on the caller's page plus the one paragraph that is true of a promotion, and is handed none of the CALLS THAT HAVE ALREADY RUN section. A conversation too big for the worker's window is refused in a result the model reads, and no worker is started.

Proof

make test-laws (21 packages, incl. sidecar_law_test.go, admission_law_test.go, iconlaw), go test ./internal/manual/, the law registry, both prefix budgets, the schema law, go build ./..., go vet, and focused sweeps across the checkpoint / quick / route / carry / handoff / write-seam families.

Review, second pass — the nine findings

Coordinator's six:

  1. Lean prefix over budget. It was, on the base it was really measured against. Cut from the inherit description and the beltfacts.go sentence until the lean prefix is 31,482 of 31,500 on dev 84016ad09; the budget was not touched, and the before/after above is measured on that base.
  2. how-tasks-run.md contradicted tasks.md about which tasks inherit a transcript. Its "what a task can do while it runs" paragraph now says the transcript is inherited only by a promotion — quick_task with inherit, or a reply the ceiling carried on — and that every other task gets one assembled brief.
  3. readTurnFacts walked the whole session. It walks messages[meter.opened:] now, and turnOpenedAt finds the message the person opened THIS turn with (walking back over the tool traffic to the newest person's message, then over a steer that sits on the ask it steered). TestTheNotesFiguresCountThisTurnAndNotTheOnesBeforeIt builds a two-turn transcript with a steer in it and asserts the note counts one file, not three.
  4. promotedFromTurn had lost if read.sketch.handsBack { return nil }. Restored, with TestATurnHoldingCoordinationIsNotPromotedWithItAsAChecklist — which fails without it.
  5. The change entry overstated turnHasRunAway. Corrected: the function reads context; the loop, the wall and the disproved claim reach the same ceiling by roads that were already written.
  6. outOfRoom latched and never cleared. It stays one-way, and the reason is now prose on the field: the only thing that can put a turn back under the line is a compaction, and a folded transcript is exactly what a promotion must not carry.

The checkpoint owner's three:

  • A — the net had no once-only latch. Fixed as described above (netFiredAt / netRested).
  • B — the manual still described the deleted machinery. screen.md's taking stock row, models-and-cost.md's markreader paragraph and role table row, and tasks.md's "looking at the work decides everything else" all say what the code does now. And because [taking stock] is now both a status-line phase word and the model-facing note's lead, screen.md gains its own ## section separating them (a new heading rather than prose added to an existing one — BM25 length normalisation charges the padded section its own probes).
  • C — TestARacedVerdictWithoutBreadthArmsNothingOfItsOwn kept its name and lost its assertions. Replaced by TestARacedVerdictWithoutBreadthWritesNoBreadthOfItsOwn, which asserts what is actually true now: two narrow readings arm no breadth, and either reader saying Wide arms it.

checkpoint.go's own header was three claims stale beside this change (a sketch at every mark, a look that costs a call, "nothing is injected into the turn at any mark") and now says what the file does: the notes tell and ask nothing, the ceiling is a property and not a count, and the one reading that must come back is the sidecar's at the net.

Proof at this head (d84120b30, rebased onto dev 84016ad09)

go build ./..., go vet ./internal/session/, make test-focus PKGS=./internal/session RUN='Prefix|Inherit|Promot|Coordination|LawRegistry', the checkpoint / quick / route / verdict / handoff families, go test ./internal/manual/ (green, TestHeldOutQuestionsReachTheirPage included), make test-focus PKGS=./internal/tui3 RUN='Manual', and make test-laws (25 packages).

Rebased through #929 (the task-start wave), #973 and #975: inherit is a quickAsk field, the carry-on road answers a *quickAsk with inherit: true, and both go through admitQuick. There is one constructor, not two.

Spark, isolated clone ~/af-suite-923 at this head, make pr-ready BASE=84016ad09 behind the box's one-suite lock:

make --no-print-directory test-touched
touched: ./internal/manual ./internal/session
./scripts/one-suite.sh go test -timeout 15m -count=1 -p 1 -skip '^(TestEveryLockInTheGuardedTreeUnlocksFromADefer)$' ./internal/manual ./internal/session
ok  	github.com/Agent-Field/aforge-v2/internal/manual	1.640s
ok  	github.com/Agent-Field/aforge-v2/internal/session	173.261s

Test binary for the owner: ~/af-promote/bin/aforge, shasum e66947bc1473cb29becf763a7e55f33a5869de4e.

The two CI reds, and what they were

The lean prefix cell measures an environment-dependent quantity, and that is a defect of its own. CI read 31,555 where the laptop read 31,484 at the same sha. The cause is grep: bare.grepToolDescription probes for ripgrep once per process (exec.LookPath("rg")) and a machine without it carries the longer fallback description — 134 bytes more on the GitHub runner, which has no rg. Reproduced on the laptop by hiding rg and fd from PATH: the same tree then measures 31,520, matching CI to two bytes. So the honest baseline for this gate is the no-ripgrep one, dev sits at 31,448 there, and this PR now sits at 31,375 — 73 bytes below dev, in the environment the gate actually runs in. Filed as #981 rather than patched here: a budget whose reading moves with what is installed on the runner cannot be reasoned about by the lane that trips it.

TestAContinuationSayingNothingIsLeftDropsTheCeilingHandover was this PR's own fixture, and the mechanism it exposed is a real boundary. The turn answers the handover with NOTHING LEFT TO DO; checkpointMeter.believeDone charges that claim once and arms askAgainAt = rounds + checkpointPrice, so ten more rounds of REAL work disprove it and the turn is met again — pre-existing law, pinned by TestTheRunningModelsSayS0IsBelievedOnceAndThenMet. This PR widened checkpointSlack from checkpointMarks + 4 (7) to checkpointPrice + checkpointMarks (13) so scripts could outlast the sidecar's gap, and 13 tail steps straddle that price: how many of them become rounds of work depends on how many the reading eats, which is why it failed 13/30 on the Spark and 0/30 here. The fixture was asserting the scheduler.

The fix is in two parts, neither of them a rerun:

  • checkpointClaimSlack = checkpointPrice - 1 is the slack for the three fixtures whose turn CLAIMS IT IS FINISHED and carries on (checkpoint_test.go ×2, unattendeddoor_test.go). By construction they cannot reach the claim rung, so each tests the ending it is named after and nothing else. The constant says why in its own doc.
  • TestABelievedClaimIsMetAgainOnlyAfterAPriceOfRealRounds pins the boundary on the meter, with no scheduler in it: every round of the quiet rings nothing and leaves the net unrested, the round that completes the price rings checkpointMarks through the meter rather than the net, the claim is not believed a second time, and a batch that only watched work already out buys none of the price. It fails on a broken latch (netRested forced true: "the net was rested 1 rounds after it fired, 9 short of its price").

Spark at this head: go test -count=30 -run '^TestAContinuationSayingNothingIsLeftDropsTheCeilingHandover$' → 30 RUN, 30 PASS, 0 FAIL.

Folded in from the peer's follow-ups

  • netFiredAt was a zero-value sentinel and round zero is a real round (a turn whose opening boundaries all watch never leaves round zero). The latch is now two fields for two facts, netFired bool and netFiredAt int.
  • checkpoint.go's new header said "three times the handoff price" where checkpointMarkAt(2) is checkpointRatio times it. The figure is interpolated from the constant, not restated.
  • newQuickTaskSpec was built twice on the inherit path. The branch is hoisted: a promotion is built INSTEAD of the plain node, never after it.
  • An empty checklist can reach a promotion, and now the comment makes the argument the deleted gate's comment used to make against it: a promoted worker holds the whole transcript, so the checklist is a convenience on top of the work rather than the only description of it.
  • screen.md listed three firing conditions and there are four. The believed claim disproved by ten more rounds of real work is named in both places the page describes the moment.

The rest of the review — the two windows behind one line (TrustedWindowFor), the transcript walked twice a boundary, the unpinned double-composed page, awaitReading's silent sleep-poll, and checkpointMarkAt(checkpointMarks) being production-dead — are filed together as #979, with the reviewer's own wording and an acceptance line each.

🤖 Generated with Claude Code

santoshkumarradha and others added 2 commits September 12, 2026 04:31
…way net

A one-line read-only design question read home-panel files for ten rounds in
48 seconds and was taken away from the conversation that was answering it. The
round-10 ceiling fired, a summary reader said `split`, and a cold worker opened
on a brief plus stub pointers: its first six calls re-read the chat's own
results, then fourteen more files, 551k input tokens at 24% cached over 393
seconds, and it never ticked an item. Three things were wrong at once. The
decision was made by the wrong party — a reader of a summary rather than the
model holding the context. It was made on the wrong signal — a count of rounds,
which measures nothing about whether the work is going anywhere. And the move
threw away everything the turn had read.

So the count no longer moves anything, and what it buys instead is a sentence.

THE TURN DECIDES, INFORMED. The rungs below the last one call nobody and move
nothing. They put one note into the running turn at its next step, through the
one ambient door a loop nudge already uses: the facts it cannot see about itself
(rounds so far, distinct files read, bytes of results held) and the three roads
on, in plain words — answer now; carry the rest on in a room with `quick_task`
and `inherit`; hand out what it has not read. The cost is stated once, where the
choice is: a worker that does not inherit opens on a brief about your work
instead of on your work, and reads it all again. No timer, no model call, no
message[0].

PROMOTION INHERITS. `quick_task` gains `inherit`: the worker is a read-only fork
of the caller's transcript, so its first request carries the caller's messages
and tool results verbatim and it reads nothing twice. It is bounded by a
property rather than a number — refused, in a result the model reads, when the
caller's context does not fit the worker model's own working room
(`compactThresholdOf`, the build's own line for how far a conversation may
grow), and the refusal names the road still open.

THE RUNAWAY NET. `readMark` survives as the last rung only, and it no longer
asks about rounds: it fires when the turn's context has passed the room it can
still work in, when the loop detector has already nudged twice, or when the wall
has run out. Each of those is a fact the build already had. A turn reading ten
files in 48 seconds reaches none of them.

AND THE CARRY-ON ROAD PROMOTES. When the net does fire, the carry-on goes
through the same admission door as the tool, with the drawn items as its
checklist and the context inherited. The brief road survives only where
inheritance cannot go — the rung that fired because the context is too big to
carry — and there the brief now carries the compiled results themselves rather
than pointers at calls the worker would have to run again.

Deleted with it: `checkpointSplitNote` and the `split` decision, the seam mark,
`quickFromDrawing`'s bare-spec path, and the stub-pointer section for an
inherited worker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t mark

#962's fixture holds a drawing past the turn's last step, and it opened that
window by crossing the FIRST mark. With the rungs below the last one buying
nothing, the first mark asks nobody, so the fixture now takes the turn to THE
RUNAWAY NET instead: every scripted round reports a fuller window
([checkpointFillAt]) until the context bound fires, and the note rungs' own
rows are dropped from what it returns, because a row that cost no call is not a
row about a drawing landing late.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…asker's words in its heading

The probe "the run said the brief could not be written, what happened" reached
adaptive-runs in the last of four slots on dev and lost that slot once this
change's task pages said more about briefs. Retrieval is the feature, so the
heading now says it the way a person asks it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@santoshkumarradha
santoshkumarradha merged commit 3143fef into dev Sep 12, 2026
3 checks passed
@santoshkumarradha
santoshkumarradha deleted the feat/turn-promotes-itself branch September 12, 2026 13:27
santoshkumarradha added a commit that referenced this pull request Sep 12, 2026
The rebase onto dev, resolved against #923. Two mechanisms were about to
exist for one shape and now there is one: the ceiling's carry-on rung reads
[Agent.contextRoomFor] — the net's own arithmetic — rather than a second
opinion about how full a window is, and [checkpointMeter.compacted] states
why it does not clear the net's [checkpointMeter.outOfRoom] latch: a turn
that bought room by folding is a turn whose transcript is a summary of its
results, which is exactly the transcript a promotion must not carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
santoshkumarradha added a commit that referenced this pull request Sep 12, 2026
The rebase onto dev, resolved against #923. Two mechanisms were about to
exist for one shape and now there is one: the ceiling's carry-on rung reads
[Agent.contextRoomFor] — the net's own arithmetic — rather than a second
opinion about how full a window is, and [checkpointMeter.compacted] states
why it does not clear the net's [checkpointMeter.outOfRoom] latch: a turn
that bought room by folding is a turn whose transcript is a summary of its
results, which is exactly the transcript a promotion must not carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
santoshkumarradha added a commit that referenced this pull request Sep 12, 2026
The page already said a long answer is not moved for being long; what it did
not say is that the fold runs first and hands the reply back, and that the
counting restarts from the round the room was bought on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
santoshkumarradha added a commit that referenced this pull request Sep 12, 2026
…g is deleted

#923 took the count out of the decision — the ladder climbs no further than
the two notes, and the runaway net is what moves a running turn now. That
left one thing standing and it was an ORDER, not a mechanism: the net reads a
quantity this loop reduces on its own, every step, for free, and the reading
stood one statement ABOVE the reduction.

So two statements moved, and the compact-and-continue rung this lane wrote
against the old shape is deleted whole — checkpointCanCarryOn, the meter's
boughtAt offset and compacted(), the "compacted" journal word, the ceiling's
carry-on branch, the looped.go bypass, the sidecar_law endingDoors edit and
the two test-fixture worlds. Two mechanisms for one shape is the thing the
bar forbids. What guards the rule is foldbeforeprice_law_test.go, which reads
loop.go's own tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
santoshkumarradha added a commit that referenced this pull request Sep 12, 2026
The rebase onto dev, resolved against #923. Two mechanisms were about to
exist for one shape and now there is one: the ceiling's carry-on rung reads
[Agent.contextRoomFor] — the net's own arithmetic — rather than a second
opinion about how full a window is, and [checkpointMeter.compacted] states
why it does not clear the net's [checkpointMeter.outOfRoom] latch: a turn
that bought room by folding is a turn whose transcript is a summary of its
results, which is exactly the transcript a promotion must not carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
santoshkumarradha added a commit that referenced this pull request Sep 12, 2026
The page already said a long answer is not moved for being long; what it did
not say is that the fold runs first and hands the reply back, and that the
counting restarts from the round the room was bought on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
santoshkumarradha added a commit that referenced this pull request Sep 12, 2026
…g is deleted

#923 took the count out of the decision — the ladder climbs no further than
the two notes, and the runaway net is what moves a running turn now. That
left one thing standing and it was an ORDER, not a mechanism: the net reads a
quantity this loop reduces on its own, every step, for free, and the reading
stood one statement ABOVE the reduction.

So two statements moved, and the compact-and-continue rung this lane wrote
against the old shape is deleted whole — checkpointCanCarryOn, the meter's
boughtAt offset and compacted(), the "compacted" journal word, the ceiling's
carry-on branch, the looped.go bypass, the sidecar_law endingDoors edit and
the two test-fixture worlds. Two mechanisms for one shape is the thing the
bar forbids. What guards the rule is foldbeforeprice_law_test.go, which reads
loop.go's own tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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