Status:
draft · Version 0.1 · Filed 2026-04-26SPEC-046 v0.1 — Agent Surface Adapters: Per-Surface Lifecycle Modules for prism_start / prism_wrap
Version
0.1Status
draftOrigin
Two converging signals from the 2026-04-26 work surface this:-
The SPEC-044 idle-wake fix (commit
4652772) addedchannel_bridge.capture_session_lazy()toprism_startandchannel_bridge.reset_all()toprism_wrap. Both calls are Claude Code-specific — Codex, Cursor, and Claude Desktop have no use for the channel bridge — but they live unconditionally on the hot path of every bootstrap and every wrap. Today they’re inert no-ops on non-channel surfaces; tomorrow’s Codex App Server inject hook (SPEC-035 trajectory) will need its own bootstrap+wrap pair, and conditionals will start to multiply inline. -
mcp/strategies/already implements the right pattern at half-scale.select_strategy(surface)returns aSignalDeliveryStrategyperPRISM_AGENT_SURFACE, and adding Codex inject support reduces to “one new class + one map entry.” That pattern exists because signal delivery is genuinely surface-specific. It belongs to a wider concern: the entire MCP-process lifecycle is surface-specific in places, not just signal delivery.
§1 — Goals
- Single dispatch site for surface-specific behavior. Adding a new agent surface (next candidates: ChatGPT Desktop, ACP-compliant editors) is one new file plus one registry entry.
- Symmetric
prism_start/prism_wraplifecycle. Whatever a surface attaches on bootstrap, it detaches on wrap. No more “the channel bridge release is in prism_wrap because there was nowhere else to put it.” - No surface-specific code on the hot path of
prism_start/prism_wrapcores. Both verbs read the surface once viaget_surface()and delegate. - No new env-var plumbing or argument propagation. Surface
resolution stays on
PRISM_AGENT_SURFACE(already exported bybin/coder.sh,bin/coder.ps1, andinstall/install.pyper SPEC-032 Phase A identity-fix delta).
Non-goals (v0.1)
- Status-card rendering hints. Today’s status card is uniform across surfaces and no surface has asked for variation. Pull into the surface contract only when concretely needed.
- Identity-elicitation response shape. Same reasoning.
- Heartbeat cadence overrides. Heartbeat is surface-agnostic — every surface refreshes the same Redis TTL.
- Any change to the WebSocket session stream (SPEC-045) endpoint or
framing. The strategy attached to the stream is surface-supplied;
the stream itself stays in
prism_startcore.
§2 — Surface Contract
BootstrapContext / WrapContext are minimal dataclasses carrying the
in-process state a surface might legitimately need. Each new field is
justified by a real surface requirement; no speculative additions.
§3 — File Layout
get_surface() reads PRISM_AGENT_SURFACE env, looks it up
in SURFACE_REGISTRY (a dict of name → class), falls through to
OtherSurface() for unknown values. Memoized per-process via a module-
level singleton. Lookup cost is amortized to zero after first call.
§4 — Integration with prism_start
The replacement atmcp/server.py prism_start (line ~1764–1840) is:
ClaudeCodeSurface.on_bootstrap performs:
on_bootstrap is empty (pass).
§5 — Integration with prism_wrap
The replacement atmcp/server.py prism_wrap (line ~2278–2281) is:
ClaudeCodeSurface.on_wrap performs:
on_wrap is empty.
§6 — Cutover Plan
Phase 1 — extract ClaudeCodeSurface only
- Create
mcp/agent_surfaces/package with protocol, registry,OtherSurfacefallback. - Move
channel_bridge.capture_session_lazy+reset_coalescingintoClaudeCodeSurface.on_bootstrap. - Move
channel_bridge.reset_allintoClaudeCodeSurface.on_wrap. - Move signal-strategy selection from
strategies.select_strategyinto surface modules.strategies.select_strategybecomes a thin back-compat shim that delegates toget_surface().signal_strategy(). - All non-claude_code surfaces resolve to
OtherSurface()until Phase 2 — piggyback strategy, empty hooks. No behavioral change for them. prism_startandprism_wrapeach gain oneawait surface.on_*(ctx)call site replacing the inlinechannel_bridge.*logic.
Phase 2 — fill in named surfaces
CodexSurface,ClaudeDesktopSurface,CursorSurfaceeach become thin classes:name+signal_strategy()returning piggyback + empty hooks.- These exist for clarity (one file per known surface) and as the
insertion point when surface-specific behavior actually arrives
(Codex
inject_itemsbeing the obvious first case).
§7 — Verification
prism_starton Claude Code: channel bridge captured. The existing SPEC-044 idle-wake doorbell smoke remains green.prism_wrapon Claude Code: channel bridge released; no captured handles persist between sessions inside the same MCP process.prism_startwithPRISM_AGENT_SURFACE=codex: nochannel_bridge.*calls fire; signals route via piggyback.prism_wrapwithPRISM_AGENT_SURFACE=codex: nochannel_bridge.*calls fire;on_wrapis a no-op even ifon_bootstrapnever ran (bootstrap-aborted-on-identity_conflict path).- Unknown
PRISM_AGENT_SURFACEvalue: resolves toOtherSurface, piggyback delivery, no errors. One-lineWARNINGlog naming the unknown surface. - Re-bootstrap in same MCP process (
prism_start→prism_wrap→prism_starton a different PID):on_bootstrapcalled twice without crashing;on_wrapcalled twice without crashing. - Existing SPEC-044, SPEC-045, and SPEC-038 smokes all green.
mcp/server.py prism_startno longer mentionschannel_bridgeby name (lint-style assertion). Same forprism_wrap.
§8 — Decisions
- D1 — Lifecycle hooks only. v0.1 surface contract owns bootstrap
- wrap + signal strategy. Status-card formatting and identity elicitation response shape are NOT in the contract; pull in only when a second surface concretely requires variation.
- D2 — Env-var dispatch, no argument plumbing.
PRISM_AGENT_SURFACEis the contract. Surface is read once at first call (memoized). Adding asurface=argument toprism_start/prism_wrapis rejected as redundant — the env var is already set by every supported launcher (coder.sh,coder.ps1,install/install.pyeditor configs). - D3 —
strategies/stays. The strategy classes (ChannelsPushStrategy,PiggybackStrategy) keep their current home. Surface modules instantiate strategies; they don’t redefine them.strategies.select_strategybecomes a thin back-compat shim and is a candidate for full removal once no caller imports it directly. - D4 —
OtherSurfaceis the safe default. Unknown surfaces never raise; they get piggyback + empty hooks + a one-line warning. Same fail-soft posture as SPEC-021 §4.1’s three-tier identity resolution. - D5 — Async hook signatures from v0.1. Even though Claude Code
is the only currently-async surface concern, Codex
inject_itemswill be async (network IO into the App Server). Keeping the contract async from v0.1 means no signature break when Codex lands.
§9 — Open Questions
- Q1 —
on_bootstraporder vs WebSocket stream spawn. The stream is constructed from the surface’ssignal_strategy(), so the surface must exist first. The hook fires AFTER the stream spawn because the surface may want a stream-up handle for any post-spawn attach work. Confirm during implementation. - Q2 —
surface.on_signal_received(signal)post-delivery hook? Today the strategy IS the delivery point. A separate hook would be useful for surface-specific logging or telemetry, but no caller has asked. Defer. - Q3 — Memoization invalidation. If
PRISM_AGENT_SURFACEwere to change inside a running MCP process (very unlikely — env is read once at coder launch), the memoized surface would go stale. Resolution: document the contract as “surface is fixed at process start”; do not invalidate. Same posture as_agent_identity()today.
§10 — Performance Consciousness
The surface object is constructed once per MCP process and memoized. Per-bootstrap cost: one dict lookup + one method dispatch. Per-wrap: same. No hot-path cost added. Thechannel_bridge.* calls already
happen on the hot path today; this spec just moves them inside a
surface method.
§11 — Authorship
- Author: Donna (Claude Code, session 973926f3) 2026-04-26.
- Trigger: chat-level architecture discussion with Frank, recall
surfaced when reviewing commit
4652772(the SPEC-044 idle-wake fix that added Claude Code-specific calls to bothprism_startandprism_wrapwithout a surface abstraction to host them). - Reviewer: Lola (Desktop) — should review the surface contract shape (§2) and the cutover phase split (§6) before any code lands.
- Implementation gate: Frank’s GO. Do not touch
mcp/server.pyprism_start / prism_wrap until the spec is reviewed.
§12 — Relationship to Other Specs
- Generalizes SPEC-034 §7 (signal delivery strategies). The
strategies/package keeps its strategy class definitions; the surface→strategy mapping moves into surface modules. - Resolves the surface-specificity smell introduced by SPEC-044’s
channel bridge calls in
prism_start/prism_wrap(commit4652772). Those calls becomeClaudeCodeSurfacemethods. - Pre-positions SPEC-035 trajectory work (Codex App Server inject_items). The surface module is the natural insertion point.
- Does not modify SPEC-045 (WebSocket data plane) — the stream
itself stays in
prism_startcore; surfaces only supply the strategy attached to it. - Does not modify SPEC-038 (collision policy + operator-gated force) — controller registration stays in core; surface hooks fire AFTER registration succeeds.
- Consistent with SPEC-033 §4 Layer 4 tiered signal delivery (Tier 1 channels / Tier 2 inject / Tier 3 ACP / Tier 4 piggyback). One surface module per tier-implementation is the natural shape this spec implements.

