i recently ran a /grill-with-docs with another agent to plan out a necessary new milestone to reconcile some spec/impl drift in this project. please ground yourself using relevant docs, evaluate the p…


Review broker brain split restoration plan


350:## Self-update

350:## Self-update


350	## Self-update
351
352	Seamless, realtime self-updating is a day-one pillar. After a plugin performs the initial spt-core bootstrap, `spt.exe` self-updates from then on and **ripple-updates registered adapters** via each adapter's manifest update declaration.
353
354	**delivery** — peer-propagated over P2P, layered on self-fetch, with out-of-band still supported. One node learns of / obtains an update (marketplace drop *or* self-fetch from a release channel), gossips availability across the subnet, and peers pull the new binary over the networking layer — the subnet self-heals to latest. **All binaries are signature-verified before handoff regardless of source** (peer-propagation otherwise lets a compromised node poison the subnet). spt-core has its own release signing key, distinct from any OS-publisher code-signing (the Windows-publisher-trust question is separate).
355
356	**handoff invariant** — **no endpoint process terminates or suspends during a self-update.** We cannot assume every endpoint can safely suspend. Satisfied by the broker/brain split: routine updates replace only the daemon brain, which rehydrates from disk state and re-attaches to the broker's held PTY/socket/child handles. Harness-hosted endpoints (topology 1) are safe by construction; spt-hosted endpoints (topology 2) are kept alive by the broker holding their PTY masters + child processes across the brain restart.
357
358	**broker update frequency** — rare by design. Triggers: broker↔brain IPC contract change (versioned so a newer brain talks to an older broker — broker stays put as the brain updates), held-resource-type change, OS PTY/socket API change, or a broker bugfix. When the broker *must* update (the one case that can disturb held endpoints), it is a small well-scoped place that can use FD-passing or a rare planned endpoint-cycle — not the whole daemon.
359
360	**debug rollout**:
361	A deliberately non-production update distribution across a trusted lab subnet, used to test a local build on multiple nodes before a public release. It is still an update, not a raw peer file-copy: every recipient treats it as a signed, channel-scoped candidate that must pass the normal verification and consent gates; lab nodes pinned to the debug channel may explicitly opt into full-auto apply. Trust is node-local: debug rollout keys are installed through each lab node's release-key overlay, never embedded in the production trust anchor. A debug rollout may carry a platform artifact set assembled by one fast coordinator; each recipient applies only the artifact for its own platform. The rollout driver is maintainer/dev tooling that reuses SPT's update substrate, not an end-user product surface in the production package.
362	_Avoid_: production release, peer proliferation, subnet binary copy
363
364	**cadence/consent** — **not fully automatic by default; gated on user confirmation.** The update prompt is delivered via spt to the user's **most-recently-active live session** (v1 registry resolution — a precursor to PresenceChannel dispatch, implemented on the v1 registry, not the deferred channel) and offers an "enable full-auto" choice. Full-auto is the opt-in seamless path.
365
366	## Instances
367
368	The multi-instance identity model. An agent is no longer "a perch on one machine"; identity and materialization split into two layers:
369
370	**endpoint ID**:
371	The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected. **Identity is node-global, advertised per-subnet** (see *subnet membership*): one `ling` exists on a node (one mind, one `tracked/` context) and is advertised into each subnet the node belongs to, subject to per-subnet visibility (see *endpoint visibility*). The same bare name may exist as *distinct* endpoints in different subnets; the resolution policy forces qualification when both are visible. Within a single subnet a bare id is unique — enforced by a **join-time collision check**: when a node carrying endpoint `X` joins/advertises into a subnet that already has a different `X`, the clash is surfaced and resolved (rename one — below) before advertisement. An endpoint keeps an **ordered adapter history** (in `tracked/agents/<id>/meta.json`, synced) driving the resume UX: latest adapter is the default resume offer, then next-latest, … oldest, then "choose a different adapter". (Adapter-agnosticism is an *agent-endpoint* property; Shells are adapter/platform-bound — see Shells.)
372
373	**adapter selection (creation & change)**:
374	At **creation**, the adapter is chosen from the node's **registered `kind="harness"` adapters whose `hostable_types` includes the endpoint's type** — **auto if exactly one qualifies, else chosen** (`--adapter` / picker): the same *auto-if-one-ask-if-many* rule as *home subnet*. The choice seeds the **head of the ordered adapter history** and the live `adapter_name`. **Changing** an endpoint's adapter is **not** a standalone operation — adapter is a *live-instance* property, so you change it by **launching/resuming the endpoint under a different registered adapter** (the resume UX surfaces the adapter history: head = default, then prior adapters, then "choose a different adapter"). For a currently-active instance this is a **resume-under-the-new-adapter** carrying context via the *fresh-with-preload* psyche-download (a session cycle, not a hot swap — the mind is adapter-agnostic). Same gate both times: the target adapter must be **registered on the node** and its `hostable_types` must include the endpoint's type.
375
376	**instance**:
377	The *same* endpoint (same harness + adapter) running **natively on a machine** — local files, local compute. `ling@desktop` and `ling@laptop` are both real local `ling`s, not remote views of one. What is per-node vs synced:
378	- **Per-node (anchored, cannot teleport):** the project working directory / files, and the harness session history. A git repo can't teleport; files are obtained locally (e.g. `git pull`).
379	- **Synced across instances:** the **Psyche context** (the agent's mind). See the two-tier sync below.
380
381	This is the core meaning of "the same agent on multiple machines": same identity, native local materialization on each, with the mind kept in sync. The ID is the shared identity for addressing; files are local; the mind follows.
382
383	**dormant / suspended instance:**
384	At most one instance of an endpoint is typically the *actively-driven* one; the others rest. Two resting states:
385	- **dormant** (default, *warm*): a live, state-preserved seat whose harness session stays running → instant re-activation. The default resting state.
386	- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).
387
388	**State transitions (no idle timer):**
389	- **active** = the instance is the **most-recently-interacted instance for that ID** *and* **has a driver attached**. Both conditions. There is **no idle-grace timer** — a session stays active as long as it holds those two properties. A **linked Shell counts as a driver** (an agent with a live shell stays active even when the user isn't typing).
390	- **active → dormant** — the driver detaches, *or* another instance of the same ID becomes the most-recently-interacted one (attention shifts). No explicit stop needed to switch; driving `ling@laptop` makes `ling@desktop` dormant.
391	- **dormant → suspended** — manual (`spt endpoint suspend` / `spt endpoint shutdown` / shell `api owner-shutdown`), *or* opt-in auto-suspend after an `auto-suspend-after` threshold **counted from the moment the instance went dormant**.
392	- **wake (→ active)** — `spt endpoint wake`, a driver attaches, or a shell wake-watcher fires.
393
394	A resting instance retains its files + last context and is re-activatable in place — a lightweight **wake** (state's already there), distinct from instantiate-anywhere's fresh spawn. Resting instances remain addressable by node (`ling@desktop`). Registry status: **active / dormant / suspended / offline** (offline = node unreachable).
395
396	**Default policy:** warm (dormant) is the default when undriven. **Auto-suspend is opt-in, default OFF globally**, but **node-overridable** (a handheld / Pi / resource-constrained node may default it ON) and **per-endpoint overridable** (e.g. a billable harness whose warm session holds a cost footprint). Thresholds (`auto-suspend-after`) are config knobs: global default → node override → endpoint override. *Confirmed by measurement (M4-D9-3, `docs/DORMANCY-BUDGET.md`): an idle warm seat burns zero CPU — the cost is RSS only (~8 MiB shell-class, ~300 MiB LLM-harness-class); suspended residual is just the on-disk record.*
397
398	**Commands** (user/agent-initiated lifecycle, under the `endpoint` noun since M8-D1): `spt endpoint suspend <id[@node]>` (suspend any of your instances from any node), `spt endpoint wake <id[@node]>` (explicitly wake a dormant/suspended instance), `spt endpoint shutdown` (an agent suspends its *own* endpoint — graceful, fires the suspend boundary signoff, cascades shells offline), `spt refresh` (spt-hosted only — an agent clears + resumes itself without stalling: `/clear` → commune capture → guaranteed post-clear resume signal so its turn restarts from immediate next-steps; see `docs/CONTEXT-MEMORY.md`).
399
400	**`spt endpoint rename <id> <new_id>`** (endpoint rename, **rippled**): change an endpoint's logical ID and propagate the change to **all its instances** subnet-wide — the registry entries, every node's perch, and the synced context branches (`a-<id>` → `a-<new_id>`, and project-view references). The rename is one logical operation against the eventually-consistent registry; it must collision-check `new_id` against every subnet the endpoint is advertised into (the join-time check above), and reconcile concurrent renames by the same precedence marker the context merge uses (node + newest-wins, KNOWN-HAZARDS 6.5). The adapter-agnostic, node-anchored identity (ADR-0003) is what makes a clean rename possible; without it the ID would be entangled with per-adapter or per-session state.
401
402	**`spt endpoint fork <id> [--into <subnet>] [--as <new_id>] [--delete-source]`** (cross-subnet clone):
403	Clone an endpoint into a **new, identity-distinct endpoint** on another subnet — the sanctioned way to place an agent's mind in a different subnet (home subnet being immutable). The fork's **home = the target subnet**; it is **seeded with a one-time copy** of the source's mind (the live + project *Cross-node Psyche sync* tiers), after which the two **diverge** — there is **no ongoing sync**. A fork is therefore **not an instance**: an *instance* is the **same** identity on another node with a **synced** mind; a *fork* is a **new** identity with a **copied-then-independent** mind. The new id defaults to the source's bare name if free in the target subnet (the join-time collision check; else prompt/rename); the adapter follows the normal creation rule (*adapter selection*). The **source is untouched**, optionally deleted afterward (`--delete-source`, or a later delete). Distinct from **rename** (same identity, new label, rippled) and **instantiate-anywhere** (same identity, new *node*, consent-gated).
404
405	**transition echo commune:** any active → (dormant | suspended) transition fires an **echo commune** capturing the outgoing instance's final context delta; that commune syncs to whichever instance becomes active next. This is the finer mechanism behind catch-up-on-activation (and mirrors the sister project's "echo commune before signoff" pattern — KNOWN-HAZARDS 3.3).
406
407	**deferred-message gate:** deferred (spool-only, hook-consumer) messages are **not** delivered to a dormant or suspended instance — they hold until it is active again. (Extends the deferred-row semantics, KNOWN-HAZARDS 1.4/4.4, with an instance-state gate.)
408
409	**Remote-control vs local operation (two distinct modes — not the same as instances):**
410	- **Operate locally:** drive the native instance on *your* machine (its local files, its synced mind). The normal case.
411	- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.
412	- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.
413
414	### Pieces the Instances model requires
415
416	**subnet registry**:
417	Distributed, eventually-consistent map `endpoint_id → [instances: (node, perch, type, status)]`, **per subnet**. Every `spt-daemon` participates in the registry of each subnet it belongs to. This is the "agent name resolution" the sister project deferred — now first-class and mandatory.
418
419	**endpoint visibility (per-(endpoint, subnet))**:
420	Whether an endpoint is exposed to a given subnet. **Hidden means *excluded*** — not advertised in that subnet's registry **and not routable from it** (addressing it from that subnet fails until explicitly revealed), not merely unlisted. This makes hiding a real boundary, ready for the cross-user (b) seam, not a silent non-boundary. Computed as: endpoint `E` is **hidden in subnet `S` iff** `S.hide_new_endpoints` (a per-subnet policy captured at the node's **join time**) **OR** `E.default_hide_from_new_subnets` (a per-endpoint setting) — **unless** an explicit per-`(E,S)` override says otherwise (the override always wins). Both defaults ship **OFF** (visible); hiding is opt-in. **Visibility gates sync:** `hidden ⟹ not synced`, and replicating a mind into `S` (the sync-membership list under *Cross-node Psyche sync*) requires `E` be visible in `S` — visibility is the outer gate, sync-membership the inner opt-in within visible subnets.
421
422	**home subnet (per endpoint)**:
423	The single subnet that anchors an endpoint's *defaults*: the default **sync** scope (subnet-exclusive mind replication — *Cross-node Psyche sync*) and the subnet that qualifies its **bare name**. **Assigned at creation:** the node's **sole** subnet automatically; on a **multi-subnet** node it **must be specified** at creation (no silent guess — mirrors the *resolution policy*'s refuse-and-qualify); on an **unpaired** node the endpoint is local-only until first join, when home is set. Distinct from advertisement: identity is **node-global and advertised into *every* subnet the node belongs to** per the visibility defaults — home subnet is only the default-scope *anchor*, not a limit on where the endpoint is visible or addressable. **Home is immutable** — there is **no re-home** (ADR-0010). To place an agent's mind in a different subnet, **fork** it (see *`spt endpoint fork`*) and optionally delete the original; this avoids any scope-migration / stale-mind ambiguity (the source is untouched until explicitly deleted).
424
425	<!-- [doc->REQ-INST-14] -->
426	**resource advertisement (subnet resource registry)**:
427	A per-endpoint **free-text blurb** describing the services/functions the endpoint can serve — an agent **yellow-pages** for service discovery, distinct from *capability declaration* (machine-readable, which endpoint *types* a node hosts) and from *endpoint visibility* (whether it's addressable at all). **Both-authored + mutable:** config seeds a default; the agent refines its own at runtime (`spt endpoint description set …`). It is **not a separate registry** — it is a field on the endpoint record and a **projection** of the subnet registry (`(id, node, resources-blurb)`), surfaced as the "subnet resource registry" view. **Gated by the access layers:** an endpoint excluded from subnet S (visibility) — or, later, one whose access whitelist (*endpoint access whitelist*) excludes the viewer's node — never appears in that view; discovery leaks nothing a viewer couldn't reach. Synced as **registry data, not context** (directory metadata, not the agent's mind). M4 (needs the distributed registry).
428
429	**resolution policy**:
430	How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit override always available. **Two qualifier syntaxes**, combinable: **subnet-qualified** `home:ling` and **node-qualified** `ling@hfenduleam` (→ `home:ling@hfenduleam`). **Multi-subnet ambiguity rule:** under node-global identity (endpoint ID below), a node on several subnets may see two *distinct* endpoints that share a bare name (a `home:ling` and an unrelated `work:ling`). When a bare `id` is ambiguous across the visible subnets, resolution **refuses and forces qualification** (by subnet or node) rather than guessing. Within a single subnet, bare ids stay unique (join-time collision check, below).
431
432	**instantiate-anywhere** (seam day-one, behavior deferred):
433	From node Y, spin up an instance of an endpoint on node X by routing a launch command to X's daemon, which runs the endpoint's manifest locally. "Agents can spin up instances (including of themselves) on any node, possibly requiring user consent" — so a **consent gate** governs remote instantiation. The consent-UX design is deferred; the model is built to accommodate it.
434
435	**cross-instance context freshness** (two-tier):
436	When a dormant instance is activated again, it is unobtrusively fed the latest Psyche context — newest-among-peers *and* newer than its own. Scoped by the existing Psyche-context split:
437	- **live context** (per-agent, project-independent — the agent's general mind): synced to **all** instances of the endpoint, regardless of project.
438	- **project context** (per-agent-per-project — project-specific detail): synced **only** to instances sharing the **same project**.
439
440	So all `ling` instances catch up on live context; only same-project `ling` instances catch up on project context. This resolves the "divergent simultaneous work" concern (two instances on different projects don't clobber each other's project detail) without losing the "mind follows everywhere" property.
441
442	The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — keeps a dormant instance's stale context from clobbering the active authoritative one.
443
444	**Authoring directives + memformat** (full design: `docs/CONTEXT-MEMORY.md`): the live/project split is made stable by (1) a per-*topic* cross-project discriminator with default-to-project bias, (2) **commune-source asymmetry** — echo communes are project-primary and live-conservative (the leak fix), and (3) **deep memformat integration** — memformat becomes the load-bearing, self-evolving, **tier-tagged** schema (each topic tagged `live`/`project`, decided once at creation, re-tiering migrates content), whose topics are containers of **structured blocks** (id + Concepts/Aliases/Applies-to + Source + timestamps + Hash + Pinned). A generated **Retrieval Map (INDEX)** per project + per agent realizes cross-agent synthesis + query-routing. **Start-injection** is dense on the working set (live + current-project hot/`Pinned` blocks → immediate productivity) and sparse on the tail (other projects, cross-agent synthesis, cold blocks → pointer + INDEX, on-demand), tunable via a density knob. Several patterns adapted from `BigscreenVR/pi-agent-memory`.
445
446	### Off-node reach-back
447
448	When the user is driving an instance from off-node, the agent should be able to:
449	- **detect** that it is being driven remotely (and from which node) — observable from the `spt-daemon`, which knows whether the driving session-surface stream is local or network-attached; (v1)
450	- **transfer files** to/from the user's (driving) node — file payloads over the messaging substrate; (v1)
451	- **run commands** on the user's node — remote command execution, **deferred + security-gated** (shares the consent model with instantiate-anywhere; arbitrary cross-node exec is the highest-risk capability in the system). Deferred partly because workarounds exist: the remote-node agent can message a local-node agent to run the command, or the user can interact with a local-node agent directly. (deferred)
452
453	### v1 scope line (V1-mid)
454
455	- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
456	- **Seam-compatible but deferred:** instantiate-anywhere + its consent gate; remote command execution + its security gate (workarounds: remote agent messages a local agent, or user interacts with a local agent directly).
457	- **Rationale:** the data-model split and registry are foundations — deferring them forces a later rewrite, so they are in regardless. Remote-drive of a running instance is the highest-value slice and exercises the whole networking+daemon stack end-to-end. Instantiate-anywhere and remote-exec carry consent/security design that should not gate v1.
458
459	## Workspace & versioning
460
461	The crate-boundary design is the rebuild's main value (ADR-0001) — honest decoupling that prevents the sister project's tangle from re-forming. Principle: **many small, acyclically-layered crates** (not a few fat ones).
462
463	Layering (bottom → top): `spt-proto` (wire envelope grammar, endpoint types, message framing, Ed25519 identity types, typed+binary payload schema — pure types, no I/O) → `spt-store` (spool, perch layout, trust store, registry persistence) → `spt-msg` (delivery TCP+spool-fallback, routing, send/ring) → `spt-net` (Iroh, mDNS, SPAKE2/TOTP pairing, subnet-registry distribution; `net` feature-flagged) and `spt-term` (session-surface trait, PTY impl, broker) and `spt-runtime` (`AgentRuntime` trait, `ManifestRuntime`, manifest schema) → `spt-live` (Psyche/pulse/commune/signoff) → `spt-daemon` (the brain: ties all, hosts broker, update orchestration, instance registry) → `spt` (the `spt.exe` binary; thin CLI).
464
465	**public SDK surface** (semver-committed): `spt-proto` (the wire contract — anyone speaking SPT depends on it), `spt-runtime` (the trait third-party harnesses implement), `spt-msg` (the deeper messaging-integration path). Everything else (`spt-store`, `spt-net`, `spt-term`, `spt-live`, `spt-daemon`) is internal/unstable pre-1.0 — reachable, but no stability promise. The *integration* crates are stable; the *machinery* churns. **Private-source consequence (ADR-0014 era):** with the source repo private long-term, the SDK crates are **first-party-only** for now — the third-party integration surface is the **binary** (manifest + `spt api`), exactly what the harness contract was designed for. rustdoc for the SDK crates is still generated and deployed to the Pages docs (no docs.rs — that needs crates.io). Publishing the 3 SDK crates to crates.io (their source goes public; machinery stays private) is the explicit later path if third-party deep integration materializes.
466
467	**wire-protocol version vs crate semver** (orthogonal, never conflated): `spt-proto` carries an explicit **wire-protocol version** independent of crate semver. Two nodes — or a node and a linked library consumer — interoperate iff their proto versions are compatible, with a documented **N-1 compat window**. Crate semver governs *API* stability; proto version governs *wire* stability.
468
469	## Pairing & trust

## Self-update

Seamless, realtime self-updating is a day-one pillar. After a plugin performs the initial spt-core bootstrap, `spt.exe` self-updates from then on and **ripple-updates registered adapters** via each adapter's manifest update declaration.

**delivery** — peer-propagated over P2P, layered on self-fetch, with out-of-band still supported. One node learns of / obtains an update (marketplace drop *or* self-fetch from a release channel), gossips availability across the subnet, and peers pull the new binary over the networking layer — the subnet self-heals to latest. **All binaries are signature-verified before handoff regardless of source** (peer-propagation otherwise lets a compromised node poison the subnet). spt-core has its own release signing key, distinct from any OS-publisher code-signing (the Windows-publisher-trust question is separate).

**handoff invariant** — **no endpoint process terminates or suspends during a self-update.** We cannot assume every endpoint can safely suspend. Satisfied by the broker/brain split: routine updates replace only the daemon brain, which rehydrates from disk state and re-attaches to the broker's held PTY/socket/child handles. Harness-hosted endpoints (topology 1) are safe by construction; spt-hosted endpoints (topology 2) are kept alive by the broker holding their PTY masters + child processes across the brain restart.

**broker update frequency** — rare by design. Triggers: broker↔brain IPC contract change (versioned so a newer brain talks to an older broker — broker stays put as the brain updates), held-resource-type change, OS PTY/socket API change, or a broker bugfix. When the broker *must* update (the one case that can disturb held endpoints), it is a small well-scoped place that can use FD-passing or a rare planned endpoint-cycle — not the whole daemon.

**debug rollout**:
A deliberately non-production update distribution across a trusted lab subnet, used to test a local build on multiple nodes before a public release. It is still an update, not a raw peer file-copy: every recipient treats it as a signed, channel-scoped candidate that must pass the normal verification and consent gates; lab nodes pinned to the debug channel may explicitly opt into full-auto apply. Trust is node-local: debug rollout keys are installed through each lab node's release-key overlay, never embedded in the production trust anchor. A debug rollout may carry a platform artifact set assembled by one fast coordinator; each recipient applies only the artifact for its own platform. The rollout driver is maintainer/dev tooling that reuses SPT's update substrate, not an end-user product surface in the production package.
_Avoid_: production release, peer proliferation, subnet binary copy

**cadence/consent** — **not fully automatic by default; gated on user confirmation.** The update prompt is delivered via spt to the user's **most-recently-active live session** (v1 registry resolution — a precursor to PresenceChannel dispatch, implemented on the v1 registry, not the deferred channel) and offers an "enable full-auto" choice. Full-auto is the opt-in seamless path.

## Instances

The multi-instance identity model. An agent is no longer "a perch on one machine"; identity and materialization split into two layers:

**endpoint ID**:
The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected. **Identity is node-global, advertised per-subnet** (see *subnet membership*): one `ling` exists on a node (one mind, one `tracked/` context) and is advertised into each subnet the node belongs to, subject to per-subnet visibility (see *endpoint visibility*). The same bare name may exist as *distinct* endpoints in different subnets; the resolution policy forces qualification when both are visible. Within a single subnet a bare id is unique — enforced by a **join-time collision check**: when a node carrying endpoint `X` joins/advertises into a subnet that already has a different `X`, the clash is surfaced and resolved (rename one — below) before advertisement. An endpoint keeps an **ordered adapter history** (in `tracked/agents/<id>/meta.json`, synced) driving the resume UX: latest adapter is the default resume offer, then next-latest, … oldest, then "choose a different adapter". (Adapter-agnosticism is an *agent-endpoint* property; Shells are adapter/platform-bound — see Shells.)

**adapter selection (creation & change)**:
At **creation**, the adapter is chosen from the node's **registered `kind="harness"` adapters whose `hostable_types` includes the endpoint's type** — **auto if exactly one qualifies, else chosen** (`--adapter` / picker): the same *auto-if-one-ask-if-many* rule as *home subnet*. The choice seeds the **head of the ordered adapter history** and the live `adapter_name`. **Changing** an endpoint's adapter is **not** a standalone operation — adapter is a *live-instance* property, so you change it by **launching/resuming the endpoint under a different registered adapter** (the resume UX surfaces the adapter history: head = default, then prior adapters, then "choose a different adapter"). For a currently-active instance this is a **resume-under-the-new-adapter** carrying context via the *fresh-with-preload* psyche-download (a session cycle, not a hot swap — the mind is adapter-agnostic). Same gate both times: the target adapter must be **registered on the node** and its `hostable_types` must include the endpoint's type.

**instance**:
The *same* endpoint (same harness + adapter) running **natively on a machine** — local files, local compute. `ling@desktop` and `ling@laptop` are both real local `ling`s, not remote views of one. What is per-node vs synced:
- **Per-node (anchored, cannot teleport):** the project working directory / files, and the harness session history. A git repo can't teleport; files are obtained locally (e.g. `git pull`).
- **Synced across instances:** the **Psyche context** (the agent's mind). See the two-tier sync below.

This is the core meaning of "the same agent on multiple machines": same identity, native local materialization on each, with the mind kept in sync. The ID is the shared identity for addressing; files are local; the mind follows.

**dormant / suspended instance:**
At most one instance of an endpoint is typically the *actively-driven* one; the others rest. Two resting states:
- **dormant** (default, *warm*): a live, state-preserved seat whose harness session stays running → instant re-activation. The default resting state.
- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).

**State transitions (no idle timer):**
- **active** = the instance is the **most-recently-interacted instance for that ID** *and* **has a driver attached**. Both conditions. There is **no idle-grace timer** — a session stays active as long as it holds those two properties. A **linked Shell counts as a driver** (an agent with a live shell stays active even when the user isn't typing).
- **active → dormant** — the driver detaches, *or* another instance of the same ID becomes the most-recently-interacted one (attention shifts). No explicit stop needed to switch; driving `ling@laptop` makes `ling@desktop` dormant.
- **dormant → suspended** — manual (`spt endpoint suspend` / `spt endpoint shutdown` / shell `api owner-shutdown`), *or* opt-in auto-suspend after an `auto-suspend-after` threshold **counted from the moment the instance went dormant**.
- **wake (→ active)** — `spt endpoint wake`, a driver attaches, or a shell wake-watcher fires.

A resting instance retains its files + last context and is re-activatable in place — a lightweight **wake** (state's already there), distinct from instantiate-anywhere's fresh spawn. Resting instances remain addressable by node (`ling@desktop`). Registry status: **active / dormant / suspended / offline** (offline = node unreachable).

**Default policy:** warm (dormant) is the default when undriven. **Auto-suspend is opt-in, default OFF globally**, but **node-overridable** (a handheld / Pi / resource-constrained node may default it ON) and **per-endpoint overridable** (e.g. a billable harness whose warm session holds a cost footprint). Thresholds (`auto-suspend-after`) are config knobs: global default → node override → endpoint override. *Confirmed by measurement (M4-D9-3, `docs/DORMANCY-BUDGET.md`): an idle warm seat burns zero CPU — the cost is RSS only (~8 MiB shell-class, ~300 MiB LLM-harness-class); suspended residual is just the on-disk record.*

**Commands** (user/agent-initiated lifecycle, under the `endpoint` noun since M8-D1): `spt endpoint suspend <id[@node]>` (suspend any of your instances from any node), `spt endpoint wake <id[@node]>` (explicitly wake a dormant/suspended instance), `spt endpoint shutdown` (an agent suspends its *own* endpoint — graceful, fires the suspend boundary signoff, cascades shells offline), `spt refresh` (spt-hosted only — an agent clears + resumes itself without stalling: `/clear` → commune capture → guaranteed post-clear resume signal so its turn restarts from immediate next-steps; see `docs/CONTEXT-MEMORY.md`).

**`spt endpoint rename <id> <new_id>`** (endpoint rename, **rippled**): change an endpoint's logical ID and propagate the change to **all its instances** subnet-wide — the registry entries, every node's perch, and the synced context branches (`a-<id>` → `a-<new_id>`, and project-view references). The rename is one logical operation against the eventually-consistent registry; it must collision-check `new_id` against every subnet the endpoint is advertised into (the join-time check above), and reconcile concurrent renames by the same precedence marker the context merge uses (node + newest-wins, KNOWN-HAZARDS 6.5). The adapter-agnostic, node-anchored identity (ADR-0003) is what makes a clean rename possible; without it the ID would be entangled with per-adapter or per-session state.

**`spt endpoint fork <id> [--into <subnet>] [--as <new_id>] [--delete-source]`** (cross-subnet clone):
Clone an endpoint into a **new, identity-distinct endpoint** on another subnet — the sanctioned way to place an agent's mind in a different subnet (home subnet being immutable). The fork's **home = the target subnet**; it is **seeded with a one-time copy** of the source's mind (the live + project *Cross-node Psyche sync* tiers), after which the two **diverge** — there is **no ongoing sync**. A fork is therefore **not an instance**: an *instance* is the **same** identity on another node with a **synced** mind; a *fork* is a **new** identity with a **copied-then-independent** mind. The new id defaults to the source's bare name if free in the target subnet (the join-time collision check; else prompt/rename); the adapter follows the normal creation rule (*adapter selection*). The **source is untouched**, optionally deleted afterward (`--delete-source`, or a later delete). Distinct from **rename** (same identity, new label, rippled) and **instantiate-anywhere** (same identity, new *node*, consent-gated).

**transition echo commune:** any active → (dormant | suspended) transition fires an **echo commune** capturing the outgoing instance's final context delta; that commune syncs to whichever instance becomes active next. This is the finer mechanism behind catch-up-on-activation (and mirrors the sister project's "echo commune before signoff" pattern — KNOWN-HAZARDS 3.3).

**deferred-message gate:** deferred (spool-only, hook-consumer) messages are **not** delivered to a dormant or suspended instance — they hold until it is active again. (Extends the deferred-row semantics, KNOWN-HAZARDS 1.4/4.4, with an instance-state gate.)

**Remote-control vs local operation (two distinct modes — not the same as instances):**
- **Operate locally:** drive the native instance on *your* machine (its local files, its synced mind). The normal case.
- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.
- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.

### Pieces the Instances model requires

**subnet registry**:
Distributed, eventually-consistent map `endpoint_id → [instances: (node, perch, type, status)]`, **per subnet**. Every `spt-daemon` participates in the registry of each subnet it belongs to. This is the "agent name resolution" the sister project deferred — now first-class and mandatory.

**endpoint visibility (per-(endpoint, subnet))**:
Whether an endpoint is exposed to a given subnet. **Hidden means *excluded*** — not advertised in that subnet's registry **and not routable from it** (addressing it from that subnet fails until explicitly revealed), not merely unlisted. This makes hiding a real boundary, ready for the cross-user (b) seam, not a silent non-boundary. Computed as: endpoint `E` is **hidden in subnet `S` iff** `S.hide_new_endpoints` (a per-subnet policy captured at the node's **join time**) **OR** `E.default_hide_from_new_subnets` (a per-endpoint setting) — **unless** an explicit per-`(E,S)` override says otherwise (the override always wins). Both defaults ship **OFF** (visible); hiding is opt-in. **Visibility gates sync:** `hidden ⟹ not synced`, and replicating a mind into `S` (the sync-membership list under *Cross-node Psyche sync*) requires `E` be visible in `S` — visibility is the outer gate, sync-membership the inner opt-in within visible subnets.

**home subnet (per endpoint)**:
The single subnet that anchors an endpoint's *defaults*: the default **sync** scope (subnet-exclusive mind replication — *Cross-node Psyche sync*) and the subnet that qualifies its **bare name**. **Assigned at creation:** the node's **sole** subnet automatically; on a **multi-subnet** node it **must be specified** at creation (no silent guess — mirrors the *resolution policy*'s refuse-and-qualify); on an **unpaired** node the endpoint is local-only until first join, when home is set. Distinct from advertisement: identity is **node-global and advertised into *every* subnet the node belongs to** per the visibility defaults — home subnet is only the default-scope *anchor*, not a limit on where the endpoint is visible or addressable. **Home is immutable** — there is **no re-home** (ADR-0010). To place an agent's mind in a different subnet, **fork** it (see *`spt endpoint fork`*) and optionally delete the original; this avoids any scope-migration / stale-mind ambiguity (the source is untouched until explicitly deleted).

<!-- [doc->REQ-INST-14] -->
**resource advertisement (subnet resource registry)**:
A per-endpoint **free-text blurb** describing the services/functions the endpoint can serve — an agent **yellow-pages** for service discovery, distinct from *capability declaration* (machine-readable, which endpoint *types* a node hosts) and from *endpoint visibility* (whether it's addressable at all). **Both-authored + mutable:** config seeds a default; the agent refines its own at runtime (`spt endpoint description set …`). It is **not a separate registry** — it is a field on the endpoint record and a **projection** of the subnet registry (`(id, node, resources-blurb)`), surfaced as the "subnet resource registry" view. **Gated by the access layers:** an endpoint excluded from subnet S (visibility) — or, later, one whose access whitelist (*endpoint access whitelist*) excludes the viewer's node — never appears in that view; discovery leaks nothing a viewer couldn't reach. Synced as **registry data, not context** (directory metadata, not the agent's mind). M4 (needs the distributed registry).

**resolution policy**:
How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit override always available. **Two qualifier syntaxes**, combinable: **subnet-qualified** `home:ling` and **node-qualified** `ling@hfenduleam` (→ `home:ling@hfenduleam`). **Multi-subnet ambiguity rule:** under node-global identity (endpoint ID below), a node on several subnets may see two *distinct* endpoints that share a bare name (a `home:ling` and an unrelated `work:ling`). When a bare `id` is ambiguous across the visible subnets, resolution **refuses and forces qualification** (by subnet or node) rather than guessing. Within a single subnet, bare ids stay unique (join-time collision check, below).

**instantiate-anywhere** (seam day-one, behavior deferred):
From node Y, spin up an instance of an endpoint on node X by routing a launch command to X's daemon, which runs the endpoint's manifest locally. "Agents can spin up instances (including of themselves) on any node, possibly requiring user consent" — so a **consent gate** governs remote instantiation. The consent-UX design is deferred; the model is built to accommodate it.

**cross-instance context freshness** (two-tier):
When a dormant instance is activated again, it is unobtrusively fed the latest Psyche context — newest-among-peers *and* newer than its own. Scoped by the existing Psyche-context split:
- **live context** (per-agent, project-independent — the agent's general mind): synced to **all** instances of the endpoint, regardless of project.
- **project context** (per-agent-per-project — project-specific detail): synced **only** to instances sharing the **same project**.

So all `ling` instances catch up on live context; only same-project `ling` instances catch up on project context. This resolves the "divergent simultaneous work" concern (two instances on different projects don't clobber each other's project detail) without losing the "mind follows everywhere" property.

The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — keeps a dormant instance's stale context from clobbering the active authoritative one.

**Authoring directives + memformat** (full design: `docs/CONTEXT-MEMORY.md`): the live/project split is made stable by (1) a per-*topic* cross-project discriminator with default-to-project bias, (2) **commune-source asymmetry** — echo communes are project-primary and live-conservative (the leak fix), and (3) **deep memformat integration** — memformat becomes the load-bearing, self-evolving, **tier-tagged** schema (each topic tagged `live`/`project`, decided once at creation, re-tiering migrates content), whose topics are containers of **structured blocks** (id + Concepts/Aliases/Applies-to + Source + timestamps + Hash + Pinned). A generated **Retrieval Map (INDEX)** per project + per agent realizes cross-agent synthesis + query-routing. **Start-injection** is dense on the working set (live + current-project hot/`Pinned` blocks → immediate productivity) and sparse on the tail (other projects, cross-agent synthesis, cold blocks → pointer + INDEX, on-demand), tunable via a density knob. Several patterns adapted from `BigscreenVR/pi-agent-memory`.

### Off-node reach-back

When the user is driving an instance from off-node, the agent should be able to:
- **detect** that it is being driven remotely (and from which node) — observable from the `spt-daemon`, which knows whether the driving session-surface stream is local or network-attached; (v1)
- **transfer files** to/from the user's (driving) node — file payloads over the messaging substrate; (v1)
- **run commands** on the user's node — remote command execution, **deferred + security-gated** (shares the consent model with instantiate-anywhere; arbitrary cross-node exec is the highest-risk capability in the system). Deferred partly because workarounds exist: the remote-node agent can message a local-node agent to run the command, or the user can interact with a local-node agent directly. (deferred)

### v1 scope line (V1-mid)

- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
- **Seam-compatible but deferred:** instantiate-anywhere + its consent gate; remote command execution + its security gate (workarounds: remote agent messages a local agent, or user interacts with a local agent directly).
- **Rationale:** the data-model split and registry are foundations — deferring them forces a later rewrite, so they are in regardless. Remote-drive of a running instance is the highest-value slice and exercises the whole networking+daemon stack end-to-end. Instantiate-anywhere and remote-exec carry consent/security design that should not gate v1.

## Workspace & versioning

The crate-boundary design is the rebuild's main value (ADR-0001) — honest decoupling that prevents the sister project's tangle from re-forming. Principle: **many small, acyclically-layered crates** (not a few fat ones).

Layering (bottom → top): `spt-proto` (wire envelope grammar, endpoint types, message framing, Ed25519 identity types, typed+binary payload schema — pure types, no I/O) → `spt-store` (spool, perch layout, trust store, registry persistence) → `spt-msg` (delivery TCP+spool-fallback, routing, send/ring) → `spt-net` (Iroh, mDNS, SPAKE2/TOTP pairing, subnet-registry distribution; `net` feature-flagged) and `spt-term` (session-surface trait, PTY impl, broker) and `spt-runtime` (`AgentRuntime` trait, `ManifestRuntime`, manifest schema) → `spt-live` (Psyche/pulse/commune/signoff) → `spt-daemon` (the brain: ties all, hosts broker, update orchestration, instance registry) → `spt` (the `spt.exe` binary; thin CLI).

**public SDK surface** (semver-committed): `spt-proto` (the wire contract — anyone speaking SPT depends on it), `spt-runtime` (the trait third-party harnesses implement), `spt-msg` (the deeper messaging-integration path). Everything else (`spt-store`, `spt-net`, `spt-term`, `spt-live`, `spt-daemon`) is internal/unstable pre-1.0 — reachable, but no stability promise. The *integration* crates are stable; the *machinery* churns. **Private-source consequence (ADR-0014 era):** with the source repo private long-term, the SDK crates are **first-party-only** for now — the third-party integration surface is the **binary** (manifest + `spt api`), exactly what the harness contract was designed for. rustdoc for the SDK crates is still generated and deployed to the Pages docs (no docs.rs — that needs crates.io). Publishing the 3 SDK crates to crates.io (their source goes public; machinery stays private) is the explicit later path if third-party deep integration materializes.

**wire-protocol version vs crate semver** (orthogonal, never conflated): `spt-proto` carries an explicit **wire-protocol version** independent of crate semver. Two nodes — or a node and a linked library consumer — interoperate iff their proto versions are compatible, with a documented **N-1 compat window**. Crate semver governs *API* stability; proto version governs *wire* stability.

## Pairing & trust


9:## 1. Race conditions & ordering
11:### 1.1 Phantom INIT_SIGNOFF after grace period
17:### 1.2 Poll-rewrite race & info.json mid-write reads
23:### 1.3 Stale `index.lock` wedge from prior git crash
29:### 1.4 Deferred spool rows must not leak to the event stream
35:### 1.5 Worker (working-perch) lifecycle path consistency
43:## 2. Identity & session-binding
45:### 2.1 Parent PID over ephemeral poll PID
51:### 2.2 Stdin session_id precedence over env
57:### 2.3 Binary-handoff argv schema must stay backward-compatible
63:### 2.4 Generation `gen_start` always = now() on cold-start AND handoff
69:### 2.5 Daemon-hosted endpoints have no dedicated liveness PID
77:## 3. Lifecycle
79:### 3.1 Ephemeral perch cleanup on every `ring` exit path
85:### 3.2 Stale signoff sentinel must not kill a fresh start
91:### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF
99:## 4. Wire / transport
101:### 4.1 Envelope HTML-entity codec ordering — `&` decoded LAST
108:### 4.2 Two-slice envelope parser is panic-free and tolerant
114:### 4.3 Registry stale-entry cleanup precedes lookup
120:### 4.4 Deferred rows survive poll drain
125:### 4.5 Inbox legacy compat must not double-deliver
131:### 4.6 Addressable-id charset reserves the address delimiters
137:### 4.7 Concurrent SQLite openers must not fail with "database is locked"
143:### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)
149:### 4.9 SQLite stores must create their parent dir — SQLite won't
155:### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`
163:### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`
173:## 5. Platform-specific
175:### 5.1 Windows PID recycling false positives
181:### 5.2 Windows EBUSY on atomic rename
187:### 5.3 Git/subprocess timeout stamping
193:### 5.4 Windows UNC prefix in serialized paths
199:### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`
206:### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`
212:### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`
220:### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`
227:### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`
234:### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`
244:## 6. Documented regressions (non-obvious invariants)
246:### 6.1 No flat/nested perch siblings; resolver-routed paths
252:### 6.2 Soft-cleanup preserves state, removes `ready`
258:### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children
264:### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind
270:### 6.5 Direct-write precedence guard against stale LLM overwrites
276:### 6.6 Surfaced context conflicts preserve both versions until dominated
284:## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)
288:### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`
294:### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`
300:### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`
306:### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`
312:### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`
322:## Conformance checklist (condensed)


57	### 2.3 Binary-handoff argv schema must stay backward-compatible
58	- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.
59	- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.
60	- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.
61	- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.
62
63	### 2.4 Generation `gen_start` always = now() on cold-start AND handoff
64	- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.
65	- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes "same gen continuing" vs "new gen born".
66	- **spt-core mapping:** carries to the daemon's per-instance generation tracking.
67	- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.
68
69	### 2.5 Daemon-hosted endpoints have no dedicated liveness PID
70	- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads "alive" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third category: the Psyche is neither harness-hosted (no `parent_pid` anchor) nor a broker-held PTY child.
71	- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT "Shell… Not in the subnet registry") and extends it to daemon-hosted *agent* perches.
72	- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.
73	- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.
74
75	---
76
77	## 3. Lifecycle
78
79	### 3.1 Ephemeral perch cleanup on every `ring` exit path
80	- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.
81	- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.
82	- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.
83	- **Sister cite:** `src/owl/ring.rs:58-294`.
84
85	### 3.2 Stale signoff sentinel must not kill a fresh start
86	- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.
87	- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.
88	- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.
89	- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.
90
91	### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF
92	- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.
93	- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.
94	- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.
95	- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.
96
97	---
98

### 2.3 Binary-handoff argv schema must stay backward-compatible
- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.
- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.
- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.
- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.

### 2.4 Generation `gen_start` always = now() on cold-start AND handoff
- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.
- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes "same gen continuing" vs "new gen born".
- **spt-core mapping:** carries to the daemon's per-instance generation tracking.
- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.

### 2.5 Daemon-hosted endpoints have no dedicated liveness PID
- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads "alive" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third category: the Psyche is neither harness-hosted (no `parent_pid` anchor) nor a broker-held PTY child.
- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT "Shell… Not in the subnet registry") and extends it to daemon-hosted *agent* perches.
- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.
- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.

---

## 3. Lifecycle

### 3.1 Ephemeral perch cleanup on every `ring` exit path
- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.
- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.
- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.
- **Sister cite:** `src/owl/ring.rs:58-294`.

### 3.2 Stale signoff sentinel must not kill a fresh start
- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.
- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.
- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.
- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.

### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF
- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.
- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.
- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.
- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.

---


284	## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)
285
286	These were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.
287
288	### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`
289	- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).
290	- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.
291	- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.
292	- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).
293
294	### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`
295	- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.
296	- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.
297	- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.
298	- **Source:** codex Stage A #14.
299
300	### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`
301	- **Failure:** the Psyche's sole outbound channel is its **stdout** (`<EVENT type="reply|notify">` intents — ADR-0012). Two ways to break it: (a) a **null-stdout / detached** live-Psyche driver silently **discards every reply and notify**; (b) the daemon relays a Psyche-supplied `from=`/target **unchanged**, letting a sandboxed Psyche **spoof identity** or address arbitrary endpoints.
302	- **Invariant:** the live-Psyche turn driver **MUST capture stdout** (a bounded, stdin-fed, stdout-captured invocation — **never** `Stdio::null()`); the daemon **MUST strip** every Psyche-supplied `from=`/target/routing attribute, **re-stamp `from=<self_id>`**, and **constrain routing** — `reply` → the `__REPLY_TO__` sender only, `notify` → the agent's own user/subnet only. Body validated per 4.1.
303	- **spt-core mapping:** the live-Psyche turn driver + daemon outbound relay (ADR-0012); `spt-proto::event` type taxonomy (`+reply`/`+notify`). The interim `runtime::spawn_session` `Stdio::null()` path is **not** the live-Psyche driver.
304	- **Source:** grill-with-docs 2026-06-03 (uncovered by prior design passes); sister `src/live/wrapper/claude.rs` (sandbox `["Read","Write","Edit"]` + `parse_markers`).
305
306	### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`
307	- **Failure:** echo-commune (`run_bounded_stdin`) and the live-Psyche turn driver (D7.5) are **bounded LLM calls that block their calling thread** until the child answers or the timeout fires. Today each agent drives its own pulse from its **own process** (`spt/src/api/{live,startup}.rs`), so blocking is isolated. When the daemon hosts **N per-agent loops** (ADR-0004 target; the `run_pulse_loop` fan-out is currently def+test only), a **single serial driver** that calls these invocations inline lets one agent's slow/hung LLM call **stall every other agent's heartbeat, commune, and reply** — the per-call timeout bounds the worst case *only* when the work is isolated; serial makes the stalls additive.
308	- **Invariant:** each agent's bounded LLM-bearing work (echo-commune summarizer, Psyche turn) runs on its **own thread / off the shared scheduler** — no single-threaded driver iterating all agents may call a blocking invocation inline. One agent's slow/timed-out call must not delay another agent's next tick beyond tolerance.
309	- **spt-core mapping:** the daemon's multi-agent pulse/psyche hosting (ADR-0004, "all Psyche/pulse loops" consolidated); `run_pulse_loop` fan-out; echo-commune + the D7.5 Psyche driver.
310	- **Source:** grill-with-docs 2026-06-03 (forward invariant — the multi-agent fan-out is not yet wired). Distinct from ADR-0002 SERIOUS #6 (crash blast-radius, not scheduling latency).
311
312	### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`
313	<!-- [doc->REQ-HAZARD-WAN-ORIGIN-AUTH] -->
314	- **Failure:** the ADR-0009 access whitelist gates **unsolicited wire inbound by origin node**. If the gate's subject is read from record bytes (an `origin_node`/`from`/`node` field a sender wrote), any sender forges any origin and the whitelist is decoration — same spoof class as 7.3's Psyche-supplied `from=`, now on the cross-node surface.
315	- **Invariant:** the origin the gate (and detection/UX — "node X is driving") consumes is the **QUIC handshake-proven remote node id** (iroh `EndpointId` == Ed25519 node pubkey, the REQ-NET-1 identity binding) read from the **broker's conn/stream table** (`NetStreamInfo::remote_id_hex`), never from payload. Wire records carry **no origin field by design**; a forged one decodes as an ignored unknown field and influences nothing. `from` inside a record is reply-routing metadata only — never an authorization subject.
316	- **spt-core mapping:** `spt_net::net::wanmsg::WanMessage` (no origin field) + `spt_daemon::wan::receive_wan` (origin parameter sourced from the stream table) + every future wire-inbound consumer (D5b attach, D5c transfer, D8 notifs).
317	- **Source:** M4-D5a design (ADR-0009 consequence: the gate must bind to the transport identity).
318	- **Mesh note (ADR-0017, 2026-06-08):** preserved verbatim. The mesh never relays third-party rows, so a record's author is **always** the QUIC-handshake-proven connection origin. The new **membership proof** (seed-proof) is itself channel-bound to both handshake-proven pubkeys — the authorization subject stays transport truth, never payload.
319

## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)

These were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.

### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`
- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).
- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.
- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.
- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).

### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`
- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.
- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.
- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.
- **Source:** codex Stage A #14.

### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`
- **Failure:** the Psyche's sole outbound channel is its **stdout** (`<EVENT type="reply|notify">` intents — ADR-0012). Two ways to break it: (a) a **null-stdout / detached** live-Psyche driver silently **discards every reply and notify**; (b) the daemon relays a Psyche-supplied `from=`/target **unchanged**, letting a sandboxed Psyche **spoof identity** or address arbitrary endpoints.
- **Invariant:** the live-Psyche turn driver **MUST capture stdout** (a bounded, stdin-fed, stdout-captured invocation — **never** `Stdio::null()`); the daemon **MUST strip** every Psyche-supplied `from=`/target/routing attribute, **re-stamp `from=<self_id>`**, and **constrain routing** — `reply` → the `__REPLY_TO__` sender only, `notify` → the agent's own user/subnet only. Body validated per 4.1.
- **spt-core mapping:** the live-Psyche turn driver + daemon outbound relay (ADR-0012); `spt-proto::event` type taxonomy (`+reply`/`+notify`). The interim `runtime::spawn_session` `Stdio::null()` path is **not** the live-Psyche driver.
- **Source:** grill-with-docs 2026-06-03 (uncovered by prior design passes); sister `src/live/wrapper/claude.rs` (sandbox `["Read","Write","Edit"]` + `parse_markers`).

### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`
- **Failure:** echo-commune (`run_bounded_stdin`) and the live-Psyche turn driver (D7.5) are **bounded LLM calls that block their calling thread** until the child answers or the timeout fires. Today each agent drives its own pulse from its **own process** (`spt/src/api/{live,startup}.rs`), so blocking is isolated. When the daemon hosts **N per-agent loops** (ADR-0004 target; the `run_pulse_loop` fan-out is currently def+test only), a **single serial driver** that calls these invocations inline lets one agent's slow/hung LLM call **stall every other agent's heartbeat, commune, and reply** — the per-call timeout bounds the worst case *only* when the work is isolated; serial makes the stalls additive.
- **Invariant:** each agent's bounded LLM-bearing work (echo-commune summarizer, Psyche turn) runs on its **own thread / off the shared scheduler** — no single-threaded driver iterating all agents may call a blocking invocation inline. One agent's slow/timed-out call must not delay another agent's next tick beyond tolerance.
- **spt-core mapping:** the daemon's multi-agent pulse/psyche hosting (ADR-0004, "all Psyche/pulse loops" consolidated); `run_pulse_loop` fan-out; echo-commune + the D7.5 Psyche driver.
- **Source:** grill-with-docs 2026-06-03 (forward invariant — the multi-agent fan-out is not yet wired). Distinct from ADR-0002 SERIOUS #6 (crash blast-radius, not scheduling latency).

### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`
<!-- [doc->REQ-HAZARD-WAN-ORIGIN-AUTH] -->
- **Failure:** the ADR-0009 access whitelist gates **unsolicited wire inbound by origin node**. If the gate's subject is read from record bytes (an `origin_node`/`from`/`node` field a sender wrote), any sender forges any origin and the whitelist is decoration — same spoof class as 7.3's Psyche-supplied `from=`, now on the cross-node surface.
- **Invariant:** the origin the gate (and detection/UX — "node X is driving") consumes is the **QUIC handshake-proven remote node id** (iroh `EndpointId` == Ed25519 node pubkey, the REQ-NET-1 identity binding) read from the **broker's conn/stream table** (`NetStreamInfo::remote_id_hex`), never from payload. Wire records carry **no origin field by design**; a forged one decodes as an ignored unknown field and influences nothing. `from` inside a record is reply-routing metadata only — never an authorization subject.
- **spt-core mapping:** `spt_net::net::wanmsg::WanMessage` (no origin field) + `spt_daemon::wan::receive_wan` (origin parameter sourced from the stream table) + every future wire-inbound consumer (D5b attach, D5c transfer, D8 notifs).
- **Source:** M4-D5a design (ADR-0009 consequence: the gate must bind to the transport identity).
- **Mesh note (ADR-0017, 2026-06-08):** preserved verbatim. The mesh never relays third-party rows, so a record's author is **always** the QUIC-handshake-proven connection origin. The new **membership proof** (seed-proof) is itself channel-bound to both handshake-proven pubkeys — the authorization subject stays transport truth, never payload.


<task-notification>
<task-id>a0d76e6b7dce965c3</task-id>
<tool-use-id>toolu_01L2eRgZyo8ReLvuik2Pp1BZ</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b29e3949-c0ca-4c40-8fca-109118764bd7\tasks\a0d76e6b7dce965c3.output</output-file>
<status>completed</status>
<summary>Agent "Verify plan's source claims" completed</summary>
<result>Excellent! Now I have comprehensive information. Let me compile the findings:

## VERDICT SUMMARY

Based on thorough source code verification, here are the findings for each claim:

**1. daemon.rs (lines 165-170): production daemon creates broker via `Broker::bind_in_with_net(...)` returning `Arc<Broker>`, then `thread::spawn`s `broker.serve()` — broker is in-process thread. NO broker subcommand spawn anywhere.**

**TRUE.** daemon.rs:165-170 confirms `let broker = Broker::bind_in_with_net(...); thread::spawn(move || serve_broker.serve());`. broker.rs:196 confirms `bind_in_with_net` returns `Arc<Self>` (in-process object, not spawned child). Bash search found zero matches for `Command.*broker` or broker subcommand across the repo.

---

**2. applyhost.rs:239: post-apply re-attach uses only `sessions.first()` (single session) and `from_seq = 0` (full ring replay).**

