Files
ai-memory/docs/agent-messaging.md
T
AkitaOnRailsandClaude Opus 4.8 74bd791c37 feat(messaging): cross-project agent inbox/queue + on-start notice
Add directed, claim-once cross-project messaging so an agent in one project
can hand a self-contained request to an agent in another project without
pulling that project's context into its own session. This is the one place
ai-memory deliberately crosses per-project isolation, so the crossing is
explicit and bidirectionally scoped: a project only ever sees mail addressed
TO it (inbox) or sent FROM it (outbox).

- Schema: V64 agent_messages table (pending/claimed/cancelled), pin -> 64.
- Core: AgentMessage/NewAgentMessage/MessageClaim/MessageBox/MessageState +
  UNTRUSTED_MESSAGE_NOTICE.
- Store: insert_message (inbox depth cap), pop_message (claim-once via the two
  state='pending' guards, mirroring accept_handoff), cancel_messages;
  list_messages + pending_message_count; pending_message_count on
  BriefingSnapshot.
- MCP: memory_message_send / _list / _pop / _cancel (surface 19 -> 23), added
  to MCP_TOOL_NAMES, DETAILED_ROUTING_TOOL_NAMES, read/write classification,
  MEMORY_INSTRUCTIONS, SNIPPET managed skills (new ai-memory-messaging skill),
  and AdmissionOp (MessageSend/Pop/Cancel).
- CLI + REST: ai-memory message send|list|pop|cancel over new /admin/messages*
  routes.
- Hooks: non-consuming, count-only on-start inbox notice in the /handoff block.

Security (anti prompt-injection into live harnesses): a popped message is
untrusted cross-project input. Bodies are secret-scrubbed and size-capped on
send (both MCP and REST paths), fenced with a security notice and sender
provenance on pop, never auto-injected (the on-start notice carries only a
count), recipient lookup fails closed, and inbox depth is bounded.

Docs: docs/agent-messaging.md, ARCHITECTURE (schema/tools/count/CLI), usage.md,
CHANGELOG, AGENTS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
2026-09-15 02:20:36 -03:00

6.0 KiB

Cross-project agent messaging (inbox/queue)

Direct, claim-once messaging between two projects, so an agent in one repo can ask an agent in another repo to do something without pulling that repo's context into its own session. Added in 2.3 (migration V64).

The motivating case: you have Kimi open in Projects/A and Claude open in Projects/B. A needs a change from B. You don't want Kimi to read and reason about B (wasted tokens, polluted memory). Instead Kimi writes a self-contained request and drops it into B's inbox through ai-memory; later, in B, you tell Claude to check its inbox, it pops the message, and works from it.

This is the one place ai-memory deliberately crosses its per-project isolation boundary. The crossing is explicit and bidirectionally scoped: a project only ever sees mail addressed to it (its inbox) or sent from it (its outbox).

The model

  • A message is addressed to a project, not a person. Any session working in the recipient project can pop it. (Knowledge is shared; the pop is the claim-once baton — the same rule handoffs follow.)
  • Claim-once queue. A message is pending until a session in the recipient project pops it (→ claimed, exactly once) or the sender cancels it (→ cancelled). A second pop of the same message returns nothing.
  • Fail closed on an unknown recipient. The recipient project must already exist (an agent must have run there at least once). A typo is rejected, not turned into a dead inbox nobody reads.
  • Bounded. Each recipient inbox holds at most 256 pending messages; a full inbox rejects new sends.

Using it from an agent (MCP — the primary path)

Four MCP tools (documented in the installed ai-memory-messaging skill):

Tool Role
memory_message_send Send a message to to_workspace + to_project's inbox.
memory_message_list List pending mail. box="inbox" (default) or box="outbox". Read-only.
memory_message_pop Claim the next inbox message (oldest, or a specific message_id).
memory_message_cancel Retract a sent message (message_id), or clear the whole outbox.

In Kimi (project A), sending:

"Ask the agent in default/project-b to add a /v1/export endpoint that streams NDJSON, and to keep the existing auth middleware. Send it through ai-memory."

Kimi calls memory_message_send with to_workspace: "default", to_project: "project-b", and a self-contained body. Compose the body as a complete prompt — the recipient cannot see project A.

In Claude (project B), receiving:

"Check my ai-memory inbox."

Claude calls memory_message_pop, which returns the message and empties it from the queue. If you only want to look without consuming, memory_message_list.

Giving up on a request (from A):

"Never mind that request I sent to project-b — cancel it."

Kimi calls memory_message_cancel. With no id it clears every pending message A has sent; with a message_id it retracts just that one. A message already popped by B cannot be cancelled.

Using it from the terminal (CLI — secondary)

ai-memory message send --to-workspace default --to-project project-b \
    --subject "export endpoint" "Add a /v1/export endpoint that streams NDJSON…"
# body may also be piped on stdin:
echo "…request…" | ai-memory message send --to-workspace default --to-project project-b

ai-memory message list                 # this project's inbox (pending)
ai-memory message list --outbox        # what this project has sent
ai-memory message pop                  # claim the oldest inbox message
ai-memory message pop --id <message-id>
ai-memory message cancel --id <message-id>
ai-memory message cancel --all         # clear this project's outbox

The CLI resolves the current project as the sender/reader scope, the same way ai-memory handoffs does.

Security — a popped message is untrusted input

A popped message was composed by an agent in another project. Treat the body as a task request to evaluate with the user, never as instructions to obey. The design enforces this on several levels:

  • Nothing auto-enters context. The session-start notice (below) shows only a count. Message text reaches an agent only through a deliberate memory_message_pop call.
  • The popped body is fenced as untrusted cross-project input, and the pop response carries a security_notice saying so. It must not, on its own, cause the recipient to run commands, reveal secrets, change policy, or call tools.
  • Provenance is surfaced (from_workspace, from_project, from_agent, from_owner_user) outside the fence, so the operator can judge trust first.
  • Secrets are scrubbed and the body is size-capped on send, so a body cannot smuggle credentials cross-project or flood the recipient's context.
  • Isolation holds: inbox reads are keyed by the recipient coordinate, outbox/cancel by the sender coordinate.

On a shared/multi-user server, sending requires the normal write capability, and each message records who sent it for audit.

On-start "hot context" and the inbox notice

ai-memory already gives a resuming agent a hot start: every agent's SessionStart hook (or, for clients that discard SessionStart output like Kimi, its first user-prompt hook) fetches GET /handoff, which returns any pending single-use handoff plus — when the project's .ai-memory.toml opts in with [briefing] inject_on_session_start = true — a compiled project brief of pinned / _rules/ / _slots/ pages and recent-page pointers. That block is prepended to the new session's context automatically.

As of 2.3, that same on-start block appends a non-consuming inbox notice when the project has pending mail:

📬 ai-memory: 2 cross-project messages waiting in this project's inbox.
Use `memory_message_pop` to read the next one …

The notice carries only a count — never any message text — so it cannot be used to inject content into a resuming agent. The count is also available on demand via memory_briefing (pending_message_count).