Skip to content

session: the turn seals when the answer is done, and the route judge is spent when it lands (review needed) - #1011

Closed
santoshkumarradha wants to merge 35 commits into
devfrom
simplify-D
Closed

santoshkumarradha wants to merge 35 commits into
devfrom
simplify-D

Conversation

@santoshkumarradha

Copy link
Copy Markdown
Member

Lane D of the simplify wave: two behaviour changes, both about work that a
person was made to wait through for no reason they could see.

Stacked on #996 (fix/auxiliary-inventory), which is the bug-fix half of
the same lane; this PR's base is that branch, so the diff here is lane D's
behaviour changes alone. Merge #996 first and this rebases to nothing.

1. The judge no longer holds the seal

The end of a turn waited for BOTH end-of-turn readings. One of them — the
work-or-words judge — stood between the model's last word and the seal, blocking
on a cheap screen plus, on a yes, a mastermind confirm. The answer was already
fully drawn on the person's screen. Parked behind that wait were the seal, the
usage row, the next Submit and the follow-up drain.

The turn's job was never to know whether the ruling had arrived. It was to say
when one may be spent, which it still says, and then walk away:
judgeRace.takeAtTheEnd (which blocked) is judgeRace.spendWhenItLands (which
does not). A yes that lands after the turn ended starts its task then.

That needed a lifetime for a reading that outlives the turn that bought it —
afterTurn in sidecar.go — because the judge's whole effect is to start a
task, which lands outside the turn anyway. A reading that refuses to be taken
still releases what was held for it, and a reading beside a turn stops writing
into a conversation that has closed.

The status line's whether that should be work is deleted, because the wait it
named no longer exists. Only the reader that can RE-OPEN the turn is still
waited for.

2. reaskMs — the recall's re-ask is priced

This is an instrument, not a fix, and it is here because the ≤50 ms law can be
satisfied by a request that is then thrown away. On a turn where the person
waited 7,389 ms for their first word, sendMs read 1. The recall had
cut its own request and asked again, and nothing measured the gap.