**TRUE.** applyhost.rs:238-239 reads: `if let Some(session) = reply.sessions.first() { if let Err(e) = b.attach(session.session_id, 0)` — exactly `sessions.first()` + `from_seq=0`.

---

**3. applyhost.rs:~176: `record_applied(version)` + last-outcome=applied written BEFORE brain handoff completes (optimistic record). Also rollback at ~lines 167-170.**

**TRUE.** applyhost.rs:176-177: `let _ = cache.record_applied(version); let _ = cache.record_last_outcome("applied");` occurs BEFORE `apply_brain_only` call at line 186. Binary swap rollback at lines 167-170: `if let Err(e) = land_artifact(...) { let _ = std::fs::rename(&aside, exe_path); return Err(...) }`.

---

**4. applyhost.rs:36-38: comment stating daemon's resident loops keep running old logic until next restart; `ensure_running` spawns executable path.**

**TRUE.** applyhost.rs:36-38: "The daemon's own resident loops keep running the old logic until its next restart (`ensure_running` spawns the executable path — now the new binary)". daemon.rs:8-13 confirms `ensure_running` is the auto-start seam calling `spt daemon` subcommand.

---

**5. peerloop.rs:82-91: SUPERVISE_BACKOFF_BASE/CAP + SUPERVISE_HEALTHY_RUN constants (60s healthy run resets backoff) — WHAT do they currently supervise?**

**PARTIAL.** Constants exist at peerloop.rs:82-91 — `SUPERVISE_BACKOFF_BASE=5s`, `SUPERVISE_BACKOFF_CAP=300s`, `SUPERVISE_HEALTHY_RUN=60s`. However, searching the code reveals these are **defined but not currently used in production code** — they were prepared for the peer-pump supervisor but the supervision logic itself is not yet wired (the pump is in the daemon but supervisor backoff restart is the broker-restoration work pending).

---

**6. lifecycle.rs:486: pulse loop uses `sleep_interruptible(pulse_period)` — phase-relative, phase resets on restart.**

**TRUE.** lifecycle.rs:478 in `pulse_tick`: `sleep_interruptible(self.cfg.pulse_period, stop);`. Lines 484-497 show the function sleeps in 10ms slices. The pulse period IS phase-relative — phase resets on daemon restart (no durable deadline anchor yet; BROKER-BRAIN-SPLIT-RESTORATION.md Q4 identifies this as a gap).

---

**7. peerloop.rs:~304: pump cadences are catch-up/idempotent ("stagger from everything due now" or similar).**

**TRUE.** peerloop.rs:304 comment: "Stagger from 'everything due now' — the first tick primes peers." The cadences are already implemented as catch-up (lines 335-339 show `due(last_reg, now, cadence.registry)` — only fires when the interval has elapsed, starting from "None" = immediately due).

---

**8. brain.rs: What does `BrainState` contain TODAY — full field list. What do `cold_start`/`handoff`/`snapshot` do. Anything beyond session re-attach + timing that would be LOST?**

**TRUE with full details.** BrainState fields (brain.rs:64-74):
- `session_id: u64` — session the brain was driving
- `generation: u64` — generation counter (+1 per handoff)
- `next_seq: u64` — resume cursor (next output sequence needed)
- `gen_start_ms: u64` — prior generation's wall-clock start (INTENTIONALLY not carried forward on handoff)

`cold_start` (line 151): connects fresh, generation=0, gen_start=now, no session.
`handoff` (line 169): connects, rehydrates generation+1 and next_seq from prior, sets gen_start=NOW (not prior), re-subscribes from prior.next_seq.
`snapshot` (line 207): captures (session_id, generation, next_seq, gen_start_ms) for a future handoff.

