Skip to content

fix: a person's stop reaches a run - #1250

Closed
santoshkumarradha wants to merge 10 commits into
santos/devfrom
fix/a-stop-reaches-the-run
Closed

santoshkumarradha wants to merge 10 commits into
santos/devfrom
fix/a-stop-reaches-the-run

Conversation

@santoshkumarradha

@santoshkumarradha santoshkumarradha commented Sep 19, 2026

Copy link
Copy Markdown
Member

What was wrong

With the belt on, nothing a person pressed could stop a run. Measured on the real binary, hosted, at trunk 12840ee on 2026-09-19:

gesture what happened count
x over an empty box, then stop it on the card there is no task 1 in this session; the run carried on and landed by itself 4 of 4
x stop it on the run's own page the harness owns the root task every time
p pause on the run's own page the harness owns the root task every time

Cause, read in the code. A run's rows wear task numbers and are not in the task graph, and the stop a surface sends for a row is task:N, which only the graph answered. The run was started on a context nothing could cut, and no cancel was kept anywhere. The store refuses every verb on the run's own task for every caller, and the page offered two of them.

What changed

The store (internal/plandb). StopRoot is the runtime's verb for a person's word. It ends the run's own task and every task still open in one write, and leaves ended tasks as they ended. A run left open in its store is adopted by the next hand-off, so a stop has to be written there.

The session. The id a surface already sends is resolved to whoever owns the row: the live run first, the graph second. Nothing on the wire changes, so a window built before this change stops a run through an engine built after it, and the model's own stop ends a run too. A stop goes in this order: the store first, so no part reads as a failure and the next hand-off cannot adopt the run; the context next, so every worker and every call it has out ends now; the work last, committed on the run's own branch with the copy given back. Nothing is merged into the folder and no model turn is bought to narrate the ending. A stop on a row that joined a run ends that work only. The page's stop on the run's own task is the same stop, and on a run nobody is running any more it ends the run in the store.

The surface. x on the run's own page raises the stop card, and the page steps aside for it the way a background job's page does. p pause is not offered under the run's own task and is a letter there. ctrl+c is read before a page that is on its way and before the page. Every ending of the page's read ends the key hold, including an answer for a tab the person has left. That read is already bounded by the wire's own call deadline, which answers no page. A key that leaves the page ends the replay, so x typed while the run's own page opens reaches the card.

What a person is told. On the run's page and once in the conversation:

stopped · its work so far is kept on <branch> and did not go into <folder> · merge that branch to bring it in, or delete it to drop it

A run stopped before it changed anything says stopped · it had changed nothing, and then neither the sentence nor the row names a branch.

No further model call. A surface asks for a run's four-line summary again whenever its rows move, and a stop moves them. The engine declines a summary for a run a person stopped; the last reading stands.

Two laws

  • internal/session: every function that publishes a row in a state a stop means something in is listed with the cancel kind that reaches its owner and the test that proves it by stopping one. A new publisher fails the law until it has both. I removed the run's entry to check it: the law named startKnownTaskRun and its line.
  • internal/tui3: for every kind of row a run's store can hold, every verb word the foot offers is pressed against a real store over the real wire, and none may be refused. I put p pause back under the run's own task to check it: the law quoted the store's refusal.

Not in this change

ctrl+c, /quit and a closed window do not stop a run today: the window closes, the run carries on in the engine and its work lands by itself. Measured three of three for each at 12840ee, and ten of ten in a separate reproduction with the terminal killed under the window. That is unchanged here. It is a separate open question and not a regression of this change.

Hosted acceptance, real binary, belt on

Nine drives at 8ca974c, the same code without the summary commit, then eight ledger probes on the later heads.

path card raised row out of running after stop it landed in the folder copy given back work on the kept branch
x in the conversation 3 of 3 0.26, 0.27, 0.27 s 0 of 3 3 of 3 26, 14 and 8 files
x on the run's own page 3 of 3 0.27, 0.27, 0.54 s 0 of 3 3 of 3 18, 14 and 22 files
x typed in the page-load gap 3 of 3 0.27, 0.26, 0.26 s 0 of 3 3 of 3 6, 6 and 12 files

