Story 2152: Wave B — surface_context stamping

Wave B of the founder's viewer plan: "Mira always knows which page counsel is on." CE-14 (Story-2072) put a pinned ContextBundle on agent_invoke.v1 at launch. Story-2152 adds — strictly additively — the one fact that bundle didn't carry: the surface counsel is looking at. When that surface names a document, the same pinned bundle is scoped document-first, and the surface rides to the runtime so Mira's prompt can say "Counsel is viewing: HARSHA_VENKATESH_PLAIN.pdf."

WHY — a document-blind bundle answers the wrong question

A matter's pinned bundle is matter-wide: every document's cards, chunks, and assertions, budgeted and ordered by the builder. But counsel is not asking about the whole matter — they are on one page, reading one document, and their question ("what does this say?", "is this consistent?") is about that document. If the bundle leads with matter-wide content, the document counsel is staring at can fall past the budget's most-attended region (lost-in-the-middle), and Mira answers as if it never saw the page. Wave B tells the broker which document, so that document's evidence leads.

This is orientation, never authority: document_id only reprioritizes a read that already runs under the caller's RLS — naming a document cannot widen what that caller may see.

What changed

SurfaceChange
src/shared/agent_contracts/session_context.py New SurfaceContext model — { surface_key, params }. surface_key is a durable id validated by the shared _reject_url_like_surface_key (never a raw URL, same grammar as SurfaceEntry); params is a typed bag ({document_id: …}).
src/shared/agent_contracts/invoke.py Additive optional AgentInvoke.surface_context: SurfaceContext | None, beside memory_pack/context_bundle. Absent → the brain behaves as today (fail-open). No matrix rule: surface is orthogonal to kind, so any launch may carry it.
src/shared/agent_commands.py Contract-first: AgentCommand.surface_context (launch-only, dropped from the wire when absent) so the app-side sender — a sibling story after codegen sync — can populate it. The non-launch matrix rejects it, like every other launch-only field.
src/shared/context/bundle_prefetch.py prefetch_context_bundle(…, focus_document_id) + scope_bundle_document_first(bundle, document_id): within every section, items grounded in that document float to the front (stable), matter-wide items follow. Item SET, section order, and version_hash are unchanged — priority over the same pinned evidence, not a rebuild.
src/clerk_worker/runner.py _stamp_prefetch_packs reads surface_context.params.document_id (defensively — a malformed value degrades to None) and passes it to the prefetch as focus_document_id. _invoke_brain stamps surface_context onto the envelope beside the bundle.

Document-first, not sandwich-reorder

The prompt sandwich is untouched: system → frame@ver → memory → bundle@version_hash → per-turn suffix. Section order is untouched: cards → chronology → evidence → assertions → contradictions. Document-first reprioritizes only within each section:

cards:  [docB, docA, docB]   --scope(docA)-->   [docA, docB, docB]
evidence: [docB, docA]      --scope(docA)-->   [docA, docB]     (via span.document_id)
assertions:[docB, docA]     --scope(docA)-->   [docA, docB]     (via spans[].document_id)

Every bundle item type carries document identity — a DocumentCard directly, an EvidenceHit via its span, an Assertion/ChronologyEvent/ContradictionItem via its spans — so one helper covers all five sections.

Why the version_hash stays the same (the pin holds)

CE-14's invariant is that every panel role of a run reads one version_hash. The broker scopes the bundle once and pins the scoped bundle for the whole run, so all roles still read exactly the same bytes. Reordering is broker-side presentation over the same evidence set; it is not a rebuild — rebuilding inline would blow the Lambda round-trip budget (DR-1). If the model must one day see a hash that covers item order, the upgrade path is to fold focus_document_id into the debounced builder's hash input (noted in a ponytail: comment).

Acceptance criteria → proof

ACTest
Invoke with surface_context(document_id) → bundle demonstrably document-first test_document_named_surface_scopes_the_bundle_document_first (drives process_command end-to-end; DOC_A is cached second, leads the invoke's bundle once the surface names it) + test_document_first_floats_the_named_document_within_each_section (pure, all three item accessors).
Invoke without surface_context → byte-identical to today (regression) test_absent_surface_context_stamps_no_surface_context (envelope surface_context is null — degrades exactly like memory_pack/context_bundle) + test_bundle_unchanged_when_surface_context_absent (a cached bundle keeps its stored item order — no reshuffle).
Contract additive only; snapshots + permission map regenerated test_surface_context_is_optional_and_defaults_absent, test_surface_context_rides_additively_and_round_trips, test_surface_context_rejects_url_shaped_surface_key; make openapi + make gen-permissions produce no diff — agent_invoke.v1/AgentCommand are internal broker/SQS contracts, not HTTP DTOs (same as CE-14).

Fail-open, everywhere

A missing surface → nothing stamped, bundle untouched. A surface with no document_id → the surface still rides to the runtime (Mira states the page), the bundle is not reordered. A malformed document_id → parsed to None defensively, no scoping. A document named that has no items in the bundle → each section is returned untouched. Surface context is an enrichment, never a dependency.

Closes pramaan-io/pramaan-spec#2152.