**ONLY session re-attach info + generation counter + output cursor are carried.** Gen_start is deliberately reset (KNOWN-HAZARDS 2.4). No other state exists in BrainState — it is minimal by design (Spike 06 constraint 1: brain state is small, durable recovery lives in broker's EffectJournal).

---

**9. update.rs:~234: comment/doc claiming "the live daemon execs the verified new binary" — confirm no exec is actually wired.**

**FALSE (the claim about wiring is aspirational, not implemented).** update.rs:233-234 states `relaunch` "stands for 'exec the new binary's brain, which re-attaches'" — but docs/BROKER-BRAIN-SPLIT-RESTORATION.md §2.2 establishes this is **aspirational; never wired**: "the process split it depends on does not exist in production" (the broker is in-process, not a separate process, so the brain cannot exec away). The in-process broker thread would be orphaned. **No exec/execvp call exists on the apply path.**

---

**10. broker.rs (lines 73-143): Does broker track per-subscriber DELIVERED cursor or only ring + from_seq replay? Is EffectJournal broker-owned? Where does EffectJournal live?**

**PARTIAL.** OutputLog (lines 80-140) has:
- `subscriber: Option<SharedSend>` — ONE subscriber at a time (a dead brain replaced)
- `ring: VecDeque<(u64, Vec<u8>)>` — the buffered output + `next_seq` (the next sequence to assign)
- `attach` replays from `from_seq` onward

**Does NOT track per-subscriber delivered cursor TODAY** — only one subscriber is held and implicit last-sent is the ring's position. EffectJournal is **broker-owned** (broker.rs:168 `journal: Arc<EffectJournal>`). EffectJournal lives at `default_journal_path()` = `<spt_home>/daemon-effects.log` (broker.rs line at bottom).

---

**11. daemon.rs:206-217: shellwake boot sweep does orphan cleanup of wake_command watcher children on start.**

**TRUE.** daemon.rs:206-217 comment: "Boot sweep doubles as orphan cleanup after a daemon crash." Calls `crate::shellwake::spawn_wake_host(stop)`. shellwake.rs §1-24 (module docs) confirm: orphaned watchers from a dead daemon are cleaned up on the fresh daemon's boot reconcile loop.

---

**12. daemon.rs:60-103: de-elevation guard at daemon entry.**

**TRUE.** daemon.rs:45-103 shows:
- Unix (lines 60-74): calls `crate::deelevate::daemon_target_user()` to drop to unelevated, aborts if drop fails.
- Windows (lines 75-103): calls `crate::deelevate::is_elevated()`, spawns de-elevated child via UAC linked token if elevated, parent exits.

Guard is in place and blocks service as root/elevated.

---

**13. NetHost ownership TODAY: where created, held. Broker struct field? Daemon local? Arc shared?**

**TRUE — Broker-owned via OnceLock.** nethost.rs:411-435 shows `pub struct NetHost { runtime, endpoint, ... }`. broker.rs:175 shows `net: std::sync::OnceLock<NetHost>` (broker struct field). daemon.rs:154-186 shows `try_start_net()` creates NetHost, daemon passes it to `Broker::bind_in_with_net(..., net)` at line 165. The OnceLock is attach-once (net_retry_attach at line 185 calls `broker.attach_net(host)` on success). **NetHost is broker-owned; net bring-up retry (REQ-DAEMON-9) lives in daemon.rs lines 284-300** (`net_retry_attach` function loops on `try_start_net()` and calls broker-side `attach_net`).

---

**14. Update orchestration: trace `spt update apply` CLI → applyhost.**

**TRUE.** CLI flow: `crates/spt/src/cli.rs:818` routes `UpdateCmd::Apply` → `cmd_update_apply()` (line 1718). Function calls `spt_daemon::apply_staged(...)` at line 1731, passing cache + keys + exe + broker_socket_name. `apply_staged` is defined in `crates/spt-daemon/src/applyhost.rs:79`. **NO explicit seed-control channel seeding — the CLI invokes the daemon-hosted function directly via library link.** The seed-control channel (REQ-DAEMON-3 anchor) is used by `ensure_daemon_announced()` called at line 1719 to ping the daemon before applying.

---

**15. State migration on startup: does daemon mutate/migrate on-disk state file formats?**

**FALSE — NO version/schema migration code found.** Grep across daemon startup paths (daemon.rs, lifecycle.rs, broker.rs) found zero migration logic. Startup resets attachment state (`daemon.rs:127-137`), sweeps locks, loads config, but never mutates schemas. Applied.json is written by applyhost.rs (version field only). No migration code for applied.json, manifest caches, or journal format.

---

**16. traceable-reqs.toml quotes for REQ-UPD-3, REQ-UPD-4, REQ-UPD-6, REQ-UPD-8, REQ-DAEMON-3, REQ-DAEMON-9 + evidence tags.**

**PARTIAL.** The file is large; reading shows the requirements exist (confirmed via earlier bash grep finding evidence tags). REQ-UPD-3 has evidence tags `[impl->REQ-UPD-3]` in applyhost.rs:39,77 and update.rs:246. REQ-UPD-4 in applyhost.rs:40,78 and cli.rs. REQ-DAEMON-3 in daemon.rs:14. REQ-DAEMON-9 in daemon.rs:183,236,269. **Evidence actually exercises in-process handoff (lines 238-239 attach from broker's ring), NOT separate-process endpoint survival across restart. REQ-UPD-3's "no endpoint terminates" is provable only for in-process scenario TODAY**, not the two-process model the requirement describes.

---

**17. Historical notes on in-process broker as DELIBERATE interim step.**

**FALSE — NO deliberate deferral found.** Grep of PLAN files, git log, and code comments found:
- M0-PLAN.md: "Out (later): daemon/broker (M2/M3)"
- M3*/PLAN.md: all name "broker process + IPC (M3b)"
- No comments like "for now", "interim", "later" near broker setup or in Broker::bind
- docs/BROKER-BRAIN-SPLIT-RESTORATION.md §2.3 confirms: **"unintended drift. The operator confirmed there was NO deliberate decision to collapse the two processes"**

The collapse was **silent, not documented as a deferral.**</result>
<usage><subagent_tokens>88693</subagent_tokens><tool_uses>42</tool_uses><duration_ms>97003</duration_ms></usage>
</task-notification>


CI-SELFHOST-PLAN.md
DAEMON-LIFECYCLE-PLAN.md
M0-PLAN.md
M1-PLAN.md
M2a-PLAN.md
M2b-PLAN.md
M3-PLAN.md
M3a-PLAN.md
M3b-PLAN.md
M3c-PLAN.md
M4-D2-WIRE-PLAN.md
M4-D2f-PLAN.md
M4-D2g-PLAN.md
M4-D3-PLAN.md
M4-D4-PLAN.md
M4-D5-PLAN.md
M4-D6-PLAN.md
M4-D7-PLAN.md
M4-D7.5-PLAN.md
M4-D8-PLAN.md
M4-D9-PLAN.md
M4-PLAN.md
M5-D3-PLAN.md
M5-D4-PLAN.md
M5-D5-PLAN.md
M5-D6-PLAN.md
M5-D7-PLAN.md
M5-D8-PLAN.md
M5-D9-PLAN.md
M5-PLAN.md
M6-PLAN.md
M7-PLAN.md
M8-PLAN.md
MESH-D0-PLAN.md
MESH-D1-PLAN.md
MESH-D2-PLAN.md
MESH-D3-PLAN.md
MESH-D4-PLAN.md
MESH-D5-PLAN.md
MESH-D6-PLAN.md
MESH-D7-PLAN.md
MESH-D8-PLAN.md
SUBNET-MESH-PLAN.md
docs\DEBUG-CONVERGE-PLAN.md
V031-PLAN.md
V032-PLAN.md


M0-PLAN.md
M1-PLAN.md
M2a-PLAN.md
M2b-PLAN.md
M3-PLAN.md
M3a-PLAN.md
M3b-PLAN.md
M3c-PLAN.md
M4-D2-WIRE-PLAN.md
M4-D2f-PLAN.md
M4-D2g-PLAN.md
M4-D3-PLAN.md
M4-D4-PLAN.md
M4-D5-PLAN.md
M4-D6-PLAN.md
M4-D7-PLAN.md
M4-D7.5-PLAN.md
M4-D8-PLAN.md
M4-D9-PLAN.md
M4-PLAN.md
M5-D3-PLAN.md
M5-D4-PLAN.md
M5-D5-PLAN.md
M5-D6-PLAN.md
M5-D7-PLAN.md
M5-D8-PLAN.md
M5-D9-PLAN.md
M5-PLAN.md
M6-PLAN.md
M7-PLAN.md
M8-PLAN.md
MESH-D0-PLAN.md
MESH-D1-PLAN.md
MESH-D2-PLAN.md
MESH-D3-PLAN.md
MESH-D4-PLAN.md
MESH-D5-PLAN.md
MESH-D6-PLAN.md
MESH-D7-PLAN.md
MESH-D8-PLAN.md


Prompt:
<task-notification>
<task-id>a0d76e6b7dce965c3</task-id>
<tool-use-id>toolu_01L2eRgZyo8ReLvuik2Pp1BZ</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b29e3949-c0ca-4c40-8fca-109118764bd7\tasks\a0d76e6b7dce965c3.output</output-file>
<status>completed</status>
<summary>Agent "Verify plan's source claims" completed</summary>
<result>Excellent! Now I have comprehensive information. Let me compile the findings:

## VERDICT SUMMARY

Based on thorough source code verification, here are the findings for each claim:

**1. daemon.rs (lines 165-170): production daemon creates broker via `Broker::bind_in_with_net(...)` returning `Arc<Broker>`, then `thread::spawn`s `broker.serve()` — broker is in-process thread. NO broker subcommand spawn anywhere.**

**TRUE.** daemon.rs:165-170 confirms `let broker = Broker::bind_in_with_net(...); thread::spawn(move || serve_broker.serve());`. broker.rs:196 confirms `bind_in_with_net` returns `Arc<Self>` (in-process object, not spawned child). Bash search found zero matches for `Command.*broker` or broker subcommand across the repo.

---

**2. applyhost.rs:239: post-apply re-attach uses only `sessions.first()` (single session) and `from_seq = 0` (full ring replay).**

**TRUE.** applyhost.rs:238-239 reads: `if let Some(session) = reply.sessions.first() { if let Err(e) = b.attach(session.session_id, 0)` — exactly `sessions.first()` + `from_seq=0`.

---

**3. applyhost.rs:~176: `record_applied(version)` + last-outcome=applied written BEFORE brain handoff completes (optimistic record). Also rollback at ~lines 167-170.**

**TRUE.** applyhost.rs:176-177: `let _ = cache.record_applied(version); let _ = cache.record_last_outcome("applied");` occurs BEFORE `apply_brain_only` call at line 186. Binary swap rollback at lines 167-170: `if let Err(e) = land_artifact(...) { let _ = std::fs::rename(&aside, exe_path); return Err(...) }`.

---

**4. applyhost.rs:36-38: comment stating daemon's resident loops keep running old logic until next restart; `ensure_running` spawns executable path.**

**TRUE.** applyhost.rs:36-38: "The daemon's own resident loops keep running the old logic until its next restart (`ensure_running` spawns the executable path — now the new binary)". daemon.rs:8-13 confirms `ensure_running` is the auto-start seam calling `spt daemon` subcommand.

---

**5. peerloop.rs:82-91: SUPERVISE_BACKOFF_BASE/CAP + SUPERVISE_HEALTHY_RUN constants (60s healthy run resets backoff) — WHAT do they currently supervise?**

**PARTIAL.** Constants exist at peerloop.rs:82-91 — `SUPERVISE_BACKOFF_BASE=5s`, `SUPERVISE_BACKOFF_CAP=300s`, `SUPERVISE_HEALTHY_RUN=60s`. However, searching the code reveals these are **defined but not currently used in production code** — they were prepared for the peer-pump supervisor but the supervision logic itself is not yet wired (the pump is in the daemon but supervisor backoff restart is the broker-restoration work pending).

---

**6. lifecycle.rs:486: pulse loop uses `sleep_interruptible(pulse_period)` — phase-relative, phase resets on restart.**

**TRUE.** lifecycle.rs:478 in `pulse_tick`: `sleep_interruptible(self.cfg.pulse_period, stop);`. Lines 484-497 show the function sleeps in 10ms slices. The pulse period IS phase-relative — phase resets on daemon restart (no durable deadline anchor yet; BROKER-BRAIN-SPLIT-RESTORATION.md Q4 identifies this as a gap).

---

**7. peerloop.rs:~304: pump cadences are catch-up/idempotent ("stagger from everything due now" or similar).**

**TRUE.** peerloop.rs:304 comment: "Stagger from 'everything due now' — the first tick primes peers." The cadences are already implemented as catch-up (lines 335-339 show `due(last_reg, now, cadence.registry)` — only fires when the interval has elapsed, starting from "None" = immediately due).

---

**8. brain.rs: What does `BrainState` contain TODAY — full field list. What do `cold_start`/`handoff`/`snapshot` do. Anything beyond session re-attach + timing that would be LOST?**

**TRUE with full details.** BrainState fields (brain.rs:64-74):
- `session_id: u64` — session the brain was driving
- `generation: u64` — generation counter (+1 per handoff)
- `next_seq: u64` — resume cursor (next output sequence needed)
- `gen_start_ms: u64` — prior generation's wall-clock start (INTENTIONALLY not carried forward on handoff)

`cold_start` (line 151): connects fresh, generation=0, gen_start=now, no session.
`handoff` (line 169): connects, rehydrates generation+1 and next_seq from prior, sets gen_start=NOW (not prior), re-subscribes from prior.next_seq.
`snapshot` (line 207): captures (session_id, generation, next_seq, gen_start_ms) for a future handoff.

**ONLY session re-attach info + generation counter + output cursor are carried.** Gen_start is deliberately reset (KNOWN-HAZARDS 2.4). No other state exists in BrainState — it is minimal by design (Spike 06 constraint 1: brain state is small, durable recovery lives in broker's EffectJournal).

---

**9. update.rs:~234: comment/doc claiming "the live daemon execs the verified new binary" — confirm no exec is actually wired.**

**FALSE (the claim about wiring is aspirational, not implemented).** update.rs:233-234 states `relaunch` "stands for 'exec the new binary's brain, which re-attaches'" — but docs/BROKER-BRAIN-SPLIT-RESTORATION.md §2.2 establishes this is **aspirational; never wired**: "the process split it depends on does not exist in production" (the broker is in-process, not a separate process, so the brain cannot exec away). The in-process broker thread would be orphaned. **No exec/execvp call exists on the apply path.**

---

**10. broker.rs (lines 73-143): Does broker track per-subscriber DELIVERED cursor or only ring + from_seq replay? Is EffectJournal broker-owned? Where does EffectJournal live?**

**PARTIAL.** OutputLog (lines 80-140) has:
- `subscriber: Option<SharedSend>` — ONE subscriber at a time (a dead brain replaced)
- `ring: VecDeque<(u64, Vec<u8>)>` — the buffered output + `next_seq` (the next sequence to assign)
- `attach` replays from `from_seq` onward

**Does NOT track per-subscriber delivered cursor TODAY** — only one subscriber is held and implicit last-sent is the ring's position. EffectJournal is **broker-owned** (broker.rs:168 `journal: Arc<EffectJournal>`). EffectJournal lives at `default_journal_path()` = `<spt_home>/daemon-effects.log` (broker.rs line at bottom).

---

**11. daemon.rs:206-217: shellwake boot sweep does orphan cleanup of wake_command watcher children on start.**

**TRUE.** daemon.rs:206-217 comment: "Boot sweep doubles as orphan cleanup after a daemon crash." Calls `crate::shellwake::spawn_wake_host(stop)`. shellwake.rs §1-24 (module docs) confirm: orphaned watchers from a dead daemon are cleaned up on the fresh daemon's boot reconcile loop.

---

**12. daemon.rs:60-103: de-elevation guard at daemon entry.**

**TRUE.** daemon.rs:45-103 shows:
- Unix (lines 60-74): calls `crate::deelevate::daemon_target_user()` to drop to unelevated, aborts if drop fails.
- Windows (lines 75-103): calls `crate::deelevate::is_elevated()`, spawns de-elevated child via UAC linked token if elevated, parent exits.

Guard is in place and blocks service as root/elevated.

---

**13. NetHost ownership TODAY: where created, held. Broker struct field? Daemon local? Arc shared?**

**TRUE — Broker-owned via OnceLock.** nethost.rs:411-435 shows `pub struct NetHost { runtime, endpoint, ... }`. broker.rs:175 shows `net: std::sync::OnceLock<NetHost>` (broker struct field). daemon.rs:154-186 shows `try_start_net()` creates NetHost, daemon passes it to `Broker::bind_in_with_net(..., net)` at line 165. The OnceLock is attach-once (net_retry_attach at line 185 calls `broker.attach_net(host)` on success). **NetHost is broker-owned; net bring-up retry (REQ-DAEMON-9) lives in daemon.rs lines 284-300** (`net_retry_attach` function loops on `try_start_net()` and calls broker-side `attach_net`).

---

**14. Update orchestration: trace `spt update apply` CLI → applyhost.**

**TRUE.** CLI flow: `crates/spt/src/cli.rs:818` routes `UpdateCmd::Apply` → `cmd_update_apply()` (line 1718). Function calls `spt_daemon::apply_staged(...)` at line 1731, passing cache + keys + exe + broker_socket_name. `apply_staged` is defined in `crates/spt-daemon/src/applyhost.rs:79`. **NO explicit seed-control channel seeding — the CLI invokes the daemon-hosted function directly via library link.** The seed-control channel (REQ-DAEMON-3 anchor) is used by `ensure_daemon_announced()` called at line 1719 to ping the daemon before applying.

---

**15. State migration on startup: does daemon mutate/migrate on-disk state file formats?**

**FALSE — NO version/schema migration code found.** Grep across daemon startup paths (daemon.rs, lifecycle.rs, broker.rs) found zero migration logic. Startup resets attachment state (`daemon.rs:127-137`), sweeps locks, loads config, but never mutates schemas. Applied.json is written by applyhost.rs (version field only). No migration code for applied.json, manifest caches, or journal format.

---

**16. traceable-reqs.toml quotes for REQ-UPD-3, REQ-UPD-4, REQ-UPD-6, REQ-UPD-8, REQ-DAEMON-3, REQ-DAEMON-9 + evidence tags.**

**PARTIAL.** The file is large; reading shows the requirements exist (confirmed via earlier bash grep finding evidence tags). REQ-UPD-3 has evidence tags `[impl->REQ-UPD-3]` in applyhost.rs:39,77 and update.rs:246. REQ-UPD-4 in applyhost.rs:40,78 and cli.rs. REQ-DAEMON-3 in daemon.rs:14. REQ-DAEMON-9 in daemon.rs:183,236,269. **Evidence actually exercises in-process handoff (lines 238-239 attach from broker's ring), NOT separate-process endpoint survival across restart. REQ-UPD-3's "no endpoint terminates" is provable only for in-process scenario TODAY**, not the two-process model the requirement describes.

---

**17. Historical notes on in-process broker as DELIBERATE interim step.**

**FALSE — NO deliberate deferral found.** Grep of PLAN files, git log, and code comments found:
- M0-PLAN.md: "Out (later): daemon/broker (M2/M3)"
- M3*/PLAN.md: all name "broker process + IPC (M3b)"
- No comments like "for now", "interim", "later" near broker setup or in Broker::bind
- docs/BROKER-BRAIN-SPLIT-RESTORATION.md §2.3 confirms: **"unintended drift. The operator confirmed there was NO deliberate decision to collapse the two processes"**

The collapse was **silent, not documented as a deferral.**</result>
<usage><subagent_tokens>88693</subagent_tokens><tool_uses>42</tool_uses><duration_ms>97003</duration_ms></usage>
</task-notification>


Bash

git -C /c/Users/decid/Documents/projects/spt-core log --oneline -20

Recent commit history for milestone state