journalPace now carries sealMs (the model's last word to the seal) and
reaskMs (how long the request a recall threw away was on the wire), in place
of sendMs/firstWordMs/stepGapMs. The share of turns that pay for a
cut-and-re-ask still cannot be derived from one ledger row — that is stated in
the change entry rather than claimed as closed.

The law

loop_speed_test.go asserted two figures; it asserts three, and the third is
the one that matters here: the seal must not be held by any reading whose
answer could be spent afterwards.
afterturn_law_test.go is new and on the
laws gate.

Proof

  • go build ./..., go vet ./..., gofmt clean.
  • Laws, session, tui3, config, roles, manual, replay and cmd suites on the
    Spark against this exact head (tail in a comment below).

🤖 Generated with Claude Code

santoshkumarradha and others added 30 commits September 12, 2026 10:07
…ed a model since the summariser went

RoleCompaction was a priced, described, settings-visible dial wired to nothing.
[Agent.compact] takes `_ context.Context` and makes no model call at all — a
pass is two mechanical rearrangements of text the session already has — so the
row on the Providers tab offered a person a model for a call that never
happens, and TierHigh's patience was derived from a summary nobody writes.

The role, its tier assignment, its description and the settings and manual text
that offered it all go. The high tier keeps every tenant it actually has —
the auditor, the shaper, vision, the repair, the careful part of a division —
each registered from the package that makes the call.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…thing that spells it

NINE OF THE ELEVEN CALLERS OF THIS PACKAGE'S MODEL DOOR REACHED THE WIRE
ANONYMOUSLY. The guardian, vision, the shaper, the spell-out, the subharness
intake, the run planner, the harness designer, the ceiling's handoff draft and a
saved program's own AI step all called completeWithModel with no
provider.WithCallTag anywhere above them, so the model-call log could not say
what any of them was: 2,309 of the 2,839 untagged finishes in the ten days to
2026-09-10 are this package's, and with them the answer to "what was this build
spending that model on all night".

The tag was a context value a caller could set or forget, and forgetting it is
silent. It is an argument now — clientdoor.go's `callPurpose` — so a call with
no purpose does not compile, and the door is the one thing in the package that
calls provider.WithCallTag. The three callers that did stamp their own (the
turn, the ask tool, the errand ladder) pass a purpose instead, which is one
spelling of one fact where there were four.

AND auxiliary.go's OWN CLAIM WAS THE FALSE ONE. It said callRole was "the one
door all auxiliary provider calls pass through" while ten calls went round it.
It is the one door every ERRAND passes through, which is a narrower and true
claim, and the sentence says so and names what is legitimately not an errand —
vision streams into the room in the person's own turn, the handoff draft is
billed to the turn on the turn's own model, a program's step runs on no role's
model at all.

nohiddenwork_test.go is the law, on the laws gate: a blank purpose fails the
build, a WithCallTag outside the door fails the build, and the walk fails if it
ever stops finding the door.

No behaviour changes. Every call goes to the same model with the same options;
what is new is that its row in the log has a name on it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…carries

THE GATE WAS POINTED AT A SHAPE NOBODY RUNS. fixedPrefixBudget's own doc comment
has always said it bounds "the belt the shipping conversation door assembles".
The fixture under it was v3ShapedAgent: a conversation with no memory store, no
accounts hub, no standing items and no saved programs — eighteen tools, 39,073
bytes, eight kilobytes of headroom, green every day.

A machine somebody has finished setting up carries twenty-three tools and weighs
53,025 bytes, which is 5,025 OVER the 48,000 cap the gate was enforcing. It has
been over for as long as the shape existed and the gate could not see it, which
is #576's shape exactly.

The gate now weighs beltShapes[0] — "the shipping conversation, fully wired",
the same definition prompt_belt_test.go uses, through one new helper so the two
cannot drift apart again.

AND THE FIGURE BECOMES A RATCHET. "The measurement plus a tenth" was right when
the number came out of a diet that had just been paid for. This measurement is
not the end of a diet; it is the first honest weighing of a prefix that had
never been weighed, already past what the last diet aimed at, so a tenth on top
would be five more kilobytes nobody chose. It is the measurement and nothing
else, and anything that grows the shipped prefix now fails the build.

WHAT IS OWED IS FILED, NOT FIXED. 48,000 stays as the stated target and the test
prints the 5,025 shortfall on every green run. The bill is one tool: `stand` is
9,607 bytes, more than a quarter of the whole tool block and nearly twice the
next heaviest. Trimming a schema is a change to what the model is told, not to a
byte count, so it belongs to whoever owns the standing belt rather than here.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…s stated now

TestEveryTagTheBuildWritesResolvesToARoleSomebodyDeclared walks the tree for
provider.WithCallTag. internal/session has exactly one of those now — the relay
line inside the door — so the law was reading a variable it could not resolve
and finding fourteen tags for a table of thirty-seven.

It reads the door's own call sites instead, which is the same thing the build
does: the purpose argument is what reaches calllog.Record.Tag. The
`callPurpose(...)` conversion comes off before the argument is read, a role worn
as a purpose resolves to internal/roles' vocabulary (over-approximated, because
this law is safe answering with more tags than one site writes and holed
answering with fewer), and the door's own constants are read from their
declaration the way roleWords already reads internal/roles.

AND IT IMMEDIATELY FOUND THE TWO CALLS THAT HAD JUST BEEN GIVEN NAMES. The
ceiling's handoff draft and a saved program's AI step now write tags, and
nothing priced them: both are lane.RoleAuxiliary and both have a row. The draft
rung is the dearest side-call on the ceiling road and it has been invisible to
every reading of calls.jsonl until now.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…isters

The models-and-cost page's roles table is described on the page itself as
'built from what is registered in the running binary, so it is the truth about
this build'. It listed `compaction`, which nothing registers any more, and the
crew's careful line and its account of what aforge calls on your behalf both
named the summary a compaction keeps — a call that has not existed since the
summariser was deleted.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…ritten on

The first spelling of the ratchet was 53,100 — the laptop that weighed it said
53,025 — and it failed on the runner that proved it, at 53,132. The prefix is
not the same size everywhere: `grep` says a longer sentence about itself where
ripgrep is absent, which is 134 bytes on every request of every turn.

