Skip to content

feat(worker): claim-exempt secondmate message lane (R2/R3 PR 3) - #266

Merged
ruby-dlee merged 3 commits into
mainfrom
fm/secondmate-message-lane
Aug 20, 2026
Merged

feat(worker): claim-exempt secondmate message lane (R2/R3 PR 3)#266
ruby-dlee merged 3 commits into
mainfrom
fm/secondmate-message-lane

Conversation

@ruby-dlee

@ruby-dlee ruby-dlee commented Aug 20, 2026

Copy link
Copy Markdown
Owner

What

R2/R3 PR 3 (R2R3-DESIGN.md section C item 3, contract from B.1/B.4/B.9/D.3): the claim-exempt secondmate message lane. Includes the fixes from the adversarial review (incremental collect, digest-proven put replay, role-scope and fencing contract lines).

  • bin/fm-azure-worker-provider.py: two new raw provider verbs, message-put and message-collect, dispatched exactly like inventory (no claim, no lease, no landed-code gate, never reachable through the mutate verb). message-put uploads one bounded payload to the slot's own state container at a content-addressed name: session/in/<sha256>.json (valid JSON, 262144-byte bound) or session/in/attach/<sha256>.bundle (binary, 256 MiB bound, no JSON requirement). Idempotent by content address: a replay converges only when the existing blob's stamped content_digest metadata matches the computed digest (and the size matches); a mismatch, a same-length different-bytes blob, or a digestless foreign writer refuses. message-collect walks session/out/ incrementally: an existing local name is judged WITHOUT a transfer (digest metadata when the writer stamped it, exact size for digestless guest-written blobs; the monitor's chain verification owns full integrity), so polls never re-pay collected history; new blobs download under a per-call transfer budget equal to the attach ceiling; the walk takes an optional --after <local-name> cursor, follows the service continuation marker across truncated listing pages (bounded), processes one bounded page per call, and reports cursor and more instead of ever hard-refusing a deep mailbox. It does NO chain verification (the PR 4 monitor owns that) and never deletes or overwrites a local file: divergence refuses. require_session_blob_name enforces the D.3 namespace boundary inside the op functions (any name outside session/, or a traversal alias, raises ProviderError), and the docstrings state the carve.
  • bin/fm-worker-lifecycle.py: message-put/message-collect commands. Each verifies the exact assigned worker with command_execute's own gates (assigned status, exact assignment generation, no release proof), read-only under the fleet lock, then calls the provider WITHOUT make_action/claim_pending/apply_pending/slot_lease: the one deliberate claim-exempt carve (design B.1). Neither op ever writes controller.json. The collect subprocess deadline is sized from the same constant the provider's per-call transfer budget equals, so the deadline covers what one call can actually fetch. The message verbs stay out of ACTION_TYPES so a message spec can never be stored as a pending claim.
  • bin/fm-worker-lifecycle.sh: dispatch entries through the gated lane.
  • docs/azure-workers.md: the B.9 carve sentence, verbatim, placed in the claim-contract paragraph.

Why

A leg's execute claim occupies pending_actions[slot] for its whole wall and claim_pending refuses any different key on that slot, so a claimed message lane could never deliver during a leg, precisely when delivery matters (design B.1, defect ii of proposal A). Idempotency for this payload class comes from content addressing, which is stronger than a claim; the exemption is safe because the ops touch no compute, no money, and no lifecycle state.

Contract notes for PR 4

  • Role scope (interim): until PR 4's spawn lane creates compartments, no secondmate worker exists to address, so the lane deliberately serves author-role workers as well as secondmate compartments; PR 4/6 narrows the callers to compartments. Stated in both op docstrings and the controller helper.
  • Delivery fencing: a slot-addressed transfer runs outside the controller lock, so a late message-put can land in a recreated slot's container. The PR 4 monitor therefore stamps assignment_generation inside every message envelope and the session runner refuses envelopes naming a foreign generation. The contract line lives in the provider module docstring and the message_put docstring so PR 4 implements both sides.

Tests

bash tests/fm-worker-lifecycle.test.sh: 22/22 units green (19 existing + 3 new), plus green runs of fm-azure-pilot, fm-spawn-cloud, fm-azure-runner, fm-gate-refuse, fm-worker-outcome-transport, and bin/fm-lint.sh / shellcheck on the touched shell files.

  • message_lane_provider_contract: drives the REAL provider ops against a stubbed az that counts download invocations. Covers content-addressed naming; replay convergence with zero re-upload; exact refusal strings for the 262144-byte JSON bound, non-JSON payloads, empty payloads, and the attach bound (via the constant seam, never a 256 MiB fixture); replay divergence via digest metadata including the same-length different-bytes case and the digestless-foreign-writer case; namespace guard refusals (foreign names, traversal aliases, nested paths, unbounded sizes, symlinked local targets, malformed cursors); collect proving existing-name blobs are NOT re-downloaded (download counter pinned across the skip, the divergence refusal, and the digestless same-size skip), digestless size-mismatch refusal, cursor pagination collecting a mailbox deeper than the per-call page across three calls (page seam), the bounded marker walk collecting a five-blob mailbox through two-entry listing pages in one call, and the per-call transfer budget stopping early with an honest cursor (budget seam); plus an end-to-end stdin invocation of the real provider binary proving main() routes message-put into the same bounded op.
  • message_lane_claim_exemption: THE test the design says proposal A would have failed. Wedges a durable execute claim on the slot (outcome-disposition skew), then message-put and message-collect both succeed across it; positive control: a fresh compute-mutating execute on the same slot still refuses with the different-key claim refusal, and the durable claim survives the message traffic byte-identically. Also proves controller.json is byte-identical across every message op, CLI content-address/replay/attach behavior, collect fetch/skip/divergence/cursor (--after) through the wrapper, and the refusal shapes for unassigned tasks, wrong generations, and malformed flag pairs.
  • message_lane_static_contract (design D.3): AST pin that provider inventory reads exactly three named blob records (reservation.json, request.json, result.json) through blob_record's exact-name show and contains no blob-list call; pins the message commands to zero claim-machinery call sites, mutate still refused outside provider_mutate, message verbs absent from ACTION_TYPES, the size constants in step across controller and provider, the transfer budget equal to the deadline-sizing constant, the mailbox-depth hard refusal staying gone, the never-re-downloads and role-scope docstring lines, the assignment_generation fencing contract in both the module and op docstrings, the verbatim carve sentence within four lines of the claim contract, and the wrapper dispatch.

Mutation proofs (each proven red, then reverted)

  1. Removed the JSON size bound in message_put -> message_lane_provider_contract red: AssertionError: no refusal containing 'message payload exceeds its 262144-byte bound'.
  2. Widened require_session_blob_name to accept any string -> red: AssertionError: no refusal containing 'outside the session/ namespace'.
  3. Routed command_message_put through the durable claim path (claim_pending before the provider call) -> message_lane_claim_exemption red: AssertionError: a message op rewrote controller.json (and the static pin also goes red on the claim_pending( call site).
  4. Made collect overwrite divergent local files -> red: divergence assertion fired with the silently-overwritten fetch result.
  5. Removed the exists-skip branch so collect re-downloads history -> red: the second collect fetched instead of skipping (both the skip assertion and the download-counter assertion cover it).
  6. Reverted the put replay check to length-only -> red: AssertionError: no refusal containing 'differs from its content address' on the same-length different-bytes case.

Honest limits

  • Live blob-plane behavior (private-endpoint DNS, PUT/GET/LIST semantics, container lastModified) is proven separately by the D.2 probe, which PASSED from a real worker on 2026-08-20 through the sanctioned execute lane (d2-worker-probe-evidence-2026-08-20.json); this PR's tests are hermetic against a stubbed/fixture az by design.
  • A digestless guest-written blob whose local copy matches by exact size is presumed already collected without a transfer; the PR 4 monitor's sequence/chain verification owns full integrity (design B.4), and any digest-stamped blob is judged by digest.
  • One call collects at most one processing page and one transfer budget of new content; a deeper mailbox drains across successive calls via the reported cursor (the PR 4 monitor polls anyway).

…ant carve

message-put/message-collect as their own provider verbs dispatched like
inventory: bounded, content-addressed, idempotent data-plane blob transfers
in the slot's own state container, enforced to the session/ namespace inside
the op functions. Controller commands verify the exact assigned worker
read-only and never touch claims, leases, or controller.json. The carve
sentence lands next to the claim contract in docs/azure-workers.md.
… pin

Fixture provider grows the two message verbs; the real provider ops run
hermetically against a stubbed az for bounds, content addressing, replay,
namespace, and collect semantics; the e2e unit proves message-put succeeds
across an outstanding execute claim while a fresh compute action refuses;
the static unit pins inventory to exactly three named blob reads and the
message commands to zero claim-machinery call sites.
…ut replay proves the digest

Adversarial review of the message lane: an existing local name is now judged
without a transfer (digest metadata when stamped, exact size for digestless
guest blobs), so a poll no longer re-downloads the whole outbox and cannot
grow past the deadline; the 4096 hard refusal is replaced by a name-ordered
--after cursor with a bounded marker walk, a per-call processing page, and a
per-call transfer budget equal to the constant the subprocess deadline is
sized from; put replay convergence now requires the stamped content_digest,
refusing same-length different bytes and digestless foreign writers; the
interim both-roles scope and the PR 4 assignment_generation delivery-fencing
contract are stated in the docstrings.
@ruby-dlee
ruby-dlee merged commit faf8f0f into main Aug 20, 2026
13 checks passed
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