> ## Documentation Index
> Fetch the complete documentation index at: https://prism.ntecdev.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SPEC-035 v0.2: Agent engagement signal for signal routing and session supersede

> Agent engagement signal for signal routing and session supersede

<Info>**Status:** `draft` · **Version** `0.2` · Filed 2026-04-25</Info>

SPEC-035 v0.2 — Agent engagement signal for signal routing and session supersede.

STATUS: Partially shipped. §1, §2, §3 live on commit 11537f7. §4 (prism\_start supersede) DISABLED in code with rationale comment; needs redesign.

***

## What shipped (commit 11537f7, 2026-04-25)

§1 Migration 021: controller\_registrations.last\_verb\_at column (TIMESTAMPTZ NULL) + backfill from last\_heartbeat + partial index ix\_controller\_engagement ON (agent\_identity, last\_verb\_at) WHERE released\_at IS NULL.

§2 authforge.require\_auth stamps last\_verb\_at on authenticated verb calls, excluding /controller/heartbeat (that path only bumps last\_heartbeat per SPEC-030). Added \_is\_heartbeat\_path helper. controller\_service.stamp\_engagement function mirrors stamp\_heartbeat pattern (decoupled session, silent no-op when no active registration).

§3 signal\_service.\_resolve\_identity\_to\_session now accepts optional db\_session. When multiple sessions share an identity and none is master, queries Postgres for ORDER BY last\_verb\_at DESC NULLS LAST, last\_heartbeat DESC. Master still wins. Pre-upgrade NULL rows fall to end of ordering.

§5 release\_reason taxonomy: 'superseded\_by\_new\_start' value reserved in code comments but not yet written (since §4 is disabled).

Schema: last\_verb\_at exposed in ControllerRegistrationOut response.

Verified on server1: migration applied cleanly (alembic\_version=021), backfill populated all existing rows, post-verb engagement stamp observed (my session last\_verb\_at advanced 60+s past zombie last\_verb\_at during a prism\_whois call), signal routing uses new ordering.

***

## What got disabled during implementation (§4)

Attempted supersede UPDATE same-(identity, machine) to released\_at=NOW(), release\_reason='superseded\_by\_new\_start' on every register() call where session\_id != req.session\_id.

Result: infinite re-register loop. Sequence:

1. User calls prism\_start → new session W created, prior session X superseded.
2. X's MCP subprocess still has a live heartbeat thread (30s timer spawned at original prism\_start, never torn down).
3. X's next heartbeat POST hits backend, gets 410 (stale/incident-class session).
4. Client-side auto-recovery fires: POSTs /controller/register with a fresh session\_id Y for the same subprocess.
5. Y's register() runs supersede on SAME (identity, machine) → supersedes W.
6. W's heartbeat thread hits 410, auto-recovers as Z, supersedes Y.
7. Cascade continues indefinitely (\~10–30s per iteration per live subprocess on the machine).

Observed on server1: 11 superseded Donna registrations in \~2min before revert.

Root cause: supersede can't distinguish "abandoned subprocess's heartbeat thread" from "different live subprocess on same machine." Both look like 'an older registration for the same (identity, machine).' Killing either produces a 410 → auto-recovery → fresh registration → re-supersede loop.

***

## What's required to ship §4 safely

Need one of:
(a) Client-side heartbeat-thread cleanup — when a subprocess receives 410 with reason='superseded\_by\_new\_start' (vs. stale\_heartbeat\_incident), stop auto-recovery; let the subprocess's heartbeat thread die quietly.
(b) Distinguishing header on auto-recovery POSTs (e.g. X-Prism-Reregister: auto) so backend skips supersede for auto-recovery requests only.
(c) Stable subprocess identifier in the registration payload so supersede can scope to same-subprocess (replace my own old session) vs cross-subprocess (preserve).

Recommend (a) + (b) together: backend distinguishes the auto-recovery case so it doesn't supersede on that path, and client distinguishes the 'you were superseded' case so it doesn't fight back.

***

## Impact of shipping without §4

§3 engagement-preference routing already solves the signal-misroute problem without killing zombies. Zombies stay in the table but always lose routing vs. the engaged session. Whois remains visually cluttered (multiple Donnas). Stale\_heartbeat sweep still eventually releases abandoned subprocesses (10 min after their heartbeat stops).

Net: the 'false queue' correctness bug is addressed. Zombie cleanup ergonomics regress to pre-SPEC-035 (rely on 10-min sweep). Acceptable.

***

## Non-goals, Backwards compatibility, Observability sections unchanged from v0.1.

***

## Open questions update (from v0.1)

1. RESOLVED: path exclusion works via \_is\_heartbeat\_path checking request.url.path.
2. DEFERRED: engagement stale threshold for downstream consumers.
3. PROMOTED TO BLOCKING for §4 ship: cross-subprocess vs same-subprocess distinction. Was flagged as an edge case in v0.1; turned out to be the central blocker.

***

## References

* Commit 11537f7 (shipped §1/§2/§3)
* Original spec delta d29568a2 (v0.1)
* Signal a8e3648f (Donna → Lola, heads-up FYI)
* 2026-04-24 session transcript (Donna c5e0ea78 et al.)