So the gate weighs what the WIDEST machine pays. bare.WidestGrepDescription is
the one place that knows which of the two sentences that is, and widestBelt
swaps it in wherever the gate runs, with a law that fails if the tool it names
ever leaves the belt — otherwise the budget would quietly get easier.

The residual is the shelf: load_capability lists the groups THIS BUILD has, 27
bytes more on a machine that can edit video. That is filed in the file's head
rather than fixed here, because naming what a build COULD have is a change to
what the model is told.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…in it

Deleting RoleCompaction left the word in every config.json that had pinned it,
and the panel re-serialises the whole `models.roles` string on any change — so
the next time that person moved ANY OTHER pin the write came back `"compaction"
is not a role`, and every role pin on the machine was unchangeable until
somebody hand-edited config.json. The deletion had no reading for what it
deleted.

roles.Retired is that reading: a one-way list of the words this build used to
answer to, with the line that says what happened to each. It is the only memory
the build has of them, and without it a dead word is indistinguishable from a
typo.

ParseModelRoles drops those keys and says so once per process; writeModelRoles
takes them out of the TEXT, which is what is actually stored, so a person who
changes one pin stops re-saving a sentence about nothing. The test is the exact
string a pre-deletion profile holds.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A compaction is two mechanical passes over the transcript; it has never asked a
model. Six comments and two manual lines still offered it as the example of a
billed auxiliary errand — the pocket the title comes out of, the thing the
careful tier is for, a non-streamed call like a harness node — which is a
sentence about a call that does not exist, and was the figure the careful tier's
own patience was derived from until this PR re-derived it.

The settings hint for `careful work` no longer promises it either: what that
tier actually buys is the check on finished task work, the brief a task is
shaped into, and reading an image.

The transcript NOTE a compaction writes is still called a summary in agent.go,
because that one is real.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
`const spellOutRole roles.Role = "spellout"` lived in internal/session. It
registered, resolved and tagged its call correctly — and reached the cost report
as a word nobody had written down, because roleWords parses internal/roles and
nothing else. roleOf prices an unrecognised tag as a background errand with
nobody waiting, so the ONE auxiliary a person sits and watches was the one
counted as if it did not matter. Nothing failed; a number was quietly wrong.

The word moves to roles.RoleSpellOut beside its siblings, the Register call
stays at the call site, and TestEveryRoleWordIsDeclaredWhereTheVocabularyIs
fails the next one.

And the replay walker stops enumerating receiver spellings. callName flattened
one selector level, so three of the six rows in that switch —
`p.agent.completeWithModel` and friends — rendered as "" and could never match:
the ladder looked complete and was half dead. methodName asks the only question
the law has, which is the method's own name.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
THE LEAN ARM HAD THE SAME HOLE THE FULL ARM WAS REPAIRED FOR. leanShapedAgent
built its own thin config — no memory store, no accounts hub, no standing items,
no saved programs — so the arm that bounds the prefix for the person with the
LEAST room weighed a belt without `stand` (9.6 KB on its own), `remember` or
`search_conversations`, none of which leanCapabilityGroups shelves. It is the
shipped shape on a small window now, which is what its own comment always
claimed.

And three ways the number could go back to depending on who ran it:

  beltShapes[0] read the fullest shape only until somebody put a row in front of
  it. beltShapeNamed asks for it by name.

  WidestGrepDescription returned the fallback sentence because that one is
  longer today; it now measures both and returns the longer, and widestBelt
  ASSIGNS rather than comparing — a swap that only fires when it makes the
  number bigger is a swap that stops firing silently.

  load_capability names the groups this BUILD has, and `edit_video` exists only
  where ffmpeg is on PATH — 27 bytes of prefix that depend on a program nobody
  installed on purpose. loadCapabilityDescription takes the members as a
  question now, so the gate can ask for the widest answer instead of this
  machine's.

Both budget figures are re-measured in the next commit, on the Spark.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
… say what they are

THE CAPS. Full 53,141 and lean 44,989, both weighed on the Spark against the
shipped shape at the widest machine, both with no headroom, both printing the
shortfall against the target they owe on every green run. PERF.md moves in this
commit, which is the rule for a cap.

