Skip to content

One run per batch: simultaneous hand-offs join one run, a failed run road says so - #1417

Merged
santoshkumarradha merged 11 commits into
santos/dev2from
task/one-run-per-batch
Sep 23, 2026
Merged

santoshkumarradha merged 11 commits into
santos/dev2from
task/one-run-per-batch

Conversation

@santoshkumarradha

@santoshkumarradha santoshkumarradha commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

What was wrong

One message proposed eight tasks, and all eight were approved at once. Their hand-offs raced to open the conversation's one plan store. A run is registered only after its store is open and its copy is cut, so every hand-off saw "no live run" and opened or set aside the same plandb.db. Two runs started over one path. The other six failed ("plan store belongs to a different run", or a disk I/O error) and quietly became nodes of the older task graph. The second run's worker was bound to the store by path alone, so it filed its children and its dones into the other run's plan. That run's row never settled, and its room answered no task 1 in this session.

What changed

  • One run per batch. Starting a run is now one critical section (beltStartMu / lockBeltStart). It runs from the look for a live run until the run is registered. Other hand-offs wait at the door, then join the run as children, the same way a later hand-off does. ContinueRun takes the same lock, and openBeltRunStore refuses to open while a run is live.
  • A worker writes only its own run's store. NewBeltWorker takes the run's root from the run's open handle. A worker's command prefix exports PLANDB_RUN=<root> beside PLANDB_DB. The plandb CLI refuses a store at that path whose root is another run's, and neither reads nor writes it.
  • No silent fallback, on both doors. When the run road fails, the propose door and the typed /task both answer task N did not start: <reason>. Nothing is running for it and nothing was started in its place; .... The proposal receipt is marked failed, and nothing is admitted to the older graph. A store seeded for a run that never started is removed. Only a missing run engine or a missing plan place still takes the older road. This replaces do, run, session: codeaf do edits in place, commits nothing, and stops at a price #1416's It runs on the older task engine, because the run engine could not start it: receipt. That receipt was honest, but the work still became an old-graph node, which contradicts the "an approved hand-off under the bash belt is a run, never a session-tree node" law. do, run, session: codeaf do edits in place, commits nothing, and stops at a price #1416's test now asserts the refusal shape.
  • Run rows in a room. A message to a live run's row is left as a note on its task's page (left on the task's page — its worker reads it between steps). A row nothing drives says so and that it can be stopped. It never says no task N in this session. A stop settles such a row as stopped.
  • Manual: worker-harness.md. Change entry: 1417-one-run-per-batch.md (fixed).
  • Also fixed, because the full check was red on santos/dev2 without it: TestAPathOnAnotherMachineDoesNotRefuseTheTask spelled a real checkout under /home as its "remote" path. That directory exists on the machine it was measured on, so the test failed there. It now names a folder that exists nowhere.

This PR does not touch internal/tui3. The ◑ $ guard and the running glyph moved to #1420.

Tests: each shown red on the unfixed code first (Spark, commit 199bd9d, tests plus seams, no behaviour change)

Test Red on unfixed code
TestEightHandoffsApprovedAtOnceAreOneRun: 8 approved proposals committed at once through stageTask / ResolveTask / Commit, with the first copy cut held slow and a fixed clock "hand-off 1..7 became a node of the older tree", "0 hand-offs joined the run, want 7"
TestAHandoffWhoseRunRoadFailsIsNotASilentNode (both doors) receipt task 1 started, a node admitted, typed /task err=nil
TestARunRowNothingDrivesAnswersAMessageAndStops no task 1 in this session; the stop said "has already finished"
TestALiveRunsRowsTakeAMessageAndAMissingOneStops a message to a joined row was refused
TestABeltWorkerIsBoundToItsOwnRun the worker's prefix carried no run
TestPlandbCliRefusesAnotherRunsStore (internal/plandb) exit 0, want 1
TestBashWorkerCannotWriteIntoAnotherRunsStore (internal/run, a real worker and the real CLI) the worker filed its task into the other run's store

All seven pass on the final head, f0bc033.

Gate: Spark, fresh clone, final head f0bc033 (includes a real merge of santos/dev2 at 147382a)

  • go build ./...: EXIT 0
  • go vet ./...: EXIT 0
  • touched packages, make test PKGS="./internal/session ./internal/plandb ./internal/run ./internal/manual" TEST_FLAGS=-count=1 (box lock): EXIT 0
  • make check TEST_FLAGS=-count=1: EXIT 0 (bin/codeaf 53412105 bytes, under budget; cross builds pass)

One touched run before that went red on TestARecallThatLandsBeforeTheFirstWordIsAskedAgainWithIt (turn did not finish; events so far: []). This red is not caused by this change. The same test fails the same way on a clean santos/dev2 at 147382a when starved (GOMAXPROCS=1 -cpu 1 -count=60: one failure on each tree). It is a load-sensitive defect in the recall path and is worth its own issue.

The tests, and only the seams they need (a slow or failing copy cut, a
waiting hook, the run's root handed to a belt worker, the name of the
run binding). No behaviour changes in this commit; every new test is
expected to fail on it.
… store

Starting a run is now one critical section, from the look for a live run
until the run is registered, and every other hand-off waits at its door
and then joins the run as a child. A batch approved at one moment is one
run with every hand-off in it.

A run road that fails says "task N did not start: <reason>" on both the
proposal door and the typed /task, reads as a failure, and starts nothing
on the older engine. Only a missing run engine or plan place still takes
the older road.

A worker is bound to its run's root as well as its store path
(PLANDB_RUN), and the plandb CLI refuses a store at that path whose root
is another run's.

A run row's room takes a message as a note on its task's page. A run row
nothing drives answers that it can be stopped instead of "no task N in
this session", and a stop settles it.
…ading stops competing with how to stop a task
…son's folder

The pr line was left unstaged by the rename. The test spelled a real
checkout under /home, which exists on the machine it was measured on,
so it failed there.
…er-batch

# Conflicts:
#	docs/changes/unreleased/1418-ground-lint-remote-path.md
#	docs/changes/unreleased/1421-ground-lint-remote-path.md
#	docs/changes/unreleased/ground-lint-remote-path.md
#	internal/manual/chat/worker-harness.md
#	internal/session/task.go
@santoshkumarradha
santoshkumarradha marked this pull request as ready for review September 23, 2026 20:33
@santoshkumarradha
santoshkumarradha merged commit b8765e1 into santos/dev2 Sep 23, 2026
4 checks passed
@santoshkumarradha
santoshkumarradha deleted the task/one-run-per-batch branch September 23, 2026 20:34
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