Skip to main content
Status: draft · Version 0.2 · Filed 2026-04-25

SPEC-041: prism_start Signal Separation — Session Lifecycle Does Not Multiplex with Messaging

Version

0.2

Status

draft

Origin

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.
This spec strips the drain from 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_signals field.
  • The require_bootstrap decorator (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.
Signals are delivered via:
  • 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 on prism_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

  1. Fresh prism_start call → response has no pending_signals key.
  2. prism_checkpoint call → response has no pending_signals key.
  3. prism_wrap call → response has no pending_signals key.
  4. Send a signal to identity Donna while no Donna session is active. Start a new Donna session via prism_start → response has no pending_signals. Call any other bootstrapped verb (e.g., prism_status) → signal is delivered via piggyback.
  5. Same scenario, but caller invokes prism_signals_pending immediately after prism_start → signal is returned.
  6. SPEC-037 piggyback test suite still passes (drain works on non-lifecycle verbs).
  7. 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 calls hd_exec → signal returned in pending_signals field.
  8. Signals queued during a session that ends without a draining verb call (pathological case: prism_start → prism_wrap with 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, and prism_wrap all 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_pending polls. Per-type TTL is a future spec if queue growth becomes a concern. PAIRED with discipline note (D): prism_wrap docstring suggests prism_signals_pending before 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.
Last modified on April 27, 2026