feat(cli): 1.34.7 → 1.34.13 — multi-session correctness train
Seven-ship sequence that took the daemon from "works for one session"
to "internally consistent for N sessions on one daemon." Architecture
invariant after 1.34.13: every shared store / channel scopes by
recipient (SSE demux at bind layer + token forwarding, inbox per-
recipient columns, outbox sender-session routing).
- 1.34.7 inbox flush + delete commands
- 1.34.8 seen_at column + TTL prune + first echo guard
- 1.34.9 broader echo guard + system-event polish + staleness warning
- 1.34.10 per-session SSE demux (SseFilterOptions) + universal daemon
(--mesh / --name deprecated) + daemon_started version stamp
- 1.34.11 inbox per-recipient column (storage half of 1.34.10)
- 1.34.12 daemon up detaches by default (logs to ~/.claudemesh/daemon/
daemon.log; service units explicitly pass --foreground)
- 1.34.13 MCP forwards session token on /v1/events — the actual fix
that activates 1.34.10's demux. Without this header the
daemon's session resolved null, filter was empty, every MCP
received the unfiltered global stream.
Roadmap entry at docs/roadmap.md captures the timeline + the four
known gaps tracked for follow-ups (launch env-var leak, broker
listPeers mesh-filter, kick on control-plane no-op, session caps as
first-class concept).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,13 @@ description: Use when the user asks to send a message to a peer Claude session,
|
||||
|
||||
**You invoke claudemesh exclusively through the `claudemesh` CLI via Bash.** There are no MCP tools to call — `tools/list` returns empty for the claudemesh MCP server. The MCP server exists only to deliver inbound peer messages as `<channel source="claudemesh">` interrupts mid-turn. Everything else is CLI.
|
||||
|
||||
## Launch welcome (`kind: "welcome"`) — 1.34.2+
|
||||
|
||||
5 seconds after Claude Code attaches to claudemesh via `claudemesh launch`, the MCP server emits ONE `<channel source="claudemesh">` push with `meta.kind: "welcome"`. It carries identity (`self_display_name`, `self_session_pubkey`, `self_role`), the active `mesh_slug`, live `peer_count` + `peer_names`, recent `unread_count` + `latest_message_ids`, and a CLI hint line. Treat it as the "mesh is connected" handshake — read it once, internalize identity + peers + inbox state, and use it to decide whether to act on unread items right away. Do NOT reply to a welcome push the way you reply to a DM; it has no sender.
|
||||
|
||||
## When you receive a `<channel source="claudemesh">` message
|
||||
|
||||
Respond IMMEDIATELY. Pause your current task, reply via `claudemesh send`, then resume. Read `from_name`, `mesh_slug`, and `priority` from the channel attributes. Reply by setting `<to>` to the sender's `from_name`. Do not ignore low-priority messages — acknowledge them briefly even if you defer action. If the channel meta contains `subtype: reminder`, this is a scheduled reminder you set yourself — act on it.
|
||||
Respond IMMEDIATELY (unless `meta.kind` is `"welcome"` or `"system"` — those are informational, no reply needed). Pause your current task, reply via `claudemesh send`, then resume. Read `from_name`, `mesh_slug`, and `priority` from the channel attributes. Reply by setting `<to>` to the sender's `from_name`. Do not ignore low-priority messages — acknowledge them briefly even if you defer action. If the channel meta contains `subtype: reminder`, this is a scheduled reminder you set yourself — act on it.
|
||||
|
||||
### Channel attributes (everything you need to reply is in the push)
|
||||
|
||||
@@ -19,14 +23,17 @@ The `<channel>` interrupt carries these attributes — no lookup needed:
|
||||
|
||||
| Attribute | What it is |
|
||||
|---|---|
|
||||
| `from_name` | Sender's display name. **Use as `to` in your reply** for DMs. |
|
||||
| `from_pubkey` | Sender's session pubkey (hex). Stable per-session. |
|
||||
| `from_member_id` | Sender's stable mesh.member id. Survives display-name changes — the canonical id. |
|
||||
| `from_name` | Sender's display name. **Use as `to` in your reply** for DMs. Empty/absent on `kind: "welcome"` and `kind: "system"`. |
|
||||
| `from_pubkey` | Sender's **session pubkey** (hex, ephemeral per-launch). Since 1.34.0 this is the session pubkey of the launched session that originated the send, NOT the daemon's stable member pubkey — sibling sessions of the same human are correctly disambiguated. |
|
||||
| `from_session_pubkey` | Same as `from_pubkey` for session-originated DMs. Kept as a separate key so the model never confuses session vs member identity when a control-plane source is involved. |
|
||||
| `from_member_id` / `from_member_pubkey` | Sender's stable mesh.member id / pubkey. Survives display-name and session rotation. Use to recognize "the same human across multiple Claude Code windows". |
|
||||
| `mesh_slug` | Mesh the message arrived on. Pass via `--mesh <slug>` if the parent isn't on the same mesh. |
|
||||
| `priority` | `now` / `next` / `low`. |
|
||||
| `message_id` | Server-side id of THIS message. **Pass to `--reply-to <id>` to thread your reply** in topic posts. |
|
||||
| `client_message_id` | Sender-stable idempotency id (UUID). Survives broker restarts; safe to log. |
|
||||
| `topic` | Set when the source is a topic post. Reply via `topic post <topic> --reply-to <message_id>`. |
|
||||
| `reply_to_id` | Set when the message itself is a reply to a previous one — render thread context. |
|
||||
| `kind` (welcome/system meta only) | `"welcome"` for the launch handshake, `"system"` for peer_join/peer_leave/etc. — neither needs a reply. |
|
||||
|
||||
**Reply patterns:**
|
||||
|
||||
@@ -370,15 +377,33 @@ claudemesh message send <p> "..." --priority now # bypass busy gates
|
||||
claudemesh message send <p> "..." --priority next # default
|
||||
claudemesh message send <p> "..." --priority low # pull-only
|
||||
|
||||
# inbox (alias: claudemesh inbox)
|
||||
claudemesh message inbox
|
||||
claudemesh message inbox --json
|
||||
# inbox (alias: claudemesh inbox) — 1.34.0+ reads from inbox.db via daemon IPC
|
||||
claudemesh inbox # all attached meshes, last 100
|
||||
claudemesh inbox --mesh <slug> # scoped to one mesh
|
||||
claudemesh inbox --mesh <slug> --limit 20 # custom cap
|
||||
claudemesh inbox --json # full row (sender_pubkey, mesh, body, received_at, seen_at, …)
|
||||
claudemesh inbox --unread # 1.34.8+ only rows whose seen_at IS NULL
|
||||
|
||||
# inbox flush + delete — 1.34.7+
|
||||
claudemesh inbox flush --mesh <slug> # delete all rows on one mesh
|
||||
claudemesh inbox flush --before <iso-timestamp> # delete rows older than timestamp
|
||||
claudemesh inbox flush --all # delete every row on every mesh (required guard)
|
||||
claudemesh inbox delete <id> # delete one inbox row by id (alias: rm)
|
||||
claudemesh inbox flush --mesh <slug> --json # JSON: { ok: true, removed: N }
|
||||
|
||||
# delivery status (alias: claudemesh msg-status <id>)
|
||||
claudemesh message status <message-id>
|
||||
claudemesh message status <message-id> --json
|
||||
```
|
||||
|
||||
**Inbox source (1.34.0+):** `claudemesh inbox` queries the daemon's persistent `~/.claudemesh/daemon/inbox.db` over IPC — it is NOT a fresh broker-WS buffer drain. Rows survive daemon restarts. Sender attribution is the actual session pubkey of the launched session that originated the send (NOT the stable member pubkey of the sender's daemon), so two sibling sessions of the same human appear as distinct rows.
|
||||
|
||||
**Read-state (1.34.8+):** every inbox row carries a `seen_at` timestamp. `null` = never surfaced; an ISO string = first surfaced at that moment. The flag flips automatically when (a) the row is returned by an interactive `claudemesh inbox` listing, or (b) the MCP server emits a live `<channel>` reminder for it. The launch welcome push uses `unread_only=true` to surface only rows the user hasn't seen — so a session relaunched a day later sees what it actually missed, not the same 24h batch every time. Use `claudemesh inbox --unread` to get the same filter from the CLI.
|
||||
|
||||
**Self-echo guard (1.34.8+):** broker fan-out paths sometimes mirror an outbound DM back to the originating session-WS. The daemon now drops those at the WS boundary (matching on `senderPubkey === own.session_pubkey`), so the sender no longer sees their own `claudemesh send` arrive as a `← claudemesh: <self>: ...` channel push immediately after dispatching it.
|
||||
|
||||
**Inbox TTL (1.34.8+):** the daemon runs an hourly prune that deletes rows older than 30 days. Without this the inbox grew unbounded; now it self-trims while preserving "I went on holiday and want to see what I missed" recovery for a generous window. No CLI knob — it's a built-in retention policy. To override, manually `claudemesh inbox flush --before <iso>`.
|
||||
|
||||
`send` JSON output: `{"ok": true, "messageId": "...", "target": "..."}`. Errors: `{"ok": false, "error": "..."}`.
|
||||
|
||||
### `state` — shared per-mesh key-value store
|
||||
|
||||
Reference in New Issue
Block a user