Child pids: the engine's pid and its child pids were recorded before each stop. Five drives had no child alive at that instant and four had one shell child. Every recorded pid was gone after the stop, and the engine had no children, nine of nine.

Page foot on the run's own task, every page drive: ↑↓ scroll · enter send · x stop it · esc back.

Usage ledger rows stamped after the stop instant, read 60 s after it:

head path rows before rows after
f55c27e conversation 4 0
f55c27e page, three probes 4, 3, 3 1, 1, 1
f55c27e gap 2 0
cdd8e77 page, three probes 5, 12, 9 0, 1, 0
cdd8e77 conversation 6 0
cdd8e77 gap 6 0

No billed row was ever stamped after a stop. The one kind of row that was is a marker, not a call: a model name, unbilled: true, no dollars, no tokens, no role, about 20.5 s after the stop. The engine writes it when it looks up the receipt of a call whose stream ended before its usage block and finds none. A stop cuts a call mid-stream, so the marker is the bookkeeping of a call that began before the stop and was cut by it. I first read it as a new summary call. The summary commit is still right, because a test shows the engine did make that call when asked, twice, and now makes none. It is not what the marker is, and the body says so rather than claim zero.

The day figure on screen lags by a beat. In one drive it moved after a stop. The ledger had no row for that; it was earlier spend drawn late. Nobody should read that figure as spend after a stop.

One drive took 7.09 s for the row to leave running, with a shell child alive at the stop. The row says running for that time. A small follow-up branch gives the row the stopping reading a stopped task already has; it is not in this PR so this head stays the one that was proven.

All drives used a throwaway home with private permissions. Each removed the key copy first, ended its own engine by socket holder and executable equality, then removed the home. No drive home remains.

How "the worker is gone and the spend has ended" is checked. The worker is not a process: it is a goroutine in the engine, and its steps are short-lived shell children. So each drive records the engine's pid and its child pids before the stop, and checks those pids are gone after it. Spend is read from the usage ledger's rows stamped after the stop instant. The day figure on screen is drawn on a beat and can show earlier spend late, so it is not the evidence.

Proof

  • Head aad44e4 sits on fix: a gesture never waits behind a run's summary, and an open page reads on a beat #1251's head bd0c2af. It is cdd8e77 plus one line in the change entry.
  • Spark, tree at cdd8e77, under the one-suite lock: whole internal/tui3 exit 0 in 324 s, whole internal/session exit 0 in 246 s.
  • Spark, same tree: internal/plandb, internal/run, internal/manual, internal/guard, internal/namelaw ok; make test-laws exit 0; gofmt -l ./cmd ./internal empty.
  • Failing tests first, each seen red with the sentence from the real screen: there is no task 71 in this session, the harness owns the root task, the foot offering p pause, ctrl+c held with the page's keys, the hold left standing, two summary calls after a stop.
  • Manual: two new sections in worker-harness.md with three probes, and the sentence about the run's own task in task-controls.md. The store's old refusal is no longer quoted as the answer.

🤖 Generated with Claude Code

santoshkumarradha and others added 2 commits September 19, 2026 00:45
The line exists so the engine sees a person's gestures in the order they
were made. The run's summary, a sentence a model writes under a budget of
ten seconds that nobody pressed for, was asked through it, and stood in
front of whatever a person did next. Measured on a real screen: a press on
a run's row waited 7.6 s for a read that took 2 ms, and a stop or a pause
pressed in that window would have waited the same.

The summary is now asked beside the line, its fold unchanged. A pause
pressed while the summary is held on a channel is answered before it
returns, gestures made meanwhile still reach the plan in the order they
were made, and a structural law names with its reason every door that may
be asked beside the line, so a gesture cannot be moved there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The follow was offered its read on every tick of the paint clock and held
back only while one was out, so a fast engine was asked again the moment it
answered: 509 reads over the wire in ninety seconds on a real screen, about
six a second, for one open page. It now reads once every three seconds, the
rail's own beat, counted from the last time the page was read for any
reason, and only while the task is queued or running. A page on a task that
has ended is read once, to open it.

