Skip to content

Read agent sessions from cmux, with a registry fallback for sessions cmux missed - #51

Merged
elitecoder merged 2 commits into
mainfrom
cmux-native-sessions
Oct 19, 2026
Merged

elitecoder merged 2 commits into
mainfrom
cmux-native-sessions

Conversation

@elitecoder

@elitecoder elitecoder commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

Agent sessions are now read from cmux's own records: cmux sessions list --json --all, via the new bin/cmux_sessions.py. Sessions cmux missed fall back to mukul-memory's ~/.claude/cmux-registry.json. The ledger hook is retired, the forward to cmux is now reliable, and fresh machines get the hook registered. Nothing is deployed.

This revision fixes the two blocking reviews. cmux alone wasn't a complete source: live session ee4ee641 had no cmux record and no resume binding, so the first revision dropped it.

Merging deploys this

The pulse self-update pulls main and runs install.sh --apply within about an hour.

On live state: same verified sessions as base

Both scanners ran on this machine's live state, with output sent to a scratch dir.

personal/main this branch
live sessions 11 10
verified 10 10 (same 10, ee4ee641 included, via session_start_registry from the registry fallback)
only in base aea1111b, unverified — (a May registry row whose pid 2255 now belongs to another program; dropped on purpose)
workspace session_ids — identical
first_recorded_at ledger cmux started_at: 4 sessions differ by under a second, and 3e46079d shows its original 10-06 start because it moved workspaces on 10-10
_meta.session_record_fallback — {"hits": 1, "session_ids": ["ee4ee641-…"]}

The watcher on each scanner's world.json finds the same 10 sessions, ee4ee641 included. The cmux-restore-sessions --dry-run plan is identical to base: same panels, same commands. That includes ee4ee641's panel, which has no resume binding.

What changed

Adapter. bin/cmux_sessions.py:

  • list_sessions() waits up to 15 seconds, was 5; the reviewers measured 1.5–4.8 seconds under load. On failure it returns [] and writes one stderr line.
  • registry_fallback(path, records) returns registry rows only for sessions cmux has no record of.
  • agent_process(pid) rejects a registry pid that is no longer claude or droid.

Readers. All five use cmux first, then the registry fallback:

  • world-scanner.py tries cmux's active record for each surface first, then a registry row. Two active cmux records for one surface verify neither. It writes _meta.session_record_fallback and logs fallback_hits=N.
  • session-context-watcher.py
  • build-ws-context.py, where two cmux records for one surface are ambiguous and the newest registry row wins.
  • transcript-tail.py
  • cmux-restore-sessions.py: kept, and migrated from the ledger, because native cmux restore --surface needs a binding.

Forward. hooks/cmux-auto-resume.py starts a detached worker and returns at once. The worker:

  • checks the cmux hooks <provider> session-start exit code;
  • confirms the record with cmux sessions list --session <id>;
  • retries after 0, 2, 5, 15, and 30 seconds;
  • after the last try, writes one cmux-session-forward-fail entry to ~/.assistant/actions-ledger.jsonl, which the brief and Slack feed surface.

It never raises out of the hook.

Ledger hook. hooks/cmux-session-ledger.py is a no-op stub for one release, so the old registration still exits 0 between git pull and --apply. Its link stays.

Installer. install/patch-settings.py:

  • registers python3 $HOME/.claude/hooks/cmux-auto-resume.py (timeout 10) on SessionStart, in both Claude settings and Factory hooks, if it's missing;
  • removes the ledger registrations;
  • never touches mukul-memory's cmux-registry.py;
  • writes with ensure_ascii=False;
  • when one file can't be read, still patches the others and exits 1.

install.sh is base apart from its help text, comments, and messages.

Coverage gate. It now measures hooks/ and install/ Python.

Known, not changed

cmux-restore-sessions (base and branch alike) treats live sessions started outside cmux's wrapper as dead. Their argv doesn't carry the session id, so pgrep misses them: 5 panels here, including ee4ee641 and the warm comms session. Its --dry-run is safe; a real run would type claude --resume into those live panes. This predates the PR.