THE ROADS. memory_consolidate.go, standing_run.go and tools_doc.go each build a
provider client the one door does not make — a longer timeout, static routing, a
document config — and complete on it directly. That is a real difference, so the
answer is not to force them through the door; it is to make them say the word
the door says. clientdoor.go's withPurpose is that, and it is still the only
file in internal/session that spells provider.WithCallTag.

The two errands this build runs most often with nobody there reached the call
log with NO TAG AT ALL, which made them indistinguishable from a turn that had
lost its name. The standing check is `standing-check` and not `sentinel`,
because cmd/aforge already writes that word for the resident's quorum errand and
two calls under one tag is a reading of neither.

The law now pairs the halves: a file that builds its own client owes a
withPurpose, and it fails if it finds fewer than three such files — a structural
law that matched nothing would pass for ever.

And `compaction` comes out of the replay role table: no build can write it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A file-level pairing is satisfied by one road naming itself while the road
beside it stays anonymous, which is the exact shape of the failure this law
exists for: the forgettable one is always the second one.

Every completion in a file that builds its own provider client must now carry a
withPurpose on the context it is handed — inline, or through a local the purpose
was folded into. standing_run.go writes the purpose four wrappers deep and
completes on `callCtx` several lines later, so the reader follows assignments to
a fixed point rather than looking only at the argument.

And the second pass counts the CALLS it judged, not the files: three files that
build a client and nought completions in them would pass in silence.

cmd/aforge-replay's two doc comments stop citing auxiliary.go's deleted
WithCallTag line and point at clientdoor.go's callPurpose, which is where the
tag is spelled now.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
TestEveryRoleWordIsDeclaredWhereTheVocabularyIs caught
`const standingSentinelRole roles.Role = "sentinel"` in internal/session the
first time it ran, which is the second instance of exactly the defect it was
written for and the reason it is a law rather than a note. The word moves to
roles.RoleSentinel; the Register call stays at the call site.

And with `spellout` finally visible to roleWords, the tag law asked
cmd/aforge-replay to say what it prices as — so the table gains the row, which
is spellout.go's own lane.RoleAuxiliary and not a second reading of it.

That row carries the seam it exposes: a person SITS AND WATCHES a spell-out, and
an auxiliary is by definition a call nobody's seconds are being spent on. Either
the lane role or the product is wrong there. The table's job is to agree with
the build, so it does, and says so.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…g on the machine

THE RETIRED LEDGER IS DELETED. It was a map of the words this build used to
answer to, so a dropped pin could be told apart from a typo — and it could not
earn its keep. Both are a pin no call will ever consult, both should stop being
written back, and the only reader who can act on either is the person adding a
word right now. Keeping it meant every future deletion owed it an entry, and the
day somebody forgot, the old failure would come back exactly as it was.

So the reading is symmetric: a pin for any word that is not a role is dropped,
the writer refuses only a name being ADDED, and nothing dead is ever written
back. The row DRAWS what is acted on (config.LivePinsAt) rather than the raw
stored text — it used to draw `compaction: some/model` as a live pin while the
list under it had no such line — and the panel says "no longer a role — that pin
is ignored" to the person about to lose it. It is a sentence and not the
log.Printf this had, which was the only one in internal/config and landed on
stderr in every headless and --json door.

AND THREE MORE MACHINE-DEPENDENT NUMBERS. The lean page interpolates the working
directory, so that arm measured 45,034 on macOS against 44,989 on Linux and
failed — a fixture artefact worth tens of bytes in the gate whose entire purpose
is to be one number for everyone. The lean arm was also being handed the FULL
shape's load_capability sentence, making its figure a hybrid of two belts that
neither pays. And the replay table priced `vision` as an errand nobody waits on
while image.go, its only writer, sets lane.RoleTalk with the reasoning beside
it: a look at an image streams into the room during the person's own turn.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
THE DEBT WAS INVISIBLE. It was a t.Logf, and nothing in the Makefile passes -v —
so the one line saying the shipped prefix is thousands of bytes over what it is
supposed to be was printed where no human and no CI log would show it. The cap
is the TARGET plus a dated waiver now, one per arm in one place, and a waiver
only ever shrinks: known-red's discipline applied to bytes. Growing the prefix
takes a diff with somebody's name on it.

ONE WEIGHING. The two arms were forty lines each, written twice, and the copies
had already drifted — only one printed its shortfall, and it was the arm with
the smaller debt. reportPrefix is called twice.

ONE SHIPPED SHAPE. v3ShapedAgent WAS the second opinion: its own thin config,
eighteen tools, weighed by four gates including both budgets. It is
shippedShapeAgent now. beltShapes' lean row built a third config again, so the
belt law and the prefix gate were measuring different lean conversations under
one name; both call buildShippedConversation.

ONE VOCABULARY READER. purposeWords and roleWords were the same function with a
different path; they are constWordsOfType, read once, with the empty-result
panic inside it. The role law catches the CONVERSION spelling too —
`roleRepair = roles.Role("repair")` was invisible to it — and now also reads
every roles.Register argument, which is the spelling that cannot be worked
around. roles.RoleRepair is declared with the others.

And the manual stops teaching a summarizer rung, its `session: summarizer
returned nothing` string, and a focus appended to its instructions. None of that
exists: a pass stubs and folds, asks no model, and never folds your own words.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The working directory was a fixture artefact worth tens of bytes; the
Workstation line is runtime.GOOS/GOARCH and worth one, which a ratchet with no
headroom fails on just as hard. Both are normalised at the one place that needs
them to be, and the product still tells the model the truth about the machine it
is on.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
… price

The lean figure went 44,989 → 44,920 because the page's working directory
stopped being a fixture artefact and the lean arm got its OWN shelf sentence
instead of the full belt's. That is sixty-nine bytes off a number that had been
measured on a hybrid, and the waiver comes down with it — which is the whole
behaviour the ledger exists to have.

And `repair` reaches the replay table. It only became visible there when the
role stopped being conjured at its call site as roles.Role("repair"): a word
spelled that way is invisible to every reader of the vocabulary, which was the
defect, and the law that now catches it found this the moment it could see it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…ent question

A REGRESSION I INTRODUCED, CAUGHT BY THE SUITE. Making the read drop any word
the REGISTRY does not know threw away `imagegen:paint/model` — a real pin doing
real work. A role gets into the registry by registering, which is how it gets a
tier, and a role with no tier never does: a painter is chosen by a pin or not at
all, since a chat model in tiers.high is not a statement about painting. So the
registry was never the answer to "what are the roles".

roles.Vocabulary is that answer and roles.Known is the reader's door to it. The
list is hand-written, which is a thing that falls behind, so a law parses the
file it lives in and fails when a declared role is missing from it or a listed
one is not declared. The refusal lists the vocabulary too, so a person is shown
`imagegen` as a name they may pin.

And the retired-pin test seeds the row the way a profile written before the
deletion holds it — straight into the file, because the writer is exactly what
would refuse it today and that person never typed it today either.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The `standing-check` rename and [widestBelt] are both measurements around a
problem rather than fixes for it, and both are deferred to a cross-package wave
rather than grown here. Each now says so where a reader meets it, and names
issue #1010 (spine: #928) as the place it is deleted.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…n's file

writeModelRoles forgives a retired word it FINDS and refuses one being TYPED,
and it asked parseModelRoles which of the two it was looking at. That reader
drops every retired word by design, so the stored row it handed back could never
contain the one word the check existed to find: a name that had sat in the file
since before the deletion read as a name typed for the first time, and a machine
with an old `compaction:` pin could not save a change to any other pin.

The set is read off the stored TEXT now — storedRoleWords — because every reader
above that line drops what it does not recognise, and the dropped word is
exactly the word this question is about.

The asymmetry gets its own test: the same word, forgiven when found and refused
when typed. The old test only ever typed a word that was never a role, so the
half that broke was the half nothing watched.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…spent when it lands

THE POST-TURN JUDGE HELD THE SEAL. `judge.takeAtTheEnd()` stood between the
model's last word and [Agent.sealTurn] and blocked on a cheap screen plus, on a
yes, a mastermind confirm — measured at up to half a minute with the answer
already fully written on the person's screen, under a phase line reading
"whether that should be work". Parked behind it were the seal, the usage row,
the next Submit and the follow-up drain.

The turn's job was never to know WHETHER the ruling had arrived. It was to say
when one may be SPENT — past the reader that could re-open the turn — and it
still says exactly that, through a fourth verb on the mechanism that already
owns this shape: [sidecar.spendWhenItLands]. The answer is handed over the
instant it lands, on the reading's own goroutine, with every one-answer rule
[sidecar.take] keeps.

AND THE READING MOVES OFF THE TURN'S CONTEXT, because otherwise not waiting for
it would be cancelling it. It is the one reading in the package whose EFFECT
lands outside the turn — a yes starts a task, which is as good a minute later —
so it runs on the session's own lifetime ([afterTurn], which also names the two
older hand-written spellings of that bargain and why they are not folded in
yet). The told-after line goes to whichever stream is live when it arrives,
which is [Agent.sayMemory]'s door.

AND THE LAW GREW THE FIGURE IT WAS MISSING. journalPace measured the front of a
turn and the middle of it; the END had no name, which is where this wait stood
unnoticed. `sealMs` is the model's last word to the seal, and
loop_speed_test.go now holds it to being bounded by the re-open reader — the
guardian's named exception read at the other end of the turn — and by nothing
else.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…h is not evidence

THE MEASUREMENT FIRST, BECAUSE IT IS THE POINT. The pace ledger holds THREE rows
on this machine and exactly ONE has a recall beside it (2026-09-12T03:06Z:
sendMs 1, firstWordMs 7,389, aside recall / recall:reasked / mark). journalPace
landed days ago, so there is no population here to derive a share from, and the
7.4 s the audit quotes is that single row.

WHAT THE ROW DOES SHOW IS A HOLE IN THE INSTRUMENT. loop.go's law asserts
`message → the main request on the wire ≤ 50 ms`, and this turn passed it at one
millisecond. The request that left in that millisecond was then THROWN AWAY: the
recall landed 5.6 s later, cut it, and the request that carried the person's
answer did not leave until then. The row reported the law kept on a turn where a
reading cost the person seven seconds — satisfied in the letter by sending a
request nobody intended to read.

So the row carries `reaskMs` now: the first send to the cut, which is exactly
what cutting costs over having assembled the block before the first request. The
two moves reach the person's first word at the SAME instant — the recall's own
duration plus one time-to-first-token — and the re-ask pays for two requests to
get there. Whether that second request is cheap for having warmed the prefix
cache is the whole question, and it is a question about a population of turns.

AND NOTHING IS CHANGED ABOUT THE MOVE ITSELF, deliberately. Choosing between
cutting and pre-assembling on one row would be a threshold nobody could derive,
which is the one thing the architecture bar refuses outright. This is the
instrument that makes the choice decidable in a week instead of guessable today.

Two laws hold it: a re-asked turn must price its throw-away, and a turn that
threw nothing away must price nothing (the emptiness law on the same field).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The screen page said the two end-of-turn readers are asked at the same time and
the stage lasts as long as the slower of them. Only one of them can keep a
person now, and the page says which, why, and what happens when the other one
answers late. The tasks page gains the same fact where somebody looks for it:
the line can arrive after the turn ended, and nothing about it is different for
being a moment late.

The status word 'whether that should be work' is deleted from the corpus with
the wait it named.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…lose bargain

[afterTurn] is new machinery and its whole claim is a pair: nothing starts once
the session is closing, and nothing in flight can hold a quit. Both are the
kind of thing that is true when it is written and quietly stops being true, so
they are tests: a settle cancels the work in flight and returns when it
unwinds, a settle is bounded by its grace against work that ignores its
cancellation, a second settle is a no-op rather than a panic, and a nil
lifetime answers every door.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…eld for it

