Releases: max-listov/ccmux
Release list
ccmux v0.4.0
Claude session status from structured sources (statusLine JSON + hooks), not pane-scraping
feat: Claude session status from structured sources (statusLine JSON + hooks), not pane-scraping
- Context %, model and cost now come from the STRUCTURED JSON Claude Code feeds its statusLine command
(context_window.used_percentage×context_window_size), captured by an injected statusLine
wrapper that ALSO runs the user's own statusline unchanged (or renders a minimalmodel · ctx%
default if they have none). This removes the regex-over-rendered-text scrape and its dependency on
the user's statusline FORMAT, so context % now works on default Claude Code and any user's setup —
not only a bespoke statusline — with no hardcoded model→window map (the window size comes from Claude). - Turn-boundary hooks (UserPromptSubmit/Stop/SessionStart) write a per-session working/idle lifecycle
file. Working/idle display stays pane-decisive (the spinner is reliable and, unlike the hooks, reads
idle correctly right after an ESC-interrupt); the hook fills only the cold-start gap andSessionStart
clears a staleworking. The lifecycle file is the substrate for future event-driven push/"waiting". - Both inject via
--settingsand coexist with the chat Stop hook; status files live under
~/.ccmux/status/and are cleared on stop/rm/restart. Fully fail-open — a status/statusline hiccup
can never wedge a turn or corrupt the rendered bar.
ccmux v0.3.0
Codex launch/resume (close the launch gap) + shell completions
feat: Codex launch/resume (close the launch gap) + shell completions
- Codex sessions now launch and resume through ccmux, 1:1 with Claude as far as the Codex CLI
allows. Codex has no--session-id(a fresh session mints its own rollout id) and no
--append-system-prompt, so: the first launch injects the ccmux management instructions as the
leading positional PROMPT, and a newdetectForkreconciles Codex's self-assigned id back into the
registry through the SAME follow-fork pipeline Claude uses — after whichcodex resume <uuid>tracks
the real conversation (no prompt re-injected on resume). RC has no Codex equivalent (that's a
claude.ai feature), so it stays Claude-only. The root daemon strips Codex's--dangerously-bypass-*
switches, mirroring the Claude root guard. ccmux completions <bash|zsh|fish>— prints a shell completion script generated from the same
COMMANDSregistryccmux helpuses, so a new/renamed verb can never drift from what completes.- Test coverage filled in for the transcript adapters (Claude tool-call folding + Codex response items),
thelistcontext-label parse, and the TUI width/wrap primitives.
ccmux v0.2.1
model from transcript (source of truth), not the statusline whitelist — a new Claude family (Fable/Mythos) is never shown as a blank model again
fix: session model is read from the transcript (source of truth), not scraped from the statusline against a family whitelist — so a new Claude family (Fable, Mythos, …) is never shown as a blank model again
ccmux listreportedmodel: nullfor sessions on any family the pane scraper hadn't been taught.
The model was matched with a(Opus|Sonnet|Haiku)regex against the rendered statusline — a
whitelist that silently dropped Fable 5 (and would drop the next family too), and that depended on
the user's arbitrary custom statusline and reflected the start-time model, not the current one.- The model now comes from the transcript's
message.model(Claude) /turn_context.model(Codex) —
the source of truth, always fresh, format-independent.<synthetic>turns are skipped and only real
assistant turns are trusted (image-gen model ids live in tool payloads). Display formatting is a pure
transform (prettyModel:claude-fable-5→ "Fable 5"), never a lookup table, so a future family
renders with zero code change; anything off-shape falls back to the raw stripped id. - The pane scraper keeps only genuinely-live signals (working/idle, best-effort context); its old
double-duty "model → booted" gate is replaced by a statusline-independentreadymarker. The
managed-list and external-discover paths now share one model source and one formatter.
fix: shipped bundle is truly self-contained — stub react-devtools-core at build time so a cache-cleared / offline machine no longer dies on start with ENOENT; + guard test against future hoisted externals
- Fix the shipped bundle silently depending on the global bun cache / npm at startup. ink imports an
optional DEV-only React DevTools client (react-devtools-core) via a HOISTED static import, so it
loaded on every launch — and built with--externalit resolved that import at runtime against
~/.bun/install/cache(or an npm auto-install). A machine whose cache was cleared, or that had no
network, died on start withENOENT ... react-devtools-core— the daemon (and every session it
supervises) down. The "self-contained" bundle was never actually self-contained. The build now
compiles an inert stub in its place (Bun.build()API + a resolve plugin instead of the
bun build --externalspawn), so the single-file bundle carries no external import and starts
offline / with an empty cache. The bundling moved toscripts/bundle.ts(one build path shared by
stage / CI / release), the misleading "never reached in prod" comment and the obsolete "build only
outside the project tree" caveat are gone, and a guard test builds via that same path and asserts
the bundle starts under a wiped cache + dead registry — so this can never silently regress.
ccmux v0.2.0
fix: shipped bundle is truly self-contained — stub react-devtools-core at build time so a cache-cleared / offline machine no longer dies on start with ENOENT; + guard test against future hoisted externals
- Fix the shipped bundle silently depending on the global bun cache / npm at startup. ink imports an
optional DEV-only React DevTools client (react-devtools-core) via a HOISTED static import, so it
loaded on every launch — and built with--externalit resolved that import at runtime against
~/.bun/install/cache(or an npm auto-install). A machine whose cache was cleared, or that had no
network, died on start withENOENT ... react-devtools-core— the daemon (and every session it
supervises) down. The "self-contained" bundle was never actually self-contained. The build now
compiles an inert stub in its place (Bun.build()API + a resolve plugin instead of the
bun build --externalspawn), so the single-file bundle carries no external import and starts
offline / with an empty cache. The bundling moved toscripts/bundle.ts(one build path shared by
stage / CI / release), the misleading "never reached in prod" comment and the obsolete "build only
outside the project tree" caveat are gone, and a guard test builds via that same path and asserts
the bundle starts under a wiped cache + dead registry — so this can never silently regress.
ccmux v0.1.19
session-reader library seam — expose the tested block-parser as 'ccmux/session-reader' for external consumers (readSession/parseSession/detect + types), lean (no ink/react), inert for the fleet bundle
- Expose the transcript reader as a library —
ccmux/session-reader(src/lib.ts+ a package.json
exportssubpath).readSession(path, agent, textLimit?)/parseSession(lines, agent, textLimit?)
/detect(lines)plus theTranscriptMessagetypes, so an external consumer reuses the tested,
agent-agnostic (Claude + Codex) block-parser instead of duplicating it.textLimitis a passthrough
(default 6000; pass higher for full-text indexing). Lean by construction: the seam wires only the
PURE parsers +readLines+ types, so importing it pulls inzod+node:fs— not ink/react/tmux
(verified:src/lib.tsbundles to ~15 KB with zero ink/react in the subgraph). Inert for the fleet:
the shipped bundle is built fromcli.ts, andexportsonly affects externalimport "ccmux/…"
resolution. The format sniff moved tosrc/agent/detect.ts(normalize-only deps) and is re-exported
fromagent/index.tsunder its existing name.
ccmux v0.1.18
fix: 'ccmux update --check' is now read-only and a stale/older staged bundle can't silently downgrade a machine
- Fix
ccmux update --checkmutating the machine. With a leftover staged bundle present (a forgotten
bun run stage),--checkapplied it instead of just reporting — and applied it even when it was
OLDER than the running version, silently downgrading. Two root fixes: (1) the update decision is now
a puredecideUpdatethat--checkcan only ever ask for aprintfrom — read-only by
construction; (2) a staged bundle wins only when NEWER-or-equal than the running version (an
unreadable one counts as not-newer) — a stale/older staged build is refused as a downgrade unless
--force, with a message pointing at the forgotten file. The legit "test a newer build locally"
path is unchanged.
ccmux v0.1.17
chat-layer follow-ups from acceptance testing: cancellable watchdogs (msg cancel + --task dedup), honest single-source usage, stdin body, --after+--defer trap warning
ccmux msg cancel <task>— drop a sender's still-undelivered mail for a task (an armed--after
watchdog or a queued--deferthat hasn't fired). Cancellation is a tombstone in the append-only
ack-log (by: "cancel"), the same log the daemon and the Stop hook already consult — so a cancel
suppresses delivery in BOTH channels with no new coordination surface and no ledger rewrite.
Scoped to the sender, so a session can never cancel another's dispatch; already-delivered mail
can't be un-sent.- Watchdog dedup by task: re-arming a conditional (
--defer/--after) with the same
(sender, recipient, task)now REPLACES the sender's prior undelivered one instead of piling up a
duplicate ping — closing the router's "two identical watchdogs both fired" gap. Immediate mail is
never replaced (it's delivered at once). --after+--defernow prints a note on send: the two multiply to "not before T AND only at a
turn boundary", so a self-watchdog armed that way won't arrive on time in a long turn — a watchdog
should use bare--after(delivered between tool calls). Not blocked; the flags are compatible.ccmux msgreads the body from stdin (echo "…" | ccmux msg <to>) when no inline text is given
and stdin isn't a TTY — matching the rest of the toolchain.ccmux msg --helpnow shows the full flag set (--defer/--after/--on-behalf-of/cancel). The
short help and the arg-error usage were two separate strings that had drifted; they now render from
one source, so they can't diverge again.ccmux inboxhelp clarifies it's the fallback for held/offline mail, not an archive — a message
already pushed into a pane doesn't sit there.- Diagnostic: the daemon now logs when it HOLDS a pending message solely because a human is attached
to the recipient's pane (delivery resumes on detach) — so "the message never arrived" is traceable
to that transient cause instead of looking like a broken chat.
ccmux v0.1.16
inter-agent deferred chat + autonomous router sessions + time-delayed watchdog delivery
- Deferred chat delivery:
ccmux msg <to> --deferholds a follow-up until the recipient
VOLUNTARILY finishes its turn, then delivers it as if a human typed it — never interrupting
mid-work (Claude's native queue is steering; it flushes between tool calls). Delivered by a
Claude Stop hook the instant the turn ends, or by the daemon once the target is stably idle
(spinner off + assistant-message-last + transcript quiet for a grace window). The Stop hook is
auto-provisioned at launch, merged into a single--settingsobject (verified it does not
clobber the user's own hooks). Coordination is an append-only ack-log keyed by message id —
the daemon stays the sole writer of the delivery cursor, so there is no lost-update race and no
block-loop. - Router sessions — an autonomous manager.
ccmux new <name> <dir> --router/ccmux router on|off <name>gives a session a versioned "manager protocol": it routes an owner-dictated
follow-up to the right target with--defer, waits, validates the result against a stated
done-criterion, re-asks on a gap (bounded), and escalates to the human ONLY when genuinely
stuck — never nagging with "continue?". Activated via apromptModulesdata field (a key into
an in-code module registry, resolved fresh at every launch — no stale snapshot), so it's a
capability toggle, not a persisted role. - Time-delayed delivery:
ccmux msg <to> --after <sec>(anotBeforeinstant). A router arms a
self-watchdogper dispatch, so a target that finishes but never reports back no longer hangs
it — the timer returns control, the router checks the transcript and closes or escalates on its
own. Delivery is now two-track — immediate mail flows in order through the cursor, while
deferred / time-delayed mail is delivered by id off the cursor, so a pending conditional message
never head-of-line-blocks an immediate reply behind it. - Honest relay provenance:
ccmux msg --on-behalf-of <who>renders "on behalf of " so a
router can carry the owner's authority without ever spoofing the sender — gated so only a router
(or the cli) may relay, never a plain peer. - Owner-language: sessions reply to
ownerin the owner's own language by default; an optional
ownerLangin machine.json forces a fixed language.
ccmux v0.1.15
inter-agent chat (menu-safe pane delivery + one-way Telegram mirror) + isolated dev instance
- Isolated dev instance: run a full second ccmux (daemon + sessions + chat) beside prod on one
machine, fully isolated — atmuxSocketconfig scopes every tmux call to its own server (-L),
CCMUX_HOMEoverridable for its own app/log/boot-state,remoteControl:falsekeeps its sessions
out of the claude.ai app. tmux doesn't propagate env into panes, sonew-session -epins the
instance'sCCMUX_HOME/CONFIG/SESSIONS, and the injected prompt teaches the instance's own cli
(not the prod shim) whenCCMUX_HOMEis non-default. Scaffold + teardown viascripts/dev-instance.sh. - Inter-agent chat, sender identity: the sender is automatic and unspoofable — an agent sends as its
own session, a command-line invocation ascli; there is no--from.owneris a reserved
recipient (the human — Telegram-only, no pane); the injected prompt frames[chat from owner|cli]
as the human side (user-level trust) vs[chat from <peer>]as a fellow agent. - Dev daemon hot-reload:
bun daemon:watch(=bun --watch src/cli.ts daemon) restarts the
process on any source change — fresh timers each time, unlike--hot, which re-runs the entry
WITHOUT tearing down the oldensure/chat loops (they'd accumulate; proven with aBun.sleep
probe). The boot-loop guard is now skipped when running from live source (IS_DEV): it protects
the auto-updated prod bundle (revert to.bak), has no bundle to revert in dev, and would only
churn false "boot-loop" errors under rapid--watchrestarts. - Telegram chat mirror: the routing header (
from → to, or📩 for you — from …for a message to
the human) is now bold (HTML parse_mode) so who-is-talking-to-whom reads at a glance; the message
body is HTML-escaped so</>/&render verbatim and never trip a 400 that would drop the message. - Inter-agent chat: opt-in messaging between managed sessions.
ccmux msg <to> "..."/
ccmux inbox/ccmux chat log|on|off, with a per-sessionchatEnabledflag (default off).
The daemon push-delivers into the recipient's pane on a fast cadence, tagged[chat from X]
(framed to the agent as a peer, not the user), gated so it never injects at a selection menu
(would auto-pick an option — proven live) or while a human is attached; a busy recipient just
gets it queued at its next turn boundary. In-order per recipient, no double-push across daemon
bounces; loop/rate guards cap a runaway A→B→A. An append-only ledger (~/.ccmux-chat.jsonl) is
the source of truth; multi-line bodies deliver via bracketed paste. Optional one-way Telegram
mirror (telegramin machine.json → group/DM/topic; fail-soft, outbound only).
ccmux v0.1.14
auto-answer Claude's resume-from-summary picker so daemon-healed reboots don't strand large sessions at the menu
- Auto-answer Claude's blocking "Resume from summary?" picker on an unattended resume. Claude
2.1.x shows this menu on--resumeof a large/old session; a daemon-healed reboot had nobody
to answer it, so big sessions stranded at the menu (typed input — app or tmux — landed on the
menu, not the conversation) until a human manually restarted each one. The_runsupervisor now
watches the freshly-resumed pane and answers per a newresumePickermachine-config policy
(full= keep all context [default] ·summary= compact ·off= leave for a human). It reads
the option NUMBER from the pane (robust to reordering) and confirms with Enter only if the number
key didn't. Claude-only; other agents have no such picker.