Story 2073: Context Engine CE-21 — FramerInput.context_bundle (strictly additive)

CE-14 (Story-2072) put the pinned context_bundle.v1 on the broker's wire, for the Mira command/converse rail. CE-21 does the same for the other matter-evidence consumer — Vega, the frame builder — but through a different door: the framer_trigger worker that fires on matter.created and assembles Vega's stateless FramerInput. The bundle rides strictly additively; the raw documents field is retained exactly as today (RFD docs/notes/rfd-context-engine.md §10 consumers table, §14.3 row CE-21, §5.4).

WHY — the first-frame race is permanent, so documents stays

framer_trigger fires on matter.created — before any card, chunk, or graph node can exist. A bundle fetched at that instant is empty by construction. So the raw documents field is retained indefinitely as the first-frame grounding, and the bundle is additive enrichment for later frames. Any future retirement of FramerInput.documents is gated on a parity eval that includes the first-frame case (§10; FramerInput's versioning stays extend-only per pramaan-docs ADR-153). CE-21 changes nothing about the first frame — it only lets a warm matter's later reframes see the assembled bundle.

What changed

SurfaceChange
src/workers/framer_trigger/processor.py — attach_context_bundle() (new) A cache READ, never a build. Reads the pinned context.bundles row at Vega's axes — brief × L, the widest recipe at the biggest tier (the framer edits the whole brief) — and, on a hit, stamps framer_input["context_bundle"] = bundle.model_dump(...). Runs in its OWN session with its own try: a miss or error is enrichment, never a run-breaker, and never poisons the already-committed claim transaction. Mirrors CE-14's prefetch_context_bundle fail-open doctrine.
src/workers/framer_trigger/processor.py — process_matter_created() One call inserted after the claim + run row COMMIT, before the payload leaves: await attach_context_bundle(deps, firm_id=…, matter_id=…, framer_input=claimed.framer_input). The raw documents assembly in _build_framer_input is untouched.
FRAMER_BUNDLE_PROFILE = "brief", FRAMER_BUNDLE_BUDGET = "L" Vega's pinned-bundle axes, named as constants (RFD §14.3 CE-21 get_bundle(brief, L)).

Where the bundle attaches — after the commit, beside documents

process_matter_created
  └─ _claim_and_mint_run           # atomic lease claim + agent_run row (COMMITS)
  └─ attach_context_bundle         # ← CE-21: own session, cache read, fail-open
       └─ framer_input["context_bundle"] = bundle.model_dump()   # only on a HIT
  └─ build_vega_invoke_payload     # input carries documents (always) + bundle (if warm)
  └─ invoke_agent(payload)

The bundle read is deliberately its own session, separate from the claim transaction: the claim + run row are committed before the enrichment runs, so a bundle-store error can never roll back a minted run. This is the same fail-open ordering the CE-14 broker uses when it stamps context_bundle beside the Memory Pack.

"Done" = the empty first-frame produces today's behaviour, unchanged

The acceptance criterion is that the first frame is byte-for-byte what it was before CE-21. Three unit tests (tests/unit/test_framer_trigger_processor.py) lock it:

Scope boundary — what CE-21 does NOT build

The pramaan-agents FramerInput mirror is unchanged this story: the functions side builds a plain dict and adds one optional key, forward-compatible with the Vega schema gaining context_bundle when its own story lands (additive only). No migration — the bundle is read from the existing context.bundles store (CE-13). Building on a cache miss is not done here — that is the debounced rebuild worker's job (§9); the trigger only reads, keeping the invoke path DR-1-bounded. Frame provenance (CE-23/CE-24) sits after CE-21 and is out of scope. Retiring documents is gated on the first-frame parity eval (§10) and is explicitly not attempted.

Verify

uv run ruff check
uv run pytest tests/unit/test_framer_trigger_processor.py \
              tests/unit/test_framer_trigger_handler.py \
              tests/unit/agent_contracts/test_golden_parity.py   # frame parity green
make openapi   # app/admin snapshots unchanged (FramerInput is not an HTTP DTO)

Recall: CE-21 attaches the pinned context_bundle.v1 (brief × L) onto Vega's FramerInput in a fail-open, own-session read after the claim commits — strictly additive beside the raw documents that stays as the permanent first-frame grounding, so an empty first frame produces today's behaviour unchanged.