You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The message mailbox (spec message-mailbox, implemented) is deliberately store-and-forward / polling-only: a phone message lands in .mothership/messages/, but an agent only sees it if the operator (or the agent) runs mship inbox. The spec explicitly defers the trigger layer:
non-goal: "Auto-dispatch: spawning/notifying an agent on a new message (delivery model vendor-superpowers-skills #2) — a later layer"
non-goal: "Notifications / push when a reply lands — a later slice"
risk: "an agent only answers if it (or the operator) runs mship inbox … the later auto-dispatch layer removes the manual step."
This issue tracks that deferred layer: let agents like Claude learn a message is waiting without a human manually polling.
Core constraint
Agents are turn-based, not daemons — there's no socket to push to. So "notice a new message" decomposes by what the agent is doing when the message lands:
Agent state
Mechanism
Live & working in a session
A Stop hook runs mship inbox --json, blocks the turn from ending if anything awaits, and injects the pending text → the session becomes self-draining
Alive but idle
A mship inbox wait long-poll run as a background task; when it returns the harness re-invokes the agent (event wakeup, no busy-poll)
Nothing running
serve-side auto-dispatch: mship serve already receives POST /threads — on a new human message it spawns claude -p / mship dispatch, or a cron checks mship inbox
Proposed phasing
Phase 1 — Stop hook (smallest, biggest win, no new infra). A Stop hook that drains mship inbox at each turn boundary. Removes the "operator must run mship inbox" step for the common case (you're actively working with Claude). Own quick spec; testable.
Phase 2 — notifications + auto-dispatch. Needs one new primitive: a long-pollmship inbox wait [--since <cursor>] [--timeout N] (+ GET /threads?wait=1&since=… on serve, ideally inotify-backed on .mothership/messages/). That single primitive powers both directions — phone reply-latency and idle-agent wakeup. Then layer serve-side auto-spawn for the no-session case.
Decisions that shape it
Routing by task_slug. Threads carry an optional task_slug. A steering message on a bound thread should wake/steer that task's agent; an unbound capture message should spawn a new drafting agent.
Idempotency / thread ownership. Auto-wake/auto-spawn must not let two agents grab the same thread or re-fire on an already-handled message. Today "awaiting" is derived purely from latest-message-role — answers "anything pending?" but not "anything new since I last looked?" → Phase 2 needs a read cursor or a lightweight per-thread lease.
Acceptance (high level)
Phase 1: a Stop hook makes a live agent answer an inbox message at its next turn boundary with no manual mship inbox.
Phase 2: mship inbox wait blocks until a new awaiting message (or timeout), keyed off a cursor so it doesn't re-fire; serve exposes the matching long-poll; auto-dispatch spawns/steers an agent keyed on task_slug with thread-ownership idempotency.
Problem
The message mailbox (spec
message-mailbox, implemented) is deliberately store-and-forward / polling-only: a phone message lands in.mothership/messages/, but an agent only sees it if the operator (or the agent) runsmship inbox. The spec explicitly defers the trigger layer:mship inbox… the later auto-dispatch layer removes the manual step."This issue tracks that deferred layer: let agents like Claude learn a message is waiting without a human manually polling.
Core constraint
Agents are turn-based, not daemons — there's no socket to push to. So "notice a new message" decomposes by what the agent is doing when the message lands:
mship inbox --json, blocks the turn from ending if anything awaits, and injects the pending text → the session becomes self-drainingmship inbox waitlong-poll run as a background task; when it returns the harness re-invokes the agent (event wakeup, no busy-poll)mship servealready receivesPOST /threads— on a new human message it spawnsclaude -p/mship dispatch, or a cron checksmship inboxProposed phasing
Phase 1 — Stop hook (smallest, biggest win, no new infra). A
Stophook that drainsmship inboxat each turn boundary. Removes the "operator must runmship inbox" step for the common case (you're actively working with Claude). Own quick spec; testable.Phase 2 — notifications + auto-dispatch. Needs one new primitive: a long-poll
mship inbox wait [--since <cursor>] [--timeout N](+GET /threads?wait=1&since=…on serve, ideally inotify-backed on.mothership/messages/). That single primitive powers both directions — phone reply-latency and idle-agent wakeup. Then layer serve-side auto-spawn for the no-session case.Decisions that shape it
task_slug. Threads carry an optionaltask_slug. A steering message on a bound thread should wake/steer that task's agent; an unbound capture message should spawn a new drafting agent.Acceptance (high level)
mship inbox.mship inbox waitblocks until a new awaiting message (or timeout), keyed off a cursor so it doesn't re-fire; serve exposes the matching long-poll; auto-dispatch spawns/steers an agent keyed ontask_slugwith thread-ownership idempotency.