@martintrojer/murmur 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 // 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":"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","names":["join","join","execFileSync","execFileSync","SSH_OPTIONS","execFileSync"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/agents.ts","../src/channel.ts","../src/identity.ts","../src/paths.ts","../src/mux.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 type CollectResult,\n collect,\n MAX_CONCURRENT_PEERS,\n STALENESS_MS,\n} from \"./collector.js\";\nexport { eventFromWire, exportJsonl, SCHEMA_VERSION } from \"./export.js\";\nexport {\n type AgentView,\n attentionSort,\n foldAgent,\n foldAll,\n isStale,\n type LiveCheck,\n} from \"./fold.js\";\nexport { glance } from \"./glance.js\";\nexport { ensureIdentity, loadIdentity, type NodeIdentity } from \"./identity.js\";\nexport { type Mux, pidAlive, tmux } from \"./mux.js\";\nexport { configDir, dbPath, stateDir } from \"./paths.js\";\nexport { type Status, status } from \"./status.js\";\nexport { type NewEvent, openStore, STORE_VERSION, type Store } from \"./store.js\";\nexport {\n type AgentState,\n DEFAULT_DRIVER,\n type Driver,\n type Event,\n type Peer,\n} from \"./types.js\";\n","import { spawnSync } from \"node:child_process\";\nimport { SSH_OPTIONS } from \"./channel.js\";\nimport { loadIdentity } from \"./identity.js\";\nimport { type Mux, tmux } from \"./mux.js\";\nimport type { Status } from \"./status.js\";\nimport type { Store } from \"./store.js\";\n\nexport type Agent = Status[\"agents\"][number];\n\n/**\n * The most specific human-readable name an agent has, never a tmux id.\n *\n * Four sources, most to least specific: mu's agent name, pi's session name,\n * the tmux window name, the tmux session name. The old picker showed window\n * names and that was the thing it did better than raw `$26:@79`; these are all\n * recorded on the event, so this reads the same for a local and a remote agent.\n *\n * Falls back to the window id only when a node recorded no names at all, which\n * means a pre-names event or a non-tmux harness.\n */\nexport function agentLabel(agent: Agent): string {\n const name = agent.agent_name ?? agent.pi_session ?? agent.window_name ?? agent.session_name;\n return terminalText(name ?? agent.window);\n}\n\n/**\n * Where the agent lives, for the second column. Names only -- the ids are what\n * jumps, not what a human reads.\n */\nexport function agentLocation(agent: Agent): string {\n const session = agent.session_name ?? agent.session;\n const window = agent.window_name ?? agent.window;\n return terminalText(session === window ? session : `${session}:${window}`);\n}\n\nexport function terminalText(value: string): string {\n return [...value]\n .map((character) => {\n const code = character.charCodeAt(0);\n return code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f) ? \"�\" : character;\n })\n .join(\"\");\n}\n\nexport function shellQuote(value: string): string {\n return `'${value.replaceAll(\"'\", `'\\\\''`)}'`;\n}\n\n/**\n * The one process call jump makes that is not a tmux command: the remote probe,\n * and the direct ssh attach when we are not inside tmux. Injectable so the jump\n * decision table can be tested without an ssh binary or a live peer -- without\n * this seam, `jumpToAgent` had no behavioural coverage at all and replacing its\n * body with `return { ok: true }` kept every jump test green.\n */\nexport type Runner = (\n file: string,\n args: string[],\n inherit?: boolean,\n) => { status: number | null; stdout: string; failed: boolean };\n\nexport const spawnRunner: Runner = (file, args, inherit = false) => {\n const result = spawnSync(file, args, {\n encoding: \"utf8\",\n timeout: 10_000,\n ...(inherit ? { stdio: \"inherit\" as const } : {}),\n });\n return {\n status: result.status,\n stdout: result.stdout ?? \"\",\n // spawnSync reports a failure to even start the child in `error`, leaving\n // status null. Collapsing both here keeps the decision table below reading\n // as one question rather than two.\n failed: result.error !== undefined,\n };\n};\n\nexport type JumpResult =\n | { ok: true }\n | {\n ok: false;\n reason: \"no_peer\" | \"unreachable\" | \"no_tmux\" | \"window_gone\" | \"attach_failed\";\n message: string;\n };\n\n/**\n * Drop a dead agent's rows from the local replica.\n *\n * Export on the authoring node clears dead windows, but that only runs when the\n * peer is next polled, and a window can die between a fetch and a jump. When a\n * jump proves the window is gone, the agent should leave this HUD now rather\n * than at the next collect.\n *\n * DELETE rather than append a `cleared` event, because this node cannot author\n * an event about another node's agent. `store.append` stamps the local host_id,\n * and `status()` folds local and remote events separately (local needs a pid\n * check, remote cannot have one) -- so a local row about a remote agent lands\n * in the other fold and shows up as a SECOND agent with the same agent_id,\n * which is exactly what it did before this was a delete.\n *\n * Deleting a replica is safe ONLY IF the rows can come back, and that needs the\n * peer's watermark rewound as well. Ingest asks for events after the watermark,\n * so deleting rows below it deletes them permanently: bubba's agents vanished\n * from the picker and no amount of collecting brought them back, even with the\n * node alive and the events still in its log.\n *\n * Rewinding to zero rather than to the deleted seq: the log is bounded by the\n * retention horizon, ingest is idempotent on (host_id, seq), and a re-read of a\n * small table is cheaper than tracking which seq belonged to which agent. The\n * next collect re-reads everything the peer still has, so if the window is\n * genuinely alive the agent reappears -- which is the answer to the race where\n * the host comes back up between the jump and the next poll.\n *\n * For a local agent there is no watermark and nothing to rewind: the pane is\n * gone, so nothing will ever author about it again.\n */\n/**\n * A jump proved this peer has no tmux server, so none of its agents exist.\n *\n * Drops every replicated row for that origin and rewinds the watermark, the\n * same recoverable delete `forgetReplica` does for one agent — just scoped to\n * the node, because \"no tmux server\" is a fact about the host rather than about\n * the window we happened to aim at. Leaving the rows and only labelling them\n * meant the picker kept offering four dead agents you had just been told were\n * gone.\n *\n * The mark stays on the peer as well: it is what stops an empty export being\n * read as recovery, and it is why the rows do not immediately reappear.\n */\nexport function forgetHostReplica(store: Store, hostId: string): void {\n try {\n const peer = store.peers().find((candidate) => candidate.host_id === hostId);\n store.forgetHost(hostId);\n if (peer) {\n // Watermark deliberately NOT rewound here, unlike the single-agent case.\n // Rewinding re-ingests the very rows just deleted, and because the\n // collector reads any ingest as \"the node is authoring again\", it also\n // cleared the mark -- so the dead agents reappeared looking healthy on\n // the next collect, one second later.\n //\n // Keeping the watermark means recovery waits for a NEW event, which is\n // the correct bar: the node has to actually say something before its\n // agents come back. Nothing is lost, since the rows describe windows a\n // live tmux server would re-announce.\n store.upsertPeer({\n name: peer.name,\n target: peer.target,\n tmux_down_at: Date.now(),\n });\n }\n } catch {\n // Advisory only: the next collect reconciles either way.\n }\n}\n\nexport function forgetReplica(store: Store, agentId: string, hostId: string): void {\n try {\n store.forgetAgent(agentId);\n const peer = store.peers().find((candidate) => candidate.host_id === hostId);\n if (peer) store.upsertPeer({ name: peer.name, target: peer.target, watermark: 0 });\n } catch {\n // Cosmetic only: the next collect reconciles either way.\n }\n}\n\n/**\n * Drop one agent from the picker by hand.\n *\n * The escape hatch for a row that is stuck and that nothing else will clear: an\n * agent whose pane died in a way that left no terminal event, or a replica from\n * a peer that will never report again. Everything else here reconciles on its\n * own, so this exists for the cases that do not.\n *\n * A local agent also gets its tmux badge cleared. Deleting only the row would\n * leave `@agent_state` set, which the status bar and the tms session picker\n * both read — so the glyph would survive the row it came from and nothing would\n * ever clear it.\n *\n * Not authoritative, and cannot be: for a remote agent this deletes a replica,\n * and the owning node still holds the truth. If that node reports again the\n * agent comes back, which is correct — a row you dismissed while the agent was\n * alive should return.\n */\nexport function forgetOneAgent(store: Store, agent: Agent, mux: Mux = tmux): void {\n const identity = loadIdentity();\n if (agent.host_id === identity?.host_id) {\n try {\n mux.setState(agent.window, null);\n } catch {\n // Best effort: the row still goes.\n }\n }\n forgetReplica(store, agent.agent_id, agent.host_id);\n}\n\nexport function jumpToAgent(\n store: Store,\n agent: Agent,\n mux: Mux = tmux,\n run: Runner = spawnRunner,\n): JumpResult {\n const identity = loadIdentity();\n if (agent.host_id === identity?.host_id) {\n const live = mux.liveWindows();\n if (live && !live.has(agent.window)) {\n forgetReplica(store, agent.agent_id, agent.host_id);\n return {\n ok: false,\n reason: \"window_gone\",\n message: `${agentLabel(agent)} is gone -- its window no longer exists. Cleared.`,\n };\n }\n // Reporting the attach rather than assuming it. A select-window that fails\n // is the local twin of the remote symptom: the picker closes, nothing\n // moves, and nothing says why.\n if (!mux.attach(agent.session, agent.window)) {\n return {\n ok: false,\n reason: \"attach_failed\",\n message: `could not attach to ${agentLabel(agent)} (tmux select-window failed).`,\n };\n }\n return { ok: true };\n }\n const peer = store.peers().find((candidate) => candidate.host_id === agent.host_id);\n const target = peer?.target ?? peer?.name;\n if (!target) {\n return {\n ok: false,\n reason: \"no_peer\",\n message: `No peer configured for host ${agent.host_id.slice(0, 8)}. Try: murmur peer add <target>`,\n };\n }\n\n // Check the window is still there before opening a window to attach to it.\n // Without this the attach fails inside a new tmux window that closes\n // instantly, which is indistinguishable from \"enter did nothing\" -- the\n // symptom that sent us looking for a quoting bug that did not exist.\n // ssh does not take an argv: it joins its arguments and hands the string to a\n // shell on the far side. An unquoted `#{window_id}` is mangled by that shell\n // and tmux answers `-F expects an argument`, which looked exactly like an\n // unreachable host. One quoted string, so the remote shell passes the format\n // through untouched.\n //\n // Shares the collector's SSH_OPTIONS rather than passing BatchMode alone.\n // Without ControlPath the probe could not use the warm master socket the\n // collector rides, and without ConnectTimeout it inherited the kernel's dial\n // -- 75s on macOS, bounded only by the timeout below, so a sleeping laptop\n // froze the picker for ten seconds before admitting it was unreachable.\n const probe = run(\"ssh\", [\n ...SSH_OPTIONS,\n target,\n `tmux list-windows -a -F ${shellQuote(\"#{window_id}\")}`,\n ]);\n if (probe.status !== 0) {\n // 255 is ssh's own failure code; anything else came from the remote\n // command. Conflating them was wrong in the common case: with a warm\n // ControlMaster socket the host answers instantly and it is tmux that is\n // gone, so \"unreachable\" sent you looking at the network for a problem that\n // was not there.\n const sshFailed = probe.status === 255 || probe.failed;\n if (sshFailed) {\n // No mark: we learned nothing about the peer's tmux, only that we could\n // not ask. Its agents may be perfectly alive behind a cold socket or a\n // sleeping laptop, and deleting them here would be guessing.\n return {\n ok: false,\n reason: \"unreachable\",\n message: `cannot reach ${target} over ssh. Nothing here ever prompts for auth, so check the host is awake and reachable, or connect once by hand to see the real error.`,\n };\n }\n\n // ssh worked, tmux did not. That is a real fact about the host and the\n // strongest one available: a successful export only proves the murmur\n // binary ran, which it does happily on a box whose tmux server is gone --\n // which is why these agents read as fresh for three hours.\n forgetHostReplica(store, agent.host_id);\n return {\n ok: false,\n reason: \"no_tmux\",\n message: `${target} has no tmux server running, so its agents are gone. Removed them; they will come back when it reports again.`,\n };\n }\n const remoteWindows = new Set(probe.stdout.split(\"\\n\").filter(Boolean));\n if (!remoteWindows.has(agent.window)) {\n forgetReplica(store, agent.agent_id, agent.host_id);\n return {\n ok: false,\n reason: \"window_gone\",\n message: `${agentLabel(agent)} is gone -- ${target} no longer has that window. Cleared.`,\n };\n }\n\n const attachTarget = shellQuote(`${agent.session}:${agent.window}`);\n\n // Hand the ssh to tmux as its own window rather than running it here.\n // `murmur pick` is usually a display-popup, and a popup is modal: an ssh\n // session started inside it is killed the moment the picker exits, so the\n // remote pane flashed and vanished. A new window outlives the popup and\n // gives the remote tmux a real terminal to attach to.\n //\n // Nested tmux is the known cost here (see the spec's open question on inner\n // prefixes); a window at least makes it visible and closable.\n if (process.env.TMUX) {\n // `tmux new-window <command>` runs the command through a shell, so the\n // string is expanded LOCALLY before ssh sees it. A tmux session id is\n // always `$N`, so `$0:@6` arrived as `:@6` and the remote attach failed\n // with \"can't find session\". shellQuote alone is not enough: it protects\n // the remote shell, this protects the local one.\n const command = `ssh -t ${shellQuote(target)} tmux attach -t ${shellQuote(attachTarget)}`;\n // Named after the peer as configured, matching what the picker's host\n // column shows. The machine's self-reported display_name can be something\n // like a container id, which makes the window unrecognisable.\n const name = `@${peer?.name ?? target}`;\n\n // Reuse an existing window for this host rather than stacking a new one on\n // every jump. murmur navigates to agents; the window is only here because a\n // remote attach needs a terminal that outlives the popup, so one per host is\n // the whole requirement. Jumping to bubba three times used to leave three\n // identical @bubba windows behind.\n //\n // Matched on window name, which is the only handle available: the ssh is\n // opaque from here, and the remote session id is not a local address.\n const existing = mux.windowNamed(name);\n if (existing) {\n return mux.selectWindow(existing)\n ? { ok: true }\n : {\n ok: false,\n reason: \"attach_failed\",\n message: `could not switch to the existing ${name} window.`,\n };\n }\n\n return mux.newWindow(name, command)\n ? { ok: true }\n : {\n ok: false,\n reason: \"attach_failed\",\n message: `could not open a window to attach to ${target}.`,\n };\n }\n\n // Outside tmux there is no popup to escape, so run it directly. stdio is\n // inherited, so this blocks until the user leaves the remote session; a\n // nonzero exit means the attach itself failed.\n const attach = run(\"ssh\", [\"-t\", target, \"tmux\", \"attach\", \"-t\", attachTarget], true);\n return attach.status === 0 && !attach.failed\n ? { ok: true }\n : {\n ok: false,\n reason: \"attach_failed\",\n message: `ssh attach to ${target} failed.`,\n };\n}\n","import { execFile, execFileSync } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\nconst CONTROL_PATH = \"~/.ssh/control/%r@%h:%p\";\n\n// Both timeouts are sized against the tmux status bar, because that is what\n// actually drives collection: `murmur status` collects, and tmux re-runs it\n// every `status-interval` — 5s on the author's setup, 15s by default. A collect\n// that outlives its tick is a collect overlapping itself, and tmux offers no\n// way to cancel the last one.\n//\n// So the budget for the whole exchange is under 5s, and these are deliberately\n// aggressive: a really slow node is rejected rather than allowed to hold up the\n// HUD. That is cheap because the cost of losing the race is one tick of\n// staleness, and the next tick is five seconds away.\n\n// OpenSSH's default TCP connect timeout is the kernel's, 75s on macOS, which\n// made `murmur pick` unusable against a sleeping laptop. One second is still\n// ~6x a real cold handshake on a LAN or VPN (measured: 168ms cold, 42ms on a\n// warm control socket), and a peer that misses it simply shows stale — the\n// designed outcome for a host you cannot reach.\nconst CONNECT_TIMEOUT_S = 1;\n\n// Belt and braces for a host that completes the TCP connect and then stops\n// responding — ConnectTimeout does not cover that, and it is how a sleeping\n// laptop behaves. Bounds the whole exchange rather than just the dial, so it\n// has to leave room for the dial plus an export: three seconds is the tick\n// budget minus headroom for the rest of `status`.\nconst EXEC_TIMEOUT_MS = 3_000;\n\n// Warm if possible, cold if not, never interactive.\n//\n// ControlMaster=no attaches to a master socket left behind by an ordinary\n// `ssh <host>` (given ControlMaster auto + ControlPersist in ssh_config), so a\n// peer you have touched recently costs a new channel on an authenticated\n// connection rather than a handshake.\n//\n// When no socket is listening OpenSSH falls back to connecting normally, and we\n// want that: with plain key auth a cold peer collects fine, just slower\n// (~170ms against ~10ms measured on a LAN). Fleet visibility should not depend\n// on having ssh'd somewhere today.\n//\n// BatchMode=yes bounds what that fallback may do. It disables every\n// interactive prompt — password, passphrase, host key confirmation — so a\n// cold peer that cannot authenticate silently fails immediately instead of\n// blocking a background collect on a human. Note this is \"never prompt\", not\n// \"never authenticate\": a host demanding a hardware-token touch per connection\n// is the case this does not fully cover, and the reason `hasWarmSocket` exists\n// should that ever need gating.\n//\n// Exported because every ssh murmur runs wants exactly this posture -- the\n// collector, the picker's preview, the jump probe. Three hand-rolled copies is\n// how one of them ends up without BatchMode and starts prompting for auth on\n// every keypress.\nexport const SSH_OPTIONS = [\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"ControlMaster=no\",\n \"-o\",\n `ControlPath=${CONTROL_PATH}`,\n \"-o\",\n `ConnectTimeout=${CONNECT_TIMEOUT_S}`,\n];\n\nexport interface Channel {\n exec(target: string, argv: string[]): Promise<string>;\n}\n\n// Node's execFile defaults to a 1 MiB stdout ceiling and rejects with\n// ERR_CHILD_PROCESS_STDIO_MAXBUFFER past it, killing the child. An export is\n// the peer's whole delta, and at ~400 bytes an event (measured) 1 MiB is only\n// ~2,600 events -- reachable inside the 7-day horizon on a busy box, and\n// certain on a first sync from watermark 0.\n//\n// The failure would also be permanent, not transient: the watermark only\n// advances on a successful parse, so every subsequent collect would re-request\n// the same oversized range and fail identically. A reachable peer would sit\n// stale forever.\n//\n// 64 MiB is ~170k events, far above what the horizon can hold, and it is a\n// ceiling rather than an allocation. The timeout is the real bound on a\n// runaway peer.\nconst MAX_EXPORT_BYTES = 64 * 1024 * 1024;\n\nexport const ssh: Channel = {\n async exec(target, argv) {\n const { stdout } = await execFileAsync(\"ssh\", [...SSH_OPTIONS, target, ...argv], {\n encoding: \"utf8\",\n timeout: EXEC_TIMEOUT_MS,\n maxBuffer: MAX_EXPORT_BYTES,\n });\n return stdout;\n },\n};\n\nexport function hasWarmSocket(target: string): boolean {\n try {\n execFileSync(\"ssh\", [...SSH_OPTIONS, \"-O\", \"check\", target], { stdio: \"ignore\" });\n return true;\n } catch {\n return false;\n }\n}\n","import { randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\nimport { stateDir } from \"./paths.js\";\n\nexport type NodeIdentity = {\n host_id: string;\n display_name: string;\n};\n\nexport function loadIdentity(): NodeIdentity | null {\n const path = join(stateDir(), \"identity.json\");\n return existsSync(path) ? JSON.parse(readFileSync(path, \"utf8\")) : null;\n}\n\nexport function ensureIdentity(displayName = hostname()): NodeIdentity {\n const existing = loadIdentity();\n if (existing) return existing;\n\n const identity = { host_id: randomUUID(), display_name: displayName };\n mkdirSync(stateDir(), { recursive: true });\n writeFileSync(join(stateDir(), \"identity.json\"), `${JSON.stringify(identity, null, 2)}\\n`);\n return identity;\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function stateDir(): string {\n return (\n process.env.MURMUR_STATE_DIR ??\n join(process.env.XDG_STATE_HOME ?? join(homedir(), \".local\", \"state\"), \"murmur\")\n );\n}\n\nexport function configDir(): string {\n return (\n process.env.MURMUR_CONFIG_DIR ??\n join(process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\"), \"murmur\")\n );\n}\n\nexport function dbPath(): string {\n return join(stateDir(), \"events.db\");\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { AgentState } from \"./types.js\";\n\nexport type Location = {\n session: string;\n window: string;\n pane: string;\n session_name: string | null;\n window_name: string | null;\n};\n\nexport interface Mux {\n currentWindow(): Location | null;\n liveWindows(): Set<string> | null;\n setState(window: string, state: AgentState | null): void;\n // Reports whether the attach actually happened. runTmux swallows failures to\n // return null, and a jump that silently failed looked exactly like \"enter did\n // nothing\" -- the symptom the remote probe was added to prevent, reproduced\n // on the local path.\n attach(session: string, window: string): boolean;\n windowNames(): Map<string, string>;\n windowForPane(pane: string): string | null;\n panesInWindow(window: string): string[];\n windowNamed(name: string): string | null;\n selectWindow(window: string): boolean;\n newWindow(name: string, command: string): boolean;\n capture(pane: string, lines?: number): string | null;\n}\n\nfunction runTmux(args: string[]): string | null {\n try {\n return execFileSync(\"tmux\", args, {\n encoding: \"utf8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n } catch {\n return null;\n }\n}\n\nexport const tmux: Mux = {\n currentWindow() {\n // $TMUX_PANE is the only trustworthy signal that we are inside a pane, and\n // it is set by tmux for every process in one.\n //\n // Asking tmux instead does not work: `display-message` answers from any\n // process on a machine with a running server, and reports whichever pane\n // that server considers active. A pi started outside tmux -- a bare ssh\n // login, a plain terminal, cron -- would then record itself as living in\n // some unrelated agent's pane and overwrite that agent's state. Falling\n // back to `display-message` here was exactly that bug.\n const pane = process.env.TMUX_PANE;\n if (!pane) return null;\n\n // One call for ids and names together. The names are recorded on every\n // event because a reader cannot resolve a remote window id against its own\n // tmux, so they have to travel with the event.\n const fields = runTmux([\n \"display-message\",\n \"-t\",\n pane,\n \"-p\",\n \"#{session_id}\\t#{window_id}\\t#{session_name}\\t#{window_name}\",\n ]);\n const [session, window, sessionName, windowName] = fields?.split(\"\\t\") ?? [];\n if (!session || !window) return null;\n return {\n session,\n window,\n pane,\n session_name: sessionName || null,\n window_name: windowName || null,\n };\n },\n\n // Which of this host's windows still exist. Only the authoring node can\n // answer this, which is why the check runs on export rather than on the\n // reader: a peer holding a `blocked` row for a window that died has nothing\n // to supersede it, and the agent stays in every HUD forever.\n //\n // null means \"could not tell\" (no tmux server, tmux missing) and is\n // deliberately distinct from an empty set, which means \"tmux answered, and\n // there are no windows\". Treating the first as the second would clear every\n // agent on the host the moment tmux was unreachable.\n //\n // Unlike currentWindow, this deliberately asks tmux rather than reading the\n // environment, and it is right to: \"which windows exist on this host\" is a\n // server-wide question with one answer, and export runs over ssh with no\n // pane of its own. currentWindow asks \"which pane am I in\", which only\n // $TMUX_PANE can answer.\n liveWindows() {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\"]);\n if (out === null) return null;\n return new Set(out.split(\"\\n\").filter(Boolean));\n },\n\n setState(window, state) {\n if (state === null) {\n runTmux([\"set-window-option\", \"-qu\", \"-t\", window, \"@agent_state\"]);\n } else {\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@agent_state\", state]);\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@pane_agent\", \"1\"]);\n }\n runTmux([\"refresh-client\", \"-S\"]);\n },\n\n attach(session, window) {\n // Two steps, because switch-client alone is a no-op when the target window\n // is in the session you are already attached to — which is the common case\n // for a local agent, and why \"enter\" appeared to do nothing.\n // switch-client moves the client between sessions; select-window moves\n // that session to the right window.\n //\n // Only select-window decides the result. switch-client legitimately fails\n // when there is no client to switch (running outside tmux), and treating\n // that as a failed jump would report an error for a working attach.\n runTmux([\"switch-client\", \"-t\", session]);\n return runTmux([\"select-window\", \"-t\", window]) !== null;\n },\n\n // Window ids are what the log stores, because they are stable; names are\n // what a human recognises in a picker. Names are live tmux state, not\n // history, so they are resolved at render time rather than recorded.\n windowNames() {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\\t#{window_name}\"]);\n const names = new Map<string, string>();\n for (const line of out?.split(\"\\n\") ?? []) {\n const [id, name] = line.split(\"\\t\");\n if (id && name) names.set(id, name);\n }\n return names;\n },\n\n // First window carrying this exact name, or null. Used to reuse a per-host\n // ssh window instead of opening another one.\n // Sibling panes, for deciding whether an unowned pane may clear the window's\n // badge. A window holding an agent and a shell must not lose the badge when\n // you focus the shell.\n panesInWindow(window) {\n const out = runTmux([\"list-panes\", \"-t\", window, \"-F\", \"#{pane_id}\"]);\n return out?.split(\"\\n\").filter(Boolean) ?? [];\n },\n\n windowNamed(name) {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\\t#{window_name}\"]);\n for (const line of out?.split(\"\\n\") ?? []) {\n const [id, windowName] = line.split(\"\\t\");\n if (id && windowName === name) return id;\n }\n return null;\n },\n\n selectWindow(window) {\n return runTmux([\"select-window\", \"-t\", window]) !== null;\n },\n\n newWindow(name, command) {\n return runTmux([\"new-window\", \"-n\", name, command]) !== null;\n },\n\n // The window a pane belongs to, for a pane murmur has no event for. Clearing\n // a badge is a tmux operation and does not require murmur to own the pane.\n windowForPane(pane) {\n return runTmux([\"display-message\", \"-t\", pane, \"-p\", \"#{window_id}\"]) || null;\n },\n\n capture(pane, lines) {\n const args = [\"capture-pane\", \"-p\", \"-t\", pane];\n if (lines !== undefined) args.push(\"-S\", `-${lines}`);\n return runTmux(args);\n },\n};\n\nexport function pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code !== \"ESRCH\";\n }\n}\n","export type AgentState = \"working\" | \"blocked\" | \"done\" | \"crashed\" | \"cleared\";\n\nexport type Driver = \"human\" | \"orchestrated\";\n\nexport const DEFAULT_DRIVER: Driver = \"human\";\n\nexport type Event = {\n host_id: string;\n seq: number;\n ts: number;\n agent_id: string;\n session: string;\n window: string;\n pane: string;\n // Human-readable names, recorded by the node that owns the pane. tmux ids are\n // stable and are what jumps; names are what a human recognises. They are\n // *recorded* rather than resolved at render time because a reader cannot look\n // a remote window id up in its own tmux -- doing so labelled a remote agent\n // with whatever this host had at that id. Cost: a renamed window keeps its\n // old name until the next event, which is the same property the history rows\n // always had.\n session_name: string | null;\n window_name: string | null;\n // The agent's own idea of what it is working on: pi's session name, and mu's\n // $MU_AGENT_NAME for an orchestrated agent. Both are richer than the window\n // name when they exist, and neither is derivable from tmux.\n agent_name: string | null;\n pi_session: string | null;\n workstream: string | null;\n role: string | null;\n cli: string | null;\n driver: Driver | null;\n kind: string;\n state: AgentState | string;\n message: string;\n pid: number | null;\n synthetic: boolean;\n reason: string;\n extra: Record<string, unknown>;\n};\n\nexport type Peer = {\n name: string;\n target: string;\n host_id: string | null;\n display_name: string | null;\n watermark: number;\n fetched_at: number | null;\n /** When a jump last found this peer's tmux server down. Null once it answers. */\n tmux_down_at: number | null;\n};\n","import { type AgentState, DEFAULT_DRIVER, type Driver, type Event } from \"./types.js\";\n\nexport type LiveCheck = (pid: number) => boolean;\n\nexport type AgentView = {\n agent_id: string;\n host_id: string;\n state: AgentState | null;\n event: Event | null;\n workstream: string | null;\n role: string | null;\n cli: string | null;\n driver: Driver;\n session: string;\n window: string;\n pane: string;\n // Names as recorded by the authoring node, so a remote agent is labelled by\n // its own host's tmux rather than by whatever this host has at that id.\n session_name: string | null;\n window_name: string | null;\n agent_name: string | null;\n pi_session: string | null;\n fetched_at: number | null;\n};\n\nexport function foldAgent(\n events: Event[],\n isAlive: LiveCheck,\n): { state: AgentState | null; event: Event | null } {\n for (let index = events.length - 1; index >= 0; index -= 1) {\n const event = events[index];\n if (!event) continue;\n\n switch (event.state) {\n case \"blocked\":\n case \"done\":\n case \"crashed\":\n return { state: event.state, event };\n case \"cleared\":\n return { state: null, event: null };\n case \"working\":\n return {\n state: event.pid !== null && event.pid > 0 && isAlive(event.pid) ? \"working\" : \"crashed\",\n event,\n };\n }\n }\n\n return { state: null, event: null };\n}\n\nexport function foldAll(events: Event[], isAlive: LiveCheck): AgentView[] {\n const byAgent = new Map<string, Event[]>();\n for (const event of events) {\n const agentEvents = byAgent.get(event.agent_id);\n if (agentEvents) agentEvents.push(event);\n else byAgent.set(event.agent_id, [event]);\n }\n\n return [...byAgent.values()].map((agentEvents) => {\n const folded = foldAgent(agentEvents, isAlive);\n const source = folded.event ?? agentEvents[agentEvents.length - 1];\n if (!source) throw new Error(\"agent event group cannot be empty\");\n\n return {\n agent_id: source.agent_id,\n host_id: source.host_id,\n state: folded.state,\n event: folded.event,\n workstream: source.workstream,\n role: source.role,\n cli: source.cli,\n driver: source.driver ?? DEFAULT_DRIVER,\n session: source.session,\n window: source.window,\n pane: source.pane,\n session_name: source.session_name,\n window_name: source.window_name,\n agent_name: source.agent_name,\n pi_session: source.pi_session,\n fetched_at: null,\n };\n });\n}\n\nconst ATTENTION_ORDER: Record<AgentState, number> = {\n blocked: 0,\n done: 1,\n crashed: 2,\n working: 3,\n cleared: 4,\n};\n\nexport function attentionSort(views: AgentView[]): AgentView[] {\n return [...views].sort((left, right) => {\n const stateOrder =\n (left.state === null ? 4 : ATTENTION_ORDER[left.state]) -\n (right.state === null ? 4 : ATTENTION_ORDER[right.state]);\n if (stateOrder !== 0) return stateOrder;\n return (right.event?.ts ?? 0) - (left.event?.ts ?? 0);\n });\n}\n\nexport function isStale(fetchedAt: number | null, now: number, thresholdMs = 60_000): boolean {\n return fetchedAt !== null && now - fetchedAt > thresholdMs;\n}\n","import { foldAgent, type LiveCheck } from \"./fold.js\";\nimport { ensureIdentity } from \"./identity.js\";\nimport type { Store } from \"./store.js\";\nimport type { Driver, Event } from \"./types.js\";\n\nexport const SCHEMA_VERSION = 2;\n\nexport type Envelope = {\n schema_version: number;\n host_id: string;\n display_name: string;\n exported_at: number;\n};\n\nconst EVENT_FIELDS = new Set([\n \"host_id\",\n \"seq\",\n \"ts\",\n \"agent_id\",\n \"session\",\n \"window\",\n \"pane\",\n \"session_name\",\n \"window_name\",\n \"agent_name\",\n \"pi_session\",\n \"workstream\",\n \"role\",\n \"cli\",\n \"driver\",\n \"kind\",\n \"state\",\n \"message\",\n \"pid\",\n \"synthetic\",\n \"reason\",\n]);\n\nfunction eventToWire(event: Event): Record<string, unknown> {\n const { extra, ...known } = event;\n return { ...extra, ...known };\n}\n\nexport function eventFromWire(wire: Record<string, unknown>): Event {\n const extra = Object.fromEntries(Object.entries(wire).filter(([key]) => !EVENT_FIELDS.has(key)));\n return {\n host_id: wire.host_id as string,\n seq: wire.seq as number,\n ts: wire.ts as number,\n agent_id: wire.agent_id as string,\n session: wire.session as string,\n window: wire.window as string,\n pane: wire.pane as string,\n session_name: (wire.session_name as string | null | undefined) ?? null,\n window_name: (wire.window_name as string | null | undefined) ?? null,\n agent_name: (wire.agent_name as string | null | undefined) ?? null,\n pi_session: (wire.pi_session as string | null | undefined) ?? null,\n workstream: (wire.workstream as string | null | undefined) ?? null,\n role: (wire.role as string | null | undefined) ?? null,\n cli: (wire.cli as string | null | undefined) ?? null,\n driver: (wire.driver as Driver | null | undefined) ?? null,\n kind: wire.kind as string,\n state: wire.state as string,\n message: wire.message as string,\n pid: (wire.pid as number | null | undefined) ?? null,\n synthetic: wire.synthetic as boolean,\n reason: wire.reason as string,\n extra,\n };\n}\n\nfunction synthesizeCrashes(store: Store, hostId: string, isAlive: LiveCheck): void {\n const byAgent = new Map<string, Event[]>();\n for (const event of store.allEvents()) {\n if (event.host_id !== hostId) continue;\n const events = byAgent.get(event.agent_id);\n if (events) events.push(event);\n else byAgent.set(event.agent_id, [event]);\n }\n\n for (const events of byAgent.values()) {\n events.sort((left, right) => left.seq - right.seq);\n const newest = events.at(-1);\n if (\n newest &&\n newest.state === \"working\" &&\n !newest.synthetic &&\n foldAgent(events, isAlive).state === \"crashed\"\n ) {\n const { host_id: _hostId, seq: _seq, ts: _ts, ...event } = newest;\n store.append({ ...event, state: \"crashed\", synthetic: true, reason: \"pid_gone\" });\n }\n }\n}\n\n/**\n * Clear agents whose tmux window is gone.\n *\n * A window that dies takes its agent with it, but the log's newest row still\n * says `blocked`, so every peer keeps showing an agent that cannot be jumped\n * to -- the fold has nothing to supersede that row with. Only the authoring\n * node can tell, which is why this runs on export beside crash synthesis\n * rather than on the reader.\n *\n * `cleared` is the right state: it already means \"no longer wants attention\"\n * and resets the fold to none. An appended event rather than an export-time\n * filter, so the fact replicates once and explains itself, instead of every\n * peer having to re-derive it from an absence.\n */\nexport function clearDeadWindows(store: Store, hostId: string, live: Set<string> | null): void {\n // null means tmux could not answer. An empty set means it did and there are\n // no windows. Conflating them would clear every agent on the host whenever\n // tmux was briefly unreachable.\n if (live === null) return;\n\n const newest = new Map<string, Event>();\n for (const event of store.allEvents()) {\n if (event.host_id !== hostId) continue;\n const previous = newest.get(event.agent_id);\n if (!previous || event.seq > previous.seq) newest.set(event.agent_id, event);\n }\n\n for (const event of newest.values()) {\n if (event.state === \"cleared\") continue;\n if (live.has(event.window)) continue;\n const { host_id: _hostId, seq: _seq, ts: _ts, ...rest } = event;\n store.append({\n ...rest,\n state: \"cleared\",\n synthetic: true,\n reason: \"window_gone\",\n message: \"\",\n });\n }\n}\n\nexport function exportJsonl(\n store: Store,\n since: number,\n isAlive: LiveCheck,\n live?: Set<string> | null,\n): string {\n const identity = ensureIdentity();\n synthesizeCrashes(store, identity.host_id, isAlive);\n if (live !== undefined) clearDeadWindows(store, identity.host_id, live);\n\n const envelope: Envelope = {\n schema_version: SCHEMA_VERSION,\n host_id: identity.host_id,\n display_name: identity.display_name,\n exported_at: Date.now(),\n };\n const lines = [\n JSON.stringify(envelope),\n ...store\n .eventsSince(identity.host_id, since)\n .map((event) => JSON.stringify(eventToWire(event))),\n ];\n return `${lines.join(\"\\n\")}\\n`;\n}\n","import type { Channel } from \"./channel.js\";\nimport { type Envelope, eventFromWire, SCHEMA_VERSION } from \"./export.js\";\nimport type { Store } from \"./store.js\";\nimport type { Event } from \"./types.js\";\n\n/**\n * How long a peer may go unfetched before it renders stale.\n *\n * Not derived from a collect interval, because murmur has no scheduler: there\n * is no timer here, and `collect` runs only when a command asks for it. In\n * practice the cadence is the operator's tmux `status-interval`, since\n * `murmur status` collects and tmux re-runs it on a tick.\n *\n * So this is a judgement about the operator's setup, not arithmetic on a\n * constant murmur controls. Sixty seconds is comfortably above a default 15s\n * status bar -- a peer needs to miss several ticks before it is called out,\n * which keeps one slow fetch from flickering the HUD. A status bar slower than\n * this will show every peer permanently stale; that is the number to change if\n * so.\n */\nexport const STALENESS_MS = 60_000;\n\n// A reachable peer is cheap — milliseconds on a warm control socket, still\n// only a couple hundred cold. The cap is not about those.\n//\n// It is about the unreachable ones. Each in-flight peer is a forked ssh client\n// process, and a peer that is asleep or off the VPN holds that process for the\n// full ConnectTimeout. Unbounded fan-out over a long list puts every one of\n// them resident at once, which is process churn and file descriptors spent on\n// hosts that were never going to answer.\n//\n// Eight keeps the realistic fleet fully parallel while bounding that.\nexport const MAX_CONCURRENT_PEERS = 8;\n\n// The cap alone does not bound the collect, which is what an earlier version of\n// this comment got wrong. The per-peer ssh timeout applies once per wave, so\n// nine unreachable peers cost two waves and seventeen cost three: the pool\n// serialises the timeouts it is there to limit. At a 5s status-interval that is\n// exactly the tick overlap the concurrency work set out to remove, just moved\n// to a longer peer list.\n//\n// So the whole collect gets its own deadline, independent of peer count. Peers\n// still in flight when it expires are abandoned and render stale, which is\n// already the designed outcome for a host that did not answer in time.\n//\n// Four seconds: under a 5s tick, and above one full wave (a 3s exec ceiling\n// plus overhead) so a single wave is never cut short by the deadline itself.\nexport const COLLECT_DEADLINE_MS = 4_000;\n\n/**\n * Runs `task` over `items` with at most `limit` in flight, preserving input\n * order in the output. Workers pull from a shared cursor rather than running\n * fixed batches, so one slow peer occupies a single slot instead of holding a\n * batch boundary.\n *\n * `deadline` bounds the whole run, not each task. Once it passes, workers stop\n * claiming new items and anything unstarted is left `undefined` for the caller\n * to treat as \"did not answer\". Tasks already in flight are not cancelled --\n * there is nothing to cancel a forked ssh with here -- but they no longer hold\n * the collect open, because the deadline races the pool rather than joining it.\n */\nasync function mapSettled<T, R>(\n items: readonly T[],\n limit: number,\n task: (item: T) => Promise<R>,\n deadline?: Promise<void>,\n): Promise<(PromiseSettledResult<R> | undefined)[]> {\n const results = new Array<PromiseSettledResult<R> | undefined>(items.length);\n let cursor = 0;\n let expired = false;\n const stop = deadline?.then(() => {\n expired = true;\n });\n const worker = async () => {\n while (cursor < items.length && !expired) {\n const index = cursor++;\n try {\n results[index] = { status: \"fulfilled\", value: await task(items[index] as T) };\n } catch (reason) {\n results[index] = { status: \"rejected\", reason };\n }\n }\n };\n const pool = Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n await (stop ? Promise.race([pool, stop]) : pool);\n return results;\n}\n\nexport type CollectResult = {\n peer: string;\n ok: boolean;\n ingested: number;\n error?: string;\n};\n\nfunction parseJsonl(output: string): { envelope: Envelope; events: Event[] } {\n const lines = output.trim().split(\"\\n\");\n const envelope = JSON.parse(lines.shift() ?? \"\") as Envelope;\n if (envelope.schema_version > SCHEMA_VERSION) {\n throw new Error(\n `unsupported schema version ${envelope.schema_version} (supports ${SCHEMA_VERSION})`,\n );\n }\n return {\n envelope,\n events: lines.map((line) => eventFromWire(JSON.parse(line) as Record<string, unknown>)),\n };\n}\n\n/**\n * Peers are fetched concurrently and applied serially.\n *\n * Concurrent because an unreachable peer costs the full ssh timeout, and a\n * serial loop charged that to every other peer behind it: three asleep laptops\n * made `murmur status` hang for thirty seconds and let the HUD tick overlap\n * itself. Fanning out makes the whole collect cost the slowest peer, not the\n * sum — capped at MAX_CONCURRENT_PEERS in flight and bounded overall by\n * COLLECT_DEADLINE_MS.\n *\n * Applied serially, in peer order, because better-sqlite3 is synchronous: there\n * is nothing to win by interleaving writes, and keeping the order stable keeps\n * the result list aligned with `store.peers()`.\n */\nexport async function collect(\n store: Store,\n channel: Channel,\n now = Date.now(),\n deadline?: Promise<void>,\n): Promise<CollectResult[]> {\n const results: CollectResult[] = [];\n let timer: NodeJS.Timeout | undefined;\n try {\n const peers = store.peers();\n // Default deadline, injectable so tests do not have to wait out real time.\n // Unref'd: a pending timer must not hold the process open after a CLI\n // command has printed its output and finished.\n const bounded =\n deadline ??\n new Promise<void>((resolve) => {\n timer = setTimeout(resolve, COLLECT_DEADLINE_MS);\n timer.unref?.();\n });\n // Settled, not raw: a peer that fails while we are still applying an\n // earlier one would otherwise be an unhandled rejection for as long as it\n // sits in the queue, which Node reports and future Node kills the process\n // over.\n const fetches = await mapSettled(\n peers,\n MAX_CONCURRENT_PEERS,\n async (peer) =>\n parseJsonl(\n await channel.exec(peer.target, [\"murmur\", \"export\", \"--since\", String(peer.watermark)]),\n ),\n bounded,\n );\n for (const [index, peer] of peers.entries()) {\n const fetch = fetches[index];\n try {\n // Undefined means the deadline passed before this peer was claimed or\n // finished. Not an error about the peer, so it says so plainly and\n // leaves fetched_at alone: the peer goes stale, which is the designed\n // outcome for a host that did not answer in time.\n if (!fetch) throw new Error(\"collect deadline passed before this peer answered\");\n if (fetch.status === \"rejected\") throw fetch.reason;\n const { envelope, events } = fetch.value;\n const ingested = store.ingest(events);\n const origin = events.filter((event) => event.host_id === envelope.host_id);\n const watermark = origin.reduce(\n (highest, event) => Math.max(highest, event.seq),\n peer.watermark,\n );\n store.upsertPeer({\n name: peer.name,\n target: peer.target,\n host_id: envelope.host_id,\n display_name: envelope.display_name,\n watermark,\n fetched_at: now,\n // New events mean the node is authoring again, so whatever a jump\n // observed about its tmux is out of date. Only clear on actual new\n // events: an export that returns nothing proves the binary ran, not\n // that tmux is back, which is the distinction that let a dead host\n // look healthy for three hours.\n //\n // Keyed on the watermark advancing, not on ingest's insert count.\n // Two reasons the count was wrong. Ingest is INSERT OR IGNORE, so a\n // retry after a partial apply re-sees the same events and reports\n // zero -- leaving a recovered host marked down until it happened to\n // author again. And the count includes rows from other origins that\n // this peer merely relayed, which say nothing about whether this\n // peer's tmux is back.\n tmux_down_at: watermark > peer.watermark ? null : peer.tmux_down_at,\n });\n results.push({ peer: peer.name, ok: true, ingested });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`murmur: collect: peer ${peer.name}: ${message}\\n`);\n results.push({ peer: peer.name, ok: false, ingested: 0, error: message });\n }\n }\n } catch (error) {\n process.stderr.write(\n `murmur: collect: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n } finally {\n clearTimeout(timer);\n }\n\n // Once per collect, outside the peer loop and outside its try, for two\n // reasons.\n //\n // It used to run per successful peer, so four peers meant four DELETEs with a\n // window function over the whole table on every status-bar tick, to enforce a\n // horizon measured in days. Idempotent work, repeated.\n //\n // And it ran only when a peer succeeded, so the single-machine case -- no\n // peers at all, the everyday path -- pruned never and grew local events\n // forever. The horizon is a property of the log, not of federation.\n //\n // Retention must not be able to fail a command, hence its own try.\n try {\n store.prune();\n } catch (error) {\n process.stderr.write(\n `murmur: collect: prune: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n }\n return results;\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { Agent } from \"./agents.js\";\nimport { SSH_OPTIONS } from \"./channel.js\";\nimport { loadIdentity } from \"./identity.js\";\nimport { tmux } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\n\n/**\n * Glance: the last few lines a pane printed.\n *\n * This is the cheap half of the two things \"render any pane from the master\"\n * hides. It is a stateless `capture-pane`, not a frame stream — no resize\n * negotiation, no input routing, no reconnect. That deferral is what keeps\n * murmur a state layer instead of a multiplexer (DESIGN-NOTES, \"Deferring\n * interactive remote rendering\"), and it is why this file is thirty lines\n * rather than most of herdr.\n */\n\nconst GLANCE_LINES = 40;\n\nexport function glance(store: Store, agent: Agent, lines = GLANCE_LINES): string | null {\n if (agent.host_id === loadIdentity()?.host_id) return tmux.capture(agent.pane, lines);\n\n const peer = store.peers().find((candidate) => candidate.host_id === agent.host_id);\n const target = peer?.target ?? peer?.name;\n if (!target) return null;\n try {\n // The pane id is `%N`, which a remote shell leaves alone, but quote it\n // anyway: the same class of bug as the `$N` session id that made remote\n // jump fail silently for a day.\n return execFileSync(\n \"ssh\",\n [\n ...SSH_OPTIONS,\n target,\n \"tmux\",\n \"capture-pane\",\n \"-p\",\n \"-t\",\n `'${agent.pane}'`,\n \"-S\",\n `-${lines}`,\n ],\n { encoding: \"utf8\", timeout: 3000, stdio: [\"ignore\", \"pipe\", \"ignore\"] },\n );\n } catch {\n // Unreachable, cold socket, dead tmux, gone pane. The preview says so\n // rather than the picker failing.\n return null;\n }\n}\n","import { type Channel, ssh } from \"./channel.js\";\nimport { collect, STALENESS_MS } from \"./collector.js\";\nimport { type AgentView, attentionSort, foldAll, isStale } from \"./fold.js\";\nimport { loadIdentity } from \"./identity.js\";\nimport { pidAlive } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\n\ntype StatusState = \"working\" | \"blocked\" | \"done\" | \"crashed\" | \"idle\";\ntype Counts = Record<StatusState, number>;\n\nexport type Status = {\n counts: Counts;\n orchestrated_counts: Counts;\n agents: (AgentView & {\n stale: boolean;\n age_ms: number | null;\n event_age_ms: number | null;\n tmux_down: boolean;\n host: string;\n })[];\n peers: {\n name: string;\n display_name: string | null;\n fetched_at: number | null;\n stale: boolean;\n }[];\n};\n\nfunction emptyCounts(): Counts {\n return { working: 0, blocked: 0, done: 0, crashed: 0, idle: 0 };\n}\n\nexport function tmuxStatus(view: Status): string {\n const urgency: StatusState[] = [\"crashed\", \"blocked\", \"done\", \"working\", \"idle\"];\n return urgency\n .filter((state) => view.counts[state] > 0)\n .map((state) => `${state}\\t${view.counts[state]}\\n`)\n .join(\"\");\n}\n\n/**\n * Fold the current view. Pure with respect to the network: the caller decides\n * whether to collect first (see `statusWithCollect`).\n */\nexport function status(store: Store, now = Date.now()): Status {\n const identity = loadIdentity();\n const peers = store.peers();\n const peersByHost = new Map(\n peers.flatMap((peer) => (peer.host_id === null ? [] : [[peer.host_id, peer] as const])),\n );\n const events = store.allEvents();\n const local = foldAll(\n events.filter((event) => event.host_id === identity?.host_id),\n pidAlive,\n );\n const remote = foldAll(\n events.filter((event) => event.host_id !== identity?.host_id),\n () => true,\n );\n const counts = emptyCounts();\n const orchestratedCounts = emptyCounts();\n const agents = attentionSort([...local, ...remote]).map((agent) => {\n const peer = peersByHost.get(agent.host_id);\n const fetchedAt = peer?.fetched_at ?? null;\n const state: StatusState =\n agent.state === null || agent.state === \"cleared\" ? \"idle\" : agent.state;\n const target = agent.driver === \"human\" ? counts : orchestratedCounts;\n target[state] += 1;\n return {\n ...agent,\n fetched_at: fetchedAt,\n // Replica freshness: how long since we last reached the peer. Local rows\n // have no fetched_at and are never stale.\n stale: isStale(fetchedAt, now, STALENESS_MS),\n age_ms: fetchedAt === null ? null : now - fetchedAt,\n // Information age: how long since the agent itself said anything. This\n // is the number a human means by \"how stale is that row\". A successful\n // fetch of a three-hour-old event resets age_ms to zero but leaves this\n // at three hours, which is why they cannot be the same field.\n event_age_ms: agent.event === null ? null : Math.max(0, now - agent.event.ts),\n // A jump proved this host's tmux was down and nothing has authored since.\n // Stronger than staleness: the host answers, its agents are just gone.\n tmux_down: peer?.tmux_down_at != null,\n // The name the human typed, not the machine's self-reported hostname. A\n // peer added as `linuxpc` reported `18c04d69b860` (a container hostname)\n // and that is what the picker showed — a string that appears nowhere\n // else in the tool and cannot be typed at `peer remove` or searched for.\n // Only the local node, which has no peer row, falls back to its own\n // discovered display_name.\n host:\n peer?.name ?? (agent.host_id === identity?.host_id ? identity.display_name : agent.host_id),\n };\n });\n\n return {\n counts,\n orchestrated_counts: orchestratedCounts,\n agents,\n peers: peers.map((peer) => ({\n name: peer.name,\n display_name: peer.display_name,\n fetched_at: peer.fetched_at,\n // A peer we have never reached is stale, not fresh. `isStale` reads a\n // null `fetched_at` as \"local, therefore never stale\", which is right\n // for an agent row but backwards for a peer: null there means the very\n // first collect has not succeeded yet. Left to `isStale`, an\n // unreachable host you just added would render as up to date.\n stale: peer.fetched_at === null || isStale(peer.fetched_at, now, STALENESS_MS),\n })),\n };\n}\n\n/**\n * Collect from peers, then fold. This is what every user-facing surface wants:\n * the view reflects the sync that just ran, rather than the one before it.\n *\n * Awaiting matters for two reasons. A fire-and-forget collect makes every\n * invocation show data one run stale — you never see what you just fetched.\n * And the callers close the store in a `finally`, so a collect still in flight\n * lands on a closed handle and reports \"The database connection is not open\",\n * which looks like corruption rather than a race.\n *\n * Sync must never fail a command, so a peer failure only warns. With no peers\n * this is a loop over an empty array: no network, no added latency, which is\n * the everyday single-machine path.\n */\nexport async function statusWithCollect(\n store: Store,\n now = Date.now(),\n channel: Channel = ssh,\n): Promise<Status> {\n try {\n await collect(store, channel, now);\n } catch (error) {\n process.stderr.write(\n `murmur: status: collect: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n }\n return status(store, now);\n}\n","import { rmSync } from \"node:fs\";\nimport Database from \"better-sqlite3\";\nimport { ensureIdentity } from \"./identity.js\";\nimport { dbPath } from \"./paths.js\";\nimport type { Driver, Event, Peer } from \"./types.js\";\n\nconst DEFAULT_RETENTION_MS = 7 * 86_400_000;\n\n/**\n * Local storage shape. Bump on any change to the events or peers tables.\n *\n * Distinct from `SCHEMA_VERSION` in export.ts, which versions the *wire*: a\n * node can change how it stores events without changing what it sends, and a\n * wire change should not throw away local history.\n */\nexport const STORE_VERSION = 2;\n\n/**\n * Migration strategy: there isn't one. A version mismatch deletes the database\n * and starts again.\n *\n * This is only acceptable because nothing in events.db is authoritative or\n * irreplaceable. It is a bounded-retention observability log: remote events\n * re-sync from their authoring peer on the next collect, local agents re-report\n * on their next state change, and node identity deliberately lives in a\n * separate file. If anything durable is ever added here, this stops being safe\n * and a real migration is required.\n *\n * Peers survive, because they are the one thing a human typed. Watermarks are\n * reset with the events they indexed -- keeping them would skip the events the\n * new database no longer has -- and re-reading a peer from zero is free, since\n * ingest is idempotent.\n */\nfunction resetIfStale(path: string): Peer[] {\n let salvaged: Peer[] = [];\n try {\n const existing = new Database(path, { fileMustExist: true });\n const version = (existing.pragma(\"user_version\", { simple: true }) as number) ?? 0;\n if (version === STORE_VERSION) {\n existing.close();\n return salvaged;\n }\n try {\n salvaged = existing\n .prepare(\"SELECT name, target, host_id, display_name FROM peers\")\n .all() as Peer[];\n } catch {\n // Old enough not to have the table, or unreadable. Nothing to save.\n }\n existing.close();\n } catch {\n // No database yet, or one too broken to open. Either way, recreate.\n return salvaged;\n }\n\n // -wal and -shm must go too: a stale sidecar against a fresh main file is a\n // documented way to corrupt sqlite.\n for (const suffix of [\"\", \"-wal\", \"-shm\"]) rmSync(`${path}${suffix}`, { force: true });\n return salvaged;\n}\n\n// The name fields are optional on the way in: a caller that has no name for a\n// thing should not have to say `null` four times, and a non-tmux harness has\n// none of them. They are non-optional on `Event` itself, so a reader never has\n// to distinguish absent from null.\nexport type NewEvent = Omit<\n Event,\n \"host_id\" | \"seq\" | \"ts\" | \"session_name\" | \"window_name\" | \"agent_name\" | \"pi_session\"\n> & {\n ts?: number;\n session_name?: string | null;\n window_name?: string | null;\n agent_name?: string | null;\n pi_session?: string | null;\n};\n\ntype EventRow = Omit<Event, \"synthetic\" | \"extra\"> & {\n synthetic: number;\n extra: string;\n};\n\nfunction eventValues(event: Event): unknown[] {\n return [\n event.host_id,\n event.seq,\n event.ts,\n event.agent_id,\n event.session,\n event.window,\n event.pane,\n event.session_name,\n event.window_name,\n event.agent_name,\n event.pi_session,\n event.workstream,\n event.role,\n event.cli,\n event.driver,\n event.kind,\n event.state,\n event.message,\n event.pid,\n Number(event.synthetic),\n event.reason,\n JSON.stringify(event.extra),\n ];\n}\n\nfunction toEvent(row: EventRow): Event {\n return {\n ...row,\n driver: row.driver as Driver | null,\n synthetic: row.synthetic === 1,\n extra: JSON.parse(row.extra) as Record<string, unknown>,\n };\n}\n\nexport interface Store {\n append(event: NewEvent): Event;\n ingest(events: Event[]): number;\n eventsSince(hostId: string, seq: number): Event[];\n allEvents(): Event[];\n /**\n * The most recent event for one agent, or null.\n *\n * Exists so the `clear` hook does not have to open its own SQLite handle and\n * write its own `ORDER BY seq DESC LIMIT 1`, which is what it used to do --\n * making \"store is the only module touching SQL\" false, and putting knowledge\n * of agent_id construction and event ordering in a CLI file where a schema\n * change would miss it. That path swallows its own errors, so the miss would\n * have been silent.\n */\n latestForAgent(hostId: string, agentId: string): Event | null;\n maxSeq(hostId: string): number;\n prune(horizonMs?: number): number;\n peers(): Peer[];\n /**\n * Drop every event for one agent from this node's replica.\n *\n * For a remote agent this is a replica eviction, not a claim about truth: the\n * authoring node still owns it, and a collect re-reads from the watermark if\n * it is still alive.\n */\n forgetAgent(agentId: string): number;\n forgetHost(hostId: string): number;\n upsertPeer(peer: Partial<Peer> & { name: string; target: string }): void;\n removePeer(name: string): boolean;\n close(): void;\n}\n\nexport function openStore(): Store {\n const identity = ensureIdentity();\n const path = dbPath();\n const salvagedPeers = resetIfStale(path);\n const database = new Database(path);\n database.pragma(\"journal_mode = WAL\");\n database.pragma(`user_version = ${STORE_VERSION}`);\n database.exec(`\n CREATE TABLE IF NOT EXISTS events (\n host_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n ts INTEGER NOT NULL,\n agent_id TEXT NOT NULL,\n session TEXT NOT NULL,\n window TEXT NOT NULL,\n pane TEXT NOT NULL,\n session_name TEXT,\n window_name TEXT,\n agent_name TEXT,\n pi_session TEXT,\n workstream TEXT,\n role TEXT,\n cli TEXT,\n driver TEXT,\n kind TEXT NOT NULL,\n state TEXT NOT NULL,\n message TEXT NOT NULL,\n pid INTEGER,\n synthetic INTEGER NOT NULL,\n reason TEXT NOT NULL,\n extra TEXT NOT NULL,\n PRIMARY KEY (host_id, seq)\n );\n CREATE INDEX IF NOT EXISTS events_agent_seq ON events (agent_id, seq);\n CREATE TABLE IF NOT EXISTS peers (\n name TEXT PRIMARY KEY,\n target TEXT NOT NULL,\n host_id TEXT,\n display_name TEXT,\n watermark INTEGER NOT NULL,\n fetched_at INTEGER,\n -- When a jump last proved this peer's tmux was not answering. Reader\n -- state, not an event: this node cannot author facts about another\n -- node's agents, and a jump is a local observation, not something the\n -- peer said. Cleared by the next successful collect.\n tmux_down_at INTEGER\n );\n `);\n\n // Additive migration: an existing peers table predates tmux_down_at.\n try {\n database.exec(\"ALTER TABLE peers ADD COLUMN tmux_down_at INTEGER\");\n } catch {\n // Already present.\n }\n\n // Put back the peers the wipe took, at watermark 0 so the next collect\n // re-reads each one from the start.\n if (salvagedPeers.length > 0) {\n const restore = database.prepare(\n `INSERT OR IGNORE INTO peers (name, target, host_id, display_name, watermark, fetched_at)\n VALUES (?, ?, ?, ?, 0, NULL)`,\n );\n for (const peer of salvagedPeers) {\n restore.run(peer.name, peer.target, peer.host_id ?? null, peer.display_name ?? null);\n }\n }\n\n const eventColumns = `\n host_id, seq, ts, agent_id, session, window, pane,\n session_name, window_name, agent_name, pi_session,\n workstream, role, cli, driver, kind, state, message, pid,\n synthetic, reason, extra`;\n const eventPlaceholders = new Array(22).fill(\"?\").join(\", \");\n const insertEvent = database.prepare(\n `INSERT INTO events (${eventColumns}) VALUES (${eventPlaceholders})`,\n );\n const ingestEvent = database.prepare(\n `INSERT OR IGNORE INTO events (${eventColumns}) VALUES (${eventPlaceholders})`,\n );\n const selectMaxSeq = database.prepare(\n \"SELECT COALESCE(MAX(seq), 0) AS seq FROM events WHERE host_id = ?\",\n );\n const append = database.transaction((event: NewEvent): Event => {\n const row = selectMaxSeq.get(identity.host_id) as { seq: number };\n const stored: Event = {\n ...event,\n host_id: identity.host_id,\n seq: row.seq + 1,\n ts: event.ts ?? Date.now(),\n session_name: event.session_name ?? null,\n window_name: event.window_name ?? null,\n agent_name: event.agent_name ?? null,\n pi_session: event.pi_session ?? null,\n };\n insertEvent.run(...eventValues(stored));\n return stored;\n });\n const ingest = database.transaction((events: Event[]): number => {\n let inserted = 0;\n for (const event of events) inserted += ingestEvent.run(...eventValues(event)).changes;\n return inserted;\n });\n\n return {\n append,\n ingest,\n eventsSince(hostId, seq) {\n const rows = database\n .prepare(\"SELECT * FROM events WHERE host_id = ? AND seq > ? ORDER BY seq\")\n .all(hostId, seq) as EventRow[];\n return rows.map(toEvent);\n },\n allEvents() {\n const rows = database\n .prepare(\"SELECT * FROM events ORDER BY ts, host_id, seq\")\n .all() as EventRow[];\n return rows.map(toEvent);\n },\n latestForAgent(hostId, agentId) {\n const row = database\n .prepare(\n `SELECT * FROM events\n WHERE host_id = ? AND agent_id = ?\n ORDER BY seq DESC LIMIT 1`,\n )\n .get(hostId, agentId) as EventRow | undefined;\n return row ? toEvent(row) : null;\n },\n maxSeq(hostId) {\n return (selectMaxSeq.get(hostId) as { seq: number }).seq;\n },\n prune(horizonMs = Number(process.env.MURMUR_RETENTION_MS ?? DEFAULT_RETENTION_MS)) {\n return database\n .prepare(`\n DELETE FROM events\n WHERE ts < ?\n AND (host_id, seq) NOT IN (\n SELECT host_id, seq FROM (\n SELECT host_id, seq,\n ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY ts DESC, seq DESC) rn\n FROM events\n ) WHERE rn = 1\n )\n `)\n .run(Date.now() - horizonMs).changes;\n },\n peers() {\n return database.prepare(\"SELECT * FROM peers ORDER BY name\").all() as Peer[];\n },\n forgetAgent(agentId) {\n return database.prepare(\"DELETE FROM events WHERE agent_id = ?\").run(agentId).changes;\n },\n forgetHost(hostId) {\n // Every replicated row for one origin node. Only ever called about a\n // REMOTE host: the local host's rows are this node's own authorship and\n // the retention horizon owns them.\n return database.prepare(\"DELETE FROM events WHERE host_id = ?\").run(hostId).changes;\n },\n upsertPeer(peer) {\n const current = database.prepare(\"SELECT * FROM peers WHERE name = ?\").get(peer.name) as\n | Peer\n | undefined;\n database\n .prepare(`\n INSERT INTO peers (name, target, host_id, display_name, watermark, fetched_at, tmux_down_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n target = excluded.target,\n host_id = excluded.host_id,\n display_name = excluded.display_name,\n watermark = excluded.watermark,\n fetched_at = excluded.fetched_at,\n tmux_down_at = excluded.tmux_down_at\n `)\n .run(\n peer.name,\n peer.target,\n peer.host_id !== undefined ? peer.host_id : (current?.host_id ?? null),\n peer.display_name !== undefined ? peer.display_name : (current?.display_name ?? null),\n peer.watermark !== undefined ? peer.watermark : (current?.watermark ?? 0),\n peer.fetched_at !== undefined ? peer.fetched_at : (current?.fetched_at ?? null),\n peer.tmux_down_at !== undefined ? peer.tmux_down_at : (current?.tmux_down_at ?? null),\n );\n },\n removePeer(name) {\n // Drops the peer and its watermark. Replicated events stay: they are\n // real history authored elsewhere, and the retention horizon already\n // ages them out. Re-adding the peer re-syncs from zero, which ingest\n // makes free.\n return database.prepare(\"DELETE FROM peers WHERE name = ?\").run(name).changes > 0;\n },\n close() {\n database.close();\n },\n };\n}\n"],"mappings":"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","names":["join","join","execFileSync","execFileSync","execFileSync"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martintrojer/murmur",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Agent state across every machine you work on, in one view.",
5
5
  "publishConfig": { "access": "public" },
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "./extension-store": "./dist/extension/store.js"
18
18
  },
19
19
  "bin": { "murmur": "./dist/cli.js" },
20
- "files": ["dist", "README.md", "ARCHITECTURE.md"],
20
+ "files": ["dist", "README.md", "ARCHITECTURE.md", "CHANGELOG.md"],
21
21
  "scripts": {
22
22
  "build": "tsup",
23
23
  "test": "vitest run",