[sidecar.spendWhenItLands] reports whether it TOOK the reading, which is the
one-answer rule read from the caller's side. The judge holds a place on the
session's lifetime for its reading and releases it from inside the spend; a
sidecar that was already spent would hand that closure nothing, leave the
reading counted for ever, and make a closing session wait out its whole grace
for work that had already finished.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
santoshkumarradha and others added 4 commits September 12, 2026 11:35
…ht it

THE TOLD-AFTER LINE COULD BE LOST, and that is the defect the judge change
exposed. `this looked like work, so task 4 started: …` is the one thing that
stops a task appearing on the rail from being a surprise, and the ruling that
starts it is spent whenever it lands — which is sometimes after the turn sealed
and closed its hub. Sent there, it was a line nobody read.

There is ONE mechanism for that shape now and it is [Agent.sayLate]: said on
whichever stream is live, held for the next turn when there is none. It was the
memory brain's own queue, which meant a session with memory OFF had no queue at
all and the second caller's line would have been dropped in silence — so the
queue moves onto the session, the drain moves to the top of every turn (the
memory refresh it used to live in does not run at all without a brain), and
`memoryBrain.said`, `queueNotice` and `takeNotices` are deleted.

── AND THE FIXTURES THAT LEANED ON THE OLD ORDERING ──

Four tests encoded "the turn waits for the judge", each in a different way, and
each is rebuilt on the fact rather than on the timing:

  · the rate-limit test asserted the told-after line was on THAT turn's events.
    It waits for the task and reads the line's NUMBER, because the first turn's
    line legitimately rides the second turn's stream now.
  · the multi-part-ask test did the same and now accepts the line on either
    turn — the person being told is the claim worth holding.
  · the raced-confirm test counted the post-turn read on the spot. It waits for
    it, because the read is still in flight when the stream closes.
  · M12's fall-through fixture PARKED ON THE JUDGE to hold a turn open past its
    last boundary. That prop is gone. It parks on the remains reader instead —
    the one end-of-turn reading the turn still waits for, because it decides
    whether there is a turn to seal — and grinds to the ceiling's price gate to
    reach it, which makes the window an honest property of the turn's shape.

And two new tests for the door itself: a line said with no turn live reaches the
next one, and a session with no brain holds one just the same.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
… beside-work is waited for

The turn stopped waiting for the route judge, so twelve tests that read the
judge's effects off the turn's OWN collected events were reading "not yet" as
"never". Two readers replace that, and they are the only two shapes there are:

  routeNotice(agent, collected) — the line wherever it landed, asked once and
  now: this turn's stream when the ruling beat the seal, the session's held
  queue when it did not. It is the reader for a negative.

  routeSaid(t, agent, collected) — the same reading, waited for. A ruling that
  starts work is spent whenever it lands, and a positive has to wait for it.

And routeSettled(t, agent) waits for exactly what Close waits for: the
after-turn lifetime's own running work, registered while the turn is still
going. "Was the judge asked", "did work start" are claims about the session now,
not about the instant the turn sealed.

The rate-limit test counts what STARTED rather than what was announced: the
told-after line rides whichever stream is live when the ruling lands, so
counting lines would charge turn 1's announcement to turn 2 and read the gap as
broken.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…t ran

TestTheRateLimitHolds counted the fixture's started RUNS, which begin on a
goroutine of their own after admission — so on a machine that schedules the
runner a moment late, turn 1's task is counted against turn 2 and the gap reads
as broken. It failed that way on the Linux runner, deterministically, where the
laptop it was written on never saw it.

Admission is the judge's own act, inside the ruling the turn now waits for
(routeSettled), and it is the thing routeJudgeGap is a rule about. The runs are
still asserted, once, at the end.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…er lane's

997 is fix/pick-clock-under-load, merged. This lane is 1011.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…filename

The rename landed and the line inside it did not.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Base automatically changed from fix/auxiliary-inventory to dev September 12, 2026 20:29
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@santoshkumarradha

Copy link
Copy Markdown
Member Author

Closed per owner request.

@santoshkumarradha santoshkumarradha added the area:session The engine — turns, tasks, the toolbelt, checkpoints label Oct 3, 2026
@santoshkumarradha santoshkumarradha added this to the Reliable agent milestone Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:session The engine — turns, tasks, the toolbelt, checkpoints

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants