Status:
draft · Version 0.2 · Filed 2026-04-25SPEC-041: prism_start Signal Separation — Session Lifecycle Does Not Multiplex with Messaging
Version
0.2Status
draftOrigin
prism_start currently drains pending signals as part of its bootstrap
return payload (mcp/server.py:1649-1661). Frank’s principle: prism_start
is for starting a session, not for getting signals. Conflating the two:
- couples session lifecycle to messaging — every future change to either has to think about both;
- creates a race: signals are drained against the resolved identity before the new session is registered, so they target an identity, not a session;
- makes the verb’s contract grow over time, opaque to vibe-coding callers.
prism_start and codifies the separation
as a methodology rule. Per Q1 decision, the rule extends to all three
session-lifecycle verbs (prism_start, prism_checkpoint, prism_wrap).
Per Q3 decision, the rationale is captured in ADR-25 with a one-line
rule mirrored to PRISM.md.
§1 — Rule
Session-lifecycle verbs MUST NOT drain or return pending signals. Three verbs are in scope:prism_start, prism_checkpoint, prism_wrap.
- Their response payloads MUST NOT contain a
pending_signalsfield. - The
require_bootstrapdecorator (which gates these verbs and applies the SPEC-037 per-verb piggyback drain to all bootstrapped verbs) MUST special-case all three — drain is skipped for any verb in this set.
- Explicit verb:
prism_signals_pending(pid)— agent’s deliberate poll. Idempotent, drains undelivered signals for the caller’s identity. - Implicit piggyback: SPEC-037 §4 — every bootstrapped verb EXCEPT
the three lifecycle verbs carries pending signals on its return payload
via
_piggyback_drain(mcp/server.py:174-234).hd_exec(SPEC-040) is NOT a lifecycle verb — it’s a session-pickup verb and DOES drain.
§2 — Rationale
Captured in full in ADR-25 (Session Lifecycle Does Not Multiplex with Messaging). Summary: Single responsibility. A lifecycle verb has one job — manage session state. Mixing in messaging delivery couples two unrelated concerns; every future change to either has to reason about both. Race correctness on prism_start specifically. Identity-targeted signals queued before a new session existed are stale by definition. Delivering them onprism_start silently re-routes them. (This concern
does not apply to wrap or checkpoint — those run after registration.
Symmetry of the principle alone justifies exempting them.)
Composition with hd_exec. SPEC-040’s hd_exec is intentionally a
non-lifecycle session verb that DOES drain. The asymmetry is load-bearing:
a pickup is a continuation (inherits unread messages); a fresh start is
a beginning (does not).
§3 — Implementation
mcp/server.py:1649-1661: delete the drain_pending_signals call and
the result["pending_signals"] assignment. Adjust the surrounding logic
that may reference the field.
mcp/server.py:237-265 (require_bootstrap): introduce a
LIFECYCLE_VERBS = {"prism_start", "prism_checkpoint", "prism_wrap"} set
in module scope. Inside the wrapper, if func.__name__ is in
LIFECYCLE_VERBS, skip _piggyback_drain entirely.
prism_wrap docstring update (Q2 paired with D): add a note —
“Consider calling prism_signals_pending immediately before
prism_wrap if you want last-chance delivery of any signals queued
during the session. The wrap itself does not drain.”
Constitution updates (Q3 — ADR + PRISM.md):
- ADR-25 already filed (this commit).
- PRISM.md §3 (or wherever signal handling is discussed): add a
one-line rule pointing at ADR-25.
“Session lifecycle verbs (
prism_start,prism_checkpoint,prism_wrap) do not multiplex with messaging. See ADR-25.” - Mirror the PRISM.md edit to
templates/prism-base.md,templates/CLAUDE.md,templates/AGENTS.md.
§4 — Verification
- Fresh
prism_startcall → response has nopending_signalskey. prism_checkpointcall → response has nopending_signalskey.prism_wrapcall → response has nopending_signalskey.- Send a signal to identity
Donnawhile no Donna session is active. Start a new Donna session viaprism_start→ response has nopending_signals. Call any other bootstrapped verb (e.g.,prism_status) → signal is delivered via piggyback. - Same scenario, but caller invokes
prism_signals_pendingimmediately afterprism_start→ signal is returned. - SPEC-037 piggyback test suite still passes (drain works on non-lifecycle verbs).
hd_exec(SPEC-040) — being a non-lifecycle session verb — DOES drain pending signals via piggyback. Verify: send signal to Donna while no Donna active, then Donna callshd_exec→ signal returned inpending_signalsfield.- Signals queued during a session that ends without a draining verb
call (pathological case:
prism_start → prism_wrapwith no other verbs between) accumulate in queue. Next session of same identity drains them on first non-lifecycle verb (Q2 decision).
§5 — Decisions
- Q1 (scope of lifecycle exemption): ALL THREE EXEMPT.
prism_start,prism_checkpoint, andprism_wrapall skip the SPEC-037 piggyback drain. The principle “session lifecycle verbs do not multiplex with messaging” applies uniformly. Cost: signals queued during a session may not deliver before wrap; mitigated by SPEC-037’s contract that signals persist in the queue across sessions. - Q2 (signal queue hygiene / TTL): NO EXPIRY in v0.1. Signals
accumulate in the queue, drained by future verb calls or explicit
prism_signals_pendingpolls. Per-type TTL is a future spec if queue growth becomes a concern. PAIRED with discipline note (D):prism_wrapdocstring suggestsprism_signals_pendingbefore wrap for last-chance delivery. - Q3 (constitution rule placement): BOTH. ADR records the why (rationale, alternatives considered, race-correctness analysis) — see ADR-25. PRISM.md gets a one-line rule pointing at the ADR for agents to enforce at runtime.

