@martintrojer/murmur 0.1.1 → 0.1.3
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/README.md +1 -1
- package/dist/cli.js +50 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ session, and its host, as literal substrings rather than scattered characters.
|
|
|
126
126
|
|
|
127
127
|
## Status
|
|
128
128
|
|
|
129
|
-
**0.1.
|
|
129
|
+
**0.1.3.** In daily use on one machine and verified across two over real ssh.
|
|
130
130
|
It is new and not battle-tested. The known gaps are listed at the end of
|
|
131
131
|
[ARCHITECTURE.md](ARCHITECTURE.md#known-gaps); the one most likely to annoy you
|
|
132
132
|
is that jumping to a remote agent nests tmux inside tmux, which every tool in
|
package/dist/cli.js
CHANGED
|
@@ -842,6 +842,18 @@ function sshHosts() {
|
|
|
842
842
|
return [];
|
|
843
843
|
}
|
|
844
844
|
}
|
|
845
|
+
function formatTable(rows) {
|
|
846
|
+
const widths = [];
|
|
847
|
+
for (const row of rows) {
|
|
848
|
+
row.forEach((cell, index) => {
|
|
849
|
+
widths[index] = Math.max(widths[index] ?? 0, cell.length);
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
return rows.map(
|
|
853
|
+
(row) => row.map((cell, index) => index === row.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)).join(" ").trimEnd()
|
|
854
|
+
).map((line) => `${line}
|
|
855
|
+
`).join("");
|
|
856
|
+
}
|
|
845
857
|
function registerPeer(program2) {
|
|
846
858
|
const peer = program2.command("peer").description("Manage peers");
|
|
847
859
|
peer.command("add").description("Add a peer and discover its identity").argument("<name>").argument("[target]").action(async (name, target = name) => {
|
|
@@ -904,16 +916,26 @@ function registerPeer(program2) {
|
|
|
904
916
|
const store = openStore();
|
|
905
917
|
try {
|
|
906
918
|
const peers = store.peers();
|
|
907
|
-
if (options.json)
|
|
919
|
+
if (options.json) {
|
|
920
|
+
process.stdout.write(`${JSON.stringify(peers)}
|
|
908
921
|
`);
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
);
|
|
915
|
-
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (peers.length === 0) {
|
|
925
|
+
process.stdout.write("no peers configured\n");
|
|
926
|
+
return;
|
|
916
927
|
}
|
|
928
|
+
const rows = [
|
|
929
|
+
// HOSTNAME, not HOST: this is what the node reported about itself,
|
|
930
|
+
// which is not the handle any other command takes. NAME is.
|
|
931
|
+
["NAME", "TARGET", "HOSTNAME"],
|
|
932
|
+
...peers.map((configured) => [
|
|
933
|
+
configured.name,
|
|
934
|
+
configured.target,
|
|
935
|
+
configured.display_name ?? "unknown"
|
|
936
|
+
])
|
|
937
|
+
];
|
|
938
|
+
process.stdout.write(formatTable(rows));
|
|
917
939
|
} finally {
|
|
918
940
|
store.close();
|
|
919
941
|
}
|
|
@@ -1038,7 +1060,7 @@ function jumpToAgent(store, agent) {
|
|
|
1038
1060
|
const attachTarget = shellQuote(`${agent.session}:${agent.window}`);
|
|
1039
1061
|
if (process.env.TMUX) {
|
|
1040
1062
|
const command = `ssh -t ${shellQuote(target)} tmux attach -t ${shellQuote(attachTarget)}`;
|
|
1041
|
-
const name = `@${peer?.
|
|
1063
|
+
const name = `@${peer?.name ?? target}`;
|
|
1042
1064
|
const existing = tmux.windowNamed(name);
|
|
1043
1065
|
if (existing) {
|
|
1044
1066
|
tmux.selectWindow(existing);
|
|
@@ -1137,7 +1159,13 @@ function status(store, now = Date.now()) {
|
|
|
1137
1159
|
// A jump proved this host's tmux was down and nothing has authored since.
|
|
1138
1160
|
// Stronger than staleness: the host answers, its agents are just gone.
|
|
1139
1161
|
tmux_down: peer?.tmux_down_at != null,
|
|
1140
|
-
|
|
1162
|
+
// The name the human typed, not the machine's self-reported hostname. A
|
|
1163
|
+
// peer added as `linuxpc` reported `18c04d69b860` (a container hostname)
|
|
1164
|
+
// and that is what the picker showed — a string that appears nowhere
|
|
1165
|
+
// else in the tool and cannot be typed at `peer remove` or searched for.
|
|
1166
|
+
// Only the local node, which has no peer row, falls back to its own
|
|
1167
|
+
// discovered display_name.
|
|
1168
|
+
host: peer?.name ?? (agent.host_id === identity?.host_id ? identity.display_name : agent.host_id)
|
|
1141
1169
|
};
|
|
1142
1170
|
});
|
|
1143
1171
|
return {
|
|
@@ -1260,6 +1288,9 @@ function pad(value, width) {
|
|
|
1260
1288
|
const tail = value.slice(index).match(ANSI_AT_END);
|
|
1261
1289
|
return `${out}\u2026${tail?.[0] ?? ""}${" ".repeat(Math.max(0, width - budget - 1))}`;
|
|
1262
1290
|
}
|
|
1291
|
+
function isPopup(env) {
|
|
1292
|
+
return Boolean(env.TMUX) && !env.TMUX_PANE;
|
|
1293
|
+
}
|
|
1263
1294
|
function pickerRow(agent, showHost, current, local = true) {
|
|
1264
1295
|
const state = agent.state ?? "idle";
|
|
1265
1296
|
const colour = COLOUR[state] ?? "";
|
|
@@ -1350,6 +1381,7 @@ async function runPick(store, options = {}) {
|
|
|
1350
1381
|
const prompt = URGENCY.filter((state) => counts.get(state)).map((state) => `${COLOUR[state]}${GLYPH[state]}${counts.get(state)}${RESET}`).join(" ");
|
|
1351
1382
|
const self = process.argv[1] ?? "murmur";
|
|
1352
1383
|
const allFlag = options.all ? " --all" : "";
|
|
1384
|
+
const inPopup = isPopup(process.env);
|
|
1353
1385
|
const width = process.stdout.columns ?? 0;
|
|
1354
1386
|
const previewLayout = width > 0 && width < 150 ? "bottom:60%,border-top,wrap" : "right:58%,border-left,wrap";
|
|
1355
1387
|
const preview = `${process.execPath} ${self} pick --preview {1}`;
|
|
@@ -1380,7 +1412,15 @@ async function runPick(store, options = {}) {
|
|
|
1380
1412
|
"begin,index",
|
|
1381
1413
|
"--layout",
|
|
1382
1414
|
"reverse",
|
|
1415
|
+
// `display-popup` draws its own border, so fzf's is a second one a
|
|
1416
|
+
// character inside the first. A popup is the normal way to run this, via
|
|
1417
|
+
// the prefix+a binding, so the doubled frame was what you saw most.
|
|
1418
|
+
//
|
|
1419
|
+
// Detected by $TMUX set with $TMUX_PANE unset: tmux exports TMUX to a
|
|
1420
|
+
// popup but not TMUX_PANE, since a popup is not a pane. Outside tmux
|
|
1421
|
+
// neither is set, so the three cases stay distinguishable.
|
|
1383
1422
|
"--border",
|
|
1423
|
+
inPopup ? "none" : "rounded",
|
|
1384
1424
|
"--info",
|
|
1385
1425
|
"inline",
|
|
1386
1426
|
"--prompt",
|
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 * 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 // 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 \"--border\",\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/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\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\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) {\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 { 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 // 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 = 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 // 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(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"]}
|
package/dist/index.js
CHANGED
|
@@ -255,7 +255,7 @@ function jumpToAgent(store, agent) {
|
|
|
255
255
|
const attachTarget = shellQuote(`${agent.session}:${agent.window}`);
|
|
256
256
|
if (process.env.TMUX) {
|
|
257
257
|
const command = `ssh -t ${shellQuote(target)} tmux attach -t ${shellQuote(attachTarget)}`;
|
|
258
|
-
const name = `@${peer?.
|
|
258
|
+
const name = `@${peer?.name ?? target}`;
|
|
259
259
|
const existing = tmux.windowNamed(name);
|
|
260
260
|
if (existing) {
|
|
261
261
|
tmux.selectWindow(existing);
|
|
@@ -631,7 +631,13 @@ function status(store, now = Date.now()) {
|
|
|
631
631
|
// A jump proved this host's tmux was down and nothing has authored since.
|
|
632
632
|
// Stronger than staleness: the host answers, its agents are just gone.
|
|
633
633
|
tmux_down: peer?.tmux_down_at != null,
|
|
634
|
-
|
|
634
|
+
// The name the human typed, not the machine's self-reported hostname. A
|
|
635
|
+
// peer added as `linuxpc` reported `18c04d69b860` (a container hostname)
|
|
636
|
+
// and that is what the picker showed — a string that appears nowhere
|
|
637
|
+
// else in the tool and cannot be typed at `peer remove` or searched for.
|
|
638
|
+
// Only the local node, which has no peer row, falls back to its own
|
|
639
|
+
// discovered display_name.
|
|
640
|
+
host: peer?.name ?? (agent.host_id === identity?.host_id ? identity.display_name : agent.host_id)
|
|
635
641
|
};
|
|
636
642
|
});
|
|
637
643
|
return {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/agents.ts","../src/identity.ts","../src/paths.ts","../src/mux.ts","../src/channel.ts","../src/types.ts","../src/fold.ts","../src/export.ts","../src/collector.ts","../src/glance.ts","../src/status.ts","../src/store.ts"],"sourcesContent":["// 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","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 { 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 { 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 { 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 { 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"],"mappings":"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","names":["join","join","execFileSync","execFileSync","SSH_OPTIONS","execFileSync"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/agents.ts","../src/identity.ts","../src/paths.ts","../src/mux.ts","../src/channel.ts","../src/types.ts","../src/fold.ts","../src/export.ts","../src/collector.ts","../src/glance.ts","../src/status.ts","../src/store.ts"],"sourcesContent":["// 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","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 // 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 = 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 { 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 { 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 { 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 // 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(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 { 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"],"mappings":";AAMA,SAAS,qBAAqB;;;ACN9B,SAAS,iBAAiB;;;ACA1B,SAAS,kBAAkB;AAC3B,SAAS,YAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,gBAAgB;AACzB,SAAS,QAAAA,aAAY;;;ACHrB,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,SAAS,WAAmB;AACjC,SACE,QAAQ,IAAI,oBACZ,KAAK,QAAQ,IAAI,kBAAkB,KAAK,QAAQ,GAAG,UAAU,OAAO,GAAG,QAAQ;AAEnF;AAEO,SAAS,YAAoB;AAClC,SACE,QAAQ,IAAI,qBACZ,KAAK,QAAQ,IAAI,mBAAmB,KAAK,QAAQ,GAAG,SAAS,GAAG,QAAQ;AAE5E;AAEO,SAAS,SAAiB;AAC/B,SAAO,KAAK,SAAS,GAAG,WAAW;AACrC;;;ADRO,SAAS,eAAoC;AAClD,QAAM,OAAOC,MAAK,SAAS,GAAG,eAAe;AAC7C,SAAO,WAAW,IAAI,IAAI,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC,IAAI;AACrE;AAEO,SAAS,eAAe,cAAc,SAAS,GAAiB;AACrE,QAAM,WAAW,aAAa;AAC9B,MAAI,SAAU,QAAO;AAErB,QAAM,WAAW,EAAE,SAAS,WAAW,GAAG,cAAc,YAAY;AACpE,YAAU,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AACzC,gBAAcA,MAAK,SAAS,GAAG,eAAe,GAAG,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAAA,CAAI;AACzF,SAAO;AACT;;;AExBA,SAAS,oBAAoB;AAwB7B,SAAS,QAAQ,MAA+B;AAC9C,MAAI;AACF,WAAO,aAAa,QAAQ,MAAM;AAAA,MAChC,UAAU;AAAA,MACV,SAAS;AAAA,MACT,OAAO,CAAC,UAAU,QAAQ,QAAQ;AAAA,IACpC,CAAC,EAAE,KAAK;AAAA,EACV,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,IAAM,OAAY;AAAA,EACvB,gBAAgB;AAUd,UAAM,OAAO,QAAQ,IAAI;AACzB,QAAI,CAAC,KAAM,QAAO;AAKlB,UAAM,SAAS,QAAQ;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,UAAM,CAAC,SAAS,QAAQ,aAAa,UAAU,IAAI,QAAQ,MAAM,GAAI,KAAK,CAAC;AAC3E,QAAI,CAAC,WAAW,CAAC,OAAQ,QAAO;AAChC,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,cAAc,eAAe;AAAA,MAC7B,aAAa,cAAc;AAAA,IAC7B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,cAAc;AACZ,UAAM,MAAM,QAAQ,CAAC,gBAAgB,MAAM,MAAM,cAAc,CAAC;AAChE,QAAI,QAAQ,KAAM,QAAO;AACzB,WAAO,IAAI,IAAI,IAAI,MAAM,IAAI,EAAE,OAAO,OAAO,CAAC;AAAA,EAChD;AAAA,EAEA,SAAS,QAAQ,OAAO;AACtB,QAAI,UAAU,MAAM;AAClB,cAAQ,CAAC,qBAAqB,OAAO,MAAM,QAAQ,cAAc,CAAC;AAAA,IACpE,OAAO;AACL,cAAQ,CAAC,qBAAqB,MAAM,MAAM,QAAQ,gBAAgB,KAAK,CAAC;AACxE,cAAQ,CAAC,qBAAqB,MAAM,MAAM,QAAQ,eAAe,GAAG,CAAC;AAAA,IACvE;AACA,YAAQ,CAAC,kBAAkB,IAAI,CAAC;AAAA,EAClC;AAAA,EAEA,OAAO,SAAS,QAAQ;AAMtB,YAAQ,CAAC,iBAAiB,MAAM,OAAO,CAAC;AACxC,YAAQ,CAAC,iBAAiB,MAAM,MAAM,CAAC;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc;AACZ,UAAM,MAAM,QAAQ,CAAC,gBAAgB,MAAM,MAAM,6BAA8B,CAAC;AAChF,UAAM,QAAQ,oBAAI,IAAoB;AACtC,eAAW,QAAQ,KAAK,MAAM,IAAI,KAAK,CAAC,GAAG;AACzC,YAAM,CAAC,IAAI,IAAI,IAAI,KAAK,MAAM,GAAI;AAClC,UAAI,MAAM,KAAM,OAAM,IAAI,IAAI,IAAI;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,QAAQ;AACpB,UAAM,MAAM,QAAQ,CAAC,cAAc,MAAM,QAAQ,MAAM,YAAY,CAAC;AACpE,WAAO,KAAK,MAAM,IAAI,EAAE,OAAO,OAAO,KAAK,CAAC;AAAA,EAC9C;AAAA,EAEA,YAAY,MAAM;AAChB,UAAM,MAAM,QAAQ,CAAC,gBAAgB,MAAM,MAAM,6BAA8B,CAAC;AAChF,eAAW,QAAQ,KAAK,MAAM,IAAI,KAAK,CAAC,GAAG;AACzC,YAAM,CAAC,IAAI,UAAU,IAAI,KAAK,MAAM,GAAI;AACxC,UAAI,MAAM,eAAe,KAAM,QAAO;AAAA,IACxC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,aAAa,QAAQ;AACnB,YAAQ,CAAC,iBAAiB,MAAM,MAAM,CAAC;AAAA,EACzC;AAAA;AAAA;AAAA,EAIA,cAAc,MAAM;AAClB,WAAO,QAAQ,CAAC,mBAAmB,MAAM,MAAM,MAAM,cAAc,CAAC,KAAK;AAAA,EAC3E;AAAA,EAEA,QAAQ,MAAM,OAAO;AACnB,UAAM,OAAO,CAAC,gBAAgB,MAAM,MAAM,IAAI;AAC9C,QAAI,UAAU,OAAW,MAAK,KAAK,MAAM,IAAI,KAAK,EAAE;AACpD,WAAO,QAAQ,IAAI;AAAA,EACrB;AACF;AAEO,SAAS,SAAS,KAAsB;AAC7C,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAQ,MAAgC,SAAS;AAAA,EACnD;AACF;;;AHrJO,SAAS,WAAW,OAAsB;AAC/C,QAAM,OAAO,MAAM,cAAc,MAAM,cAAc,MAAM,eAAe,MAAM;AAChF,SAAO,aAAa,QAAQ,MAAM,MAAM;AAC1C;AAMO,SAAS,cAAc,OAAsB;AAClD,QAAM,UAAU,MAAM,gBAAgB,MAAM;AAC5C,QAAM,SAAS,MAAM,eAAe,MAAM;AAC1C,SAAO,aAAa,YAAY,SAAS,UAAU,GAAG,OAAO,IAAI,MAAM,EAAE;AAC3E;AAEO,SAAS,aAAa,OAAuB;AAClD,SAAO,CAAC,GAAG,KAAK,EACb,IAAI,CAAC,cAAc;AAClB,UAAM,OAAO,UAAU,WAAW,CAAC;AACnC,WAAO,OAAO,MAAQ,SAAS,OAAS,QAAQ,OAAQ,QAAQ,MAAQ,WAAM;AAAA,EAChF,CAAC,EACA,KAAK,EAAE;AACZ;AAEO,SAAS,WAAW,OAAuB;AAChD,SAAO,IAAI,MAAM,WAAW,KAAK,OAAO,CAAC;AAC3C;AAkDO,SAAS,kBAAkB,OAAc,QAAsB;AACpE,MAAI;AACF,UAAM,OAAO,MAAM,MAAM,EAAE,KAAK,CAAC,cAAc,UAAU,YAAY,MAAM;AAC3E,UAAM,WAAW,MAAM;AACvB,QAAI,MAAM;AAWR,YAAM,WAAW;AAAA,QACf,MAAM,KAAK;AAAA,QACX,QAAQ,KAAK;AAAA,QACb,cAAc,KAAK,IAAI;AAAA,MACzB,CAAC;AAAA,IACH;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,cAAc,OAAc,SAAiB,QAAsB;AACjF,MAAI;AACF,UAAM,YAAY,OAAO;AACzB,UAAM,OAAO,MAAM,MAAM,EAAE,KAAK,CAAC,cAAc,UAAU,YAAY,MAAM;AAC3E,QAAI,KAAM,OAAM,WAAW,EAAE,MAAM,KAAK,MAAM,QAAQ,KAAK,QAAQ,WAAW,EAAE,CAAC;AAAA,EACnF,QAAQ;AAAA,EAER;AACF;AAgCO,SAAS,YAAY,OAAc,OAA0B;AAClE,QAAM,WAAW,aAAa;AAC9B,MAAI,MAAM,YAAY,UAAU,SAAS;AACvC,UAAM,OAAO,KAAK,YAAY;AAC9B,QAAI,QAAQ,CAAC,KAAK,IAAI,MAAM,MAAM,GAAG;AACnC,oBAAc,OAAO,MAAM,UAAU,MAAM,OAAO;AAClD,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,SAAS,GAAG,WAAW,KAAK,CAAC;AAAA,MAC/B;AAAA,IACF;AACA,SAAK,OAAO,MAAM,SAAS,MAAM,MAAM;AACvC,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AACA,QAAM,OAAO,MAAM,MAAM,EAAE,KAAK,CAAC,cAAc,UAAU,YAAY,MAAM,OAAO;AAClF,QAAM,SAAS,MAAM,UAAU,MAAM;AACrC,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS,+BAA+B,MAAM,QAAQ,MAAM,GAAG,CAAC,CAAC;AAAA,IACnE;AAAA,EACF;AAWA,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA,CAAC,MAAM,iBAAiB,QAAQ,2BAA2B,WAAW,cAAc,CAAC,EAAE;AAAA,IACvF,EAAE,UAAU,QAAQ,SAAS,IAAO;AAAA,EACtC;AACA,MAAI,MAAM,WAAW,GAAG;AAMtB,UAAM,YAAY,MAAM,WAAW,OAAO,MAAM,UAAU;AAC1D,QAAI,WAAW;AAIb,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,SAAS,gBAAgB,MAAM;AAAA,MACjC;AAAA,IACF;AAMA,sBAAkB,OAAO,MAAM,OAAO;AACtC,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS,GAAG,MAAM;AAAA,IACpB;AAAA,EACF;AACA,QAAM,gBAAgB,IAAI,KAAK,MAAM,UAAU,IAAI,MAAM,IAAI,EAAE,OAAO,OAAO,CAAC;AAC9E,MAAI,CAAC,cAAc,IAAI,MAAM,MAAM,GAAG;AACpC,kBAAc,OAAO,MAAM,UAAU,MAAM,OAAO;AAClD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS,GAAG,WAAW,KAAK,CAAC,eAAe,MAAM;AAAA,IACpD;AAAA,EACF;AAEA,QAAM,eAAe,WAAW,GAAG,MAAM,OAAO,IAAI,MAAM,MAAM,EAAE;AAUlE,MAAI,QAAQ,IAAI,MAAM;AAMpB,UAAM,UAAU,UAAU,WAAW,MAAM,CAAC,mBAAmB,WAAW,YAAY,CAAC;AAIvF,UAAM,OAAO,IAAI,MAAM,QAAQ,MAAM;AAUrC,UAAM,WAAW,KAAK,YAAY,IAAI;AACtC,QAAI,UAAU;AACZ,WAAK,aAAa,QAAQ;AAC1B,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AAEA,cAAU,QAAQ,CAAC,cAAc,MAAM,MAAM,OAAO,GAAG,EAAE,OAAO,SAAS,CAAC;AAC1E,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AAGA,YAAU,OAAO,CAAC,MAAM,QAAQ,QAAQ,UAAU,MAAM,YAAY,GAAG,EAAE,OAAO,UAAU,CAAC;AAC3F,SAAO,EAAE,IAAI,KAAK;AACpB;;;AI1RA,SAAS,UAAU,gBAAAC,qBAAoB;AACvC,SAAS,iBAAiB;AAE1B,IAAM,gBAAgB,UAAU,QAAQ;AACxC,IAAM,eAAe;AAQrB,IAAM,oBAAoB;AAK1B,IAAM,kBAAkB;AAExB,IAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe,YAAY;AAAA,EAC3B;AAAA,EACA,kBAAkB,iBAAiB;AACrC;AAMO,IAAM,MAAe;AAAA,EAC1B,MAAM,KAAK,QAAQ,MAAM;AACvB,UAAM,EAAE,OAAO,IAAI,MAAM,cAAc,OAAO,CAAC,GAAG,aAAa,QAAQ,GAAG,IAAI,GAAG;AAAA,MAC/E,UAAU;AAAA,MACV,SAAS;AAAA,IACX,CAAC;AACD,WAAO;AAAA,EACT;AACF;AAEO,SAAS,cAAc,QAAyB;AACrD,MAAI;AACF,IAAAA,cAAa,OAAO,CAAC,GAAG,aAAa,MAAM,SAAS,MAAM,GAAG,EAAE,OAAO,SAAS,CAAC;AAChF,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AC/CO,IAAM,iBAAyB;;;ACqB/B,SAAS,UACd,QACA,SACmD;AACnD,WAAS,QAAQ,OAAO,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;AAC1D,UAAM,QAAQ,OAAO,KAAK;AAC1B,QAAI,CAAC,MAAO;AAEZ,YAAQ,MAAM,OAAO;AAAA,MACnB,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO,EAAE,OAAO,MAAM,OAAO,MAAM;AAAA,MACrC,KAAK;AACH,eAAO,EAAE,OAAO,MAAM,OAAO,KAAK;AAAA,MACpC,KAAK;AACH,eAAO;AAAA,UACL,OAAO,MAAM,QAAQ,QAAQ,MAAM,MAAM,KAAK,QAAQ,MAAM,GAAG,IAAI,YAAY;AAAA,UAC/E;AAAA,QACF;AAAA,IACJ;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,MAAM,OAAO,KAAK;AACpC;AAEO,SAAS,QAAQ,QAAiB,SAAiC;AACxE,QAAM,UAAU,oBAAI,IAAqB;AACzC,aAAW,SAAS,QAAQ;AAC1B,UAAM,cAAc,QAAQ,IAAI,MAAM,QAAQ;AAC9C,QAAI,YAAa,aAAY,KAAK,KAAK;AAAA,QAClC,SAAQ,IAAI,MAAM,UAAU,CAAC,KAAK,CAAC;AAAA,EAC1C;AAEA,SAAO,CAAC,GAAG,QAAQ,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB;AAChD,UAAM,SAAS,UAAU,aAAa,OAAO;AAC7C,UAAM,SAAS,OAAO,SAAS,YAAY,YAAY,SAAS,CAAC;AACjE,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAEhE,WAAO;AAAA,MACL,UAAU,OAAO;AAAA,MACjB,SAAS,OAAO;AAAA,MAChB,OAAO,OAAO;AAAA,MACd,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,MAAM,OAAO;AAAA,MACb,KAAK,OAAO;AAAA,MACZ,QAAQ,OAAO,UAAU;AAAA,MACzB,SAAS,OAAO;AAAA,MAChB,QAAQ,OAAO;AAAA,MACf,MAAM,OAAO;AAAA,MACb,cAAc,OAAO;AAAA,MACrB,aAAa,OAAO;AAAA,MACpB,YAAY,OAAO;AAAA,MACnB,YAAY,OAAO;AAAA,MACnB,YAAY;AAAA,IACd;AAAA,EACF,CAAC;AACH;AAEA,IAAM,kBAA8C;AAAA,EAClD,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AACX;AAEO,SAAS,cAAc,OAAiC;AAC7D,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU;AACtC,UAAM,cACH,KAAK,UAAU,OAAO,IAAI,gBAAgB,KAAK,KAAK,MACpD,MAAM,UAAU,OAAO,IAAI,gBAAgB,MAAM,KAAK;AACzD,QAAI,eAAe,EAAG,QAAO;AAC7B,YAAQ,MAAM,OAAO,MAAM,MAAM,KAAK,OAAO,MAAM;AAAA,EACrD,CAAC;AACH;AAEO,SAAS,QAAQ,WAA0B,KAAa,cAAc,KAAiB;AAC5F,SAAO,cAAc,QAAQ,MAAM,YAAY;AACjD;;;ACpGO,IAAM,iBAAiB;AAS9B,IAAM,eAAe,oBAAI,IAAI;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,YAAY,OAAuC;AAC1D,QAAM,EAAE,OAAO,GAAG,MAAM,IAAI;AAC5B,SAAO,EAAE,GAAG,OAAO,GAAG,MAAM;AAC9B;AAEO,SAAS,cAAc,MAAsC;AAClE,QAAM,QAAQ,OAAO,YAAY,OAAO,QAAQ,IAAI,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,aAAa,IAAI,GAAG,CAAC,CAAC;AAC/F,SAAO;AAAA,IACL,SAAS,KAAK;AAAA,IACd,KAAK,KAAK;AAAA,IACV,IAAI,KAAK;AAAA,IACT,UAAU,KAAK;AAAA,IACf,SAAS,KAAK;AAAA,IACd,QAAQ,KAAK;AAAA,IACb,MAAM,KAAK;AAAA,IACX,cAAe,KAAK,gBAA8C;AAAA,IAClE,aAAc,KAAK,eAA6C;AAAA,IAChE,YAAa,KAAK,cAA4C;AAAA,IAC9D,YAAa,KAAK,cAA4C;AAAA,IAC9D,YAAa,KAAK,cAA4C;AAAA,IAC9D,MAAO,KAAK,QAAsC;AAAA,IAClD,KAAM,KAAK,OAAqC;AAAA,IAChD,QAAS,KAAK,UAAwC;AAAA,IACtD,MAAM,KAAK;AAAA,IACX,OAAO,KAAK;AAAA,IACZ,SAAS,KAAK;AAAA,IACd,KAAM,KAAK,OAAqC;AAAA,IAChD,WAAW,KAAK;AAAA,IAChB,QAAQ,KAAK;AAAA,IACb;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,OAAc,QAAgB,SAA0B;AACjF,QAAM,UAAU,oBAAI,IAAqB;AACzC,aAAW,SAAS,MAAM,UAAU,GAAG;AACrC,QAAI,MAAM,YAAY,OAAQ;AAC9B,UAAM,SAAS,QAAQ,IAAI,MAAM,QAAQ;AACzC,QAAI,OAAQ,QAAO,KAAK,KAAK;AAAA,QACxB,SAAQ,IAAI,MAAM,UAAU,CAAC,KAAK,CAAC;AAAA,EAC1C;AAEA,aAAW,UAAU,QAAQ,OAAO,GAAG;AACrC,WAAO,KAAK,CAAC,MAAM,UAAU,KAAK,MAAM,MAAM,GAAG;AACjD,UAAM,SAAS,OAAO,GAAG,EAAE;AAC3B,QACE,UACA,OAAO,UAAU,aACjB,CAAC,OAAO,aACR,UAAU,QAAQ,OAAO,EAAE,UAAU,WACrC;AACA,YAAM,EAAE,SAAS,SAAS,KAAK,MAAM,IAAI,KAAK,GAAG,MAAM,IAAI;AAC3D,YAAM,OAAO,EAAE,GAAG,OAAO,OAAO,WAAW,WAAW,MAAM,QAAQ,WAAW,CAAC;AAAA,IAClF;AAAA,EACF;AACF;AAgBO,SAAS,iBAAiB,OAAc,QAAgB,MAAgC;AAI7F,MAAI,SAAS,KAAM;AAEnB,QAAM,SAAS,oBAAI,IAAmB;AACtC,aAAW,SAAS,MAAM,UAAU,GAAG;AACrC,QAAI,MAAM,YAAY,OAAQ;AAC9B,UAAM,WAAW,OAAO,IAAI,MAAM,QAAQ;AAC1C,QAAI,CAAC,YAAY,MAAM,MAAM,SAAS,IAAK,QAAO,IAAI,MAAM,UAAU,KAAK;AAAA,EAC7E;AAEA,aAAW,SAAS,OAAO,OAAO,GAAG;AACnC,QAAI,MAAM,UAAU,UAAW;AAC/B,QAAI,KAAK,IAAI,MAAM,MAAM,EAAG;AAC5B,UAAM,EAAE,SAAS,SAAS,KAAK,MAAM,IAAI,KAAK,GAAG,KAAK,IAAI;AAC1D,UAAM,OAAO;AAAA,MACX,GAAG;AAAA,MACH,OAAO;AAAA,MACP,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AACF;AAEO,SAAS,YACd,OACA,OACA,SACA,MACQ;AACR,QAAM,WAAW,eAAe;AAChC,oBAAkB,OAAO,SAAS,SAAS,OAAO;AAClD,MAAI,SAAS,OAAW,kBAAiB,OAAO,SAAS,SAAS,IAAI;AAEtE,QAAM,WAAqB;AAAA,IACzB,gBAAgB;AAAA,IAChB,SAAS,SAAS;AAAA,IAClB,cAAc,SAAS;AAAA,IACvB,aAAa,KAAK,IAAI;AAAA,EACxB;AACA,QAAM,QAAQ;AAAA,IACZ,KAAK,UAAU,QAAQ;AAAA,IACvB,GAAG,MACA,YAAY,SAAS,SAAS,KAAK,EACnC,IAAI,CAAC,UAAU,KAAK,UAAU,YAAY,KAAK,CAAC,CAAC;AAAA,EACtD;AACA,SAAO,GAAG,MAAM,KAAK,IAAI,CAAC;AAAA;AAC5B;;;AC1JO,IAAM,sBAAsB;AAC5B,IAAM,eAAe,IAAI;AAShC,SAAS,WAAW,QAAyD;AAC3E,QAAM,QAAQ,OAAO,KAAK,EAAE,MAAM,IAAI;AACtC,QAAM,WAAW,KAAK,MAAM,MAAM,MAAM,KAAK,EAAE;AAC/C,MAAI,SAAS,iBAAiB,gBAAgB;AAC5C,UAAM,IAAI;AAAA,MACR,8BAA8B,SAAS,cAAc,cAAc,cAAc;AAAA,IACnF;AAAA,EACF;AACA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,MAAM,IAAI,CAAC,SAAS,cAAc,KAAK,MAAM,IAAI,CAA4B,CAAC;AAAA,EACxF;AACF;AAEA,eAAsB,QACpB,OACA,SACA,MAAM,KAAK,IAAI,GACW;AAC1B,QAAM,UAA2B,CAAC;AAClC,MAAI;AACF,eAAW,QAAQ,MAAM,MAAM,GAAG;AAChC,UAAI;AACF,cAAM,SAAS,MAAM,QAAQ,KAAK,KAAK,QAAQ;AAAA,UAC7C;AAAA,UACA;AAAA,UACA;AAAA,UACA,OAAO,KAAK,SAAS;AAAA,QACvB,CAAC;AACD,cAAM,EAAE,UAAU,OAAO,IAAI,WAAW,MAAM;AAC9C,cAAM,WAAW,MAAM,OAAO,MAAM;AACpC,cAAM,YAAY,OACf,OAAO,CAAC,UAAU,MAAM,YAAY,SAAS,OAAO,EACpD,OAAO,CAAC,SAAS,UAAU,KAAK,IAAI,SAAS,MAAM,GAAG,GAAG,KAAK,SAAS;AAC1E,cAAM,WAAW;AAAA,UACf,MAAM,KAAK;AAAA,UACX,QAAQ,KAAK;AAAA,UACb,SAAS,SAAS;AAAA,UAClB,cAAc,SAAS;AAAA,UACvB;AAAA,UACA,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMZ,cAAc,WAAW,IAAI,OAAO,KAAK;AAAA,QAC3C,CAAC;AACD,cAAM,MAAM;AACZ,gBAAQ,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,MAAM,SAAS,CAAC;AAAA,MACtD,SAAS,OAAO;AACd,cAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,gBAAQ,OAAO,MAAM,yBAAyB,KAAK,IAAI,KAAK,OAAO;AAAA,CAAI;AACvE,gBAAQ,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,OAAO,UAAU,GAAG,OAAO,QAAQ,CAAC;AAAA,MAC1E;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,OAAO;AAAA,MACb,oBAAoB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA;AAAA,IAC5E;AAAA,EACF;AACA,SAAO;AACT;;;AC7EA,SAAS,gBAAAC,qBAAoB;AAiB7B,IAAM,eAAe;AAIrB,IAAMC,eAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,OAAO,OAAc,OAAc,QAAQ,cAA6B;AACtF,MAAI,MAAM,YAAY,aAAa,GAAG,QAAS,QAAO,KAAK,QAAQ,MAAM,MAAM,KAAK;AAEpF,QAAM,OAAO,MAAM,MAAM,EAAE,KAAK,CAAC,cAAc,UAAU,YAAY,MAAM,OAAO;AAClF,QAAM,SAAS,MAAM,UAAU,MAAM;AACrC,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI;AAIF,WAAOC;AAAA,MACL;AAAA,MACA;AAAA,QACE,GAAGD;AAAA,QACH;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,IAAI,MAAM,IAAI;AAAA,QACd;AAAA,QACA,IAAI,KAAK;AAAA,MACX;AAAA,MACA,EAAE,UAAU,QAAQ,SAAS,KAAM,OAAO,CAAC,UAAU,QAAQ,QAAQ,EAAE;AAAA,IACzE;AAAA,EACF,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;;;AClCA,SAAS,cAAsB;AAC7B,SAAO,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE;AAChE;AAcO,SAAS,OAAO,OAAc,MAAM,KAAK,IAAI,GAAW;AAC7D,QAAM,WAAW,aAAa;AAC9B,QAAM,QAAQ,MAAM,MAAM;AAC1B,QAAM,cAAc,IAAI;AAAA,IACtB,MAAM,QAAQ,CAAC,SAAU,KAAK,YAAY,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,SAAS,IAAI,CAAU,CAAE;AAAA,EACxF;AACA,QAAM,SAAS,MAAM,UAAU;AAC/B,QAAM,QAAQ;AAAA,IACZ,OAAO,OAAO,CAAC,UAAU,MAAM,YAAY,UAAU,OAAO;AAAA,IAC5D;AAAA,EACF;AACA,QAAM,SAAS;AAAA,IACb,OAAO,OAAO,CAAC,UAAU,MAAM,YAAY,UAAU,OAAO;AAAA,IAC5D,MAAM;AAAA,EACR;AACA,QAAM,SAAS,YAAY;AAC3B,QAAM,qBAAqB,YAAY;AACvC,QAAM,SAAS,cAAc,CAAC,GAAG,OAAO,GAAG,MAAM,CAAC,EAAE,IAAI,CAAC,UAAU;AACjE,UAAM,OAAO,YAAY,IAAI,MAAM,OAAO;AAC1C,UAAM,YAAY,MAAM,cAAc;AACtC,UAAM,QACJ,MAAM,UAAU,QAAQ,MAAM,UAAU,YAAY,SAAS,MAAM;AACrE,UAAM,SAAS,MAAM,WAAW,UAAU,SAAS;AACnD,WAAO,KAAK,KAAK;AACjB,WAAO;AAAA,MACL,GAAG;AAAA,MACH,YAAY;AAAA;AAAA;AAAA,MAGZ,OAAO,QAAQ,WAAW,KAAK,YAAY;AAAA,MAC3C,QAAQ,cAAc,OAAO,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,MAK1C,cAAc,MAAM,UAAU,OAAO,OAAO,KAAK,IAAI,GAAG,MAAM,MAAM,MAAM,EAAE;AAAA;AAAA;AAAA,MAG5E,WAAW,MAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOjC,MACE,MAAM,SAAS,MAAM,YAAY,UAAU,UAAU,SAAS,eAAe,MAAM;AAAA,IACvF;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA,qBAAqB;AAAA,IACrB;AAAA,IACA,OAAO,MAAM,IAAI,CAAC,UAAU;AAAA,MAC1B,MAAM,KAAK;AAAA,MACX,cAAc,KAAK;AAAA,MACnB,YAAY,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMjB,OAAO,KAAK,eAAe,QAAQ,QAAQ,KAAK,YAAY,KAAK,YAAY;AAAA,IAC/E,EAAE;AAAA,EACJ;AACF;;;AC9GA,SAAS,cAAc;AACvB,OAAO,cAAc;AAKrB,IAAM,uBAAuB,IAAI;AAS1B,IAAM,gBAAgB;AAkB7B,SAAS,aAAa,MAAsB;AAC1C,MAAI,WAAmB,CAAC;AACxB,MAAI;AACF,UAAM,WAAW,IAAI,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;AAC3D,UAAM,UAAW,SAAS,OAAO,gBAAgB,EAAE,QAAQ,KAAK,CAAC,KAAgB;AACjF,QAAI,YAAY,eAAe;AAC7B,eAAS,MAAM;AACf,aAAO;AAAA,IACT;AACA,QAAI;AACF,iBAAW,SACR,QAAQ,uDAAuD,EAC/D,IAAI;AAAA,IACT,QAAQ;AAAA,IAER;AACA,aAAS,MAAM;AAAA,EACjB,QAAQ;AAEN,WAAO;AAAA,EACT;AAIA,aAAW,UAAU,CAAC,IAAI,QAAQ,MAAM,EAAG,QAAO,GAAG,IAAI,GAAG,MAAM,IAAI,EAAE,OAAO,KAAK,CAAC;AACrF,SAAO;AACT;AAsBA,SAAS,YAAY,OAAyB;AAC5C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO,MAAM,SAAS;AAAA,IACtB,MAAM;AAAA,IACN,KAAK,UAAU,MAAM,KAAK;AAAA,EAC5B;AACF;AAEA,SAAS,QAAQ,KAAsB;AACrC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI,cAAc;AAAA,IAC7B,OAAO,KAAK,MAAM,IAAI,KAAK;AAAA,EAC7B;AACF;AAwBO,SAAS,YAAmB;AACjC,QAAM,WAAW,eAAe;AAChC,QAAM,OAAO,OAAO;AACpB,QAAM,gBAAgB,aAAa,IAAI;AACvC,QAAM,WAAW,IAAI,SAAS,IAAI;AAClC,WAAS,OAAO,oBAAoB;AACpC,WAAS,OAAO,kBAAkB,aAAa,EAAE;AACjD,WAAS,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAwCb;AAGD,MAAI;AACF,aAAS,KAAK,mDAAmD;AAAA,EACnE,QAAQ;AAAA,EAER;AAIA,MAAI,cAAc,SAAS,GAAG;AAC5B,UAAM,UAAU,SAAS;AAAA,MACvB;AAAA;AAAA,IAEF;AACA,eAAW,QAAQ,eAAe;AAChC,cAAQ,IAAI,KAAK,MAAM,KAAK,QAAQ,KAAK,WAAW,MAAM,KAAK,gBAAgB,IAAI;AAAA,IACrF;AAAA,EACF;AAEA,QAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAKrB,QAAM,oBAAoB,IAAI,MAAM,EAAE,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI;AAC3D,QAAM,cAAc,SAAS;AAAA,IAC3B,uBAAuB,YAAY,aAAa,iBAAiB;AAAA,EACnE;AACA,QAAM,cAAc,SAAS;AAAA,IAC3B,iCAAiC,YAAY,aAAa,iBAAiB;AAAA,EAC7E;AACA,QAAM,eAAe,SAAS;AAAA,IAC5B;AAAA,EACF;AACA,QAAM,SAAS,SAAS,YAAY,CAAC,UAA2B;AAC9D,UAAM,MAAM,aAAa,IAAI,SAAS,OAAO;AAC7C,UAAM,SAAgB;AAAA,MACpB,GAAG;AAAA,MACH,SAAS,SAAS;AAAA,MAClB,KAAK,IAAI,MAAM;AAAA,MACf,IAAI,MAAM,MAAM,KAAK,IAAI;AAAA,MACzB,cAAc,MAAM,gBAAgB;AAAA,MACpC,aAAa,MAAM,eAAe;AAAA,MAClC,YAAY,MAAM,cAAc;AAAA,MAChC,YAAY,MAAM,cAAc;AAAA,IAClC;AACA,gBAAY,IAAI,GAAG,YAAY,MAAM,CAAC;AACtC,WAAO;AAAA,EACT,CAAC;AACD,QAAM,SAAS,SAAS,YAAY,CAAC,WAA4B;AAC/D,QAAI,WAAW;AACf,eAAW,SAAS,OAAQ,aAAY,YAAY,IAAI,GAAG,YAAY,KAAK,CAAC,EAAE;AAC/E,WAAO;AAAA,EACT,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,YAAY,QAAQ,KAAK;AACvB,YAAM,OAAO,SACV,QAAQ,iEAAiE,EACzE,IAAI,QAAQ,GAAG;AAClB,aAAO,KAAK,IAAI,OAAO;AAAA,IACzB;AAAA,IACA,YAAY;AACV,YAAM,OAAO,SACV,QAAQ,gDAAgD,EACxD,IAAI;AACP,aAAO,KAAK,IAAI,OAAO;AAAA,IACzB;AAAA,IACA,OAAO,QAAQ;AACb,aAAQ,aAAa,IAAI,MAAM,EAAsB;AAAA,IACvD;AAAA,IACA,MAAM,YAAY,OAAO,QAAQ,IAAI,uBAAuB,oBAAoB,GAAG;AACjF,aAAO,SACJ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAUR,EACA,IAAI,KAAK,IAAI,IAAI,SAAS,EAAE;AAAA,IACjC;AAAA,IACA,QAAQ;AACN,aAAO,SAAS,QAAQ,mCAAmC,EAAE,IAAI;AAAA,IACnE;AAAA,IACA,YAAY,SAAS;AACnB,aAAO,SAAS,QAAQ,uCAAuC,EAAE,IAAI,OAAO,EAAE;AAAA,IAChF;AAAA,IACA,WAAW,QAAQ;AAIjB,aAAO,SAAS,QAAQ,sCAAsC,EAAE,IAAI,MAAM,EAAE;AAAA,IAC9E;AAAA,IACA,WAAW,MAAM;AACf,YAAM,UAAU,SAAS,QAAQ,oCAAoC,EAAE,IAAI,KAAK,IAAI;AAGpF,eACG,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAUR,EACA;AAAA,QACC,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK,YAAY,SAAY,KAAK,UAAW,SAAS,WAAW;AAAA,QACjE,KAAK,iBAAiB,SAAY,KAAK,eAAgB,SAAS,gBAAgB;AAAA,QAChF,KAAK,cAAc,SAAY,KAAK,YAAa,SAAS,aAAa;AAAA,QACvE,KAAK,eAAe,SAAY,KAAK,aAAc,SAAS,cAAc;AAAA,QAC1E,KAAK,iBAAiB,SAAY,KAAK,eAAgB,SAAS,gBAAgB;AAAA,MAClF;AAAA,IACJ;AAAA,IACA,WAAW,MAAM;AAKf,aAAO,SAAS,QAAQ,kCAAkC,EAAE,IAAI,IAAI,EAAE,UAAU;AAAA,IAClF;AAAA,IACA,QAAQ;AACN,eAAS,MAAM;AAAA,IACjB;AAAA,EACF;AACF;;;AZ7TA,IAAM,WAAW,cAAc,YAAY,GAAG,EAAE,iBAAiB;AAC1D,IAAM,UAAkB,SAAS;","names":["join","join","execFileSync","execFileSync","SSH_OPTIONS","execFileSync"]}
|