Skip to content

Message mailbox: agent notification / auto-dispatch layer (trigger for store-and-forward inbox) #239

Description

@atomikpanda

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) 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-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

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions