Story-3291 Walkthrough

Outcome: when Blake carves a bundle, he now writes one small file โ€” runs/<run>/steps/1-carve.json โ€” and his run ends there. S3 rings a bell on that file, and the broker starts cutting the sixteen members while Blake is already finished. Nothing else about the carve pass changed: same prompt, same seat, same one model call, same durable artifact at the end.

This is the agents half of one wave. The broker mints the envelopes (pramaan-functions Story-3287), the S3 โ†’ EventBridge โ†’ SQS ingress is Terraform (pramaan-infra Chore-3288), and the consumer that acts on the drop is Story-3290. This PR is the piece in the middle: Blake writing the file.

๐Ÿ›ฐ๏ธ Satellite โ€” why a bundle run had to be cut in half

A 337-page bundle used to be one Blake run: carve it into sixteen members, then classify, card and segment all sixteen, holding everything in memory and writing a single file at the very end. Three bills came with that:

ADR-180 splits the worker in two. The carver looks at a pile of paper and says "this is sixteen documents". The clerk takes ONE document and registers it properly. They are different jobs, and they are now different runs โ€” which means member #11 failing is a retry of member #11, not of the bundle.

โœˆ๏ธ Airplane โ€” the one new file, and who reads it

bundle upload
   โ””โ”€ broker invokes Blake  mode="carve"
        โ”œโ”€ 1 model call: "where do documents START?"
        โ”œโ”€ PUT  runs/run-991/steps/1-carve.json     โ† NEW (this story)
        โ”‚        โ””โ”€ S3 fires an event โ”€โ†’ EventBridge โ”€โ†’ SQS โ”€โ†’ the broker
        โ”‚              โ””โ”€ cuts 16 PDFs ยท registers members ยท invokes the MAIN clerk
        โ””โ”€ PUT  the run's durable artifact                โ† unchanged
      run ENDS. No classify. No card. No segment.

Both PUTs go through the same one function, agents/_shared/result_delivery.py. That is the point worth staring at: the story adds a destination, never an egress path. Blake still holds no S3 key, no AWS SDK, and no bucket name; he holds two one-time signed URLs the broker minted for him, and CI still fails the build if a second HTTPS PUT site appears anywhere in agents/.

๐Ÿ”ฌ Microscopic โ€” what the drop actually says

{
  "schema_version": "step_drop.v1",
  "run_id": "run-991",  "document_id": "doc-42",  "bundle_document_id": "doc-42",
  "step": "carve",      "member": null,           "generation": 1,
  "drop_id": "doc-42:1:carve",
  "main_member": 0,
  "expected_members": [0, 1, 2, ... 15],  "expected_member_count": 16,
  "payload": {
    "status": "ok", "bundle_id": "bundle-7", "schemes": ["REGULATOR-ID"],
    "members": [
      {"member": 0, "role": "main_document", "title": "Complaint",  "page_start": 1,  "page_end": 34,  ...},
      {"member": 1, "role": "annexure",      "label": "NFRA/2",     "page_start": 35, "page_end": 62,  ...}
    ]
  },
  "proposed_effects": [ {"schema": "document.family.v1", "data": { ...today's carve effect... } } ]
}
FieldWhy it exists
payload.members Blake keeps the main document (head) and the annexures (members) in separate fields, deliberately โ€” they are different things to him. The broker cuts, registers and clerks all of them as ONE ordered list, and every later drop is addressed member-NN. Somebody has to declare that numbering once, or sixteen consumers each invent their own.
main_member D18 ordering: the main member's clerk runs FIRST, and the fifteen annexure clerks fan out carrying its card. Declared, never inferred.
expected_members + count "Is this bundle finished?" is answered from this declared set, never from how many queue messages happened to arrive. Delivery order is not semantic order.
drop_id The dedupe key, derived from what the drop is (document:generation:step) โ€” never from how it travelled. An SQS message id changes on every redelivery, so a consumer keyed on one would apply the same work twice.
generation Re-carve identity. It comes off the envelope the broker minted; the brain never invents one, because only the broker knows this is the second carve of doc-42.
proposed_effects Blake calls no tools and writes nothing (registrar.yaml pins tools.allowed: []). He proposes; the broker validates and applies. Today's document.family.v1 rides across verbatim, so the broker's existing family-persist code keeps working untouched.

Three ways this could have gone wrong, and what stops each

FailureWhat happens instead
The envelope for the carve step never arrives. Fails closed before the run is even registered โ€” StepEnvelopeMissingError, with remediation naming step_envelopes. Discovering it after the model call would burn a bundle-sized call on an answer with nowhere to land.
The drop PUT fails, quietly. The run's own artifact is written with outcome: "failure" and failure_stage: "step_drop". A silent drop failure would leave the broker waiting forever for a bell that never rings.
The run failed, but a drop gets written anyway. A drop is a claim that a step SUCCEEDED, so a failed run writes none โ€” and logs that it wrote none, with the reason. A branch that exists so nothing happens must say it took itself (the Story-3115 lesson: a guard that silently skipped both branches shipped and ran zero times for eleven merges).

What did NOT change

Where to look

agents/_shared/step_drop.py          the contract: StepEnvelope + StepDropV1 + the drop id
agents/_shared/artifact_runtime.py   picks the step's envelope, writes the drop, then the artifact
agents/registrar/carve_drop.py       Blake's carve builder โ€” the numbering lives here
agents/registrar/main.py             _STEP_DROP_BY_MODE = {"carve": CARVE_STEP_DROP}
agent_contracts/schemas/step_drop.v1.schema.json    the generated artifact under custody

tests/test_step_drop_contract.py                    exact key ยท fail-closed ยท no-drop-on-failure
agents/registrar/tests/test_registrar_carve_drop.py numbering ยท expected set ยท run ends
Merge order. The wire contract is the 2026-08-02 amendment on pramaan-docs PR #283 (branch adr/180-blake-step-drops), which is still OPEN. This code is written against it and must not merge before it is accepted.