@martintrojer/murmur 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/cli/clear.ts","../src/identity.ts","../src/paths.ts","../src/mux.ts","../src/store.ts","../src/channel.ts","../src/types.ts","../src/fold.ts","../src/export.ts","../src/collector.ts","../src/cli/collect.ts","../src/cli/export.ts","../src/cli/init.ts","../src/cli/link.ts","../src/cli/peer.ts","../src/cli/pick.ts","../src/agents.ts","../src/glance.ts","../src/status.ts","../src/cli/status.ts","../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { registerClear } from \"./cli/clear.js\";\nimport { registerCollect } from \"./cli/collect.js\";\nimport { registerExport } from \"./cli/export.js\";\nimport { registerInit } from \"./cli/init.js\";\nimport { registerLink } from \"./cli/link.js\";\nimport { registerPeer } from \"./cli/peer.js\";\nimport { registerPick } from \"./cli/pick.js\";\nimport { registerStatus } from \"./cli/status.js\";\nimport { VERSION } from \"./index.js\";\n\nconst program = new Command();\nprogram\n .name(\"murmur\")\n .description(\"Agent state across every machine, in one view.\")\n .version(VERSION);\nregisterInit(program);\nregisterLink(program);\nregisterExport(program);\nregisterCollect(program);\nregisterClear(program);\nregisterPeer(program);\nregisterStatus(program);\nregisterPick(program);\nprogram.parse();\n","import Database from \"better-sqlite3\";\nimport type { Command } from \"commander\";\nimport { loadIdentity } from \"../identity.js\";\nimport { type Mux, tmux } from \"../mux.js\";\nimport { dbPath } from \"../paths.js\";\nimport { openStore } from \"../store.js\";\nimport type { Driver } from \"../types.js\";\n\ntype OwnedPane = {\n agent_id: string;\n session_name: string | null;\n window_name: string | null;\n agent_name: string | null;\n pi_session: string | null;\n session: string;\n window: string;\n pane: string;\n workstream: string | null;\n role: string | null;\n cli: string | null;\n driver: Driver | null;\n state: string;\n};\n\n/**\n * Does any OTHER pane in this window own an agent?\n *\n * Read-only, and best effort: if tmux or the database cannot answer we say yes,\n * which leaves the badge alone. Wrongly keeping a badge is recoverable by\n * focusing the agent's own pane; wrongly clearing one loses the signal.\n */\nfunction windowHasAgent(\n window: string,\n focused: string,\n hostId: string | undefined,\n mux: Mux,\n): boolean {\n // No identity means this node has authored nothing, so no sibling can own an\n // agent and there is nothing to protect. Returning true here blocked the\n // orphan-badge clear on a node that had murmur installed but never ran init.\n if (!hostId) return false;\n const siblings = mux.panesInWindow(window).filter((candidate) => candidate !== focused);\n // No siblings means nothing to protect. Checked before opening the database\n // so a node with no events yet still clears an orphan badge: treating a\n // missing database as \"a sibling might own an agent\" left every stale badge\n // in place on a fresh install.\n if (siblings.length === 0) return false;\n try {\n const database = new Database(dbPath(), { readonly: true, fileMustExist: true });\n try {\n for (const sibling of siblings) {\n const row = database\n .prepare(\n `SELECT state FROM events\n WHERE host_id = ? AND agent_id = ?\n ORDER BY seq DESC LIMIT 1`,\n )\n .get(hostId, `${hostId}:${sibling}`) as { state?: string } | undefined;\n if (row && row.state !== \"cleared\") return true;\n }\n } finally {\n database.close();\n }\n return false;\n } catch {\n return true;\n }\n}\n\nexport function clearPane(pane: string, mux: Mux = tmux): void {\n try {\n if (!pane) return;\n\n // The badge is a tmux window option, not murmur state, so clearing it never\n // needs murmur to know anything. Resolve the window up front: an\n // uninitialised node or a missing database must still clear rather than\n // abort the hook.\n const window = mux.windowForPane(pane);\n const identity = loadIdentity();\n\n let owner: OwnedPane | undefined;\n if (identity) {\n try {\n const database = new Database(dbPath(), { readonly: true, fileMustExist: true });\n try {\n owner = database\n .prepare(\n `SELECT agent_id, session, window, pane, session_name, window_name,\n agent_name, pi_session, workstream, role, cli, driver, state\n FROM events\n WHERE host_id = ? AND agent_id = ?\n ORDER BY seq DESC\n LIMIT 1`,\n )\n .get(identity.host_id, `${identity.host_id}:${pane}`) as OwnedPane | undefined;\n } finally {\n database.close();\n }\n } catch {\n // No database yet. Nothing is owned; the badge still clears below.\n }\n }\n\n // A pane murmur has no event for can still carry a badge: an orphan from\n // the agent-attention era, or a window murmur never recorded. Left alone it\n // sits in the status bar and the tms picker forever, because nothing else\n // will ever clear it.\n //\n // But only when no SIBLING pane owns an agent. The badge is a window\n // option while \"the user looked\" is only true of one pane, so clearing on\n // any pane in the window let a shell pane wipe the agent's badge next to\n // it -- which is the exact case --pane exists to distinguish.\n if (!owner) {\n if (window && !windowHasAgent(window, pane, identity?.host_id, mux)) {\n mux.setState(window, null);\n }\n return;\n }\n // Already cleared in the log, but the badge may still be set: the two can\n // disagree when a `cleared` event was written by a path that did not touch\n // tmux, and nothing else reconciles them. Clear the option and return\n // without appending a second, redundant `cleared` event.\n if (owner.state === \"cleared\") {\n mux.setState(owner.window, null);\n return;\n }\n\n const store = openStore();\n try {\n store.append({\n agent_id: owner.agent_id,\n session: owner.session,\n window: owner.window,\n pane: owner.pane,\n // Carry the names forward: a `cleared` row that drops them makes the\n // agent's last event nameless, which is what left \"@75\" in the picker.\n session_name: owner.session_name,\n window_name: owner.window_name,\n agent_name: owner.agent_name,\n pi_session: owner.pi_session,\n workstream: owner.workstream,\n role: owner.role,\n cli: owner.cli,\n driver: owner.driver,\n kind: \"state\",\n state: \"cleared\",\n message: \"\",\n pid: null,\n synthetic: false,\n reason: \"\",\n extra: {},\n });\n } finally {\n store.close();\n }\n mux.setState(owner.window, null);\n } catch {\n // Focus hooks run inside the tmux server: they must always be silent and total.\n }\n}\n\nexport function registerClear(program: Command): void {\n program\n .command(\"clear\")\n .description(\"Clear attention for the agent in a pane\")\n .option(\"--pane <pane-id>\", \"focused tmux pane id\")\n .action((options: { pane?: string }) => clearPane(options.pane ?? \"\"));\n}\n","import { randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\nimport { stateDir } from \"./paths.js\";\n\nexport type NodeIdentity = {\n host_id: string;\n display_name: string;\n};\n\nexport function loadIdentity(): NodeIdentity | null {\n const path = join(stateDir(), \"identity.json\");\n return existsSync(path) ? JSON.parse(readFileSync(path, \"utf8\")) : null;\n}\n\nexport function ensureIdentity(displayName = hostname()): NodeIdentity {\n const existing = loadIdentity();\n if (existing) return existing;\n\n const identity = { host_id: randomUUID(), display_name: displayName };\n mkdirSync(stateDir(), { recursive: true });\n writeFileSync(join(stateDir(), \"identity.json\"), `${JSON.stringify(identity, null, 2)}\\n`);\n return identity;\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function stateDir(): string {\n return (\n process.env.MURMUR_STATE_DIR ??\n join(process.env.XDG_STATE_HOME ?? join(homedir(), \".local\", \"state\"), \"murmur\")\n );\n}\n\nexport function configDir(): string {\n return (\n process.env.MURMUR_CONFIG_DIR ??\n join(process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\"), \"murmur\")\n );\n}\n\nexport function dbPath(): string {\n return join(stateDir(), \"events.db\");\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { AgentState } from \"./types.js\";\n\nexport type Location = {\n session: string;\n window: string;\n pane: string;\n session_name: string | null;\n window_name: string | null;\n};\n\nexport interface Mux {\n currentWindow(): Location | null;\n liveWindows(): Set<string> | null;\n setState(window: string, state: AgentState | null): void;\n attach(session: string, window: string): void;\n windowNames(): Map<string, string>;\n windowForPane(pane: string): string | null;\n panesInWindow(window: string): string[];\n windowNamed(name: string): string | null;\n selectWindow(window: string): void;\n capture(pane: string, lines?: number): string | null;\n}\n\nfunction runTmux(args: string[]): string | null {\n try {\n return execFileSync(\"tmux\", args, {\n encoding: \"utf8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n } catch {\n return null;\n }\n}\n\nexport const tmux: Mux = {\n currentWindow() {\n // $TMUX_PANE is the only trustworthy signal that we are inside a pane, and\n // it is set by tmux for every process in one.\n //\n // Asking tmux instead does not work: `display-message` answers from any\n // process on a machine with a running server, and reports whichever pane\n // that server considers active. A pi started outside tmux -- a bare ssh\n // login, a plain terminal, cron -- would then record itself as living in\n // some unrelated agent's pane and overwrite that agent's state. Falling\n // back to `display-message` here was exactly that bug.\n const pane = process.env.TMUX_PANE;\n if (!pane) return null;\n\n // One call for ids and names together. The names are recorded on every\n // event because a reader cannot resolve a remote window id against its own\n // tmux, so they have to travel with the event.\n const fields = runTmux([\n \"display-message\",\n \"-t\",\n pane,\n \"-p\",\n \"#{session_id}\\t#{window_id}\\t#{session_name}\\t#{window_name}\",\n ]);\n const [session, window, sessionName, windowName] = fields?.split(\"\\t\") ?? [];\n if (!session || !window) return null;\n return {\n session,\n window,\n pane,\n session_name: sessionName || null,\n window_name: windowName || null,\n };\n },\n\n // Which of this host's windows still exist. Only the authoring node can\n // answer this, which is why the check runs on export rather than on the\n // reader: a peer holding a `blocked` row for a window that died has nothing\n // to supersede it, and the agent stays in every HUD forever.\n //\n // null means \"could not tell\" (no tmux server, tmux missing) and is\n // deliberately distinct from an empty set, which means \"tmux answered, and\n // there are no windows\". Treating the first as the second would clear every\n // agent on the host the moment tmux was unreachable.\n //\n // Unlike currentWindow, this deliberately asks tmux rather than reading the\n // environment, and it is right to: \"which windows exist on this host\" is a\n // server-wide question with one answer, and export runs over ssh with no\n // pane of its own. currentWindow asks \"which pane am I in\", which only\n // $TMUX_PANE can answer.\n liveWindows() {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\"]);\n if (out === null) return null;\n return new Set(out.split(\"\\n\").filter(Boolean));\n },\n\n setState(window, state) {\n if (state === null) {\n runTmux([\"set-window-option\", \"-qu\", \"-t\", window, \"@agent_state\"]);\n } else {\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@agent_state\", state]);\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@pane_agent\", \"1\"]);\n }\n runTmux([\"refresh-client\", \"-S\"]);\n },\n\n attach(session, window) {\n // Two steps, because switch-client alone is a no-op when the target window\n // is in the session you are already attached to — which is the common case\n // for a local agent, and why \"enter\" appeared to do nothing.\n // switch-client moves the client between sessions; select-window moves\n // that session to the right window.\n runTmux([\"switch-client\", \"-t\", session]);\n runTmux([\"select-window\", \"-t\", window]);\n },\n\n // Window ids are what the log stores, because they are stable; names are\n // what a human recognises in a picker. Names are live tmux state, not\n // history, so they are resolved at render time rather than recorded.\n windowNames() {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\\t#{window_name}\"]);\n const names = new Map<string, string>();\n for (const line of out?.split(\"\\n\") ?? []) {\n const [id, name] = line.split(\"\\t\");\n if (id && name) names.set(id, name);\n }\n return names;\n },\n\n // First window carrying this exact name, or null. Used to reuse a per-host\n // ssh window instead of opening another one.\n // Sibling panes, for deciding whether an unowned pane may clear the window's\n // badge. A window holding an agent and a shell must not lose the badge when\n // you focus the shell.\n panesInWindow(window) {\n const out = runTmux([\"list-panes\", \"-t\", window, \"-F\", \"#{pane_id}\"]);\n return out?.split(\"\\n\").filter(Boolean) ?? [];\n },\n\n windowNamed(name) {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\\t#{window_name}\"]);\n for (const line of out?.split(\"\\n\") ?? []) {\n const [id, windowName] = line.split(\"\\t\");\n if (id && windowName === name) return id;\n }\n return null;\n },\n\n selectWindow(window) {\n runTmux([\"select-window\", \"-t\", window]);\n },\n\n // The window a pane belongs to, for a pane murmur has no event for. Clearing\n // a badge is a tmux operation and does not require murmur to own the pane.\n windowForPane(pane) {\n return runTmux([\"display-message\", \"-t\", pane, \"-p\", \"#{window_id}\"]) || null;\n },\n\n capture(pane, lines) {\n const args = [\"capture-pane\", \"-p\", \"-t\", pane];\n if (lines !== undefined) args.push(\"-S\", `-${lines}`);\n return runTmux(args);\n },\n};\n\nexport function pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code !== \"ESRCH\";\n }\n}\n","import { rmSync } from \"node:fs\";\nimport Database from \"better-sqlite3\";\nimport { ensureIdentity } from \"./identity.js\";\nimport { dbPath } from \"./paths.js\";\nimport type { Driver, Event, Peer } from \"./types.js\";\n\nconst DEFAULT_RETENTION_MS = 7 * 86_400_000;\n\n/**\n * Local storage shape. Bump on any change to the events or peers tables.\n *\n * Distinct from `SCHEMA_VERSION` in export.ts, which versions the *wire*: a\n * node can change how it stores events without changing what it sends, and a\n * wire change should not throw away local history.\n */\nexport const STORE_VERSION = 2;\n\n/**\n * Migration strategy: there isn't one. A version mismatch deletes the database\n * and starts again.\n *\n * This is only acceptable because nothing in events.db is authoritative or\n * irreplaceable. It is a bounded-retention observability log: remote events\n * re-sync from their authoring peer on the next collect, local agents re-report\n * on their next state change, and node identity deliberately lives in a\n * separate file. If anything durable is ever added here, this stops being safe\n * and a real migration is required.\n *\n * Peers survive, because they are the one thing a human typed. Watermarks are\n * reset with the events they indexed -- keeping them would skip the events the\n * new database no longer has -- and re-reading a peer from zero is free, since\n * ingest is idempotent.\n */\nfunction resetIfStale(path: string): Peer[] {\n let salvaged: Peer[] = [];\n try {\n const existing = new Database(path, { fileMustExist: true });\n const version = (existing.pragma(\"user_version\", { simple: true }) as number) ?? 0;\n if (version === STORE_VERSION) {\n existing.close();\n return salvaged;\n }\n try {\n salvaged = existing\n .prepare(\"SELECT name, target, host_id, display_name FROM peers\")\n .all() as Peer[];\n } catch {\n // Old enough not to have the table, or unreadable. Nothing to save.\n }\n existing.close();\n } catch {\n // No database yet, or one too broken to open. Either way, recreate.\n return salvaged;\n }\n\n // -wal and -shm must go too: a stale sidecar against a fresh main file is a\n // documented way to corrupt sqlite.\n for (const suffix of [\"\", \"-wal\", \"-shm\"]) rmSync(`${path}${suffix}`, { force: true });\n return salvaged;\n}\n\n// The name fields are optional on the way in: a caller that has no name for a\n// thing should not have to say `null` four times, and a non-tmux harness has\n// none of them. They are non-optional on `Event` itself, so a reader never has\n// to distinguish absent from null.\nexport type NewEvent = Omit<\n Event,\n \"host_id\" | \"seq\" | \"ts\" | \"session_name\" | \"window_name\" | \"agent_name\" | \"pi_session\"\n> & {\n ts?: number;\n session_name?: string | null;\n window_name?: string | null;\n agent_name?: string | null;\n pi_session?: string | null;\n};\n\ntype EventRow = Omit<Event, \"synthetic\" | \"extra\"> & {\n synthetic: number;\n extra: string;\n};\n\nfunction eventValues(event: Event): unknown[] {\n return [\n event.host_id,\n event.seq,\n event.ts,\n event.agent_id,\n event.session,\n event.window,\n event.pane,\n event.session_name,\n event.window_name,\n event.agent_name,\n event.pi_session,\n event.workstream,\n event.role,\n event.cli,\n event.driver,\n event.kind,\n event.state,\n event.message,\n event.pid,\n Number(event.synthetic),\n event.reason,\n JSON.stringify(event.extra),\n ];\n}\n\nfunction toEvent(row: EventRow): Event {\n return {\n ...row,\n driver: row.driver as Driver | null,\n synthetic: row.synthetic === 1,\n extra: JSON.parse(row.extra) as Record<string, unknown>,\n };\n}\n\nexport interface Store {\n append(event: NewEvent): Event;\n ingest(events: Event[]): number;\n eventsSince(hostId: string, seq: number): Event[];\n allEvents(): Event[];\n maxSeq(hostId: string): number;\n prune(horizonMs?: number): number;\n peers(): Peer[];\n /**\n * Drop every event for one agent from this node's replica.\n *\n * For a remote agent this is a replica eviction, not a claim about truth: the\n * authoring node still owns it, and a collect re-reads from the watermark if\n * it is still alive.\n */\n forgetAgent(agentId: string): number;\n forgetHost(hostId: string): number;\n upsertPeer(peer: Partial<Peer> & { name: string; target: string }): void;\n removePeer(name: string): boolean;\n close(): void;\n}\n\nexport function openStore(): Store {\n const identity = ensureIdentity();\n const path = dbPath();\n const salvagedPeers = resetIfStale(path);\n const database = new Database(path);\n database.pragma(\"journal_mode = WAL\");\n database.pragma(`user_version = ${STORE_VERSION}`);\n database.exec(`\n CREATE TABLE IF NOT EXISTS events (\n host_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n ts INTEGER NOT NULL,\n agent_id TEXT NOT NULL,\n session TEXT NOT NULL,\n window TEXT NOT NULL,\n pane TEXT NOT NULL,\n session_name TEXT,\n window_name TEXT,\n agent_name TEXT,\n pi_session TEXT,\n workstream TEXT,\n role TEXT,\n cli TEXT,\n driver TEXT,\n kind TEXT NOT NULL,\n state TEXT NOT NULL,\n message TEXT NOT NULL,\n pid INTEGER,\n synthetic INTEGER NOT NULL,\n reason TEXT NOT NULL,\n extra TEXT NOT NULL,\n PRIMARY KEY (host_id, seq)\n );\n CREATE INDEX IF NOT EXISTS events_agent_seq ON events (agent_id, seq);\n CREATE TABLE IF NOT EXISTS peers (\n name TEXT PRIMARY KEY,\n target TEXT NOT NULL,\n host_id TEXT,\n display_name TEXT,\n watermark INTEGER NOT NULL,\n fetched_at INTEGER,\n -- When a jump last proved this peer's tmux was not answering. Reader\n -- state, not an event: this node cannot author facts about another\n -- node's agents, and a jump is a local observation, not something the\n -- peer said. Cleared by the next successful collect.\n tmux_down_at INTEGER\n );\n `);\n\n // Additive migration: an existing peers table predates tmux_down_at.\n try {\n database.exec(\"ALTER TABLE peers ADD COLUMN tmux_down_at INTEGER\");\n } catch {\n // Already present.\n }\n\n // Put back the peers the wipe took, at watermark 0 so the next collect\n // re-reads each one from the start.\n if (salvagedPeers.length > 0) {\n const restore = database.prepare(\n `INSERT OR IGNORE INTO peers (name, target, host_id, display_name, watermark, fetched_at)\n VALUES (?, ?, ?, ?, 0, NULL)`,\n );\n for (const peer of salvagedPeers) {\n restore.run(peer.name, peer.target, peer.host_id ?? null, peer.display_name ?? null);\n }\n }\n\n const eventColumns = `\n host_id, seq, ts, agent_id, session, window, pane,\n session_name, window_name, agent_name, pi_session,\n workstream, role, cli, driver, kind, state, message, pid,\n synthetic, reason, extra`;\n const eventPlaceholders = new Array(22).fill(\"?\").join(\", \");\n const insertEvent = database.prepare(\n `INSERT INTO events (${eventColumns}) VALUES (${eventPlaceholders})`,\n );\n const ingestEvent = database.prepare(\n `INSERT OR IGNORE INTO events (${eventColumns}) VALUES (${eventPlaceholders})`,\n );\n const selectMaxSeq = database.prepare(\n \"SELECT COALESCE(MAX(seq), 0) AS seq FROM events WHERE host_id = ?\",\n );\n const append = database.transaction((event: NewEvent): Event => {\n const row = selectMaxSeq.get(identity.host_id) as { seq: number };\n const stored: Event = {\n ...event,\n host_id: identity.host_id,\n seq: row.seq + 1,\n ts: event.ts ?? Date.now(),\n session_name: event.session_name ?? null,\n window_name: event.window_name ?? null,\n agent_name: event.agent_name ?? null,\n pi_session: event.pi_session ?? null,\n };\n insertEvent.run(...eventValues(stored));\n return stored;\n });\n const ingest = database.transaction((events: Event[]): number => {\n let inserted = 0;\n for (const event of events) inserted += ingestEvent.run(...eventValues(event)).changes;\n return inserted;\n });\n\n return {\n append,\n ingest,\n eventsSince(hostId, seq) {\n const rows = database\n .prepare(\"SELECT * FROM events WHERE host_id = ? AND seq > ? ORDER BY seq\")\n .all(hostId, seq) as EventRow[];\n return rows.map(toEvent);\n },\n allEvents() {\n const rows = database\n .prepare(\"SELECT * FROM events ORDER BY ts, host_id, seq\")\n .all() as EventRow[];\n return rows.map(toEvent);\n },\n maxSeq(hostId) {\n return (selectMaxSeq.get(hostId) as { seq: number }).seq;\n },\n prune(horizonMs = Number(process.env.MURMUR_RETENTION_MS ?? DEFAULT_RETENTION_MS)) {\n return database\n .prepare(`\n DELETE FROM events\n WHERE ts < ?\n AND (host_id, seq) NOT IN (\n SELECT host_id, seq FROM (\n SELECT host_id, seq,\n ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY ts DESC, seq DESC) rn\n FROM events\n ) WHERE rn = 1\n )\n `)\n .run(Date.now() - horizonMs).changes;\n },\n peers() {\n return database.prepare(\"SELECT * FROM peers ORDER BY name\").all() as Peer[];\n },\n forgetAgent(agentId) {\n return database.prepare(\"DELETE FROM events WHERE agent_id = ?\").run(agentId).changes;\n },\n forgetHost(hostId) {\n // Every replicated row for one origin node. Only ever called about a\n // REMOTE host: the local host's rows are this node's own authorship and\n // the retention horizon owns them.\n return database.prepare(\"DELETE FROM events WHERE host_id = ?\").run(hostId).changes;\n },\n upsertPeer(peer) {\n const current = database.prepare(\"SELECT * FROM peers WHERE name = ?\").get(peer.name) as\n | Peer\n | undefined;\n database\n .prepare(`\n INSERT INTO peers (name, target, host_id, display_name, watermark, fetched_at, tmux_down_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n target = excluded.target,\n host_id = excluded.host_id,\n display_name = excluded.display_name,\n watermark = excluded.watermark,\n fetched_at = excluded.fetched_at,\n tmux_down_at = excluded.tmux_down_at\n `)\n .run(\n peer.name,\n peer.target,\n peer.host_id !== undefined ? peer.host_id : (current?.host_id ?? null),\n peer.display_name !== undefined ? peer.display_name : (current?.display_name ?? null),\n peer.watermark !== undefined ? peer.watermark : (current?.watermark ?? 0),\n peer.fetched_at !== undefined ? peer.fetched_at : (current?.fetched_at ?? null),\n peer.tmux_down_at !== undefined ? peer.tmux_down_at : (current?.tmux_down_at ?? null),\n );\n },\n removePeer(name) {\n // Drops the peer and its watermark. Replicated events stay: they are\n // real history authored elsewhere, and the retention horizon already\n // ages them out. Re-adding the peer re-syncs from zero, which ingest\n // makes free.\n return database.prepare(\"DELETE FROM peers WHERE name = ?\").run(name).changes > 0;\n },\n close() {\n database.close();\n },\n };\n}\n","import { execFile, execFileSync } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\nconst CONTROL_PATH = \"~/.ssh/control/%r@%h:%p\";\n\n// A peer that is merely unreachable — asleep, off the VPN, a stale address —\n// must not hold up a command. OpenSSH's default TCP connect timeout is the\n// kernel's, which is 75s on macOS; at that point `murmur pick` is unusable and\n// the HUD tick overlaps itself. Two seconds is far above any real handshake on\n// a LAN or a VPN, and a peer that misses it simply shows stale, which is the\n// designed outcome for a host you cannot reach.\nconst CONNECT_TIMEOUT_S = 2;\n\n// Belt and braces for a host that completes the TCP connect and then stops\n// responding — ConnectTimeout does not cover that, and it is how a sleeping\n// laptop behaves. Bounds the whole exchange rather than just the dial.\nconst EXEC_TIMEOUT_MS = 10_000;\n\nconst SSH_OPTIONS = [\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"ControlMaster=no\",\n \"-o\",\n `ControlPath=${CONTROL_PATH}`,\n \"-o\",\n `ConnectTimeout=${CONNECT_TIMEOUT_S}`,\n];\n\nexport interface Channel {\n exec(target: string, argv: string[]): Promise<string>;\n}\n\nexport const ssh: Channel = {\n async exec(target, argv) {\n const { stdout } = await execFileAsync(\"ssh\", [...SSH_OPTIONS, target, ...argv], {\n encoding: \"utf8\",\n timeout: EXEC_TIMEOUT_MS,\n });\n return stdout;\n },\n};\n\nexport function hasWarmSocket(target: string): boolean {\n try {\n execFileSync(\"ssh\", [...SSH_OPTIONS, \"-O\", \"check\", target], { stdio: \"ignore\" });\n return true;\n } catch {\n return false;\n }\n}\n","export type AgentState = \"working\" | \"blocked\" | \"done\" | \"crashed\" | \"cleared\";\n\nexport type Driver = \"human\" | \"orchestrated\";\n\nexport const DEFAULT_DRIVER: Driver = \"human\";\n\nexport type Event = {\n host_id: string;\n seq: number;\n ts: number;\n agent_id: string;\n session: string;\n window: string;\n pane: string;\n // Human-readable names, recorded by the node that owns the pane. tmux ids are\n // stable and are what jumps; names are what a human recognises. They are\n // *recorded* rather than resolved at render time because a reader cannot look\n // a remote window id up in its own tmux -- doing so labelled a remote agent\n // with whatever this host had at that id. Cost: a renamed window keeps its\n // old name until the next event, which is the same property the history rows\n // always had.\n session_name: string | null;\n window_name: string | null;\n // The agent's own idea of what it is working on: pi's session name, and mu's\n // $MU_AGENT_NAME for an orchestrated agent. Both are richer than the window\n // name when they exist, and neither is derivable from tmux.\n agent_name: string | null;\n pi_session: string | null;\n workstream: string | null;\n role: string | null;\n cli: string | null;\n driver: Driver | null;\n kind: string;\n state: AgentState | string;\n message: string;\n pid: number | null;\n synthetic: boolean;\n reason: string;\n extra: Record<string, unknown>;\n};\n\nexport type Peer = {\n name: string;\n target: string;\n host_id: string | null;\n display_name: string | null;\n watermark: number;\n fetched_at: number | null;\n /** When a jump last found this peer's tmux server down. Null once it answers. */\n tmux_down_at: number | null;\n};\n","import { type AgentState, DEFAULT_DRIVER, type Driver, type Event } from \"./types.js\";\n\nexport type LiveCheck = (pid: number) => boolean;\n\nexport type AgentView = {\n agent_id: string;\n host_id: string;\n state: AgentState | null;\n event: Event | null;\n workstream: string | null;\n role: string | null;\n cli: string | null;\n driver: Driver;\n session: string;\n window: string;\n pane: string;\n // Names as recorded by the authoring node, so a remote agent is labelled by\n // its own host's tmux rather than by whatever this host has at that id.\n session_name: string | null;\n window_name: string | null;\n agent_name: string | null;\n pi_session: string | null;\n fetched_at: number | null;\n};\n\nexport function foldAgent(\n events: Event[],\n isAlive: LiveCheck,\n): { state: AgentState | null; event: Event | null } {\n for (let index = events.length - 1; index >= 0; index -= 1) {\n const event = events[index];\n if (!event) continue;\n\n switch (event.state) {\n case \"blocked\":\n case \"done\":\n case \"crashed\":\n return { state: event.state, event };\n case \"cleared\":\n return { state: null, event: null };\n case \"working\":\n return {\n state: event.pid !== null && event.pid > 0 && isAlive(event.pid) ? \"working\" : \"crashed\",\n event,\n };\n }\n }\n\n return { state: null, event: null };\n}\n\nexport function foldAll(events: Event[], isAlive: LiveCheck): AgentView[] {\n const byAgent = new Map<string, Event[]>();\n for (const event of events) {\n const agentEvents = byAgent.get(event.agent_id);\n if (agentEvents) agentEvents.push(event);\n else byAgent.set(event.agent_id, [event]);\n }\n\n return [...byAgent.values()].map((agentEvents) => {\n const folded = foldAgent(agentEvents, isAlive);\n const source = folded.event ?? agentEvents[agentEvents.length - 1];\n if (!source) throw new Error(\"agent event group cannot be empty\");\n\n return {\n agent_id: source.agent_id,\n host_id: source.host_id,\n state: folded.state,\n event: folded.event,\n workstream: source.workstream,\n role: source.role,\n cli: source.cli,\n driver: source.driver ?? DEFAULT_DRIVER,\n session: source.session,\n window: source.window,\n pane: source.pane,\n session_name: source.session_name,\n window_name: source.window_name,\n agent_name: source.agent_name,\n pi_session: source.pi_session,\n fetched_at: null,\n };\n });\n}\n\nconst ATTENTION_ORDER: Record<AgentState, number> = {\n blocked: 0,\n done: 1,\n crashed: 2,\n working: 3,\n cleared: 4,\n};\n\nexport function attentionSort(views: AgentView[]): AgentView[] {\n return [...views].sort((left, right) => {\n const stateOrder =\n (left.state === null ? 4 : ATTENTION_ORDER[left.state]) -\n (right.state === null ? 4 : ATTENTION_ORDER[right.state]);\n if (stateOrder !== 0) return stateOrder;\n return (right.event?.ts ?? 0) - (left.event?.ts ?? 0);\n });\n}\n\nexport function isStale(fetchedAt: number | null, now: number, thresholdMs = 60_000): boolean {\n return fetchedAt !== null && now - fetchedAt > thresholdMs;\n}\n","import { foldAgent, type LiveCheck } from \"./fold.js\";\nimport { ensureIdentity } from \"./identity.js\";\nimport type { Store } from \"./store.js\";\nimport type { Driver, Event } from \"./types.js\";\n\nexport const SCHEMA_VERSION = 2;\n\nexport type Envelope = {\n schema_version: number;\n host_id: string;\n display_name: string;\n exported_at: number;\n};\n\nconst EVENT_FIELDS = new Set([\n \"host_id\",\n \"seq\",\n \"ts\",\n \"agent_id\",\n \"session\",\n \"window\",\n \"pane\",\n \"session_name\",\n \"window_name\",\n \"agent_name\",\n \"pi_session\",\n \"workstream\",\n \"role\",\n \"cli\",\n \"driver\",\n \"kind\",\n \"state\",\n \"message\",\n \"pid\",\n \"synthetic\",\n \"reason\",\n]);\n\nfunction eventToWire(event: Event): Record<string, unknown> {\n const { extra, ...known } = event;\n return { ...extra, ...known };\n}\n\nexport function eventFromWire(wire: Record<string, unknown>): Event {\n const extra = Object.fromEntries(Object.entries(wire).filter(([key]) => !EVENT_FIELDS.has(key)));\n return {\n host_id: wire.host_id as string,\n seq: wire.seq as number,\n ts: wire.ts as number,\n agent_id: wire.agent_id as string,\n session: wire.session as string,\n window: wire.window as string,\n pane: wire.pane as string,\n session_name: (wire.session_name as string | null | undefined) ?? null,\n window_name: (wire.window_name as string | null | undefined) ?? null,\n agent_name: (wire.agent_name as string | null | undefined) ?? null,\n pi_session: (wire.pi_session as string | null | undefined) ?? null,\n workstream: (wire.workstream as string | null | undefined) ?? null,\n role: (wire.role as string | null | undefined) ?? null,\n cli: (wire.cli as string | null | undefined) ?? null,\n driver: (wire.driver as Driver | null | undefined) ?? null,\n kind: wire.kind as string,\n state: wire.state as string,\n message: wire.message as string,\n pid: (wire.pid as number | null | undefined) ?? null,\n synthetic: wire.synthetic as boolean,\n reason: wire.reason as string,\n extra,\n };\n}\n\nfunction synthesizeCrashes(store: Store, hostId: string, isAlive: LiveCheck): void {\n const byAgent = new Map<string, Event[]>();\n for (const event of store.allEvents()) {\n if (event.host_id !== hostId) continue;\n const events = byAgent.get(event.agent_id);\n if (events) events.push(event);\n else byAgent.set(event.agent_id, [event]);\n }\n\n for (const events of byAgent.values()) {\n events.sort((left, right) => left.seq - right.seq);\n const newest = events.at(-1);\n if (\n newest &&\n newest.state === \"working\" &&\n !newest.synthetic &&\n foldAgent(events, isAlive).state === \"crashed\"\n ) {\n const { host_id: _hostId, seq: _seq, ts: _ts, ...event } = newest;\n store.append({ ...event, state: \"crashed\", synthetic: true, reason: \"pid_gone\" });\n }\n }\n}\n\n/**\n * Clear agents whose tmux window is gone.\n *\n * A window that dies takes its agent with it, but the log's newest row still\n * says `blocked`, so every peer keeps showing an agent that cannot be jumped\n * to -- the fold has nothing to supersede that row with. Only the authoring\n * node can tell, which is why this runs on export beside crash synthesis\n * rather than on the reader.\n *\n * `cleared` is the right state: it already means \"no longer wants attention\"\n * and resets the fold to none. An appended event rather than an export-time\n * filter, so the fact replicates once and explains itself, instead of every\n * peer having to re-derive it from an absence.\n */\nexport function clearDeadWindows(store: Store, hostId: string, live: Set<string> | null): void {\n // null means tmux could not answer. An empty set means it did and there are\n // no windows. Conflating them would clear every agent on the host whenever\n // tmux was briefly unreachable.\n if (live === null) return;\n\n const newest = new Map<string, Event>();\n for (const event of store.allEvents()) {\n if (event.host_id !== hostId) continue;\n const previous = newest.get(event.agent_id);\n if (!previous || event.seq > previous.seq) newest.set(event.agent_id, event);\n }\n\n for (const event of newest.values()) {\n if (event.state === \"cleared\") continue;\n if (live.has(event.window)) continue;\n const { host_id: _hostId, seq: _seq, ts: _ts, ...rest } = event;\n store.append({\n ...rest,\n state: \"cleared\",\n synthetic: true,\n reason: \"window_gone\",\n message: \"\",\n });\n }\n}\n\nexport function exportJsonl(\n store: Store,\n since: number,\n isAlive: LiveCheck,\n live?: Set<string> | null,\n): string {\n const identity = ensureIdentity();\n synthesizeCrashes(store, identity.host_id, isAlive);\n if (live !== undefined) clearDeadWindows(store, identity.host_id, live);\n\n const envelope: Envelope = {\n schema_version: SCHEMA_VERSION,\n host_id: identity.host_id,\n display_name: identity.display_name,\n exported_at: Date.now(),\n };\n const lines = [\n JSON.stringify(envelope),\n ...store\n .eventsSince(identity.host_id, since)\n .map((event) => JSON.stringify(eventToWire(event))),\n ];\n return `${lines.join(\"\\n\")}\\n`;\n}\n","import type { Channel } from \"./channel.js\";\nimport { type Envelope, eventFromWire, SCHEMA_VERSION } from \"./export.js\";\nimport type { Store } from \"./store.js\";\nimport type { Event } from \"./types.js\";\n\nexport const COLLECT_INTERVAL_MS = 30_000;\nexport const STALENESS_MS = 2 * COLLECT_INTERVAL_MS;\n\nexport type CollectResult = {\n peer: string;\n ok: boolean;\n ingested: number;\n error?: string;\n};\n\nfunction parseJsonl(output: string): { envelope: Envelope; events: Event[] } {\n const lines = output.trim().split(\"\\n\");\n const envelope = JSON.parse(lines.shift() ?? \"\") as Envelope;\n if (envelope.schema_version > SCHEMA_VERSION) {\n throw new Error(\n `unsupported schema version ${envelope.schema_version} (supports ${SCHEMA_VERSION})`,\n );\n }\n return {\n envelope,\n events: lines.map((line) => eventFromWire(JSON.parse(line) as Record<string, unknown>)),\n };\n}\n\nexport async function collect(\n store: Store,\n channel: Channel,\n now = Date.now(),\n): Promise<CollectResult[]> {\n const results: CollectResult[] = [];\n try {\n for (const peer of store.peers()) {\n try {\n const output = await channel.exec(peer.target, [\n \"murmur\",\n \"export\",\n \"--since\",\n String(peer.watermark),\n ]);\n const { envelope, events } = parseJsonl(output);\n const ingested = store.ingest(events);\n const watermark = events\n .filter((event) => event.host_id === envelope.host_id)\n .reduce((highest, event) => Math.max(highest, event.seq), peer.watermark);\n store.upsertPeer({\n name: peer.name,\n target: peer.target,\n host_id: envelope.host_id,\n display_name: envelope.display_name,\n watermark,\n fetched_at: now,\n // New events mean the node is authoring again, so whatever a jump\n // observed about its tmux is out of date. Only clear on actual new\n // events: an export that returns nothing proves the binary ran, not\n // that tmux is back, which is the distinction that let a dead host\n // look healthy for three hours.\n tmux_down_at: ingested > 0 ? null : peer.tmux_down_at,\n });\n store.prune();\n results.push({ peer: peer.name, ok: true, ingested });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`murmur: collect: peer ${peer.name}: ${message}\\n`);\n results.push({ peer: peer.name, ok: false, ingested: 0, error: message });\n }\n }\n } catch (error) {\n process.stderr.write(\n `murmur: collect: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n }\n return results;\n}\n","import type { Command } from \"commander\";\nimport { ssh } from \"../channel.js\";\nimport { collect } from \"../collector.js\";\nimport { openStore } from \"../store.js\";\n\nexport function registerCollect(program: Command): void {\n program\n .command(\"collect\")\n .description(\"Collect events from configured peers\")\n .action(async () => {\n const store = openStore();\n try {\n await collect(store, ssh);\n } finally {\n store.close();\n }\n });\n}\n","import type { Command } from \"commander\";\nimport { exportJsonl } from \"../export.js\";\nimport { pidAlive, tmux } from \"../mux.js\";\nimport { openStore } from \"../store.js\";\n\nexport function registerExport(program: Command): void {\n program\n .command(\"export\")\n .description(\"Export local events as JSONL\")\n .requiredOption(\"--since <seq>\", \"export events after this sequence\", Number)\n .action((options: { since: number }) => {\n const store = openStore();\n try {\n process.stdout.write(exportJsonl(store, options.since, pidAlive, tmux.liveWindows()));\n } finally {\n store.close();\n }\n });\n}\n","import type { Command } from \"commander\";\nimport { ensureIdentity } from \"../identity.js\";\n\nexport function registerInit(program: Command): void {\n program\n .command(\"init\")\n .description(\"Initialize this node's identity\")\n .option(\"--name <name>\", \"display name\")\n .action((opts: { name?: string }) => {\n const identity = ensureIdentity(opts.name);\n console.log(`host_id: ${identity.host_id}`);\n console.log(`display_name: ${identity.display_name}`);\n });\n}\n","import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Command } from \"commander\";\n\nexport function registerLink(program: Command): void {\n program\n .command(\"link\")\n .description(\"Install a murmur integration\")\n .argument(\"<target>\", \"integration to install\")\n .action((target: string) => {\n if (target !== \"pi\") throw new Error(`unsupported link target: ${target}`);\n const destination = join(\n process.env.MURMUR_PI_HOME ?? homedir(),\n \".pi\",\n \"agent\",\n \"extensions\",\n \"murmur.ts\",\n );\n mkdirSync(dirname(destination), { recursive: true });\n\n // Pin the store import to this installation's absolute path. The\n // extension lives in ~/.pi/agent/extensions, where a bare\n // \"@martintrojer/murmur/extension-store\" specifier cannot resolve — not\n // even for a global install. Unpinned, every append silently no-ops:\n // the tmux badge still paints, so nothing looks broken while the log\n // stays empty and the node exports nothing.\n const source = readFileSync(\n fileURLToPath(new URL(\"./extension/murmur-pi.js\", import.meta.url)),\n \"utf8\",\n );\n const storePath = fileURLToPath(new URL(\"./extension/store.js\", import.meta.url));\n const pinned = source.replace(\n /\"@martintrojer\\/murmur\\/extension-store\"/,\n JSON.stringify(storePath),\n );\n if (pinned === source) {\n throw new Error(\"link pi: could not pin the store import; extension build changed\");\n }\n writeFileSync(destination, pinned);\n console.log(destination);\n });\n}\n","import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { Command } from \"commander\";\nimport { hasWarmSocket, ssh } from \"../channel.js\";\nimport type { Envelope } from \"../export.js\";\nimport { loadIdentity } from \"../identity.js\";\nimport { openStore } from \"../store.js\";\n\nexport function parseSshHosts(config: string): string[] {\n const hosts: string[] = [];\n for (const line of config.split(\"\\n\")) {\n const tokens = line.replace(/#.*$/, \"\").trim().split(/\\s+/);\n if (tokens[0]?.toLowerCase() !== \"host\") continue;\n for (const host of tokens.slice(1)) {\n if (!/[*?!]/.test(host)) hosts.push(host);\n }\n }\n return hosts;\n}\n\nfunction sshHosts(): string[] {\n try {\n return parseSshHosts(readFileSync(join(homedir(), \".ssh\", \"config\"), \"utf8\"));\n } catch {\n return [];\n }\n}\n\nexport function registerPeer(program: Command): void {\n const peer = program.command(\"peer\").description(\"Manage peers\");\n\n peer\n .command(\"add\")\n .description(\"Add a peer and discover its identity\")\n .argument(\"<name>\")\n .argument(\"[target]\")\n .action(async (name: string, target = name) => {\n const store = openStore();\n try {\n // Probe BEFORE writing. Identity is discovered, so the probe is what\n // tells us whether this is a node we already have under another name\n // — and a peer written first would be found by its own duplicate\n // check.\n let envelope: Envelope | null = null;\n try {\n const output = await ssh.exec(target, [\"murmur\", \"export\", \"--since\", \"0\"]);\n envelope = JSON.parse(output.trim().split(\"\\n\")[0] ?? \"\") as Envelope;\n } catch {\n envelope = null;\n }\n\n if (envelope) {\n // Adding yourself would fold your own events back in as a \"remote\"\n // host and collect over ssh to reach a database you already hold.\n if (envelope.host_id === loadIdentity()?.host_id) {\n process.stderr.write(`${target} is this node; not adding it as a peer\\n`);\n process.exitCode = 1;\n return;\n }\n // One node, one peer. Two names for one host_id means two ssh\n // round-trips per command and the same machine listed twice; the\n // events dedupe on (host_id, seq), so nothing looks wrong until you\n // notice every collect is doing double the work.\n const existing = store\n .peers()\n .find((candidate) => candidate.host_id === envelope.host_id && candidate.name !== name);\n if (existing) {\n process.stderr.write(\n `${target} is already configured as peer \"${existing.name}\" ` +\n `(${envelope.display_name}); remove it first to rename\\n`,\n );\n process.exitCode = 1;\n return;\n }\n }\n\n store.upsertPeer({\n name,\n target,\n host_id: envelope?.host_id ?? null,\n display_name: envelope?.display_name ?? null,\n });\n process.stdout.write(\n envelope\n ? `Added ${name} (${envelope.display_name})\\n`\n : `Added ${name} (identity pending)\\n`,\n );\n } finally {\n store.close();\n }\n });\n\n peer\n .command(\"remove\")\n .description(\"Remove a peer\")\n .argument(\"<name>\", \"peer to remove\")\n .action((name: string) => {\n const store = openStore();\n try {\n if (store.removePeer(name)) process.stdout.write(`Removed ${name}\\n`);\n else {\n process.stderr.write(`no such peer: ${name}\\n`);\n process.exitCode = 1;\n }\n } finally {\n store.close();\n }\n });\n\n peer\n .command(\"list\")\n .description(\"List configured peers\")\n .option(\"--json\", \"print JSON\")\n .action((options: { json?: boolean }) => {\n const store = openStore();\n try {\n const peers = store.peers();\n if (options.json) process.stdout.write(`${JSON.stringify(peers)}\\n`);\n else {\n for (const configured of peers) {\n process.stdout.write(\n `${configured.name}\\t${configured.target}\\t${configured.display_name ?? \"unknown\"}\\n`,\n );\n }\n }\n } finally {\n store.close();\n }\n });\n\n peer\n .command(\"discover\")\n .description(\"Check SSH hosts for warm control sockets\")\n .action(() => {\n for (const host of sshHosts()) {\n process.stdout.write(`${hasWarmSocket(host) ? \"[x]\" : \"[ ]\"} ${host}\\n`);\n }\n });\n}\n","import { spawnSync } from \"node:child_process\";\nimport type { Command } from \"commander\";\nimport {\n type Agent,\n agentLabel,\n agentLocation,\n forgetOneAgent,\n jumpToAgent,\n terminalText,\n} from \"../agents.js\";\nimport { glance } from \"../glance.js\";\nimport { loadIdentity } from \"../identity.js\";\nimport { status, statusWithCollect } from \"../status.js\";\nimport { openStore, type Store } from \"../store.js\";\n\ntype PickOptions = { all?: boolean };\n\nconst PREVIEW_EVENTS = 8;\nconst PREVIEW_MESSAGE_MAX = 300;\n\n// Same glyphs the tmux status bar and window labels use, so one symbol means\n// one thing in every surface. Ported from the dotfiles' _tmux_common.\nconst GLYPH: Record<string, string> = {\n crashed: \"\\u2717\", // ✗\n blocked: \"!\",\n done: \"\\u2713\", // ✓\n working: \"\\u25b6\", // ▶\n idle: \"\\u00b7\", // ·\n};\n\n// Mirrors the window-glyph colours: red needs you now, peach needs you soon,\n// teal is finished-unseen, grey is busy or idle and carries no signal.\nconst COLOUR: Record<string, string> = {\n crashed: \"\\u001b[31m\",\n blocked: \"\\u001b[33m\",\n done: \"\\u001b[36m\",\n working: \"\\u001b[37m\",\n idle: \"\\u001b[90m\",\n};\n// Built from a char class rather than written literally: a bare \\u001b in a\n// regex trips biome's noControlCharactersInRegex, and the rule is right that\n// an invisible byte in a pattern is a hazard.\nconst ANSI_PATTERN = `${String.fromCharCode(27)}\\\\[[0-9;]*m`;\nconst ANSI_ESCAPE = new RegExp(ANSI_PATTERN, \"g\");\n// Non-global twin for anchored single matches: `exec` on a /g/ regex carries\n// lastIndex between calls, so reusing ANSI_ESCAPE inside a loop silently skips\n// sequences.\nconst ANSI_AT_START = new RegExp(`^${ANSI_PATTERN}`);\nconst ANSI_AT_END = new RegExp(`(?:${ANSI_PATTERN})+$`);\n// Remote rows get a colour of their own: cyan reads as \"elsewhere\" without\n// competing with the state colours, which own red/peach/teal.\nconst REMOTE = \"\\u001b[36m\";\nconst BOLD = \"\\u001b[1m\";\nconst DIM = \"\\u001b[2m\";\nconst RESET = \"\\u001b[0m\";\n\n// Attention order, and the order the prompt counts appear in.\nconst URGENCY = [\"crashed\", \"blocked\", \"done\", \"working\", \"idle\"] as const;\n\n/**\n * Column widths, in one place because the header and the rows must agree. They\n * were duplicated as literals in two functions and had already drifted by a\n * column once.\n */\nconst COLUMNS = {\n glyph: 3, // marker + state glyph\n state: 8,\n name: 30,\n stream: 13,\n streamWide: 18, // when no host column is shown\n host: 14,\n} as const;\n\n/**\n * The column header fzf pins above the list.\n *\n * Built from COLUMNS so it cannot drift from the rows, and dim so it reads as\n * furniture rather than as an agent.\n */\nexport function headerRow(showHost: boolean): string {\n return [\n \" \".repeat(COLUMNS.glyph),\n pad(\"state\", COLUMNS.state),\n pad(\"agent\", COLUMNS.name),\n pad(\"stream\", showHost ? COLUMNS.stream : COLUMNS.streamWide),\n showHost ? pad(\"host\", COLUMNS.host) : \"\",\n \"age / flags\",\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\n/**\n * State filter keys — an axis kept separate from the text query, so ctrl-b\n * shows blocked agents rather than searching for the word \"blocked\" (which\n * would also match an agent merely *named* that). Inherited wholesale from the\n * old picker, including the choice to shadow fzf defaults: the query here is a\n * word or two, so home/left/bspace still cover the editing jobs.\n */\nconst FILTER_KEYS: [string, string][] = [\n [\"ctrl-a\", \"\"],\n [\"ctrl-x\", \"crashed\"],\n [\"ctrl-b\", \"blocked\"],\n [\"ctrl-d\", \"done\"],\n [\"ctrl-w\", \"working\"],\n];\n\nfunction timestamp(ts: number): string {\n return new Date(ts).toLocaleTimeString([], {\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n}\n\n/**\n * Human age. Blank under a minute: a row that just changed does not need a\n * column saying so, and \"0s\" on every live agent is noise that hides the one\n * row reading \"3h\".\n */\nfunction age(ms: number | null): string {\n if (ms === null || ms < 60_000) return \"\";\n if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;\n if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h`;\n return `${Math.floor(ms / 86_400_000)}d`;\n}\n\n/**\n * Fit a cell to exactly `width` visible columns, padding or truncating.\n *\n * Both halves are needed. Padding counts VISIBLE length, because a value\n * wrapped in bold plus reset carries nine escape bytes and `padEnd` counts\n * them, which pads nine short and shears every column to its right.\n *\n * Truncating is what was missing: `pad` only ever grew a string, so one long\n * agent name (\"Gchatui 2026 Rebaseline Finalization\", 36 chars in a 30-wide\n * column) pushed the host and flags columns right and broke the grid for that\n * row only. Long pi session names are the normal case, not an edge one.\n *\n * The truncation walks the string and copies escape sequences through without\n * counting them, so a cut never lands inside one. Cutting mid-sequence would\n * leak the colour into the rest of the line and drop the reset that ends it.\n */\nfunction pad(value: string, width: number): string {\n const visible = [...value.replace(ANSI_ESCAPE, \"\")].length;\n if (visible <= width) return value + \" \".repeat(width - visible);\n\n // Room for the ellipsis, which is one column wide.\n const budget = Math.max(0, width - 1);\n let out = \"\";\n let shown = 0;\n let index = 0;\n while (index < value.length && shown < budget) {\n const sequence = ANSI_AT_START.exec(value.slice(index));\n if (sequence) {\n out += sequence[0];\n index += sequence[0].length;\n continue;\n }\n out += value[index];\n index += 1;\n shown += 1;\n }\n // Copy any trailing escapes (the reset) so the cell closes its own styling.\n const tail = value.slice(index).match(ANSI_AT_END);\n return `${out}\\u2026${tail?.[0] ?? \"\"}${\" \".repeat(Math.max(0, width - budget - 1))}`;\n}\n\n/**\n * Are we running inside a `display-popup` rather than a pane?\n *\n * tmux exports $TMUX to a popup but not $TMUX_PANE, because a popup is not a\n * pane. Outside tmux neither is set, so the three cases stay distinguishable\n * with no tmux call.\n */\nexport function isPopup(env: NodeJS.ProcessEnv): boolean {\n return Boolean(env.TMUX) && !env.TMUX_PANE;\n}\n\n/**\n * One fzf row: a hidden key column, a hidden filter column, then the label.\n *\n * The key is `agent_id`, not a tmux target: a target only means something on\n * the agent's own host, so resolving it is `jumpToAgent`'s job once a selection\n * comes back.\n */\nexport function pickerRow(agent: Agent, showHost: boolean, current: boolean, local = true): string {\n const state = agent.state ?? \"idle\";\n const colour = COLOUR[state] ?? \"\";\n const glyph = GLYPH[state] ?? \"?\";\n const marker = current ? `${BOLD}\\u25c6${RESET}` : \" \"; // ◆ you are here\n // Richest name first: mu names its agents, pi names its sessions, tmux names\n // windows. All three travel on the event, recorded by the node that owns the\n // pane, so this reads the same for a local and a remote agent.\n const name = agent.agent_name ?? agent.pi_session ?? agentLabel(agent);\n // Local and remote must be tellable apart at a glance. Two hostnames in one\n // dim column means you have to know your own machine's name to read the list\n // — and the difference is not cosmetic: a local row is a keystroke away, a\n // remote one costs an ssh and a nested tmux.\n //\n // \"here\" rather than the local hostname, because the reader already knows\n // which machine they are on; what they need is which rows are not it. Remote\n // hosts keep their name and get an arrow, so the column scans as \"here /\n // elsewhere\" before you read any words.\n // Both forms start in the same column: a leading space where the arrow would\n // be, so \"here\" and \"→ bubba\" line up and the arrows form a single vertical\n // run you can scan without reading a word.\n const host = showHost\n ? local\n ? `${DIM} here${RESET}`\n : `${REMOTE}\\u2192 ${terminalText(agent.host)}${RESET}`\n : \"\";\n // Workstream if mu set one, otherwise the tmux session name. Both answer\n // \"which piece of work is this\", and only mu-spawned agents have a\n // workstream, so the column was empty for most human agents.\n //\n // The session name is also what the tms picker shows and what you have\n // trained yourself to search on: a session called `hacking/murmur` holding a\n // pi whose window is named `Python` was unfindable by typing `murmur`. This\n // is the one thing tms had that murmur did not, and folding whole sessions\n // into this list was the wrong way to get it -- a session without an agent\n // has no place here.\n const group = agent.workstream ?? agent.session_name;\n const workstream = group ? `${DIM}${terminalText(group)}${RESET}` : \"\";\n // Two ages, and the one worth showing is how old the AGENT'S news is, not\n // how recently we reached its host. A peer we polled a second ago can be\n // serving events from three hours back — which read as fresh until this\n // column existed. `unreachable` is the other axis: the replica itself is old.\n const flags = [\n agent.driver === \"orchestrated\" ? \"crew\" : \"\",\n agent.stale ? \"unreachable\" : \"\",\n // A jump already proved this one dead. Say so plainly rather than leaving\n // the row looking merely old, and sort it last.\n agent.tmux_down ? \"no tmux\" : \"\",\n age(agent.event_age_ms),\n ]\n .filter(Boolean)\n .join(\" \");\n // The state word is IN the label, not a hidden column. fzf's --with-nth\n // re-indexes fields, so any --nth that excluded the label broke plain\n // name matching (typing \"glance\" returned 0/4). Keeping state visible costs\n // eight columns and makes both the ctrl-key filters and text search work on\n // one field set — and the word is worth reading anyway.\n const label = [\n `${marker} ${colour}${glyph}${RESET}`,\n `${colour}${pad(state, COLUMNS.state)}${RESET}`,\n pad(`${BOLD}${terminalText(name)}${RESET}`, COLUMNS.name),\n pad(workstream, showHost ? COLUMNS.stream : COLUMNS.streamWide),\n showHost ? pad(host, COLUMNS.host) : \"\",\n flags ? `${DIM}${flags}${RESET}` : \"\",\n ]\n .filter(Boolean)\n .join(\" \");\n return `${agent.agent_id}\\t${label}`;\n}\n\nfunction previewText(store: Store, agent: Agent): string {\n const state = agent.state ?? \"idle\";\n const colour = COLOUR[state] ?? \"\";\n const head = [\n `${colour}${GLYPH[state] ?? \"?\"} ${state}${RESET} ${BOLD}${agent.agent_name ? terminalText(agent.agent_name) : agentLabel(agent)}${RESET}`,\n // Says where, and whether \"where\" is this machine. The glance below is a\n // local capture-pane or an ssh depending on this one fact, so it belongs in\n // the header rather than being inferred from a hostname.\n agent.host_id === loadIdentity()?.host_id\n ? `${DIM}here ${agentLocation(agent)}${RESET}`\n : `${REMOTE}\\u2192 ${terminalText(agent.host)}${RESET} ${DIM}${agentLocation(agent)}${RESET}`,\n ];\n const facts = [\n agent.workstream ? `stream ${terminalText(agent.workstream)}` : \"\",\n agent.role ? `role ${terminalText(agent.role)}` : \"\",\n agent.pi_session ? `session ${terminalText(agent.pi_session)}` : \"\",\n agent.driver === \"orchestrated\" ? \"driver orchestrated (crew)\" : \"\",\n agent.stale ? `fetched ${age(agent.age_ms)} ago` : \"\",\n ].filter(Boolean);\n\n // The glance is the point of the preview: what is the agent actually doing.\n // Events are history and answer a different question, so they go underneath\n // and stay short.\n const pane = glance(store, agent);\n const live = pane?.trimEnd()\n ? [`${DIM}── pane ──${RESET}`, pane.trimEnd()]\n : [`${DIM}── pane ──${RESET}`, `${DIM}unavailable (host unreachable, or pane gone)${RESET}`];\n\n const events = store\n .allEvents()\n .filter((event) => event.agent_id === agent.agent_id)\n .slice(-PREVIEW_EVENTS);\n const history = events.length\n ? events.map((event) => {\n let message = terminalText(event.message);\n if (message.length > PREVIEW_MESSAGE_MAX) {\n message = `${message.slice(0, PREVIEW_MESSAGE_MAX)}…`;\n }\n const detail = message && message !== event.state ? ` ${message}` : \"\";\n return `${DIM}${timestamp(event.ts)}${RESET} ${terminalText(event.state).padEnd(8)}${detail}`;\n })\n : [`${DIM}no recorded events${RESET}`];\n\n return [...head, \"\", ...facts, \"\", ...live, \"\", `${DIM}── history ──${RESET}`, ...history].join(\n \"\\n\",\n );\n}\n\n/**\n * Emit the preview body for one agent. `murmur pick` re-invokes itself here so\n * fzf's `--preview` has a per-row command, rather than the picker precomputing\n * every preview up front — which would mean an ssh round-trip per remote agent\n * before the list even paints.\n */\nexport function runPreview(store: Store, agentId: string): void {\n // Runs as a child of a picker that has just collected, so it reads the store\n // directly rather than syncing again.\n const agent = status(store).agents.find((candidate) => candidate.agent_id === agentId);\n if (!agent) return;\n process.stdout.write(`${previewText(store, agent)}\\n`);\n}\n\nexport async function runPick(store: Store, options: PickOptions = {}): Promise<void> {\n const identity = loadIdentity();\n const view = await statusWithCollect(store);\n const agents = view.agents.filter((agent) => options.all || agent.driver === \"human\");\n const hidden = view.agents.length - agents.length;\n\n if (agents.length === 0) {\n process.stdout.write(\n hidden ? `No human agents (+${hidden} crew — rerun with --all)\\n` : \"No agents\\n\",\n );\n return;\n }\n\n const showHost = agents.some((agent) => agent.host_id !== identity?.host_id);\n const currentPane = process.env.TMUX_PANE ?? \"\";\n const input = agents\n .map((agent) =>\n pickerRow(agent, showHost, agent.pane === currentPane, agent.host_id === identity?.host_id),\n )\n .join(\"\\n\");\n\n const counts = new Map<string, number>();\n for (const agent of agents) {\n const state = agent.state ?? \"idle\";\n counts.set(state, (counts.get(state) ?? 0) + 1);\n }\n const prompt = URGENCY.filter((state) => counts.get(state))\n .map((state) => `${COLOUR[state]}${GLYPH[state]}${counts.get(state)}${RESET}`)\n .join(\" \");\n\n const self = process.argv[1] ?? \"murmur\";\n const allFlag = options.all ? \" --all\" : \"\";\n const inPopup = isPopup(process.env);\n // A preview beside the list needs room for both. Below ~150 columns the\n // 58% split squeezes the host and flags columns off the end, so start\n // stacked and let ctrl-p cycle from there.\n const width = process.stdout.columns ?? 0;\n const previewLayout =\n width > 0 && width < 150 ? \"bottom:60%,border-top,wrap\" : \"right:58%,border-left,wrap\";\n const preview = `${process.execPath} ${self} pick --preview {1}`;\n // Narrow on the hidden state column with an exact-prefix query, then restore\n // the real query. ctrl-a clears it.\n const filterBinds = FILTER_KEYS.flatMap(([key, state]) => [\n \"--bind\",\n state ? `${key}:change-query(${state})` : `${key}:change-query()`,\n ]);\n\n const result = spawnSync(\n \"fzf\",\n [\n \"--delimiter\",\n \"\\t\",\n \"--with-nth\",\n \"2..\",\n \"--ansi\",\n // Literal substring matching, and matching only the visible columns.\n // Default fuzzy scatters query characters across the row: `re` matched\n // \"Fix Murmur Pick Fzf Filter\" as well as \"recovered\". A query here is a\n // word or two of an agent or workstream name, so substring is what the\n // fingers expect. Prefix a token with ' to opt back into fuzzy.\n // Same choice as the tms session picker, for consistency across the two.\n \"--exact\",\n // `begin` ranks earlier match positions higher, so `scratch` puts the\n // scratch workstream above a row that merely mentions it. `index` is the\n // empty-query fallback and preserves the attention order the fold\n // produced, which is the whole point of the list.\n \"--tiebreak\",\n \"begin,index\",\n \"--layout\",\n \"reverse\",\n // `display-popup` draws its own border, so fzf's is a second one a\n // character inside the first. A popup is the normal way to run this, via\n // the prefix+a binding, so the doubled frame was what you saw most.\n //\n // Detected by $TMUX set with $TMUX_PANE unset: tmux exports TMUX to a\n // popup but not TMUX_PANE, since a popup is not a pane. Outside tmux\n // neither is set, so the three cases stay distinguishable.\n \"--border\",\n inPopup ? \"none\" : \"rounded\",\n \"--info\",\n \"inline\",\n \"--prompt\",\n `${prompt}${prompt ? \" \" : \"\"}`,\n \"--header\",\n [\n `enter jump ^r refresh ^p preview del forget filter: ${FILTER_KEYS.map(\n ([key, state]) => `${key.replace(\"ctrl-\", \"^\")} ${state || \"all\"}`,\n ).join(\" \")}`,\n hidden ? `${hidden} crew hidden (--all)` : \"\",\n headerRow(showHost),\n ]\n .filter(Boolean)\n .join(\"\\n\"),\n \"--preview\",\n preview,\n // Narrow terminals cannot show both the columns and a 58% preview, and\n // the columns are the point of the list. ctrl-p cycles right / bottom /\n // hidden, so every column is reachable on a small viewport without\n // giving up the glance entirely.\n \"--preview-window\",\n previewLayout,\n \"--bind\",\n \"ctrl-p:change-preview-window(bottom:60%,border-top,wrap|hidden|right:58%,border-left,wrap)\",\n \"--bind\",\n `ctrl-r:reload(${process.execPath} ${self} pick --rows${allFlag})`,\n // Manual dismissal for a row nothing else will clear.\n //\n // The delete key, not a ctrl chord. ctrl-shift-d does not exist -- a\n // terminal sends the same bytes as ctrl-d -- and ctrl-alt-d, while it\n // does dispatch distinctly, sits one modifier away from ctrl-d in a\n // header that lists both. One is a filter and the other destroys a row,\n // so a near-miss is a deleted agent. `delete` is the key that already\n // means remove this, and it collides with no filter letter.\n \"--bind\",\n `delete:reload(${process.execPath} ${self} pick --forget {1}${allFlag})`,\n ...filterBinds,\n \"--no-select-1\",\n \"--no-exit-0\",\n ],\n {\n input,\n encoding: \"utf8\",\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n // FZF_DEFAULT_OPTS can carry a conflicting layout or bindings from the\n // user's shell; the old picker stripped it for the same reason.\n env: Object.fromEntries(\n Object.entries(process.env).filter(([key]) => !key.startsWith(\"FZF_DEFAULT_OPTS\")),\n ),\n },\n );\n\n const selected = result.stdout?.trim().split(\"\\t\")[0];\n if (!selected) return;\n const agent = agents.find((candidate) => candidate.agent_id === selected);\n if (!agent) return;\n const jump = jumpToAgent(store, agent);\n // A popup closes the moment this returns, so a bare failure looked exactly\n // like \"enter did nothing\". Say what happened and fail loudly.\n if (!jump.ok) {\n process.stderr.write(`${jump.message}\\n`);\n process.exitCode = 1;\n }\n}\n\n/**\n * Delete one agent, then print the remaining rows.\n *\n * One command rather than two because fzf's `reload` replaces the list with a\n * command's stdout: doing the delete and the reprint separately would race the\n * reload against the delete and redraw the row it had just removed.\n */\nexport async function runForget(\n store: Store,\n agentId: string,\n options: PickOptions = {},\n): Promise<void> {\n const view = status(store);\n const agent = view.agents.find((candidate) => candidate.agent_id === agentId);\n if (agent) forgetOneAgent(store, agent);\n await runRows(store, options);\n}\n\n/** Print the row list only, for fzf's `reload` binding. */\nexport async function runRows(store: Store, options: PickOptions = {}): Promise<void> {\n const identity = loadIdentity();\n const view = await statusWithCollect(store);\n const agents = view.agents.filter((agent) => options.all || agent.driver === \"human\");\n const showHost = agents.some((agent) => agent.host_id !== identity?.host_id);\n const currentPane = process.env.TMUX_PANE ?? \"\";\n for (const agent of agents) {\n process.stdout.write(\n `${pickerRow(agent, showHost, agent.pane === currentPane, agent.host_id === identity?.host_id)}\\n`,\n );\n }\n}\n\nexport function registerPick(program: Command): void {\n program\n .command(\"pick\")\n .description(\"Pick an agent and jump to it\")\n .option(\"--all\", \"include orchestrated agents\")\n .option(\"--preview <agent-id>\", \"render the preview pane for one agent (internal)\")\n .option(\"--rows\", \"print picker rows only (internal, for reload)\")\n .option(\"--forget <agent-id>\", \"drop one agent, then print rows (internal)\")\n .action(\n async (options: PickOptions & { preview?: string; rows?: boolean; forget?: string }) => {\n const store = openStore();\n try {\n if (options.preview) runPreview(store, options.preview);\n else if (options.forget) await runForget(store, options.forget, options);\n else if (options.rows) await runRows(store, options);\n else await runPick(store, options);\n } finally {\n store.close();\n }\n },\n );\n}\n","import { spawnSync } from \"node:child_process\";\nimport { loadIdentity } from \"./identity.js\";\nimport { type Mux, tmux } from \"./mux.js\";\nimport type { Status } from \"./status.js\";\nimport type { Store } from \"./store.js\";\n\nexport type Agent = Status[\"agents\"][number];\n\n/**\n * The most specific human-readable name an agent has, never a tmux id.\n *\n * Four sources, most to least specific: mu's agent name, pi's session name,\n * the tmux window name, the tmux session name. The old picker showed window\n * names and that was the thing it did better than raw `$26:@79`; these are all\n * recorded on the event, so this reads the same for a local and a remote agent.\n *\n * Falls back to the window id only when a node recorded no names at all, which\n * means a pre-names event or a non-tmux harness.\n */\nexport function agentLabel(agent: Agent): string {\n const name = agent.agent_name ?? agent.pi_session ?? agent.window_name ?? agent.session_name;\n return terminalText(name ?? agent.window);\n}\n\n/**\n * Where the agent lives, for the second column. Names only -- the ids are what\n * jumps, not what a human reads.\n */\nexport function agentLocation(agent: Agent): string {\n const session = agent.session_name ?? agent.session;\n const window = agent.window_name ?? agent.window;\n return terminalText(session === window ? session : `${session}:${window}`);\n}\n\nexport function terminalText(value: string): string {\n return [...value]\n .map((character) => {\n const code = character.charCodeAt(0);\n return code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f) ? \"�\" : character;\n })\n .join(\"\");\n}\n\nexport function shellQuote(value: string): string {\n return `'${value.replaceAll(\"'\", `'\\\\''`)}'`;\n}\n\nexport type JumpResult =\n | { ok: true }\n | { ok: false; reason: \"no_peer\" | \"unreachable\" | \"no_tmux\" | \"window_gone\"; message: string };\n\n/**\n * Drop a dead agent's rows from the local replica.\n *\n * Export on the authoring node clears dead windows, but that only runs when the\n * peer is next polled, and a window can die between a fetch and a jump. When a\n * jump proves the window is gone, the agent should leave this HUD now rather\n * than at the next collect.\n *\n * DELETE rather than append a `cleared` event, because this node cannot author\n * an event about another node's agent. `store.append` stamps the local host_id,\n * and `status()` folds local and remote events separately (local needs a pid\n * check, remote cannot have one) -- so a local row about a remote agent lands\n * in the other fold and shows up as a SECOND agent with the same agent_id,\n * which is exactly what it did before this was a delete.\n *\n * Deleting a replica is safe ONLY IF the rows can come back, and that needs the\n * peer's watermark rewound as well. Ingest asks for events after the watermark,\n * so deleting rows below it deletes them permanently: bubba's agents vanished\n * from the picker and no amount of collecting brought them back, even with the\n * node alive and the events still in its log.\n *\n * Rewinding to zero rather than to the deleted seq: the log is bounded by the\n * retention horizon, ingest is idempotent on (host_id, seq), and a re-read of a\n * small table is cheaper than tracking which seq belonged to which agent. The\n * next collect re-reads everything the peer still has, so if the window is\n * genuinely alive the agent reappears -- which is the answer to the race where\n * the host comes back up between the jump and the next poll.\n *\n * For a local agent there is no watermark and nothing to rewind: the pane is\n * gone, so nothing will ever author about it again.\n */\n/**\n * A jump proved this peer has no tmux server, so none of its agents exist.\n *\n * Drops every replicated row for that origin and rewinds the watermark, the\n * same recoverable delete `forgetReplica` does for one agent — just scoped to\n * the node, because \"no tmux server\" is a fact about the host rather than about\n * the window we happened to aim at. Leaving the rows and only labelling them\n * meant the picker kept offering four dead agents you had just been told were\n * gone.\n *\n * The mark stays on the peer as well: it is what stops an empty export being\n * read as recovery, and it is why the rows do not immediately reappear.\n */\nexport function forgetHostReplica(store: Store, hostId: string): void {\n try {\n const peer = store.peers().find((candidate) => candidate.host_id === hostId);\n store.forgetHost(hostId);\n if (peer) {\n // Watermark deliberately NOT rewound here, unlike the single-agent case.\n // Rewinding re-ingests the very rows just deleted, and because the\n // collector reads any ingest as \"the node is authoring again\", it also\n // cleared the mark -- so the dead agents reappeared looking healthy on\n // the next collect, one second later.\n //\n // Keeping the watermark means recovery waits for a NEW event, which is\n // the correct bar: the node has to actually say something before its\n // agents come back. Nothing is lost, since the rows describe windows a\n // live tmux server would re-announce.\n store.upsertPeer({\n name: peer.name,\n target: peer.target,\n tmux_down_at: Date.now(),\n });\n }\n } catch {\n // Advisory only: the next collect reconciles either way.\n }\n}\n\nexport function forgetReplica(store: Store, agentId: string, hostId: string): void {\n try {\n store.forgetAgent(agentId);\n const peer = store.peers().find((candidate) => candidate.host_id === hostId);\n if (peer) store.upsertPeer({ name: peer.name, target: peer.target, watermark: 0 });\n } catch {\n // Cosmetic only: the next collect reconciles either way.\n }\n}\n\n/**\n * Drop one agent from the picker by hand.\n *\n * The escape hatch for a row that is stuck and that nothing else will clear: an\n * agent whose pane died in a way that left no terminal event, or a replica from\n * a peer that will never report again. Everything else here reconciles on its\n * own, so this exists for the cases that do not.\n *\n * A local agent also gets its tmux badge cleared. Deleting only the row would\n * leave `@agent_state` set, which the status bar and the tms session picker\n * both read — so the glyph would survive the row it came from and nothing would\n * ever clear it.\n *\n * Not authoritative, and cannot be: for a remote agent this deletes a replica,\n * and the owning node still holds the truth. If that node reports again the\n * agent comes back, which is correct — a row you dismissed while the agent was\n * alive should return.\n */\nexport function forgetOneAgent(store: Store, agent: Agent, mux: Mux = tmux): void {\n const identity = loadIdentity();\n if (agent.host_id === identity?.host_id) {\n try {\n mux.setState(agent.window, null);\n } catch {\n // Best effort: the row still goes.\n }\n }\n forgetReplica(store, agent.agent_id, agent.host_id);\n}\n\nexport function jumpToAgent(store: Store, agent: Agent): JumpResult {\n const identity = loadIdentity();\n if (agent.host_id === identity?.host_id) {\n const live = tmux.liveWindows();\n if (live && !live.has(agent.window)) {\n forgetReplica(store, agent.agent_id, agent.host_id);\n return {\n ok: false,\n reason: \"window_gone\",\n message: `${agentLabel(agent)} is gone -- its window no longer exists. Cleared.`,\n };\n }\n tmux.attach(agent.session, agent.window);\n return { ok: true };\n }\n const peer = store.peers().find((candidate) => candidate.host_id === agent.host_id);\n const target = peer?.target ?? peer?.name;\n if (!target) {\n return {\n ok: false,\n reason: \"no_peer\",\n message: `No peer configured for host ${agent.host_id.slice(0, 8)}. Try: murmur peer add <target>`,\n };\n }\n\n // Check the window is still there before opening a window to attach to it.\n // Without this the attach fails inside a new tmux window that closes\n // instantly, which is indistinguishable from \"enter did nothing\" -- the\n // symptom that sent us looking for a quoting bug that did not exist.\n // ssh does not take an argv: it joins its arguments and hands the string to a\n // shell on the far side. An unquoted `#{window_id}` is mangled by that shell\n // and tmux answers `-F expects an argument`, which looked exactly like an\n // unreachable host. One quoted string, so the remote shell passes the format\n // through untouched.\n const probe = spawnSync(\n \"ssh\",\n [\"-o\", \"BatchMode=yes\", target, `tmux list-windows -a -F ${shellQuote(\"#{window_id}\")}`],\n { encoding: \"utf8\", timeout: 10_000 },\n );\n if (probe.status !== 0) {\n // 255 is ssh's own failure code; anything else came from the remote\n // command. Conflating them was wrong in the common case: with a warm\n // ControlMaster socket the host answers instantly and it is tmux that is\n // gone, so \"unreachable\" sent you looking at the network for a problem that\n // was not there.\n const sshFailed = probe.status === 255 || probe.error !== undefined;\n if (sshFailed) {\n // No mark: we learned nothing about the peer's tmux, only that we could\n // not ask. Its agents may be perfectly alive behind a cold socket or a\n // sleeping laptop, and deleting them here would be guessing.\n return {\n ok: false,\n reason: \"unreachable\",\n message: `cannot reach ${target} over ssh. The collector never prompts for auth, so connect once by hand to warm the connection, then retry.`,\n };\n }\n\n // ssh worked, tmux did not. That is a real fact about the host and the\n // strongest one available: a successful export only proves the murmur\n // binary ran, which it does happily on a box whose tmux server is gone --\n // which is why these agents read as fresh for three hours.\n forgetHostReplica(store, agent.host_id);\n return {\n ok: false,\n reason: \"no_tmux\",\n message: `${target} has no tmux server running, so its agents are gone. Removed them; they will come back when it reports again.`,\n };\n }\n const remoteWindows = new Set((probe.stdout ?? \"\").split(\"\\n\").filter(Boolean));\n if (!remoteWindows.has(agent.window)) {\n forgetReplica(store, agent.agent_id, agent.host_id);\n return {\n ok: false,\n reason: \"window_gone\",\n message: `${agentLabel(agent)} is gone -- ${target} no longer has that window. Cleared.`,\n };\n }\n\n const attachTarget = shellQuote(`${agent.session}:${agent.window}`);\n\n // Hand the ssh to tmux as its own window rather than running it here.\n // `murmur pick` is usually a display-popup, and a popup is modal: an ssh\n // session started inside it is killed the moment the picker exits, so the\n // remote pane flashed and vanished. A new window outlives the popup and\n // gives the remote tmux a real terminal to attach to.\n //\n // Nested tmux is the known cost here (see the spec's open question on inner\n // prefixes); a window at least makes it visible and closable.\n if (process.env.TMUX) {\n // `tmux new-window <command>` runs the command through a shell, so the\n // string is expanded LOCALLY before ssh sees it. A tmux session id is\n // always `$N`, so `$0:@6` arrived as `:@6` and the remote attach failed\n // with \"can't find session\". shellQuote alone is not enough: it protects\n // the remote shell, this protects the local one.\n const command = `ssh -t ${shellQuote(target)} tmux attach -t ${shellQuote(attachTarget)}`;\n const name = `@${peer?.display_name ?? target}`;\n\n // Reuse an existing window for this host rather than stacking a new one on\n // every jump. murmur navigates to agents; the window is only here because a\n // remote attach needs a terminal that outlives the popup, so one per host is\n // the whole requirement. Jumping to bubba three times used to leave three\n // identical @bubba windows behind.\n //\n // Matched on window name, which is the only handle available: the ssh is\n // opaque from here, and the remote session id is not a local address.\n const existing = tmux.windowNamed(name);\n if (existing) {\n tmux.selectWindow(existing);\n return { ok: true };\n }\n\n spawnSync(\"tmux\", [\"new-window\", \"-n\", name, command], { stdio: \"ignore\" });\n return { ok: true };\n }\n\n // Outside tmux there is no popup to escape, so run it directly.\n spawnSync(\"ssh\", [\"-t\", target, \"tmux\", \"attach\", \"-t\", attachTarget], { stdio: \"inherit\" });\n return { ok: true };\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { Agent } from \"./agents.js\";\nimport { loadIdentity } from \"./identity.js\";\nimport { tmux } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\n\n/**\n * Glance: the last few lines a pane printed.\n *\n * This is the cheap half of the two things \"render any pane from the master\"\n * hides. It is a stateless `capture-pane`, not a frame stream — no resize\n * negotiation, no input routing, no reconnect. That deferral is what keeps\n * murmur a state layer instead of a multiplexer (DESIGN-NOTES, \"Deferring\n * interactive remote rendering\"), and it is why this file is thirty lines\n * rather than most of herdr.\n */\n\nconst GLANCE_LINES = 40;\n\n// Same posture as the collector: ride a warm socket or fail fast, never\n// prompt. A preview pane must not trigger a yubikey touch on every keypress.\nconst SSH_OPTIONS = [\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"ControlMaster=no\",\n \"-o\",\n \"ControlPath=~/.ssh/control/%r@%h:%p\",\n \"-o\",\n \"ConnectTimeout=2\",\n];\n\nexport function glance(store: Store, agent: Agent, lines = GLANCE_LINES): string | null {\n if (agent.host_id === loadIdentity()?.host_id) return tmux.capture(agent.pane, lines);\n\n const peer = store.peers().find((candidate) => candidate.host_id === agent.host_id);\n const target = peer?.target ?? peer?.name;\n if (!target) return null;\n try {\n // The pane id is `%N`, which a remote shell leaves alone, but quote it\n // anyway: the same class of bug as the `$N` session id that made remote\n // jump fail silently for a day.\n return execFileSync(\n \"ssh\",\n [\n ...SSH_OPTIONS,\n target,\n \"tmux\",\n \"capture-pane\",\n \"-p\",\n \"-t\",\n `'${agent.pane}'`,\n \"-S\",\n `-${lines}`,\n ],\n { encoding: \"utf8\", timeout: 3000, stdio: [\"ignore\", \"pipe\", \"ignore\"] },\n );\n } catch {\n // Unreachable, cold socket, dead tmux, gone pane. The preview says so\n // rather than the picker failing.\n return null;\n }\n}\n","import { ssh } from \"./channel.js\";\nimport { collect, STALENESS_MS } from \"./collector.js\";\nimport { type AgentView, attentionSort, foldAll, isStale } from \"./fold.js\";\nimport { loadIdentity } from \"./identity.js\";\nimport { pidAlive } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\n\ntype StatusState = \"working\" | \"blocked\" | \"done\" | \"crashed\" | \"idle\";\ntype Counts = Record<StatusState, number>;\n\nexport type Status = {\n counts: Counts;\n orchestrated_counts: Counts;\n agents: (AgentView & {\n stale: boolean;\n age_ms: number | null;\n event_age_ms: number | null;\n tmux_down: boolean;\n host: string;\n })[];\n peers: {\n name: string;\n display_name: string | null;\n fetched_at: number | null;\n stale: boolean;\n }[];\n};\n\nfunction emptyCounts(): Counts {\n return { working: 0, blocked: 0, done: 0, crashed: 0, idle: 0 };\n}\n\nexport function tmuxStatus(view: Status): string {\n const urgency: StatusState[] = [\"crashed\", \"blocked\", \"done\", \"working\", \"idle\"];\n return urgency\n .filter((state) => view.counts[state] > 0)\n .map((state) => `${state}\\t${view.counts[state]}\\n`)\n .join(\"\");\n}\n\n/**\n * Fold the current view. Pure with respect to the network: the caller decides\n * whether to collect first (see `statusWithCollect`).\n */\nexport function status(store: Store, now = Date.now()): Status {\n const identity = loadIdentity();\n const peers = store.peers();\n const peersByHost = new Map(\n peers.flatMap((peer) => (peer.host_id === null ? [] : [[peer.host_id, peer] as const])),\n );\n const events = store.allEvents();\n const local = foldAll(\n events.filter((event) => event.host_id === identity?.host_id),\n pidAlive,\n );\n const remote = foldAll(\n events.filter((event) => event.host_id !== identity?.host_id),\n () => true,\n );\n const counts = emptyCounts();\n const orchestratedCounts = emptyCounts();\n const agents = attentionSort([...local, ...remote]).map((agent) => {\n const peer = peersByHost.get(agent.host_id);\n const fetchedAt = peer?.fetched_at ?? null;\n const state: StatusState =\n agent.state === null || agent.state === \"cleared\" ? \"idle\" : agent.state;\n const target = agent.driver === \"human\" ? counts : orchestratedCounts;\n target[state] += 1;\n return {\n ...agent,\n fetched_at: fetchedAt,\n // Replica freshness: how long since we last reached the peer. Local rows\n // have no fetched_at and are never stale.\n stale: isStale(fetchedAt, now, STALENESS_MS),\n age_ms: fetchedAt === null ? null : now - fetchedAt,\n // Information age: how long since the agent itself said anything. This\n // is the number a human means by \"how stale is that row\". A successful\n // fetch of a three-hour-old event resets age_ms to zero but leaves this\n // at three hours, which is why they cannot be the same field.\n event_age_ms: agent.event === null ? null : Math.max(0, now - agent.event.ts),\n // A jump proved this host's tmux was down and nothing has authored since.\n // Stronger than staleness: the host answers, its agents are just gone.\n tmux_down: peer?.tmux_down_at != null,\n host:\n peer?.display_name ??\n peer?.name ??\n (agent.host_id === identity?.host_id ? identity.display_name : agent.host_id),\n };\n });\n\n return {\n counts,\n orchestrated_counts: orchestratedCounts,\n agents,\n peers: peers.map((peer) => ({\n name: peer.name,\n display_name: peer.display_name,\n fetched_at: peer.fetched_at,\n // A peer we have never reached is stale, not fresh. `isStale` reads a\n // null `fetched_at` as \"local, therefore never stale\", which is right\n // for an agent row but backwards for a peer: null there means the very\n // first collect has not succeeded yet. Left to `isStale`, an\n // unreachable host you just added would render as up to date.\n stale: peer.fetched_at === null || isStale(peer.fetched_at, now, STALENESS_MS),\n })),\n };\n}\n\n/**\n * Collect from peers, then fold. This is what every user-facing surface wants:\n * the view reflects the sync that just ran, rather than the one before it.\n *\n * Awaiting matters for two reasons. A fire-and-forget collect makes every\n * invocation show data one run stale — you never see what you just fetched.\n * And the callers close the store in a `finally`, so a collect still in flight\n * lands on a closed handle and reports \"The database connection is not open\",\n * which looks like corruption rather than a race.\n *\n * Sync must never fail a command, so a peer failure only warns. With no peers\n * this is a loop over an empty array: no network, no added latency, which is\n * the everyday single-machine path.\n */\nexport async function statusWithCollect(store: Store, now = Date.now()): Promise<Status> {\n try {\n await collect(store, ssh, now);\n } catch (error) {\n process.stderr.write(\n `murmur: status: collect: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n }\n return status(store, now);\n}\n","import type { Command } from \"commander\";\nimport { statusWithCollect, tmuxStatus } from \"../status.js\";\nimport { openStore } from \"../store.js\";\n\nexport function registerStatus(program: Command): void {\n program\n .command(\"status\")\n .description(\"Show folded agent status\")\n .option(\"--json\", \"print JSON\")\n .action(async (options: { json?: boolean }) => {\n const store = openStore();\n try {\n const view = await statusWithCollect(store);\n process.stdout.write(\n options.json ? `${JSON.stringify(view, null, 2)}\\n` : tmuxStatus(view),\n );\n } finally {\n store.close();\n }\n });\n}\n","// SDK entry. package.json advertises this as the \".\" export, so anything a\n// consumer needs to drive murmur without shelling out to the CLI belongs here.\n// The CLI is a thin layer over exactly these units.\n// Read from the manifest rather than restated here: the version lived in\n// package.json and in this file, and two copies of one fact drift. npm bumps\n// the manifest, so the manifest is the source.\nimport { createRequire } from \"node:module\";\n\nconst manifest = createRequire(import.meta.url)(\"../package.json\") as { version: string };\nexport const VERSION: string = manifest.version;\n\nexport {\n type Agent,\n agentLabel,\n agentLocation,\n type JumpResult,\n jumpToAgent,\n shellQuote,\n} from \"./agents.js\";\nexport { type Channel, hasWarmSocket, ssh } from \"./channel.js\";\nexport {\n COLLECT_INTERVAL_MS,\n type CollectResult,\n collect,\n STALENESS_MS,\n} from \"./collector.js\";\nexport { eventFromWire, exportJsonl, SCHEMA_VERSION } from \"./export.js\";\nexport {\n type AgentView,\n attentionSort,\n foldAgent,\n foldAll,\n isStale,\n type LiveCheck,\n} from \"./fold.js\";\nexport { glance } from \"./glance.js\";\nexport { ensureIdentity, loadIdentity, type NodeIdentity } from \"./identity.js\";\nexport { type Mux, pidAlive, tmux } from \"./mux.js\";\nexport { configDir, dbPath, stateDir } from \"./paths.js\";\nexport { type Status, status } from \"./status.js\";\nexport { type NewEvent, openStore, STORE_VERSION, type Store } from \"./store.js\";\nexport {\n type AgentState,\n DEFAULT_DRIVER,\n type Driver,\n type Event,\n type Peer,\n} from \"./types.js\";\n"],"mappings":"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","names":["Database","join","join","Database","program","execFileSync","program","program","program","mkdirSync","readFileSync","writeFileSync","homedir","join","program","readFileSync","homedir","join","readFileSync","join","homedir","program","spawnSync","execFileSync","SSH_OPTIONS","execFileSync","agent","spawnSync","program","program"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/identity.ts","../src/paths.ts","../src/mux.ts","../src/store.ts","../src/cli/clear.ts","../src/channel.ts","../src/types.ts","../src/fold.ts","../src/export.ts","../src/collector.ts","../src/cli/collect.ts","../src/cli/export.ts","../src/cli/init.ts","../src/cli/link.ts","../src/cli/peer.ts","../src/cli/pick.ts","../src/agents.ts","../src/glance.ts","../src/status.ts","../src/cli/status.ts","../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { registerClear } from \"./cli/clear.js\";\nimport { registerCollect } from \"./cli/collect.js\";\nimport { registerExport } from \"./cli/export.js\";\nimport { registerInit } from \"./cli/init.js\";\nimport { registerLink } from \"./cli/link.js\";\nimport { registerPeer } from \"./cli/peer.js\";\nimport { registerPick } from \"./cli/pick.js\";\nimport { registerStatus } from \"./cli/status.js\";\nimport { VERSION } from \"./index.js\";\n\nconst program = new Command();\nprogram\n .name(\"murmur\")\n .description(\"Agent state across every machine, in one view.\")\n .version(VERSION);\nregisterInit(program);\nregisterLink(program);\nregisterExport(program);\nregisterCollect(program);\nregisterClear(program);\nregisterPeer(program);\nregisterStatus(program);\nregisterPick(program);\nprogram.parse();\n","import { randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\nimport { stateDir } from \"./paths.js\";\n\nexport type NodeIdentity = {\n host_id: string;\n display_name: string;\n};\n\nexport function loadIdentity(): NodeIdentity | null {\n const path = join(stateDir(), \"identity.json\");\n return existsSync(path) ? JSON.parse(readFileSync(path, \"utf8\")) : null;\n}\n\nexport function ensureIdentity(displayName = hostname()): NodeIdentity {\n const existing = loadIdentity();\n if (existing) return existing;\n\n const identity = { host_id: randomUUID(), display_name: displayName };\n mkdirSync(stateDir(), { recursive: true });\n writeFileSync(join(stateDir(), \"identity.json\"), `${JSON.stringify(identity, null, 2)}\\n`);\n return identity;\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function stateDir(): string {\n return (\n process.env.MURMUR_STATE_DIR ??\n join(process.env.XDG_STATE_HOME ?? join(homedir(), \".local\", \"state\"), \"murmur\")\n );\n}\n\nexport function configDir(): string {\n return (\n process.env.MURMUR_CONFIG_DIR ??\n join(process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\"), \"murmur\")\n );\n}\n\nexport function dbPath(): string {\n return join(stateDir(), \"events.db\");\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { AgentState } from \"./types.js\";\n\nexport type Location = {\n session: string;\n window: string;\n pane: string;\n session_name: string | null;\n window_name: string | null;\n};\n\nexport interface Mux {\n currentWindow(): Location | null;\n liveWindows(): Set<string> | null;\n setState(window: string, state: AgentState | null): void;\n // Reports whether the attach actually happened. runTmux swallows failures to\n // return null, and a jump that silently failed looked exactly like \"enter did\n // nothing\" -- the symptom the remote probe was added to prevent, reproduced\n // on the local path.\n attach(session: string, window: string): boolean;\n windowNames(): Map<string, string>;\n windowForPane(pane: string): string | null;\n panesInWindow(window: string): string[];\n windowNamed(name: string): string | null;\n selectWindow(window: string): boolean;\n newWindow(name: string, command: string): boolean;\n capture(pane: string, lines?: number): string | null;\n}\n\nfunction runTmux(args: string[]): string | null {\n try {\n return execFileSync(\"tmux\", args, {\n encoding: \"utf8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n } catch {\n return null;\n }\n}\n\nexport const tmux: Mux = {\n currentWindow() {\n // $TMUX_PANE is the only trustworthy signal that we are inside a pane, and\n // it is set by tmux for every process in one.\n //\n // Asking tmux instead does not work: `display-message` answers from any\n // process on a machine with a running server, and reports whichever pane\n // that server considers active. A pi started outside tmux -- a bare ssh\n // login, a plain terminal, cron -- would then record itself as living in\n // some unrelated agent's pane and overwrite that agent's state. Falling\n // back to `display-message` here was exactly that bug.\n const pane = process.env.TMUX_PANE;\n if (!pane) return null;\n\n // One call for ids and names together. The names are recorded on every\n // event because a reader cannot resolve a remote window id against its own\n // tmux, so they have to travel with the event.\n const fields = runTmux([\n \"display-message\",\n \"-t\",\n pane,\n \"-p\",\n \"#{session_id}\\t#{window_id}\\t#{session_name}\\t#{window_name}\",\n ]);\n const [session, window, sessionName, windowName] = fields?.split(\"\\t\") ?? [];\n if (!session || !window) return null;\n return {\n session,\n window,\n pane,\n session_name: sessionName || null,\n window_name: windowName || null,\n };\n },\n\n // Which of this host's windows still exist. Only the authoring node can\n // answer this, which is why the check runs on export rather than on the\n // reader: a peer holding a `blocked` row for a window that died has nothing\n // to supersede it, and the agent stays in every HUD forever.\n //\n // null means \"could not tell\" (no tmux server, tmux missing) and is\n // deliberately distinct from an empty set, which means \"tmux answered, and\n // there are no windows\". Treating the first as the second would clear every\n // agent on the host the moment tmux was unreachable.\n //\n // Unlike currentWindow, this deliberately asks tmux rather than reading the\n // environment, and it is right to: \"which windows exist on this host\" is a\n // server-wide question with one answer, and export runs over ssh with no\n // pane of its own. currentWindow asks \"which pane am I in\", which only\n // $TMUX_PANE can answer.\n liveWindows() {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\"]);\n if (out === null) return null;\n return new Set(out.split(\"\\n\").filter(Boolean));\n },\n\n setState(window, state) {\n if (state === null) {\n runTmux([\"set-window-option\", \"-qu\", \"-t\", window, \"@agent_state\"]);\n } else {\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@agent_state\", state]);\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@pane_agent\", \"1\"]);\n }\n runTmux([\"refresh-client\", \"-S\"]);\n },\n\n attach(session, window) {\n // Two steps, because switch-client alone is a no-op when the target window\n // is in the session you are already attached to — which is the common case\n // for a local agent, and why \"enter\" appeared to do nothing.\n // switch-client moves the client between sessions; select-window moves\n // that session to the right window.\n //\n // Only select-window decides the result. switch-client legitimately fails\n // when there is no client to switch (running outside tmux), and treating\n // that as a failed jump would report an error for a working attach.\n runTmux([\"switch-client\", \"-t\", session]);\n return runTmux([\"select-window\", \"-t\", window]) !== null;\n },\n\n // Window ids are what the log stores, because they are stable; names are\n // what a human recognises in a picker. Names are live tmux state, not\n // history, so they are resolved at render time rather than recorded.\n windowNames() {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\\t#{window_name}\"]);\n const names = new Map<string, string>();\n for (const line of out?.split(\"\\n\") ?? []) {\n const [id, name] = line.split(\"\\t\");\n if (id && name) names.set(id, name);\n }\n return names;\n },\n\n // First window carrying this exact name, or null. Used to reuse a per-host\n // ssh window instead of opening another one.\n // Sibling panes, for deciding whether an unowned pane may clear the window's\n // badge. A window holding an agent and a shell must not lose the badge when\n // you focus the shell.\n panesInWindow(window) {\n const out = runTmux([\"list-panes\", \"-t\", window, \"-F\", \"#{pane_id}\"]);\n return out?.split(\"\\n\").filter(Boolean) ?? [];\n },\n\n windowNamed(name) {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\\t#{window_name}\"]);\n for (const line of out?.split(\"\\n\") ?? []) {\n const [id, windowName] = line.split(\"\\t\");\n if (id && windowName === name) return id;\n }\n return null;\n },\n\n selectWindow(window) {\n return runTmux([\"select-window\", \"-t\", window]) !== null;\n },\n\n newWindow(name, command) {\n return runTmux([\"new-window\", \"-n\", name, command]) !== null;\n },\n\n // The window a pane belongs to, for a pane murmur has no event for. Clearing\n // a badge is a tmux operation and does not require murmur to own the pane.\n windowForPane(pane) {\n return runTmux([\"display-message\", \"-t\", pane, \"-p\", \"#{window_id}\"]) || null;\n },\n\n capture(pane, lines) {\n const args = [\"capture-pane\", \"-p\", \"-t\", pane];\n if (lines !== undefined) args.push(\"-S\", `-${lines}`);\n return runTmux(args);\n },\n};\n\nexport function pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code !== \"ESRCH\";\n }\n}\n","import { rmSync } from \"node:fs\";\nimport Database from \"better-sqlite3\";\nimport { ensureIdentity } from \"./identity.js\";\nimport { dbPath } from \"./paths.js\";\nimport type { Driver, Event, Peer } from \"./types.js\";\n\nconst DEFAULT_RETENTION_MS = 7 * 86_400_000;\n\n/**\n * Local storage shape. Bump on any change to the events or peers tables.\n *\n * Distinct from `SCHEMA_VERSION` in export.ts, which versions the *wire*: a\n * node can change how it stores events without changing what it sends, and a\n * wire change should not throw away local history.\n */\nexport const STORE_VERSION = 2;\n\n/**\n * Migration strategy: there isn't one. A version mismatch deletes the database\n * and starts again.\n *\n * This is only acceptable because nothing in events.db is authoritative or\n * irreplaceable. It is a bounded-retention observability log: remote events\n * re-sync from their authoring peer on the next collect, local agents re-report\n * on their next state change, and node identity deliberately lives in a\n * separate file. If anything durable is ever added here, this stops being safe\n * and a real migration is required.\n *\n * Peers survive, because they are the one thing a human typed. Watermarks are\n * reset with the events they indexed -- keeping them would skip the events the\n * new database no longer has -- and re-reading a peer from zero is free, since\n * ingest is idempotent.\n */\nfunction resetIfStale(path: string): Peer[] {\n let salvaged: Peer[] = [];\n try {\n const existing = new Database(path, { fileMustExist: true });\n const version = (existing.pragma(\"user_version\", { simple: true }) as number) ?? 0;\n if (version === STORE_VERSION) {\n existing.close();\n return salvaged;\n }\n try {\n salvaged = existing\n .prepare(\"SELECT name, target, host_id, display_name FROM peers\")\n .all() as Peer[];\n } catch {\n // Old enough not to have the table, or unreadable. Nothing to save.\n }\n existing.close();\n } catch {\n // No database yet, or one too broken to open. Either way, recreate.\n return salvaged;\n }\n\n // -wal and -shm must go too: a stale sidecar against a fresh main file is a\n // documented way to corrupt sqlite.\n for (const suffix of [\"\", \"-wal\", \"-shm\"]) rmSync(`${path}${suffix}`, { force: true });\n return salvaged;\n}\n\n// The name fields are optional on the way in: a caller that has no name for a\n// thing should not have to say `null` four times, and a non-tmux harness has\n// none of them. They are non-optional on `Event` itself, so a reader never has\n// to distinguish absent from null.\nexport type NewEvent = Omit<\n Event,\n \"host_id\" | \"seq\" | \"ts\" | \"session_name\" | \"window_name\" | \"agent_name\" | \"pi_session\"\n> & {\n ts?: number;\n session_name?: string | null;\n window_name?: string | null;\n agent_name?: string | null;\n pi_session?: string | null;\n};\n\ntype EventRow = Omit<Event, \"synthetic\" | \"extra\"> & {\n synthetic: number;\n extra: string;\n};\n\nfunction eventValues(event: Event): unknown[] {\n return [\n event.host_id,\n event.seq,\n event.ts,\n event.agent_id,\n event.session,\n event.window,\n event.pane,\n event.session_name,\n event.window_name,\n event.agent_name,\n event.pi_session,\n event.workstream,\n event.role,\n event.cli,\n event.driver,\n event.kind,\n event.state,\n event.message,\n event.pid,\n Number(event.synthetic),\n event.reason,\n JSON.stringify(event.extra),\n ];\n}\n\nfunction toEvent(row: EventRow): Event {\n return {\n ...row,\n driver: row.driver as Driver | null,\n synthetic: row.synthetic === 1,\n extra: JSON.parse(row.extra) as Record<string, unknown>,\n };\n}\n\nexport interface Store {\n append(event: NewEvent): Event;\n ingest(events: Event[]): number;\n eventsSince(hostId: string, seq: number): Event[];\n allEvents(): Event[];\n /**\n * The most recent event for one agent, or null.\n *\n * Exists so the `clear` hook does not have to open its own SQLite handle and\n * write its own `ORDER BY seq DESC LIMIT 1`, which is what it used to do --\n * making \"store is the only module touching SQL\" false, and putting knowledge\n * of agent_id construction and event ordering in a CLI file where a schema\n * change would miss it. That path swallows its own errors, so the miss would\n * have been silent.\n */\n latestForAgent(hostId: string, agentId: string): Event | null;\n maxSeq(hostId: string): number;\n prune(horizonMs?: number): number;\n peers(): Peer[];\n /**\n * Drop every event for one agent from this node's replica.\n *\n * For a remote agent this is a replica eviction, not a claim about truth: the\n * authoring node still owns it, and a collect re-reads from the watermark if\n * it is still alive.\n */\n forgetAgent(agentId: string): number;\n forgetHost(hostId: string): number;\n upsertPeer(peer: Partial<Peer> & { name: string; target: string }): void;\n removePeer(name: string): boolean;\n close(): void;\n}\n\nexport function openStore(): Store {\n const identity = ensureIdentity();\n const path = dbPath();\n const salvagedPeers = resetIfStale(path);\n const database = new Database(path);\n database.pragma(\"journal_mode = WAL\");\n database.pragma(`user_version = ${STORE_VERSION}`);\n database.exec(`\n CREATE TABLE IF NOT EXISTS events (\n host_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n ts INTEGER NOT NULL,\n agent_id TEXT NOT NULL,\n session TEXT NOT NULL,\n window TEXT NOT NULL,\n pane TEXT NOT NULL,\n session_name TEXT,\n window_name TEXT,\n agent_name TEXT,\n pi_session TEXT,\n workstream TEXT,\n role TEXT,\n cli TEXT,\n driver TEXT,\n kind TEXT NOT NULL,\n state TEXT NOT NULL,\n message TEXT NOT NULL,\n pid INTEGER,\n synthetic INTEGER NOT NULL,\n reason TEXT NOT NULL,\n extra TEXT NOT NULL,\n PRIMARY KEY (host_id, seq)\n );\n CREATE INDEX IF NOT EXISTS events_agent_seq ON events (agent_id, seq);\n CREATE TABLE IF NOT EXISTS peers (\n name TEXT PRIMARY KEY,\n target TEXT NOT NULL,\n host_id TEXT,\n display_name TEXT,\n watermark INTEGER NOT NULL,\n fetched_at INTEGER,\n -- When a jump last proved this peer's tmux was not answering. Reader\n -- state, not an event: this node cannot author facts about another\n -- node's agents, and a jump is a local observation, not something the\n -- peer said. Cleared by the next successful collect.\n tmux_down_at INTEGER\n );\n `);\n\n // Additive migration: an existing peers table predates tmux_down_at.\n try {\n database.exec(\"ALTER TABLE peers ADD COLUMN tmux_down_at INTEGER\");\n } catch {\n // Already present.\n }\n\n // Put back the peers the wipe took, at watermark 0 so the next collect\n // re-reads each one from the start.\n if (salvagedPeers.length > 0) {\n const restore = database.prepare(\n `INSERT OR IGNORE INTO peers (name, target, host_id, display_name, watermark, fetched_at)\n VALUES (?, ?, ?, ?, 0, NULL)`,\n );\n for (const peer of salvagedPeers) {\n restore.run(peer.name, peer.target, peer.host_id ?? null, peer.display_name ?? null);\n }\n }\n\n const eventColumns = `\n host_id, seq, ts, agent_id, session, window, pane,\n session_name, window_name, agent_name, pi_session,\n workstream, role, cli, driver, kind, state, message, pid,\n synthetic, reason, extra`;\n const eventPlaceholders = new Array(22).fill(\"?\").join(\", \");\n const insertEvent = database.prepare(\n `INSERT INTO events (${eventColumns}) VALUES (${eventPlaceholders})`,\n );\n const ingestEvent = database.prepare(\n `INSERT OR IGNORE INTO events (${eventColumns}) VALUES (${eventPlaceholders})`,\n );\n const selectMaxSeq = database.prepare(\n \"SELECT COALESCE(MAX(seq), 0) AS seq FROM events WHERE host_id = ?\",\n );\n const append = database.transaction((event: NewEvent): Event => {\n const row = selectMaxSeq.get(identity.host_id) as { seq: number };\n const stored: Event = {\n ...event,\n host_id: identity.host_id,\n seq: row.seq + 1,\n ts: event.ts ?? Date.now(),\n session_name: event.session_name ?? null,\n window_name: event.window_name ?? null,\n agent_name: event.agent_name ?? null,\n pi_session: event.pi_session ?? null,\n };\n insertEvent.run(...eventValues(stored));\n return stored;\n });\n const ingest = database.transaction((events: Event[]): number => {\n let inserted = 0;\n for (const event of events) inserted += ingestEvent.run(...eventValues(event)).changes;\n return inserted;\n });\n\n return {\n append,\n ingest,\n eventsSince(hostId, seq) {\n const rows = database\n .prepare(\"SELECT * FROM events WHERE host_id = ? AND seq > ? ORDER BY seq\")\n .all(hostId, seq) as EventRow[];\n return rows.map(toEvent);\n },\n allEvents() {\n const rows = database\n .prepare(\"SELECT * FROM events ORDER BY ts, host_id, seq\")\n .all() as EventRow[];\n return rows.map(toEvent);\n },\n latestForAgent(hostId, agentId) {\n const row = database\n .prepare(\n `SELECT * FROM events\n WHERE host_id = ? AND agent_id = ?\n ORDER BY seq DESC LIMIT 1`,\n )\n .get(hostId, agentId) as EventRow | undefined;\n return row ? toEvent(row) : null;\n },\n maxSeq(hostId) {\n return (selectMaxSeq.get(hostId) as { seq: number }).seq;\n },\n prune(horizonMs = Number(process.env.MURMUR_RETENTION_MS ?? DEFAULT_RETENTION_MS)) {\n return database\n .prepare(`\n DELETE FROM events\n WHERE ts < ?\n AND (host_id, seq) NOT IN (\n SELECT host_id, seq FROM (\n SELECT host_id, seq,\n ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY ts DESC, seq DESC) rn\n FROM events\n ) WHERE rn = 1\n )\n `)\n .run(Date.now() - horizonMs).changes;\n },\n peers() {\n return database.prepare(\"SELECT * FROM peers ORDER BY name\").all() as Peer[];\n },\n forgetAgent(agentId) {\n return database.prepare(\"DELETE FROM events WHERE agent_id = ?\").run(agentId).changes;\n },\n forgetHost(hostId) {\n // Every replicated row for one origin node. Only ever called about a\n // REMOTE host: the local host's rows are this node's own authorship and\n // the retention horizon owns them.\n return database.prepare(\"DELETE FROM events WHERE host_id = ?\").run(hostId).changes;\n },\n upsertPeer(peer) {\n const current = database.prepare(\"SELECT * FROM peers WHERE name = ?\").get(peer.name) as\n | Peer\n | undefined;\n database\n .prepare(`\n INSERT INTO peers (name, target, host_id, display_name, watermark, fetched_at, tmux_down_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n target = excluded.target,\n host_id = excluded.host_id,\n display_name = excluded.display_name,\n watermark = excluded.watermark,\n fetched_at = excluded.fetched_at,\n tmux_down_at = excluded.tmux_down_at\n `)\n .run(\n peer.name,\n peer.target,\n peer.host_id !== undefined ? peer.host_id : (current?.host_id ?? null),\n peer.display_name !== undefined ? peer.display_name : (current?.display_name ?? null),\n peer.watermark !== undefined ? peer.watermark : (current?.watermark ?? 0),\n peer.fetched_at !== undefined ? peer.fetched_at : (current?.fetched_at ?? null),\n peer.tmux_down_at !== undefined ? peer.tmux_down_at : (current?.tmux_down_at ?? null),\n );\n },\n removePeer(name) {\n // Drops the peer and its watermark. Replicated events stay: they are\n // real history authored elsewhere, and the retention horizon already\n // ages them out. Re-adding the peer re-syncs from zero, which ingest\n // makes free.\n return database.prepare(\"DELETE FROM peers WHERE name = ?\").run(name).changes > 0;\n },\n close() {\n database.close();\n },\n };\n}\n","import type { Command } from \"commander\";\nimport { loadIdentity } from \"../identity.js\";\nimport { type Mux, tmux } from \"../mux.js\";\nimport { openStore, type Store } from \"../store.js\";\nimport type { Driver } from \"../types.js\";\n\ntype OwnedPane = {\n agent_id: string;\n session_name: string | null;\n window_name: string | null;\n agent_name: string | null;\n pi_session: string | null;\n session: string;\n window: string;\n pane: string;\n workstream: string | null;\n role: string | null;\n cli: string | null;\n driver: Driver | null;\n state: string;\n};\n\n/**\n * Does any OTHER pane in this window own an agent?\n *\n * Read-only, and best effort: if tmux or the database cannot answer we say yes,\n * which leaves the badge alone. Wrongly keeping a badge is recoverable by\n * focusing the agent's own pane; wrongly clearing one loses the signal.\n */\nfunction windowHasAgent(\n window: string,\n focused: string,\n hostId: string | undefined,\n mux: Mux,\n store: Store | undefined,\n): boolean {\n // No identity means this node has authored nothing, so no sibling can own an\n // agent and there is nothing to protect. Returning true here blocked the\n // orphan-badge clear on a node that had murmur installed but never ran init.\n if (!hostId) return false;\n const siblings = mux.panesInWindow(window).filter((candidate) => candidate !== focused);\n // No siblings means nothing to protect. Checked before opening the database\n // so a node with no events yet still clears an orphan badge: treating a\n // missing database as \"a sibling might own an agent\" left every stale badge\n // in place on a fresh install.\n if (siblings.length === 0) return false;\n // No database yet: nothing is recorded, so no sibling owns an agent.\n if (!store) return false;\n try {\n for (const sibling of siblings) {\n const latest = store.latestForAgent(hostId, `${hostId}:${sibling}`);\n if (latest && latest.state !== \"cleared\") return true;\n }\n return false;\n } catch {\n return true;\n }\n}\n\nexport function clearPane(pane: string, mux: Mux = tmux): void {\n let store: Store | undefined;\n try {\n if (!pane) return;\n\n // The badge is a tmux window option, not murmur state, so clearing it never\n // needs murmur to know anything. Resolve the window up front: an\n // uninitialised node or a missing database must still clear rather than\n // abort the hook.\n const window = mux.windowForPane(pane);\n const identity = loadIdentity();\n\n let owner: OwnedPane | undefined;\n if (identity) {\n try {\n store = openStore();\n owner =\n (store.latestForAgent(identity.host_id, `${identity.host_id}:${pane}`) as\n | OwnedPane\n | undefined) ?? undefined;\n } catch {\n // No database yet. Nothing is owned; the badge still clears below.\n }\n }\n\n // A pane murmur has no event for can still carry a badge: an orphan from\n // the agent-attention era, or a window murmur never recorded. Left alone it\n // sits in the status bar and the tms picker forever, because nothing else\n // will ever clear it.\n //\n // But only when no SIBLING pane owns an agent. The badge is a window\n // option while \"the user looked\" is only true of one pane, so clearing on\n // any pane in the window let a shell pane wipe the agent's badge next to\n // it -- which is the exact case --pane exists to distinguish.\n if (!owner) {\n // No store means no database, so no sibling can own an agent and the\n // orphan badge must still clear. Gating this on `store` left every stale\n // badge in place on a fresh install -- which is what the comment above\n // was already warning about.\n if (window && !windowHasAgent(window, pane, identity?.host_id, mux, store)) {\n mux.setState(window, null);\n }\n return;\n }\n // Already cleared in the log, but the badge may still be set: the two can\n // disagree when a `cleared` event was written by a path that did not touch\n // tmux, and nothing else reconciles them. Clear the option and return\n // without appending a second, redundant `cleared` event.\n if (owner.state === \"cleared\") {\n mux.setState(owner.window, null);\n return;\n }\n\n // `owner` came from this store, so it is open; the check is for the type.\n try {\n store?.append({\n agent_id: owner.agent_id,\n session: owner.session,\n window: owner.window,\n pane: owner.pane,\n // Carry the names forward: a `cleared` row that drops them makes the\n // agent's last event nameless, which is what left \"@75\" in the picker.\n session_name: owner.session_name,\n window_name: owner.window_name,\n agent_name: owner.agent_name,\n pi_session: owner.pi_session,\n workstream: owner.workstream,\n role: owner.role,\n cli: owner.cli,\n driver: owner.driver,\n kind: \"state\",\n state: \"cleared\",\n message: \"\",\n pid: null,\n synthetic: false,\n reason: \"\",\n extra: {},\n });\n } catch {\n // An append that fails must not stop the badge clearing below: the badge\n // is tmux state, and leaving it set is the visible failure.\n }\n mux.setState(owner.window, null);\n } catch {\n // Focus hooks run inside the tmux server: they must always be silent and total.\n } finally {\n // One handle for the whole hook, closed once. Two opens raced each other on\n // the same WAL for no benefit.\n try {\n store?.close();\n } catch {\n // Silent and total.\n }\n }\n}\n\nexport function registerClear(program: Command): void {\n program\n .command(\"clear\")\n .description(\"Clear attention for the agent in a pane\")\n .option(\"--pane <pane-id>\", \"focused tmux pane id\")\n .action((options: { pane?: string }) => clearPane(options.pane ?? \"\"));\n}\n","import { execFile, execFileSync } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\nconst CONTROL_PATH = \"~/.ssh/control/%r@%h:%p\";\n\n// Both timeouts are sized against the tmux status bar, because that is what\n// actually drives collection: `murmur status` collects, and tmux re-runs it\n// every `status-interval` — 5s on the author's setup, 15s by default. A collect\n// that outlives its tick is a collect overlapping itself, and tmux offers no\n// way to cancel the last one.\n//\n// So the budget for the whole exchange is under 5s, and these are deliberately\n// aggressive: a really slow node is rejected rather than allowed to hold up the\n// HUD. That is cheap because the cost of losing the race is one tick of\n// staleness, and the next tick is five seconds away.\n\n// OpenSSH's default TCP connect timeout is the kernel's, 75s on macOS, which\n// made `murmur pick` unusable against a sleeping laptop. One second is still\n// ~6x a real cold handshake on a LAN or VPN (measured: 168ms cold, 42ms on a\n// warm control socket), and a peer that misses it simply shows stale — the\n// designed outcome for a host you cannot reach.\nconst CONNECT_TIMEOUT_S = 1;\n\n// Belt and braces for a host that completes the TCP connect and then stops\n// responding — ConnectTimeout does not cover that, and it is how a sleeping\n// laptop behaves. Bounds the whole exchange rather than just the dial, so it\n// has to leave room for the dial plus an export: three seconds is the tick\n// budget minus headroom for the rest of `status`.\nconst EXEC_TIMEOUT_MS = 3_000;\n\n// Warm if possible, cold if not, never interactive.\n//\n// ControlMaster=no attaches to a master socket left behind by an ordinary\n// `ssh <host>` (given ControlMaster auto + ControlPersist in ssh_config), so a\n// peer you have touched recently costs a new channel on an authenticated\n// connection rather than a handshake.\n//\n// When no socket is listening OpenSSH falls back to connecting normally, and we\n// want that: with plain key auth a cold peer collects fine, just slower\n// (~170ms against ~10ms measured on a LAN). Fleet visibility should not depend\n// on having ssh'd somewhere today.\n//\n// BatchMode=yes bounds what that fallback may do. It disables every\n// interactive prompt — password, passphrase, host key confirmation — so a\n// cold peer that cannot authenticate silently fails immediately instead of\n// blocking a background collect on a human. Note this is \"never prompt\", not\n// \"never authenticate\": a host demanding a hardware-token touch per connection\n// is the case this does not fully cover, and the reason `hasWarmSocket` exists\n// should that ever need gating.\n//\n// Exported because every ssh murmur runs wants exactly this posture -- the\n// collector, the picker's preview, the jump probe. Three hand-rolled copies is\n// how one of them ends up without BatchMode and starts prompting for auth on\n// every keypress.\nexport const SSH_OPTIONS = [\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"ControlMaster=no\",\n \"-o\",\n `ControlPath=${CONTROL_PATH}`,\n \"-o\",\n `ConnectTimeout=${CONNECT_TIMEOUT_S}`,\n];\n\nexport interface Channel {\n exec(target: string, argv: string[]): Promise<string>;\n}\n\n// Node's execFile defaults to a 1 MiB stdout ceiling and rejects with\n// ERR_CHILD_PROCESS_STDIO_MAXBUFFER past it, killing the child. An export is\n// the peer's whole delta, and at ~400 bytes an event (measured) 1 MiB is only\n// ~2,600 events -- reachable inside the 7-day horizon on a busy box, and\n// certain on a first sync from watermark 0.\n//\n// The failure would also be permanent, not transient: the watermark only\n// advances on a successful parse, so every subsequent collect would re-request\n// the same oversized range and fail identically. A reachable peer would sit\n// stale forever.\n//\n// 64 MiB is ~170k events, far above what the horizon can hold, and it is a\n// ceiling rather than an allocation. The timeout is the real bound on a\n// runaway peer.\nconst MAX_EXPORT_BYTES = 64 * 1024 * 1024;\n\nexport const ssh: Channel = {\n async exec(target, argv) {\n const { stdout } = await execFileAsync(\"ssh\", [...SSH_OPTIONS, target, ...argv], {\n encoding: \"utf8\",\n timeout: EXEC_TIMEOUT_MS,\n maxBuffer: MAX_EXPORT_BYTES,\n });\n return stdout;\n },\n};\n\nexport function hasWarmSocket(target: string): boolean {\n try {\n execFileSync(\"ssh\", [...SSH_OPTIONS, \"-O\", \"check\", target], { stdio: \"ignore\" });\n return true;\n } catch {\n return false;\n }\n}\n","export type AgentState = \"working\" | \"blocked\" | \"done\" | \"crashed\" | \"cleared\";\n\nexport type Driver = \"human\" | \"orchestrated\";\n\nexport const DEFAULT_DRIVER: Driver = \"human\";\n\nexport type Event = {\n host_id: string;\n seq: number;\n ts: number;\n agent_id: string;\n session: string;\n window: string;\n pane: string;\n // Human-readable names, recorded by the node that owns the pane. tmux ids are\n // stable and are what jumps; names are what a human recognises. They are\n // *recorded* rather than resolved at render time because a reader cannot look\n // a remote window id up in its own tmux -- doing so labelled a remote agent\n // with whatever this host had at that id. Cost: a renamed window keeps its\n // old name until the next event, which is the same property the history rows\n // always had.\n session_name: string | null;\n window_name: string | null;\n // The agent's own idea of what it is working on: pi's session name, and mu's\n // $MU_AGENT_NAME for an orchestrated agent. Both are richer than the window\n // name when they exist, and neither is derivable from tmux.\n agent_name: string | null;\n pi_session: string | null;\n workstream: string | null;\n role: string | null;\n cli: string | null;\n driver: Driver | null;\n kind: string;\n state: AgentState | string;\n message: string;\n pid: number | null;\n synthetic: boolean;\n reason: string;\n extra: Record<string, unknown>;\n};\n\nexport type Peer = {\n name: string;\n target: string;\n host_id: string | null;\n display_name: string | null;\n watermark: number;\n fetched_at: number | null;\n /** When a jump last found this peer's tmux server down. Null once it answers. */\n tmux_down_at: number | null;\n};\n","import { type AgentState, DEFAULT_DRIVER, type Driver, type Event } from \"./types.js\";\n\nexport type LiveCheck = (pid: number) => boolean;\n\nexport type AgentView = {\n agent_id: string;\n host_id: string;\n state: AgentState | null;\n event: Event | null;\n workstream: string | null;\n role: string | null;\n cli: string | null;\n driver: Driver;\n session: string;\n window: string;\n pane: string;\n // Names as recorded by the authoring node, so a remote agent is labelled by\n // its own host's tmux rather than by whatever this host has at that id.\n session_name: string | null;\n window_name: string | null;\n agent_name: string | null;\n pi_session: string | null;\n fetched_at: number | null;\n};\n\nexport function foldAgent(\n events: Event[],\n isAlive: LiveCheck,\n): { state: AgentState | null; event: Event | null } {\n for (let index = events.length - 1; index >= 0; index -= 1) {\n const event = events[index];\n if (!event) continue;\n\n switch (event.state) {\n case \"blocked\":\n case \"done\":\n case \"crashed\":\n return { state: event.state, event };\n case \"cleared\":\n return { state: null, event: null };\n case \"working\":\n return {\n state: event.pid !== null && event.pid > 0 && isAlive(event.pid) ? \"working\" : \"crashed\",\n event,\n };\n }\n }\n\n return { state: null, event: null };\n}\n\nexport function foldAll(events: Event[], isAlive: LiveCheck): AgentView[] {\n const byAgent = new Map<string, Event[]>();\n for (const event of events) {\n const agentEvents = byAgent.get(event.agent_id);\n if (agentEvents) agentEvents.push(event);\n else byAgent.set(event.agent_id, [event]);\n }\n\n return [...byAgent.values()].map((agentEvents) => {\n const folded = foldAgent(agentEvents, isAlive);\n const source = folded.event ?? agentEvents[agentEvents.length - 1];\n if (!source) throw new Error(\"agent event group cannot be empty\");\n\n return {\n agent_id: source.agent_id,\n host_id: source.host_id,\n state: folded.state,\n event: folded.event,\n workstream: source.workstream,\n role: source.role,\n cli: source.cli,\n driver: source.driver ?? DEFAULT_DRIVER,\n session: source.session,\n window: source.window,\n pane: source.pane,\n session_name: source.session_name,\n window_name: source.window_name,\n agent_name: source.agent_name,\n pi_session: source.pi_session,\n fetched_at: null,\n };\n });\n}\n\nconst ATTENTION_ORDER: Record<AgentState, number> = {\n blocked: 0,\n done: 1,\n crashed: 2,\n working: 3,\n cleared: 4,\n};\n\nexport function attentionSort(views: AgentView[]): AgentView[] {\n return [...views].sort((left, right) => {\n const stateOrder =\n (left.state === null ? 4 : ATTENTION_ORDER[left.state]) -\n (right.state === null ? 4 : ATTENTION_ORDER[right.state]);\n if (stateOrder !== 0) return stateOrder;\n return (right.event?.ts ?? 0) - (left.event?.ts ?? 0);\n });\n}\n\nexport function isStale(fetchedAt: number | null, now: number, thresholdMs = 60_000): boolean {\n return fetchedAt !== null && now - fetchedAt > thresholdMs;\n}\n","import { foldAgent, type LiveCheck } from \"./fold.js\";\nimport { ensureIdentity } from \"./identity.js\";\nimport type { Store } from \"./store.js\";\nimport type { Driver, Event } from \"./types.js\";\n\nexport const SCHEMA_VERSION = 2;\n\nexport type Envelope = {\n schema_version: number;\n host_id: string;\n display_name: string;\n exported_at: number;\n};\n\nconst EVENT_FIELDS = new Set([\n \"host_id\",\n \"seq\",\n \"ts\",\n \"agent_id\",\n \"session\",\n \"window\",\n \"pane\",\n \"session_name\",\n \"window_name\",\n \"agent_name\",\n \"pi_session\",\n \"workstream\",\n \"role\",\n \"cli\",\n \"driver\",\n \"kind\",\n \"state\",\n \"message\",\n \"pid\",\n \"synthetic\",\n \"reason\",\n]);\n\nfunction eventToWire(event: Event): Record<string, unknown> {\n const { extra, ...known } = event;\n return { ...extra, ...known };\n}\n\nexport function eventFromWire(wire: Record<string, unknown>): Event {\n const extra = Object.fromEntries(Object.entries(wire).filter(([key]) => !EVENT_FIELDS.has(key)));\n return {\n host_id: wire.host_id as string,\n seq: wire.seq as number,\n ts: wire.ts as number,\n agent_id: wire.agent_id as string,\n session: wire.session as string,\n window: wire.window as string,\n pane: wire.pane as string,\n session_name: (wire.session_name as string | null | undefined) ?? null,\n window_name: (wire.window_name as string | null | undefined) ?? null,\n agent_name: (wire.agent_name as string | null | undefined) ?? null,\n pi_session: (wire.pi_session as string | null | undefined) ?? null,\n workstream: (wire.workstream as string | null | undefined) ?? null,\n role: (wire.role as string | null | undefined) ?? null,\n cli: (wire.cli as string | null | undefined) ?? null,\n driver: (wire.driver as Driver | null | undefined) ?? null,\n kind: wire.kind as string,\n state: wire.state as string,\n message: wire.message as string,\n pid: (wire.pid as number | null | undefined) ?? null,\n synthetic: wire.synthetic as boolean,\n reason: wire.reason as string,\n extra,\n };\n}\n\nfunction synthesizeCrashes(store: Store, hostId: string, isAlive: LiveCheck): void {\n const byAgent = new Map<string, Event[]>();\n for (const event of store.allEvents()) {\n if (event.host_id !== hostId) continue;\n const events = byAgent.get(event.agent_id);\n if (events) events.push(event);\n else byAgent.set(event.agent_id, [event]);\n }\n\n for (const events of byAgent.values()) {\n events.sort((left, right) => left.seq - right.seq);\n const newest = events.at(-1);\n if (\n newest &&\n newest.state === \"working\" &&\n !newest.synthetic &&\n foldAgent(events, isAlive).state === \"crashed\"\n ) {\n const { host_id: _hostId, seq: _seq, ts: _ts, ...event } = newest;\n store.append({ ...event, state: \"crashed\", synthetic: true, reason: \"pid_gone\" });\n }\n }\n}\n\n/**\n * Clear agents whose tmux window is gone.\n *\n * A window that dies takes its agent with it, but the log's newest row still\n * says `blocked`, so every peer keeps showing an agent that cannot be jumped\n * to -- the fold has nothing to supersede that row with. Only the authoring\n * node can tell, which is why this runs on export beside crash synthesis\n * rather than on the reader.\n *\n * `cleared` is the right state: it already means \"no longer wants attention\"\n * and resets the fold to none. An appended event rather than an export-time\n * filter, so the fact replicates once and explains itself, instead of every\n * peer having to re-derive it from an absence.\n */\nexport function clearDeadWindows(store: Store, hostId: string, live: Set<string> | null): void {\n // null means tmux could not answer. An empty set means it did and there are\n // no windows. Conflating them would clear every agent on the host whenever\n // tmux was briefly unreachable.\n if (live === null) return;\n\n const newest = new Map<string, Event>();\n for (const event of store.allEvents()) {\n if (event.host_id !== hostId) continue;\n const previous = newest.get(event.agent_id);\n if (!previous || event.seq > previous.seq) newest.set(event.agent_id, event);\n }\n\n for (const event of newest.values()) {\n if (event.state === \"cleared\") continue;\n if (live.has(event.window)) continue;\n const { host_id: _hostId, seq: _seq, ts: _ts, ...rest } = event;\n store.append({\n ...rest,\n state: \"cleared\",\n synthetic: true,\n reason: \"window_gone\",\n message: \"\",\n });\n }\n}\n\nexport function exportJsonl(\n store: Store,\n since: number,\n isAlive: LiveCheck,\n live?: Set<string> | null,\n): string {\n const identity = ensureIdentity();\n synthesizeCrashes(store, identity.host_id, isAlive);\n if (live !== undefined) clearDeadWindows(store, identity.host_id, live);\n\n const envelope: Envelope = {\n schema_version: SCHEMA_VERSION,\n host_id: identity.host_id,\n display_name: identity.display_name,\n exported_at: Date.now(),\n };\n const lines = [\n JSON.stringify(envelope),\n ...store\n .eventsSince(identity.host_id, since)\n .map((event) => JSON.stringify(eventToWire(event))),\n ];\n return `${lines.join(\"\\n\")}\\n`;\n}\n","import type { Channel } from \"./channel.js\";\nimport { type Envelope, eventFromWire, SCHEMA_VERSION } from \"./export.js\";\nimport type { Store } from \"./store.js\";\nimport type { Event } from \"./types.js\";\n\n/**\n * How long a peer may go unfetched before it renders stale.\n *\n * Not derived from a collect interval, because murmur has no scheduler: there\n * is no timer here, and `collect` runs only when a command asks for it. In\n * practice the cadence is the operator's tmux `status-interval`, since\n * `murmur status` collects and tmux re-runs it on a tick.\n *\n * So this is a judgement about the operator's setup, not arithmetic on a\n * constant murmur controls. Sixty seconds is comfortably above a default 15s\n * status bar -- a peer needs to miss several ticks before it is called out,\n * which keeps one slow fetch from flickering the HUD. A status bar slower than\n * this will show every peer permanently stale; that is the number to change if\n * so.\n */\nexport const STALENESS_MS = 60_000;\n\n// A reachable peer is cheap — milliseconds on a warm control socket, still\n// only a couple hundred cold. The cap is not about those.\n//\n// It is about the unreachable ones. Each in-flight peer is a forked ssh client\n// process, and a peer that is asleep or off the VPN holds that process for the\n// full ConnectTimeout. Unbounded fan-out over a long list puts every one of\n// them resident at once, which is process churn and file descriptors spent on\n// hosts that were never going to answer.\n//\n// Eight keeps the realistic fleet fully parallel while bounding that.\nexport const MAX_CONCURRENT_PEERS = 8;\n\n// The cap alone does not bound the collect, which is what an earlier version of\n// this comment got wrong. The per-peer ssh timeout applies once per wave, so\n// nine unreachable peers cost two waves and seventeen cost three: the pool\n// serialises the timeouts it is there to limit. At a 5s status-interval that is\n// exactly the tick overlap the concurrency work set out to remove, just moved\n// to a longer peer list.\n//\n// So the whole collect gets its own deadline, independent of peer count. Peers\n// still in flight when it expires are abandoned and render stale, which is\n// already the designed outcome for a host that did not answer in time.\n//\n// Four seconds: under a 5s tick, and above one full wave (a 3s exec ceiling\n// plus overhead) so a single wave is never cut short by the deadline itself.\nexport const COLLECT_DEADLINE_MS = 4_000;\n\n/**\n * Runs `task` over `items` with at most `limit` in flight, preserving input\n * order in the output. Workers pull from a shared cursor rather than running\n * fixed batches, so one slow peer occupies a single slot instead of holding a\n * batch boundary.\n *\n * `deadline` bounds the whole run, not each task. Once it passes, workers stop\n * claiming new items and anything unstarted is left `undefined` for the caller\n * to treat as \"did not answer\". Tasks already in flight are not cancelled --\n * there is nothing to cancel a forked ssh with here -- but they no longer hold\n * the collect open, because the deadline races the pool rather than joining it.\n */\nasync function mapSettled<T, R>(\n items: readonly T[],\n limit: number,\n task: (item: T) => Promise<R>,\n deadline?: Promise<void>,\n): Promise<(PromiseSettledResult<R> | undefined)[]> {\n const results = new Array<PromiseSettledResult<R> | undefined>(items.length);\n let cursor = 0;\n let expired = false;\n const stop = deadline?.then(() => {\n expired = true;\n });\n const worker = async () => {\n while (cursor < items.length && !expired) {\n const index = cursor++;\n try {\n results[index] = { status: \"fulfilled\", value: await task(items[index] as T) };\n } catch (reason) {\n results[index] = { status: \"rejected\", reason };\n }\n }\n };\n const pool = Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n await (stop ? Promise.race([pool, stop]) : pool);\n return results;\n}\n\nexport type CollectResult = {\n peer: string;\n ok: boolean;\n ingested: number;\n error?: string;\n};\n\nfunction parseJsonl(output: string): { envelope: Envelope; events: Event[] } {\n const lines = output.trim().split(\"\\n\");\n const envelope = JSON.parse(lines.shift() ?? \"\") as Envelope;\n if (envelope.schema_version > SCHEMA_VERSION) {\n throw new Error(\n `unsupported schema version ${envelope.schema_version} (supports ${SCHEMA_VERSION})`,\n );\n }\n return {\n envelope,\n events: lines.map((line) => eventFromWire(JSON.parse(line) as Record<string, unknown>)),\n };\n}\n\n/**\n * Peers are fetched concurrently and applied serially.\n *\n * Concurrent because an unreachable peer costs the full ssh timeout, and a\n * serial loop charged that to every other peer behind it: three asleep laptops\n * made `murmur status` hang for thirty seconds and let the HUD tick overlap\n * itself. Fanning out makes the whole collect cost the slowest peer, not the\n * sum — capped at MAX_CONCURRENT_PEERS in flight and bounded overall by\n * COLLECT_DEADLINE_MS.\n *\n * Applied serially, in peer order, because better-sqlite3 is synchronous: there\n * is nothing to win by interleaving writes, and keeping the order stable keeps\n * the result list aligned with `store.peers()`.\n */\nexport async function collect(\n store: Store,\n channel: Channel,\n now = Date.now(),\n deadline?: Promise<void>,\n): Promise<CollectResult[]> {\n const results: CollectResult[] = [];\n let timer: NodeJS.Timeout | undefined;\n try {\n const peers = store.peers();\n // Default deadline, injectable so tests do not have to wait out real time.\n // Unref'd: a pending timer must not hold the process open after a CLI\n // command has printed its output and finished.\n const bounded =\n deadline ??\n new Promise<void>((resolve) => {\n timer = setTimeout(resolve, COLLECT_DEADLINE_MS);\n timer.unref?.();\n });\n // Settled, not raw: a peer that fails while we are still applying an\n // earlier one would otherwise be an unhandled rejection for as long as it\n // sits in the queue, which Node reports and future Node kills the process\n // over.\n const fetches = await mapSettled(\n peers,\n MAX_CONCURRENT_PEERS,\n async (peer) =>\n parseJsonl(\n await channel.exec(peer.target, [\"murmur\", \"export\", \"--since\", String(peer.watermark)]),\n ),\n bounded,\n );\n for (const [index, peer] of peers.entries()) {\n const fetch = fetches[index];\n try {\n // Undefined means the deadline passed before this peer was claimed or\n // finished. Not an error about the peer, so it says so plainly and\n // leaves fetched_at alone: the peer goes stale, which is the designed\n // outcome for a host that did not answer in time.\n if (!fetch) throw new Error(\"collect deadline passed before this peer answered\");\n if (fetch.status === \"rejected\") throw fetch.reason;\n const { envelope, events } = fetch.value;\n const ingested = store.ingest(events);\n const origin = events.filter((event) => event.host_id === envelope.host_id);\n const watermark = origin.reduce(\n (highest, event) => Math.max(highest, event.seq),\n peer.watermark,\n );\n store.upsertPeer({\n name: peer.name,\n target: peer.target,\n host_id: envelope.host_id,\n display_name: envelope.display_name,\n watermark,\n fetched_at: now,\n // New events mean the node is authoring again, so whatever a jump\n // observed about its tmux is out of date. Only clear on actual new\n // events: an export that returns nothing proves the binary ran, not\n // that tmux is back, which is the distinction that let a dead host\n // look healthy for three hours.\n //\n // Keyed on the watermark advancing, not on ingest's insert count.\n // Two reasons the count was wrong. Ingest is INSERT OR IGNORE, so a\n // retry after a partial apply re-sees the same events and reports\n // zero -- leaving a recovered host marked down until it happened to\n // author again. And the count includes rows from other origins that\n // this peer merely relayed, which say nothing about whether this\n // peer's tmux is back.\n tmux_down_at: watermark > peer.watermark ? null : peer.tmux_down_at,\n });\n results.push({ peer: peer.name, ok: true, ingested });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`murmur: collect: peer ${peer.name}: ${message}\\n`);\n results.push({ peer: peer.name, ok: false, ingested: 0, error: message });\n }\n }\n } catch (error) {\n process.stderr.write(\n `murmur: collect: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n } finally {\n clearTimeout(timer);\n }\n\n // Once per collect, outside the peer loop and outside its try, for two\n // reasons.\n //\n // It used to run per successful peer, so four peers meant four DELETEs with a\n // window function over the whole table on every status-bar tick, to enforce a\n // horizon measured in days. Idempotent work, repeated.\n //\n // And it ran only when a peer succeeded, so the single-machine case -- no\n // peers at all, the everyday path -- pruned never and grew local events\n // forever. The horizon is a property of the log, not of federation.\n //\n // Retention must not be able to fail a command, hence its own try.\n try {\n store.prune();\n } catch (error) {\n process.stderr.write(\n `murmur: collect: prune: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n }\n return results;\n}\n","import type { Command } from \"commander\";\nimport { ssh } from \"../channel.js\";\nimport { collect } from \"../collector.js\";\nimport { openStore } from \"../store.js\";\n\nexport function registerCollect(program: Command): void {\n program\n .command(\"collect\")\n .description(\"Collect events from configured peers\")\n .action(async () => {\n const store = openStore();\n try {\n await collect(store, ssh);\n } finally {\n store.close();\n }\n });\n}\n","import type { Command } from \"commander\";\nimport { exportJsonl } from \"../export.js\";\nimport { pidAlive, tmux } from \"../mux.js\";\nimport { openStore } from \"../store.js\";\n\nexport function registerExport(program: Command): void {\n program\n .command(\"export\")\n .description(\"Export local events as JSONL\")\n .requiredOption(\"--since <seq>\", \"export events after this sequence\", Number)\n .action((options: { since: number }) => {\n const store = openStore();\n try {\n process.stdout.write(exportJsonl(store, options.since, pidAlive, tmux.liveWindows()));\n } finally {\n store.close();\n }\n });\n}\n","import type { Command } from \"commander\";\nimport { ensureIdentity } from \"../identity.js\";\n\nexport function registerInit(program: Command): void {\n program\n .command(\"init\")\n .description(\"Initialize this node's identity\")\n .option(\"--name <name>\", \"display name\")\n .action((opts: { name?: string }) => {\n const identity = ensureIdentity(opts.name);\n console.log(`host_id: ${identity.host_id}`);\n console.log(`display_name: ${identity.display_name}`);\n });\n}\n","import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Command } from \"commander\";\n\nexport function registerLink(program: Command): void {\n program\n .command(\"link\")\n .description(\"Install a murmur integration\")\n .argument(\"<target>\", \"integration to install\")\n .action((target: string) => {\n if (target !== \"pi\") throw new Error(`unsupported link target: ${target}`);\n const destination = join(\n process.env.MURMUR_PI_HOME ?? homedir(),\n \".pi\",\n \"agent\",\n \"extensions\",\n \"murmur.ts\",\n );\n mkdirSync(dirname(destination), { recursive: true });\n\n // Pin the store import to this installation's absolute path. The\n // extension lives in ~/.pi/agent/extensions, where a bare\n // \"@martintrojer/murmur/extension-store\" specifier cannot resolve — not\n // even for a global install. Unpinned, every append silently no-ops:\n // the tmux badge still paints, so nothing looks broken while the log\n // stays empty and the node exports nothing.\n const source = readFileSync(\n fileURLToPath(new URL(\"./extension/murmur-pi.js\", import.meta.url)),\n \"utf8\",\n );\n const storePath = fileURLToPath(new URL(\"./extension/store.js\", import.meta.url));\n const pinned = source.replace(\n /\"@martintrojer\\/murmur\\/extension-store\"/,\n JSON.stringify(storePath),\n );\n if (pinned === source) {\n throw new Error(\"link pi: could not pin the store import; extension build changed\");\n }\n writeFileSync(destination, pinned);\n console.log(destination);\n });\n}\n","import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { Command } from \"commander\";\nimport { hasWarmSocket, ssh } from \"../channel.js\";\nimport type { Envelope } from \"../export.js\";\nimport { loadIdentity } from \"../identity.js\";\nimport { openStore } from \"../store.js\";\nimport type { Peer } from \"../types.js\";\n\nexport function parseSshHosts(config: string): string[] {\n const hosts: string[] = [];\n for (const line of config.split(\"\\n\")) {\n const tokens = line.replace(/#.*$/, \"\").trim().split(/\\s+/);\n if (tokens[0]?.toLowerCase() !== \"host\") continue;\n for (const host of tokens.slice(1)) {\n if (!/[*?!]/.test(host)) hosts.push(host);\n }\n }\n return hosts;\n}\n\nfunction sshHosts(): string[] {\n try {\n return parseSshHosts(readFileSync(join(homedir(), \".ssh\", \"config\"), \"utf8\"));\n } catch {\n return [];\n }\n}\n\n/**\n * Column-aligned plain text. Rows are all-ASCII here (peer names, ssh targets\n * and hostnames), so `length` is a fine width; trailing cells are not padded so\n * the output stays clean for `cut` and friends.\n */\nexport function formatTable(rows: string[][]): string {\n const widths: number[] = [];\n for (const row of rows) {\n row.forEach((cell, index) => {\n widths[index] = Math.max(widths[index] ?? 0, cell.length);\n });\n }\n return rows\n .map((row) =>\n row\n .map((cell, index) => (index === row.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)))\n .join(\" \")\n .trimEnd(),\n )\n .map((line) => `${line}\\n`)\n .join(\"\");\n}\n\n/**\n * Whether `peer add` must refuse, and what to say. Returns null to proceed.\n *\n * Split out of the commander action because that action opens a store, shells\n * out over ssh and sets process.exitCode, so the rules below were unreachable\n * from a test: the suite ended up asserting a reimplementation of this lookup\n * instead, and disabling the real branch left it green.\n */\nexport function peerAddDecision(input: {\n name: string;\n target: string;\n envelope: Envelope | null;\n selfHostId: string | null;\n peers: Peer[];\n}): string | null {\n const { name, target, envelope, selfHostId, peers } = input;\n // No identity means an unreachable host. It is still added, on the operator's\n // word, and the first successful collect fills in who it is.\n if (!envelope) return null;\n\n // Adding yourself would fold your own events back in as a \"remote\" host and\n // collect over ssh to reach a database you already hold.\n if (envelope.host_id === selfHostId) {\n return `${target} is this node; not adding it as a peer\\n`;\n }\n\n // One node, one peer. Two names for one host_id means two ssh round-trips per\n // command and the same machine listed twice; the events dedupe on\n // (host_id, seq), so nothing looks wrong until you notice every collect is\n // doing double the work. Excluding `name` itself keeps re-adding the same\n // peer idempotent, which is how a target gets corrected.\n const existing = peers.find(\n (candidate) => candidate.host_id === envelope.host_id && candidate.name !== name,\n );\n if (existing) {\n return (\n `${target} is already configured as peer \"${existing.name}\" ` +\n `(${envelope.display_name}); remove it first to rename\\n`\n );\n }\n return null;\n}\n\nexport function registerPeer(program: Command): void {\n const peer = program.command(\"peer\").description(\"Manage peers\");\n\n peer\n .command(\"add\")\n .description(\"Add a peer and discover its identity\")\n // The decision itself is `peerAddDecision` below, so it can be tested\n // without an ssh binary or a commander harness.\n .argument(\"<name>\")\n .argument(\"[target]\")\n .action(async (name: string, target = name) => {\n const store = openStore();\n try {\n // Probe BEFORE writing. Identity is discovered, so the probe is what\n // tells us whether this is a node we already have under another name\n // — and a peer written first would be found by its own duplicate\n // check.\n let envelope: Envelope | null = null;\n try {\n const output = await ssh.exec(target, [\"murmur\", \"export\", \"--since\", \"0\"]);\n envelope = JSON.parse(output.trim().split(\"\\n\")[0] ?? \"\") as Envelope;\n } catch {\n envelope = null;\n }\n\n const refusal = peerAddDecision({\n name,\n target,\n envelope,\n selfHostId: loadIdentity()?.host_id ?? null,\n peers: store.peers(),\n });\n if (refusal) {\n process.stderr.write(refusal);\n process.exitCode = 1;\n return;\n }\n\n store.upsertPeer({\n name,\n target,\n host_id: envelope?.host_id ?? null,\n display_name: envelope?.display_name ?? null,\n });\n process.stdout.write(\n envelope\n ? `Added ${name} (${envelope.display_name})\\n`\n : `Added ${name} (identity pending)\\n`,\n );\n } finally {\n store.close();\n }\n });\n\n peer\n .command(\"remove\")\n .description(\"Remove a peer\")\n .argument(\"<name>\", \"peer to remove\")\n .action((name: string) => {\n const store = openStore();\n try {\n if (store.removePeer(name)) process.stdout.write(`Removed ${name}\\n`);\n else {\n process.stderr.write(`no such peer: ${name}\\n`);\n process.exitCode = 1;\n }\n } finally {\n store.close();\n }\n });\n\n peer\n .command(\"list\")\n .description(\"List configured peers\")\n .option(\"--json\", \"print JSON\")\n .action((options: { json?: boolean }) => {\n const store = openStore();\n try {\n const peers = store.peers();\n if (options.json) {\n process.stdout.write(`${JSON.stringify(peers)}\\n`);\n return;\n }\n if (peers.length === 0) {\n process.stdout.write(\"no peers configured\\n\");\n return;\n }\n const rows = [\n // HOSTNAME, not HOST: this is what the node reported about itself,\n // which is not the handle any other command takes. NAME is.\n [\"NAME\", \"TARGET\", \"HOSTNAME\"],\n ...peers.map((configured) => [\n configured.name,\n configured.target,\n configured.display_name ?? \"unknown\",\n ]),\n ];\n process.stdout.write(formatTable(rows));\n } finally {\n store.close();\n }\n });\n\n peer\n .command(\"discover\")\n .description(\"Check SSH hosts for warm control sockets\")\n .action(() => {\n for (const host of sshHosts()) {\n process.stdout.write(`${hasWarmSocket(host) ? \"[x]\" : \"[ ]\"} ${host}\\n`);\n }\n });\n}\n","import { spawnSync } from \"node:child_process\";\nimport type { Command } from \"commander\";\nimport {\n type Agent,\n agentLabel,\n agentLocation,\n forgetOneAgent,\n jumpToAgent,\n terminalText,\n} from \"../agents.js\";\nimport { glance } from \"../glance.js\";\nimport { loadIdentity } from \"../identity.js\";\nimport { status, statusWithCollect } from \"../status.js\";\nimport { openStore, type Store } from \"../store.js\";\n\ntype PickOptions = { all?: boolean };\n\nconst PREVIEW_EVENTS = 8;\nconst PREVIEW_MESSAGE_MAX = 300;\n\n// Same glyphs the tmux status bar and window labels use, so one symbol means\n// one thing in every surface. Ported from the dotfiles' _tmux_common.\nconst GLYPH: Record<string, string> = {\n crashed: \"\\u2717\", // ✗\n blocked: \"!\",\n done: \"\\u2713\", // ✓\n working: \"\\u25b6\", // ▶\n idle: \"\\u00b7\", // ·\n};\n\n// Mirrors the window-glyph colours: red needs you now, peach needs you soon,\n// teal is finished-unseen, grey is busy or idle and carries no signal.\nconst COLOUR: Record<string, string> = {\n crashed: \"\\u001b[31m\",\n blocked: \"\\u001b[33m\",\n done: \"\\u001b[36m\",\n working: \"\\u001b[37m\",\n idle: \"\\u001b[90m\",\n};\n// Built from a char class rather than written literally: a bare \\u001b in a\n// regex trips biome's noControlCharactersInRegex, and the rule is right that\n// an invisible byte in a pattern is a hazard.\nconst ANSI_PATTERN = `${String.fromCharCode(27)}\\\\[[0-9;]*m`;\nconst ANSI_ESCAPE = new RegExp(ANSI_PATTERN, \"g\");\n// Non-global twin for anchored single matches: `exec` on a /g/ regex carries\n// lastIndex between calls, so reusing ANSI_ESCAPE inside a loop silently skips\n// sequences.\nconst ANSI_AT_START = new RegExp(`^${ANSI_PATTERN}`);\nconst ANSI_AT_END = new RegExp(`(?:${ANSI_PATTERN})+$`);\n// Remote rows get a colour of their own: cyan reads as \"elsewhere\" without\n// competing with the state colours, which own red/peach/teal.\nconst REMOTE = \"\\u001b[36m\";\nconst BOLD = \"\\u001b[1m\";\nconst DIM = \"\\u001b[2m\";\nconst RESET = \"\\u001b[0m\";\n\n// Attention order, and the order the prompt counts appear in.\nconst URGENCY = [\"crashed\", \"blocked\", \"done\", \"working\", \"idle\"] as const;\n\n/**\n * Column widths, in one place because the header and the rows must agree. They\n * were duplicated as literals in two functions and had already drifted by a\n * column once.\n */\nconst COLUMNS = {\n glyph: 3, // marker + state glyph\n state: 8,\n name: 30,\n stream: 13,\n streamWide: 18, // when no host column is shown\n host: 14,\n} as const;\n\n/**\n * The column header fzf pins above the list.\n *\n * Built from COLUMNS so it cannot drift from the rows, and dim so it reads as\n * furniture rather than as an agent.\n */\nexport function headerRow(showHost: boolean): string {\n return [\n \" \".repeat(COLUMNS.glyph),\n pad(\"state\", COLUMNS.state),\n pad(\"agent\", COLUMNS.name),\n pad(\"stream\", showHost ? COLUMNS.stream : COLUMNS.streamWide),\n showHost ? pad(\"host\", COLUMNS.host) : \"\",\n \"age / flags\",\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\n/**\n * State filter keys — an axis kept separate from the text query, so ctrl-b\n * shows blocked agents rather than searching for the word \"blocked\" (which\n * would also match an agent merely *named* that). Inherited wholesale from the\n * old picker, including the choice to shadow fzf defaults: the query here is a\n * word or two, so home/left/bspace still cover the editing jobs.\n */\nconst FILTER_KEYS: [string, string][] = [\n [\"ctrl-a\", \"\"],\n [\"ctrl-x\", \"crashed\"],\n [\"ctrl-b\", \"blocked\"],\n [\"ctrl-d\", \"done\"],\n [\"ctrl-w\", \"working\"],\n];\n\nfunction timestamp(ts: number): string {\n return new Date(ts).toLocaleTimeString([], {\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n}\n\n/**\n * Human age. Blank under a minute: a row that just changed does not need a\n * column saying so, and \"0s\" on every live agent is noise that hides the one\n * row reading \"3h\".\n */\nfunction age(ms: number | null): string {\n if (ms === null || ms < 60_000) return \"\";\n if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;\n if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h`;\n return `${Math.floor(ms / 86_400_000)}d`;\n}\n\n/**\n * Fit a cell to exactly `width` visible columns, padding or truncating.\n *\n * Both halves are needed. Padding counts VISIBLE length, because a value\n * wrapped in bold plus reset carries nine escape bytes and `padEnd` counts\n * them, which pads nine short and shears every column to its right.\n *\n * Truncating is what was missing: `pad` only ever grew a string, so one long\n * agent name (\"Gchatui 2026 Rebaseline Finalization\", 36 chars in a 30-wide\n * column) pushed the host and flags columns right and broke the grid for that\n * row only. Long pi session names are the normal case, not an edge one.\n *\n * The truncation walks the string and copies escape sequences through without\n * counting them, so a cut never lands inside one. Cutting mid-sequence would\n * leak the colour into the rest of the line and drop the reset that ends it.\n */\nfunction pad(value: string, width: number): string {\n const visible = [...value.replace(ANSI_ESCAPE, \"\")].length;\n if (visible <= width) return value + \" \".repeat(width - visible);\n\n // Room for the ellipsis, which is one column wide.\n const budget = Math.max(0, width - 1);\n let out = \"\";\n let shown = 0;\n let index = 0;\n while (index < value.length && shown < budget) {\n const sequence = ANSI_AT_START.exec(value.slice(index));\n if (sequence) {\n out += sequence[0];\n index += sequence[0].length;\n continue;\n }\n out += value[index];\n index += 1;\n shown += 1;\n }\n // Copy any trailing escapes (the reset) so the cell closes its own styling.\n const tail = value.slice(index).match(ANSI_AT_END);\n return `${out}\\u2026${tail?.[0] ?? \"\"}${\" \".repeat(Math.max(0, width - budget - 1))}`;\n}\n\n/**\n * Are we running inside a `display-popup` rather than a pane?\n *\n * tmux exports $TMUX to a popup but not $TMUX_PANE, because a popup is not a\n * pane. Outside tmux neither is set, so the three cases stay distinguishable\n * with no tmux call.\n */\nexport function isPopup(env: NodeJS.ProcessEnv): boolean {\n return Boolean(env.TMUX) && !env.TMUX_PANE;\n}\n\n/**\n * One fzf row: a hidden key column, a hidden filter column, then the label.\n *\n * The key is `agent_id`, not a tmux target: a target only means something on\n * the agent's own host, so resolving it is `jumpToAgent`'s job once a selection\n * comes back.\n */\nexport function pickerRow(agent: Agent, showHost: boolean, current: boolean, local = true): string {\n const state = agent.state ?? \"idle\";\n const colour = COLOUR[state] ?? \"\";\n const glyph = GLYPH[state] ?? \"?\";\n const marker = current ? `${BOLD}\\u25c6${RESET}` : \" \"; // ◆ you are here\n // Richest name first: mu names its agents, pi names its sessions, tmux names\n // windows. All three travel on the event, recorded by the node that owns the\n // pane, so this reads the same for a local and a remote agent.\n const name = agent.agent_name ?? agent.pi_session ?? agentLabel(agent);\n // Local and remote must be tellable apart at a glance. Two hostnames in one\n // dim column means you have to know your own machine's name to read the list\n // — and the difference is not cosmetic: a local row is a keystroke away, a\n // remote one costs an ssh and a nested tmux.\n //\n // \"here\" rather than the local hostname, because the reader already knows\n // which machine they are on; what they need is which rows are not it. Remote\n // hosts keep their name and get an arrow, so the column scans as \"here /\n // elsewhere\" before you read any words.\n // Both forms start in the same column: a leading space where the arrow would\n // be, so \"here\" and \"→ bubba\" line up and the arrows form a single vertical\n // run you can scan without reading a word.\n const host = showHost\n ? local\n ? `${DIM} here${RESET}`\n : `${REMOTE}\\u2192 ${terminalText(agent.host)}${RESET}`\n : \"\";\n // Workstream if mu set one, otherwise the tmux session name. Both answer\n // \"which piece of work is this\", and only mu-spawned agents have a\n // workstream, so the column was empty for most human agents.\n //\n // The session name is also what the tms picker shows and what you have\n // trained yourself to search on: a session called `hacking/murmur` holding a\n // pi whose window is named `Python` was unfindable by typing `murmur`. This\n // is the one thing tms had that murmur did not, and folding whole sessions\n // into this list was the wrong way to get it -- a session without an agent\n // has no place here.\n const group = agent.workstream ?? agent.session_name;\n const workstream = group ? `${DIM}${terminalText(group)}${RESET}` : \"\";\n // Two ages, and the one worth showing is how old the AGENT'S news is, not\n // how recently we reached its host. A peer we polled a second ago can be\n // serving events from three hours back — which read as fresh until this\n // column existed. `unreachable` is the other axis: the replica itself is old.\n const flags = [\n agent.driver === \"orchestrated\" ? \"crew\" : \"\",\n agent.stale ? \"unreachable\" : \"\",\n // A jump already proved this one dead. Say so plainly rather than leaving\n // the row looking merely old, and sort it last.\n agent.tmux_down ? \"no tmux\" : \"\",\n age(agent.event_age_ms),\n ]\n .filter(Boolean)\n .join(\" \");\n // The state word is IN the label, not a hidden column. fzf's --with-nth\n // re-indexes fields, so any --nth that excluded the label broke plain\n // name matching (typing \"glance\" returned 0/4). Keeping state visible costs\n // eight columns and makes both the ctrl-key filters and text search work on\n // one field set — and the word is worth reading anyway.\n const label = [\n `${marker} ${colour}${glyph}${RESET}`,\n `${colour}${pad(state, COLUMNS.state)}${RESET}`,\n pad(`${BOLD}${terminalText(name)}${RESET}`, COLUMNS.name),\n pad(workstream, showHost ? COLUMNS.stream : COLUMNS.streamWide),\n showHost ? pad(host, COLUMNS.host) : \"\",\n flags ? `${DIM}${flags}${RESET}` : \"\",\n ]\n .filter(Boolean)\n .join(\" \");\n return `${agent.agent_id}\\t${label}`;\n}\n\nfunction previewText(store: Store, agent: Agent): string {\n const state = agent.state ?? \"idle\";\n const colour = COLOUR[state] ?? \"\";\n const head = [\n `${colour}${GLYPH[state] ?? \"?\"} ${state}${RESET} ${BOLD}${agent.agent_name ? terminalText(agent.agent_name) : agentLabel(agent)}${RESET}`,\n // Says where, and whether \"where\" is this machine. The glance below is a\n // local capture-pane or an ssh depending on this one fact, so it belongs in\n // the header rather than being inferred from a hostname.\n agent.host_id === loadIdentity()?.host_id\n ? `${DIM}here ${agentLocation(agent)}${RESET}`\n : `${REMOTE}\\u2192 ${terminalText(agent.host)}${RESET} ${DIM}${agentLocation(agent)}${RESET}`,\n ];\n const facts = [\n agent.workstream ? `stream ${terminalText(agent.workstream)}` : \"\",\n agent.role ? `role ${terminalText(agent.role)}` : \"\",\n agent.pi_session ? `session ${terminalText(agent.pi_session)}` : \"\",\n agent.driver === \"orchestrated\" ? \"driver orchestrated (crew)\" : \"\",\n agent.stale ? `fetched ${age(agent.age_ms)} ago` : \"\",\n ].filter(Boolean);\n\n // The glance is the point of the preview: what is the agent actually doing.\n // Events are history and answer a different question, so they go underneath\n // and stay short.\n const pane = glance(store, agent);\n const live = pane?.trimEnd()\n ? [`${DIM}── pane ──${RESET}`, pane.trimEnd()]\n : [`${DIM}── pane ──${RESET}`, `${DIM}unavailable (host unreachable, or pane gone)${RESET}`];\n\n const events = store\n .allEvents()\n .filter((event) => event.agent_id === agent.agent_id)\n .slice(-PREVIEW_EVENTS);\n const history = events.length\n ? events.map((event) => {\n let message = terminalText(event.message);\n if (message.length > PREVIEW_MESSAGE_MAX) {\n message = `${message.slice(0, PREVIEW_MESSAGE_MAX)}…`;\n }\n const detail = message && message !== event.state ? ` ${message}` : \"\";\n return `${DIM}${timestamp(event.ts)}${RESET} ${terminalText(event.state).padEnd(8)}${detail}`;\n })\n : [`${DIM}no recorded events${RESET}`];\n\n return [...head, \"\", ...facts, \"\", ...live, \"\", `${DIM}── history ──${RESET}`, ...history].join(\n \"\\n\",\n );\n}\n\n/**\n * Emit the preview body for one agent. `murmur pick` re-invokes itself here so\n * fzf's `--preview` has a per-row command, rather than the picker precomputing\n * every preview up front — which would mean an ssh round-trip per remote agent\n * before the list even paints.\n */\nexport function runPreview(store: Store, agentId: string): void {\n // Runs as a child of a picker that has just collected, so it reads the store\n // directly rather than syncing again.\n const agent = status(store).agents.find((candidate) => candidate.agent_id === agentId);\n if (!agent) return;\n process.stdout.write(`${previewText(store, agent)}\\n`);\n}\n\nexport async function runPick(store: Store, options: PickOptions = {}): Promise<void> {\n const identity = loadIdentity();\n const view = await statusWithCollect(store);\n const agents = view.agents.filter((agent) => options.all || agent.driver === \"human\");\n const hidden = view.agents.length - agents.length;\n\n if (agents.length === 0) {\n process.stdout.write(\n hidden ? `No human agents (+${hidden} crew — rerun with --all)\\n` : \"No agents\\n\",\n );\n return;\n }\n\n const showHost = agents.some((agent) => agent.host_id !== identity?.host_id);\n const currentPane = process.env.TMUX_PANE ?? \"\";\n const input = agents\n .map((agent) =>\n pickerRow(agent, showHost, agent.pane === currentPane, agent.host_id === identity?.host_id),\n )\n .join(\"\\n\");\n\n const counts = new Map<string, number>();\n for (const agent of agents) {\n const state = agent.state ?? \"idle\";\n counts.set(state, (counts.get(state) ?? 0) + 1);\n }\n const prompt = URGENCY.filter((state) => counts.get(state))\n .map((state) => `${COLOUR[state]}${GLYPH[state]}${counts.get(state)}${RESET}`)\n .join(\" \");\n\n const self = process.argv[1] ?? \"murmur\";\n const allFlag = options.all ? \" --all\" : \"\";\n const inPopup = isPopup(process.env);\n // A preview beside the list needs room for both. Below ~150 columns the\n // 58% split squeezes the host and flags columns off the end, so start\n // stacked and let ctrl-p cycle from there.\n const width = process.stdout.columns ?? 0;\n const previewLayout =\n width > 0 && width < 150 ? \"bottom:60%,border-top,wrap\" : \"right:58%,border-left,wrap\";\n const preview = `${process.execPath} ${self} pick --preview {1}`;\n // Narrow on the hidden state column with an exact-prefix query, then restore\n // the real query. ctrl-a clears it.\n const filterBinds = FILTER_KEYS.flatMap(([key, state]) => [\n \"--bind\",\n state ? `${key}:change-query(${state})` : `${key}:change-query()`,\n ]);\n\n const result = spawnSync(\n \"fzf\",\n [\n \"--delimiter\",\n \"\\t\",\n \"--with-nth\",\n \"2..\",\n \"--ansi\",\n // Literal substring matching, and matching only the visible columns.\n // Default fuzzy scatters query characters across the row: `re` matched\n // \"Fix Murmur Pick Fzf Filter\" as well as \"recovered\". A query here is a\n // word or two of an agent or workstream name, so substring is what the\n // fingers expect. Prefix a token with ' to opt back into fuzzy.\n // Same choice as the tms session picker, for consistency across the two.\n \"--exact\",\n // `begin` ranks earlier match positions higher, so `scratch` puts the\n // scratch workstream above a row that merely mentions it. `index` is the\n // empty-query fallback and preserves the attention order the fold\n // produced, which is the whole point of the list.\n \"--tiebreak\",\n \"begin,index\",\n \"--layout\",\n \"reverse\",\n // `display-popup` draws its own border, so fzf's is a second one a\n // character inside the first. A popup is the normal way to run this, via\n // the prefix+a binding, so the doubled frame was what you saw most.\n //\n // Detected by $TMUX set with $TMUX_PANE unset: tmux exports TMUX to a\n // popup but not TMUX_PANE, since a popup is not a pane. Outside tmux\n // neither is set, so the three cases stay distinguishable.\n \"--border\",\n inPopup ? \"none\" : \"rounded\",\n \"--info\",\n \"inline\",\n \"--prompt\",\n `${prompt}${prompt ? \" \" : \"\"}`,\n \"--header\",\n [\n `enter jump ^r refresh ^p preview del forget filter: ${FILTER_KEYS.map(\n ([key, state]) => `${key.replace(\"ctrl-\", \"^\")} ${state || \"all\"}`,\n ).join(\" \")}`,\n hidden ? `${hidden} crew hidden (--all)` : \"\",\n headerRow(showHost),\n ]\n .filter(Boolean)\n .join(\"\\n\"),\n \"--preview\",\n preview,\n // Narrow terminals cannot show both the columns and a 58% preview, and\n // the columns are the point of the list. ctrl-p cycles right / bottom /\n // hidden, so every column is reachable on a small viewport without\n // giving up the glance entirely.\n \"--preview-window\",\n previewLayout,\n \"--bind\",\n \"ctrl-p:change-preview-window(bottom:60%,border-top,wrap|hidden|right:58%,border-left,wrap)\",\n \"--bind\",\n `ctrl-r:reload(${process.execPath} ${self} pick --rows${allFlag})`,\n // Manual dismissal for a row nothing else will clear.\n //\n // The delete key, not a ctrl chord. ctrl-shift-d does not exist -- a\n // terminal sends the same bytes as ctrl-d -- and ctrl-alt-d, while it\n // does dispatch distinctly, sits one modifier away from ctrl-d in a\n // header that lists both. One is a filter and the other destroys a row,\n // so a near-miss is a deleted agent. `delete` is the key that already\n // means remove this, and it collides with no filter letter.\n \"--bind\",\n `delete:reload(${process.execPath} ${self} pick --forget {1}${allFlag})`,\n ...filterBinds,\n \"--no-select-1\",\n \"--no-exit-0\",\n ],\n {\n input,\n encoding: \"utf8\",\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n // FZF_DEFAULT_OPTS can carry a conflicting layout or bindings from the\n // user's shell; the old picker stripped it for the same reason.\n env: Object.fromEntries(\n Object.entries(process.env).filter(([key]) => !key.startsWith(\"FZF_DEFAULT_OPTS\")),\n ),\n },\n );\n\n const selected = result.stdout?.trim().split(\"\\t\")[0];\n if (!selected) return;\n const agent = agents.find((candidate) => candidate.agent_id === selected);\n if (!agent) return;\n const jump = jumpToAgent(store, agent);\n // A popup closes the moment this returns, so a bare failure looked exactly\n // like \"enter did nothing\". Say what happened and fail loudly.\n if (!jump.ok) {\n process.stderr.write(`${jump.message}\\n`);\n process.exitCode = 1;\n }\n}\n\n/**\n * Delete one agent, then print the remaining rows.\n *\n * One command rather than two because fzf's `reload` replaces the list with a\n * command's stdout: doing the delete and the reprint separately would race the\n * reload against the delete and redraw the row it had just removed.\n */\nexport async function runForget(\n store: Store,\n agentId: string,\n options: PickOptions = {},\n): Promise<void> {\n const view = status(store);\n const agent = view.agents.find((candidate) => candidate.agent_id === agentId);\n if (agent) forgetOneAgent(store, agent);\n await runRows(store, options);\n}\n\n/** Print the row list only, for fzf's `reload` binding. */\nexport async function runRows(store: Store, options: PickOptions = {}): Promise<void> {\n const identity = loadIdentity();\n const view = await statusWithCollect(store);\n const agents = view.agents.filter((agent) => options.all || agent.driver === \"human\");\n const showHost = agents.some((agent) => agent.host_id !== identity?.host_id);\n const currentPane = process.env.TMUX_PANE ?? \"\";\n for (const agent of agents) {\n process.stdout.write(\n `${pickerRow(agent, showHost, agent.pane === currentPane, agent.host_id === identity?.host_id)}\\n`,\n );\n }\n}\n\nexport function registerPick(program: Command): void {\n program\n .command(\"pick\")\n .description(\"Pick an agent and jump to it\")\n .option(\"--all\", \"include orchestrated agents\")\n .option(\"--preview <agent-id>\", \"render the preview pane for one agent (internal)\")\n .option(\"--rows\", \"print picker rows only (internal, for reload)\")\n .option(\"--forget <agent-id>\", \"drop one agent, then print rows (internal)\")\n .action(\n async (options: PickOptions & { preview?: string; rows?: boolean; forget?: string }) => {\n const store = openStore();\n try {\n if (options.preview) runPreview(store, options.preview);\n else if (options.forget) await runForget(store, options.forget, options);\n else if (options.rows) await runRows(store, options);\n else await runPick(store, options);\n } finally {\n store.close();\n }\n },\n );\n}\n","import { spawnSync } from \"node:child_process\";\nimport { SSH_OPTIONS } from \"./channel.js\";\nimport { loadIdentity } from \"./identity.js\";\nimport { type Mux, tmux } from \"./mux.js\";\nimport type { Status } from \"./status.js\";\nimport type { Store } from \"./store.js\";\n\nexport type Agent = Status[\"agents\"][number];\n\n/**\n * The most specific human-readable name an agent has, never a tmux id.\n *\n * Four sources, most to least specific: mu's agent name, pi's session name,\n * the tmux window name, the tmux session name. The old picker showed window\n * names and that was the thing it did better than raw `$26:@79`; these are all\n * recorded on the event, so this reads the same for a local and a remote agent.\n *\n * Falls back to the window id only when a node recorded no names at all, which\n * means a pre-names event or a non-tmux harness.\n */\nexport function agentLabel(agent: Agent): string {\n const name = agent.agent_name ?? agent.pi_session ?? agent.window_name ?? agent.session_name;\n return terminalText(name ?? agent.window);\n}\n\n/**\n * Where the agent lives, for the second column. Names only -- the ids are what\n * jumps, not what a human reads.\n */\nexport function agentLocation(agent: Agent): string {\n const session = agent.session_name ?? agent.session;\n const window = agent.window_name ?? agent.window;\n return terminalText(session === window ? session : `${session}:${window}`);\n}\n\nexport function terminalText(value: string): string {\n return [...value]\n .map((character) => {\n const code = character.charCodeAt(0);\n return code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f) ? \"�\" : character;\n })\n .join(\"\");\n}\n\nexport function shellQuote(value: string): string {\n return `'${value.replaceAll(\"'\", `'\\\\''`)}'`;\n}\n\n/**\n * The one process call jump makes that is not a tmux command: the remote probe,\n * and the direct ssh attach when we are not inside tmux. Injectable so the jump\n * decision table can be tested without an ssh binary or a live peer -- without\n * this seam, `jumpToAgent` had no behavioural coverage at all and replacing its\n * body with `return { ok: true }` kept every jump test green.\n */\nexport type Runner = (\n file: string,\n args: string[],\n inherit?: boolean,\n) => { status: number | null; stdout: string; failed: boolean };\n\nexport const spawnRunner: Runner = (file, args, inherit = false) => {\n const result = spawnSync(file, args, {\n encoding: \"utf8\",\n timeout: 10_000,\n ...(inherit ? { stdio: \"inherit\" as const } : {}),\n });\n return {\n status: result.status,\n stdout: result.stdout ?? \"\",\n // spawnSync reports a failure to even start the child in `error`, leaving\n // status null. Collapsing both here keeps the decision table below reading\n // as one question rather than two.\n failed: result.error !== undefined,\n };\n};\n\nexport type JumpResult =\n | { ok: true }\n | {\n ok: false;\n reason: \"no_peer\" | \"unreachable\" | \"no_tmux\" | \"window_gone\" | \"attach_failed\";\n message: string;\n };\n\n/**\n * Drop a dead agent's rows from the local replica.\n *\n * Export on the authoring node clears dead windows, but that only runs when the\n * peer is next polled, and a window can die between a fetch and a jump. When a\n * jump proves the window is gone, the agent should leave this HUD now rather\n * than at the next collect.\n *\n * DELETE rather than append a `cleared` event, because this node cannot author\n * an event about another node's agent. `store.append` stamps the local host_id,\n * and `status()` folds local and remote events separately (local needs a pid\n * check, remote cannot have one) -- so a local row about a remote agent lands\n * in the other fold and shows up as a SECOND agent with the same agent_id,\n * which is exactly what it did before this was a delete.\n *\n * Deleting a replica is safe ONLY IF the rows can come back, and that needs the\n * peer's watermark rewound as well. Ingest asks for events after the watermark,\n * so deleting rows below it deletes them permanently: bubba's agents vanished\n * from the picker and no amount of collecting brought them back, even with the\n * node alive and the events still in its log.\n *\n * Rewinding to zero rather than to the deleted seq: the log is bounded by the\n * retention horizon, ingest is idempotent on (host_id, seq), and a re-read of a\n * small table is cheaper than tracking which seq belonged to which agent. The\n * next collect re-reads everything the peer still has, so if the window is\n * genuinely alive the agent reappears -- which is the answer to the race where\n * the host comes back up between the jump and the next poll.\n *\n * For a local agent there is no watermark and nothing to rewind: the pane is\n * gone, so nothing will ever author about it again.\n */\n/**\n * A jump proved this peer has no tmux server, so none of its agents exist.\n *\n * Drops every replicated row for that origin and rewinds the watermark, the\n * same recoverable delete `forgetReplica` does for one agent — just scoped to\n * the node, because \"no tmux server\" is a fact about the host rather than about\n * the window we happened to aim at. Leaving the rows and only labelling them\n * meant the picker kept offering four dead agents you had just been told were\n * gone.\n *\n * The mark stays on the peer as well: it is what stops an empty export being\n * read as recovery, and it is why the rows do not immediately reappear.\n */\nexport function forgetHostReplica(store: Store, hostId: string): void {\n try {\n const peer = store.peers().find((candidate) => candidate.host_id === hostId);\n store.forgetHost(hostId);\n if (peer) {\n // Watermark deliberately NOT rewound here, unlike the single-agent case.\n // Rewinding re-ingests the very rows just deleted, and because the\n // collector reads any ingest as \"the node is authoring again\", it also\n // cleared the mark -- so the dead agents reappeared looking healthy on\n // the next collect, one second later.\n //\n // Keeping the watermark means recovery waits for a NEW event, which is\n // the correct bar: the node has to actually say something before its\n // agents come back. Nothing is lost, since the rows describe windows a\n // live tmux server would re-announce.\n store.upsertPeer({\n name: peer.name,\n target: peer.target,\n tmux_down_at: Date.now(),\n });\n }\n } catch {\n // Advisory only: the next collect reconciles either way.\n }\n}\n\nexport function forgetReplica(store: Store, agentId: string, hostId: string): void {\n try {\n store.forgetAgent(agentId);\n const peer = store.peers().find((candidate) => candidate.host_id === hostId);\n if (peer) store.upsertPeer({ name: peer.name, target: peer.target, watermark: 0 });\n } catch {\n // Cosmetic only: the next collect reconciles either way.\n }\n}\n\n/**\n * Drop one agent from the picker by hand.\n *\n * The escape hatch for a row that is stuck and that nothing else will clear: an\n * agent whose pane died in a way that left no terminal event, or a replica from\n * a peer that will never report again. Everything else here reconciles on its\n * own, so this exists for the cases that do not.\n *\n * A local agent also gets its tmux badge cleared. Deleting only the row would\n * leave `@agent_state` set, which the status bar and the tms session picker\n * both read — so the glyph would survive the row it came from and nothing would\n * ever clear it.\n *\n * Not authoritative, and cannot be: for a remote agent this deletes a replica,\n * and the owning node still holds the truth. If that node reports again the\n * agent comes back, which is correct — a row you dismissed while the agent was\n * alive should return.\n */\nexport function forgetOneAgent(store: Store, agent: Agent, mux: Mux = tmux): void {\n const identity = loadIdentity();\n if (agent.host_id === identity?.host_id) {\n try {\n mux.setState(agent.window, null);\n } catch {\n // Best effort: the row still goes.\n }\n }\n forgetReplica(store, agent.agent_id, agent.host_id);\n}\n\nexport function jumpToAgent(\n store: Store,\n agent: Agent,\n mux: Mux = tmux,\n run: Runner = spawnRunner,\n): JumpResult {\n const identity = loadIdentity();\n if (agent.host_id === identity?.host_id) {\n const live = mux.liveWindows();\n if (live && !live.has(agent.window)) {\n forgetReplica(store, agent.agent_id, agent.host_id);\n return {\n ok: false,\n reason: \"window_gone\",\n message: `${agentLabel(agent)} is gone -- its window no longer exists. Cleared.`,\n };\n }\n // Reporting the attach rather than assuming it. A select-window that fails\n // is the local twin of the remote symptom: the picker closes, nothing\n // moves, and nothing says why.\n if (!mux.attach(agent.session, agent.window)) {\n return {\n ok: false,\n reason: \"attach_failed\",\n message: `could not attach to ${agentLabel(agent)} (tmux select-window failed).`,\n };\n }\n return { ok: true };\n }\n const peer = store.peers().find((candidate) => candidate.host_id === agent.host_id);\n const target = peer?.target ?? peer?.name;\n if (!target) {\n return {\n ok: false,\n reason: \"no_peer\",\n message: `No peer configured for host ${agent.host_id.slice(0, 8)}. Try: murmur peer add <target>`,\n };\n }\n\n // Check the window is still there before opening a window to attach to it.\n // Without this the attach fails inside a new tmux window that closes\n // instantly, which is indistinguishable from \"enter did nothing\" -- the\n // symptom that sent us looking for a quoting bug that did not exist.\n // ssh does not take an argv: it joins its arguments and hands the string to a\n // shell on the far side. An unquoted `#{window_id}` is mangled by that shell\n // and tmux answers `-F expects an argument`, which looked exactly like an\n // unreachable host. One quoted string, so the remote shell passes the format\n // through untouched.\n //\n // Shares the collector's SSH_OPTIONS rather than passing BatchMode alone.\n // Without ControlPath the probe could not use the warm master socket the\n // collector rides, and without ConnectTimeout it inherited the kernel's dial\n // -- 75s on macOS, bounded only by the timeout below, so a sleeping laptop\n // froze the picker for ten seconds before admitting it was unreachable.\n const probe = run(\"ssh\", [\n ...SSH_OPTIONS,\n target,\n `tmux list-windows -a -F ${shellQuote(\"#{window_id}\")}`,\n ]);\n if (probe.status !== 0) {\n // 255 is ssh's own failure code; anything else came from the remote\n // command. Conflating them was wrong in the common case: with a warm\n // ControlMaster socket the host answers instantly and it is tmux that is\n // gone, so \"unreachable\" sent you looking at the network for a problem that\n // was not there.\n const sshFailed = probe.status === 255 || probe.failed;\n if (sshFailed) {\n // No mark: we learned nothing about the peer's tmux, only that we could\n // not ask. Its agents may be perfectly alive behind a cold socket or a\n // sleeping laptop, and deleting them here would be guessing.\n return {\n ok: false,\n reason: \"unreachable\",\n message: `cannot reach ${target} over ssh. Nothing here ever prompts for auth, so check the host is awake and reachable, or connect once by hand to see the real error.`,\n };\n }\n\n // ssh worked, tmux did not. That is a real fact about the host and the\n // strongest one available: a successful export only proves the murmur\n // binary ran, which it does happily on a box whose tmux server is gone --\n // which is why these agents read as fresh for three hours.\n forgetHostReplica(store, agent.host_id);\n return {\n ok: false,\n reason: \"no_tmux\",\n message: `${target} has no tmux server running, so its agents are gone. Removed them; they will come back when it reports again.`,\n };\n }\n const remoteWindows = new Set(probe.stdout.split(\"\\n\").filter(Boolean));\n if (!remoteWindows.has(agent.window)) {\n forgetReplica(store, agent.agent_id, agent.host_id);\n return {\n ok: false,\n reason: \"window_gone\",\n message: `${agentLabel(agent)} is gone -- ${target} no longer has that window. Cleared.`,\n };\n }\n\n const attachTarget = shellQuote(`${agent.session}:${agent.window}`);\n\n // Hand the ssh to tmux as its own window rather than running it here.\n // `murmur pick` is usually a display-popup, and a popup is modal: an ssh\n // session started inside it is killed the moment the picker exits, so the\n // remote pane flashed and vanished. A new window outlives the popup and\n // gives the remote tmux a real terminal to attach to.\n //\n // Nested tmux is the known cost here (see the spec's open question on inner\n // prefixes); a window at least makes it visible and closable.\n if (process.env.TMUX) {\n // `tmux new-window <command>` runs the command through a shell, so the\n // string is expanded LOCALLY before ssh sees it. A tmux session id is\n // always `$N`, so `$0:@6` arrived as `:@6` and the remote attach failed\n // with \"can't find session\". shellQuote alone is not enough: it protects\n // the remote shell, this protects the local one.\n const command = `ssh -t ${shellQuote(target)} tmux attach -t ${shellQuote(attachTarget)}`;\n // Named after the peer as configured, matching what the picker's host\n // column shows. The machine's self-reported display_name can be something\n // like a container id, which makes the window unrecognisable.\n const name = `@${peer?.name ?? target}`;\n\n // Reuse an existing window for this host rather than stacking a new one on\n // every jump. murmur navigates to agents; the window is only here because a\n // remote attach needs a terminal that outlives the popup, so one per host is\n // the whole requirement. Jumping to bubba three times used to leave three\n // identical @bubba windows behind.\n //\n // Matched on window name, which is the only handle available: the ssh is\n // opaque from here, and the remote session id is not a local address.\n const existing = mux.windowNamed(name);\n if (existing) {\n return mux.selectWindow(existing)\n ? { ok: true }\n : {\n ok: false,\n reason: \"attach_failed\",\n message: `could not switch to the existing ${name} window.`,\n };\n }\n\n return mux.newWindow(name, command)\n ? { ok: true }\n : {\n ok: false,\n reason: \"attach_failed\",\n message: `could not open a window to attach to ${target}.`,\n };\n }\n\n // Outside tmux there is no popup to escape, so run it directly. stdio is\n // inherited, so this blocks until the user leaves the remote session; a\n // nonzero exit means the attach itself failed.\n const attach = run(\"ssh\", [\"-t\", target, \"tmux\", \"attach\", \"-t\", attachTarget], true);\n return attach.status === 0 && !attach.failed\n ? { ok: true }\n : {\n ok: false,\n reason: \"attach_failed\",\n message: `ssh attach to ${target} failed.`,\n };\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { Agent } from \"./agents.js\";\nimport { SSH_OPTIONS } from \"./channel.js\";\nimport { loadIdentity } from \"./identity.js\";\nimport { tmux } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\n\n/**\n * Glance: the last few lines a pane printed.\n *\n * This is the cheap half of the two things \"render any pane from the master\"\n * hides. It is a stateless `capture-pane`, not a frame stream — no resize\n * negotiation, no input routing, no reconnect. That deferral is what keeps\n * murmur a state layer instead of a multiplexer (DESIGN-NOTES, \"Deferring\n * interactive remote rendering\"), and it is why this file is thirty lines\n * rather than most of herdr.\n */\n\nconst GLANCE_LINES = 40;\n\nexport function glance(store: Store, agent: Agent, lines = GLANCE_LINES): string | null {\n if (agent.host_id === loadIdentity()?.host_id) return tmux.capture(agent.pane, lines);\n\n const peer = store.peers().find((candidate) => candidate.host_id === agent.host_id);\n const target = peer?.target ?? peer?.name;\n if (!target) return null;\n try {\n // The pane id is `%N`, which a remote shell leaves alone, but quote it\n // anyway: the same class of bug as the `$N` session id that made remote\n // jump fail silently for a day.\n return execFileSync(\n \"ssh\",\n [\n ...SSH_OPTIONS,\n target,\n \"tmux\",\n \"capture-pane\",\n \"-p\",\n \"-t\",\n `'${agent.pane}'`,\n \"-S\",\n `-${lines}`,\n ],\n { encoding: \"utf8\", timeout: 3000, stdio: [\"ignore\", \"pipe\", \"ignore\"] },\n );\n } catch {\n // Unreachable, cold socket, dead tmux, gone pane. The preview says so\n // rather than the picker failing.\n return null;\n }\n}\n","import { type Channel, ssh } from \"./channel.js\";\nimport { collect, STALENESS_MS } from \"./collector.js\";\nimport { type AgentView, attentionSort, foldAll, isStale } from \"./fold.js\";\nimport { loadIdentity } from \"./identity.js\";\nimport { pidAlive } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\n\ntype StatusState = \"working\" | \"blocked\" | \"done\" | \"crashed\" | \"idle\";\ntype Counts = Record<StatusState, number>;\n\nexport type Status = {\n counts: Counts;\n orchestrated_counts: Counts;\n agents: (AgentView & {\n stale: boolean;\n age_ms: number | null;\n event_age_ms: number | null;\n tmux_down: boolean;\n host: string;\n })[];\n peers: {\n name: string;\n display_name: string | null;\n fetched_at: number | null;\n stale: boolean;\n }[];\n};\n\nfunction emptyCounts(): Counts {\n return { working: 0, blocked: 0, done: 0, crashed: 0, idle: 0 };\n}\n\nexport function tmuxStatus(view: Status): string {\n const urgency: StatusState[] = [\"crashed\", \"blocked\", \"done\", \"working\", \"idle\"];\n return urgency\n .filter((state) => view.counts[state] > 0)\n .map((state) => `${state}\\t${view.counts[state]}\\n`)\n .join(\"\");\n}\n\n/**\n * Fold the current view. Pure with respect to the network: the caller decides\n * whether to collect first (see `statusWithCollect`).\n */\nexport function status(store: Store, now = Date.now()): Status {\n const identity = loadIdentity();\n const peers = store.peers();\n const peersByHost = new Map(\n peers.flatMap((peer) => (peer.host_id === null ? [] : [[peer.host_id, peer] as const])),\n );\n const events = store.allEvents();\n const local = foldAll(\n events.filter((event) => event.host_id === identity?.host_id),\n pidAlive,\n );\n const remote = foldAll(\n events.filter((event) => event.host_id !== identity?.host_id),\n () => true,\n );\n const counts = emptyCounts();\n const orchestratedCounts = emptyCounts();\n const agents = attentionSort([...local, ...remote]).map((agent) => {\n const peer = peersByHost.get(agent.host_id);\n const fetchedAt = peer?.fetched_at ?? null;\n const state: StatusState =\n agent.state === null || agent.state === \"cleared\" ? \"idle\" : agent.state;\n const target = agent.driver === \"human\" ? counts : orchestratedCounts;\n target[state] += 1;\n return {\n ...agent,\n fetched_at: fetchedAt,\n // Replica freshness: how long since we last reached the peer. Local rows\n // have no fetched_at and are never stale.\n stale: isStale(fetchedAt, now, STALENESS_MS),\n age_ms: fetchedAt === null ? null : now - fetchedAt,\n // Information age: how long since the agent itself said anything. This\n // is the number a human means by \"how stale is that row\". A successful\n // fetch of a three-hour-old event resets age_ms to zero but leaves this\n // at three hours, which is why they cannot be the same field.\n event_age_ms: agent.event === null ? null : Math.max(0, now - agent.event.ts),\n // A jump proved this host's tmux was down and nothing has authored since.\n // Stronger than staleness: the host answers, its agents are just gone.\n tmux_down: peer?.tmux_down_at != null,\n // The name the human typed, not the machine's self-reported hostname. A\n // peer added as `linuxpc` reported `18c04d69b860` (a container hostname)\n // and that is what the picker showed — a string that appears nowhere\n // else in the tool and cannot be typed at `peer remove` or searched for.\n // Only the local node, which has no peer row, falls back to its own\n // discovered display_name.\n host:\n peer?.name ?? (agent.host_id === identity?.host_id ? identity.display_name : agent.host_id),\n };\n });\n\n return {\n counts,\n orchestrated_counts: orchestratedCounts,\n agents,\n peers: peers.map((peer) => ({\n name: peer.name,\n display_name: peer.display_name,\n fetched_at: peer.fetched_at,\n // A peer we have never reached is stale, not fresh. `isStale` reads a\n // null `fetched_at` as \"local, therefore never stale\", which is right\n // for an agent row but backwards for a peer: null there means the very\n // first collect has not succeeded yet. Left to `isStale`, an\n // unreachable host you just added would render as up to date.\n stale: peer.fetched_at === null || isStale(peer.fetched_at, now, STALENESS_MS),\n })),\n };\n}\n\n/**\n * Collect from peers, then fold. This is what every user-facing surface wants:\n * the view reflects the sync that just ran, rather than the one before it.\n *\n * Awaiting matters for two reasons. A fire-and-forget collect makes every\n * invocation show data one run stale — you never see what you just fetched.\n * And the callers close the store in a `finally`, so a collect still in flight\n * lands on a closed handle and reports \"The database connection is not open\",\n * which looks like corruption rather than a race.\n *\n * Sync must never fail a command, so a peer failure only warns. With no peers\n * this is a loop over an empty array: no network, no added latency, which is\n * the everyday single-machine path.\n */\nexport async function statusWithCollect(\n store: Store,\n now = Date.now(),\n channel: Channel = ssh,\n): Promise<Status> {\n try {\n await collect(store, channel, now);\n } catch (error) {\n process.stderr.write(\n `murmur: status: collect: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n }\n return status(store, now);\n}\n","import type { Command } from \"commander\";\nimport { statusWithCollect, tmuxStatus } from \"../status.js\";\nimport { openStore } from \"../store.js\";\n\nexport function registerStatus(program: Command): void {\n program\n .command(\"status\")\n .description(\"Show folded agent status\")\n .option(\"--json\", \"print JSON\")\n .action(async (options: { json?: boolean }) => {\n const store = openStore();\n try {\n const view = await statusWithCollect(store);\n process.stdout.write(\n options.json ? `${JSON.stringify(view, null, 2)}\\n` : tmuxStatus(view),\n );\n } finally {\n store.close();\n }\n });\n}\n","// SDK entry. package.json advertises this as the \".\" export, so anything a\n// consumer needs to drive murmur without shelling out to the CLI belongs here.\n// The CLI is a thin layer over exactly these units.\n// Read from the manifest rather than restated here: the version lived in\n// package.json and in this file, and two copies of one fact drift. npm bumps\n// the manifest, so the manifest is the source.\nimport { createRequire } from \"node:module\";\n\nconst manifest = createRequire(import.meta.url)(\"../package.json\") as { version: string };\nexport const VERSION: string = manifest.version;\n\nexport {\n type Agent,\n agentLabel,\n agentLocation,\n type JumpResult,\n jumpToAgent,\n shellQuote,\n} from \"./agents.js\";\nexport { type Channel, hasWarmSocket, ssh } from \"./channel.js\";\nexport {\n type CollectResult,\n collect,\n MAX_CONCURRENT_PEERS,\n STALENESS_MS,\n} from \"./collector.js\";\nexport { eventFromWire, exportJsonl, SCHEMA_VERSION } from \"./export.js\";\nexport {\n type AgentView,\n attentionSort,\n foldAgent,\n foldAll,\n isStale,\n type LiveCheck,\n} from \"./fold.js\";\nexport { glance } from \"./glance.js\";\nexport { ensureIdentity, loadIdentity, type NodeIdentity } from \"./identity.js\";\nexport { type Mux, pidAlive, tmux } from \"./mux.js\";\nexport { configDir, dbPath, stateDir } from \"./paths.js\";\nexport { type Status, status } from \"./status.js\";\nexport { type NewEvent, openStore, STORE_VERSION, type Store } from \"./store.js\";\nexport {\n type AgentState,\n DEFAULT_DRIVER,\n type Driver,\n type Event,\n type Peer,\n} from \"./types.js\";\n"],"mappings":"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","names":["join","join","program","execFileSync","program","program","program","mkdirSync","readFileSync","writeFileSync","homedir","join","program","readFileSync","homedir","join","readFileSync","join","homedir","program","spawnSync","execFileSync","execFileSync","agent","spawnSync","program","program"]}