Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .collab/findings/2026-10-13T09-22-36Z-muse-parity-gaps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Muse parity gaps: documented/installed-but-inert class (T2/T4/T6)

Date: 2026-10-13T09:22:36Z · Scope: muse-parity branch, tasks T2/T4/T6
(cf. #50: a wake/delivery path can be documented, installed, and inert)
Severity: MEDIUM overall (no mail loss; silent no-ops on three shipped
surfaces, each contradicting its own docs). Wake tier unaffected: muse
stays NONE at true idle, CONDITIONAL only while `c2c monitor` is armed.

## T2 — Skill refresh defined and tested, never called

- **Symptom:** after a `c2c` upgrade, muse sessions kept the old `/c2c`
skill forever unless the operator re-ran `c2c install muse`. Docs
claimed SessionStart "refreshes the skill on drift only" in the present
tense.
- **Root cause:** `C2c_setup.refresh_muse_skill_if_stale` existed with
unit tests, but had zero call sites in the muse SessionStart hook —
unlike the claude/kimi arms, which refresh on SessionStart.
- **Fix:** the muse SessionStart path now calls it best-effort (drift-only
write, skill file only — never `settings.json`, whose rewrite resets
plugin trust). Muse snapshots its skill catalogue at session start, so
the refreshed skill is read by the *next* session (the #83 lag class).
- **Severity:** LOW (stale guidance only, but self-contradicting docs).

## T4 — Schedules double-broken for default (no-MCP) muse

- **Symptom:** `c2c schedule set` for a managed muse session never fired,
silently — no error, no self-DM, no log line an operator would find.
- **Root cause:** two stacked defects. (1) Managed start hardcoded
`C2C_MCP_SCHEDULE_TIMER=1`, which suppresses the start-watcher fallback
on the assumption an MCP server runs the Lwt timer — but default muse
installs no MCP server, so *neither* owner fired. (2)
`default_heartbeat_clients` excluded muse, so no builtin heartbeat
self-DM existed either. The "=1 means enabled" reading inverted the
actual semantics: for no-MCP muse it meant suppression.
- **Fix:** one shared ownership decision
(`schedule_timer_enabled_for_instance` in `ocaml/c2c_start.ml`): the
timer env is "1" only when an MCP server will actually run for the
instance (muse: an `mcpServers.c2c` entry in muse settings), else "0"
and the watcher owns firing; an explicit operator
`C2C_MCP_SCHEDULE_TIMER` still wins. Muse added to
`default_heartbeat_clients`. Honesty retained: a fired schedule is a
self-DM enqueue, not a wake.
- **Severity:** MEDIUM (a user-facing feature was a silent no-op on the
default install shape).

## T6 — Rooms auto-join was MCP-only, so default muse never joined

- **Symptom:** a default (no-MCP) muse session registered fine but never
joined `C2C_MCP_AUTO_JOIN_ROOMS` rooms (product default `swarm-lounge`),
so room mail never reached it and peers saw it absent from the room.
- **Root cause:** the only consumer of `C2C_MCP_AUTO_JOIN_ROOMS` was MCP
server startup. The muse hook path — the default identity/registration
path — had no auto-join at all, and install writes the env only inside
the MCP config a default session never runs.
- **Fix:** muse SessionStart joins after successful register/adopt, under
the just-resolved identity (a B361-adopted row joins as the managed
alias, never a freshly minted one), reusing the same env parsing and
`Broker.join_room` call; env absent falls back to the product default
room, env pinned empty joins nothing, per-room failures are best-effort.
Joining is an install-time operator setting taking effect at
registration — not a message-triggered action (B098-safe).
- **Severity:** MEDIUM (rooms are a product surface; absence was silent).

## The class, and the guard

All three share the #50 shape: the mechanism existed, was installed or
documented, and was inert because exactly one wiring step was missing —
and nothing observable complained. The parity pass therefore asserts each
surface end-to-end at its real entry point (hook e2e for T2/T6, managed
env/ownership tests for T4) rather than unit-testing the helper alone:
a tested helper with no call site is how T2 shipped. When adding a muse
surface, grep for the call site, not just the definition.
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ even when the immediate task is narrower.
its own loop, like Pi Agent. Muse is CLI-first: skill + plugin/hooks;
managed `c2c start muse` is real via `MuseAdapter`; wake is Monitor-only —
no out-of-process sender in 1.4.2, B356 parked). Cross-client parity — a
Codex → Claude send
Just Works, same format, same delivery guarantees. Local-only today; broker
Codex → Claude send Just Works, same format; delivery guarantees are
per-client, not uniform (see the wake table below): Muse sits in the
Monitor-CONDITIONAL class with Claude Code and Grok — NONE at true idle
from c2c alone, CONDITIONAL only while `c2c monitor` is armed — and is
never GUARANTEED. Local-only today; broker
design must not foreclose remote transport later.
- **Topology**: 1:1 ✓, 1:N ✓ (broadcast via `send_all`), N:N ✓ (rooms:
`join_room`, `send_room`, `room_history`, `my_rooms`, `list_rooms`,
Expand Down
65 changes: 47 additions & 18 deletions docs/client-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,9 +706,15 @@ message. Gateway delivery needs a separate path and is unimplemented. See

## Muse

> **NEW client (2026-10-02, probed at muse 1.4.2).** Monitor-only wake —
> no out-of-process sender exists. Cross-client DM parity has not been
> verified live yet.
> **NEW client (2026-10-02, probed at muse 1.4.2; hook-output shapes
> re-probed at muse 1.4.7).** Monitor-only wake — no out-of-process
> sender exists. `scripts/muse-parity-e2e.py` verifies install, hook
> register/auto-join/default-alias, Stop/PostToolUse peek+nudge (never
> drain), CLI surface, doctor/health, and uninstall dry-run against the
> built binary, plus a live-TUI leg (peer DM → model retrieves it via
> `c2c poll-inbox`, token observed model-visible in the pane). It does
> not cover Monitor-armed idle delivery, muse→peer sends, room sends,
> or restart backlog.

`c2c install muse` writes:

Expand Down Expand Up @@ -744,16 +750,26 @@ a hook can never break a turn):
- SessionStart is **lazy** (fires on first turn submit, not at TUI open)
and **resume-silent** (never fires on `resume`) — measured, not assumed.
It registers the firing `session_id` (`registered_by=muse-hook`,
`client_type=muse`), refreshes the skill on drift only, and drains the
backlog into `additionalContext` (proven accepted). A resumed session is
hook-invisible: no re-onboarding turn.
- Stop (per turn) and PostToolUse (per tool call) are **touch-only
activity anchors** — never register, never drain. A touch for an unknown
sid is a registry no-op, which is also the subagent rule: subagent tool
calls arrive as PostToolUse under a *distinct* session id, so no
subagent row is ever created (live-proven: a Task subagent + 4 reminder
agents left zero phantom rows). SessionEnd deregisters hook-owned rows
only; Stop is never teardown.
`client_type=muse`), refreshes the skill on drift only, auto-joins rooms under the resolved identity — the room list comes from the install-written `c2c-auto-join-rooms` file in the muse config dir (mirroring the MCP `C2C_MCP_AUTO_JOIN_ROOMS` value, because muse scrubs `C2C_*` from hook env so the env itself never reaches hooks; an empty file joins nothing, a missing file falls back to the product default room; the env still wins when a hook process somehow carries it),
refreshes `~/.config/c2c/default-alias` fail-closed under that same
identity — and only when no other live session with a different alias
exists (so bare `c2c monitor` resolves the live inbox, including
`--ephemeral` mail, without ever clobbering a second live session) —
and drains the backlog into `additionalContext` (proven accepted; the
one hook drain surface). A resumed session gets no SessionStart; its
queued mail is count-nudged at its first Stop/PostToolUse (below) and
retrieved with `c2c poll-inbox`.
- Stop (per turn) and PostToolUse (per tool call) touch the activity anchor
and then **peek + count-nudge — never drain**. Measured on muse 1.4.7:
Stop rejects `additionalContext` (one-shot `decision:block` nudge,
suppressed when `stop_hook_active`), and a live PostToolUse drain ate
mail (no context block reached the main model), so PostToolUse emits a
debounced count nudge with no bodies. They never register: an unknown sid
(a subagent's distinct PostToolUse sid) peeks nothing and its touch is a
registry no-op, so no subagent row is ever created (live-proven: a Task
subagent + 4 reminder agents left zero phantom rows). Nudges are **not**
an idle wake. SessionEnd deregisters hook-owned rows only; Stop is never
teardown.
- **Managed adoption (B361):** muse scrubs hook env (no `C2C_*` even
though the TUI carries it), so SessionStart step 0 adopts the managed
row by normalized cwd + live (pid, pid_start_time) before minting, and
Expand All @@ -779,17 +795,30 @@ c2c-minted id). Idle signal is Unknown (fail-closed); SIGTERM exits in
out-of-process wake inside c2c alone):

```
Monitor({ description: "c2c inbox watcher", command: "c2c monitor", persistent: true })
Monitor({command:"c2c monitor",persistent:true})
```

For mail on the cross-repo sessions broker, arm the same recipe against
that broker instead:

```
Monitor({command:"c2c monitor --cross-repo",persistent:true})
```

Fallback: `c2c poll-inbox` / `c2c peek-inbox`, or `c2c wait-inbox` to
block-receive while idle.

**Wake status: CONDITIONAL** on an armed **`c2c monitor`**. Without
Monitor: **NONE** at true idle (hooks fire on activity only). `c2c doctor
hooks` reports per-session NONE/CONDITIONAL rows from live monitor locks,
and deliberately never flags a muse session DEAF — with no poster,
inbox>0 is the expected steady state, not a failure (same as Grok). The
Monitor: **NONE** at true idle (hooks fire on activity only; the activity
drain above is not a wake). `c2c doctor hooks` reports per-session
NONE/CONDITIONAL rows from live monitor locks, and deliberately never flags
a muse session DEAF — with no poster, inbox>0 is the expected steady state,
not a failure (same as Grok). `c2c doctor hooks --rearm` therefore arms
nothing for muse; it prints the verbatim `Monitor({command:"c2c monitor",persistent:true})` instruction per
affected alias for the operator to run inside that session. `c2c health`
shows muse yellow — not green — when the plugin is unapproved or no hooks
marker (trusted plugin or settings.json fallback) is present, and no
surface claims live hook firing, which doctor cannot observe. The
Tier-2 session-message sidecar stays parked: the CLI refused
out-of-process send in 1.4.2. See
[Delivery & Wake Contract](/wake-contract/#claude-code-grok-and-muse-conditional-on-monitor-never-guaranteed-from-c2c-alone)
Expand Down
Loading
Loading