Skip to content

Close the dispatch loop: attempt-grain redispatch, sessions panel, results, stop (U1–U4) - #7266

Merged
briansrls merged 7 commits into
mainfrom
session/dispatch-loop
Jul 26, 2026
Merged

briansrls merged 7 commits into
mainfrom
session/dispatch-loop

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

Summary

The operator-approved close-the-loop arc, all three slices, one PR. Before: press → session spawns → page forgets on refresh → session finishes → nothing comes back → the node is burned forever. After: press → the page knows what's live (refresh-proof) → every attempt's branch is on the row → press again whenever.

U1 — dispatch becomes attempt-grain (the redispatch fix)

Both one-shot roots dissolved by one move: branch (dispatch/<node>-a<key>), worktree, and claude session-UUID carry an attempt key — content_hash(node_id × the spawn's clock observation). The tmux name stays node-grain deliberately: it's what observe/reconcile key on, so already-live detection, kill, and tick idempotency are untouched — one live session per node remains the invariant. Nothing is deleted to make way (a stopped attempt's worktree may hold uncommitted work; the rework discipline carries prior diffs forward). Fail-closed: clock refusal refuses the spawn (attempt-clock); a same-instant double-press collides on the key and refuses at the git step, typed. Bonus: a latent branch-naming fork in the hostile-node-id witness closed.

U2 — sessions panel v0 (the refresh-amnesia fix)

The healthz digest law takes the digest over the pure page body, so live state arrives by fetch: GET /sessions.json projects the belt observation (one observation feeds status and body; Observed → 200, ObserveRefused → 503 + typed reason). On load the client marks each live node's actuator — ok band + session · live (the observation word, type-distinct from the wire answers that reply to my click) + the tmux session name in the title. Refusal or failed fetch prepends a loud .observe-refused banner — an unknowable session set never renders as an empty panel.

U3 — result surfacing v0 (the loop closes)

The attempt branches are the results ledger: GET /attempts.json lists refs/heads/dispatch/ fail-closed (git absent / non-zero = unknowable, typed 503; empty at exit 0 = genuine zero). Each attempt renders into its node's disclosure as a mono line — the branch name, copyable and true (a tree link would be dead until a push; a dead link is a fabricated result — the PR half lands when push/PR state is observed). Legacy pre-attempt-grain branches (the operator's accidental spawn among them) parse with an empty key and render as real attempts. The round-trip witness pins U1↔U3: a minted branch parses back to its own node and key.

Receipts

  • 22 new witness fns across three files: attempt-grain (fresh where debris collides, stable where identity reconciles), sessions wire both arms + loud 503 + client pins, attempts parse round-trip/legacy/multi-line + wire + argv-safety.
  • Pins moved with authorities, deliberately: route count 10→12, gate preview asserts the attempt-scoped branch, uuid determinism re-scoped per attempt.
  • Digest re-pins by execution per byte-moving slice: c4eabee5bdfb8021 (U2 banner) → c020930b7552052e (U3 row).
  • Full battery green at each slice (10 → 15 → 14 suites), on the merged tree (origin/main folded in mid-arc: the flips squash + W2-bulk: the legacy prose frontier migrates onto the ticket contract (tranche 1: dispatch-lifecycle cluster) #7240 + ~7 other lanes).
  • Riders: two TS-serializer potholes fixed at the consumer (optional-else spelling, bare-Call arrow body); the page witness's bare LiveTreeDisposition reference fixed with an explicit import (Class B shape — proven pre-existing on the stashed tree before touching it).

Post-merge, on the live page

Press dispatch on a ready row → the button latches → refresh: the row still shows session · live with the tmux name in the title. The disclosure lists attempts: dispatch/<node>-a<key>. Stop the session, press again → a second attempt spawns cleanly. Your accidental ts-authority-converge spawn appears as a legacy attempt on its row.

🤖 Generated with Claude Code

Brian and others added 4 commits July 25, 2026 20:46
The one-shot-per-node defect had two roots, both dissolved by one move
(operator-approved close-the-loop arc; the operator's accidental spawn of
ts-authority-converge was the live receipt):

- DEBRIS: Stop left dispatch/<node> + its worktree behind, so the next
  POST failed at the git step forever.
- SESSION-ID REUSE: the node-deterministic claude --session-id collided
  with the archived transcript, so a stopped node could never dispatch
  again at the claude layer either.

Dispatch attempts now carry an ATTEMPT KEY — content_hash(node_id x the
spawn's clock observation), std.content_hash the single hashing authority —
threaded into the branch (dispatch/<node>-a<key>), the worktree path, and
the claude session UUID. The TMUX SESSION NAME deliberately stays
node-grain: it is what observe/reconcile key on, so already-live detection,
kill, and tick idempotency are untouched — one live session per node
remains the invariant. Nothing is deleted to make way: prior attempts'
branches/worktrees stay as inspectable receipts (a stopped attempt's
worktree may hold uncommitted work; the rework discipline carries prior
diffs forward; cleanup is separate explicit work, never an absorbing arm
on spawn).

Fail-closed arms: a clock refusal refuses the spawn (step attempt-clock —
an unknown-stamped attempt would collide later); a same-instant double
spawn collides on the key and refuses at the git step, typed.

Also dissolved: the gate preview now names the REAL attempt (the POST
handler's one clock probe feeds gate + spawn), and the hostile-node-id
witness re-derives argv through the real naming fns instead of its own
hand copy (a latent §3 fork, closed).

Witnesses: belt_attempt_grain_holds (new — fresh where debris collides,
stable where identity reconciles: distinct key/branch/worktree/uuid across
stamps, deterministic within one, node-grain tmux invariant); the uuid
determinism suite re-scoped per attempt; the gate pin moved with the
authority (now asserts the attempt-scoped branch). Ten suites green by
execution: belt-actuate, actuator, belt, serve, http-route, page, sandbox,
spawner, register, tactile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…roof

The refresh-amnesia the operator hit (spawn a session, reload, the page
forgets) closes without the render losing purity: the healthz digest law
takes the digest over the pure page body, so live state arrives by FETCH,
exactly as dispatch answers do.

WIRE — GET /sessions.json: the belt observation projected as JSON
(belt_sessions_json_value, a pure fn over BeltObserve with every arm
witnessed on synthetic values; the effectful wrapper only threads
belt_observe in). ONE observation feeds both status and body — observing
twice could disagree. Observed → 200; ObserveRefused → 503 carrying the
typed reason: an unknowable session set is loud on the wire, never an
empty panel. Route count pin moved 10 → 11 with the authority.

CLIENT — on load the emitted program fetches the observation and marks
each live node's actuator: ok band + the session-chip morph with the
OBSERVATION word (live — type-distinct from the wire answers spawned/
already_live: those answer MY click, this reports the world) + the tmux
session name in the title channel. The refusal arm (or a failed fetch)
prepends an .observe-refused banner naming the reason — the altitude law's
anomaly-expands applied to the panel. Banner styled in the loud family
(venom edge mark, mono); digest re-pinned c4eabee5bdfb8021 by execution on
the merged tree.

Riders:
- origin/main merged in (the flips squash + ~7 other lanes; local main had
  not been pulled since #7234 — the branch now carries the true tree).
- Two TS-serializer potholes found by execution and fixed at the consumer:
  the optional-else spelling (Present{ts_block}) and a bare-Call arrow body
  the emitter's match does not cover (wrapped block+return).
- The page witness's bare LiveTreeDisposition reference lost its
  pool-coincidence under main's 439-file delta — the documented Class B
  shape, proven pre-existing by running the stashed tree, fixed with the
  explicit import (closure-independent binding).

Witnesses: 4 new sessions-panel fns (wire projection both arms, loud
refusal end-to-end incl. 503 + banner machinery + catch arm, live-mark
pinned in the served asset, banner styled). Full battery green: 15 suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The attempt branches ARE the results ledger: every U1 spawn mints
dispatch/<node>-a<key>, so listing refs/heads/dispatch/ observes every
attempt ever made — including pre-attempt-grain LEGACY branches (empty
key; the operator's accidental spawn is one), which are real attempts and
render as such, never filtered as noise.

ACTUATOR — parse_dispatch_attempt_refs: the branch-name parse authority,
discriminating by the last -a segment being exactly the 16-char key width;
the misparse edge (a node id itself ending -a<16 chars>) accepted and
named, not papered over. The round-trip witness closes the U1 loop: a
branch MINTED by dispatch_branch_name must parse back to its own node and
key — the two authorities cannot drift.

BELT — belt_attempts_observe mirrors belt_observe's fail-closed grammar:
git absent or non-zero for-each-ref is an UNKNOWABLE attempts set
(typed, carries stderr), never widened to zero; exit 0 + empty IS a
genuine zero (for-each-ref's documented no-match behavior). Runs in
belt_actuate_workdir — the branches live exactly where spawns mint them.

WIRE — GET /attempts.json (route pin 11 → 12): 200 observed / 503 refused
with the typed reason.

CLIENT — each attempt renders into its node's disclosure as a mono line:
the branch NAME, copyable and TRUE — a tree link would be dead until a
push, and a dead link is a fabricated result; the PR half lands when
push/PR state is observed, not guessed. Upsert by class (no reload
stacking); refusal reuses the loud banner, which became text-agnostic
(the baked session prefix moved to its call sites) rather than forking.

Recovery owned: the first cut of these client edits went in by python
string surgery that aborted mid-script, leaving a call to an undefined fn
committed to nothing but the worktree — reverted to the committed U2
state and re-cut with tool edits in verified steps. A scratch probe entry
also began hanging post-merge (closure-dependent, product entries
unaffected, ~14s compiles throughout) — noted, not chased.

Witnesses: 8 new (parse round-trip · three-shape parse incl. legacy ·
multi-line output · wire both arms · loud 503 · client pins for BOTH
panels · styled row · typed argv with no destructive tokens). Digest
re-pinned c020930b7552052e by execution. Battery: 14 suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@briansrls

Copy link
Copy Markdown
Contributor Author

Live drive receipt (local serve, pre-merge):

GET /sessions.json → 200 {"schema": "roadmap-sessions/v1", "observe_refused": false, "sessions": []}
GET /attempts.json → 503 {"schema": "roadmap-attempts/v1", "attempts_refused": true,
                          "reason": "git for-each-ref failed: env: cannot change directory to '/opt/gunbc/gunbc'..."}

The 200 is a genuine zero (tmux's documented no-server signal). The 503 is the refusal arm proving itself: the attempts observation runs in belt_actuate_workdir — the srv1 repo-root authority, absent on a laptop — so it refuses typed and located rather than serving an empty list dressed as truth. On srv1 both arms observe for real.

Named residue: the attempts workdir is the srv1 path authority, not a projection of the serve process's own loaded root — correct for the deployment target, loud everywhere else. Deriving it from the process root rides the storage-blind lane (W3 neighborhood), not this PR.

🤖 Generated with Claude Code

The operator asked for it mid-test, and it is the loop's missing verb:
press → watch → STOP → press again. The blocker was never the teardown
(that existed) — it was ownership PROOF: name-only tmux-ls observation
yields UnknownRefused, and R5 correctly refuses to reap without proof.

FINGERPRINT OBSERVATION (the scaffold's declared dissolution, now
DISCHARGED — its Disposition row flips Scaffold → Terminal and its witness
flips with it): tmux list-panes -a with the cited format variables yields
(session_name, pane_current_command); tmux_session_observation_with_panes
upgrades a name-only observation to SessionObserved with the fingerprint in
hand — Converged for claude, Conflict for anything else wearing the name.
A pane-read failure leaves the observation name-only → the gate refuses
fingerprint-unobserved; it NEVER widens into a kill.

STOP GATE — the R5 table as a typed decision: Converged/Drifted stop;
Absent 404; Conflict/UnknownRefused/Inaccessible 409; observe-refused 503;
kill-failed 502. One observation feeds the whole chain.

WIRE — POST /stop/{node_id} (route pin 12 → 13). CLIENT — a stop chip
appears beside each live session pill (toggle-chip family): stopping… →
reload on stopped; stop refused with the reason in the title channel
otherwise. Digest re-pinned 28c06723684117e4 by execution.

LIVE DRIVE, four arms exercised against real tmux:
- the operator's hand-made sleep session wearing a dispatch name → 409
  "foreign process… refusing to kill what we cannot prove ours" — three
  spoof shapes tried (plain, exec -a, symlink), all refused, session
  intact: p_comm resolves the real image, exactly the anti-spoof R5 wants;
- a SIP-killed copy vanished pre-observation → 404 discriminated from 409;
- the green path proven through the real gate synthetically (witnessed).

NAMED FINDING, landed as a row (dispatch_claude_fingerprint_finding_note):
a REAL claude on this macOS reports its VERSION (2.1.220) as p_comm — the
Converged arm may not fire on real workers until srv1's live fingerprint
is READ, not guessed. Decide-by-receipt: one tmux list-panes on srv1
beside a live worker; the row becomes the observed value if it differs —
a data edit. The safety half (refusals) is unaffected and proven.

Witnesses: 6 new (pane parse · the four-arm gate table with the sleep
Conflict as the star RED · wire-status discrimination incl. 404-vs-409 ·
client chip pins · styled · scaffold discharged). Battery: 13 suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@briansrls briansrls changed the title Close the dispatch loop: attempt-grain redispatch, sessions panel, result surfacing (U1–U3) Close the dispatch loop: attempt-grain redispatch, sessions panel, results, stop (U1–U4) Jul 26, 2026
@briansrls

Copy link
Copy Markdown
Contributor Author

U4 added mid-review at the operator's ask: stop from the UI.

The R5 owned-only teardown as a button — enabled by landing the fingerprint observation the actuator's scaffold row had always declared as its dissolution (tmux list-panes → pane_current_command; the Disposition row flips Scaffold → Terminal, its witness flips with it). POST /stop/{node_id}: one observation feeds the whole chain — Converged/Drifted kill (200), Absent 404, foreign/unproven 409, unknowable 503, kill-failed 502. A stop chip appears beside each live session pill; refusals land in its title channel.

Live drive, four arms against real tmux: the operator's hand-made sleep session wearing a dispatch name refused three spoof shapes (plain / exec -a / symlink — p_comm resolves the real image, exactly the anti-spoof R5 wants) and survived; a SIP-killed binary copy vanished pre-observation and discriminated 404 from 409.

Named finding, landed as a carrier row: a real claude on macOS reports its version string (2.1.220) as p_comm — so the Converged green arm may not fire on real srv1 workers until the live fingerprint is read (tmux list-panes on srv1 beside a live worker), at which point the fingerprint row becomes the observed value — a data edit. The safety half (all refusal arms) is unaffected and live-proven.

6 new witnesses; route pin 12→13; digest re-pinned 28c06723684117e4; 13 suites green.

🤖 Generated with Claude Code

Brian and others added 2 commits July 25, 2026 23:23
CI on #7266 went red two ways, and only one of them was mine.

NOT mine — dag/gunbc/hostname_read.dag: main's #7250 renamed the seed
builtin (shell_materialize_argv_for_operation ->
shell_materialize_operation_argv) and re-homed the call on the new
v2.std.operation_argv types. The branch was seven commits behind, so it
carried the old spelling. Merging main is the whole fix; nothing here
touches that file.

MINE — the Phase 0(b) admission invariant refused belt_attempt_grain_holds
as an enrolled row with zero executing consumers. The row is PURE (content
hashes and string derivations, no host reach) but I had parked it in
roadmap_belt_actuate_witness_test.dag, which is BinWitnessWet-excluded from
discovery because its OTHER witnesses execute real host commands. So the
claim was deferred to a cadence it never needed and never ran on.

The fix is to move the row, not to enroll it: it now lives in
dispatch_attempts_witness_test.dag beside U3's parse witnesses, which
discovery scans on every affected PR. That pairing is the real one — this
row proves the MINT varies per attempt, attempts_parse_roundtrips_the_minted_branch
proves the READ recovers exactly what was minted; one authority, both
directions. Enrolling it on the wet roster would have greened CI by
mislabelling a pure claim as host-executing, which is the kind of quiet
lie the invariant exists to catch. A note stays behind at the old site
saying deferral is a property of the EXECUTION a row needs, never of the
module it happens to name. Three imports went dead with the move and are
deleted rather than left as unused vocabulary.

Two roadmap rows recorded from the operator's questions at the U4 receipt:

  ts-dispatch-ownership-mint — "make it impossible for a foreign process
  to exist here?" The honest answer is that the tmux session namespace is
  host-global and not ours to close, so claiming that wall would be the
  DESIGN section-5 "never" trap. What CAN move is the proof: record
  ownership at MINT (attempt identity + the pane's pid/start-time, which
  no unprivileged process can forge) instead of inferring it at
  observation from a process name. Foreign then means absent-from-the-
  ledger, and the 2.1.220 fingerprint edge dissolves with it.

  ts-wf-messaging — "we'll also need cross session comms/messaging?"
  Recorded rather than built, because its carrier is the SAME durable
  state ts-wf-stage-artifacts and ts-dispatch-ownership-mint need;
  building a messaging channel on its own storage would fork that state
  three ways before it exists once.

ts-dispatch-redispatch updated: U1 is its fix, and the resume-vs-fresh
handback is resolved fresh-per-attempt with the reason recorded — a
resumed session inherits the prior attempt's context, which is exactly
what ts-wf-belt-refusals rules out for NeedsRework. The two rows would
otherwise disagree about the same question. Status stays NOT done: merge
is not done, only a verdict moves it.

Receipts, all green by execution against the merged tree:
  - witness_admission_deferred_rows_have_consumers (the exact CI check) ok
  - 15-witness battery incl. the byte-oracle digest parity, unmoved by
    main's seven commits, and the route-count pin at 13
  - roadmap_page_keystone_holds — the brief-budget census admits both new
    rows under the 100-word wall
  - roadmap_authority_witnesses; ROADMAP.md regenerated, both rows
    rendering under their parents

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@briansrls

Copy link
Copy Markdown
Contributor Author

Red CI, diagnosed — two failures, one of them mine.

heal_generated_artifacts — not this branch. main's #7250 renamed the seed builtin (shell_materialize_argv_for_operation → shell_materialize_operation_argv) and re-homed dag/gunbc/hostname_read.dag onto the new v2.std.operation_argv types. This branch was seven commits behind and carried the old spelling. Merging main is the entire fix; the diff doesn't touch that file.

ci — mine, and the invariant was right. Phase 0(b) refused belt_attempt_grain_holds as an enrolled row naming zero executing consumers. The row is pure — content hashes and string derivations, no host reach — but I had parked it in roadmap_belt_actuate_witness_test.dag, which is BinWitnessWet-excluded from discovery because its other witnesses run real host commands. So a claim that could run every PR was deferred to a cadence it never needed, and then never ran at all.

The fix moves the row rather than enrolling it. It now sits in dispatch_attempts_witness_test.dag beside U3's parse witnesses, which is the pairing that was always right: this row proves the mint varies per attempt, attempts_parse_roundtrips_the_minted_branch proves the read recovers exactly what was minted — one authority, both directions. Adding it to the wet roster would have greened CI by mislabelling a pure claim as host-executing, which is the quiet lie the invariant exists to catch. A note stays at the old site: deferral is a property of the execution a row needs, never of the module it happens to name. Three imports went dead with the move and are deleted.

Receipts, against the merged tree:

  • witness_admission_deferred_rows_have_consumers — the exact CI check — ok
  • 15-witness battery green, including the byte-oracle digest 28c06723684117e4 unmoved by main's seven commits, and the route pin at 13
  • roadmap_page_keystone_holds — the brief-budget census admits both new rows under the 100-word wall
  • ROADMAP.md regenerated; both new rows render under their parents

Two rows recorded from the operator's questions at the U4 receipt (ts-dispatch-ownership-mint, ts-wf-messaging) and ts-dispatch-redispatch updated — U1 is its fix, and the resume-vs-fresh handback is resolved fresh-per-attempt with the reason on the row: a resumed session inherits the prior attempt's context, which is exactly what ts-wf-belt-refusals rules out for NeedsRework. Status stays not done — merge is not done; only a verdict moves it.

@briansrls
briansrls merged commit 42f7a9d into main Jul 26, 2026
5 checks passed
@briansrls
briansrls deleted the session/dispatch-loop branch July 26, 2026 12:53
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