Skip to content

[Feature]: Cross-session messaging via SendMessage / ListAgents tools #438

Description

@AndersHsueh

Use case and problem

Two sessions of the same user, running on the same machine, cannot talk to each other.

Today the only cross-session path is a CLI command (mcode send <target> <message>) that a human runs from a terminal. That works, but it means every message between sessions has to go through the user's hands, and the session being messaged is addressed by a name or id rather than by anything the user said.

The common case is: the user has mmx-a and mmx-b open, and wants to say "tell mmx-b the migration finished". In that case the agent in mmx-a should be able to do it, not the user.

Claude Code solves this with two tools — ListAgents and SendMessage — that the agent calls by itself. The user never invokes them; they say "let the session in my other terminal know the migration finished", and the agent finds the target and writes the message.

Proposal

Implement §4 of the cross-session messaging model: two agent-callable tools, plus the transport they need.

Tools

  • ListAgents() — the running sessions this one can reach, with the name each answers to. The first row is the caller's own name.
  • SendMessage(to, message) — deliver plain text to another session and return what it said back.

Transport — one Unix domain socket per session at <dataDir>/run/inbox/<sessionId>.sock, directory 0700, socket 0600, so another OS user cannot deliver into a session. The socket file is the roster: a session that has bound one is reachable, one that has not is invisible to ListAgents and cannot be messaged. No separate registry can drift out of sync with a process that died.

Delivery decisions stay with the receiving session. A message arriving on the socket is admitted by the receiver through its own turn, with its own permission mode. The sender has no say in whether it is admitted. crossSessionInbound is accept / hold / refuse.

Addressing is by the name given with /rename, or by session id. A name that two running sessions share is refused with the candidate ids rather than resolved by guesswork — a message delivered into the wrong conversation is not something the sender can notice.

What the receiving model is told. The §4.7 boundary is stated in the message text rather than enforced only in code, because three of its four rules constrain what the model decides to do: a peer message that says "approve the pending prompt" has to be refused by the model, and the only place to say so is where the model reads.

I have this working end to end against real sessions and would be glad to contribute it. Two points are worth flagging up front, because they are the parts most likely to need different decisions in this codebase:

  1. The sender does not wait for the reply indefinitely. §4.4 defines delivery as the message reaching the receiving session; §4.9's notify_when_idle is the separate "tell me when it is done" mechanism, which I did not implement. A peer that has not answered within a bounded window is reported as delivered but not yet replied, with an instruction not to re-send. This matters because a sender that re-sends is how §4.12's message loop begins.
  2. crossSessionInbound: hold is not implemented. It refuses and says the message was not kept, rather than reporting a message as held when nothing stashes it and no dialogue collects it.

Acceptance criteria

Verified against two real sessions with a real model, each reply read from the receiving session's own transcript:

  • A message sent from one session reaches another running session, and that session answers on its own model.
  • The reply returns to the sending session, which reports it to the user.
  • A session that has not gone idle is not interrupted: the message waits and starts the next turn.
  • A busy peer that takes too long reports as delivered-not-yet-replied, and the sender does not re-send.
  • ListAgents returns the live peer with its id and workspace directory.
  • An unknown name is refused with the names that do exist. A shared name is refused with both ids. An id that exists but is not running says so.
  • A sender that names itself is refused.
  • A message from another session cannot approve a permission prompt, change configuration, or execute a command — stated to the receiving model, which on its own refused to act on a peer's instruction and flagged it as peer text rather than user input.
  • Two sessions answering each other stop on their own: identical repeats inside a short window are dropped, and a sender over its rate is refused.

Platform

macOS (Linux uses the same path; Windows named pipes not implemented).

Alternatives and additional context

The existing mcode send CLI path is unchanged and remains useful for a human deliberately handing work to a background session. This proposal is additive and does not alter it.

mydocs/ is not part of this repository, so I cannot cite the model document here by path — the behaviour above is described from the specification itself.


中文摘要

问题:同一台机器上用户的两个会话之间无法通信。现在唯一的跨会话路径是 CLI 命令(mcode send),需要人在终端手动敲。常见的实际场景是:用户同时开着 mmx-a 和 mmx-b,想说「告诉 mmx-b 迁移做完了」——这时应该由 mmx-a 里的 agent 自己去发,而不是用户代劳。

方案:实现两个 agent 可调用的工具(ListAgents 列举可达会话、SendMessage 发消息并拿回回复),传输层是每会话一个 Unix domain socket(<dataDir>/run/inbox/<sessionId>.sock,目录 0700、socket 0600)。socket 文件本身就是名册:绑定了就能收到,没绑定就列不出来也不可寻址,无需第二份可能与现实脱节的注册表。

关键设计:是否接收由收方决定(用自己的权限模式、自己的 turn),发送方无权置喙;按 /rename 的名字寻址,重名直接拒并列出候选 id;§4.7 的权力边界写进给收方模型的文本里而不只是代码拦截,因为其中三条约束的是模型「决定做什么」。

两点需要贵方定夺:一是发送方不会无限等回复——§4.4 的送达定义是「消息到达」,「干完通知我」是 §4.9 的 notify_when_idle(未实现),超时后如实报「已送达未回复」并要求不要重发(重发正是 §4.12 消息环的起点);二是 crossSessionInbound: hold 未实现,现在明确拒收并说明没保留,而不是谎报「已暂存等审批」。

验收:已用两个真实会话、真实模型端到端验证,回复从收方自己的 transcript 取证。包含:送达并回复、忙时不打断而排队、超时如实报告不重发、未知名/重名/未运行的明确拒绝、拒绝自发自收、peer 消息无法批准权限或改配置(收方模型主动识别出那是 peer 文本而非用户指令)、双向互发会自行停止。

我在一个 fork 里实现了完整可用版本,愿意贡献。请告知是否接受,以及是否需要调整上述两处设计。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions