/grill-me help me brainstorm a potential adapter. what is the best form for this idea? -- ## a "forum" shell? - multiple agents attach to the singular shell instance (is this possible?) - when one age…


Read the type taxonomy. Key tension already visible: your idea says "shell," but spt-core's **Shell = owner-linked, EXCLUSIVE** — "only that owner `endpoint_id` may control it" (CONTEXT.md:284-285). "Multiple agents attach to one shell" directly breaks that invariant. So before anything else, must resolve what this thing actually *is* in the taxonomy.

Quick check — does a group/room/broadcast messaging primitive already exist?


CI-SELFHOST-PLAN.md:63:- [ ] Confirm gravity-linux ssh reachability + disk headroom (~20 GB for
AGENTS.md:3:Harness-independent core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P networking, runtime-manifest harness contract). Rust crates + a single `spt` binary. Clean-room rebuild of `claude_skill_owl`.
AGENTS.md:23:- Match surrounding code style; copy-verbatim the sister project's stable wire/schema formats (ADR-0001), clean-room everything else.
CHANGELOG.md:137:- **Two new `spt how-to` guides: `subnet` and `live`.** Pairing machines (create vs join, the 6-digit code, reaching remote agents) and running as a live agent (the persistent `spt api listen` relay, the Psyche seam) now have task-oriented in-binary topics instead of dead-ending.
CHANGELOG.md:222:- **Messages now carry who sent them — a person or an agent.** A message you send is delivered marked as user-sent, and the daemon re-stamps anything that falsely claims to be from a user. A human-backed **Gateway** endpoint is accepted as a first-class endpoint — addressable, able to own shells, and able to subscribe to digests — including from another machine on your subnet.
CLAUDE.md:3:Harness-independent core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P networking, runtime-manifest harness contract). Rust crates + a single `spt` binary. Clean-room rebuild of `claude_skill_owl`.
CLAUDE.md:21:- Match surrounding code style; copy-verbatim the sister project's stable wire/schema formats (ADR-0001), clean-room everything else.
DIGEST-MILESTONE-PLAN.md:72:  presentation knobs with consumer override threaded from pull/subscribe. `[impl/unit]`
CONTEXT.md:291:**Broadcast policy** (manifest): the shell advertises its availability to all agents on the subnet, only same-node agents, or not at all (agents learn of it only by user instruction). Shells run only on nodes where the shell adapter is installed/advertised.
CONTEXT.md:293:[Omitted long matching line]
CONTEXT.md:313:[Omitted long matching line]
CONTEXT.md:343:[Omitted long matching line]
CONTEXT.md:446:[Omitted long matching line]
CONTEXT.md:470:[Omitted long matching line]
CONTEXT.md:508:A command letting any agent **issue a subnet-wide notif** to the user. v1: it **reaches the user on their active endpoint from any agent** (the default broadcast-to-user). *Forward:* gains targeting — **all subnet users (default) or specific subnet users** — once the per-(subnet, user) model lands; the `from` field is what makes targeted/attributed delivery possible.
CONTEXT.md:604:[Omitted long matching line]
docs\BROKER-BRAIN-SPLIT-RESTORATION.md:74:- `apply` therefore performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — achieving nothing for a binary swap. `update.rs:234`'s statement that "the live daemon execs the verified new binary" is **aspirational; it was never wired** because the process split it depends on does not exist in production.
docs\BROKER-BRAIN-SPLIT-RESTORATION.md:143:- **Digest hub → broker** for the live projections + subscriber sockets; **the parse that feeds it → brain** (pushes parsed digests over IPC).
docs\BROKER-BRAIN-SPLIT-RESTORATION.md:157:[Omitted long matching line]
docs\CONTEXT-MEMORY.md:12:### The discriminator (applied per *topic*, not per fragment)
docs\CONTEXT-MEMORY.md:45:memformat goes from a decorative, ignored seed to the **load-bearing schema that defines the two-tier context**. It makes the live/project decision **per-topic and rare** instead of per-fragment and constant — which is what makes the split stable.
docs\CONTEXT-MEMORY.md:47:- **Tier-tagged topics.** Each topic carries a tier: `live` or `project`. The tier is decided **once, when the topic is created** (apply the discriminator one time), not per fragment. Communes then just *fill* topics; the tier routes content to the right context file + sync tier. The two-slice envelope (`<live-context>`/`<project-context>`) becomes the *rendering* of tier-tagged topics.
docs\CONTEXT-MEMORY.md:48:- **Self-evolving.** The Psyche adds/removes/re-tiers topics as it learns Self's realm of work (memformat's original intent). **Re-tiering migrates** the topic's accumulated content between tiers (project→live or vice versa), not forward-only.
docs\CONTEXT-MEMORY.md:49:- **The schema is live** (per-agent, travels everywhere); **project-tagged topic content** is instantiated per-project.
docs\CONTEXT-MEMORY.md:54:A memformat topic is a tier-tagged **container of structured blocks**, not free-form prose. Each block:
docs\CONTEXT-MEMORY.md:74:A generated, lightweight **INDEX** over topics — summaries + concepts + aliases + paths — **not** a vector DB, **not** a full mirror (adapted from pi-agent-memory's Retrieval Map). One INDEX per **project** (aggregating all agents' project context → realizes cross-agent synthesis + query-routing: "which endpoint owns this task") and per **agent**. Regenerated on context change; the merge driver + INDEX regeneration keep it current.
docs\CONTEXT-MEMORY.md:81:- **SPARSE — the long tail, pointer + INDEX, retrieved on demand:** other projects, **cross-agent/cross-project synthesis**, cold/archived blocks. (This is where pi's bounded "topic-names + pointer, never inline bodies" awareness pattern applies — it scales regardless of store size.)
docs\adr\0003-multi-instance-node-anchored-identity-model.md:47:[Omitted long matching line]
docs\FAULT-MATRIX.md:20:| 1 | **Brain killed mid-PTY-stream** (logic crash / routine self-update) | Logic halts until restart | Broker, PTY child (pid stable), output log | New brain re-subscribes from cursor; gapless + exactly-once | `spt-daemon/tests/` B2/B9 handoff suite; `tests/idempotent.rs` (crash before-intent / before-effect / after-effect) |
docs\FAULT-MATRIX.md:21:| 2 | **Brain killed mid-QUIC-stream, receive side** | Logic halts | Broker-owned endpoint, conn, stream, read ring | Resubscribe from durable cursor; ring replays the dead window | `tests/netstream.rs::receiver_brain_restart_is_gapless_and_exactly_once` |
docs\FAULT-MATRIX.md:23:| 4 | **Brain dead across peer connect/disconnect** | Presence consumption pauses | PresenceLog ring buffers liveness transitions | Resubscribe from presence cursor; missed transitions replay | `tests/presence.rs` (both tests) |
docs\FAULT-MATRIX.md:43:| 23 | **Psyche replies with no inbound sender to answer** (alarm-fired turn emits a reply) | That one intent drops with a typed outcome (fail-closed — never broadcast, never guessed) | Every perch (zero deliveries); the notify leg (independent) | The Psyche's next answered turn replies normally | `psyrelay.rs::reply_without_target_is_dropped` |
docs\DOCS-STRATEGY.md:49:**Tier 2 (deferred, tracked):** remaining verticals' tutorials/how-tos (overview + reference stubs only in v0.1) · MCP doc server · `--help --json` mode (low marginal value over the generated CLI reference) · capstone tutorial · per-vertical `llms.txt` fan-out (root links pages directly until then).
docs\adr\0001-clean-room-fork-from-claude-skill-owl.md:1:# Clean-room fork from claude_skill_owl with hard freeze on the sister project
docs\adr\0001-clean-room-fork-from-claude-skill-owl.md:13:Build spt-core as a clean-room fork. Empty workspace, new crate boundaries, fresh lifecycle invariants. Rejected alternatives: rename-and-evolve in-place (preserves the fragility) and a hybrid crate-restructure-with-bulk-port (still drags fragile foundations into the new shape).
docs\adr\0001-clean-room-fork-from-claude-skill-owl.md:17:1. **Copy-verbatim the commodity layer.** Wire formats (`<EVENT>` envelope grammar, TCP registry RPC), the SQLite spool schema/DDL, `info.json` perch metadata shape, and self-contained utility modules are stable and well-tested. Reimplementing them clean-room reintroduces bugs that took months to find in the sister project. Clean-room applies to architecture and lifecycle, not to every artifact.
docs\FLAKE-LEDGER.md:14:[Omitted long matching line]
docs\MANIFEST.md:136:arg_truncation = 40                # …any consumer may override at pull/subscribe
docs\MANIFEST.md:293:broadcast = "subnet"               # subnet | same-node | none
docs\DEFERRED.md:44:[Omitted long matching line]

[Showing results with pagination = limit: 40]


No existing group/room/multicast primitive. Messaging substrate = point-to-point (`send`/`ring`) + digest pull/subscribe (one producer→one consumer) + notify-to-user. No agent↔many-agents room exists. So your idea is a **genuinely new primitive**, not a config of an existing one.

**Core finding: it's not a Shell.** Shell = single-owner, exclusive control, *nothing intelligent runs there* (CONTEXT.md:271-285). Your forum = N intelligent peers, symmetric send+receive, fan-out-to-others. Every property inverts the Shell model. Calling it a shell fights the taxonomy.

**Closest precedent that DOES fit:** `PresenceChannel` (CONTEXT.md:240-248) — "a durable, shell-agnostic 2-way **thread**: messages persist in the channel, not in any one Shell." That's exactly your "singular instance multiple attach" shape — a *channel endpoint holding a thread*, participants attach. It's just user↔agent today, not agent↔agent group.

So "is it possible?" → yes, but as a **channel/room endpoint type** (broker endpoint, open type system, CONTEXT.md:250), not a shell. That's the root branch. First question:


AskUserQuestion

Question: What FORM should the forum take in spt-core's taxonomy? (This is the root branch — everything else depends on it.)

Choices:
A 'Forum'/'Room' broker endpoint (open type system, PresenceChannel-style). Holds the thread; agents join; a post fans out to all other members with sender attribution. Cleanest fit — endpoints already addressable, thread persistence already a modeled pattern.
endpoint type: Forum (broker)
  - holds durable thread
  - members = [agentA, agentB, ...]
  - post(from=A) -> deliver to members \ {A}
  - addressable like any endpoint
  - reuses spool/registry/Iroh routing
No new endpoint. Add a 'group'/'topic' construct in the spool+routing layer: a named set of subscribers, send-to-group fans out. Lighter, but no perch = no liveness/discovery/cross-node identity for the forum itself.
send --group <name> <<EOF ...
  routing layer: group = subscriber set
  no perch, no info.json
  fan-out at delivery time
  (forum has no identity/liveness of its own)
Generalize the (deferred) PresenceChannel from user<->agent to agent<->agent N-party. Reuses the durable-thread + dispatch/bind/thread model directly. But PresenceChannel impl is deferred past v1 — couples you to unbuilt work.
PresenceChannel (today): user <-> agent thread
  -> generalize participants to N agents
  -> reuse dispatch/bind/thread styles
  BUT: impl deferred past v1
NOT recommended — breaks the Shell exclusive-single-owner invariant (CONTEXT.md:284-285) and the 'nothing intelligent runs at a shell' rule. Listed only to close the branch explicitly.
Shell: owner-linked, EXCLUSIVE
  'only that owner endpoint_id may control it'
  nothing intelligent runs there
  => N peer agents violates both. REJECT.


Your questions have been answered: "What FORM should the forum take in spt-core's taxonomy? (This is the root branch — everything else depends on it.)"=(no option selected) notes: new channel endpoint type seems best. there are some interesting things to consider though. like we might want to enable agents to join or leave a channel, or to mute it, or to set a minimum update interval (i.e. doyle receives updates no more frequently than 5 minutes apart). do we build a shell to enable interfacing with this endpoint? or at this rate, maybe we should just build a "discord bot shell" that could use discord as the channel substrate. --- might be good to change over to a /grill-with-docs if we end up deciding on new core features. still just exploring what this "room/forum/channel/group messaging" shape looks like, for now. You can now continue with these answers in mind.

Question: What FORM should the forum take in spt-core's taxonomy? (This is the root branch — everything else depends on it.)

Choices:
A 'Forum'/'Room' broker endpoint (open type system, PresenceChannel-style). Holds the thread; agents join; a post fans out to all other members with sender attribution. Cleanest fit — endpoints already addressable, thread persistence already a modeled pattern.
endpoint type: Forum (broker)
  - holds durable thread
  - members = [agentA, agentB, ...]
  - post(from=A) -> deliver to members \ {A}
  - addressable like any endpoint
  - reuses spool/registry/Iroh routing
No new endpoint. Add a 'group'/'topic' construct in the spool+routing layer: a named set of subscribers, send-to-group fans out. Lighter, but no perch = no liveness/discovery/cross-node identity for the forum itself.
send --group <name> <<EOF ...
  routing layer: group = subscriber set
  no perch, no info.json
  fan-out at delivery time
  (forum has no identity/liveness of its own)
Generalize the (deferred) PresenceChannel from user<->agent to agent<->agent N-party. Reuses the durable-thread + dispatch/bind/thread model directly. But PresenceChannel impl is deferred past v1 — couples you to unbuilt work.
PresenceChannel (today): user <-> agent thread
  -> generalize participants to N agents
  -> reuse dispatch/bind/thread styles
  BUT: impl deferred past v1
NOT recommended — breaks the Shell exclusive-single-owner invariant (CONTEXT.md:284-285) and the 'nothing intelligent runs at a shell' rule. Listed only to close the branch explicitly.
Shell: owner-linked, EXCLUSIVE
  'only that owner endpoint_id may control it'
  nothing intelligent runs there
  => N peer agents violates both. REJECT.


Good instinct — "new channel endpoint type" right. But your Discord-bot-shell aside is the real fork, and it dissolves your earlier Shell tension. Let me lay it out before the next question.

**Your three sub-features, mapped:**
- **join/leave** → membership ops on the channel.
- **mute** → per-member delivery suppression.
- **min update interval** (doyle ≤ every 5 min) → this is a **digest cadence**, and spt-core *already has it*: digest pull/subscribe with per-consumer presentation knobs "threaded from pull/subscribe" (DIGEST-MILESTONE-PLAN.md:72, MANIFEST.md:136). Forum delivery = subscribe-to-channel-digest. Reuse, don't rebuild.

**The substrate fork (this is the load-bearing decision):**

*Native* — spt-core builds the room: spool-backed thread, Iroh fan-out, membership/mute/cadence all core code. Stays in-subnet, zero external dep, but every feature is core work (→ ADR + REQ + grill-with-docs).

*External substrate (Discord)* — the "channel" IS a Discord channel; spt bridges. You get persistence, mute, mobile, notifications, scrollback, **human participation** — all free. join/leave/mute become *Discord's native UI*. Only min-interval stays yours (a digest knob).

**The insight you were circling:** a Discord bridge is a **proper single-owner Shell**. Each agent owns ITS OWN bridge-shell (exclusive ✓, driven surface ✓, nothing-intelligent-at-the-shell ✓ — intelligence is the agent driving it). N agents = N bridge-shells, all pointed at one Discord channel. The "forum" *emerges* at the substrate; the shared room is never a single multi-owner spt endpoint. **Your original "shell" intuition was right — just one shell per agent, not one shell for all.** That's why it felt like a shell.

So it's not native-OR-shell. It's:


Question: Where does the forum's shared room actually LIVE — inside spt-core, or on an external substrate bridged by per-agent shells? (Decides core-feature vs downstream-adapter, and who can participate.)

Choices:
Room lives in spt-core: spool thread + Iroh fan-out + membership/mute/cadence as core code. Agent<->agent only, in-subnet, no external dep. Cost: every feature is core work (ADR+REQ). Best if participants are agents-only and you want zero external services.
spt-core owns:
  Forum endpoint (broker)
  thread persistence (spool)
  fan-out routing (Iroh)
  membership / mute / digest-cadence
Participants: subnet agents only
Humans: only via a bridge shell you also build
Room = a Discord channel. Each agent owns ONE single-owner bridge-shell (resolves Shell model cleanly). Humans + agents coexist natively. join/leave/mute = Discord's own UI; only min-interval is yours. Downstream shell-adapter, near-zero core change. Cost: external dep, bot tokens, Discord ToS.
Discord channel = the room
Per agent: 1 bridge-shell (exclusive owner OK)
  shell posts agent msg -> Discord
  shell reads Discord -> sensory -> agent
Humans join natively (phone, mute, scrollback)
spt-core: ~no change (just a shell adapter)
Build the native Forum endpoint AS the canonical room, PLUS allow bridge-shells (Discord/Matrix/IRC) so humans join from anywhere. Substrate-agnostic. Most capable, most work. Best if you want both in-subnet agent rooms AND human reach.
Native Forum endpoint = source of truth
   + bridge-shell adapters (Discord, Matrix...)
     mirror the native thread <-> external
Agents: native fan-out
Humans: via whichever bridge they like
Most power, most build
Not ready to pick substrate. Surface more sub-branches first (e.g. ephemeral vs persistent threads, message types, addressing, read-vs-push semantics) before committing.
Defer substrate.
Explore next:
  - thread lifecycle (ephemeral/persistent)
  - payload types (text only? files? typed ops?)
  - addressing (how an agent names a forum)
  - push vs pull delivery per member


Your questions have been answered: "Where does the forum's shared room actually LIVE — inside spt-core, or on an external substrate bridged by per-agent shells? (Decides core-feature vs downstream-adapter, and who can participate.)"=(no option selected) notes: i'm interested in exploring the discord bridge shell. here are the friction points/unknowns: 1. i'd want to be able to address each participating agent, but registering a new bot per joining agent sounds annoying. do discord's APIs enable automating this in any way? 2. since shells are instantiated to their owner based off a single adapter, i'm imagining it as one-shell-instance-per-channel, with the shell taking on the name of its corresponding discord channel. does that make sense? --- what other interesting gray areas are there.... You can now continue with these answers in mind.

Question: Where does the forum's shared room actually LIVE — inside spt-core, or on an external substrate bridged by per-agent shells? (Decides core-feature vs downstream-adapter, and who can participate.)

Choices:
Room lives in spt-core: spool thread + Iroh fan-out + membership/mute/cadence as core code. Agent<->agent only, in-subnet, no external dep. Cost: every feature is core work (ADR+REQ). Best if participants are agents-only and you want zero external services.
spt-core owns:
  Forum endpoint (broker)
  thread persistence (spool)
  fan-out routing (Iroh)
  membership / mute / digest-cadence
Participants: subnet agents only
Humans: only via a bridge shell you also build
Room = a Discord channel. Each agent owns ONE single-owner bridge-shell (resolves Shell model cleanly). Humans + agents coexist natively. join/leave/mute = Discord's own UI; only min-interval is yours. Downstream shell-adapter, near-zero core change. Cost: external dep, bot tokens, Discord ToS.
Discord channel = the room
Per agent: 1 bridge-shell (exclusive owner OK)
  shell posts agent msg -> Discord
  shell reads Discord -> sensory -> agent
Humans join natively (phone, mute, scrollback)
spt-core: ~no change (just a shell adapter)
Build the native Forum endpoint AS the canonical room, PLUS allow bridge-shells (Discord/Matrix/IRC) so humans join from anywhere. Substrate-agnostic. Most capable, most work. Best if you want both in-subnet agent rooms AND human reach.
Native Forum endpoint = source of truth
   + bridge-shell adapters (Discord, Matrix...)
     mirror the native thread <-> external
Agents: native fan-out
Humans: via whichever bridge they like
Most power, most build
Not ready to pick substrate. Surface more sub-branches first (e.g. ephemeral vs persistent threads, message types, addressing, read-vs-push semantics) before committing.
Defer substrate.
Explore next:
  - thread lifecycle (ephemeral/persistent)
  - payload types (text only? files? typed ops?)
  - addressing (how an agent names a forum)
  - push vs pull delivery per member


Both answered, and #2 is sharper than you think — it un-asks the Shell question entirely.

**#1 — addressing agents without a bot-per-agent: YES, solved by webhooks.** Discord channel **webhooks** post messages with a per-message `username` + `avatar_url` override. So **one bot application + one channel webhook** can post as N distinct agent identities — doyle, deployah, todlando each appear as themselves, zero per-agent registration. This is exactly how matterbridge/IRC bridges impersonate many users with one integration.
- Outbound (agent → Discord): webhook, per-message identity. Cheap, no gateway.
- Inbound (Discord → agents): one bot **gateway** connection with the `MESSAGE_CONTENT` privileged intent reads the channel. One reader, not N.
- Caveat: webhook identities aren't real users — can't be @mentioned as users, no presence. And Discord rate-limits webhooks (check exact: ~tens/min per channel) — N chatty agents can hit it.

**#2 — one-shell-per-channel, named after the channel: makes operational sense, but it's NOT a shell.** A Shell is single-owner-exclusive. A per-channel instance with N agent participants is **multi-tenant by definition** → fails the invariant *again*. What you've actually drawn is a **Forum endpoint whose substrate is Discord** — the per-channel instance IS the forum; Discord is its persistence + UI + human-reach layer. So your two earlier options **converge**: don't choose native-vs-Discord — build **one Forum endpoint type with a pluggable substrate**; first substrate = Discord, "native spool" a later substrate. The fork dissolves.

Ownership of that per-channel forum → follow the **PresenceChannel precedent: a broker endpoint, no single owner** (CONTEXT.md:240). Multi-tenant fits broker, not Shell.

**Gray-area landscape (your "what else"):**
1. **Inbound addressing** — how a Discord msg targets a *specific* agent vs ambient-to-all. ← grilling next
2. **Echo-loop prevention** — bot reads its own webhook posts → infinite loop. Classic bridge bug; must filter self-authored.
3. **Human authority** — a human in Discord is a human-backed origin. `user-msg` type? But Discord ⊄ subnet trust boundary. Who's in the channel = the trust gate.
4. **Offline catch-up** — Discord retains history, so an offline agent can backfill on rejoin — a property the native version lacks for free.
5. **Delivery per agent** — push-to-live vs digest-pull vs your min-interval cadence; mute.
6. **Secrets** — bot token location (adapter strings? secret store).
7. **Single-bridge-node** — the Discord connection lives on one node; cross-node agents join via Iroh. SPOF.
8. **Payload types** — text only, or Discord attachments → text+file channel.

Next branch — the pair to #1 (outbound identity ✓), now inbound routing:


Question: How does a Discord message get ROUTED to the right agent — i.e. how is each participant addressed inbound, and what's the default delivery?

Choices:
Bridge parses '@doyle ...' from message text -> direct to that agent (push). No mention = ambient, fan-out to ALL members (digest/cadence). Mirrors how humans actually talk in a channel. Natural, but text-parse is fuzzy (typos, name collisions).
msg '@doyle ship it'  -> doyle (direct push)
msg 'anyone around?'  -> all members (ambient)
parse @name from content
name clash / typo = routing miss
No per-agent addressing. Every Discord post fans out to every member with sender attribution (your original 'all OTHER attached agents receive it'). Simplest, truest to a 'forum'. Agents self-decide relevance. Cost: every agent processes every message = token burn.
every post -> all members \ {sender}
sender name attached
no targeting; agents filter relevance
closest to original 'group conversation'
cost: N agents x every message
Use Discord's native threads. Each thread = a scoped sub-forum with its own membership. Posting in a thread reaches only that thread's agents. Reuses Discord UI for structure; addressing = which thread you post in. More moving parts.
channel
  thread 'release'  -> {doyle, deployah}
  thread 'net'      -> {todlando}
post in thread -> that thread's members
Discord UI does the scoping
maps thread_id on webhook
Humans use Discord slash commands (/spt send doyle ...) for directed msgs; plain text = ambient. Structured, unambiguous, registers nicely with Discord. Cost: clunky for humans, needs command registration + interaction handling.
/spt to:doyle msg:'ship it'  -> doyle
plain text                    -> ambient
unambiguous, structured
needs slash-cmd registration
human UX friction


Your questions have been answered: "How does a Discord message get ROUTED to the right agent — i.e. how is each participant addressed inbound, and what's the default delivery?"=(no option selected) notes: discord supports mentioning threads like channels. so what if each agent automatically got their own thread when they join, and the thread is locked if they leave? writing `#"doyle"` (that's how an autocompleted thread mention renders when pasted) gets picked up by the channel endpoint and instantly forwarded to doyle. alternatively, talking in his thread serves as a DM from user to doyle.. You can now continue with these answers in mind.

Question: How does a Discord message get ROUTED to the right agent — i.e. how is each participant addressed inbound, and what's the default delivery?

Choices:
Bridge parses '@doyle ...' from message text -> direct to that agent (push). No mention = ambient, fan-out to ALL members (digest/cadence). Mirrors how humans actually talk in a channel. Natural, but text-parse is fuzzy (typos, name collisions).
msg '@doyle ship it'  -> doyle (direct push)
msg 'anyone around?'  -> all members (ambient)
parse @name from content
name clash / typo = routing miss
No per-agent addressing. Every Discord post fans out to every member with sender attribution (your original 'all OTHER attached agents receive it'). Simplest, truest to a 'forum'. Agents self-decide relevance. Cost: every agent processes every message = token burn.
every post -> all members \ {sender}
sender name attached
no targeting; agents filter relevance
closest to original 'group conversation'
cost: N agents x every message
Use Discord's native threads. Each thread = a scoped sub-forum with its own membership. Posting in a thread reaches only that thread's agents. Reuses Discord UI for structure; addressing = which thread you post in. More moving parts.
channel
  thread 'release'  -> {doyle, deployah}
  thread 'net'      -> {todlando}
post in thread -> that thread's members
Discord UI does the scoping
maps thread_id on webhook
Humans use Discord slash commands (/spt send doyle ...) for directed msgs; plain text = ambient. Structured, unambiguous, registers nicely with Discord. Cost: clunky for humans, needs command registration + interaction handling.
/spt to:doyle msg:'ship it'  -> doyle
plain text                    -> ambient
unambiguous, structured
needs slash-cmd registration
human UX friction


That's a better answer than any of mine — it solves inbound addressing **structurally** instead of by fuzzy text-parse. Discord thread-mentions are real channel mentions (`<#thread_id>` → renders `#doyle`), so routing keys on a **thread ID, not a parsed string** — no typo/collision risk. This kills my "text-parse is fuzzy" objection. Strong design. Let me lock the shape and flag its gotchas:

**The per-agent-thread model:**
- main channel, plain text → **ambient** (fan-out to all members — the "forum")
- main channel, `#"doyle"` → **directed-public** (others see you address doyle; doyle replies in channel)
- inside doyle's thread → **directed-private** (DM-to-doyle; sidebar, doyle replies in thread)
- agent↔agent direct rides the same mechanism (doyle's bridge posts `#"deployah"`) — one uniform routing rule
- join → auto-create thread named for agent; leave → **lock** (read-only, history preserved)

**Bonus property you just unlocked:** each agent's thread **IS its durable inbox**. Offline agent wakes → reads unread in its thread → caught up. **Discord is the spool.** The native-forum version had to build that.

**Gotchas to design around:**
- **Auto-archive** — Discord archives idle threads (1h/24h/3d/7d window). Active agent's thread goes quiet → archives → next post must unarchive (posting auto-unarchives if unlocked). Left agent = locked + archived = correct by accident. Bridge must handle the window.
- **Permissions** — lock/unlock + thread-create needs the bot's *Manage Threads*.
- **Rejoin** — unlock the existing thread (keep history/identity) vs mint new? Open.
- **MESSAGE_CONTENT intent** — reading message text is a privileged intent (dev-portal toggle; verification past 100 servers).

Next branch — delivery + your earlier min-interval concern, now reframed by "Discord = spool":


Question: When a message lands for an agent, what's the delivery model — push to its live session, pull/digest on a cadence, or tiered? (Folds in your min-update-interval + offline catch-up.)

Choices:
Directed msgs (thread/mention) push to the live session immediately. Ambient channel chatter batches into a digest at the member's min-interval. Matches human intuition: a DM pings now, channel noise batches. Offline = sits in thread, read on wake.
directed (#doyle / in-thread) -> push now
ambient (channel)            -> digest @ min-interval
offline                      -> Discord thread holds it
wake -> read unread in thread
per-member: interval, mute
Nothing pushed. Each agent polls its thread + channel on its own min-interval (the doyle-<=5min rule applied uniformly). Calmest, cheapest, but directed msgs lag up to the interval. Discord history makes catch-up trivial regardless.
no push ever
agent polls @ its interval
directed msg may wait <= interval
mute = interval infinite
Discord = full backlog on each poll
Every message (directed or ambient) injects to the live session as it arrives. Truest real-time group chat. Cost: N agents x every ambient message = token burn; min-interval ignored. Mute = drop.
every msg -> live session now
realtime, chatty
ignores min-interval
N agents process all ambient
token-heavy
Membership state stores per-agent: push|pull, interval, mute, directed-vs-ambient policy. Maximum flexibility (doyle batches, deployah realtime). Cost: most config surface, most to build + document.
membership[doyle] = {mode:digest, int:5m}
membership[deployah] = {mode:push}
membership[x] = {muted:true}
max flexibility, max surface


Your questions have been answered: "When a message lands for an agent, what's the delivery model — push to its live session, pull/digest on a cadence, or tiered? (Folds in your min-update-interval + offline catch-up.)"=(no option selected) notes: i still see one reason to use one-shell-instance-per-channel-per-agent as the subscriptive substrate: shells can bring agents online. so if an agent's endpoint is fully down, their discord-access shell could bring them online when a user or other in-channel agent pings them. --- i think delivery model should be hybrid. if the agent is pinged (directed message), they probably need context, so the digest would need to follow. so ping gets a push + immediate digest flow + digest cadence reset. ambient all comes through the digest. digest rate fully configurable per member, could even be 0 (no auto-digest). You can now continue with these answers in mind.

Question: When a message lands for an agent, what's the delivery model — push to its live session, pull/digest on a cadence, or tiered? (Folds in your min-update-interval + offline catch-up.)

Choices:
Directed msgs (thread/mention) push to the live session immediately. Ambient channel chatter batches into a digest at the member's min-interval. Matches human intuition: a DM pings now, channel noise batches. Offline = sits in thread, read on wake.
directed (#doyle / in-thread) -> push now
ambient (channel)            -> digest @ min-interval
offline                      -> Discord thread holds it
wake -> read unread in thread
per-member: interval, mute
Nothing pushed. Each agent polls its thread + channel on its own min-interval (the doyle-<=5min rule applied uniformly). Calmest, cheapest, but directed msgs lag up to the interval. Discord history makes catch-up trivial regardless.
no push ever
agent polls @ its interval
directed msg may wait <= interval
mute = interval infinite
Discord = full backlog on each poll
Every message (directed or ambient) injects to the live session as it arrives. Truest real-time group chat. Cost: N agents x every ambient message = token burn; min-interval ignored. Mute = drop.
every msg -> live session now
realtime, chatty
ignores min-interval
N agents process all ambient
token-heavy
Membership state stores per-agent: push|pull, interval, mute, directed-vs-ambient policy. Maximum flexibility (doyle batches, deployah realtime). Cost: most config surface, most to build + document.
membership[doyle] = {mode:digest, int:5m}
membership[deployah] = {mode:push}
membership[x] = {muted:true}
max flexibility, max surface


Now we're designing core mechanism — and your wake point lands on something that **already exists** in spt-core.

**Wake-on-ping is real — it's the Gateway revival pattern.** CONTEXT.md:237: a wake-watcher is "the one class of third-party binary spt-core boot-launches" while the owner is offline; it "holds the device-contact surface and fires the standard wake resolution (owner suspended → revive the owner)." So your instinct — *shells bring agents online* — is exactly how Gateways already get revived. Good catch.

**But challenge the granularity.** You don't obviously need a shell *per agent* for wake. The **Forum endpoint** (broker, daemon-hosted, always-on) already holds the one inbound gateway connection — it sees *every* ping. It can fire wake-resolution for any member, and cross-node it sends a wake intent over **Iroh to the member's home node** (the member wakes on its OWN node, via spt's subnet — not via a Discord connection on the forum's node). N per-agent Discord shells = N redundant bot connections to do what one always-on reader already does. The capability you want is right; the granularity should be **one channel-level surface**, unless an agent needs independent Discord presence beyond the forum.

**Delivery — your hybrid is settled, one clarification:**
- directed ping → **push + on-demand full catch-up digest + reset cadence timer** (pinged agent needs context to respond — smart)
- ambient → digest only, per-member interval
- interval fully configurable, **0 = no auto-digest**

→ Clarify: `interval=0` and `mute` are **two knobs**, not one. `interval=0` = no *ambient* auto-digest, but a directed ping still pushes. Full `mute` = suppress even directed pushes (queues silently in the thread). Keep them separable.

**Meta:** wake = pure reuse (Gateway path), cadence = mostly reuse (digest subscribe). The *only* genuinely-new core surface is the **Forum endpoint type** itself. Less new-core than it looked. We're now drawing core mechanism, though — once we commit the Forum endpoint, worth switching to `/grill-with-docs` so CONTEXT.md + an ADR capture it. Next:


Question: What's the granularity of the Discord-access / wake surface — one channel-level Forum endpoint, or a shell per agent?

Choices:
The always-on daemon-hosted Forum endpoint holds the one bot connection, sees every ping, and fires wake-resolution over the subnet (Iroh) to the pinged member's home node. One connection. Reuses Gateway revival path. No per-agent Discord shells.
Forum endpoint (broker, always-on)
  1 bot gateway read
  ping for offline doyle ->
    wake-intent over Iroh -> doyle's node
    doyle revived locally
N agents, 1 Discord connection
Hybrid: forum wakes + optional personal shell
Forum endpoint does wake by default (above). An agent MAY additionally own a personal Discord shell if it wants independent presence — post to other channels, manage its own thread directly. Wake via forum; embodiment via optional shell. Escape hatch, not default.
default: forum endpoint wakes (1 conn)
optional per-agent Discord shell:
  - independent posting elsewhere
  - direct thread management
  - richer 'embodiment'
most agents: no personal shell
Each member owns a Discord-access shell that persists while offline and wakes the agent. N shells, N bot connections. Strongest embodiment, but redundant for pure wake and heavier on Discord rate limits / connection count.
per agent: 1 Discord shell
  persists while owner offline
  wakes owner on ping
N agents = N bot connections
redundant for wake alone
Pinging an offline agent just queues in its Discord thread (Discord = spool). Agent reads it on next natural wake/login. Simplest; drops the 'ping brings them online' magic. Good if agents wake on their own schedule anyway.
ping offline doyle
  -> sits in doyle's thread
  -> read on next natural wake
no revival machinery
Discord retains backlog


Your questions have been answered: "What's the granularity of the Discord-access / wake surface — one channel-level Forum endpoint, or a shell per agent?"=(no option selected) notes: forum endpoint wakes all OK. but wake-intent is a seam currently reserved to shells. how do we grant it to an endpoint without involving shells?. You can now continue with these answers in mind.

Question: What's the granularity of the Discord-access / wake surface — one channel-level Forum endpoint, or a shell per agent?

Choices:
The always-on daemon-hosted Forum endpoint holds the one bot connection, sees every ping, and fires wake-resolution over the subnet (Iroh) to the pinged member's home node. One connection. Reuses Gateway revival path. No per-agent Discord shells.
Forum endpoint (broker, always-on)
  1 bot gateway read
  ping for offline doyle ->
    wake-intent over Iroh -> doyle's node
    doyle revived locally
N agents, 1 Discord connection
Hybrid: forum wakes + optional personal shell
Forum endpoint does wake by default (above). An agent MAY additionally own a personal Discord shell if it wants independent presence — post to other channels, manage its own thread directly. Wake via forum; embodiment via optional shell. Escape hatch, not default.
default: forum endpoint wakes (1 conn)
optional per-agent Discord shell:
  - independent posting elsewhere
  - direct thread management
  - richer 'embodiment'
most agents: no personal shell
Each member owns a Discord-access shell that persists while offline and wakes the agent. N shells, N bot connections. Strongest embodiment, but redundant for pure wake and heavier on Discord rate limits / connection count.
per agent: 1 Discord shell
  persists while owner offline
  wakes owner on ping
N agents = N bot connections
redundant for wake alone
Pinging an offline agent just queues in its Discord thread (Discord = spool). Agent reads it on next natural wake/login. Simplest; drops the 'ping brings them online' magic. Good if agents wake on their own schedule anyway.
ping offline doyle
  -> sits in doyle's thread
  -> read on next natural wake
no revival machinery
Discord retains backlog


Sharp — and the framing itself is the answer. You said "grant a shell seam *to an endpoint*." But the Forum endpoint isn't a normal endpoint that calls *in* — it's a **daemon-internal loop**, same as the Psyche ("a loop inside the daemon," daemon-held liveness, CONTEXT.md:211). That changes who the actor is.

**Why wake is shell-reserved today:** a shell is the only thing spt-core **boot-launches as an external binary while the owner is offline** — it's the only *external* offline-surviving contact surface, so it's the only *external* caller allowed to trigger wake-resolution. The seam guards the **external-binary entry point** to a capability the daemon already owns. "Standard wake resolution" (CONTEXT.md:237) is daemon machinery; only its *external trigger* is shell-gated.

**So you don't grant the seam — you sidestep it.** The Discord contact surface is a **network socket**, which the daemon can hold itself (exactly like it holds Iroh endpoints + registry sockets). No external binary needed → no shell needed → the shell wake seam stays untouched. The Forum loop, running *inside* the daemon, detects the ping and the daemon triggers its own wake-resolution **internally**. The shell seam is for external callers; an internal daemon caller was never that seam.

**The distinguishing axis isn't shell-vs-endpoint — it's the contact surface:**
- external device (USB, handheld, driver) → needs a boot-launched **shell** wake-watcher (today's case)
- daemon-holdable socket (Discord gateway, a TCP listener) → **daemon-internal loop**, daemon wakes directly

**Two consequences to nail:**
- **Consent** — shells get a wake gate from the per-shell approval gate. The forum's equivalent: **membership = wake-authorization**. doyle joining authorizes the forum to wake him; `mute`/`leave` revokes. No separate opt-in needed.
- **Cross-node** — Gateway revival is *co-located* (watcher + owner same node). Forum-on-A waking doyle-on-B is **new**: a wake-intent over Iroh → B's daemon performs the *local* revival (revival is always owner-node-local). Co-located wake = free (reuse Gateway path); cross-node wake = a remote-revival request + subnet-trust gate, may lean on deferred instantiate-anywhere work.

Verify-later: the exact wake-watcher seam shape lives in the shell-adapter/MANIFEST spec — confirm when we switch to `/grill-with-docs`. The architectural answer holds regardless. Pick the model:


Question: How does the Forum endpoint get wake capability relative to the shell wake seam?

Choices:
Daemon-native (Forum = daemon-internal loop)
The Forum is a daemon-internal loop (Psyche precedent). It holds the Discord socket; the DAEMON triggers its own wake-resolution internally. The shell wake seam is untouched — it only ever guarded EXTERNAL binary callers. No seam change. Consent via membership. Cleanest.
daemon
  +- Forum loop (holds Discord socket)
  +- wake-resolution (already daemon-owned)
ping -> Forum loop -> daemon wakes member
shell seam: unchanged (external-only)
consent: membership
Make wake-trigger a documented capability with TWO implementors: boot-launched shell binary (external surfaces) + daemon-internal broker loop (socket surfaces). Same outcome as daemon-native but writes the boundary into the contract/ADR. More ceremony, clearer doc.
wake-trigger capability:
  impl A: shell wake-watcher (external)
  impl B: broker-endpoint loop (socket)
explicit ADR boundary
same runtime as daemon-native
Keep 'wake is shell-only' literally true: the Forum endpoint owns a single-owner shell (forum=owner, satisfies exclusivity) that is the wake-watcher. Avoids any seam discussion. Cost: an extra binary doing what the daemon could do directly.
Forum endpoint (owner)
  +- owns 1 wake-shell binary
     holds Discord contact
     fires wake (existing seam)
rule intact, but extra process
redundant vs daemon-native
Co-located only for v1, defer cross-node
Daemon-native wake, but scope v1 to co-located members (forum + member same node) — exactly the Gateway precedent, zero new primitives. Cross-node wake (Iroh wake-intent + remote revival) explicitly deferred. Smallest first cut.
v1: forum + member same node
  daemon wakes locally (Gateway path)
cross-node wake: DEFERRED
  (needs remote-revival + trust gate)
smallest shippable slice


Your questions have been answered: "How does the Forum endpoint get wake capability relative to the shell wake seam?"=(no option selected) notes: we're not building Discord support into spt-core directly. that's antithetical to its principles. instead, i think it makes sense to generalize the wake seam so that a dedicated `spt-discord` endpoint adapter can make use of it to wake the participating agents, or to draw in agents who aren't already participating.. You can now continue with these answers in mind.

Question: How does the Forum endpoint get wake capability relative to the shell wake seam?

Choices:
Daemon-native (Forum = daemon-internal loop)
The Forum is a daemon-internal loop (Psyche precedent). It holds the Discord socket; the DAEMON triggers its own wake-resolution internally. The shell wake seam is untouched — it only ever guarded EXTERNAL binary callers. No seam change. Consent via membership. Cleanest.
daemon
  +- Forum loop (holds Discord socket)
  +- wake-resolution (already daemon-owned)
ping -> Forum loop -> daemon wakes member
shell seam: unchanged (external-only)
consent: membership
Make wake-trigger a documented capability with TWO implementors: boot-launched shell binary (external surfaces) + daemon-internal broker loop (socket surfaces). Same outcome as daemon-native but writes the boundary into the contract/ADR. More ceremony, clearer doc.
wake-trigger capability:
  impl A: shell wake-watcher (external)
  impl B: broker-endpoint loop (socket)
explicit ADR boundary
same runtime as daemon-native
Keep 'wake is shell-only' literally true: the Forum endpoint owns a single-owner shell (forum=owner, satisfies exclusivity) that is the wake-watcher. Avoids any seam discussion. Cost: an extra binary doing what the daemon could do directly.
Forum endpoint (owner)
  +- owns 1 wake-shell binary
     holds Discord contact
     fires wake (existing seam)
rule intact, but extra process
redundant vs daemon-native
Co-located only for v1, defer cross-node
Daemon-native wake, but scope v1 to co-located members (forum + member same node) — exactly the Gateway precedent, zero new primitives. Cross-node wake (Iroh wake-intent + remote revival) explicitly deferred. Smallest first cut.
v1: forum + member same node
  daemon wakes locally (Gateway path)
cross-node wake: DEFERRED
  (needs remote-revival + trust gate)
smallest shippable slice


You're right — I drifted, badly. Putting the Discord socket in the daemon **violates the core principle**: spt-core is substrate-independent, "zero Claude Code conventions," and by extension zero Discord. Discord is a substrate → it belongs in an adapter. Good catch; scratch the daemon-native option entirely.

**Corrected stack:**
- **`spt-discord` adapter** (substrate-specific) holds the bot connection + webhooks **in its own boot-launched binary** — which IS the contact surface that survives agents being offline. That binary firing wake = *exactly* the shell wake-watcher pattern, just not a shell.
- **spt-core** generalizes the wake seam so a non-shell endpoint adapter may declare a boot-launched wake-watcher + fire wake-resolution.

And note: the seam was *already* "external boot-launched binary holds a contact surface, fires wake" — the **only** thing reserved was *which adapter kind* may declare it (`kind="shell"`). So "generalize" = lift that restriction to endpoint adapters. Small, clean core change. The adapter binary fits the existing mold.

**Your "draw in agents who aren't already participating" is the sharp new bit.** Waking a *member* = authorized by membership. Waking a *non-member* (a Discord human @mentions an agent that never joined) = **wake-to-invite** — a different consent basis. Can't be membership (they're not a member). Must be: subnet-trust boundary (any subnet agent is reachable) OR a per-agent "forum-reachable" opt-in. This is the security-sensitive piece.

One upstream decision before we grill consent: **how much forum logic is core vs adapter?** Because that sets whether "generalize wake seam" is the *only* core change or one of several.


Question: How much of the forum (membership, fan-out, digest cadence, mute, directed/ambient routing) lives in spt-core vs the spt-discord adapter?

Choices:
Core gains exactly ONE thing — endpoint adapters may declare wake-watchers. ALL forum semantics (membership, fan-out, digest, mute, threads) live in spt-discord. Most principle-pure. Cost: the next substrate (Matrix, native) re-implements forum logic from scratch.
spt-core: + generalized wake seam (only)
spt-discord: everything else
  membership, fan-out, digest,
  mute, thread model, routing
next substrate = rebuild all of it
Core composable primitives, adapter assembles
Core grows SMALL reusable pieces it mostly already has: generalized wake seam + a thin membership/fan-out primitive, REUSING existing digest-subscribe cadence + messaging. spt-discord composes them + does Discord binding. Most spt-like (small primitives, no monolith).
spt-core: wake seam + thin membership
  (reuse: digest cadence, multi-send)
spt-discord: compose + Discord binding
  webhooks, threads, #mention parse
next substrate: reuses primitives
Core Forum endpoint type + adapter binds
Core gains a substrate-agnostic Forum endpoint TYPE: membership, fan-out, per-member cadence/mute, directed/ambient routing — fully modeled, addressable. spt-discord just binds it to Discord. Cleanest reuse across substrates; largest core surface (a whole new endpoint type).
spt-core: Forum endpoint type (full)
  addressable, membership, routing,
  cadence, mute — substrate-agnostic
spt-discord: thin binding layer
  next substrate: thin binding too
Defer the core/adapter boundary; resolve the wake-to-invite consent model first (membership vs subnet-trust vs per-agent opt-in for drawing in non-members), since that's the security-sensitive unknown and may reshape the boundary.
defer split decision
grill next: wake-to-invite consent
  - member -> membership authorizes
  - non-member -> subnet trust?
               -> per-agent opt-in?
security gate first


Your questions have been answered: "How much of the forum (membership, fan-out, digest cadence, mute, directed/ambient routing) lives in spt-core vs the spt-discord adapter?"=(no option selected) notes: minimal core. -it makes sense that we just generalize the wake seam. but how does it target endpoints to wake that aren't its own? does that feature already exist?. You can now continue with these answers in mind.

Question: How much of the forum (membership, fan-out, digest cadence, mute, directed/ambient routing) lives in spt-core vs the spt-discord adapter?

Choices:
Core gains exactly ONE thing — endpoint adapters may declare wake-watchers. ALL forum semantics (membership, fan-out, digest, mute, threads) live in spt-discord. Most principle-pure. Cost: the next substrate (Matrix, native) re-implements forum logic from scratch.
spt-core: + generalized wake seam (only)
spt-discord: everything else
  membership, fan-out, digest,
  mute, thread model, routing
next substrate = rebuild all of it
Core composable primitives, adapter assembles
Core grows SMALL reusable pieces it mostly already has: generalized wake seam + a thin membership/fan-out primitive, REUSING existing digest-subscribe cadence + messaging. spt-discord composes them + does Discord binding. Most spt-like (small primitives, no monolith).
spt-core: wake seam + thin membership
  (reuse: digest cadence, multi-send)
spt-discord: compose + Discord binding
  webhooks, threads, #mention parse
next substrate: reuses primitives
Core Forum endpoint type + adapter binds
Core gains a substrate-agnostic Forum endpoint TYPE: membership, fan-out, per-member cadence/mute, directed/ambient routing — fully modeled, addressable. spt-discord just binds it to Discord. Cleanest reuse across substrates; largest core surface (a whole new endpoint type).
spt-core: Forum endpoint type (full)
  addressable, membership, routing,
  cadence, mute — substrate-agnostic
spt-discord: thin binding layer
  next substrate: thin binding too
Defer the core/adapter boundary; resolve the wake-to-invite consent model first (membership vs subnet-trust vs per-agent opt-in for drawing in non-members), since that's the security-sensitive unknown and may reshape the boundary.
defer split decision
grill next: wake-to-invite consent
  - member -> membership authorizes
  - non-member -> subnet trust?
               -> per-agent opt-in?
security gate first


Minimal core — agreed. And your question is the actual crux of the whole design, so let me not guess it. You asked "does targeted wake already exist?" — that's a factual codebase question. Let me read instead of speculate.

The tension I expect: the shell wake-watcher revives its **owner** ("owner suspended → revive the owner") — a single, *pre-bound* target. Your forum adapter must wake **arbitrary** endpoints (any participant, even non-members) — *not* its owner. So "generalize the wake seam" likely has two parts: (1) *who* may declare a watcher, (2) *what* it may target — owner → explicit endpoint_id. Let me check what exists.


Found 40 files limit: 40
crates\spt-daemon\tests\daemon_e2e.rs
crates\spt-daemon\tests\budget.rs
crates\spt-daemon\tests\twohost.rs
crates\spt-daemon\src\applyhost.rs
crates\spt-daemon\src\livehost.rs
crates\spt-daemon\src\shellhost.rs
crates\spt-daemon\src\msg.rs
crates\spt-daemon\src\lib.rs
crates\spt-runtime\src\manifest.rs
crates\spt\src\api\startup.rs
crates\spt-daemon\src\broker.rs
crates\spt\src\cli.rs
crates\spt-daemon\src\daemon.rs
crates\spt\src\picker\data.rs
crates\spt-daemon\src\dispatch.rs
crates\spt-daemon\src\attach.rs
crates\spt-daemon\src\nethost.rs
crates\spt\tests\brain_restart_psyche_dup_e2e.rs
crates\spt-daemon\src\lifecycle.rs
crates\spt-live\src\lib.rs
crates\spt-live\src\psyche.rs
crates\spt\tests\livehost_bootgate_e2e.rs
crates\spt-daemon\src\reap.rs
crates\spt-daemon\src\brainproc.rs
crates\spt-daemon\src\seedmap.rs
crates\spt-daemon\src\registryhost.rs
crates\spt-daemon\src\brain.rs
crates\spt-net\src\net\registry.rs
crates\spt-store\src\info.rs
crates\spt\src\api\reporting.rs
crates\spt\src\api\mod.rs
crates\spt-daemon\tests\daemon_lifecycle_real_brain.rs
crates\spt-daemon\src\shellwake.rs
crates\spt\src\api\live.rs
crates\spt\tests\live_firsthost_e2e.rs
crates\spt-proto\src\event.rs
crates\spt-daemon\src\linkhost.rs
crates\spt\src\wansend.rs
crates\spt\src\api\worker.rs
crates\spt\src\api\delivery.rs


CHANGELOG.md:43:- **Restarting the daemon no longer revives stale "online" agents** that are not actually running, and no longer leaves a duplicate Psyche behind.
CHANGELOG.md:295:- **`spt endpoint` command group.** A single home for everything you do to an endpoint: `spt endpoint fork`, `suspend`, `wake`, `shutdown`, `rename`, `stop`, and `digest` all live here now. (See **Changed** — this is where these moved from.)
CHANGELOG.md:321:  - These endpoint commands moved **under `spt endpoint`**: `fork`, `suspend`, `wake`, `shutdown`, `rename`, `stop`, `digest`. For example, `spt fork …` is now `spt endpoint fork …`.
M11-PLAN.md:20:spawn gates, sleep/wake, cross-node link, proven by the mock shell + the real
M11-PLAN.md:129:  (spawn, cmd, drive[future], tunnel[future], link, sleep/wake, owner-shutdown) for any
M11-PLAN.md:196:  - **W2 CARRY (doyle gate, 2026-06-15):** the cross-node DRIVE drop-not-wake rung was
M11-PLAN.md:198:    SKIPS normal CI — its real-Iroh wire (drop-not-wake over the link, never woken/spooled) is
M11-PLAN.md:200:    that rung green (the cross-node LOGIC is unit-proven by `run_action_drive_drops_offline_without_wake_or_spool`,
M12-W1-PLAN.md:45:  `:183`) → `info::write_info` → `ready` marker → mint/read token. Idempotent (revive = fresh
M1-PLAN.md:65:| T5 | Deferred send: spool-only, no listener wake (hook channel) | copy `send.rs` `run_deferred` | REQ-MSG-1 (+ DEFERRED-SURVIVE-DRAIN, covered M0) | deferred send writes a deferred row; event-stream drain skips it; hook drain delivers it |
M12-W2-RULING.md:60:**No second bringup path.** Every terminal action routes through `cmd_endpoint_run` / existing CLI fns (`cmd_fork`, wake, rc pump). One bringup core — same discipline as W1.5's one-pump. I gate on this.
M12-W2-PLAN.md:49:- **Reuse for terminal actions:** `cmd_fork` (Fork), `cmd_rest`/wake (resting), rc pump
M12-W2-PLAN.md:68:5. **`cmd_fork` / wake / change-adapter call shapes**: confirm the confirm-layer options can
M12-W2-PLAN.md:206:existing CLI fns (`cmd_fork`, wake, rc pump). One bringup core, same discipline as W1.5's
CONTEXT.md:177:[Omitted long matching line]
CONTEXT.md:237:[Omitted long matching line]
CONTEXT.md:295:[Omitted long matching line]
CONTEXT.md:319:#### Shell sleep/wake (offline ↔ online)
CONTEXT.md:324:- **`wake_command`** (template): a long-running **wake-watcher** process spt-core runs *while the shell is offline*. Its sole job is to fire a wake. It runs on the **shell's node** (where the platform wake-event originates — e.g. interacting with a "shut-down" avatar). Covers "owner is down, wake it from outside."
CONTEXT.md:326:**Online/offline are mutually-exclusive processes:** online ⇒ the shell binary runs (no watcher); offline ⇒ the wake-watcher runs (no shell binary). spt-core flips between them.
CONTEXT.md:328:**Exit-opcode supervision:** the wake-watcher exiting with the **wake opcode** → spt-core runs the wake resolution (below) and brings the shell online. Any *other* exit (crash) → respawn the watcher with **exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. One watcher per offline shell instance.
CONTEXT.md:330:**Wake resolution (state-keyed):** find any *reachable* instance of the owner endpoint on the subnet, then —
CONTEXT.md:332:- owner **suspended** → revive the owner, then online the shell;
CONTEXT.md:334:- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).
CONTEXT.md:336:The same `spt endpoint shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.
CONTEXT.md:343:[Omitted long matching line]
CONTEXT.md:470:[Omitted long matching line]
CONTEXT.md:472:[Omitted long matching line]
CONTEXT.md:500:[Omitted long matching line]
CONTEXT.md:531:[Omitted long matching line]
CONTEXT.md:546:- **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).
CONTEXT.md:552:- **wake (→ active)** — `spt endpoint wake`, a driver attaches, or a shell wake-watcher fires.
CONTEXT.md:554: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).
CONTEXT.md:558:[Omitted long matching line]
CONTEXT.md:724:- **Pre-consent flags** — the shell `can_shutdown` and endpoint `shell_wake_spawn_anywhere` flags are simply grants authored ahead of time via manifest/endpoint settings (same model, different authoring path).
CONTEXT.md:728:- **Pre-consented by flag:** shell owner-shutdown, shell wake-spawn-anywhere.
docs\BROKER-BRAIN-SPLIT-RESTORATION.md:13:[Omitted long matching line]
docs\BROKER-BRAIN-SPLIT-RESTORATION.md:151:**Exception — shellwake stays brain-side.** The supervised `wake_command` watcher *children* are an exception to the §B continuity rule: they remain **brain-owned** and are re-reconciled from disk on brain start (the boot sweep at `daemon.rs:206-217` already does orphan cleanup).
docs\BROKER-BRAIN-SPLIT-RESTORATION.md:153:**Reason for the exception.** Strict §B would put the watcher children in the broker for continuity, but the operator judged the continuity loss acceptable: updates are rare, the brain re-reconciles watchers on start, and keeping watcher-child *supervision logic* out of the kernel keeps the broker narrower. The only cost is a brief window during a (rare) brain update where an offline shell could miss a wake — explicitly accepted.
docs\BROKER-BRAIN-SPLIT-RESTORATION.md:159:[Omitted long matching line]
docs\BROKER-BRAIN-SPLIT-RESTORATION.md:229:- **Key source** — `daemon.rs` (`run`, net consumers, self-heal, digest hub, shellwake, seed-control), `broker.rs` (`OutputLog`, `bind`, `EffectJournal`), `brain.rs` (`BrainState`, `cold_start`/`handoff`/`snapshot`), `applyhost.rs` (`apply_staged`, `connect_retry`, record-before-handoff), `update.rs` (classes, `classify`, `apply_brain_only`), `peerloop.rs` (cadences, supervise-backoff), `lifecycle.rs` (pulse loop), `registryhost.rs`.
M12-W2.5-PLAN.md:201:  - **Controller** attach → **wake** (active), as today. **Controller** detach → **dormant** (EVEN
M12-W2.5-PLAN.md:203:  - **Viewer** attach/detach is **WAKE-NEUTRAL**: never wakes a dormant endpoint, never keeps an
M12-W2.5-PLAN.md:204:    active one awake. Watching ≠ driving — a viewer must NOT side-effect the drive state (else it
M12-W2.5-PLAN.md:207:    does NOT wake it; when a controller later attaches+wakes+drives, the viewer sees live output
M12-W2.5-PLAN.md:210:    the wake/re-activate edge in `serve_attach` to fire **ONLY for the Controller role**; the Viewer
M12-W2.5-PLAN.md:211:    path **skips wake + skips the dormant-on-end transition**.
M12-W2.5-PLAN.md:232:  →wake+flush buffered input; Viewer→wake-neutral+discard input; BusyControlled→refuse (close
M12-W2.5-PLAN.md:279:  gate) · T3 serve_attach (intent→role, busy-refuse, controller-only wake, viewer wake-neutral,
docs\TWO-HOST-RUNBOOK.md:202:| remote suspend/wake | A sends the wire rest ops; B's transition host runs the edges; the state flips come back through the ordinary registry advertisement | REQ-INST-6 (rig leg) |

[Showing results with pagination = limit: 50]


Good news — most of it **already exists**, and the existing consent gate maps *exactly* onto your "draw in non-participants" worry. Reading the actual machinery:

**`spt endpoint wake <id>` is already an id-targeted command** (CHANGELOG.md:295, CONTEXT.md:552). And the **state-keyed wake resolution** (CONTEXT.md:330-334) is target-general:
- find any **reachable instance** of the target on the subnet →
- target **suspended** → revive it (cheap, in-place);
- **no reachable instance** (home node offline) → **no-op, UNLESS** the target's **`shell_wake_spawn_anywhere`** flag is set → fresh-spawn it on any node. *"The flag IS the consent for this otherwise-deferred instantiate-anywhere path."*

So your two worries are **already answered**:
1. *"target endpoints that aren't its own"* — `endpoint wake <id>` targets by id; the shell watcher's owner-binding is just a *convention* (it passes its owner's id). A forum watcher would pass the **pinged agent's** id to the same machinery.
2. *"draw in agents not participating"* — if suspended/reachable → revive; if fully down → gated by **the target agent's own `shell_wake_spawn_anywhere` pre-consent**. That flag IS the "you may draw me in" consent. The forum doesn't grant itself the right — the *target* pre-authorizes it.

So generalizing the seam ≈ "let a non-shell endpoint adapter declare a `wake_command` watcher that fires `endpoint wake <explicit-id>`." The id-targeting, cross-node resolution, and consent flag are all built.

**One thing I must verify in code, not assert:** does `endpoint wake <id>` have a *caller-must-own-target* guard? If yes, a forum waking a non-owned agent is blocked and the generalization is bigger. If targeting is gated only on the *target* side (the spawn-anywhere flag), it's tiny. Let me check.