@martintrojer/murmur 0.2.1 → 0.2.2

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/agents.ts","../src/channel.ts","../src/ids.ts","../src/mux.ts","../src/snapshot.ts","../src/types.ts","../src/view.ts","../src/collector.ts","../src/glance.ts","../src/identity.ts","../src/paths.ts","../src/status.ts","../src/store.ts","../src/version.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { SSH_OPTIONS } from \"./channel.js\";\nimport { asPaneId } from \"./ids.js\";\nimport { type Mux, tmux } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\nimport type { PaneView } from \"./view.js\";\n\n/**\n * The most specific human-readable name a pane's agent has, never a tmux id.\n *\n * Four sources, most to least specific: mu's agent name, pi's session name, the\n * tmux window name, the tmux session name. All are recorded by the node that\n * owns the pane, so this reads the same for a local and a remote pane -- a\n * reader cannot resolve a remote window id against its own tmux.\n *\n * Falls back to the window id only when a node recorded no names at all, which\n * means a non-tmux harness.\n */\nexport function agentLabel(agent: PaneView): 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 pane lives, for the second column. Names only -- the ids are what\n * jumps, not what a human reads.\n */\nexport function agentLocation(agent: PaneView): 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 local session name that wraps a remote attach.\n *\n * The trailing `~` marks it as murmur's, both for a human reading a session\n * list and for the `#{m:*~,...}` match in the suggested escape-hatch binding.\n *\n * The leading character is the part that matters. A tmux `-t` target starting\n * with `@`, `$` or `%` is parsed as a window, session or pane id, so a session\n * named `@bubba` -- which is exactly what the old per-host WINDOW was called --\n * cannot be addressed at all: every `-t @bubba` fails with `can't find window`.\n * Window names were never targets, so the old name was safe; session names are.\n */\nexport function remoteSessionName(peerName: string): string {\n return `${peerName.replace(/^[@$%=]+/, \"\")}~`;\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\nconst 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 // Local to this process: the picker prints `message` and nothing else, and\n // no reason code here is ever stored or published in a snapshot.\n reason: \"no_peer\" | \"unreachable\" | \"no_tmux\" | \"pane_gone\" | \"attach_failed\";\n message: string;\n };\n\n/**\n * Jump to a pane, wherever it lives.\n *\n * NEVER MUTATES STATE ON FAILURE. A failure is a report: a reason and a message,\n * nothing written. The next collect reconciles either way, and only the owning\n * node can author facts about its own panes.\n */\nexport function jumpToAgent(\n store: Store,\n agent: PaneView,\n mux: Mux = tmux,\n run: Runner = spawnRunner,\n): JumpResult {\n if (agent.local) {\n // The PANE decides, and only the pane. This once asked whether the agent's\n // WINDOW still existed, which a live pane routinely outlives: after\n // `move-pane -s %0 -t @1`, list-panes still has %0 and list-windows no\n // longer has @0. Asking the wrong one reported healthy agents as gone.\n const panes = mux.livePanes();\n if (panes && !panes.has(agent.pane)) {\n return {\n ok: false,\n reason: \"pane_gone\",\n message: `${agentLabel(agent)} is gone -- its pane no longer exists.`,\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 pane is still there before opening a window to attach to it.\n // Panes, not windows: a recorded window id goes stale every time the pane\n // moves, so it cannot answer whether the agent exists. 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-panes -a -F ${shellQuote(\"#{pane_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. A real fact about the host, and reported as\n // one: nothing is deleted here. The peer's own next snapshot is what\n // removes its panes, because only that node may author about them, and a\n // reader that evicts rows on a probe failure is guessing.\n return {\n ok: false,\n reason: \"no_tmux\",\n message: `${target} has no tmux server running, so its agents are gone. They will disappear on the next collect.`,\n };\n }\n const remotePanes = new Set(probe.stdout.split(\"\\n\").filter(Boolean).map(asPaneId));\n if (!remotePanes.has(agent.pane)) {\n return {\n ok: false,\n reason: \"pane_gone\",\n message: `${agentLabel(agent)} is gone -- ${target} no longer has that pane.`,\n };\n }\n\n const attachTarget = shellQuote(`${agent.session}:${agent.window}`);\n\n // Hand the ssh to tmux as its own detached SESSION rather than running it\n // here. `murmur pick` is usually a display-popup, and a popup is modal: an\n // ssh started inside it is killed the moment the picker exits, so the remote\n // pane flashed and vanished. A session outlives the popup and gives the\n // remote tmux a real terminal to attach to.\n //\n // A session, not a window, because session options are per-session and that\n // is what makes the nesting stop being felt:\n //\n // status off -- no local status bar, so the remote's own bar is the only\n // one on screen and the jump reads as a full-screen ssh.\n // prefix None -- no local prefix at all, so ^b reaches the remote\n // directly. No ^b b, and no second prefix to learn.\n //\n // Both would be global if this were a window, and would break every local\n // session. The cost is that the local server is unreachable from inside the\n // wrapper; the README documents a root-table key that detaches out.\n if (process.env.TMUX) {\n // Read BEFORE the wrapper exists, or we would record the wrapper itself as\n // the place to come back to and the return would be a no-op.\n const client = mux.clientName();\n const origin = mux.currentTarget();\n\n // Named after the peer as configured, matching the picker's host column.\n // The machine's self-reported display_name can be a container id, which\n // makes the session unrecognisable in a session list.\n const name = remoteSessionName(peer?.name ?? target);\n\n // Reuse an existing wrapper for this host rather than stacking a new one on\n // every jump. Jumping to bubba three times used to leave three identical\n // windows behind. Matched on name, the only handle available: the ssh is\n // opaque from here and the remote session id is not a local address.\n if (mux.sessionNamed(name)) {\n return mux.switchClient(client, name)\n ? { ok: true }\n : {\n ok: false,\n reason: \"attach_failed\",\n message: `could not switch to the existing ${name} session.`,\n };\n }\n\n // `tmux new-session <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 attach = `ssh -t ${shellQuote(target)} tmux attach -t ${shellQuote(attachTarget)}`;\n\n // The return home, as part of the wrapper's own command. When the attach\n // exits -- inner detach, remote session killed, ssh dropped -- this runs,\n // then the wrapper has no command left and tmux destroys it.\n //\n // Explicit, rather than relying on detach-on-destroy: `previous` picks\n // tmux's idea of the previous session, which in testing was a stray\n // unrelated session rather than the one the jump started from. It is still\n // set below as a fallback for when this command cannot run (SIGKILL).\n const restore = origin\n ? `; tmux switch-client ${client ? `-c ${shellQuote(client)} ` : \"\"}-t ${shellQuote(`=${origin}`)}`\n : \"\";\n\n if (!mux.newSession(name, `${attach}${restore}`)) {\n return {\n ok: false,\n reason: \"attach_failed\",\n message: `could not open a session to attach to ${target}.`,\n };\n }\n\n mux.setSessionOption(name, \"status\", \"off\");\n mux.setSessionOption(name, \"prefix\", \"None\");\n mux.setSessionOption(name, \"detach-on-destroy\", \"previous\");\n\n return mux.switchClient(client, name)\n ? { ok: true }\n : {\n ok: false,\n reason: \"attach_failed\",\n message: `attached to ${target} in session ${name}, but could not switch to it.`,\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 //\n // None of the wrapper-session machinery above applies here, and it must not:\n // there is no local client to switch, nothing to return to but the shell that\n // invoked us, and no local status bar or prefix to suppress. This path is\n // already full-screen and already prefix-clean -- the whole problem is an\n // artifact of being inside tmux. Creating a local session here would attach a\n // client to a server the user never asked for, and leave them inside tmux on\n // exit rather than back at their prompt.\n //\n // The tradeoff is no reuse of an existing attach, since there is no local\n // server holding one. That is correct rather than missing.\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 the\n// peer's whole current state, bounded by live pane count -- a few hundred bytes\n// per pane, on a machine that cannot hold thousands of panes -- so the ceiling\n// is out of reach in practice.\n//\n// It is kept generous anyway, because the failure mode is bad out of proportion\n// to its likelihood: a peer whose document exceeds the buffer fails identically\n// on every collect, so it sits stale forever with an error that names a Node\n// internal rather than a size.\n//\n// 64 MiB is orders of magnitude above any real snapshot, 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","/**\n * tmux's three id kinds, kept apart by the type system.\n *\n * tmux itself is unambiguous about this and prints a sigil on every id --\n * `session=$25 window=@75 pane=%89` -- but they are all strings, so murmur\n * could and did pass one where another was meant. Twice, in shipped code: a\n * sweep keyed on window liveness deleted ten live agents, and a window cached\n * at extension startup badged the window a moved pane had left.\n *\n * An agent is addressed by its PANE, which keeps its id across `move-pane`,\n * `break-pane`, and a window closed and reopened. A session and a window are\n * only where that pane currently lives, and both may differ between two reports\n * from one agent. So the rule the brands enforce is:\n *\n * only a pane may decide whether an agent exists.\n *\n * Branding is a compile-time fiction: at runtime these are the same strings\n * tmux printed, which is what keeps the snapshot document and every stored row\n * byte-identical.\n */\n\ndeclare const brand: unique symbol;\n\n/** A tmux session id, `$N`. Mutable location. */\nexport type SessionId = string & { readonly [brand]: \"session\" };\n\n/** A tmux window id, `@N`. Mutable location -- never an agent's identity. */\nexport type WindowId = string & { readonly [brand]: \"window\" };\n\n/** A tmux pane id, `%N`. The agent's identity, stable for its whole life. */\nexport type PaneId = string & { readonly [brand]: \"pane\" };\n\n/*\n * The boundary. Every raw string that becomes an id passes through one of these\n * three, so the unsafe step is in one file and countable rather than scattered\n * as `as` at each call site.\n *\n * Deliberately not validating the sigil. These are called on tmux stdout, on\n * JSON off the wire, on sqlite rows and on argv, and a node that recorded an id\n * murmur does not recognise -- a future tmux, a different harness -- must still\n * round-trip it. Rejecting here would turn a naming change into a behaviour\n * change.\n */\n\nexport function asSessionId(raw: string): SessionId {\n return raw as SessionId;\n}\n\nexport function asWindowId(raw: string): WindowId {\n return raw as WindowId;\n}\n\nexport function asPaneId(raw: string): PaneId {\n return raw as PaneId;\n}\n","import { execFileSync } from \"node:child_process\";\nimport {\n asPaneId,\n asSessionId,\n asWindowId,\n type PaneId,\n type SessionId,\n type WindowId,\n} from \"./ids.js\";\nimport type { Location } from \"./types.js\";\nimport type { RenderState } from \"./view.js\";\n\nexport interface Mux {\n currentWindow(): Location | null;\n livePanes(): Set<PaneId> | null;\n // Sets `@agent_state` on a WINDOW, even though the attention it expresses\n // belongs to a pane. The asymmetry is tmux's: the status bar and the `tms`\n // picker read a window option, and there is no per-pane equivalent they\n // would read instead. Its consequence is that a pane moving between windows\n // must clear the badge it left behind, since nothing else knows it moved.\n setWindowBadge(window: WindowId, state: RenderState | 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: SessionId, window: WindowId): boolean;\n windowForPane(pane: PaneId): WindowId | null;\n panesInWindow(window: WindowId): PaneId[];\n capture(pane: PaneId, lines?: number): string | null;\n // --- remote-jump session seam -------------------------------------------\n // A remote attach lives in its own local session rather than a window, so it\n // can be full-screen (no local status bar) and prefix-free (no nested ^b).\n // See jumpToAgent for why that is worth five extra methods.\n clientName(): string | null;\n currentTarget(): string | null;\n sessionNamed(name: string): boolean;\n newSession(name: string, command: string): boolean;\n setSessionOption(session: string, option: string, value: string): void;\n switchClient(client: string | null, session: string): boolean;\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\n/**\n * A session name as an exact target, in the two spellings tmux needs.\n *\n * Bare names match by PREFIX, so a wrapper for host `bub` silently retargets a\n * session called `bubba` once one exists -- verified, and it sets options on\n * the wrong session rather than failing. A leading `=` demands an exact match.\n * (`name=` is not the syntax; it reads as part of the name and matches nothing.)\n *\n * The trailing colon is the part that is easy to get wrong. `switch-client -t`\n * takes a target-SESSION, where `=name` is right, but `set-option -t` and\n * `show-options -t` take a target-PANE, where `=name` fails outright with `no\n * such session` and the exact form is `=name:` -- the empty window/pane part\n * resolving to the session's current pane.\n *\n * Neither rescues a name starting with `@`, `$` or `%`: those introduce tmux's\n * window, session and pane id syntax. remoteSessionName keeps them out.\n *\n * Both take a session NAME -- not a SessionId, which is why neither is branded.\n * `exactPaneTarget` is named for what it RETURNS, a tmux target-pane, because\n * what it takes and what it produces are different things and the old name\n * `exactPane` read as though it took a pane.\n */\n/**\n * The window name worth RECORDING, given tmux's own answer and whether tmux is\n * renaming that window itself.\n *\n * Null while `automatic-rename` is on -- which is tmux's DEFAULT -- because the\n * name is then just the foreground process. The picker's `agent` column showed\n * `Python`, `node` and `zsh` for real agents: pi's own interpreter, labelled\n * \"agent\".\n *\n * A name nobody chose is not a name, and recording it as one is worse than\n * recording nothing: `agentLabel` prefers the window over the session, so a\n * process name shadowed `hacking/murmur` -- the string the reader actually\n * searches on. Dropped at the point of RECORDING rather than at render, so\n * every surface, and every peer reading this node's snapshot, agrees on what\n * counts as a name.\n *\n * Split out of `currentWindow` to be testable: that method shells out to a real\n * tmux server, so the decision had no reachable seam and the format string was\n * the only thing a test could have asserted on.\n */\nexport function chosenWindowName(\n name: string | undefined,\n autoRename: string | undefined,\n): string | null {\n if (autoRename === \"1\") return null;\n return name || null;\n}\n\nexport function exactSession(session: string): string {\n return `=${session}`;\n}\n\nexport function exactPaneTarget(session: string): string {\n return `=${session}:`;\n}\n\nexport function tmuxBadgeState(state: RenderState): string {\n // @agent_state is consumed by existing tmux configuration, whose public\n // vocabulary calls active work \"working\". Keep the internal activity named\n // \"running\" without forcing a coordinated config rollout.\n return state === \"running\" ? \"working\" : state;\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 raw = process.env.TMUX_PANE;\n if (!raw) return null;\n const pane = asPaneId(raw);\n\n // One call for ids and names together. The names travel with every row a\n // snapshot carries, because a reader cannot resolve a remote session or\n // window id against its own tmux.\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}\\t#{?automatic-rename,1,0}\",\n ]);\n const [session, window, sessionName, windowName, autoRename] = fields?.split(\"\\t\") ?? [];\n if (!session || !window) return null;\n return {\n session: asSessionId(session),\n window: asWindowId(window),\n pane,\n session_name: sessionName || null,\n window_name: chosenWindowName(windowName, autoRename),\n };\n },\n\n // Which of this host's PANES still exist. The only liveness question tmux is\n // ever asked, and the one that matches how an agent is addressed: a pane keeps\n // its id when it moves between windows, so a recorded window id can be gone\n // while the agent is very much alive.\n //\n // null means tmux could not answer; an empty set means it did and there are\n // none. Conflating the two would delete every agent on the host the moment\n // tmux was briefly unreachable.\n livePanes() {\n const out = runTmux([\"list-panes\", \"-a\", \"-F\", \"#{pane_id}\"]);\n if (out === null) return null;\n return new Set(out.split(\"\\n\").filter(Boolean).map(asPaneId));\n },\n\n setWindowBadge(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\", tmuxBadgeState(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 // 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).map(asPaneId) ?? [];\n },\n\n // Which client to send home when the remote attach exits. `switch-client`\n // with no -c moves whichever client tmux considers current, and `murmur pick`\n // usually runs in a popup -- a client of its own, which dies with the popup.\n // Naming the real client is what lets the return outlive the picker.\n clientName() {\n return runTmux([\"display-message\", \"-p\", \"#{client_name}\"]) || null;\n },\n\n // Where the jump started, as a switch-client target. Window-level, not just\n // the session: coming back to the right session but the wrong window is\n // still the wrong place. The window id is stable where its index is not,\n // since renumber-windows renumbers on every close.\n currentTarget() {\n return runTmux([\"display-message\", \"-p\", \"#{session_name}:#{window_id}\"]) || null;\n },\n\n // Whether a wrapper session for this host already exists. Deliberately not\n // returning an id: a session is addressed by name, so a `#{session_id}` would\n // only have to be turned back into one.\n sessionNamed(name) {\n const out = runTmux([\"list-sessions\", \"-F\", \"#{session_name}\"]);\n if (out === null) return false;\n return out.split(\"\\n\").includes(name);\n },\n\n newSession(name, command) {\n // Detached, because the caller sets the per-session options before showing\n // it. Creating it attached would paint one frame with the local status bar\n // up and the local prefix live, which is the flicker this design exists to\n // remove.\n return runTmux([\"new-session\", \"-d\", \"-s\", name, command]) !== null;\n },\n\n setSessionOption(session, option, value) {\n runTmux([\"set-option\", \"-t\", exactPaneTarget(session), option, value]);\n },\n\n switchClient(client, session) {\n const target = exactSession(session);\n const args = client\n ? [\"switch-client\", \"-c\", client, \"-t\", target]\n : [\"switch-client\", \"-t\", target];\n return runTmux(args) !== null;\n },\n\n // The window a pane belongs to, for a pane murmur holds no row for. Clearing\n // a badge is a tmux operation and does not require murmur to own the pane.\n windowForPane(pane) {\n const out = runTmux([\"display-message\", \"-t\", pane, \"-p\", \"#{window_id}\"]);\n return out ? asWindowId(out) : 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 { asPaneId, asSessionId, asWindowId } from \"./ids.js\";\nimport type {\n Activity,\n AttentionKind,\n Driver,\n Snapshot,\n SnapshotAgent,\n SnapshotAttention,\n SnapshotPane,\n} from \"./types.js\";\n\n/**\n * A peer answered, and what it said is not a snapshot.\n *\n * A distinct type because the collector must be able to tell this from an\n * unreachable host: a node that serves a bad document is REACHABLE BUT BROKEN,\n * and an operator needs to see that rather than \"asleep, probably\".\n */\nexport class SnapshotInvalidError extends Error {\n constructor(\n readonly path: string,\n detail: string,\n ) {\n // An EMPTY path means the failure is about the document as a whole, not\n // about a field in it, so there is nothing to prefix. Joining regardless\n // produced `bubba: : not JSON (...)` in `peer list` and in the one line\n // `murmur collect` prints -- measured against a real second node, and for\n // the most common remote misconfiguration there is (murmur missing, so the\n // \"document\" is a shell error). `path` itself stays \"\", because that is what\n // it means and a caller must not have to know a sentinel.\n super(path === \"\" ? detail : `${path}: ${detail}`);\n this.name = \"SnapshotInvalidError\";\n }\n}\n\nfunction fail(path: string, detail: string): never {\n throw new SnapshotInvalidError(path, detail);\n}\n\n/**\n * Exactly these keys, no more and no fewer.\n *\n * Unknown keys are rejected rather than carried, and nothing is coerced or\n * defaulted: validation happens BEFORE storage, so no unknown value can reach a\n * sort, a count or a render path.\n */\nfunction object(value: unknown, path: string, keys: readonly string[]): Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n fail(path, \"expected an object\");\n }\n const record = value as Record<string, unknown>;\n for (const key of keys) if (!(key in record)) fail(path, `missing key ${key}`);\n for (const key of Object.keys(record)) {\n if (!keys.includes(key)) fail(path, `unknown key ${key}`);\n }\n return record;\n}\n\nfunction text(value: unknown, path: string): string {\n if (typeof value !== \"string\" || value === \"\") fail(path, \"expected a non-empty string\");\n return value;\n}\n\nfunction textOrNull(value: unknown, path: string): string | null {\n if (value === null) return null;\n if (typeof value !== \"string\") fail(path, \"expected a string or null\");\n return value;\n}\n\nfunction anyText(value: unknown, path: string): string {\n if (typeof value !== \"string\") fail(path, \"expected a string\");\n return value;\n}\n\nfunction timestamp(value: unknown, path: string): number {\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < 0) {\n fail(path, \"expected a non-negative integer\");\n }\n return value;\n}\n\nfunction member<T extends string>(value: unknown, path: string, allowed: readonly T[]): T {\n if (typeof value !== \"string\" || !allowed.includes(value as T)) {\n fail(path, `expected one of ${allowed.join(\", \")}`);\n }\n return value as T;\n}\n\nconst ACTIVITIES: readonly Activity[] = [\"running\", \"stopped\"];\nconst DRIVERS: readonly Driver[] = [\"human\", \"orchestrated\"];\nconst KINDS: readonly AttentionKind[] = [\"done\", \"blocked\", \"crashed\"];\n\nconst TOP_KEYS = [\n \"murmur_snapshot\",\n \"host_id\",\n \"display_name\",\n \"murmur_version\",\n \"generated_at\",\n \"panes\",\n] as const;\nconst PANE_KEYS = [\n \"pane\",\n \"session\",\n \"window\",\n \"session_name\",\n \"window_name\",\n \"agent\",\n \"attention\",\n] as const;\nconst AGENT_KEYS = [\n \"agent_id\",\n \"activity\",\n \"agent_name\",\n \"pi_session\",\n \"workstream\",\n \"role\",\n \"cli\",\n \"driver\",\n \"claimed_at\",\n \"updated_at\",\n] as const;\nconst ATTENTION_KEYS = [\"kind\", \"message\", \"source\", \"requested_at\"] as const;\n\nfunction parseAgent(value: unknown, path: string): SnapshotAgent | null {\n if (value === null) return null;\n const row = object(value, path, AGENT_KEYS);\n return {\n agent_id: text(row.agent_id, `${path}.agent_id`),\n activity: member(row.activity, `${path}.activity`, ACTIVITIES),\n agent_name: textOrNull(row.agent_name, `${path}.agent_name`),\n pi_session: textOrNull(row.pi_session, `${path}.pi_session`),\n workstream: textOrNull(row.workstream, `${path}.workstream`),\n role: textOrNull(row.role, `${path}.role`),\n cli: text(row.cli, `${path}.cli`),\n driver: member(row.driver, `${path}.driver`, DRIVERS),\n claimed_at: timestamp(row.claimed_at, `${path}.claimed_at`),\n updated_at: timestamp(row.updated_at, `${path}.updated_at`),\n };\n}\n\nfunction parseAttention(value: unknown, path: string): SnapshotAttention[] {\n if (!Array.isArray(value)) fail(path, \"expected an array\");\n const seen = new Set<AttentionKind>();\n return value.map((entry, index) => {\n const at = `${path}[${index}]`;\n const row = object(entry, at, ATTENTION_KEYS);\n const kind = member(row.kind, `${at}.kind`, KINDS);\n if (seen.has(kind)) fail(`${at}.kind`, `duplicate kind ${kind} for this pane`);\n seen.add(kind);\n return {\n kind,\n message: anyText(row.message, `${at}.message`),\n source: anyText(row.source, `${at}.source`),\n requested_at: timestamp(row.requested_at, `${at}.requested_at`),\n };\n });\n}\n\nfunction parsePane(value: unknown, path: string): SnapshotPane {\n const row = object(value, path, PANE_KEYS);\n const agent = parseAgent(row.agent, `${path}.agent`);\n const attention = parseAttention(row.attention, `${path}.attention`);\n // Rule 3 of the document schema: a pane with neither is not a pane worth\n // publishing, so a document carrying one is malformed rather than merely\n // noisy.\n if (agent === null && attention.length === 0) {\n fail(path, \"a pane with no agent and no attention must not be emitted\");\n }\n return {\n pane: asPaneId(text(row.pane, `${path}.pane`)),\n session: asSessionId(text(row.session, `${path}.session`)),\n window: asWindowId(text(row.window, `${path}.window`)),\n session_name: textOrNull(row.session_name, `${path}.session_name`),\n window_name: textOrNull(row.window_name, `${path}.window_name`),\n agent,\n attention,\n };\n}\n\n/**\n * Parse and totally validate one snapshot document.\n *\n * `murmur_snapshot` must be exactly 1: a higher value is rejected too, because\n * forward compatibility is not offered here and a version mismatch is an\n * operator-visible pairing problem. Saying so is the honest report; guessing at\n * a newer document's meaning is not.\n */\nexport function parseSnapshot(input: string): Snapshot {\n let parsed: unknown;\n try {\n parsed = JSON.parse(input);\n } catch (error) {\n fail(\"\", `not JSON (${error instanceof Error ? error.message : String(error)})`);\n }\n const top = object(parsed, \"\", TOP_KEYS);\n if (top.murmur_snapshot !== 1) {\n fail(\"murmur_snapshot\", `expected 1, got ${JSON.stringify(top.murmur_snapshot)}`);\n }\n if (!Array.isArray(top.panes)) fail(\"panes\", \"expected an array\");\n\n const panes = top.panes.map((entry, index) => parsePane(entry, `panes[${index}]`));\n const seen = new Set<string>();\n for (const pane of panes) {\n if (seen.has(pane.pane)) fail(\"panes\", `duplicate pane ${pane.pane}`);\n seen.add(pane.pane);\n }\n\n return {\n murmur_snapshot: 1,\n host_id: text(top.host_id, \"host_id\"),\n display_name: text(top.display_name, \"display_name\"),\n murmur_version: text(top.murmur_version, \"murmur_version\"),\n generated_at: timestamp(top.generated_at, \"generated_at\"),\n panes,\n };\n}\n","import type { PaneId, SessionId, WindowId } from \"./ids.js\";\n\n/**\n * The three independent facts, as types.\n *\n * `activity` is what the pane's own process says it is doing. `attention` is\n * whether a human is wanted. `freshness` (src/view.ts) is how recently we\n * reached the node that reported. They are three independent fields, never one\n * enum, and absence carries meaning: no attention row means \"nothing to see\",\n * no agent row means \"no agent here\".\n */\nexport type Activity = \"running\" | \"stopped\";\nexport type AttentionKind = \"done\" | \"blocked\" | \"crashed\";\n\n/**\n * Who is waiting on this agent -- a human, or a supervisor that consumes the\n * result. Not \"which harness\"; that is `cli`.\n */\nexport type Driver = \"human\" | \"orchestrated\";\n\nexport const DEFAULT_DRIVER: Driver = \"human\";\n\n/**\n * Where a pane currently lives. Location, never identity.\n *\n * `pane` is the address and is stable for the life of the pane; `session` and\n * `window` are only where that pane currently is, and both change under\n * move-pane and break-pane. Only a pane may decide whether an agent exists,\n * which is what the brands in ./ids.js enforce.\n */\nexport type Location = {\n session: SessionId;\n window: WindowId;\n pane: PaneId;\n session_name: string | null;\n window_name: string | null;\n};\n\n/** Owner-reported metadata about the agent in a pane. */\nexport type AgentMeta = {\n agent_name: string | null;\n pi_session: string | null;\n workstream: string | null;\n role: string | null;\n cli: string;\n driver: Driver;\n};\n\nexport type PeerRecord = {\n name: string;\n target: string;\n host_id: string | null;\n display_name: string | null;\n /** The whole validated document, or null when we have never parsed one. */\n snapshot: Snapshot | null;\n /** The PEER's clock: when that node built the document. */\n snapshot_at: number | null;\n /** OUR clock: when we last reached it. Freshness is computed from this. */\n fetched_at: number | null;\n last_attempt_at: number | null;\n last_error: string | null;\n murmur_version: string | null;\n /** The peer's `murmur_snapshot` value, i.e. the document version it speaks. */\n snapshot_version: number | null;\n};\n\n/**\n * One node's whole current state. Complete, never a delta: a peer that returns\n * one has said everything it knows, so absence from it is absence.\n */\nexport type Snapshot = {\n murmur_snapshot: 1;\n host_id: string;\n display_name: string;\n murmur_version: string;\n generated_at: number;\n panes: SnapshotPane[];\n};\n\nexport type SnapshotPane = {\n pane: PaneId;\n session: SessionId;\n window: WindowId;\n session_name: string | null;\n window_name: string | null;\n /** Null for an attention-only pane: valid, listable, jumpable. */\n agent: SnapshotAgent | null;\n attention: SnapshotAttention[];\n};\n\nexport type SnapshotAgent = AgentMeta & {\n agent_id: string;\n activity: Activity;\n claimed_at: number;\n updated_at: number;\n};\n\nexport type SnapshotAttention = {\n kind: AttentionKind;\n message: string;\n source: string;\n requested_at: number;\n};\n\n/**\n * Whether a pid is still running. A parameter everywhere it is consulted, so a\n * test needs no process table.\n */\nexport type LiveCheck = (pid: number) => boolean;\n\nexport type AgentClaim = {\n location: Location;\n owner_pid: number;\n meta: AgentMeta;\n now?: number;\n isAlive?: LiveCheck;\n};\n\nexport type ClaimResult =\n | { outcome: \"claimed\"; agent_id: string }\n | { outcome: \"retained\"; agent_id: string }\n | { outcome: \"replaced\"; agent_id: string; previous_agent_id: string }\n | { outcome: \"refused\"; held_by_pid: number };\n\nexport type ActivityUpdate = {\n agent_id: string;\n owner_pid: number;\n activity: Activity;\n location: Location;\n now?: number;\n};\n\nexport type AgentRelease = { agent_id: string; owner_pid: number };\n\n/**\n * Everything an attention writer may say. There is no agent_id, no owner_pid,\n * no activity and no owner metadata field, and adding one is a contract change.\n */\nexport type AttentionRequest = {\n kind: AttentionKind;\n location: Location;\n message: string;\n source: string;\n now?: number;\n};\n\n/**\n * The only local facts reconciliation is allowed to consult.\n *\n * `panes` is null when tmux could not answer, which is not evidence of death.\n * `isAlive` and `now` are parameters so a test needs no process table and no\n * clock control.\n */\nexport type LocalWorld = {\n panes: Set<PaneId> | null;\n isAlive?: LiveCheck;\n now?: number;\n};\n\nexport type ReconcileSummary = {\n crashed: PaneId[];\n removed: PaneId[];\n attention_removed: PaneId[];\n};\n\nexport type PeerFetch =\n | { ok: true; snapshot: Snapshot; at: number }\n | { ok: false; error: string; at: number };\n","import type { NodeIdentity } from \"./identity.js\";\nimport type { PaneId, SessionId, WindowId } from \"./ids.js\";\nimport type { Store } from \"./store.js\";\nimport {\n type Activity,\n type AttentionKind,\n DEFAULT_DRIVER,\n type Driver,\n type SnapshotPane,\n} from \"./types.js\";\n\nexport type Freshness = \"fresh\" | \"stale\";\n\n/**\n * What a surface paints. Presentation only, derived from the three independent\n * facts and never stored.\n */\nexport type RenderState = \"crashed\" | \"blocked\" | \"done\" | \"running\" | \"idle\";\n\n/**\n * THE single ordering table: which state matters most, for sorting and for\n * choosing one word to show.\n *\n * `status.ts` and `pick.ts` import this rather than declaring their own copies,\n * so no two surfaces can sort one list differently.\n */\nexport const RENDER_PRIORITY: readonly RenderState[] = [\n \"crashed\",\n \"blocked\",\n \"done\",\n \"running\",\n \"idle\",\n];\n\n/**\n * The attention kinds only a human can answer, and the second table both\n * surfaces must agree on.\n *\n * `blocked` means waiting for an answer an orchestrator cannot give -- mu places\n * work, it cannot choose between two approaches. `crashed` means the process\n * died, which a supervisor may or may not retry. Everything else about an\n * orchestrated agent is its supervisor's business.\n *\n * `pick.ts` uses it to decide which crew rows are visible by default and\n * `status.ts` to decide which crew states reach the status bar. They were two\n * literals in two files answering one question, which is how a row that needed a\n * human became one a human could not see.\n */\nexport const NEEDS_HUMAN: readonly AttentionKind[] = [\"blocked\", \"crashed\"];\n\n/**\n * One pane, as every surface reads it: address, the three independent facts,\n * owner metadata, and ages.\n *\n * Local and remote panes are the same type, built by the same mapping, because\n * `Store.localPanes()` and a peer's cached snapshot both return\n * `SnapshotPane[]`. One mapping means local and remote cannot drift apart.\n */\nexport type PaneView = {\n // address\n host_id: string;\n /** The name the operator typed, or this node's display_name. */\n host: string;\n local: boolean;\n pane: PaneId;\n session: SessionId;\n window: WindowId;\n session_name: string | null;\n window_name: string | null;\n // the three independent facts\n /** Null for an attention-only pane, which has no agent row. */\n activity: Activity | null;\n attention: AttentionKind[];\n freshness: Freshness;\n // owner-reported metadata, null for an attention-only pane\n agent_id: string | null;\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;\n // ages\n /** When the pane's own node last said something. Never `fetched_at`. */\n updated_at: number | null;\n /** When that node generated its snapshot. Null for local. */\n snapshot_at: number | null;\n /** When we last reached that node. Null for local. */\n fetched_at: number | null;\n};\n\n/**\n * How long a peer may go unfetched before its panes render stale.\n *\n * Re-exported from here rather than imported from the collector by view\n * consumers, so freshness has one definition. See collector.ts for why sixty\n * seconds.\n */\nexport const STALENESS_MS = 60_000;\n\n/**\n * A duration as the shortest thing worth reading: \"5m\", \"2h\", \"3d\".\n *\n * Under a minute is the empty string: an age that changes every second is noise\n * in a status column. This and `freshness` are the only two places a duration\n * becomes text or a verdict.\n */\nexport function age(ms: number | null): string {\n if (ms === null || ms < 60_000) return \"\";\n if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;\n if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h`;\n return `${Math.floor(ms / 86_400_000)}d`;\n}\n\n/**\n * Freshness of a NODE, never of an agent.\n *\n * A peer we have never reached is stale rather than fresh: null means the first\n * collect has not succeeded yet, and an unreachable host you just added must not\n * render as up to date.\n */\nexport function freshness(\n fetchedAt: number | null,\n now: number,\n thresholdMs = STALENESS_MS,\n): Freshness {\n return fetchedAt !== null && now - fetchedAt <= thresholdMs ? \"fresh\" : \"stale\";\n}\n\n/**\n * One word for a pane. Attention wins over activity, because attention is a\n * request and activity is a description.\n *\n * A running agent with `blocked` attention is a valid and expected state, and\n * surfaces that can show both, do — this is only for the ones that must pick.\n */\nexport function renderState(view: Pick<PaneView, \"activity\" | \"attention\">): RenderState {\n for (const kind of [\"crashed\", \"blocked\", \"done\"] as const) {\n if (view.attention.includes(kind)) return kind;\n }\n return view.activity === \"running\" ? \"running\" : \"idle\";\n}\n\n/** The newest attention request on a pane, for the `updated_at` of one with no agent. */\nfunction newestAttention(pane: SnapshotPane): number | null {\n let newest: number | null = null;\n for (const entry of pane.attention) {\n if (newest === null || entry.requested_at > newest) newest = entry.requested_at;\n }\n return newest;\n}\n\ntype ViewSource = {\n host_id: string;\n host: string;\n local: boolean;\n freshness: Freshness;\n snapshot_at: number | null;\n fetched_at: number | null;\n};\n\nfunction paneView(pane: SnapshotPane, source: ViewSource): PaneView {\n const agent = pane.agent;\n return {\n host_id: source.host_id,\n host: source.host,\n local: source.local,\n pane: pane.pane,\n session: pane.session,\n window: pane.window,\n session_name: pane.session_name,\n window_name: pane.window_name,\n activity: agent?.activity ?? null,\n attention: pane.attention.map((entry) => entry.kind),\n freshness: source.freshness,\n agent_id: agent?.agent_id ?? null,\n agent_name: agent?.agent_name ?? null,\n pi_session: agent?.pi_session ?? null,\n workstream: agent?.workstream ?? null,\n role: agent?.role ?? null,\n cli: agent?.cli ?? null,\n driver: agent?.driver ?? DEFAULT_DRIVER,\n updated_at: agent?.updated_at ?? newestAttention(pane),\n snapshot_at: source.snapshot_at,\n fetched_at: source.fetched_at,\n };\n}\n\n/**\n * Every pane this node knows about: its own, plus one cached snapshot per peer.\n *\n * `identity` is non-null because every caller is a command that already requires\n * `murmur init`, so no pane can be misclassified as remote by an absent one.\n *\n * No liveness is probed here, for local or remote. A remote pane's `activity` is\n * whatever its own node last said; a stale node keeps its last-known fields\n * verbatim beside an explicit warning.\n */\nexport function paneViews(store: Store, identity: NodeIdentity, now = Date.now()): PaneView[] {\n const views = store.localPanes().map((pane) =>\n paneView(pane, {\n host_id: identity.host_id,\n host: identity.display_name,\n local: true,\n // Local panes are always fresh: we are the node that authored them.\n freshness: \"fresh\",\n snapshot_at: null,\n fetched_at: null,\n }),\n );\n\n for (const peer of store.peers()) {\n const snapshot = peer.snapshot;\n if (!snapshot) continue;\n const source: ViewSource = {\n host_id: snapshot.host_id,\n // The name the human typed, not the machine's self-reported hostname: a\n // peer added as `linuxpc` can report a container id, which appears\n // nowhere else in the tool and cannot be typed at `peer remove`.\n host: peer.name,\n local: false,\n freshness: freshness(peer.fetched_at, now),\n snapshot_at: peer.snapshot_at,\n fetched_at: peer.fetched_at,\n };\n for (const pane of snapshot.panes) views.push(paneView(pane, source));\n }\n\n return views;\n}\n\nconst ORDER = new Map<RenderState, number>(RENDER_PRIORITY.map((state, index) => [state, index]));\n\n/**\n * Attention-first ordering, then the newest news, then address.\n *\n * TOTAL on purpose, and that is the whole reason the last two comparisons\n * exist. Ties on state and age are ordinary rather than exotic -- a pair of\n * crashed panes reconciled in one transaction shares a `requested_at` exactly --\n * and `Array.prototype.sort` is stable only with respect to the order it was\n * GIVEN, which here is whatever SQLite and the peer loop happened to produce. An\n * unbroken tie therefore makes the list depend on that order: a status bar\n * reshuffles between two identical ticks, and a picker row moves under the\n * keypress that was aimed at it.\n *\n * Presentation only. No caller may read meaning into the position of a row --\n * pane order in a snapshot carries none either, so a reader sorts for itself\n * rather than trusting what it was served.\n */\nexport function viewSort(views: PaneView[]): PaneView[] {\n return [...views].sort((left, right) => {\n const byState = (ORDER.get(renderState(left)) ?? 99) - (ORDER.get(renderState(right)) ?? 99);\n if (byState !== 0) return byState;\n // Unknown age sorts last within its state: an attention-only pane with no\n // timestamp is not news, and 0 is older than any real clock reading.\n const byAge = (right.updated_at ?? 0) - (left.updated_at ?? 0);\n if (byAge !== 0) return byAge;\n // Address as the final key, because it is the only field guaranteed unique\n // across the whole view: `pane` is unique per node and `host` per peer.\n const byHost = left.host.localeCompare(right.host);\n return byHost !== 0 ? byHost : left.pane.localeCompare(right.pane);\n });\n}\n","import type { Channel } from \"./channel.js\";\nimport { type Mux, tmux } from \"./mux.js\";\nimport { parseSnapshot, SnapshotInvalidError } from \"./snapshot.js\";\nimport type { Store } from \"./store.js\";\nimport { STALENESS_MS } from \"./view.js\";\n\nexport { STALENESS_MS };\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. The per-peer ssh timeout applies\n// once per wave, so nine unreachable peers cost two waves: the pool serialises\n// the timeouts it exists to limit. So the whole collect gets its own deadline,\n// independent of peer count. Peers still in flight when it expires are\n// abandoned and render stale, which is already the designed outcome for a host\n// 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.\nconst 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 /** Panes in the snapshot we just stored. Zero is a normal, valid answer. */\n panes: number;\n error?: string;\n /**\n * True when the peer could not be reached at all, as opposed to answering\n * with something wrong.\n *\n * A fleet normally has nodes that are asleep or switched off, so this is the\n * expected outcome rather than a fault, and callers use it to stay quiet\n * about the ordinary case while still reporting a peer that is reachable but\n * broken -- a bad snapshot version, a missing binary, an auth problem.\n */\n unreachable?: boolean;\n};\n\n/**\n * Whether an error means \"could not reach the host\".\n *\n * ssh exits 255 for its own failures and prints a recognisable line, and the\n * exec wrapper puts both in the message. Matching on the text is unpleasant but\n * it is the only signal available: the channel seam returns an Error, not an\n * exit status.\n *\n * `Permission denied` is deliberately NOT here. An auth misconfiguration is\n * reachable-but-broken and an operator task; classing it as \"asleep, probably\"\n * is how a fixable setup error stays invisible for weeks.\n */\nfunction isUnreachable(message: string): boolean {\n return (\n /Host is down|No route to host|Connection refused|Connection timed out|Connection closed|Operation timed out|Network is unreachable|Name or service not known|Could not resolve hostname|timed out after/i.test(\n message,\n ) || /\\bssh:/.test(message)\n );\n}\n\n/**\n * Drop Node's `Command failed: <argv>` first line, keeping the child's output.\n *\n * Every rejection from the ssh channel arrives in that shape, so the first ~140\n * characters of every real failure are the invocation murmur chose: `ssh -o\n * BatchMode=yes -o ControlMaster=no -o ControlPath=... -o ConnectTimeout=1\n * <host> murmur export`. The operator cannot act on any of it, and it pushed the\n * one line that mattered past the length bound below -- measured against a real\n * second node, where a missing remote binary printed\n * `bubba: Command failed: ssh -o BatchMode=yes ... murmur: command not f...`,\n * truncated on the only actionable word in it.\n *\n * Stripped BEFORE the newlines are collapsed, because the line boundary is the\n * only thing separating the invocation from the diagnosis.\n */\nfunction stripInvocation(message: string): string {\n const firstLine = message.indexOf(\"\\n\");\n if (firstLine === -1 || !message.startsWith(\"Command failed:\")) return message;\n const rest = message.slice(firstLine + 1).trim();\n // A bare `Command failed:` line with nothing after it is all we have; saying\n // nothing would be worse than saying too much.\n return rest === \"\" ? message : rest;\n}\n\n/**\n * The one normalisation, so classification and rendering cannot disagree.\n *\n * `unreachable` (a machine-readable flag on `CollectResult`) and\n * `describeFailure` (the line a human reads) both classify with\n * `isUnreachable`. Feeding them differently-normalised text is how a peer gets\n * reported as reachable-but-broken in JSON and \"unreachable\" in print, about\n * one fetch -- so both go through here.\n */\nfunction normalizeFailure(message: string): string {\n return stripInvocation(message).replace(/\\s+/g, \" \").trim();\n}\n\n/**\n * A peer failure in one line a human can act on.\n *\n * The raw error was the whole ssh invocation plus ssh's own message -- over 200\n * characters, of which the actionable part was the host name. It also leaked\n * every ssh option murmur passes, which a user cannot do anything about.\n */\nexport function describeFailure(peer: string, message: string): string {\n const collapsed = normalizeFailure(message);\n if (isUnreachable(collapsed)) {\n const reason = /ssh: (?:connect to host \\S+ port \\d+: )?(.+?)(?: \\(|$)/i.exec(collapsed);\n return `${peer}: unreachable (${(reason?.[1] ?? \"ssh failed\").trim()})`;\n }\n // Reachable but wrong: keep the message, since it is the diagnosis, but bound\n // it so a corrupt snapshot cannot print a screenful.\n const detail = collapsed.length > 160 ? `${collapsed.slice(0, 157)}...` : collapsed;\n return `${peer}: ${detail}`;\n}\n\n/**\n * Fetch every peer's snapshot, validate it, and replace the cache whole.\n *\n * Concurrent because an unreachable peer costs the full ssh timeout, and a\n * serial loop charged that to every peer behind it: three asleep laptops made\n * `murmur status` hang for thirty seconds. Applied serially in peer order,\n * because better-sqlite3 is synchronous and a stable order keeps the result list\n * aligned with `store.peers()`.\n *\n * One round trip per peer, and never a second: the document is complete, so what\n * arrives either replaces the cache entirely or does not touch it.\n */\nexport async function collect(\n store: Store,\n channel: Channel,\n now = Date.now(),\n deadline?: Promise<void>,\n mux: Mux = tmux,\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.\n const fetches = await mapSettled(\n peers,\n MAX_CONCURRENT_PEERS,\n async (peer) => parseSnapshot(await channel.exec(peer.target, [\"murmur\", \"export\"])),\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 // Every field the cache derives comes out of the document itself, so\n // the cache structurally cannot disagree with the snapshot it holds.\n store.replacePeerSnapshot(peer.name, { ok: true, snapshot: fetch.value, at: now });\n results.push({ peer: peer.name, ok: true, panes: fetch.value.panes.length });\n } catch (error) {\n // Normalised ONCE, here, before it is stored or returned.\n //\n // `last_error` is read by `peer list`, by `status --json` and by\n // anything built on the SDK, and none of them can undo the mangling: a\n // raw `execFile` rejection leads with `Command failed: ssh -o\n // BatchMode=yes -o ControlMaster=no -o ControlPath=... <host> murmur\n // export`, which is murmur's own invocation and nothing an operator can\n // act on. Measured against a real second node, where `peer list`\n // printed 140 characters of ssh options before the four words that\n // mattered. Storing the normalised text means every surface gets the\n // diagnosis without each one having to remember to strip it.\n const message = normalizeFailure(error instanceof Error ? error.message : String(error));\n store.replacePeerSnapshot(peer.name, { ok: false, error: message, at: now });\n // Reported through the return value, never printed here. `collect` runs\n // from `murmur status` on every status-bar tick, and from `pick` inside\n // a display-popup, so a single sleeping laptop wrote to stderr forever\n // and corrupted both. Only the `collect` command -- which a human ran\n // on purpose -- prints.\n results.push({\n peer: peer.name,\n ok: false,\n panes: 0,\n error: message,\n // A peer that answered with a bad document is reachable but broken,\n // and must be visibly so rather than silently stale.\n unreachable:\n error instanceof SnapshotInvalidError\n ? false\n : isUnreachable(normalizeFailure(message)),\n });\n }\n }\n } catch (error) {\n // The whole collect failed rather than one peer -- a broken peer table, say.\n // Still not printed: the caller decides.\n results.push({\n peer: \"\",\n ok: false,\n panes: 0,\n error: error instanceof Error ? error.message : String(error),\n });\n } finally {\n clearTimeout(timer);\n }\n\n // The only housekeeping left, and it runs once per invocation including with\n // zero peers -- which is why it is here rather than on `export`, which only\n // runs when a peer asks over ssh. A single-machine node would otherwise\n // reconcile never. Idempotent, so `buildLocalSnapshot` calling it too is a\n // cheap repeat rather than a second policy.\n try {\n store.reconcileLocal({ panes: mux.livePanes(), now });\n } catch {\n // Housekeeping must not fail a command, and it must not report either.\n }\n return results;\n}\n","import { execFileSync } from \"node:child_process\";\nimport { SSH_OPTIONS } from \"./channel.js\";\nimport { tmux } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\nimport type { PaneView } from \"./view.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: PaneView, lines = GLANCE_LINES): string | null {\n if (agent.local) 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 { 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\nfunction identityPath(): string {\n return join(stateDir(), \"identity.json\");\n}\n\n/**\n * Memoized per process, keyed on the resolved path.\n *\n * `identity.json` cannot change under a running command, and the audit measured\n * eight redundant reads per invocation. Keyed on the path rather than a bare\n * boolean so a test that repoints `MURMUR_STATE_DIR` mid-process is not served\n * another directory's identity.\n */\nlet cache: { path: string; identity: NodeIdentity | null } | null = null;\n\n/**\n * This node's identity, or null when it has none.\n *\n * A READ, and only a read: nothing mints here. Every command that needs a\n * host_id fails with \"murmur is not initialised on this node; run: murmur init\"\n * rather than bringing a node into existence as a side effect of a status-bar\n * tick.\n */\nexport function loadIdentity(): NodeIdentity | null {\n const path = identityPath();\n if (cache?.path === path) return cache.identity;\n const identity = existsSync(path)\n ? (JSON.parse(readFileSync(path, \"utf8\")) as NodeIdentity)\n : null;\n cache = { path, identity };\n return identity;\n}\n\nfunction write(identity: NodeIdentity): NodeIdentity {\n mkdirSync(stateDir(), { recursive: true });\n writeFileSync(identityPath(), `${JSON.stringify(identity, null, 2)}\\n`);\n cache = { path: identityPath(), identity };\n return identity;\n}\n\n/** Create this node's identity. Only `murmur init` calls it. */\nexport function createIdentity(displayName = hostname()): NodeIdentity {\n if (loadIdentity()) throw new Error(`identity already exists: ${identityPath()}`);\n return write({ host_id: randomUUID(), display_name: displayName });\n}\n\n/**\n * Rename an existing node, keeping its `host_id`.\n *\n * `murmur init --name` on an already-initialised node used to ignore the flag\n * silently, which is the one thing a rename must not do.\n */\nexport function setDisplayName(displayName: string): NodeIdentity {\n const existing = loadIdentity();\n return write(\n existing\n ? { host_id: existing.host_id, display_name: displayName }\n : { host_id: randomUUID(), display_name: displayName },\n );\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\n/** The current-state database. The only database murmur holds. */\nexport function dbPath(): string {\n return join(stateDir(), \"state.db\");\n}\n","import { type Channel, ssh } from \"./channel.js\";\nimport { collect } from \"./collector.js\";\nimport type { NodeIdentity } from \"./identity.js\";\nimport { type Mux, tmux } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\nimport {\n freshness,\n NEEDS_HUMAN,\n type PaneView,\n paneViews,\n RENDER_PRIORITY,\n type RenderState,\n renderState,\n viewSort,\n} from \"./view.js\";\n\ntype Counts = Record<RenderState, number>;\n\nexport type Status = {\n counts: Counts;\n orchestrated_counts: Counts;\n panes: PaneView[];\n peers: {\n name: string;\n display_name: string | null;\n fetched_at: number | null;\n snapshot_at: number | null;\n last_error: string | null;\n stale: boolean;\n }[];\n};\n\nfunction emptyCounts(): Counts {\n const counts = {} as Counts;\n for (const state of RENDER_PRIORITY) counts[state] = 0;\n return counts;\n}\n\nexport function tmuxStatus(view: Status): string {\n // Orchestrated agents are counted for the states only a human can answer, and\n // hidden for the rest: a supervisor consumes a `done` worker's result, so\n // nobody needs to acknowledge it, and `running` asks for nothing. The list is\n // `NEEDS_HUMAN` in view.ts, shared with the picker's visibility rule so the\n // status bar and the list cannot disagree about which crew rows matter.\n const needsHuman = new Set<string>(NEEDS_HUMAN);\n const total = (state: RenderState): number =>\n view.counts[state] + (needsHuman.has(state) ? view.orchestrated_counts[state] : 0);\n return (\n RENDER_PRIORITY.filter((state) => total(state) > 0)\n // The tmux renderer's public vocabulary predates the internal activity\n // rename. Keep that external protocol stable until the renderer is updated.\n .map((state) => `${state === \"running\" ? \"working\" : state}\\t${total(state)}\\n`)\n .join(\"\")\n );\n}\n\n/**\n * The current view. Pure with respect to the network: the caller decides whether\n * to collect first (see `statusWithCollect`).\n *\n * `identity` is required rather than resolved here, because every caller is a\n * command that already fails without one.\n */\nexport function status(store: Store, identity: NodeIdentity, now = Date.now()): Status {\n const counts = emptyCounts();\n const orchestratedCounts = emptyCounts();\n const panes = viewSort(paneViews(store, identity, now));\n for (const pane of panes) {\n const target = pane.driver === \"human\" ? counts : orchestratedCounts;\n target[renderState(pane)] += 1;\n }\n\n return {\n counts,\n orchestrated_counts: orchestratedCounts,\n panes,\n peers: store.peers().map((peer) => ({\n name: peer.name,\n display_name: peer.display_name,\n fetched_at: peer.fetched_at,\n // Their clock and ours, separately: a peer polled a second ago can be\n // serving a three-hour-old fact, and one number cannot say both.\n snapshot_at: peer.snapshot_at,\n last_error: peer.last_error,\n // The view's verdict, not a second threshold spelled the same way. A\n // peer we have never reached is stale rather than fresh -- null\n // `fetched_at` means the first collect has not succeeded yet -- and\n // `freshness` is the one place that decides, so this list and the panes\n // the peer contributes cannot disagree about the same host.\n stale: freshness(peer.fetched_at, now) === \"stale\",\n })),\n };\n}\n\n/**\n * Collect from peers, then read. 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. And the callers close the store in a\n * `finally`, so a collect still in flight lands on a closed handle and reports\n * \"The database connection is not open\", which looks like corruption rather\n * than a race.\n *\n * Sync must never fail a command, and on this path it must never print either:\n * `status` runs on every status-bar tick and `pick` runs inside a\n * display-popup, so one sleeping laptop would otherwise write ssh diagnostics\n * to stderr several times a minute, forever. `murmur collect`, which a human\n * runs deliberately, is the only place that prints.\n */\nexport async function statusWithCollect(\n store: Store,\n identity: NodeIdentity,\n now = Date.now(),\n channel: Channel = ssh,\n mux: Mux = tmux,\n): Promise<Status> {\n try {\n await collect(store, channel, now, undefined, mux);\n } catch {\n // Total by construction: a read of whatever the cache already holds is\n // always better than no output, and this path has no one to tell.\n }\n return status(store, identity, now);\n}\n","import { randomUUID } from \"node:crypto\";\nimport { mkdirSync, rmSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport Database from \"better-sqlite3\";\nimport type { NodeIdentity } from \"./identity.js\";\nimport type { PaneId } from \"./ids.js\";\nimport { asPaneId, asSessionId, asWindowId } from \"./ids.js\";\nimport { pidAlive } from \"./mux.js\";\nimport { dbPath } from \"./paths.js\";\nimport type {\n ActivityUpdate,\n AgentClaim,\n AgentRelease,\n AttentionKind,\n AttentionRequest,\n ClaimResult,\n LocalWorld,\n PeerFetch,\n PeerRecord,\n ReconcileSummary,\n Snapshot,\n SnapshotAgent,\n SnapshotAttention,\n SnapshotPane,\n} from \"./types.js\";\nimport { MURMUR_VERSION } from \"./version.js\";\nimport { RENDER_PRIORITY } from \"./view.js\";\n\n/**\n * The storage version. Any change to any table bumps it.\n *\n * ONE version strategy: a mismatch salvages the peer names and targets a human\n * typed, deletes the file, and recreates the schema. No ALTER TABLE anywhere, so\n * there is no additive path to forget to use.\n */\nconst SCHEMA_USER_VERSION = 3;\n\nconst SCHEMA = `\n CREATE TABLE agents (\n agent_id TEXT NOT NULL PRIMARY KEY,\n pane TEXT NOT NULL UNIQUE,\n owner_pid INTEGER NOT NULL CHECK (owner_pid > 0),\n activity TEXT NOT NULL CHECK (activity IN ('running', 'stopped')),\n session TEXT NOT NULL,\n window 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 NOT NULL,\n driver TEXT NOT NULL CHECK (driver IN ('human', 'orchestrated')),\n claimed_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n ) STRICT;\n\n CREATE TABLE attention (\n pane TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('done', 'blocked', 'crashed')),\n message TEXT NOT NULL,\n source TEXT NOT NULL,\n session TEXT NOT NULL,\n window TEXT NOT NULL,\n session_name TEXT,\n window_name TEXT,\n requested_at INTEGER NOT NULL,\n PRIMARY KEY (pane, kind)\n ) STRICT;\n\n CREATE TABLE peers (\n name TEXT NOT NULL PRIMARY KEY,\n target TEXT NOT NULL,\n host_id TEXT,\n display_name TEXT,\n snapshot TEXT,\n snapshot_at INTEGER,\n fetched_at INTEGER,\n last_attempt_at INTEGER,\n last_error TEXT,\n murmur_version TEXT,\n snapshot_version INTEGER\n ) STRICT;\n`;\n\n/**\n * The store, and the only place in murmur that holds a database handle or\n * writes SQL.\n *\n * This interface is CLOSED. There is no `append`, no `ingest`, no log read, no\n * partial-row update, and no local read other than `localPanes` — each of those\n * shapes let a writer say something it had no standing to say, and each cost a\n * shipped bug. Attention methods take no agent identity at all, which is what\n * makes \"a notifier cannot corrupt an agent row\" structural.\n */\nexport interface Store {\n // --- agent lifecycle: owner-only, pid-gated -----------------------------\n claimAgent(claim: AgentClaim): ClaimResult;\n setActivity(update: ActivityUpdate): boolean;\n releaseAgent(release: AgentRelease): boolean;\n\n // --- attention: pane-addressed, no agent authority ----------------------\n requestAttention(request: AttentionRequest): void;\n acknowledgePane(pane: PaneId): number;\n\n // --- local truth --------------------------------------------------------\n /** The one local read. Joins agents and attention by pane. No reconciliation. */\n localPanes(): SnapshotPane[];\n reconcileLocal(world: LocalWorld): ReconcileSummary;\n buildLocalSnapshot(identity: NodeIdentity, world: LocalWorld): Snapshot;\n\n // --- peer cache ---------------------------------------------------------\n peers(): PeerRecord[];\n addPeer(name: string, target: string): void;\n removePeer(name: string): boolean;\n replacePeerSnapshot(name: string, fetch: PeerFetch): void;\n\n close(): void;\n}\n\ntype AgentDbRow = {\n agent_id: string;\n pane: string;\n owner_pid: number;\n activity: string;\n session: string;\n window: string;\n session_name: string | null;\n window_name: string | null;\n agent_name: string | null;\n pi_session: string | null;\n workstream: string | null;\n role: string | null;\n cli: string;\n driver: string;\n claimed_at: number;\n updated_at: number;\n};\n\ntype AttentionDbRow = {\n pane: string;\n kind: string;\n message: string;\n source: string;\n session: string;\n window: string;\n session_name: string | null;\n window_name: string | null;\n requested_at: number;\n};\n\ntype PeerDbRow = {\n name: string;\n target: string;\n host_id: string | null;\n display_name: string | null;\n snapshot: string | null;\n snapshot_at: number | null;\n fetched_at: number | null;\n last_attempt_at: number | null;\n last_error: string | null;\n murmur_version: string | null;\n snapshot_version: number | null;\n};\n\n/** Peer names and targets: the two fields a human typed, and all we salvage. */\nfunction salvagePeers(path: string): { name: string; target: string }[] {\n try {\n const existing = new Database(path, { fileMustExist: true });\n try {\n const version = (existing.pragma(\"user_version\", { simple: true }) as number) ?? 0;\n if (version === SCHEMA_USER_VERSION) return [];\n return existing.prepare(\"SELECT name, target FROM peers\").all() as {\n name: string;\n target: string;\n }[];\n } catch {\n // Too old to have the table, or unreadable. Nothing to save.\n return [];\n } finally {\n existing.close();\n }\n } catch {\n // No database yet, or one too broken to open.\n return [];\n }\n}\n\nfunction needsReset(path: string): boolean {\n try {\n const existing = new Database(path, { fileMustExist: true });\n try {\n return (\n ((existing.pragma(\"user_version\", { simple: true }) as number) ?? 0) !== SCHEMA_USER_VERSION\n );\n } finally {\n existing.close();\n }\n } catch {\n return false;\n }\n}\n\nfunction toAttention(row: AttentionDbRow): SnapshotAttention {\n return {\n kind: row.kind as AttentionKind,\n message: row.message,\n source: row.source,\n requested_at: row.requested_at,\n };\n}\n\nfunction toAgent(row: AgentDbRow): SnapshotAgent {\n return {\n agent_id: row.agent_id,\n activity: row.activity as SnapshotAgent[\"activity\"],\n agent_name: row.agent_name,\n pi_session: row.pi_session,\n workstream: row.workstream,\n role: row.role,\n cli: row.cli,\n driver: row.driver as SnapshotAgent[\"driver\"],\n claimed_at: row.claimed_at,\n updated_at: row.updated_at,\n };\n}\n\nconst PRIORITY = new Map<string, number>(RENDER_PRIORITY.map((kind, index) => [kind, index]));\n\nfunction attentionOrder(left: SnapshotAttention, right: SnapshotAttention): number {\n return (PRIORITY.get(left.kind) ?? 99) - (PRIORITY.get(right.kind) ?? 99);\n}\n\n/**\n * Open the store. Takes no arguments and mints no identity.\n *\n * `openStore` deliberately does NOT read or create `identity.json`: identity is\n * created only by `murmur init`, so a read path — a status-bar tick, a focus\n * hook — cannot bring a node into existence as a side effect.\n */\nexport function openStore(): Store {\n const path = dbPath();\n mkdirSync(dirname(path), { recursive: true });\n\n const salvaged = salvagePeers(path);\n if (needsReset(path)) {\n for (const suffix of [\"\", \"-wal\", \"-shm\"]) rmSync(`${path}${suffix}`, { force: true });\n }\n\n const database = new Database(path);\n database.pragma(\"journal_mode = WAL\");\n database.pragma(\"busy_timeout = 5000\");\n const version = (database.pragma(\"user_version\", { simple: true }) as number) ?? 0;\n if (version !== SCHEMA_USER_VERSION) {\n database.exec(SCHEMA);\n database.pragma(`user_version = ${SCHEMA_USER_VERSION}`);\n // Re-inserted with every OBSERVED column null: a salvaged peer has no\n // snapshot and has never been fetched, and saying otherwise would render a\n // never-reached host as fresh.\n const restore = database.prepare(\"INSERT OR IGNORE INTO peers (name, target) VALUES (?, ?)\");\n for (const peer of salvaged) restore.run(peer.name, peer.target);\n }\n\n const selectAgentByPane = database.prepare(\"SELECT * FROM agents WHERE pane = ?\");\n const insertAgent = database.prepare(`\n INSERT INTO agents (agent_id, pane, owner_pid, activity, session, window,\n session_name, window_name, agent_name, pi_session,\n workstream, role, cli, driver, claimed_at, updated_at)\n VALUES (@agent_id, @pane, @owner_pid, @activity, @session, @window,\n @session_name, @window_name, @agent_name, @pi_session,\n @workstream, @role, @cli, @driver, @claimed_at, @updated_at)\n `);\n const retainAgent = database.prepare(`\n UPDATE agents\n SET session = @session, window = @window, session_name = @session_name,\n window_name = @window_name, agent_name = @agent_name,\n pi_session = @pi_session, workstream = @workstream, role = @role,\n cli = @cli, driver = @driver, updated_at = @updated_at\n WHERE agent_id = @agent_id\n `);\n const deleteAgentByPane = database.prepare(\"DELETE FROM agents WHERE pane = ?\");\n const deleteAttentionForPane = database.prepare(\"DELETE FROM attention WHERE pane = ?\");\n const updateActivity = database.prepare(`\n UPDATE agents\n SET activity = @activity, session = @session, window = @window,\n session_name = @session_name, window_name = @window_name,\n updated_at = @updated_at\n WHERE agent_id = @agent_id AND owner_pid = @owner_pid\n `);\n const deleteAgentOwned = database.prepare(\n \"DELETE FROM agents WHERE agent_id = ? AND owner_pid = ?\",\n );\n const upsertAttention = database.prepare(`\n INSERT INTO attention (pane, kind, message, source, session, window,\n session_name, window_name, requested_at)\n VALUES (@pane, @kind, @message, @source, @session, @window,\n @session_name, @window_name, @requested_at)\n ON CONFLICT (pane, kind) DO UPDATE SET\n message = excluded.message,\n source = excluded.source,\n session = excluded.session,\n window = excluded.window,\n session_name = excluded.session_name,\n window_name = excluded.window_name\n `);\n const selectAgents = database.prepare(\"SELECT * FROM agents\");\n const selectAttention = database.prepare(\"SELECT * FROM attention\");\n const setActivityByPane = database.prepare(\n \"UPDATE agents SET activity = ?, updated_at = ? WHERE pane = ?\",\n );\n\n /**\n * `.immediate`, not deferred, and this is load-bearing.\n *\n * The transaction reads the incumbent row and then writes, so a deferred one\n * starts as a READER and must upgrade. Two doing that at once fails the loser\n * with SQLITE_BUSY_SNAPSHOT, which no busy_timeout can fix: waiting longer\n * cannot make a stale snapshot fresh. Measured previously at 5 of 8\n * concurrent writers failing.\n */\n const claimAgent = database.transaction((claim: AgentClaim): ClaimResult => {\n const now = claim.now ?? Date.now();\n const isAlive = claim.isAlive ?? pidAlive;\n const { location, meta, owner_pid } = claim;\n const incumbent = selectAgentByPane.get(location.pane) as AgentDbRow | undefined;\n\n const values = {\n pane: location.pane,\n owner_pid,\n session: location.session,\n window: location.window,\n session_name: location.session_name,\n window_name: location.window_name,\n agent_name: meta.agent_name,\n pi_session: meta.pi_session,\n workstream: meta.workstream,\n role: meta.role,\n cli: meta.cli,\n driver: meta.driver,\n updated_at: now,\n };\n\n if (!incumbent) {\n const agentId = randomUUID();\n insertAgent.run({ ...values, agent_id: agentId, activity: \"stopped\", claimed_at: now });\n return { outcome: \"claimed\", agent_id: agentId };\n }\n\n // Our own claim, seen again. This is what makes pi's `/reload` a no-op: pi\n // re-runs the extension factory in the same process, and a check that could\n // not recognise its own claim would silence the real agent. `activity` and\n // `agent_id` are deliberately untouched.\n if (incumbent.owner_pid === owner_pid) {\n retainAgent.run({ ...values, agent_id: incumbent.agent_id });\n return { outcome: \"retained\", agent_id: incumbent.agent_id };\n }\n\n // A different LIVE process in one pane: the nested-agent case, and the only\n // answer for it. Fails closed — `pidAlive` reports death only on ESRCH, so\n // an unanswerable probe (EPERM) reads as alive and refuses. An unknown must\n // never let a second writer displace a possibly-live owner.\n if (isAlive(incumbent.owner_pid)) {\n return { outcome: \"refused\", held_by_pid: incumbent.owner_pid };\n }\n\n // The previous occupant is gone. Its attention described a process that no\n // longer exists, and a human looking at the pane now sees a different agent.\n deleteAgentByPane.run(location.pane);\n deleteAttentionForPane.run(location.pane);\n const agentId = randomUUID();\n insertAgent.run({ ...values, agent_id: agentId, activity: \"stopped\", claimed_at: now });\n return { outcome: \"replaced\", agent_id: agentId, previous_agent_id: incumbent.agent_id };\n }).immediate;\n\n /**\n * One transaction, because the `stopped` write and its `crashed` attention row\n * must land together or not at all.\n *\n * A no-op when tmux could not answer: `panes === null` is absence of evidence,\n * not evidence of death, and conflating the two once deleted ten live agents.\n */\n const reconcileLocal = database.transaction((world: LocalWorld): ReconcileSummary => {\n const summary: ReconcileSummary = { crashed: [], removed: [], attention_removed: [] };\n if (world.panes === null) return summary;\n const live = world.panes;\n const isAlive = world.isAlive ?? pidAlive;\n const now = world.now ?? Date.now();\n\n // Which panes already carry a crash we recorded. Read once, before any\n // write, so the loop below sees the state reconciliation started from.\n const alreadyCrashed = new Set(\n (selectAttention.all() as AttentionDbRow[])\n .filter((row) => row.kind === \"crashed\")\n .map((row) => row.pane),\n );\n\n for (const row of selectAgents.all() as AgentDbRow[]) {\n const pane = asPaneId(row.pane);\n if (!live.has(pane)) {\n deleteAgentByPane.run(row.pane);\n deleteAttentionForPane.run(row.pane);\n summary.removed.push(pane);\n continue;\n }\n if (isAlive(row.owner_pid)) continue;\n\n // The asymmetry below is the point. A dead RUNNING owner is an unreported\n // crash and must leave a durable trace. A dead STOPPED owner finished\n // normally, so its row is noise — but any `done` it raised is a fact a\n // human has not yet seen, so the attention stays.\n if (row.activity === \"running\") {\n setActivityByPane.run(\"stopped\", now, row.pane);\n upsertAttention.run({\n pane: row.pane,\n kind: \"crashed\",\n message: \"\",\n source: \"murmur\",\n session: row.session,\n window: row.window,\n session_name: row.session_name,\n window_name: row.window_name,\n requested_at: now,\n });\n summary.crashed.push(pane);\n } else if (!alreadyCrashed.has(row.pane)) {\n deleteAgentByPane.run(row.pane);\n summary.removed.push(pane);\n }\n // A pane we already recorded a crash for keeps its agent row, and that is\n // the one place this deviates from a literal reading of the contract's\n // table -- which says a live pane with a dead STOPPED owner loses its row.\n // Taken literally, the second reconcile after a crash deletes the row the\n // first one had just marked `stopped`, so the crashed pane loses its\n // agent_name, workstream, role and cli one tick after the crash is\n // reported. That contradicts the contract's own idempotence requirement\n // (\"running it again changes nothing\") and it strips exactly the fields a\n // human needs to know WHICH agent died.\n //\n // The distinction the table is drawing is between an owner that finished\n // normally -- whose row is noise -- and one that died mid-run. The\n // `crashed` row we wrote is the record of which case this was, so it is\n // also the right thing to key on.\n }\n\n // Reaps attention for a pane that never had an agent row — an\n // attention-only codex pane whose window was closed. Nothing else would.\n for (const row of selectAttention.all() as AttentionDbRow[]) {\n const pane = asPaneId(row.pane);\n if (live.has(pane)) continue;\n deleteAttentionForPane.run(row.pane);\n if (!summary.attention_removed.includes(pane)) summary.attention_removed.push(pane);\n }\n\n return summary;\n }).immediate;\n\n /**\n * Both tables read at ONE point in time, or a pane can appear with an agent\n * and without the attention that was there when the agent was read.\n */\n const readLocalPanes = database.transaction((): SnapshotPane[] => {\n const agents = selectAgents.all() as AgentDbRow[];\n const attention = selectAttention.all() as AttentionDbRow[];\n const panes = new Map<string, SnapshotPane>();\n\n const locate = (row: AgentDbRow | AttentionDbRow): SnapshotPane => {\n const existing = panes.get(row.pane);\n if (existing) return existing;\n const created: SnapshotPane = {\n pane: asPaneId(row.pane),\n session: asSessionId(row.session),\n window: asWindowId(row.window),\n session_name: row.session_name,\n window_name: row.window_name,\n agent: null,\n attention: [],\n };\n panes.set(row.pane, created);\n return created;\n };\n\n for (const row of agents) locate(row).agent = toAgent(row);\n for (const row of attention) locate(row).attention.push(toAttention(row));\n\n for (const pane of panes.values()) pane.attention.sort(attentionOrder);\n return [...panes.values()].sort((left, right) => left.pane.localeCompare(right.pane));\n });\n\n function peerRecord(row: PeerDbRow): PeerRecord {\n let snapshot: Snapshot | null = null;\n if (row.snapshot !== null) {\n try {\n // Parsed leniently on the way OUT: it was validated on the way in, and\n // a read path must not throw. A stored document that no longer parses\n // reads as \"no snapshot\" and is left in place, not deleted.\n snapshot = JSON.parse(row.snapshot) as Snapshot;\n } catch {\n snapshot = null;\n }\n }\n return {\n name: row.name,\n target: row.target,\n host_id: row.host_id,\n display_name: row.display_name,\n snapshot,\n snapshot_at: row.snapshot_at,\n fetched_at: row.fetched_at,\n last_attempt_at: row.last_attempt_at,\n last_error: row.last_error,\n murmur_version: row.murmur_version,\n snapshot_version: row.snapshot_version,\n };\n }\n\n return {\n claimAgent,\n reconcileLocal,\n\n setActivity(update) {\n // Both key components are required, so a write from a REPLACED owner\n // matches nothing and returns false. That is not an error and must not be\n // retried: it means this process is no longer the owner of record, and the\n // correct response is silence.\n return (\n updateActivity.run({\n activity: update.activity,\n session: update.location.session,\n window: update.location.window,\n session_name: update.location.session_name,\n window_name: update.location.window_name,\n updated_at: update.now ?? Date.now(),\n agent_id: update.agent_id,\n owner_pid: update.owner_pid,\n }).changes === 1\n );\n },\n\n releaseAgent(release) {\n // Attention is deliberately NOT deleted: a `done` raised at settle must\n // survive the agent exiting, or completion becomes invisible the moment\n // the process quits.\n return deleteAgentOwned.run(release.agent_id, release.owner_pid).changes === 1;\n },\n\n requestAttention(request) {\n // `requested_at` is absent from the DO UPDATE list on purpose. Age means\n // \"how long this has gone unmet\", so a repeat must not reset the clock —\n // which also makes crash attention idempotent for free. Touches no\n // `agents` row, ever; there is no column here that could.\n upsertAttention.run({\n pane: request.location.pane,\n kind: request.kind,\n message: request.message,\n source: request.source,\n session: request.location.session,\n window: request.location.window,\n session_name: request.location.session_name,\n window_name: request.location.window_name,\n requested_at: request.now ?? Date.now(),\n });\n },\n\n acknowledgePane(pane) {\n // Every kind, one statement, no agent row touched: focusing a pane cannot\n // alter activity or owner metadata. This is the whole `murmur clear`\n // write path.\n return deleteAttentionForPane.run(pane).changes;\n },\n\n localPanes() {\n return readLocalPanes();\n },\n\n buildLocalSnapshot(identity, world) {\n // Reconcile first, which is what makes \"a snapshot is authoritative\"\n // true: absence from a successful snapshot means absence, so it must\n // never be produced from unreconciled rows. Two transactions rather than\n // one — a write transaction held open across the read would serialise\n // every focus hook on the machine behind an export.\n reconcileLocal(world);\n return {\n murmur_snapshot: 1,\n host_id: identity.host_id,\n display_name: identity.display_name,\n murmur_version: MURMUR_VERSION,\n generated_at: world.now ?? Date.now(),\n // Rule 3: a pane with no agent and no attention must not be published.\n // A no-op against today's `readLocalPanes`, which builds a pane entry\n // only from a row and so cannot produce an empty one -- kept because the\n // rule belongs to the DOCUMENT, and the validator rejects such an entry\n // outright. Without it, one narrowing of the local read would make this\n // node reachable-but-broken on every peer that collects it, and the\n // symptom would show up on the other machines.\n panes: readLocalPanes().filter((pane) => pane.agent !== null || pane.attention.length > 0),\n };\n },\n\n peers() {\n return (database.prepare(\"SELECT * FROM peers ORDER BY name\").all() as PeerDbRow[]).map(\n peerRecord,\n );\n },\n\n addPeer(name, target) {\n // Correcting a target must not discard the cache, so this updates only\n // the field the operator retyped.\n database\n .prepare(\n `INSERT INTO peers (name, target) VALUES (?, ?)\n ON CONFLICT(name) DO UPDATE SET target = excluded.target`,\n )\n .run(name, target);\n },\n\n removePeer(name) {\n return database.prepare(\"DELETE FROM peers WHERE name = ?\").run(name).changes > 0;\n },\n\n replacePeerSnapshot(name, fetch) {\n if (!fetch.ok) {\n // Failure touches neither snapshot, snapshot_at nor fetched_at, so the\n // last-known document stands and the peer ages into `stale` on its own.\n database\n .prepare(\"UPDATE peers SET last_attempt_at = ?, last_error = ? WHERE name = ?\")\n .run(fetch.at, fetch.error, name);\n return;\n }\n // Two clocks, and conflating them is how a freshly fetched three-hour-old\n // fact reads as new. `snapshot_at` is the PEER's clock (when it built the\n // document); `fetched_at` is OURS (when we reached it), and freshness is\n // computed from `fetched_at` only.\n database\n .prepare(\n `UPDATE peers\n SET snapshot = ?, snapshot_at = ?, fetched_at = ?, last_attempt_at = ?,\n last_error = NULL, host_id = ?, display_name = ?,\n murmur_version = ?, snapshot_version = ?\n WHERE name = ?`,\n )\n .run(\n JSON.stringify(fetch.snapshot),\n fetch.snapshot.generated_at,\n fetch.at,\n fetch.at,\n fetch.snapshot.host_id,\n fetch.snapshot.display_name,\n fetch.snapshot.murmur_version,\n fetch.snapshot.murmur_snapshot,\n name,\n );\n },\n\n close() {\n database.close();\n },\n };\n}\n","import { createRequire } from \"node:module\";\n\n/**\n * This node's murmur version, read from the manifest.\n *\n * Read rather than restated, for the reason index.ts already gives: two copies\n * of one fact drift, and npm bumps the manifest. It lives in its own module\n * because THREE bundles need it and they sit at different depths --\n * `dist/index.js`, `dist/cli.js` and `dist/extension/store.js` -- so a single\n * hardcoded `\"../package.json\"` resolves in two of them and throws in the third.\n *\n * That is not hypothetical. `openStore` moved into the extension bundle during\n * the current-state rewrite, and its `../package.json` became\n * `dist/package.json`, which does not exist. The extension catches every store\n * failure and degrades to silence, so the symptom was an agent that reported\n * nothing at all, with no error anywhere -- exactly the failure mode the\n * three-state store handle exists to make survivable, hiding a hard one.\n *\n * Hence both candidates, tried in order, and a throw if neither works: a version\n * this node cannot state belongs in a snapshot even less than a wrong one does.\n */\nfunction readVersion(): string {\n const require = createRequire(import.meta.url);\n for (const candidate of [\"../package.json\", \"../../package.json\"]) {\n try {\n return (require(candidate) as { version: string }).version;\n } catch {\n // Wrong depth for this bundle; try the next.\n }\n }\n throw new Error(\"cannot locate package.json to read the murmur version\");\n}\n\nexport const MURMUR_VERSION: string = readVersion();\n"],"mappings":"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","names":["execFileSync","execFileSync","attach","execFileSync","execFileSync","join","join","randomUUID","mkdirSync","require","mkdirSync","agentId","randomUUID"]}
1
+ {"version":3,"sources":["../src/agents.ts","../src/channel.ts","../src/ids.ts","../src/mux.ts","../src/snapshot.ts","../src/types.ts","../src/view.ts","../src/collector.ts","../src/glance.ts","../src/identity.ts","../src/paths.ts","../src/status.ts","../src/store.ts","../src/version.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { SSH_OPTIONS } from \"./channel.js\";\nimport { asPaneId } from \"./ids.js\";\nimport { type Mux, tmux } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\nimport type { PaneView } from \"./view.js\";\n\n/**\n * The last segment of a slash-separated tmux session name.\n *\n * Session names are conventionally paths -- `tms` and friends name a session\n * after the directory it was opened in -- so the last segment is the part that\n * identifies the work: `hacking/murmur` is about `murmur`.\n *\n * Only ever applied to a SESSION name. A window name that survives to the label\n * is one a human chose (see `chosenWindowName`), and shortening a deliberate\n * name would be presumptuous; a session name is the fallback of last resort and\n * is almost never chosen by hand.\n *\n * Not `path.basename`: a session name is not a filesystem path, it merely looks\n * like one. Splitting on `/` says what is meant, and cannot start resolving `..`\n * or behaving differently per platform.\n *\n * Degenerate inputs keep the original rather than returning \"\": a session called\n * `/` or ending in a slash has no last segment, and an empty name column is\n * worse than an odd one.\n */\nexport function sessionLeaf(session: string): string {\n const leaf = session.split(\"/\").filter(Boolean).at(-1);\n return leaf ?? session;\n}\n\n/**\n * The most specific human-readable name a pane's agent has, never a tmux id.\n *\n * Four sources, most to least specific: mu's agent name, pi's session name, the\n * tmux window name, the tmux session name. All are recorded by the node that\n * owns the pane, so this reads the same for a local and a remote pane -- a\n * reader cannot resolve a remote window id against its own tmux.\n *\n * The session name is shortened to its last segment, and that is load-bearing\n * rather than cosmetic. It is reached far more often than it looks: `agent_name`\n * is set only by mu, `window_name` is null whenever tmux is auto-renaming (its\n * default), and `pi_session` is null for the whole life of an unnamed session --\n * pi's own auto-namer runs at CLOSE, so a live agent, which is exactly the one\n * you are looking at, has no session name yet. So the common row for a\n * hand-started pi fell through to here and printed `hacking/murmur`: a path,\n * where a name belongs.\n *\n * The full session name is not lost. The picker's stream column shows it, and\n * shows it precisely BECAUSE of this shortening: that column is blanked when it\n * would repeat the name, so `hacking/murmur` in both cells collapsed to one\n * path and a blank. Shortened, the row reads `murmur` + `hacking/murmur` -- the\n * same width, carrying strictly more.\n *\n * Falls back to the window id only when a node recorded no names at all, which\n * means a non-tmux harness.\n */\nexport function agentLabel(agent: PaneView): string {\n const name =\n agent.agent_name ??\n agent.pi_session ??\n agent.window_name ??\n (agent.session_name === null ? null : sessionLeaf(agent.session_name));\n return terminalText(name ?? agent.window);\n}\n\n/**\n * Where the pane lives, for the second column. Names only -- the ids are what\n * jumps, not what a human reads.\n */\nexport function agentLocation(agent: PaneView): 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 local session name that wraps a remote attach.\n *\n * The trailing `~` marks it as murmur's, both for a human reading a session\n * list and for the `#{m:*~,...}` match in the suggested escape-hatch binding.\n *\n * The leading character is the part that matters. A tmux `-t` target starting\n * with `@`, `$` or `%` is parsed as a window, session or pane id, so a session\n * named `@bubba` -- which is exactly what the old per-host WINDOW was called --\n * cannot be addressed at all: every `-t @bubba` fails with `can't find window`.\n * Window names were never targets, so the old name was safe; session names are.\n */\nexport function remoteSessionName(peerName: string): string {\n return `${peerName.replace(/^[@$%=]+/, \"\")}~`;\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\nconst 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 // Local to this process: the picker prints `message` and nothing else, and\n // no reason code here is ever stored or published in a snapshot.\n reason: \"no_peer\" | \"unreachable\" | \"no_tmux\" | \"pane_gone\" | \"attach_failed\";\n message: string;\n };\n\n/**\n * Jump to a pane, wherever it lives.\n *\n * NEVER MUTATES STATE ON FAILURE. A failure is a report: a reason and a message,\n * nothing written. The next collect reconciles either way, and only the owning\n * node can author facts about its own panes.\n */\nexport function jumpToAgent(\n store: Store,\n agent: PaneView,\n mux: Mux = tmux,\n run: Runner = spawnRunner,\n): JumpResult {\n if (agent.local) {\n // The PANE decides, and only the pane. This once asked whether the agent's\n // WINDOW still existed, which a live pane routinely outlives: after\n // `move-pane -s %0 -t @1`, list-panes still has %0 and list-windows no\n // longer has @0. Asking the wrong one reported healthy agents as gone.\n const panes = mux.livePanes();\n if (panes && !panes.has(agent.pane)) {\n return {\n ok: false,\n reason: \"pane_gone\",\n message: `${agentLabel(agent)} is gone -- its pane no longer exists.`,\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 pane is still there before opening a window to attach to it.\n // Panes, not windows: a recorded window id goes stale every time the pane\n // moves, so it cannot answer whether the agent exists. 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-panes -a -F ${shellQuote(\"#{pane_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. A real fact about the host, and reported as\n // one: nothing is deleted here. The peer's own next snapshot is what\n // removes its panes, because only that node may author about them, and a\n // reader that evicts rows on a probe failure is guessing.\n return {\n ok: false,\n reason: \"no_tmux\",\n message: `${target} has no tmux server running, so its agents are gone. They will disappear on the next collect.`,\n };\n }\n const remotePanes = new Set(probe.stdout.split(\"\\n\").filter(Boolean).map(asPaneId));\n if (!remotePanes.has(agent.pane)) {\n return {\n ok: false,\n reason: \"pane_gone\",\n message: `${agentLabel(agent)} is gone -- ${target} no longer has that pane.`,\n };\n }\n\n const attachTarget = shellQuote(`${agent.session}:${agent.window}`);\n\n // Hand the ssh to tmux as its own detached SESSION rather than running it\n // here. `murmur pick` is usually a display-popup, and a popup is modal: an\n // ssh started inside it is killed the moment the picker exits, so the remote\n // pane flashed and vanished. A session outlives the popup and gives the\n // remote tmux a real terminal to attach to.\n //\n // A session, not a window, because session options are per-session and that\n // is what makes the nesting stop being felt:\n //\n // status off -- no local status bar, so the remote's own bar is the only\n // one on screen and the jump reads as a full-screen ssh.\n // prefix None -- no local prefix at all, so ^b reaches the remote\n // directly. No ^b b, and no second prefix to learn.\n //\n // Both would be global if this were a window, and would break every local\n // session. The cost is that the local server is unreachable from inside the\n // wrapper; the README documents a root-table key that detaches out.\n if (process.env.TMUX) {\n // Read BEFORE the wrapper exists, or we would record the wrapper itself as\n // the place to come back to and the return would be a no-op.\n const client = mux.clientName();\n const origin = mux.currentTarget();\n\n // Named after the peer as configured, matching the picker's host column.\n // The machine's self-reported display_name can be a container id, which\n // makes the session unrecognisable in a session list.\n const name = remoteSessionName(peer?.name ?? target);\n\n // Reuse an existing wrapper for this host rather than stacking a new one on\n // every jump. Jumping to bubba three times used to leave three identical\n // windows behind. Matched on name, the only handle available: the ssh is\n // opaque from here and the remote session id is not a local address.\n if (mux.sessionNamed(name)) {\n return mux.switchClient(client, name)\n ? { ok: true }\n : {\n ok: false,\n reason: \"attach_failed\",\n message: `could not switch to the existing ${name} session.`,\n };\n }\n\n // `tmux new-session <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 attach = `ssh -t ${shellQuote(target)} tmux attach -t ${shellQuote(attachTarget)}`;\n\n // The return home, as part of the wrapper's own command. When the attach\n // exits -- inner detach, remote session killed, ssh dropped -- this runs,\n // then the wrapper has no command left and tmux destroys it.\n //\n // Explicit, rather than relying on detach-on-destroy: `previous` picks\n // tmux's idea of the previous session, which in testing was a stray\n // unrelated session rather than the one the jump started from. It is still\n // set below as a fallback for when this command cannot run (SIGKILL).\n const restore = origin\n ? `; tmux switch-client ${client ? `-c ${shellQuote(client)} ` : \"\"}-t ${shellQuote(`=${origin}`)}`\n : \"\";\n\n if (!mux.newSession(name, `${attach}${restore}`)) {\n return {\n ok: false,\n reason: \"attach_failed\",\n message: `could not open a session to attach to ${target}.`,\n };\n }\n\n mux.setSessionOption(name, \"status\", \"off\");\n mux.setSessionOption(name, \"prefix\", \"None\");\n mux.setSessionOption(name, \"detach-on-destroy\", \"previous\");\n\n return mux.switchClient(client, name)\n ? { ok: true }\n : {\n ok: false,\n reason: \"attach_failed\",\n message: `attached to ${target} in session ${name}, but could not switch to it.`,\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 //\n // None of the wrapper-session machinery above applies here, and it must not:\n // there is no local client to switch, nothing to return to but the shell that\n // invoked us, and no local status bar or prefix to suppress. This path is\n // already full-screen and already prefix-clean -- the whole problem is an\n // artifact of being inside tmux. Creating a local session here would attach a\n // client to a server the user never asked for, and leave them inside tmux on\n // exit rather than back at their prompt.\n //\n // The tradeoff is no reuse of an existing attach, since there is no local\n // server holding one. That is correct rather than missing.\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 the\n// peer's whole current state, bounded by live pane count -- a few hundred bytes\n// per pane, on a machine that cannot hold thousands of panes -- so the ceiling\n// is out of reach in practice.\n//\n// It is kept generous anyway, because the failure mode is bad out of proportion\n// to its likelihood: a peer whose document exceeds the buffer fails identically\n// on every collect, so it sits stale forever with an error that names a Node\n// internal rather than a size.\n//\n// 64 MiB is orders of magnitude above any real snapshot, 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","/**\n * tmux's three id kinds, kept apart by the type system.\n *\n * tmux itself is unambiguous about this and prints a sigil on every id --\n * `session=$25 window=@75 pane=%89` -- but they are all strings, so murmur\n * could and did pass one where another was meant. Twice, in shipped code: a\n * sweep keyed on window liveness deleted ten live agents, and a window cached\n * at extension startup badged the window a moved pane had left.\n *\n * An agent is addressed by its PANE, which keeps its id across `move-pane`,\n * `break-pane`, and a window closed and reopened. A session and a window are\n * only where that pane currently lives, and both may differ between two reports\n * from one agent. So the rule the brands enforce is:\n *\n * only a pane may decide whether an agent exists.\n *\n * Branding is a compile-time fiction: at runtime these are the same strings\n * tmux printed, which is what keeps the snapshot document and every stored row\n * byte-identical.\n */\n\ndeclare const brand: unique symbol;\n\n/** A tmux session id, `$N`. Mutable location. */\nexport type SessionId = string & { readonly [brand]: \"session\" };\n\n/** A tmux window id, `@N`. Mutable location -- never an agent's identity. */\nexport type WindowId = string & { readonly [brand]: \"window\" };\n\n/** A tmux pane id, `%N`. The agent's identity, stable for its whole life. */\nexport type PaneId = string & { readonly [brand]: \"pane\" };\n\n/*\n * The boundary. Every raw string that becomes an id passes through one of these\n * three, so the unsafe step is in one file and countable rather than scattered\n * as `as` at each call site.\n *\n * Deliberately not validating the sigil. These are called on tmux stdout, on\n * JSON off the wire, on sqlite rows and on argv, and a node that recorded an id\n * murmur does not recognise -- a future tmux, a different harness -- must still\n * round-trip it. Rejecting here would turn a naming change into a behaviour\n * change.\n */\n\nexport function asSessionId(raw: string): SessionId {\n return raw as SessionId;\n}\n\nexport function asWindowId(raw: string): WindowId {\n return raw as WindowId;\n}\n\nexport function asPaneId(raw: string): PaneId {\n return raw as PaneId;\n}\n","import { execFileSync } from \"node:child_process\";\nimport {\n asPaneId,\n asSessionId,\n asWindowId,\n type PaneId,\n type SessionId,\n type WindowId,\n} from \"./ids.js\";\nimport type { Location } from \"./types.js\";\nimport type { RenderState } from \"./view.js\";\n\nexport interface Mux {\n currentWindow(): Location | null;\n livePanes(): Set<PaneId> | null;\n // Sets `@agent_state` on a WINDOW, even though the attention it expresses\n // belongs to a pane. The asymmetry is tmux's: the status bar and the `tms`\n // picker read a window option, and there is no per-pane equivalent they\n // would read instead. Its consequence is that a pane moving between windows\n // must clear the badge it left behind, since nothing else knows it moved.\n setWindowBadge(window: WindowId, state: RenderState | 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: SessionId, window: WindowId): boolean;\n windowForPane(pane: PaneId): WindowId | null;\n panesInWindow(window: WindowId): PaneId[];\n capture(pane: PaneId, lines?: number): string | null;\n // --- remote-jump session seam -------------------------------------------\n // A remote attach lives in its own local session rather than a window, so it\n // can be full-screen (no local status bar) and prefix-free (no nested ^b).\n // See jumpToAgent for why that is worth five extra methods.\n clientName(): string | null;\n currentTarget(): string | null;\n sessionNamed(name: string): boolean;\n newSession(name: string, command: string): boolean;\n setSessionOption(session: string, option: string, value: string): void;\n switchClient(client: string | null, session: string): boolean;\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\n/**\n * A session name as an exact target, in the two spellings tmux needs.\n *\n * Bare names match by PREFIX, so a wrapper for host `bub` silently retargets a\n * session called `bubba` once one exists -- verified, and it sets options on\n * the wrong session rather than failing. A leading `=` demands an exact match.\n * (`name=` is not the syntax; it reads as part of the name and matches nothing.)\n *\n * The trailing colon is the part that is easy to get wrong. `switch-client -t`\n * takes a target-SESSION, where `=name` is right, but `set-option -t` and\n * `show-options -t` take a target-PANE, where `=name` fails outright with `no\n * such session` and the exact form is `=name:` -- the empty window/pane part\n * resolving to the session's current pane.\n *\n * Neither rescues a name starting with `@`, `$` or `%`: those introduce tmux's\n * window, session and pane id syntax. remoteSessionName keeps them out.\n *\n * Both take a session NAME -- not a SessionId, which is why neither is branded.\n * `exactPaneTarget` is named for what it RETURNS, a tmux target-pane, because\n * what it takes and what it produces are different things and the old name\n * `exactPane` read as though it took a pane.\n */\n/**\n * The window name worth RECORDING, given tmux's own answer and whether tmux is\n * renaming that window itself.\n *\n * Null while `automatic-rename` is on -- which is tmux's DEFAULT -- because the\n * name is then just the foreground process. The picker's `agent` column showed\n * `Python`, `node` and `zsh` for real agents: pi's own interpreter, labelled\n * \"agent\".\n *\n * A name nobody chose is not a name, and recording it as one is worse than\n * recording nothing: `agentLabel` prefers the window over the session, so a\n * process name shadowed `hacking/murmur` -- the string the reader actually\n * searches on. Dropped at the point of RECORDING rather than at render, so\n * every surface, and every peer reading this node's snapshot, agrees on what\n * counts as a name.\n *\n * Split out of `currentWindow` to be testable: that method shells out to a real\n * tmux server, so the decision had no reachable seam and the format string was\n * the only thing a test could have asserted on.\n */\nexport function chosenWindowName(\n name: string | undefined,\n autoRename: string | undefined,\n): string | null {\n if (autoRename === \"1\") return null;\n return name || null;\n}\n\nexport function exactSession(session: string): string {\n return `=${session}`;\n}\n\nexport function exactPaneTarget(session: string): string {\n return `=${session}:`;\n}\n\nexport function tmuxBadgeState(state: RenderState): string {\n // @agent_state is consumed by existing tmux configuration, whose public\n // vocabulary calls active work \"working\". Keep the internal activity named\n // \"running\" without forcing a coordinated config rollout.\n return state === \"running\" ? \"working\" : state;\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 raw = process.env.TMUX_PANE;\n if (!raw) return null;\n const pane = asPaneId(raw);\n\n // One call for ids and names together. The names travel with every row a\n // snapshot carries, because a reader cannot resolve a remote session or\n // window id against its own tmux.\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}\\t#{?automatic-rename,1,0}\",\n ]);\n const [session, window, sessionName, windowName, autoRename] = fields?.split(\"\\t\") ?? [];\n if (!session || !window) return null;\n return {\n session: asSessionId(session),\n window: asWindowId(window),\n pane,\n session_name: sessionName || null,\n window_name: chosenWindowName(windowName, autoRename),\n };\n },\n\n // Which of this host's PANES still exist. The only liveness question tmux is\n // ever asked, and the one that matches how an agent is addressed: a pane keeps\n // its id when it moves between windows, so a recorded window id can be gone\n // while the agent is very much alive.\n //\n // null means tmux could not answer; an empty set means it did and there are\n // none. Conflating the two would delete every agent on the host the moment\n // tmux was briefly unreachable.\n livePanes() {\n const out = runTmux([\"list-panes\", \"-a\", \"-F\", \"#{pane_id}\"]);\n if (out === null) return null;\n return new Set(out.split(\"\\n\").filter(Boolean).map(asPaneId));\n },\n\n setWindowBadge(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\", tmuxBadgeState(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 // 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).map(asPaneId) ?? [];\n },\n\n // Which client to send home when the remote attach exits. `switch-client`\n // with no -c moves whichever client tmux considers current, and `murmur pick`\n // usually runs in a popup -- a client of its own, which dies with the popup.\n // Naming the real client is what lets the return outlive the picker.\n clientName() {\n return runTmux([\"display-message\", \"-p\", \"#{client_name}\"]) || null;\n },\n\n // Where the jump started, as a switch-client target. Window-level, not just\n // the session: coming back to the right session but the wrong window is\n // still the wrong place. The window id is stable where its index is not,\n // since renumber-windows renumbers on every close.\n currentTarget() {\n return runTmux([\"display-message\", \"-p\", \"#{session_name}:#{window_id}\"]) || null;\n },\n\n // Whether a wrapper session for this host already exists. Deliberately not\n // returning an id: a session is addressed by name, so a `#{session_id}` would\n // only have to be turned back into one.\n sessionNamed(name) {\n const out = runTmux([\"list-sessions\", \"-F\", \"#{session_name}\"]);\n if (out === null) return false;\n return out.split(\"\\n\").includes(name);\n },\n\n newSession(name, command) {\n // Detached, because the caller sets the per-session options before showing\n // it. Creating it attached would paint one frame with the local status bar\n // up and the local prefix live, which is the flicker this design exists to\n // remove.\n return runTmux([\"new-session\", \"-d\", \"-s\", name, command]) !== null;\n },\n\n setSessionOption(session, option, value) {\n runTmux([\"set-option\", \"-t\", exactPaneTarget(session), option, value]);\n },\n\n switchClient(client, session) {\n const target = exactSession(session);\n const args = client\n ? [\"switch-client\", \"-c\", client, \"-t\", target]\n : [\"switch-client\", \"-t\", target];\n return runTmux(args) !== null;\n },\n\n // The window a pane belongs to, for a pane murmur holds no row for. Clearing\n // a badge is a tmux operation and does not require murmur to own the pane.\n windowForPane(pane) {\n const out = runTmux([\"display-message\", \"-t\", pane, \"-p\", \"#{window_id}\"]);\n return out ? asWindowId(out) : 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 { asPaneId, asSessionId, asWindowId } from \"./ids.js\";\nimport type {\n Activity,\n AttentionKind,\n Driver,\n Snapshot,\n SnapshotAgent,\n SnapshotAttention,\n SnapshotPane,\n} from \"./types.js\";\n\n/**\n * A peer answered, and what it said is not a snapshot.\n *\n * A distinct type because the collector must be able to tell this from an\n * unreachable host: a node that serves a bad document is REACHABLE BUT BROKEN,\n * and an operator needs to see that rather than \"asleep, probably\".\n */\nexport class SnapshotInvalidError extends Error {\n constructor(\n readonly path: string,\n detail: string,\n ) {\n // An EMPTY path means the failure is about the document as a whole, not\n // about a field in it, so there is nothing to prefix. Joining regardless\n // produced `bubba: : not JSON (...)` in `peer list` and in the one line\n // `murmur collect` prints -- measured against a real second node, and for\n // the most common remote misconfiguration there is (murmur missing, so the\n // \"document\" is a shell error). `path` itself stays \"\", because that is what\n // it means and a caller must not have to know a sentinel.\n super(path === \"\" ? detail : `${path}: ${detail}`);\n this.name = \"SnapshotInvalidError\";\n }\n}\n\nfunction fail(path: string, detail: string): never {\n throw new SnapshotInvalidError(path, detail);\n}\n\n/**\n * Exactly these keys, no more and no fewer.\n *\n * Unknown keys are rejected rather than carried, and nothing is coerced or\n * defaulted: validation happens BEFORE storage, so no unknown value can reach a\n * sort, a count or a render path.\n */\nfunction object(value: unknown, path: string, keys: readonly string[]): Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n fail(path, \"expected an object\");\n }\n const record = value as Record<string, unknown>;\n for (const key of keys) if (!(key in record)) fail(path, `missing key ${key}`);\n for (const key of Object.keys(record)) {\n if (!keys.includes(key)) fail(path, `unknown key ${key}`);\n }\n return record;\n}\n\nfunction text(value: unknown, path: string): string {\n if (typeof value !== \"string\" || value === \"\") fail(path, \"expected a non-empty string\");\n return value;\n}\n\nfunction textOrNull(value: unknown, path: string): string | null {\n if (value === null) return null;\n if (typeof value !== \"string\") fail(path, \"expected a string or null\");\n return value;\n}\n\nfunction anyText(value: unknown, path: string): string {\n if (typeof value !== \"string\") fail(path, \"expected a string\");\n return value;\n}\n\nfunction timestamp(value: unknown, path: string): number {\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < 0) {\n fail(path, \"expected a non-negative integer\");\n }\n return value;\n}\n\nfunction member<T extends string>(value: unknown, path: string, allowed: readonly T[]): T {\n if (typeof value !== \"string\" || !allowed.includes(value as T)) {\n fail(path, `expected one of ${allowed.join(\", \")}`);\n }\n return value as T;\n}\n\nconst ACTIVITIES: readonly Activity[] = [\"running\", \"stopped\"];\nconst DRIVERS: readonly Driver[] = [\"human\", \"orchestrated\"];\nconst KINDS: readonly AttentionKind[] = [\"done\", \"blocked\", \"crashed\"];\n\nconst TOP_KEYS = [\n \"murmur_snapshot\",\n \"host_id\",\n \"display_name\",\n \"murmur_version\",\n \"generated_at\",\n \"panes\",\n] as const;\nconst PANE_KEYS = [\n \"pane\",\n \"session\",\n \"window\",\n \"session_name\",\n \"window_name\",\n \"agent\",\n \"attention\",\n] as const;\nconst AGENT_KEYS = [\n \"agent_id\",\n \"activity\",\n \"agent_name\",\n \"pi_session\",\n \"workstream\",\n \"role\",\n \"cli\",\n \"driver\",\n \"claimed_at\",\n \"updated_at\",\n] as const;\nconst ATTENTION_KEYS = [\"kind\", \"message\", \"source\", \"requested_at\"] as const;\n\nfunction parseAgent(value: unknown, path: string): SnapshotAgent | null {\n if (value === null) return null;\n const row = object(value, path, AGENT_KEYS);\n return {\n agent_id: text(row.agent_id, `${path}.agent_id`),\n activity: member(row.activity, `${path}.activity`, ACTIVITIES),\n agent_name: textOrNull(row.agent_name, `${path}.agent_name`),\n pi_session: textOrNull(row.pi_session, `${path}.pi_session`),\n workstream: textOrNull(row.workstream, `${path}.workstream`),\n role: textOrNull(row.role, `${path}.role`),\n cli: text(row.cli, `${path}.cli`),\n driver: member(row.driver, `${path}.driver`, DRIVERS),\n claimed_at: timestamp(row.claimed_at, `${path}.claimed_at`),\n updated_at: timestamp(row.updated_at, `${path}.updated_at`),\n };\n}\n\nfunction parseAttention(value: unknown, path: string): SnapshotAttention[] {\n if (!Array.isArray(value)) fail(path, \"expected an array\");\n const seen = new Set<AttentionKind>();\n return value.map((entry, index) => {\n const at = `${path}[${index}]`;\n const row = object(entry, at, ATTENTION_KEYS);\n const kind = member(row.kind, `${at}.kind`, KINDS);\n if (seen.has(kind)) fail(`${at}.kind`, `duplicate kind ${kind} for this pane`);\n seen.add(kind);\n return {\n kind,\n message: anyText(row.message, `${at}.message`),\n source: anyText(row.source, `${at}.source`),\n requested_at: timestamp(row.requested_at, `${at}.requested_at`),\n };\n });\n}\n\nfunction parsePane(value: unknown, path: string): SnapshotPane {\n const row = object(value, path, PANE_KEYS);\n const agent = parseAgent(row.agent, `${path}.agent`);\n const attention = parseAttention(row.attention, `${path}.attention`);\n // Rule 3 of the document schema: a pane with neither is not a pane worth\n // publishing, so a document carrying one is malformed rather than merely\n // noisy.\n if (agent === null && attention.length === 0) {\n fail(path, \"a pane with no agent and no attention must not be emitted\");\n }\n return {\n pane: asPaneId(text(row.pane, `${path}.pane`)),\n session: asSessionId(text(row.session, `${path}.session`)),\n window: asWindowId(text(row.window, `${path}.window`)),\n session_name: textOrNull(row.session_name, `${path}.session_name`),\n window_name: textOrNull(row.window_name, `${path}.window_name`),\n agent,\n attention,\n };\n}\n\n/**\n * Parse and totally validate one snapshot document.\n *\n * `murmur_snapshot` must be exactly 1: a higher value is rejected too, because\n * forward compatibility is not offered here and a version mismatch is an\n * operator-visible pairing problem. Saying so is the honest report; guessing at\n * a newer document's meaning is not.\n */\nexport function parseSnapshot(input: string): Snapshot {\n let parsed: unknown;\n try {\n parsed = JSON.parse(input);\n } catch (error) {\n fail(\"\", `not JSON (${error instanceof Error ? error.message : String(error)})`);\n }\n const top = object(parsed, \"\", TOP_KEYS);\n if (top.murmur_snapshot !== 1) {\n fail(\"murmur_snapshot\", `expected 1, got ${JSON.stringify(top.murmur_snapshot)}`);\n }\n if (!Array.isArray(top.panes)) fail(\"panes\", \"expected an array\");\n\n const panes = top.panes.map((entry, index) => parsePane(entry, `panes[${index}]`));\n const seen = new Set<string>();\n for (const pane of panes) {\n if (seen.has(pane.pane)) fail(\"panes\", `duplicate pane ${pane.pane}`);\n seen.add(pane.pane);\n }\n\n return {\n murmur_snapshot: 1,\n host_id: text(top.host_id, \"host_id\"),\n display_name: text(top.display_name, \"display_name\"),\n murmur_version: text(top.murmur_version, \"murmur_version\"),\n generated_at: timestamp(top.generated_at, \"generated_at\"),\n panes,\n };\n}\n","import type { PaneId, SessionId, WindowId } from \"./ids.js\";\n\n/**\n * The three independent facts, as types.\n *\n * `activity` is what the pane's own process says it is doing. `attention` is\n * whether a human is wanted. `freshness` (src/view.ts) is how recently we\n * reached the node that reported. They are three independent fields, never one\n * enum, and absence carries meaning: no attention row means \"nothing to see\",\n * no agent row means \"no agent here\".\n */\nexport type Activity = \"running\" | \"stopped\";\nexport type AttentionKind = \"done\" | \"blocked\" | \"crashed\";\n\n/**\n * Who is waiting on this agent -- a human, or a supervisor that consumes the\n * result. Not \"which harness\"; that is `cli`.\n */\nexport type Driver = \"human\" | \"orchestrated\";\n\nexport const DEFAULT_DRIVER: Driver = \"human\";\n\n/**\n * Where a pane currently lives. Location, never identity.\n *\n * `pane` is the address and is stable for the life of the pane; `session` and\n * `window` are only where that pane currently is, and both change under\n * move-pane and break-pane. Only a pane may decide whether an agent exists,\n * which is what the brands in ./ids.js enforce.\n */\nexport type Location = {\n session: SessionId;\n window: WindowId;\n pane: PaneId;\n session_name: string | null;\n window_name: string | null;\n};\n\n/** Owner-reported metadata about the agent in a pane. */\nexport type AgentMeta = {\n agent_name: string | null;\n pi_session: string | null;\n workstream: string | null;\n role: string | null;\n cli: string;\n driver: Driver;\n};\n\nexport type PeerRecord = {\n name: string;\n target: string;\n host_id: string | null;\n display_name: string | null;\n /** The whole validated document, or null when we have never parsed one. */\n snapshot: Snapshot | null;\n /** The PEER's clock: when that node built the document. */\n snapshot_at: number | null;\n /** OUR clock: when we last reached it. Freshness is computed from this. */\n fetched_at: number | null;\n last_attempt_at: number | null;\n last_error: string | null;\n murmur_version: string | null;\n /** The peer's `murmur_snapshot` value, i.e. the document version it speaks. */\n snapshot_version: number | null;\n};\n\n/**\n * One node's whole current state. Complete, never a delta: a peer that returns\n * one has said everything it knows, so absence from it is absence.\n */\nexport type Snapshot = {\n murmur_snapshot: 1;\n host_id: string;\n display_name: string;\n murmur_version: string;\n generated_at: number;\n panes: SnapshotPane[];\n};\n\nexport type SnapshotPane = {\n pane: PaneId;\n session: SessionId;\n window: WindowId;\n session_name: string | null;\n window_name: string | null;\n /** Null for an attention-only pane: valid, listable, jumpable. */\n agent: SnapshotAgent | null;\n attention: SnapshotAttention[];\n};\n\nexport type SnapshotAgent = AgentMeta & {\n agent_id: string;\n activity: Activity;\n claimed_at: number;\n updated_at: number;\n};\n\nexport type SnapshotAttention = {\n kind: AttentionKind;\n message: string;\n source: string;\n requested_at: number;\n};\n\n/**\n * Whether a pid is still running. A parameter everywhere it is consulted, so a\n * test needs no process table.\n */\nexport type LiveCheck = (pid: number) => boolean;\n\nexport type AgentClaim = {\n location: Location;\n owner_pid: number;\n meta: AgentMeta;\n now?: number;\n isAlive?: LiveCheck;\n};\n\nexport type ClaimResult =\n | { outcome: \"claimed\"; agent_id: string }\n | { outcome: \"retained\"; agent_id: string }\n | { outcome: \"replaced\"; agent_id: string; previous_agent_id: string }\n | { outcome: \"refused\"; held_by_pid: number };\n\nexport type ActivityUpdate = {\n agent_id: string;\n owner_pid: number;\n activity: Activity;\n location: Location;\n now?: number;\n};\n\nexport type AgentRelease = { agent_id: string; owner_pid: number };\n\n/**\n * Everything an attention writer may say. There is no agent_id, no owner_pid,\n * no activity and no owner metadata field, and adding one is a contract change.\n */\nexport type AttentionRequest = {\n kind: AttentionKind;\n location: Location;\n message: string;\n source: string;\n now?: number;\n};\n\n/**\n * The only local facts reconciliation is allowed to consult.\n *\n * `panes` is null when tmux could not answer, which is not evidence of death.\n * `isAlive` and `now` are parameters so a test needs no process table and no\n * clock control.\n */\nexport type LocalWorld = {\n panes: Set<PaneId> | null;\n isAlive?: LiveCheck;\n now?: number;\n};\n\nexport type ReconcileSummary = {\n crashed: PaneId[];\n removed: PaneId[];\n attention_removed: PaneId[];\n};\n\nexport type PeerFetch =\n | { ok: true; snapshot: Snapshot; at: number }\n | { ok: false; error: string; at: number };\n","import type { NodeIdentity } from \"./identity.js\";\nimport type { PaneId, SessionId, WindowId } from \"./ids.js\";\nimport type { Store } from \"./store.js\";\nimport {\n type Activity,\n type AttentionKind,\n DEFAULT_DRIVER,\n type Driver,\n type SnapshotPane,\n} from \"./types.js\";\n\nexport type Freshness = \"fresh\" | \"stale\";\n\n/**\n * What a surface paints. Presentation only, derived from the three independent\n * facts and never stored.\n */\nexport type RenderState = \"crashed\" | \"blocked\" | \"done\" | \"running\" | \"idle\";\n\n/**\n * THE single ordering table: which state matters most, for sorting and for\n * choosing one word to show.\n *\n * `status.ts` and `pick.ts` import this rather than declaring their own copies,\n * so no two surfaces can sort one list differently.\n */\nexport const RENDER_PRIORITY: readonly RenderState[] = [\n \"crashed\",\n \"blocked\",\n \"done\",\n \"running\",\n \"idle\",\n];\n\n/**\n * The attention kinds only a human can answer, and the second table both\n * surfaces must agree on.\n *\n * `blocked` means waiting for an answer an orchestrator cannot give -- mu places\n * work, it cannot choose between two approaches. `crashed` means the process\n * died, which a supervisor may or may not retry. Everything else about an\n * orchestrated agent is its supervisor's business.\n *\n * `pick.ts` uses it to decide which crew rows are visible by default and\n * `status.ts` to decide which crew states reach the status bar. They were two\n * literals in two files answering one question, which is how a row that needed a\n * human became one a human could not see.\n */\nexport const NEEDS_HUMAN: readonly AttentionKind[] = [\"blocked\", \"crashed\"];\n\n/**\n * One pane, as every surface reads it: address, the three independent facts,\n * owner metadata, and ages.\n *\n * Local and remote panes are the same type, built by the same mapping, because\n * `Store.localPanes()` and a peer's cached snapshot both return\n * `SnapshotPane[]`. One mapping means local and remote cannot drift apart.\n */\nexport type PaneView = {\n // address\n host_id: string;\n /** The name the operator typed, or this node's display_name. */\n host: string;\n local: boolean;\n pane: PaneId;\n session: SessionId;\n window: WindowId;\n session_name: string | null;\n window_name: string | null;\n // the three independent facts\n /** Null for an attention-only pane, which has no agent row. */\n activity: Activity | null;\n attention: AttentionKind[];\n freshness: Freshness;\n // owner-reported metadata, null for an attention-only pane\n agent_id: string | null;\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;\n // ages\n /** When the pane's own node last said something. Never `fetched_at`. */\n updated_at: number | null;\n /** When that node generated its snapshot. Null for local. */\n snapshot_at: number | null;\n /** When we last reached that node. Null for local. */\n fetched_at: number | null;\n};\n\n/**\n * How long a peer may go unfetched before its panes render stale.\n *\n * Re-exported from here rather than imported from the collector by view\n * consumers, so freshness has one definition. See collector.ts for why sixty\n * seconds.\n */\nexport const STALENESS_MS = 60_000;\n\n/**\n * A duration as the shortest thing worth reading: \"5m\", \"2h\", \"3d\".\n *\n * Under a minute is the empty string: an age that changes every second is noise\n * in a status column. This and `freshness` are the only two places a duration\n * becomes text or a verdict.\n */\nexport function age(ms: number | null): string {\n if (ms === null || ms < 60_000) return \"\";\n if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;\n if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h`;\n return `${Math.floor(ms / 86_400_000)}d`;\n}\n\n/**\n * Freshness of a NODE, never of an agent.\n *\n * A peer we have never reached is stale rather than fresh: null means the first\n * collect has not succeeded yet, and an unreachable host you just added must not\n * render as up to date.\n */\nexport function freshness(\n fetchedAt: number | null,\n now: number,\n thresholdMs = STALENESS_MS,\n): Freshness {\n return fetchedAt !== null && now - fetchedAt <= thresholdMs ? \"fresh\" : \"stale\";\n}\n\n/**\n * One word for a pane. Attention wins over activity, because attention is a\n * request and activity is a description.\n *\n * A running agent with `blocked` attention is a valid and expected state, and\n * surfaces that can show both, do — this is only for the ones that must pick.\n */\nexport function renderState(view: Pick<PaneView, \"activity\" | \"attention\">): RenderState {\n for (const kind of [\"crashed\", \"blocked\", \"done\"] as const) {\n if (view.attention.includes(kind)) return kind;\n }\n return view.activity === \"running\" ? \"running\" : \"idle\";\n}\n\n/** The newest attention request on a pane, for the `updated_at` of one with no agent. */\nfunction newestAttention(pane: SnapshotPane): number | null {\n let newest: number | null = null;\n for (const entry of pane.attention) {\n if (newest === null || entry.requested_at > newest) newest = entry.requested_at;\n }\n return newest;\n}\n\ntype ViewSource = {\n host_id: string;\n host: string;\n local: boolean;\n freshness: Freshness;\n snapshot_at: number | null;\n fetched_at: number | null;\n};\n\nfunction paneView(pane: SnapshotPane, source: ViewSource): PaneView {\n const agent = pane.agent;\n return {\n host_id: source.host_id,\n host: source.host,\n local: source.local,\n pane: pane.pane,\n session: pane.session,\n window: pane.window,\n session_name: pane.session_name,\n window_name: pane.window_name,\n activity: agent?.activity ?? null,\n attention: pane.attention.map((entry) => entry.kind),\n freshness: source.freshness,\n agent_id: agent?.agent_id ?? null,\n agent_name: agent?.agent_name ?? null,\n pi_session: agent?.pi_session ?? null,\n workstream: agent?.workstream ?? null,\n role: agent?.role ?? null,\n cli: agent?.cli ?? null,\n driver: agent?.driver ?? DEFAULT_DRIVER,\n updated_at: agent?.updated_at ?? newestAttention(pane),\n snapshot_at: source.snapshot_at,\n fetched_at: source.fetched_at,\n };\n}\n\n/**\n * Every pane this node knows about: its own, plus one cached snapshot per peer.\n *\n * `identity` is non-null because every caller is a command that already requires\n * `murmur init`, so no pane can be misclassified as remote by an absent one.\n *\n * No liveness is probed here, for local or remote. A remote pane's `activity` is\n * whatever its own node last said; a stale node keeps its last-known fields\n * verbatim beside an explicit warning.\n */\nexport function paneViews(store: Store, identity: NodeIdentity, now = Date.now()): PaneView[] {\n const views = store.localPanes().map((pane) =>\n paneView(pane, {\n host_id: identity.host_id,\n host: identity.display_name,\n local: true,\n // Local panes are always fresh: we are the node that authored them.\n freshness: \"fresh\",\n snapshot_at: null,\n fetched_at: null,\n }),\n );\n\n for (const peer of store.peers()) {\n const snapshot = peer.snapshot;\n if (!snapshot) continue;\n const source: ViewSource = {\n host_id: snapshot.host_id,\n // The name the human typed, not the machine's self-reported hostname: a\n // peer added as `linuxpc` can report a container id, which appears\n // nowhere else in the tool and cannot be typed at `peer remove`.\n host: peer.name,\n local: false,\n freshness: freshness(peer.fetched_at, now),\n snapshot_at: peer.snapshot_at,\n fetched_at: peer.fetched_at,\n };\n for (const pane of snapshot.panes) views.push(paneView(pane, source));\n }\n\n return views;\n}\n\nconst ORDER = new Map<RenderState, number>(RENDER_PRIORITY.map((state, index) => [state, index]));\n\n/**\n * Attention-first ordering, then the newest news, then address.\n *\n * TOTAL on purpose, and that is the whole reason the last two comparisons\n * exist. Ties on state and age are ordinary rather than exotic -- a pair of\n * crashed panes reconciled in one transaction shares a `requested_at` exactly --\n * and `Array.prototype.sort` is stable only with respect to the order it was\n * GIVEN, which here is whatever SQLite and the peer loop happened to produce. An\n * unbroken tie therefore makes the list depend on that order: a status bar\n * reshuffles between two identical ticks, and a picker row moves under the\n * keypress that was aimed at it.\n *\n * Presentation only. No caller may read meaning into the position of a row --\n * pane order in a snapshot carries none either, so a reader sorts for itself\n * rather than trusting what it was served.\n */\nexport function viewSort(views: PaneView[]): PaneView[] {\n return [...views].sort((left, right) => {\n const byState = (ORDER.get(renderState(left)) ?? 99) - (ORDER.get(renderState(right)) ?? 99);\n if (byState !== 0) return byState;\n // Unknown age sorts last within its state: an attention-only pane with no\n // timestamp is not news, and 0 is older than any real clock reading.\n const byAge = (right.updated_at ?? 0) - (left.updated_at ?? 0);\n if (byAge !== 0) return byAge;\n // Address as the final key, because it is the only field guaranteed unique\n // across the whole view: `pane` is unique per node and `host` per peer.\n const byHost = left.host.localeCompare(right.host);\n return byHost !== 0 ? byHost : left.pane.localeCompare(right.pane);\n });\n}\n","import type { Channel } from \"./channel.js\";\nimport { type Mux, tmux } from \"./mux.js\";\nimport { parseSnapshot, SnapshotInvalidError } from \"./snapshot.js\";\nimport type { Store } from \"./store.js\";\nimport type { PeerRecord } from \"./types.js\";\nimport { STALENESS_MS } from \"./view.js\";\n\nexport { STALENESS_MS };\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. The per-peer ssh timeout applies\n// once per wave, so nine unreachable peers cost two waves: the pool serialises\n// the timeouts it exists to limit. So the whole collect gets its own deadline,\n// independent of peer count. Peers still in flight when it expires are\n// abandoned and render stale, which is already the designed outcome for a host\n// 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.\nconst COLLECT_DEADLINE_MS = 4_000;\n\n/**\n * How recently a peer may have been attempted before an ambient collect skips\n * it.\n *\n * Collection is driven by the tmux status bar re-running `murmur status`, and\n * every run fetched every peer. That ties fetch rate to REDRAW rate, which is a\n * category error: the status bar's job is to repaint, not to decide how often to\n * reach a machine. It is also quadratic in a mesh -- N nodes each fetching N-1\n * peers is N*(N-1) ssh processes per tick, fleet-wide -- and it multiplies by\n * attached tmux clients, since `status-interval` fires per client. The payload\n * was never the problem (measured: ~400 bytes per pane); the forked ssh process\n * per peer per tick is.\n *\n * Thirty seconds, and the ceiling is not arbitrary: it must stay safely under\n * STALENESS_MS, or the floor itself would drive a REACHABLE peer into `stale`\n * and the HUD would flap. At half the staleness window a peer has to miss two\n * consecutive attempts before it reads stale, which is the same belt-and-braces\n * relationship CONNECT_TIMEOUT_S has with EXEC_TIMEOUT_MS.\n *\n * A constant rather than a knob, per the zero-configuration rule: a wrong value\n * here costs a release, not a silently broken user setup.\n */\nexport const COLLECT_FLOOR_MS = 30_000;\n\n/**\n * Width of the window the floor is drawn from, centred on COLLECT_FLOOR_MS.\n * Twenty seconds, so a peer is due somewhere in [20s, 40s].\n *\n * A bare floor is a SYNCHRONISER, which is worse than no floor at a hub. Every\n * node fetches, waits exactly the same interval, and fetches again, so a fleet\n * converges on hitting one machine in the same instant forever -- and the\n * convergence is sticky, because the collect that answers them all is also what\n * resets all their clocks together.\n *\n * Simulated, 20 spokes against one hub, peak simultaneous fetches per tick:\n *\n * no jitter 20 / 20\n * fixed per-peer offset 14-18 / 20\n * this (uniform +/-10s) 8-10 / 20\n *\n * A FIXED offset per peer -- hashed from a host id, say -- barely helps, and the\n * reason is the tick grid. A peer is only tested when the status bar runs, every\n * `status-interval`, so its effective period is (floor + offset) rounded up to a\n * multiple of the tick. A fixed offset collapses into a handful of distinct\n * periods (measured: 3 periods for 20 spokes at +/-15s), and peers sharing a\n * period then collide on every cycle forever. Fixed jitter does not break a\n * herd, it re-partitions it into smaller permanent herds. Fresh randomness\n * re-draws the period every cycle, so a group that collides once scatters next\n * time.\n *\n * Symmetric rather than added on top, so the MEAN stays at the floor -- a\n * one-sided [floor, floor+span] window would quietly stretch the average period\n * to 45s and make every peer's data older to buy the same spread.\n *\n * Uniform rather than normal: a bell curve concentrates mass near the mean,\n * which is precisely the opposite of spreading, and its unbounded tails would\n * need clamping -- which piles probability exactly on the bound.\n *\n * The span is capped by staleness, and this is the load-bearing constraint:\n * COLLECT_FLOOR_MS + COLLECT_JITTER_MS / 2 must stay under STALENESS_MS, or an\n * unlucky draw pushes a REACHABLE peer over the staleness line and the HUD\n * flaps between fresh and stale. 30s + 10s = 40s leaves 20s of headroom.\n */\nexport const COLLECT_JITTER_MS = 20_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 *\n * Exported for `doctor`, which fans out the same way over the same peers and\n * must not grow a second pool: a private copy there would be a second answer to\n * \"how many ssh processes may murmur have in flight\", and the two would drift.\n * The DEADLINE is the caller's, because that is the one thing the two surfaces\n * genuinely disagree about -- see DOCTOR_DEADLINE_MS.\n */\nexport async 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\n/**\n * The optional half of a collect, as a bag rather than four positional\n * arguments.\n *\n * `collect(store, ssh, now, undefined, mux)` was already the call site before\n * the floor was added, and a fifth positional -- a bare number, next to another\n * bare number -- is how `now` and `floorMs` get silently swapped.\n */\nexport type CollectOptions = {\n /** Bounds the whole run. Injectable so tests need not wait out real time. */\n deadline?: Promise<void>;\n /** The mux reconciliation asks which panes are alive. */\n mux?: Mux;\n /**\n * Skip peers attempted within this many ms. Zero -- the default -- fetches\n * every peer, which is what a deliberate `murmur collect` and the picker both\n * want. Only the status bar, which repaints on a timer, passes a floor.\n *\n * Opt IN rather than opt out: a surface that forgets this argument keeps the\n * old always-fetch behaviour, which is merely wasteful. The opposite default\n * would mean a new surface silently serves stale data.\n */\n floorMs?: number;\n /**\n * Source of the floor's jitter, in [0, 1). Injected for the same reason `now`\n * and `isAlive` are: a test pins both edges of the window by returning 0 and\n * a value approaching 1, rather than sampling and hoping.\n */\n random?: () => number;\n};\n\n/**\n * The peers an ambient collect should actually reach this run.\n *\n * Keyed on `last_attempt_at`, not `fetched_at`: the point is to bound how often\n * we ATTEMPT a machine, and an unreachable peer is the expensive case -- it\n * costs a forked ssh that sits until ConnectTimeout. Keying on the successful\n * fetch would exempt exactly the sleeping laptops the floor exists to stop\n * hammering.\n *\n * A peer never attempted (`null`) is always due, so a freshly added peer appears\n * without waiting out a floor.\n *\n * The jitter is drawn PER PEER PER CALL, which is what breaks a herd rather than\n * merely reshaping it -- see COLLECT_JITTER_MS. It applies only when a floor is\n * set: an unfloored collect is a person asking for the state now, and a random\n * skip there would be a keypress that sometimes silently does nothing.\n */\nfunction duePeers(\n peers: readonly PeerRecord[],\n now: number,\n floorMs: number,\n random: () => number,\n): PeerRecord[] {\n if (floorMs <= 0) return [...peers];\n return peers.filter((peer) => {\n if (peer.last_attempt_at === null) return true;\n // Centred on the floor: [-span/2, +span/2).\n const jitter = (random() - 0.5) * COLLECT_JITTER_MS;\n return now - peer.last_attempt_at >= floorMs + jitter;\n });\n}\n\nexport type CollectResult = {\n peer: string;\n ok: boolean;\n /** Panes in the snapshot we just stored. Zero is a normal, valid answer. */\n panes: number;\n error?: string;\n /**\n * True when the peer could not be reached at all, as opposed to answering\n * with something wrong.\n *\n * A fleet normally has nodes that are asleep or switched off, so this is the\n * expected outcome rather than a fault, and callers use it to stay quiet\n * about the ordinary case while still reporting a peer that is reachable but\n * broken -- a bad snapshot version, a missing binary, an auth problem.\n */\n unreachable?: boolean;\n};\n\n/**\n * Whether an error means \"could not reach the host\".\n *\n * ssh exits 255 for its own failures and prints a recognisable line, and the\n * exec wrapper puts both in the message. Matching on the text is unpleasant but\n * it is the only signal available: the channel seam returns an Error, not an\n * exit status.\n *\n * `Permission denied` is deliberately NOT here. An auth misconfiguration is\n * reachable-but-broken and an operator task; classing it as \"asleep, probably\"\n * is how a fixable setup error stays invisible for weeks.\n */\nfunction isUnreachable(message: string): boolean {\n return (\n /Host is down|No route to host|Connection refused|Connection timed out|Connection closed|Operation timed out|Network is unreachable|Name or service not known|Could not resolve hostname|timed out after/i.test(\n message,\n ) || /\\bssh:/.test(message)\n );\n}\n\n/**\n * Drop Node's `Command failed: <argv>` first line, keeping the child's output.\n *\n * Every rejection from the ssh channel arrives in that shape, so the first ~140\n * characters of every real failure are the invocation murmur chose: `ssh -o\n * BatchMode=yes -o ControlMaster=no -o ControlPath=... -o ConnectTimeout=1\n * <host> murmur export`. The operator cannot act on any of it, and it pushed the\n * one line that mattered past the length bound below -- measured against a real\n * second node, where a missing remote binary printed\n * `bubba: Command failed: ssh -o BatchMode=yes ... murmur: command not f...`,\n * truncated on the only actionable word in it.\n *\n * Stripped BEFORE the newlines are collapsed, because the line boundary is the\n * only thing separating the invocation from the diagnosis.\n */\nfunction stripInvocation(message: string): string {\n const firstLine = message.indexOf(\"\\n\");\n if (firstLine === -1 || !message.startsWith(\"Command failed:\")) return message;\n const rest = message.slice(firstLine + 1).trim();\n // A bare `Command failed:` line with nothing after it is all we have; saying\n // nothing would be worse than saying too much.\n return rest === \"\" ? message : rest;\n}\n\n/**\n * The one normalisation, so classification and rendering cannot disagree.\n *\n * `unreachable` (a machine-readable flag on `CollectResult`) and\n * `describeFailure` (the line a human reads) both classify with\n * `isUnreachable`. Feeding them differently-normalised text is how a peer gets\n * reported as reachable-but-broken in JSON and \"unreachable\" in print, about\n * one fetch -- so both go through here.\n */\nfunction normalizeFailure(message: string): string {\n return stripInvocation(message).replace(/\\s+/g, \" \").trim();\n}\n\n/**\n * A peer failure in one line a human can act on.\n *\n * The raw error was the whole ssh invocation plus ssh's own message -- over 200\n * characters, of which the actionable part was the host name. It also leaked\n * every ssh option murmur passes, which a user cannot do anything about.\n */\nexport function describeFailure(peer: string, message: string): string {\n const collapsed = normalizeFailure(message);\n if (isUnreachable(collapsed)) {\n const reason = /ssh: (?:connect to host \\S+ port \\d+: )?(.+?)(?: \\(|$)/i.exec(collapsed);\n return `${peer}: unreachable (${(reason?.[1] ?? \"ssh failed\").trim()})`;\n }\n // Reachable but wrong: keep the message, since it is the diagnosis, but bound\n // it so a corrupt snapshot cannot print a screenful.\n const detail = collapsed.length > 160 ? `${collapsed.slice(0, 157)}...` : collapsed;\n return `${peer}: ${detail}`;\n}\n\n/**\n * Fetch every peer's snapshot, validate it, and replace the cache whole.\n *\n * Concurrent because an unreachable peer costs the full ssh timeout, and a\n * serial loop charged that to every peer behind it: three asleep laptops made\n * `murmur status` hang for thirty seconds. Applied serially in peer order,\n * because better-sqlite3 is synchronous and a stable order keeps the result list\n * aligned with `store.peers()`.\n *\n * One round trip per peer, and never a second: the document is complete, so what\n * arrives either replaces the cache entirely or does not touch it.\n */\nexport async function collect(\n store: Store,\n channel: Channel,\n now = Date.now(),\n options: CollectOptions = {},\n): Promise<CollectResult[]> {\n const { deadline, mux = tmux, floorMs = 0, random = Math.random } = options;\n const results: CollectResult[] = [];\n let timer: NodeJS.Timeout | undefined;\n try {\n // Only the peers due a fetch are passed to the pool, so a skipped peer costs\n // no ssh, no slot and no result row.\n const peers = duePeers(store.peers(), now, floorMs, random);\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.\n const fetches = await mapSettled(\n peers,\n MAX_CONCURRENT_PEERS,\n async (peer) => parseSnapshot(await channel.exec(peer.target, [\"murmur\", \"export\"])),\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 // Every field the cache derives comes out of the document itself, so\n // the cache structurally cannot disagree with the snapshot it holds.\n store.replacePeerSnapshot(peer.name, { ok: true, snapshot: fetch.value, at: now });\n results.push({ peer: peer.name, ok: true, panes: fetch.value.panes.length });\n } catch (error) {\n // Normalised ONCE, here, before it is stored or returned.\n //\n // `last_error` is read by `peer list`, by `status --json` and by\n // anything built on the SDK, and none of them can undo the mangling: a\n // raw `execFile` rejection leads with `Command failed: ssh -o\n // BatchMode=yes -o ControlMaster=no -o ControlPath=... <host> murmur\n // export`, which is murmur's own invocation and nothing an operator can\n // act on. Measured against a real second node, where `peer list`\n // printed 140 characters of ssh options before the four words that\n // mattered. Storing the normalised text means every surface gets the\n // diagnosis without each one having to remember to strip it.\n const message = normalizeFailure(error instanceof Error ? error.message : String(error));\n store.replacePeerSnapshot(peer.name, { ok: false, error: message, at: now });\n // Reported through the return value, never printed here. `collect` runs\n // from `murmur status` on every status-bar tick, and from `pick` inside\n // a display-popup, so a single sleeping laptop wrote to stderr forever\n // and corrupted both. Only the `collect` command -- which a human ran\n // on purpose -- prints.\n results.push({\n peer: peer.name,\n ok: false,\n panes: 0,\n error: message,\n // A peer that answered with a bad document is reachable but broken,\n // and must be visibly so rather than silently stale.\n unreachable:\n error instanceof SnapshotInvalidError\n ? false\n : isUnreachable(normalizeFailure(message)),\n });\n }\n }\n } catch (error) {\n // The whole collect failed rather than one peer -- a broken peer table, say.\n // Still not printed: the caller decides.\n results.push({\n peer: \"\",\n ok: false,\n panes: 0,\n error: error instanceof Error ? error.message : String(error),\n });\n } finally {\n clearTimeout(timer);\n }\n\n // The only housekeeping left, and it runs once per invocation including with\n // zero peers -- which is why it is here rather than on `export`, which only\n // runs when a peer asks over ssh. A single-machine node would otherwise\n // reconcile never. Idempotent, so `buildLocalSnapshot` calling it too is a\n // cheap repeat rather than a second policy.\n try {\n store.reconcileLocal({ panes: mux.livePanes(), now });\n } catch {\n // Housekeeping must not fail a command, and it must not report either.\n }\n return results;\n}\n","import { execFileSync } from \"node:child_process\";\nimport { SSH_OPTIONS } from \"./channel.js\";\nimport { tmux } from \"./mux.js\";\nimport type { Store } from \"./store.js\";\nimport type { PaneView } from \"./view.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: PaneView, lines = GLANCE_LINES): string | null {\n if (agent.local) 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 { 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\nfunction identityPath(): string {\n return join(stateDir(), \"identity.json\");\n}\n\n/**\n * Memoized per process, keyed on the resolved path.\n *\n * `identity.json` cannot change under a running command, and the audit measured\n * eight redundant reads per invocation. Keyed on the path rather than a bare\n * boolean so a test that repoints `MURMUR_STATE_DIR` mid-process is not served\n * another directory's identity.\n */\nlet cache: { path: string; identity: NodeIdentity | null } | null = null;\n\n/**\n * This node's identity, or null when it has none.\n *\n * A READ, and only a read: nothing mints here. Every command that needs a\n * host_id fails with \"murmur is not initialised on this node; run: murmur init\"\n * rather than bringing a node into existence as a side effect of a status-bar\n * tick.\n */\nexport function loadIdentity(): NodeIdentity | null {\n const path = identityPath();\n if (cache?.path === path) return cache.identity;\n const identity = existsSync(path)\n ? (JSON.parse(readFileSync(path, \"utf8\")) as NodeIdentity)\n : null;\n cache = { path, identity };\n return identity;\n}\n\nfunction write(identity: NodeIdentity): NodeIdentity {\n mkdirSync(stateDir(), { recursive: true });\n writeFileSync(identityPath(), `${JSON.stringify(identity, null, 2)}\\n`);\n cache = { path: identityPath(), identity };\n return identity;\n}\n\n/** Create this node's identity. Only `murmur init` calls it. */\nexport function createIdentity(displayName = hostname()): NodeIdentity {\n if (loadIdentity()) throw new Error(`identity already exists: ${identityPath()}`);\n return write({ host_id: randomUUID(), display_name: displayName });\n}\n\n/**\n * Rename an existing node, keeping its `host_id`.\n *\n * `murmur init --name` on an already-initialised node used to ignore the flag\n * silently, which is the one thing a rename must not do.\n */\nexport function setDisplayName(displayName: string): NodeIdentity {\n const existing = loadIdentity();\n return write(\n existing\n ? { host_id: existing.host_id, display_name: displayName }\n : { host_id: randomUUID(), display_name: displayName },\n );\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\n/** The current-state database. The only database murmur holds. */\nexport function dbPath(): string {\n return join(stateDir(), \"state.db\");\n}\n","import { type Channel, ssh } from \"./channel.js\";\nimport { type CollectOptions, collect } from \"./collector.js\";\nimport type { NodeIdentity } from \"./identity.js\";\nimport type { Store } from \"./store.js\";\nimport {\n freshness,\n NEEDS_HUMAN,\n type PaneView,\n paneViews,\n RENDER_PRIORITY,\n type RenderState,\n renderState,\n viewSort,\n} from \"./view.js\";\n\ntype Counts = Record<RenderState, number>;\n\nexport type Status = {\n counts: Counts;\n orchestrated_counts: Counts;\n panes: PaneView[];\n peers: {\n name: string;\n display_name: string | null;\n fetched_at: number | null;\n snapshot_at: number | null;\n last_error: string | null;\n stale: boolean;\n }[];\n};\n\nfunction emptyCounts(): Counts {\n const counts = {} as Counts;\n for (const state of RENDER_PRIORITY) counts[state] = 0;\n return counts;\n}\n\nexport function tmuxStatus(view: Status): string {\n // Orchestrated agents are counted for the states only a human can answer, and\n // hidden for the rest: a supervisor consumes a `done` worker's result, so\n // nobody needs to acknowledge it, and `running` asks for nothing. The list is\n // `NEEDS_HUMAN` in view.ts, shared with the picker's visibility rule so the\n // status bar and the list cannot disagree about which crew rows matter.\n const needsHuman = new Set<string>(NEEDS_HUMAN);\n const total = (state: RenderState): number =>\n view.counts[state] + (needsHuman.has(state) ? view.orchestrated_counts[state] : 0);\n return (\n RENDER_PRIORITY.filter((state) => total(state) > 0)\n // The tmux renderer's public vocabulary predates the internal activity\n // rename. Keep that external protocol stable until the renderer is updated.\n .map((state) => `${state === \"running\" ? \"working\" : state}\\t${total(state)}\\n`)\n .join(\"\")\n );\n}\n\n/**\n * The current view. Pure with respect to the network: the caller decides whether\n * to collect first (see `statusWithCollect`).\n *\n * `identity` is required rather than resolved here, because every caller is a\n * command that already fails without one.\n */\nexport function status(store: Store, identity: NodeIdentity, now = Date.now()): Status {\n const counts = emptyCounts();\n const orchestratedCounts = emptyCounts();\n const panes = viewSort(paneViews(store, identity, now));\n for (const pane of panes) {\n const target = pane.driver === \"human\" ? counts : orchestratedCounts;\n target[renderState(pane)] += 1;\n }\n\n return {\n counts,\n orchestrated_counts: orchestratedCounts,\n panes,\n peers: store.peers().map((peer) => ({\n name: peer.name,\n display_name: peer.display_name,\n fetched_at: peer.fetched_at,\n // Their clock and ours, separately: a peer polled a second ago can be\n // serving a three-hour-old fact, and one number cannot say both.\n snapshot_at: peer.snapshot_at,\n last_error: peer.last_error,\n // The view's verdict, not a second threshold spelled the same way. A\n // peer we have never reached is stale rather than fresh -- null\n // `fetched_at` means the first collect has not succeeded yet -- and\n // `freshness` is the one place that decides, so this list and the panes\n // the peer contributes cannot disagree about the same host.\n stale: freshness(peer.fetched_at, now) === \"stale\",\n })),\n };\n}\n\n/**\n * Collect from peers, then read. 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. And the callers close the store in a\n * `finally`, so a collect still in flight lands on a closed handle and reports\n * \"The database connection is not open\", which looks like corruption rather\n * than a race.\n *\n * Sync must never fail a command, and on this path it must never print either:\n * `status` runs on every status-bar tick and `pick` runs inside a\n * display-popup, so one sleeping laptop would otherwise write ssh diagnostics\n * to stderr several times a minute, forever. `murmur collect`, which a human\n * runs deliberately, is the only place that prints.\n *\n * `floorMs` is how a caller says whether it is a TIMER or a PERSON. The status\n * bar repaints on `status-interval` and passes COLLECT_FLOOR_MS, so fetch rate\n * stops being tied to redraw rate. The picker passes nothing: pressing the key\n * is a person asking now, and its `^r` reload comes back through here too --\n * a refresh key that skipped the fetch would be a key that silently does\n * nothing.\n */\nexport async function statusWithCollect(\n store: Store,\n identity: NodeIdentity,\n now = Date.now(),\n channel: Channel = ssh,\n options: CollectOptions = {},\n): Promise<Status> {\n try {\n await collect(store, channel, now, options);\n } catch {\n // Total by construction: a read of whatever the cache already holds is\n // always better than no output, and this path has no one to tell.\n }\n return status(store, identity, now);\n}\n","import { randomUUID } from \"node:crypto\";\nimport { mkdirSync, rmSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport Database from \"better-sqlite3\";\nimport type { NodeIdentity } from \"./identity.js\";\nimport type { PaneId } from \"./ids.js\";\nimport { asPaneId, asSessionId, asWindowId } from \"./ids.js\";\nimport { pidAlive } from \"./mux.js\";\nimport { dbPath } from \"./paths.js\";\nimport type {\n ActivityUpdate,\n AgentClaim,\n AgentRelease,\n AttentionKind,\n AttentionRequest,\n ClaimResult,\n LocalWorld,\n PeerFetch,\n PeerRecord,\n ReconcileSummary,\n Snapshot,\n SnapshotAgent,\n SnapshotAttention,\n SnapshotPane,\n} from \"./types.js\";\nimport { MURMUR_VERSION } from \"./version.js\";\nimport { RENDER_PRIORITY } from \"./view.js\";\n\n/**\n * The storage version. Any change to any table bumps it.\n *\n * ONE version strategy: a mismatch salvages the peer names and targets a human\n * typed, deletes the file, and recreates the schema. No ALTER TABLE anywhere, so\n * there is no additive path to forget to use.\n */\nconst SCHEMA_USER_VERSION = 3;\n\nconst SCHEMA = `\n CREATE TABLE agents (\n agent_id TEXT NOT NULL PRIMARY KEY,\n pane TEXT NOT NULL UNIQUE,\n owner_pid INTEGER NOT NULL CHECK (owner_pid > 0),\n activity TEXT NOT NULL CHECK (activity IN ('running', 'stopped')),\n session TEXT NOT NULL,\n window 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 NOT NULL,\n driver TEXT NOT NULL CHECK (driver IN ('human', 'orchestrated')),\n claimed_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n ) STRICT;\n\n CREATE TABLE attention (\n pane TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('done', 'blocked', 'crashed')),\n message TEXT NOT NULL,\n source TEXT NOT NULL,\n session TEXT NOT NULL,\n window TEXT NOT NULL,\n session_name TEXT,\n window_name TEXT,\n requested_at INTEGER NOT NULL,\n PRIMARY KEY (pane, kind)\n ) STRICT;\n\n CREATE TABLE peers (\n name TEXT NOT NULL PRIMARY KEY,\n target TEXT NOT NULL,\n host_id TEXT,\n display_name TEXT,\n snapshot TEXT,\n snapshot_at INTEGER,\n fetched_at INTEGER,\n last_attempt_at INTEGER,\n last_error TEXT,\n murmur_version TEXT,\n snapshot_version INTEGER\n ) STRICT;\n`;\n\n/**\n * The store, and the only place in murmur that holds a database handle or\n * writes SQL.\n *\n * This interface is CLOSED. There is no `append`, no `ingest`, no log read, no\n * partial-row update, and no local read other than `localPanes` — each of those\n * shapes let a writer say something it had no standing to say, and each cost a\n * shipped bug. Attention methods take no agent identity at all, which is what\n * makes \"a notifier cannot corrupt an agent row\" structural.\n */\nexport interface Store {\n // --- agent lifecycle: owner-only, pid-gated -----------------------------\n claimAgent(claim: AgentClaim): ClaimResult;\n setActivity(update: ActivityUpdate): boolean;\n releaseAgent(release: AgentRelease): boolean;\n\n // --- attention: pane-addressed, no agent authority ----------------------\n requestAttention(request: AttentionRequest): void;\n acknowledgePane(pane: PaneId): number;\n\n // --- local truth --------------------------------------------------------\n /** The one local read. Joins agents and attention by pane. No reconciliation. */\n localPanes(): SnapshotPane[];\n reconcileLocal(world: LocalWorld): ReconcileSummary;\n buildLocalSnapshot(identity: NodeIdentity, world: LocalWorld): Snapshot;\n\n // --- peer cache ---------------------------------------------------------\n peers(): PeerRecord[];\n addPeer(name: string, target: string): void;\n removePeer(name: string): boolean;\n replacePeerSnapshot(name: string, fetch: PeerFetch): void;\n\n close(): void;\n}\n\ntype AgentDbRow = {\n agent_id: string;\n pane: string;\n owner_pid: number;\n activity: string;\n session: string;\n window: string;\n session_name: string | null;\n window_name: string | null;\n agent_name: string | null;\n pi_session: string | null;\n workstream: string | null;\n role: string | null;\n cli: string;\n driver: string;\n claimed_at: number;\n updated_at: number;\n};\n\ntype AttentionDbRow = {\n pane: string;\n kind: string;\n message: string;\n source: string;\n session: string;\n window: string;\n session_name: string | null;\n window_name: string | null;\n requested_at: number;\n};\n\ntype PeerDbRow = {\n name: string;\n target: string;\n host_id: string | null;\n display_name: string | null;\n snapshot: string | null;\n snapshot_at: number | null;\n fetched_at: number | null;\n last_attempt_at: number | null;\n last_error: string | null;\n murmur_version: string | null;\n snapshot_version: number | null;\n};\n\n/** Peer names and targets: the two fields a human typed, and all we salvage. */\nfunction salvagePeers(path: string): { name: string; target: string }[] {\n try {\n const existing = new Database(path, { fileMustExist: true });\n try {\n const version = (existing.pragma(\"user_version\", { simple: true }) as number) ?? 0;\n if (version === SCHEMA_USER_VERSION) return [];\n return existing.prepare(\"SELECT name, target FROM peers\").all() as {\n name: string;\n target: string;\n }[];\n } catch {\n // Too old to have the table, or unreadable. Nothing to save.\n return [];\n } finally {\n existing.close();\n }\n } catch {\n // No database yet, or one too broken to open.\n return [];\n }\n}\n\nfunction needsReset(path: string): boolean {\n try {\n const existing = new Database(path, { fileMustExist: true });\n try {\n return (\n ((existing.pragma(\"user_version\", { simple: true }) as number) ?? 0) !== SCHEMA_USER_VERSION\n );\n } finally {\n existing.close();\n }\n } catch {\n return false;\n }\n}\n\nfunction toAttention(row: AttentionDbRow): SnapshotAttention {\n return {\n kind: row.kind as AttentionKind,\n message: row.message,\n source: row.source,\n requested_at: row.requested_at,\n };\n}\n\nfunction toAgent(row: AgentDbRow): SnapshotAgent {\n return {\n agent_id: row.agent_id,\n activity: row.activity as SnapshotAgent[\"activity\"],\n agent_name: row.agent_name,\n pi_session: row.pi_session,\n workstream: row.workstream,\n role: row.role,\n cli: row.cli,\n driver: row.driver as SnapshotAgent[\"driver\"],\n claimed_at: row.claimed_at,\n updated_at: row.updated_at,\n };\n}\n\nconst PRIORITY = new Map<string, number>(RENDER_PRIORITY.map((kind, index) => [kind, index]));\n\nfunction attentionOrder(left: SnapshotAttention, right: SnapshotAttention): number {\n return (PRIORITY.get(left.kind) ?? 99) - (PRIORITY.get(right.kind) ?? 99);\n}\n\n/**\n * Open the store. Takes no arguments and mints no identity.\n *\n * `openStore` deliberately does NOT read or create `identity.json`: identity is\n * created only by `murmur init`, so a read path — a status-bar tick, a focus\n * hook — cannot bring a node into existence as a side effect.\n */\nexport function openStore(): Store {\n const path = dbPath();\n mkdirSync(dirname(path), { recursive: true });\n\n const salvaged = salvagePeers(path);\n if (needsReset(path)) {\n for (const suffix of [\"\", \"-wal\", \"-shm\"]) rmSync(`${path}${suffix}`, { force: true });\n }\n\n const database = new Database(path);\n database.pragma(\"journal_mode = WAL\");\n database.pragma(\"busy_timeout = 5000\");\n const version = (database.pragma(\"user_version\", { simple: true }) as number) ?? 0;\n if (version !== SCHEMA_USER_VERSION) {\n database.exec(SCHEMA);\n database.pragma(`user_version = ${SCHEMA_USER_VERSION}`);\n // Re-inserted with every OBSERVED column null: a salvaged peer has no\n // snapshot and has never been fetched, and saying otherwise would render a\n // never-reached host as fresh.\n const restore = database.prepare(\"INSERT OR IGNORE INTO peers (name, target) VALUES (?, ?)\");\n for (const peer of salvaged) restore.run(peer.name, peer.target);\n }\n\n const selectAgentByPane = database.prepare(\"SELECT * FROM agents WHERE pane = ?\");\n const insertAgent = database.prepare(`\n INSERT INTO agents (agent_id, pane, owner_pid, activity, session, window,\n session_name, window_name, agent_name, pi_session,\n workstream, role, cli, driver, claimed_at, updated_at)\n VALUES (@agent_id, @pane, @owner_pid, @activity, @session, @window,\n @session_name, @window_name, @agent_name, @pi_session,\n @workstream, @role, @cli, @driver, @claimed_at, @updated_at)\n `);\n const retainAgent = database.prepare(`\n UPDATE agents\n SET session = @session, window = @window, session_name = @session_name,\n window_name = @window_name, agent_name = @agent_name,\n pi_session = @pi_session, workstream = @workstream, role = @role,\n cli = @cli, driver = @driver, updated_at = @updated_at\n WHERE agent_id = @agent_id\n `);\n const deleteAgentByPane = database.prepare(\"DELETE FROM agents WHERE pane = ?\");\n const deleteAttentionForPane = database.prepare(\"DELETE FROM attention WHERE pane = ?\");\n const updateActivity = database.prepare(`\n UPDATE agents\n SET activity = @activity, session = @session, window = @window,\n session_name = @session_name, window_name = @window_name,\n updated_at = @updated_at\n WHERE agent_id = @agent_id AND owner_pid = @owner_pid\n `);\n const deleteAgentOwned = database.prepare(\n \"DELETE FROM agents WHERE agent_id = ? AND owner_pid = ?\",\n );\n const upsertAttention = database.prepare(`\n INSERT INTO attention (pane, kind, message, source, session, window,\n session_name, window_name, requested_at)\n VALUES (@pane, @kind, @message, @source, @session, @window,\n @session_name, @window_name, @requested_at)\n ON CONFLICT (pane, kind) DO UPDATE SET\n message = excluded.message,\n source = excluded.source,\n session = excluded.session,\n window = excluded.window,\n session_name = excluded.session_name,\n window_name = excluded.window_name\n `);\n const selectAgents = database.prepare(\"SELECT * FROM agents\");\n const selectAttention = database.prepare(\"SELECT * FROM attention\");\n const setActivityByPane = database.prepare(\n \"UPDATE agents SET activity = ?, updated_at = ? WHERE pane = ?\",\n );\n\n /**\n * `.immediate`, not deferred, and this is load-bearing.\n *\n * The transaction reads the incumbent row and then writes, so a deferred one\n * starts as a READER and must upgrade. Two doing that at once fails the loser\n * with SQLITE_BUSY_SNAPSHOT, which no busy_timeout can fix: waiting longer\n * cannot make a stale snapshot fresh. Measured previously at 5 of 8\n * concurrent writers failing.\n */\n const claimAgent = database.transaction((claim: AgentClaim): ClaimResult => {\n const now = claim.now ?? Date.now();\n const isAlive = claim.isAlive ?? pidAlive;\n const { location, meta, owner_pid } = claim;\n const incumbent = selectAgentByPane.get(location.pane) as AgentDbRow | undefined;\n\n const values = {\n pane: location.pane,\n owner_pid,\n session: location.session,\n window: location.window,\n session_name: location.session_name,\n window_name: location.window_name,\n agent_name: meta.agent_name,\n pi_session: meta.pi_session,\n workstream: meta.workstream,\n role: meta.role,\n cli: meta.cli,\n driver: meta.driver,\n updated_at: now,\n };\n\n if (!incumbent) {\n const agentId = randomUUID();\n insertAgent.run({ ...values, agent_id: agentId, activity: \"stopped\", claimed_at: now });\n return { outcome: \"claimed\", agent_id: agentId };\n }\n\n // Our own claim, seen again. This is what makes pi's `/reload` a no-op: pi\n // re-runs the extension factory in the same process, and a check that could\n // not recognise its own claim would silence the real agent. `activity` and\n // `agent_id` are deliberately untouched.\n if (incumbent.owner_pid === owner_pid) {\n retainAgent.run({ ...values, agent_id: incumbent.agent_id });\n return { outcome: \"retained\", agent_id: incumbent.agent_id };\n }\n\n // A different LIVE process in one pane: the nested-agent case, and the only\n // answer for it. Fails closed — `pidAlive` reports death only on ESRCH, so\n // an unanswerable probe (EPERM) reads as alive and refuses. An unknown must\n // never let a second writer displace a possibly-live owner.\n if (isAlive(incumbent.owner_pid)) {\n return { outcome: \"refused\", held_by_pid: incumbent.owner_pid };\n }\n\n // The previous occupant is gone. Its attention described a process that no\n // longer exists, and a human looking at the pane now sees a different agent.\n deleteAgentByPane.run(location.pane);\n deleteAttentionForPane.run(location.pane);\n const agentId = randomUUID();\n insertAgent.run({ ...values, agent_id: agentId, activity: \"stopped\", claimed_at: now });\n return { outcome: \"replaced\", agent_id: agentId, previous_agent_id: incumbent.agent_id };\n }).immediate;\n\n /**\n * One transaction, because the `stopped` write and its `crashed` attention row\n * must land together or not at all.\n *\n * A no-op when tmux could not answer: `panes === null` is absence of evidence,\n * not evidence of death, and conflating the two once deleted ten live agents.\n */\n const reconcileLocal = database.transaction((world: LocalWorld): ReconcileSummary => {\n const summary: ReconcileSummary = { crashed: [], removed: [], attention_removed: [] };\n if (world.panes === null) return summary;\n const live = world.panes;\n const isAlive = world.isAlive ?? pidAlive;\n const now = world.now ?? Date.now();\n\n // Which panes already carry a crash we recorded. Read once, before any\n // write, so the loop below sees the state reconciliation started from.\n const alreadyCrashed = new Set(\n (selectAttention.all() as AttentionDbRow[])\n .filter((row) => row.kind === \"crashed\")\n .map((row) => row.pane),\n );\n\n for (const row of selectAgents.all() as AgentDbRow[]) {\n const pane = asPaneId(row.pane);\n if (!live.has(pane)) {\n deleteAgentByPane.run(row.pane);\n deleteAttentionForPane.run(row.pane);\n summary.removed.push(pane);\n continue;\n }\n if (isAlive(row.owner_pid)) continue;\n\n // The asymmetry below is the point. A dead RUNNING owner is an unreported\n // crash and must leave a durable trace. A dead STOPPED owner finished\n // normally, so its row is noise — but any `done` it raised is a fact a\n // human has not yet seen, so the attention stays.\n if (row.activity === \"running\") {\n setActivityByPane.run(\"stopped\", now, row.pane);\n upsertAttention.run({\n pane: row.pane,\n kind: \"crashed\",\n message: \"\",\n source: \"murmur\",\n session: row.session,\n window: row.window,\n session_name: row.session_name,\n window_name: row.window_name,\n requested_at: now,\n });\n summary.crashed.push(pane);\n } else if (!alreadyCrashed.has(row.pane)) {\n deleteAgentByPane.run(row.pane);\n summary.removed.push(pane);\n }\n // A pane we already recorded a crash for keeps its agent row, and that is\n // the one place this deviates from a literal reading of the contract's\n // table -- which says a live pane with a dead STOPPED owner loses its row.\n // Taken literally, the second reconcile after a crash deletes the row the\n // first one had just marked `stopped`, so the crashed pane loses its\n // agent_name, workstream, role and cli one tick after the crash is\n // reported. That contradicts the contract's own idempotence requirement\n // (\"running it again changes nothing\") and it strips exactly the fields a\n // human needs to know WHICH agent died.\n //\n // The distinction the table is drawing is between an owner that finished\n // normally -- whose row is noise -- and one that died mid-run. The\n // `crashed` row we wrote is the record of which case this was, so it is\n // also the right thing to key on.\n }\n\n // Reaps attention for a pane that never had an agent row — an\n // attention-only codex pane whose window was closed. Nothing else would.\n for (const row of selectAttention.all() as AttentionDbRow[]) {\n const pane = asPaneId(row.pane);\n if (live.has(pane)) continue;\n deleteAttentionForPane.run(row.pane);\n if (!summary.attention_removed.includes(pane)) summary.attention_removed.push(pane);\n }\n\n return summary;\n }).immediate;\n\n /**\n * Both tables read at ONE point in time, or a pane can appear with an agent\n * and without the attention that was there when the agent was read.\n */\n const readLocalPanes = database.transaction((): SnapshotPane[] => {\n const agents = selectAgents.all() as AgentDbRow[];\n const attention = selectAttention.all() as AttentionDbRow[];\n const panes = new Map<string, SnapshotPane>();\n\n const locate = (row: AgentDbRow | AttentionDbRow): SnapshotPane => {\n const existing = panes.get(row.pane);\n if (existing) return existing;\n const created: SnapshotPane = {\n pane: asPaneId(row.pane),\n session: asSessionId(row.session),\n window: asWindowId(row.window),\n session_name: row.session_name,\n window_name: row.window_name,\n agent: null,\n attention: [],\n };\n panes.set(row.pane, created);\n return created;\n };\n\n for (const row of agents) locate(row).agent = toAgent(row);\n for (const row of attention) locate(row).attention.push(toAttention(row));\n\n for (const pane of panes.values()) pane.attention.sort(attentionOrder);\n return [...panes.values()].sort((left, right) => left.pane.localeCompare(right.pane));\n });\n\n function peerRecord(row: PeerDbRow): PeerRecord {\n let snapshot: Snapshot | null = null;\n if (row.snapshot !== null) {\n try {\n // Parsed leniently on the way OUT: it was validated on the way in, and\n // a read path must not throw. A stored document that no longer parses\n // reads as \"no snapshot\" and is left in place, not deleted.\n snapshot = JSON.parse(row.snapshot) as Snapshot;\n } catch {\n snapshot = null;\n }\n }\n return {\n name: row.name,\n target: row.target,\n host_id: row.host_id,\n display_name: row.display_name,\n snapshot,\n snapshot_at: row.snapshot_at,\n fetched_at: row.fetched_at,\n last_attempt_at: row.last_attempt_at,\n last_error: row.last_error,\n murmur_version: row.murmur_version,\n snapshot_version: row.snapshot_version,\n };\n }\n\n return {\n claimAgent,\n reconcileLocal,\n\n setActivity(update) {\n // Both key components are required, so a write from a REPLACED owner\n // matches nothing and returns false. That is not an error and must not be\n // retried: it means this process is no longer the owner of record, and the\n // correct response is silence.\n return (\n updateActivity.run({\n activity: update.activity,\n session: update.location.session,\n window: update.location.window,\n session_name: update.location.session_name,\n window_name: update.location.window_name,\n updated_at: update.now ?? Date.now(),\n agent_id: update.agent_id,\n owner_pid: update.owner_pid,\n }).changes === 1\n );\n },\n\n releaseAgent(release) {\n // Attention is deliberately NOT deleted: a `done` raised at settle must\n // survive the agent exiting, or completion becomes invisible the moment\n // the process quits.\n return deleteAgentOwned.run(release.agent_id, release.owner_pid).changes === 1;\n },\n\n requestAttention(request) {\n // `requested_at` is absent from the DO UPDATE list on purpose. Age means\n // \"how long this has gone unmet\", so a repeat must not reset the clock —\n // which also makes crash attention idempotent for free. Touches no\n // `agents` row, ever; there is no column here that could.\n upsertAttention.run({\n pane: request.location.pane,\n kind: request.kind,\n message: request.message,\n source: request.source,\n session: request.location.session,\n window: request.location.window,\n session_name: request.location.session_name,\n window_name: request.location.window_name,\n requested_at: request.now ?? Date.now(),\n });\n },\n\n acknowledgePane(pane) {\n // Every kind, one statement, no agent row touched: focusing a pane cannot\n // alter activity or owner metadata. This is the whole `murmur clear`\n // write path.\n return deleteAttentionForPane.run(pane).changes;\n },\n\n localPanes() {\n return readLocalPanes();\n },\n\n buildLocalSnapshot(identity, world) {\n // Reconcile first, which is what makes \"a snapshot is authoritative\"\n // true: absence from a successful snapshot means absence, so it must\n // never be produced from unreconciled rows. Two transactions rather than\n // one — a write transaction held open across the read would serialise\n // every focus hook on the machine behind an export.\n reconcileLocal(world);\n return {\n murmur_snapshot: 1,\n host_id: identity.host_id,\n display_name: identity.display_name,\n murmur_version: MURMUR_VERSION,\n generated_at: world.now ?? Date.now(),\n // Rule 3: a pane with no agent and no attention must not be published.\n // A no-op against today's `readLocalPanes`, which builds a pane entry\n // only from a row and so cannot produce an empty one -- kept because the\n // rule belongs to the DOCUMENT, and the validator rejects such an entry\n // outright. Without it, one narrowing of the local read would make this\n // node reachable-but-broken on every peer that collects it, and the\n // symptom would show up on the other machines.\n panes: readLocalPanes().filter((pane) => pane.agent !== null || pane.attention.length > 0),\n };\n },\n\n peers() {\n return (database.prepare(\"SELECT * FROM peers ORDER BY name\").all() as PeerDbRow[]).map(\n peerRecord,\n );\n },\n\n addPeer(name, target) {\n // Correcting a target must not discard the cache, so this updates only\n // the field the operator retyped.\n database\n .prepare(\n `INSERT INTO peers (name, target) VALUES (?, ?)\n ON CONFLICT(name) DO UPDATE SET target = excluded.target`,\n )\n .run(name, target);\n },\n\n removePeer(name) {\n return database.prepare(\"DELETE FROM peers WHERE name = ?\").run(name).changes > 0;\n },\n\n replacePeerSnapshot(name, fetch) {\n if (!fetch.ok) {\n // Failure touches neither snapshot, snapshot_at nor fetched_at, so the\n // last-known document stands and the peer ages into `stale` on its own.\n database\n .prepare(\"UPDATE peers SET last_attempt_at = ?, last_error = ? WHERE name = ?\")\n .run(fetch.at, fetch.error, name);\n return;\n }\n // Two clocks, and conflating them is how a freshly fetched three-hour-old\n // fact reads as new. `snapshot_at` is the PEER's clock (when it built the\n // document); `fetched_at` is OURS (when we reached it), and freshness is\n // computed from `fetched_at` only.\n database\n .prepare(\n `UPDATE peers\n SET snapshot = ?, snapshot_at = ?, fetched_at = ?, last_attempt_at = ?,\n last_error = NULL, host_id = ?, display_name = ?,\n murmur_version = ?, snapshot_version = ?\n WHERE name = ?`,\n )\n .run(\n JSON.stringify(fetch.snapshot),\n fetch.snapshot.generated_at,\n fetch.at,\n fetch.at,\n fetch.snapshot.host_id,\n fetch.snapshot.display_name,\n fetch.snapshot.murmur_version,\n fetch.snapshot.murmur_snapshot,\n name,\n );\n },\n\n close() {\n database.close();\n },\n };\n}\n","import { createRequire } from \"node:module\";\n\n/**\n * This node's murmur version, read from the manifest.\n *\n * Read rather than restated, for the reason index.ts already gives: two copies\n * of one fact drift, and npm bumps the manifest. It lives in its own module\n * because THREE bundles need it and they sit at different depths --\n * `dist/index.js`, `dist/cli.js` and `dist/extension/store.js` -- so a single\n * hardcoded `\"../package.json\"` resolves in two of them and throws in the third.\n *\n * That is not hypothetical. `openStore` moved into the extension bundle during\n * the current-state rewrite, and its `../package.json` became\n * `dist/package.json`, which does not exist. The extension catches every store\n * failure and degrades to silence, so the symptom was an agent that reported\n * nothing at all, with no error anywhere -- exactly the failure mode the\n * three-state store handle exists to make survivable, hiding a hard one.\n *\n * Hence both candidates, tried in order, and a throw if neither works: a version\n * this node cannot state belongs in a snapshot even less than a wrong one does.\n */\nfunction readVersion(): string {\n const require = createRequire(import.meta.url);\n for (const candidate of [\"../package.json\", \"../../package.json\"]) {\n try {\n return (require(candidate) as { version: string }).version;\n } catch {\n // Wrong depth for this bundle; try the next.\n }\n }\n throw new Error(\"cannot locate package.json to read the murmur version\");\n}\n\nexport const MURMUR_VERSION: string = readVersion();\n"],"mappings":";AAAA,SAAS,iBAAiB;;;ACA1B,SAAS,UAAU,oBAAoB;AACvC,SAAS,iBAAiB;AAE1B,IAAM,gBAAgB,UAAU,QAAQ;AACxC,IAAM,eAAe;AAkBrB,IAAM,oBAAoB;AAO1B,IAAM,kBAAkB;AA0BjB,IAAM,cAAc;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe,YAAY;AAAA,EAC3B;AAAA,EACA,kBAAkB,iBAAiB;AACrC;AAoBA,IAAM,mBAAmB,KAAK,OAAO;AAE9B,IAAM,MAAe;AAAA,EAC1B,MAAM,KAAK,QAAQ,MAAM;AACvB,UAAM,EAAE,OAAO,IAAI,MAAM,cAAc,OAAO,CAAC,GAAG,aAAa,QAAQ,GAAG,IAAI,GAAG;AAAA,MAC/E,UAAU;AAAA,MACV,SAAS;AAAA,MACT,WAAW;AAAA,IACb,CAAC;AACD,WAAO;AAAA,EACT;AACF;AAEO,SAAS,cAAc,QAAyB;AACrD,MAAI;AACF,iBAAa,OAAO,CAAC,GAAG,aAAa,MAAM,SAAS,MAAM,GAAG,EAAE,OAAO,SAAS,CAAC;AAChF,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AC5DO,SAAS,YAAY,KAAwB;AAClD,SAAO;AACT;AAEO,SAAS,WAAW,KAAuB;AAChD,SAAO;AACT;AAEO,SAAS,SAAS,KAAqB;AAC5C,SAAO;AACT;;;ACtDA,SAAS,gBAAAA,qBAAoB;AAyC7B,SAAS,QAAQ,MAA+B;AAC9C,MAAI;AACF,WAAOC,cAAa,QAAQ,MAAM;AAAA,MAChC,UAAU;AAAA,MACV,SAAS;AAAA,MACT,OAAO,CAAC,UAAU,QAAQ,QAAQ;AAAA,IACpC,CAAC,EAAE,KAAK;AAAA,EACV,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AA4CO,SAAS,iBACd,MACA,YACe;AACf,MAAI,eAAe,IAAK,QAAO;AAC/B,SAAO,QAAQ;AACjB;AAEO,SAAS,aAAa,SAAyB;AACpD,SAAO,IAAI,OAAO;AACpB;AAEO,SAAS,gBAAgB,SAAyB;AACvD,SAAO,IAAI,OAAO;AACpB;AAEO,SAAS,eAAe,OAA4B;AAIzD,SAAO,UAAU,YAAY,YAAY;AAC3C;AAEO,IAAM,OAAY;AAAA,EACvB,gBAAgB;AAUd,UAAM,MAAM,QAAQ,IAAI;AACxB,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,OAAO,SAAS,GAAG;AAKzB,UAAM,SAAS,QAAQ;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,UAAM,CAAC,SAAS,QAAQ,aAAa,YAAY,UAAU,IAAI,QAAQ,MAAM,GAAI,KAAK,CAAC;AACvF,QAAI,CAAC,WAAW,CAAC,OAAQ,QAAO;AAChC,WAAO;AAAA,MACL,SAAS,YAAY,OAAO;AAAA,MAC5B,QAAQ,WAAW,MAAM;AAAA,MACzB;AAAA,MACA,cAAc,eAAe;AAAA,MAC7B,aAAa,iBAAiB,YAAY,UAAU;AAAA,IACtD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,YAAY;AACV,UAAM,MAAM,QAAQ,CAAC,cAAc,MAAM,MAAM,YAAY,CAAC;AAC5D,QAAI,QAAQ,KAAM,QAAO;AACzB,WAAO,IAAI,IAAI,IAAI,MAAM,IAAI,EAAE,OAAO,OAAO,EAAE,IAAI,QAAQ,CAAC;AAAA,EAC9D;AAAA,EAEA,eAAe,QAAQ,OAAO;AAC5B,QAAI,UAAU,MAAM;AAClB,cAAQ,CAAC,qBAAqB,OAAO,MAAM,QAAQ,cAAc,CAAC;AAAA,IACpE,OAAO;AACL,cAAQ,CAAC,qBAAqB,MAAM,MAAM,QAAQ,gBAAgB,eAAe,KAAK,CAAC,CAAC;AACxF,cAAQ,CAAC,qBAAqB,MAAM,MAAM,QAAQ,eAAe,GAAG,CAAC;AAAA,IACvE;AACA,YAAQ,CAAC,kBAAkB,IAAI,CAAC;AAAA,EAClC;AAAA,EAEA,OAAO,SAAS,QAAQ;AAUtB,YAAQ,CAAC,iBAAiB,MAAM,OAAO,CAAC;AACxC,WAAO,QAAQ,CAAC,iBAAiB,MAAM,MAAM,CAAC,MAAM;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,QAAQ;AACpB,UAAM,MAAM,QAAQ,CAAC,cAAc,MAAM,QAAQ,MAAM,YAAY,CAAC;AACpE,WAAO,KAAK,MAAM,IAAI,EAAE,OAAO,OAAO,EAAE,IAAI,QAAQ,KAAK,CAAC;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa;AACX,WAAO,QAAQ,CAAC,mBAAmB,MAAM,gBAAgB,CAAC,KAAK;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgB;AACd,WAAO,QAAQ,CAAC,mBAAmB,MAAM,8BAA8B,CAAC,KAAK;AAAA,EAC/E;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,MAAM;AACjB,UAAM,MAAM,QAAQ,CAAC,iBAAiB,MAAM,iBAAiB,CAAC;AAC9D,QAAI,QAAQ,KAAM,QAAO;AACzB,WAAO,IAAI,MAAM,IAAI,EAAE,SAAS,IAAI;AAAA,EACtC;AAAA,EAEA,WAAW,MAAM,SAAS;AAKxB,WAAO,QAAQ,CAAC,eAAe,MAAM,MAAM,MAAM,OAAO,CAAC,MAAM;AAAA,EACjE;AAAA,EAEA,iBAAiB,SAAS,QAAQ,OAAO;AACvC,YAAQ,CAAC,cAAc,MAAM,gBAAgB,OAAO,GAAG,QAAQ,KAAK,CAAC;AAAA,EACvE;AAAA,EAEA,aAAa,QAAQ,SAAS;AAC5B,UAAM,SAAS,aAAa,OAAO;AACnC,UAAM,OAAO,SACT,CAAC,iBAAiB,MAAM,QAAQ,MAAM,MAAM,IAC5C,CAAC,iBAAiB,MAAM,MAAM;AAClC,WAAO,QAAQ,IAAI,MAAM;AAAA,EAC3B;AAAA;AAAA;AAAA,EAIA,cAAc,MAAM;AAClB,UAAM,MAAM,QAAQ,CAAC,mBAAmB,MAAM,MAAM,MAAM,cAAc,CAAC;AACzE,WAAO,MAAM,WAAW,GAAG,IAAI;AAAA,EACjC;AAAA,EAEA,QAAQ,MAAM,OAAO;AACnB,UAAM,OAAO,CAAC,gBAAgB,MAAM,MAAM,IAAI;AAC9C,QAAI,UAAU,OAAW,MAAK,KAAK,MAAM,IAAI,KAAK,EAAE;AACpD,WAAO,QAAQ,IAAI;AAAA,EACrB;AACF;AAEO,SAAS,SAAS,KAAsB;AAC7C,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAQ,MAAgC,SAAS;AAAA,EACnD;AACF;;;AH/OO,SAAS,YAAY,SAAyB;AACnD,QAAM,OAAO,QAAQ,MAAM,GAAG,EAAE,OAAO,OAAO,EAAE,GAAG,EAAE;AACrD,SAAO,QAAQ;AACjB;AA4BO,SAAS,WAAW,OAAyB;AAClD,QAAM,OACJ,MAAM,cACN,MAAM,cACN,MAAM,gBACL,MAAM,iBAAiB,OAAO,OAAO,YAAY,MAAM,YAAY;AACtE,SAAO,aAAa,QAAQ,MAAM,MAAM;AAC1C;AAMO,SAAS,cAAc,OAAyB;AACrD,QAAM,UAAU,MAAM,gBAAgB,MAAM;AAC5C,QAAM,SAAS,MAAM,eAAe,MAAM;AAC1C,SAAO,aAAa,YAAY,SAAS,UAAU,GAAG,OAAO,IAAI,MAAM,EAAE;AAC3E;AAEO,SAAS,aAAa,OAAuB;AAClD,SAAO,CAAC,GAAG,KAAK,EACb,IAAI,CAAC,cAAc;AAClB,UAAM,OAAO,UAAU,WAAW,CAAC;AACnC,WAAO,OAAO,MAAQ,SAAS,OAAS,QAAQ,OAAQ,QAAQ,MAAQ,WAAM;AAAA,EAChF,CAAC,EACA,KAAK,EAAE;AACZ;AAEO,SAAS,WAAW,OAAuB;AAChD,SAAO,IAAI,MAAM,WAAW,KAAK,OAAO,CAAC;AAC3C;AAcO,SAAS,kBAAkB,UAA0B;AAC1D,SAAO,GAAG,SAAS,QAAQ,YAAY,EAAE,CAAC;AAC5C;AAeA,IAAM,cAAsB,CAAC,MAAM,MAAM,UAAU,UAAU;AAC3D,QAAM,SAAS,UAAU,MAAM,MAAM;AAAA,IACnC,UAAU;AAAA,IACV,SAAS;AAAA,IACT,GAAI,UAAU,EAAE,OAAO,UAAmB,IAAI,CAAC;AAAA,EACjD,CAAC;AACD,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf,QAAQ,OAAO,UAAU;AAAA;AAAA;AAAA;AAAA,IAIzB,QAAQ,OAAO,UAAU;AAAA,EAC3B;AACF;AAmBO,SAAS,YACd,OACA,OACA,MAAW,MACX,MAAc,aACF;AACZ,MAAI,MAAM,OAAO;AAKf,UAAM,QAAQ,IAAI,UAAU;AAC5B,QAAI,SAAS,CAAC,MAAM,IAAI,MAAM,IAAI,GAAG;AACnC,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,SAAS,GAAG,WAAW,KAAK,CAAC;AAAA,MAC/B;AAAA,IACF;AAIA,QAAI,CAAC,IAAI,OAAO,MAAM,SAAS,MAAM,MAAM,GAAG;AAC5C,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,SAAS,uBAAuB,WAAW,KAAK,CAAC;AAAA,MACnD;AAAA,IACF;AACA,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AACA,QAAM,OAAO,MAAM,MAAM,EAAE,KAAK,CAAC,cAAc,UAAU,YAAY,MAAM,OAAO;AAClF,QAAM,SAAS,MAAM,UAAU,MAAM;AACrC,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS,+BAA+B,MAAM,QAAQ,MAAM,GAAG,CAAC,CAAC;AAAA,IACnE;AAAA,EACF;AAkBA,QAAM,QAAQ,IAAI,OAAO;AAAA,IACvB,GAAG;AAAA,IACH;AAAA,IACA,yBAAyB,WAAW,YAAY,CAAC;AAAA,EACnD,CAAC;AACD,MAAI,MAAM,WAAW,GAAG;AAMtB,UAAM,YAAY,MAAM,WAAW,OAAO,MAAM;AAChD,QAAI,WAAW;AAIb,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,SAAS,gBAAgB,MAAM;AAAA,MACjC;AAAA,IACF;AAMA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS,GAAG,MAAM;AAAA,IACpB;AAAA,EACF;AACA,QAAM,cAAc,IAAI,IAAI,MAAM,OAAO,MAAM,IAAI,EAAE,OAAO,OAAO,EAAE,IAAI,QAAQ,CAAC;AAClF,MAAI,CAAC,YAAY,IAAI,MAAM,IAAI,GAAG;AAChC,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS,GAAG,WAAW,KAAK,CAAC,eAAe,MAAM;AAAA,IACpD;AAAA,EACF;AAEA,QAAM,eAAe,WAAW,GAAG,MAAM,OAAO,IAAI,MAAM,MAAM,EAAE;AAmBlE,MAAI,QAAQ,IAAI,MAAM;AAGpB,UAAM,SAAS,IAAI,WAAW;AAC9B,UAAM,SAAS,IAAI,cAAc;AAKjC,UAAM,OAAO,kBAAkB,MAAM,QAAQ,MAAM;AAMnD,QAAI,IAAI,aAAa,IAAI,GAAG;AAC1B,aAAO,IAAI,aAAa,QAAQ,IAAI,IAChC,EAAE,IAAI,KAAK,IACX;AAAA,QACE,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,SAAS,oCAAoC,IAAI;AAAA,MACnD;AAAA,IACN;AAOA,UAAMC,UAAS,UAAU,WAAW,MAAM,CAAC,mBAAmB,WAAW,YAAY,CAAC;AAUtF,UAAM,UAAU,SACZ,wBAAwB,SAAS,MAAM,WAAW,MAAM,CAAC,MAAM,EAAE,MAAM,WAAW,IAAI,MAAM,EAAE,CAAC,KAC/F;AAEJ,QAAI,CAAC,IAAI,WAAW,MAAM,GAAGA,OAAM,GAAG,OAAO,EAAE,GAAG;AAChD,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,SAAS,yCAAyC,MAAM;AAAA,MAC1D;AAAA,IACF;AAEA,QAAI,iBAAiB,MAAM,UAAU,KAAK;AAC1C,QAAI,iBAAiB,MAAM,UAAU,MAAM;AAC3C,QAAI,iBAAiB,MAAM,qBAAqB,UAAU;AAE1D,WAAO,IAAI,aAAa,QAAQ,IAAI,IAChC,EAAE,IAAI,KAAK,IACX;AAAA,MACE,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS,eAAe,MAAM,eAAe,IAAI;AAAA,IACnD;AAAA,EACN;AAgBA,QAAM,SAAS,IAAI,OAAO,CAAC,MAAM,QAAQ,QAAQ,UAAU,MAAM,YAAY,GAAG,IAAI;AACpF,SAAO,OAAO,WAAW,KAAK,CAAC,OAAO,SAClC,EAAE,IAAI,KAAK,IACX;AAAA,IACE,IAAI;AAAA,IACJ,QAAQ;AAAA,IACR,SAAS,iBAAiB,MAAM;AAAA,EAClC;AACN;;;AInVO,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAC9C,YACW,MACT,QACA;AAQA,UAAM,SAAS,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,EAAE;AAVxC;AAWT,SAAK,OAAO;AAAA,EACd;AAAA,EAZW;AAab;AAEA,SAAS,KAAK,MAAc,QAAuB;AACjD,QAAM,IAAI,qBAAqB,MAAM,MAAM;AAC7C;AASA,SAAS,OAAO,OAAgB,MAAc,MAAkD;AAC9F,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,SAAK,MAAM,oBAAoB;AAAA,EACjC;AACA,QAAM,SAAS;AACf,aAAW,OAAO,KAAM,KAAI,EAAE,OAAO,QAAS,MAAK,MAAM,eAAe,GAAG,EAAE;AAC7E,aAAW,OAAO,OAAO,KAAK,MAAM,GAAG;AACrC,QAAI,CAAC,KAAK,SAAS,GAAG,EAAG,MAAK,MAAM,eAAe,GAAG,EAAE;AAAA,EAC1D;AACA,SAAO;AACT;AAEA,SAAS,KAAK,OAAgB,MAAsB;AAClD,MAAI,OAAO,UAAU,YAAY,UAAU,GAAI,MAAK,MAAM,6BAA6B;AACvF,SAAO;AACT;AAEA,SAAS,WAAW,OAAgB,MAA6B;AAC/D,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,OAAO,UAAU,SAAU,MAAK,MAAM,2BAA2B;AACrE,SAAO;AACT;AAEA,SAAS,QAAQ,OAAgB,MAAsB;AACrD,MAAI,OAAO,UAAU,SAAU,MAAK,MAAM,mBAAmB;AAC7D,SAAO;AACT;AAEA,SAAS,UAAU,OAAgB,MAAsB;AACvD,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GAAG;AACtE,SAAK,MAAM,iCAAiC;AAAA,EAC9C;AACA,SAAO;AACT;AAEA,SAAS,OAAyB,OAAgB,MAAc,SAA0B;AACxF,MAAI,OAAO,UAAU,YAAY,CAAC,QAAQ,SAAS,KAAU,GAAG;AAC9D,SAAK,MAAM,mBAAmB,QAAQ,KAAK,IAAI,CAAC,EAAE;AAAA,EACpD;AACA,SAAO;AACT;AAEA,IAAM,aAAkC,CAAC,WAAW,SAAS;AAC7D,IAAM,UAA6B,CAAC,SAAS,cAAc;AAC3D,IAAM,QAAkC,CAAC,QAAQ,WAAW,SAAS;AAErE,IAAM,WAAW;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AACA,IAAM,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AACA,IAAM,aAAa;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AACA,IAAM,iBAAiB,CAAC,QAAQ,WAAW,UAAU,cAAc;AAEnE,SAAS,WAAW,OAAgB,MAAoC;AACtE,MAAI,UAAU,KAAM,QAAO;AAC3B,QAAM,MAAM,OAAO,OAAO,MAAM,UAAU;AAC1C,SAAO;AAAA,IACL,UAAU,KAAK,IAAI,UAAU,GAAG,IAAI,WAAW;AAAA,IAC/C,UAAU,OAAO,IAAI,UAAU,GAAG,IAAI,aAAa,UAAU;AAAA,IAC7D,YAAY,WAAW,IAAI,YAAY,GAAG,IAAI,aAAa;AAAA,IAC3D,YAAY,WAAW,IAAI,YAAY,GAAG,IAAI,aAAa;AAAA,IAC3D,YAAY,WAAW,IAAI,YAAY,GAAG,IAAI,aAAa;AAAA,IAC3D,MAAM,WAAW,IAAI,MAAM,GAAG,IAAI,OAAO;AAAA,IACzC,KAAK,KAAK,IAAI,KAAK,GAAG,IAAI,MAAM;AAAA,IAChC,QAAQ,OAAO,IAAI,QAAQ,GAAG,IAAI,WAAW,OAAO;AAAA,IACpD,YAAY,UAAU,IAAI,YAAY,GAAG,IAAI,aAAa;AAAA,IAC1D,YAAY,UAAU,IAAI,YAAY,GAAG,IAAI,aAAa;AAAA,EAC5D;AACF;AAEA,SAAS,eAAe,OAAgB,MAAmC;AACzE,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,MAAK,MAAM,mBAAmB;AACzD,QAAM,OAAO,oBAAI,IAAmB;AACpC,SAAO,MAAM,IAAI,CAAC,OAAO,UAAU;AACjC,UAAM,KAAK,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAM,MAAM,OAAO,OAAO,IAAI,cAAc;AAC5C,UAAM,OAAO,OAAO,IAAI,MAAM,GAAG,EAAE,SAAS,KAAK;AACjD,QAAI,KAAK,IAAI,IAAI,EAAG,MAAK,GAAG,EAAE,SAAS,kBAAkB,IAAI,gBAAgB;AAC7E,SAAK,IAAI,IAAI;AACb,WAAO;AAAA,MACL;AAAA,MACA,SAAS,QAAQ,IAAI,SAAS,GAAG,EAAE,UAAU;AAAA,MAC7C,QAAQ,QAAQ,IAAI,QAAQ,GAAG,EAAE,SAAS;AAAA,MAC1C,cAAc,UAAU,IAAI,cAAc,GAAG,EAAE,eAAe;AAAA,IAChE;AAAA,EACF,CAAC;AACH;AAEA,SAAS,UAAU,OAAgB,MAA4B;AAC7D,QAAM,MAAM,OAAO,OAAO,MAAM,SAAS;AACzC,QAAM,QAAQ,WAAW,IAAI,OAAO,GAAG,IAAI,QAAQ;AACnD,QAAM,YAAY,eAAe,IAAI,WAAW,GAAG,IAAI,YAAY;AAInE,MAAI,UAAU,QAAQ,UAAU,WAAW,GAAG;AAC5C,SAAK,MAAM,2DAA2D;AAAA,EACxE;AACA,SAAO;AAAA,IACL,MAAM,SAAS,KAAK,IAAI,MAAM,GAAG,IAAI,OAAO,CAAC;AAAA,IAC7C,SAAS,YAAY,KAAK,IAAI,SAAS,GAAG,IAAI,UAAU,CAAC;AAAA,IACzD,QAAQ,WAAW,KAAK,IAAI,QAAQ,GAAG,IAAI,SAAS,CAAC;AAAA,IACrD,cAAc,WAAW,IAAI,cAAc,GAAG,IAAI,eAAe;AAAA,IACjE,aAAa,WAAW,IAAI,aAAa,GAAG,IAAI,cAAc;AAAA,IAC9D;AAAA,IACA;AAAA,EACF;AACF;AAUO,SAAS,cAAc,OAAyB;AACrD,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,KAAK;AAAA,EAC3B,SAAS,OAAO;AACd,SAAK,IAAI,aAAa,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC,GAAG;AAAA,EACjF;AACA,QAAM,MAAM,OAAO,QAAQ,IAAI,QAAQ;AACvC,MAAI,IAAI,oBAAoB,GAAG;AAC7B,SAAK,mBAAmB,mBAAmB,KAAK,UAAU,IAAI,eAAe,CAAC,EAAE;AAAA,EAClF;AACA,MAAI,CAAC,MAAM,QAAQ,IAAI,KAAK,EAAG,MAAK,SAAS,mBAAmB;AAEhE,QAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,OAAO,UAAU,UAAU,OAAO,SAAS,KAAK,GAAG,CAAC;AACjF,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,IAAI,KAAK,IAAI,EAAG,MAAK,SAAS,kBAAkB,KAAK,IAAI,EAAE;AACpE,SAAK,IAAI,KAAK,IAAI;AAAA,EACpB;AAEA,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,SAAS,KAAK,IAAI,SAAS,SAAS;AAAA,IACpC,cAAc,KAAK,IAAI,cAAc,cAAc;AAAA,IACnD,gBAAgB,KAAK,IAAI,gBAAgB,gBAAgB;AAAA,IACzD,cAAc,UAAU,IAAI,cAAc,cAAc;AAAA,IACxD;AAAA,EACF;AACF;;;ACnMO,IAAM,iBAAyB;;;ACM/B,IAAM,kBAA0C;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAgBO,IAAM,cAAwC,CAAC,WAAW,SAAS;AAkDnE,IAAM,eAAe;AASrB,SAAS,IAAI,IAA2B;AAC7C,MAAI,OAAO,QAAQ,KAAK,IAAQ,QAAO;AACvC,MAAI,KAAK,KAAW,QAAO,GAAG,KAAK,MAAM,KAAK,GAAM,CAAC;AACrD,MAAI,KAAK,MAAY,QAAO,GAAG,KAAK,MAAM,KAAK,IAAS,CAAC;AACzD,SAAO,GAAG,KAAK,MAAM,KAAK,KAAU,CAAC;AACvC;AASO,SAAS,UACd,WACA,KACA,cAAc,cACH;AACX,SAAO,cAAc,QAAQ,MAAM,aAAa,cAAc,UAAU;AAC1E;AASO,SAAS,YAAY,MAA6D;AACvF,aAAW,QAAQ,CAAC,WAAW,WAAW,MAAM,GAAY;AAC1D,QAAI,KAAK,UAAU,SAAS,IAAI,EAAG,QAAO;AAAA,EAC5C;AACA,SAAO,KAAK,aAAa,YAAY,YAAY;AACnD;AAGA,SAAS,gBAAgB,MAAmC;AAC1D,MAAI,SAAwB;AAC5B,aAAW,SAAS,KAAK,WAAW;AAClC,QAAI,WAAW,QAAQ,MAAM,eAAe,OAAQ,UAAS,MAAM;AAAA,EACrE;AACA,SAAO;AACT;AAWA,SAAS,SAAS,MAAoB,QAA8B;AAClE,QAAM,QAAQ,KAAK;AACnB,SAAO;AAAA,IACL,SAAS,OAAO;AAAA,IAChB,MAAM,OAAO;AAAA,IACb,OAAO,OAAO;AAAA,IACd,MAAM,KAAK;AAAA,IACX,SAAS,KAAK;AAAA,IACd,QAAQ,KAAK;AAAA,IACb,cAAc,KAAK;AAAA,IACnB,aAAa,KAAK;AAAA,IAClB,UAAU,OAAO,YAAY;AAAA,IAC7B,WAAW,KAAK,UAAU,IAAI,CAAC,UAAU,MAAM,IAAI;AAAA,IACnD,WAAW,OAAO;AAAA,IAClB,UAAU,OAAO,YAAY;AAAA,IAC7B,YAAY,OAAO,cAAc;AAAA,IACjC,YAAY,OAAO,cAAc;AAAA,IACjC,YAAY,OAAO,cAAc;AAAA,IACjC,MAAM,OAAO,QAAQ;AAAA,IACrB,KAAK,OAAO,OAAO;AAAA,IACnB,QAAQ,OAAO,UAAU;AAAA,IACzB,YAAY,OAAO,cAAc,gBAAgB,IAAI;AAAA,IACrD,aAAa,OAAO;AAAA,IACpB,YAAY,OAAO;AAAA,EACrB;AACF;AAYO,SAAS,UAAU,OAAc,UAAwB,MAAM,KAAK,IAAI,GAAe;AAC5F,QAAM,QAAQ,MAAM,WAAW,EAAE;AAAA,IAAI,CAAC,SACpC,SAAS,MAAM;AAAA,MACb,SAAS,SAAS;AAAA,MAClB,MAAM,SAAS;AAAA,MACf,OAAO;AAAA;AAAA,MAEP,WAAW;AAAA,MACX,aAAa;AAAA,MACb,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AAEA,aAAW,QAAQ,MAAM,MAAM,GAAG;AAChC,UAAM,WAAW,KAAK;AACtB,QAAI,CAAC,SAAU;AACf,UAAM,SAAqB;AAAA,MACzB,SAAS,SAAS;AAAA;AAAA;AAAA;AAAA,MAIlB,MAAM,KAAK;AAAA,MACX,OAAO;AAAA,MACP,WAAW,UAAU,KAAK,YAAY,GAAG;AAAA,MACzC,aAAa,KAAK;AAAA,MAClB,YAAY,KAAK;AAAA,IACnB;AACA,eAAW,QAAQ,SAAS,MAAO,OAAM,KAAK,SAAS,MAAM,MAAM,CAAC;AAAA,EACtE;AAEA,SAAO;AACT;AAEA,IAAM,QAAQ,IAAI,IAAyB,gBAAgB,IAAI,CAAC,OAAO,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AAkBzF,SAAS,SAAS,OAA+B;AACtD,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU;AACtC,UAAM,WAAW,MAAM,IAAI,YAAY,IAAI,CAAC,KAAK,OAAO,MAAM,IAAI,YAAY,KAAK,CAAC,KAAK;AACzF,QAAI,YAAY,EAAG,QAAO;AAG1B,UAAM,SAAS,MAAM,cAAc,MAAM,KAAK,cAAc;AAC5D,QAAI,UAAU,EAAG,QAAO;AAGxB,UAAM,SAAS,KAAK,KAAK,cAAc,MAAM,IAAI;AACjD,WAAO,WAAW,IAAI,SAAS,KAAK,KAAK,cAAc,MAAM,IAAI;AAAA,EACnE,CAAC;AACH;;;ACnPO,IAAM,uBAAuB;AAWpC,IAAM,sBAAsB;AAiErB,IAAM,oBAAoB;AAoBjC,eAAsB,WACpB,OACA,OACA,MACA,UACkD;AAClD,QAAM,UAAU,IAAI,MAA2C,MAAM,MAAM;AAC3E,MAAI,SAAS;AACb,MAAI,UAAU;AACd,QAAM,OAAO,UAAU,KAAK,MAAM;AAChC,cAAU;AAAA,EACZ,CAAC;AACD,QAAM,SAAS,YAAY;AACzB,WAAO,SAAS,MAAM,UAAU,CAAC,SAAS;AACxC,YAAM,QAAQ;AACd,UAAI;AACF,gBAAQ,KAAK,IAAI,EAAE,QAAQ,aAAa,OAAO,MAAM,KAAK,MAAM,KAAK,CAAM,EAAE;AAAA,MAC/E,SAAS,QAAQ;AACf,gBAAQ,KAAK,IAAI,EAAE,QAAQ,YAAY,OAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,OAAO,MAAM,MAAM,EAAE,GAAG,MAAM,CAAC;AACtF,SAAO,OAAO,QAAQ,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI;AAC3C,SAAO;AACT;AAkDA,SAAS,SACP,OACA,KACA,SACA,QACc;AACd,MAAI,WAAW,EAAG,QAAO,CAAC,GAAG,KAAK;AAClC,SAAO,MAAM,OAAO,CAAC,SAAS;AAC5B,QAAI,KAAK,oBAAoB,KAAM,QAAO;AAE1C,UAAM,UAAU,OAAO,IAAI,OAAO;AAClC,WAAO,MAAM,KAAK,mBAAmB,UAAU;AAAA,EACjD,CAAC;AACH;AAgCA,SAAS,cAAc,SAA0B;AAC/C,SACE,2MAA2M;AAAA,IACzM;AAAA,EACF,KAAK,SAAS,KAAK,OAAO;AAE9B;AAiBA,SAAS,gBAAgB,SAAyB;AAChD,QAAM,YAAY,QAAQ,QAAQ,IAAI;AACtC,MAAI,cAAc,MAAM,CAAC,QAAQ,WAAW,iBAAiB,EAAG,QAAO;AACvE,QAAM,OAAO,QAAQ,MAAM,YAAY,CAAC,EAAE,KAAK;AAG/C,SAAO,SAAS,KAAK,UAAU;AACjC;AAWA,SAAS,iBAAiB,SAAyB;AACjD,SAAO,gBAAgB,OAAO,EAAE,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAC5D;AAiCA,eAAsB,QACpB,OACA,SACA,MAAM,KAAK,IAAI,GACf,UAA0B,CAAC,GACD;AAC1B,QAAM,EAAE,UAAU,MAAM,MAAM,UAAU,GAAG,SAAS,KAAK,OAAO,IAAI;AACpE,QAAM,UAA2B,CAAC;AAClC,MAAI;AACJ,MAAI;AAGF,UAAM,QAAQ,SAAS,MAAM,MAAM,GAAG,KAAK,SAAS,MAAM;AAI1D,UAAM,UACJ,YACA,IAAI,QAAc,CAAC,YAAY;AAC7B,cAAQ,WAAW,SAAS,mBAAmB;AAC/C,YAAM,QAAQ;AAAA,IAChB,CAAC;AAIH,UAAM,UAAU,MAAM;AAAA,MACpB;AAAA,MACA;AAAA,MACA,OAAO,SAAS,cAAc,MAAM,QAAQ,KAAK,KAAK,QAAQ,CAAC,UAAU,QAAQ,CAAC,CAAC;AAAA,MACnF;AAAA,IACF;AACA,eAAW,CAAC,OAAO,IAAI,KAAK,MAAM,QAAQ,GAAG;AAC3C,YAAM,QAAQ,QAAQ,KAAK;AAC3B,UAAI;AAKF,YAAI,CAAC,MAAO,OAAM,IAAI,MAAM,mDAAmD;AAC/E,YAAI,MAAM,WAAW,WAAY,OAAM,MAAM;AAG7C,cAAM,oBAAoB,KAAK,MAAM,EAAE,IAAI,MAAM,UAAU,MAAM,OAAO,IAAI,IAAI,CAAC;AACjF,gBAAQ,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,MAAM,OAAO,MAAM,MAAM,MAAM,OAAO,CAAC;AAAA,MAC7E,SAAS,OAAO;AAYd,cAAM,UAAU,iBAAiB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACvF,cAAM,oBAAoB,KAAK,MAAM,EAAE,IAAI,OAAO,OAAO,SAAS,IAAI,IAAI,CAAC;AAM3E,gBAAQ,KAAK;AAAA,UACX,MAAM,KAAK;AAAA,UACX,IAAI;AAAA,UACJ,OAAO;AAAA,UACP,OAAO;AAAA;AAAA;AAAA,UAGP,aACE,iBAAiB,uBACb,QACA,cAAc,iBAAiB,OAAO,CAAC;AAAA,QAC/C,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AAGd,YAAQ,KAAK;AAAA,MACX,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAC9D,CAAC;AAAA,EACH,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AAOA,MAAI;AACF,UAAM,eAAe,EAAE,OAAO,IAAI,UAAU,GAAG,IAAI,CAAC;AAAA,EACtD,QAAQ;AAAA,EAER;AACA,SAAO;AACT;;;AC5ZA,SAAS,gBAAAC,qBAAoB;AAiB7B,IAAM,eAAe;AAEd,SAAS,OAAO,OAAc,OAAiB,QAAQ,cAA6B;AACzF,MAAI,MAAM,MAAO,QAAO,KAAK,QAAQ,MAAM,MAAM,KAAK;AAEtD,QAAM,OAAO,MAAM,MAAM,EAAE,KAAK,CAAC,cAAc,UAAU,YAAY,MAAM,OAAO;AAClF,QAAM,SAAS,MAAM,UAAU,MAAM;AACrC,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI;AAIF,WAAOC;AAAA,MACL;AAAA,MACA;AAAA,QACE,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,IAAI,MAAM,IAAI;AAAA,QACd;AAAA,QACA,IAAI,KAAK;AAAA,MACX;AAAA,MACA,EAAE,UAAU,QAAQ,SAAS,KAAM,OAAO,CAAC,UAAU,QAAQ,QAAQ,EAAE;AAAA,IACzE;AAAA,EACF,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;;;ACjDA,SAAS,kBAAkB;AAC3B,SAAS,YAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,gBAAgB;AACzB,SAAS,QAAAC,aAAY;;;ACHrB,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,SAAS,WAAmB;AACjC,SACE,QAAQ,IAAI,oBACZ,KAAK,QAAQ,IAAI,kBAAkB,KAAK,QAAQ,GAAG,UAAU,OAAO,GAAG,QAAQ;AAEnF;AAEO,SAAS,YAAoB;AAClC,SACE,QAAQ,IAAI,qBACZ,KAAK,QAAQ,IAAI,mBAAmB,KAAK,QAAQ,GAAG,SAAS,GAAG,QAAQ;AAE5E;AAGO,SAAS,SAAiB;AAC/B,SAAO,KAAK,SAAS,GAAG,UAAU;AACpC;;;ADTA,SAAS,eAAuB;AAC9B,SAAOC,MAAK,SAAS,GAAG,eAAe;AACzC;AAUA,IAAI,QAAgE;AAU7D,SAAS,eAAoC;AAClD,QAAM,OAAO,aAAa;AAC1B,MAAI,OAAO,SAAS,KAAM,QAAO,MAAM;AACvC,QAAM,WAAW,WAAW,IAAI,IAC3B,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC,IACtC;AACJ,UAAQ,EAAE,MAAM,SAAS;AACzB,SAAO;AACT;AAEA,SAAS,MAAM,UAAsC;AACnD,YAAU,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AACzC,gBAAc,aAAa,GAAG,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAAA,CAAI;AACtE,UAAQ,EAAE,MAAM,aAAa,GAAG,SAAS;AACzC,SAAO;AACT;AAGO,SAAS,eAAe,cAAc,SAAS,GAAiB;AACrE,MAAI,aAAa,EAAG,OAAM,IAAI,MAAM,4BAA4B,aAAa,CAAC,EAAE;AAChF,SAAO,MAAM,EAAE,SAAS,WAAW,GAAG,cAAc,YAAY,CAAC;AACnE;AAQO,SAAS,eAAe,aAAmC;AAChE,QAAM,WAAW,aAAa;AAC9B,SAAO;AAAA,IACL,WACI,EAAE,SAAS,SAAS,SAAS,cAAc,YAAY,IACvD,EAAE,SAAS,WAAW,GAAG,cAAc,YAAY;AAAA,EACzD;AACF;;;AEtCA,SAAS,cAAsB;AAC7B,QAAM,SAAS,CAAC;AAChB,aAAW,SAAS,gBAAiB,QAAO,KAAK,IAAI;AACrD,SAAO;AACT;AAEO,SAAS,WAAW,MAAsB;AAM/C,QAAM,aAAa,IAAI,IAAY,WAAW;AAC9C,QAAM,QAAQ,CAAC,UACb,KAAK,OAAO,KAAK,KAAK,WAAW,IAAI,KAAK,IAAI,KAAK,oBAAoB,KAAK,IAAI;AAClF,SACE,gBAAgB,OAAO,CAAC,UAAU,MAAM,KAAK,IAAI,CAAC,EAG/C,IAAI,CAAC,UAAU,GAAG,UAAU,YAAY,YAAY,KAAK,IAAK,MAAM,KAAK,CAAC;AAAA,CAAI,EAC9E,KAAK,EAAE;AAEd;AASO,SAAS,OAAO,OAAc,UAAwB,MAAM,KAAK,IAAI,GAAW;AACrF,QAAM,SAAS,YAAY;AAC3B,QAAM,qBAAqB,YAAY;AACvC,QAAM,QAAQ,SAAS,UAAU,OAAO,UAAU,GAAG,CAAC;AACtD,aAAW,QAAQ,OAAO;AACxB,UAAM,SAAS,KAAK,WAAW,UAAU,SAAS;AAClD,WAAO,YAAY,IAAI,CAAC,KAAK;AAAA,EAC/B;AAEA,SAAO;AAAA,IACL;AAAA,IACA,qBAAqB;AAAA,IACrB;AAAA,IACA,OAAO,MAAM,MAAM,EAAE,IAAI,CAAC,UAAU;AAAA,MAClC,MAAM,KAAK;AAAA,MACX,cAAc,KAAK;AAAA,MACnB,YAAY,KAAK;AAAA;AAAA;AAAA,MAGjB,aAAa,KAAK;AAAA,MAClB,YAAY,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMjB,OAAO,UAAU,KAAK,YAAY,GAAG,MAAM;AAAA,IAC7C,EAAE;AAAA,EACJ;AACF;AAyBA,eAAsB,kBACpB,OACA,UACA,MAAM,KAAK,IAAI,GACf,UAAmB,KACnB,UAA0B,CAAC,GACV;AACjB,MAAI;AACF,UAAM,QAAQ,OAAO,SAAS,KAAK,OAAO;AAAA,EAC5C,QAAQ;AAAA,EAGR;AACA,SAAO,OAAO,OAAO,UAAU,GAAG;AACpC;;;AClIA,SAAS,cAAAC,mBAAkB;AAC3B,SAAS,aAAAC,YAAW,cAAc;AAClC,SAAS,eAAe;AACxB,OAAO,cAAc;;;ACHrB,SAAS,qBAAqB;AAqB9B,SAAS,cAAsB;AAC7B,QAAMC,WAAU,cAAc,YAAY,GAAG;AAC7C,aAAW,aAAa,CAAC,mBAAmB,oBAAoB,GAAG;AACjE,QAAI;AACF,aAAQA,SAAQ,SAAS,EAA0B;AAAA,IACrD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,QAAM,IAAI,MAAM,uDAAuD;AACzE;AAEO,IAAM,iBAAyB,YAAY;;;ADElD,IAAM,sBAAsB;AAE5B,IAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiIf,SAAS,aAAa,MAAkD;AACtE,MAAI;AACF,UAAM,WAAW,IAAI,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;AAC3D,QAAI;AACF,YAAM,UAAW,SAAS,OAAO,gBAAgB,EAAE,QAAQ,KAAK,CAAC,KAAgB;AACjF,UAAI,YAAY,oBAAqB,QAAO,CAAC;AAC7C,aAAO,SAAS,QAAQ,gCAAgC,EAAE,IAAI;AAAA,IAIhE,QAAQ;AAEN,aAAO,CAAC;AAAA,IACV,UAAE;AACA,eAAS,MAAM;AAAA,IACjB;AAAA,EACF,QAAQ;AAEN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,WAAW,MAAuB;AACzC,MAAI;AACF,UAAM,WAAW,IAAI,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;AAC3D,QAAI;AACF,cACI,SAAS,OAAO,gBAAgB,EAAE,QAAQ,KAAK,CAAC,KAAgB,OAAO;AAAA,IAE7E,UAAE;AACA,eAAS,MAAM;AAAA,IACjB;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,YAAY,KAAwC;AAC3D,SAAO;AAAA,IACL,MAAM,IAAI;AAAA,IACV,SAAS,IAAI;AAAA,IACb,QAAQ,IAAI;AAAA,IACZ,cAAc,IAAI;AAAA,EACpB;AACF;AAEA,SAAS,QAAQ,KAAgC;AAC/C,SAAO;AAAA,IACL,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA,IACd,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,MAAM,IAAI;AAAA,IACV,KAAK,IAAI;AAAA,IACT,QAAQ,IAAI;AAAA,IACZ,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,EAClB;AACF;AAEA,IAAM,WAAW,IAAI,IAAoB,gBAAgB,IAAI,CAAC,MAAM,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC;AAE5F,SAAS,eAAe,MAAyB,OAAkC;AACjF,UAAQ,SAAS,IAAI,KAAK,IAAI,KAAK,OAAO,SAAS,IAAI,MAAM,IAAI,KAAK;AACxE;AASO,SAAS,YAAmB;AACjC,QAAM,OAAO,OAAO;AACpB,EAAAC,WAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAE5C,QAAM,WAAW,aAAa,IAAI;AAClC,MAAI,WAAW,IAAI,GAAG;AACpB,eAAW,UAAU,CAAC,IAAI,QAAQ,MAAM,EAAG,QAAO,GAAG,IAAI,GAAG,MAAM,IAAI,EAAE,OAAO,KAAK,CAAC;AAAA,EACvF;AAEA,QAAM,WAAW,IAAI,SAAS,IAAI;AAClC,WAAS,OAAO,oBAAoB;AACpC,WAAS,OAAO,qBAAqB;AACrC,QAAM,UAAW,SAAS,OAAO,gBAAgB,EAAE,QAAQ,KAAK,CAAC,KAAgB;AACjF,MAAI,YAAY,qBAAqB;AACnC,aAAS,KAAK,MAAM;AACpB,aAAS,OAAO,kBAAkB,mBAAmB,EAAE;AAIvD,UAAM,UAAU,SAAS,QAAQ,0DAA0D;AAC3F,eAAW,QAAQ,SAAU,SAAQ,IAAI,KAAK,MAAM,KAAK,MAAM;AAAA,EACjE;AAEA,QAAM,oBAAoB,SAAS,QAAQ,qCAAqC;AAChF,QAAM,cAAc,SAAS,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAOpC;AACD,QAAM,cAAc,SAAS,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAOpC;AACD,QAAM,oBAAoB,SAAS,QAAQ,mCAAmC;AAC9E,QAAM,yBAAyB,SAAS,QAAQ,sCAAsC;AACtF,QAAM,iBAAiB,SAAS,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAMvC;AACD,QAAM,mBAAmB,SAAS;AAAA,IAChC;AAAA,EACF;AACA,QAAM,kBAAkB,SAAS,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAYxC;AACD,QAAM,eAAe,SAAS,QAAQ,sBAAsB;AAC5D,QAAM,kBAAkB,SAAS,QAAQ,yBAAyB;AAClE,QAAM,oBAAoB,SAAS;AAAA,IACjC;AAAA,EACF;AAWA,QAAM,aAAa,SAAS,YAAY,CAAC,UAAmC;AAC1E,UAAM,MAAM,MAAM,OAAO,KAAK,IAAI;AAClC,UAAM,UAAU,MAAM,WAAW;AACjC,UAAM,EAAE,UAAU,MAAM,UAAU,IAAI;AACtC,UAAM,YAAY,kBAAkB,IAAI,SAAS,IAAI;AAErD,UAAM,SAAS;AAAA,MACb,MAAM,SAAS;AAAA,MACf;AAAA,MACA,SAAS,SAAS;AAAA,MAClB,QAAQ,SAAS;AAAA,MACjB,cAAc,SAAS;AAAA,MACvB,aAAa,SAAS;AAAA,MACtB,YAAY,KAAK;AAAA,MACjB,YAAY,KAAK;AAAA,MACjB,YAAY,KAAK;AAAA,MACjB,MAAM,KAAK;AAAA,MACX,KAAK,KAAK;AAAA,MACV,QAAQ,KAAK;AAAA,MACb,YAAY;AAAA,IACd;AAEA,QAAI,CAAC,WAAW;AACd,YAAMC,WAAUC,YAAW;AAC3B,kBAAY,IAAI,EAAE,GAAG,QAAQ,UAAUD,UAAS,UAAU,WAAW,YAAY,IAAI,CAAC;AACtF,aAAO,EAAE,SAAS,WAAW,UAAUA,SAAQ;AAAA,IACjD;AAMA,QAAI,UAAU,cAAc,WAAW;AACrC,kBAAY,IAAI,EAAE,GAAG,QAAQ,UAAU,UAAU,SAAS,CAAC;AAC3D,aAAO,EAAE,SAAS,YAAY,UAAU,UAAU,SAAS;AAAA,IAC7D;AAMA,QAAI,QAAQ,UAAU,SAAS,GAAG;AAChC,aAAO,EAAE,SAAS,WAAW,aAAa,UAAU,UAAU;AAAA,IAChE;AAIA,sBAAkB,IAAI,SAAS,IAAI;AACnC,2BAAuB,IAAI,SAAS,IAAI;AACxC,UAAM,UAAUC,YAAW;AAC3B,gBAAY,IAAI,EAAE,GAAG,QAAQ,UAAU,SAAS,UAAU,WAAW,YAAY,IAAI,CAAC;AACtF,WAAO,EAAE,SAAS,YAAY,UAAU,SAAS,mBAAmB,UAAU,SAAS;AAAA,EACzF,CAAC,EAAE;AASH,QAAM,iBAAiB,SAAS,YAAY,CAAC,UAAwC;AACnF,UAAM,UAA4B,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC,GAAG,mBAAmB,CAAC,EAAE;AACpF,QAAI,MAAM,UAAU,KAAM,QAAO;AACjC,UAAM,OAAO,MAAM;AACnB,UAAM,UAAU,MAAM,WAAW;AACjC,UAAM,MAAM,MAAM,OAAO,KAAK,IAAI;AAIlC,UAAM,iBAAiB,IAAI;AAAA,MACxB,gBAAgB,IAAI,EAClB,OAAO,CAAC,QAAQ,IAAI,SAAS,SAAS,EACtC,IAAI,CAAC,QAAQ,IAAI,IAAI;AAAA,IAC1B;AAEA,eAAW,OAAO,aAAa,IAAI,GAAmB;AACpD,YAAM,OAAO,SAAS,IAAI,IAAI;AAC9B,UAAI,CAAC,KAAK,IAAI,IAAI,GAAG;AACnB,0BAAkB,IAAI,IAAI,IAAI;AAC9B,+BAAuB,IAAI,IAAI,IAAI;AACnC,gBAAQ,QAAQ,KAAK,IAAI;AACzB;AAAA,MACF;AACA,UAAI,QAAQ,IAAI,SAAS,EAAG;AAM5B,UAAI,IAAI,aAAa,WAAW;AAC9B,0BAAkB,IAAI,WAAW,KAAK,IAAI,IAAI;AAC9C,wBAAgB,IAAI;AAAA,UAClB,MAAM,IAAI;AAAA,UACV,MAAM;AAAA,UACN,SAAS;AAAA,UACT,QAAQ;AAAA,UACR,SAAS,IAAI;AAAA,UACb,QAAQ,IAAI;AAAA,UACZ,cAAc,IAAI;AAAA,UAClB,aAAa,IAAI;AAAA,UACjB,cAAc;AAAA,QAChB,CAAC;AACD,gBAAQ,QAAQ,KAAK,IAAI;AAAA,MAC3B,WAAW,CAAC,eAAe,IAAI,IAAI,IAAI,GAAG;AACxC,0BAAkB,IAAI,IAAI,IAAI;AAC9B,gBAAQ,QAAQ,KAAK,IAAI;AAAA,MAC3B;AAAA,IAeF;AAIA,eAAW,OAAO,gBAAgB,IAAI,GAAuB;AAC3D,YAAM,OAAO,SAAS,IAAI,IAAI;AAC9B,UAAI,KAAK,IAAI,IAAI,EAAG;AACpB,6BAAuB,IAAI,IAAI,IAAI;AACnC,UAAI,CAAC,QAAQ,kBAAkB,SAAS,IAAI,EAAG,SAAQ,kBAAkB,KAAK,IAAI;AAAA,IACpF;AAEA,WAAO;AAAA,EACT,CAAC,EAAE;AAMH,QAAM,iBAAiB,SAAS,YAAY,MAAsB;AAChE,UAAM,SAAS,aAAa,IAAI;AAChC,UAAM,YAAY,gBAAgB,IAAI;AACtC,UAAM,QAAQ,oBAAI,IAA0B;AAE5C,UAAM,SAAS,CAAC,QAAmD;AACjE,YAAM,WAAW,MAAM,IAAI,IAAI,IAAI;AACnC,UAAI,SAAU,QAAO;AACrB,YAAM,UAAwB;AAAA,QAC5B,MAAM,SAAS,IAAI,IAAI;AAAA,QACvB,SAAS,YAAY,IAAI,OAAO;AAAA,QAChC,QAAQ,WAAW,IAAI,MAAM;AAAA,QAC7B,cAAc,IAAI;AAAA,QAClB,aAAa,IAAI;AAAA,QACjB,OAAO;AAAA,QACP,WAAW,CAAC;AAAA,MACd;AACA,YAAM,IAAI,IAAI,MAAM,OAAO;AAC3B,aAAO;AAAA,IACT;AAEA,eAAW,OAAO,OAAQ,QAAO,GAAG,EAAE,QAAQ,QAAQ,GAAG;AACzD,eAAW,OAAO,UAAW,QAAO,GAAG,EAAE,UAAU,KAAK,YAAY,GAAG,CAAC;AAExE,eAAW,QAAQ,MAAM,OAAO,EAAG,MAAK,UAAU,KAAK,cAAc;AACrE,WAAO,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,KAAK,cAAc,MAAM,IAAI,CAAC;AAAA,EACtF,CAAC;AAED,WAAS,WAAW,KAA4B;AAC9C,QAAI,WAA4B;AAChC,QAAI,IAAI,aAAa,MAAM;AACzB,UAAI;AAIF,mBAAW,KAAK,MAAM,IAAI,QAAQ;AAAA,MACpC,QAAQ;AACN,mBAAW;AAAA,MACb;AAAA,IACF;AACA,WAAO;AAAA,MACL,MAAM,IAAI;AAAA,MACV,QAAQ,IAAI;AAAA,MACZ,SAAS,IAAI;AAAA,MACb,cAAc,IAAI;AAAA,MAClB;AAAA,MACA,aAAa,IAAI;AAAA,MACjB,YAAY,IAAI;AAAA,MAChB,iBAAiB,IAAI;AAAA,MACrB,YAAY,IAAI;AAAA,MAChB,gBAAgB,IAAI;AAAA,MACpB,kBAAkB,IAAI;AAAA,IACxB;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IAEA,YAAY,QAAQ;AAKlB,aACE,eAAe,IAAI;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,SAAS,OAAO,SAAS;AAAA,QACzB,QAAQ,OAAO,SAAS;AAAA,QACxB,cAAc,OAAO,SAAS;AAAA,QAC9B,aAAa,OAAO,SAAS;AAAA,QAC7B,YAAY,OAAO,OAAO,KAAK,IAAI;AAAA,QACnC,UAAU,OAAO;AAAA,QACjB,WAAW,OAAO;AAAA,MACpB,CAAC,EAAE,YAAY;AAAA,IAEnB;AAAA,IAEA,aAAa,SAAS;AAIpB,aAAO,iBAAiB,IAAI,QAAQ,UAAU,QAAQ,SAAS,EAAE,YAAY;AAAA,IAC/E;AAAA,IAEA,iBAAiB,SAAS;AAKxB,sBAAgB,IAAI;AAAA,QAClB,MAAM,QAAQ,SAAS;AAAA,QACvB,MAAM,QAAQ;AAAA,QACd,SAAS,QAAQ;AAAA,QACjB,QAAQ,QAAQ;AAAA,QAChB,SAAS,QAAQ,SAAS;AAAA,QAC1B,QAAQ,QAAQ,SAAS;AAAA,QACzB,cAAc,QAAQ,SAAS;AAAA,QAC/B,aAAa,QAAQ,SAAS;AAAA,QAC9B,cAAc,QAAQ,OAAO,KAAK,IAAI;AAAA,MACxC,CAAC;AAAA,IACH;AAAA,IAEA,gBAAgB,MAAM;AAIpB,aAAO,uBAAuB,IAAI,IAAI,EAAE;AAAA,IAC1C;AAAA,IAEA,aAAa;AACX,aAAO,eAAe;AAAA,IACxB;AAAA,IAEA,mBAAmB,UAAU,OAAO;AAMlC,qBAAe,KAAK;AACpB,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB,SAAS,SAAS;AAAA,QAClB,cAAc,SAAS;AAAA,QACvB,gBAAgB;AAAA,QAChB,cAAc,MAAM,OAAO,KAAK,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAQpC,OAAO,eAAe,EAAE,OAAO,CAAC,SAAS,KAAK,UAAU,QAAQ,KAAK,UAAU,SAAS,CAAC;AAAA,MAC3F;AAAA,IACF;AAAA,IAEA,QAAQ;AACN,aAAQ,SAAS,QAAQ,mCAAmC,EAAE,IAAI,EAAkB;AAAA,QAClF;AAAA,MACF;AAAA,IACF;AAAA,IAEA,QAAQ,MAAM,QAAQ;AAGpB,eACG;AAAA,QACC;AAAA;AAAA,MAEF,EACC,IAAI,MAAM,MAAM;AAAA,IACrB;AAAA,IAEA,WAAW,MAAM;AACf,aAAO,SAAS,QAAQ,kCAAkC,EAAE,IAAI,IAAI,EAAE,UAAU;AAAA,IAClF;AAAA,IAEA,oBAAoB,MAAM,OAAO;AAC/B,UAAI,CAAC,MAAM,IAAI;AAGb,iBACG,QAAQ,qEAAqE,EAC7E,IAAI,MAAM,IAAI,MAAM,OAAO,IAAI;AAClC;AAAA,MACF;AAKA,eACG;AAAA,QACC;AAAA;AAAA;AAAA;AAAA;AAAA,MAKF,EACC;AAAA,QACC,KAAK,UAAU,MAAM,QAAQ;AAAA,QAC7B,MAAM,SAAS;AAAA,QACf,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM,SAAS;AAAA,QACf,MAAM,SAAS;AAAA,QACf,MAAM,SAAS;AAAA,QACf,MAAM,SAAS;AAAA,QACf;AAAA,MACF;AAAA,IACJ;AAAA,IAEA,QAAQ;AACN,eAAS,MAAM;AAAA,IACjB;AAAA,EACF;AACF;","names":["execFileSync","execFileSync","attach","execFileSync","execFileSync","join","join","randomUUID","mkdirSync","require","mkdirSync","agentId","randomUUID"]}