Counted through the real server and client: twenty ticks a beat over five
beats cost 120 reads before and cost five now; twenty beats on an ended
task cost none. The follow stays in the ordered line because its fold
replaces the page and a note re-reads it too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
santoshkumarradha added a commit that referenced this pull request Sep 19, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
santoshkumarradha and others added 6 commits September 19, 2026 01:22
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every verb refused the run's own task, so a run a person stopped had no
ending the store could hold: it stayed open and the next hand-off in the
same conversation adopted it, stopped work included. StopRoot cancels the
run's task and every task still open in one transaction, leaves every
ended task as it ended, and is a no-op on a run that is already over.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A run's rows wear task numbers and are not in the task graph, so the stop
every surface sends for one (task:N) was refused with 'there is no task N in
this session' and the run carried on to its own landing: four of four on the
real binary. The run was started on a context nothing could cut and no
cancel was kept. The page's stop on the run's own task was refused with the
store's sentence about who owns what.

The id a surface already sends is now resolved to whoever owns the row: the
live run first, the graph second. A stop ends the run in the store first (so
no part of it reads as a failure and the next hand-off cannot adopt it), cuts
the context every worker and call runs under, and keeps what was made on the
run's own branch without putting anything in the person's folder. The person
is told once where the work is and how it comes in or goes away. A stop on a
joined row ends that work only. The page's stop on the run's task is the same
stop.

A ledger law lists every function that publishes a row in a state a stop
means something in, with the cancel kind that reaches its owner and the test
that proves it by stopping one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…offers no verb the store refuses

On the run's own page the foot offered 'x stop it' and 'p pause', and the
store refuses both there for every caller, so both answered 'the harness
owns the root task'. x on the run's own task now raises the stop card (the
page steps aside for it, as a job's page does) and the card's 'stop it' ends
the run through the plan's own door; p is not offered there and is a letter,
because nothing holds a whole run. A stop on a run nobody is running any more
ends it in the store. A hold asked of a run answers in a person's words.

A law holds the foot to the store by property: for every kind of row a run's
store can hold, every word the foot offers is pressed against a real store
over the real wire, and none may be refused.

The page-open hold let nothing past: ctrl+c is now read before the hold and
before the page, every ending of the read ends the hold (an answer for a
front the person left used to leave it standing), and a key that leaves the
page ends the replay. x typed while the run's own page opens reaches the card.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n it

Measured on the real binary: a run stopped in its first seconds drew 'branch
kept' beside 'it had changed nothing'.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@santoshkumarradha
santoshkumarradha force-pushed the fix/a-stop-reaches-the-run branch from fe0fa1d to f55c27e Compare September 19, 2026 05:29
santoshkumarradha and others added 2 commits September 19, 2026 01:39
A surface asks for the run's summary again whenever its rows move, and a stop
moves them, so every stop taken from the run's page was followed about twenty
seconds later by one more model call about work the person had just ended the
spend on: three of three in the usage ledger on the real binary. The run's own
task is cancelled by nothing but a person's stop, so that state is the test,
and the last reading the run had stands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@santoshkumarradha

Copy link
Copy Markdown
Member Author

Landed on santos/dev as 7b8be4b, inside a stack whose top is 992779e (tree 5c6d4b87b); the second commit removes a cell's FINDINGS.md that had ridden into the repository root with #1239. This PR was stacked on #1251's head, so its diff over that head was applied onto the trunk that already carried #1251; the directories it touches are byte-identical to the PR head aad44e4. Full make check on Spark on 992779e: EXIT 0, 114 packages ok, first run, log ~/src/trees/check-sdev-992779ed6.log; format clean and a Windows cross-build of ./... ok on the same tree. Squashed by hand, so GitHub shows this as closed, not merged; the branch is level with dev 9258f70.

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