Repository navigation
Close the dispatch loop: attempt-grain redispatch, sessions panel, results, stop (U1–U4) - #7266
Conversation
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>
|
Live drive receipt (local serve, pre-merge): 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 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>
|
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 ( Live drive, four arms against real tmux: the operator's hand-made Named finding, landed as a carrier row: a real claude on macOS reports its version string ( 6 new witnesses; route pin 12→13; digest re-pinned 🤖 Generated with Claude Code |
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>
|
Red CI, diagnosed — two failures, one of them mine.
The fix moves the row rather than enrolling it. It now sits in Receipts, against the merged tree:
Two rows recorded from the operator's questions at the U4 receipt ( |
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.jsonprojects 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-refusedbanner — 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.jsonlistsrefs/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
c4eabee5bdfb8021(U2 banner) →c020930b7552052e(U3 row).LiveTreeDispositionreference 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 · livewith the tmux name in the title. The disclosure listsattempts: dispatch/<node>-a<key>. Stop the session, press again → a second attempt spawns cleanly. Your accidentalts-authority-convergespawn appears as a legacy attempt on its row.🤖 Generated with Claude Code