FN-8424: route CLI chat replies through inbox mail

Route agent replies to the correct CLI or dashboard mailbox with bounded polling deadlines.

- add reply-parent routing validation and CLI/dashboard inbox selection
- preserve named mailbox conversations while handling per-message reply deadlines
- document chat and inbox interfaces and cover deadline and routing regressions

Files changed:
 .changeset/fn-8424-cli-chat-reply-routing.md       |   7 +
 docs/agents.md                                     |  20 +-
 docs/cli-reference.md                              |  29 +-
 packages/cli/src/bin.ts                            |  15 +-
 packages/cli/src/commands/__tests__/chat.test.ts   | 262 +++++++++---------
 .../cli/src/commands/__tests__/message.test.ts     |  12 +
 packages/cli/src/commands/chat.ts                  | 293 ++++++++++++---------
 packages/cli/src/commands/message.ts               |  18 +-
 ...tools-send-message-recipient-validation.test.ts |  86 +++++-
 packages/engine/src/agent-heartbeat-prompts.ts     |   8 +-
 packages/engine/src/agent-tools.ts                 |  71 +++--
 11 files changed, 523 insertions(+), 298 deletions(-)

Fusion-Task-Id: FN-8424

Fusion-Task-Lineage: 28d0ef88-717e-4f39-8880-64d2fef94706

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-20 11:26:08 -07:00
parent 00891c225f
commit 3962222863
11 changed files with 527 additions and 302 deletions

View File

@@ -26,16 +26,17 @@ Use `fn chat` to message an agent from your terminal.
### Synopsis
```bash
fn chat <agent-id> [message…] [--once] [--non-interactive] [--poll-ms <n>] [--conversation-id <id>]
fn chat <agent-id> [message…] [--once] [--non-interactive] [--poll-ms <n>] [--reply-timeout-ms <n>] [--conversation-id <id>]
```
### Behavior
- `fn chat <agent-id>` opens an interactive **mailbox conversation** REPL. It delivers to the target agent's MessageStore inbox (`fn_read_messages`), not to a dashboard ChatStore/ChatView session or multi-agent room.
- `fn chat <agent-id>` opens an interactive mailbox-conversation REPL.
- Each message is stored as a `user-to-agent` MessageStore message from `cli` with `metadata.wakeRecipient=true`, `metadata.kind="cli-chat"`, and a durable `metadata.conversationId`.
- The default conversation id is `cli-chat:cli:<agent-id>` and is stable for that CLI-user/agent pair in the selected project. Use `--conversation-id <id>` to name or share a different mailbox thread; the CLI banner prints the active id.
- `fn message send` remains a distinct one-shot mailbox command and does not add CLI-chat conversation metadata.
- Agent replies are polled from your inbox and printed only when they carry that `conversationId` or use `replyTo.messageId` to reference a message already in the thread; reply streaming and dashboard operator-inbox readability are separately tracked by FN-8424 / issue #2363.
- CLI chat is MessageStore mail plus polling, not a token-streaming SSE session. Replies are printed only when they carry the active conversation ID or reply to a known thread message.
- On a direct-message reply, agents must pass `reply_to_message_id` and either set `to_id` to the exact `[from: type:id]` value reported by `fn_read_messages` (including `cli`) or omit it to use the safe parent-sender default. Parent-derived routing is allowed only when the parent was addressed to the replying agent; an explicit `to_id` remains available for intentional forwarding.
- The default conversation ID is `cli-chat:cli:<agent-id>`; use `--conversation-id <id>` to name or share a different mailbox thread.
- One-shot replies have a deadline independent of `--poll-ms`; polling sleeps are capped at the remaining deadline. The interactive REPL maintains one pending deadline per outbound message, reports and clears an unanswered request, then continues to receive later replies.
- Dashboard-created agent chat sessions request the target agent's declared `metadata.skills` plus enabled plugin-contributed skills, so skills such as `ce-debug` are available in chat when the contributing plugin is enabled for the requesting project. Model-only QuickChat sessions request enabled plugin skills, and room responder sessions request the responder agent's skills.
- Agent-acting session lanes share the same skill-injection contract as executor sessions: executor, merger, triage, reviewer, heartbeat, step-session, dashboard chat/room responders, CLI agent execution, planning, mission interview, milestone/slice interview, agent-onboarding interview, workflow design, memory dreams/insight extraction, and scheduled cron automation all request agent/fallback skills plus enabled plugin-contributed skills when a plugin runner is available. Utility-only lanes that only summarize/extract/generate JSON (title/PR summaries, memory compaction, subtask breakdown, text refinement, agent generation, PR metadata generation, evaluator/research synthesis, and similar one-shot helpers) intentionally stay exempt to avoid loading skills where no agent-style tool loop can use them.
- In dashboard model-loop chat (main chat, QuickChat, and room responders), typing `/skill:{name}` requests that skill for the current AI session and strips the slash token from the prompt sent to the model. Slash and catalog-style names such as `/skill:review/pr`, `/skill:review/pr/SKILL.md`, and `source::skills/review/pr/SKILL.md` resolve to the matching discovered bare skill token across chat and agent session lanes. The requested skill is still subject to the normal enabled/disabled execution-skill filters; CLI-agent-backed PTY chat keeps raw terminal input semantics and does not interpret this command.
@@ -63,8 +64,9 @@ For the user-facing gallery and notification UX, see [Artifacts View](./dashboar
- `--once` send one message and exit after first reply (or timeout)
- `--non-interactive` read full stdin to EOF as message body
- `--poll-ms <n>` override poll interval in milliseconds (default `1000`, or `FUSION_CHAT_POLL_MS`)
- `--conversation-id <id>` override the default named mailbox conversation
- `--poll-ms <n>` override poll interval in milliseconds (default `1000`, or `FUSION_CHAT_POLL_MS`); sleeps never extend past the nearest reply deadline
- `--reply-timeout-ms <n>` set the per-reply deadline in milliseconds (default `60000`, or `FUSION_CHAT_REPLY_TIMEOUT_MS`)
- `--conversation-id <id>` override the default named mailbox conversation ID
### Examples
@@ -1052,8 +1054,8 @@ Mailbox replies use `message.metadata.replyTo.messageId` as the stable reply lin
- `fn_read_messages` includes each message ID in its human-readable output so agents can target a specific message.
- When a message has `metadata.replyTo.messageId`, `fn_read_messages` now includes one-level reply-parent context inline (and in structured tool details) so heartbeat/mailbox runs can understand what the message is replying to without expanding full threads.
- `fn_send_message` supports `reply_to_message_id`; when provided, the sent message is stored with `metadata.replyTo.messageId`.
- Heartbeat prompts explicitly instruct agents to include `reply_to_message_id` when replying.
- `fn_send_message` supports `reply_to_message_id`; when provided, the sent message is stored with `metadata.replyTo.messageId`. If that parent was addressed to the sending agent, the tool safely defaults its recipient to the parent's sender; foreign, missing, or misaddressed parents cannot supply a recipient.
- Heartbeat prompts explicitly instruct agents to include `reply_to_message_id` and the exact sender ID when replying.
The dashboard mailbox UI also uses the same metadata contract when users click **Reply**, so user and agent replies share one threading model.