Tests

  • Full suite (Python 3.13, as in CI): 3020 passed and 1 skipped on base; 3097 passed and 1 skipped on this branch, plus 186 subtests both times. CI on 47f922a: 3097 passed, 2 skipped.
  • New-code coverage gate: passed locally and in CI. Changed lines and branches in bin/, hooks/, and install/ are 100% covered, and all 5 install.sh hunks are mutation-proven.
  • tests/mutation_smoke.py: 39 of 39 rejected (19 new), locally and in CI.
  • One-off mutation probes on the new code: 42 probes. 41 were killed on the first pass; the survivor (the hook accepting another session's record) was killed by a new test.
  • The two surviving mutants (len(…) != 1): now killed by tests with a claude and a factory record on one surface.

Deploy (after merge; not run)

git -C ~/dev/assistant pull --ff-only personal main
~/dev/assistant/install.sh            # dry run: preview
~/dev/assistant/install.sh --apply

~/.claude/bin is a symlink to ~/dev/assistant/bin, so the pull updates the readers immediately. --apply registers the hook and drops the ledger registrations.

To retire the fallback, watch _meta.session_record_fallback.hits in ~/.claude/cache/world.json and the fallback_hits= count in ~/.assistant/logs/world-scanner.out. Once both stay at zero for a soak period, remove registry_fallback and its callers. Delete the ledger stub and its install.sh link in the next release.

Rollback

Revert the merge commit, then run install.sh --apply. That restores:

  • the readers on registry and ledger;
  • cmux-restore-sessions on the ledger;
  • the single fire-and-forget forward;
  • the ledger hook, because base patch-settings.py re-registers it on SessionStart and SessionEnd. The ledger file resumes where it stopped.

Two things a revert doesn't undo:

  • The auto-resume registration this branch adds on a fresh machine stays, because the base installer never removes hooks. It's harmless: the base hook makes the same single forward.
  • Ledger events from the deploy window are lost, so First recorded for sessions started in that window stays unknown until they restart.

🤖 Generated with Claude Code

elitecoder and others added 2 commits October 19, 2026 08:24
cmux 0.64.25 records every agent session itself, and `cmux sessions list
--json --all` prints those records without its socket. bin/cmux_sessions.py
is the one place that runs it: it returns typed records, finds the CLI via
CMUX_BIN, an executable CMUX_BUNDLED_CLI_PATH, PATH, then the app bundle, and
returns [] with one stderr line when cmux can't answer.

world-scanner, session-context-watcher, build-ws-context and transcript-tail
now read it instead of ~/.claude/cmux-registry.json and
~/.cmux-session-ledger.jsonl. Only the session cmux marks active on a surface
counts as running there, and first_recorded_at comes from cmux's started_at.
Output fields keep their names and values.

Remove the ledger hook and cmux-restore-sessions (use cmux restore-session or
cmux restore --surface). install.sh drops their old links, and
patch-settings.py now removes the ledger and cmux-registry.py registrations
instead of adding them. Keep hooks/cmux-auto-resume.py: agents started
without cmux's Claude wrapper (absolute ~/.local/bin/claude, the fleet's
scheduled spawns) are only recorded by cmux because it forwards their
SessionStart.

The coverage gate now measures hooks/ and install/ Python.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two reviews blocked the first revision: cmux is not a complete source. Live
session ee4ee641 (workspace:27, started outside cmux's wrapper) has no cmux
record and no resume binding, because cmux-auto-resume.py forwarded its
SessionStart once, fire-and-forget, and the forward failed.

- bin/cmux_sessions.py: registry_fallback() returns ~/.claude/cmux-registry.json
  rows (mukul-memory's hook) only for sessions cmux has no record of;
  agent_process() rejects a reused pid; the CLI timeout is 15 s.
- All readers use cmux first, then the fallback. world-scanner writes
  _meta.session_record_fallback and logs fallback_hits=N so the fallback can
  be retired after a soak with zero hits. On live state it verifies the same
  10 sessions as base, ee4ee641 included; only the aea1111b pid-reuse ghost
  drops.
- hooks/cmux-auto-resume.py starts a detached worker that checks the exit
  code, confirms the record with `cmux sessions list --session`, retries with
  backoff, and ledgers a final failure as cmux-session-forward-fail.
- cmux-restore-sessions stays, reading cmux records then the registry, since
  native restore needs a resume binding.
- hooks/cmux-session-ledger.py is a no-op stub for one release; its link
  stays, so a registration that outlives the pull still exits 0.
- patch-settings.py registers cmux-auto-resume.py when missing, removes only
  the ledger registrations, leaves cmux-registry.py alone, and writes UTF-8.
- Tests kill the two surviving len(...) != 1 mutants.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@elitecoder elitecoder changed the title Read agent sessions from cmux instead of the registry and ledger files Read agent sessions from cmux, with a registry fallback for sessions cmux missed Oct 19, 2026
@elitecoder
elitecoder merged commit ae4e0bf into main Oct 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant