@martintrojer/murmur 0.1.3 → 0.2.0
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/ARCHITECTURE.md +742 -219
- package/CHANGELOG.md +172 -0
- package/README.md +136 -27
- package/dist/cli.js +1419 -869
- package/dist/cli.js.map +1 -1
- package/dist/extension/murmur-pi.js +190 -102
- package/dist/extension/murmur-pi.js.map +1 -1
- package/dist/extension/store.js +413 -190
- package/dist/extension/store.js.map +1 -1
- package/dist/index.d.ts +492 -147
- package/dist/index.js +970 -641
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/identity.ts","../../src/paths.ts","../../src/store.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\nimport { stateDir } from \"./paths.js\";\n\nexport type NodeIdentity = {\n host_id: string;\n display_name: string;\n};\n\nexport function loadIdentity(): NodeIdentity | null {\n const path = join(stateDir(), \"identity.json\");\n return existsSync(path) ? JSON.parse(readFileSync(path, \"utf8\")) : null;\n}\n\nexport function ensureIdentity(displayName = hostname()): NodeIdentity {\n const existing = loadIdentity();\n if (existing) return existing;\n\n const identity = { host_id: randomUUID(), display_name: displayName };\n mkdirSync(stateDir(), { recursive: true });\n writeFileSync(join(stateDir(), \"identity.json\"), `${JSON.stringify(identity, null, 2)}\\n`);\n return identity;\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function stateDir(): string {\n return (\n process.env.MURMUR_STATE_DIR ??\n join(process.env.XDG_STATE_HOME ?? join(homedir(), \".local\", \"state\"), \"murmur\")\n );\n}\n\nexport function configDir(): string {\n return (\n process.env.MURMUR_CONFIG_DIR ??\n join(process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\"), \"murmur\")\n );\n}\n\nexport function dbPath(): string {\n return join(stateDir(), \"events.db\");\n}\n","import { rmSync } from \"node:fs\";\nimport Database from \"better-sqlite3\";\nimport { ensureIdentity } from \"./identity.js\";\nimport { dbPath } from \"./paths.js\";\nimport type { Driver, Event, Peer } from \"./types.js\";\n\nconst DEFAULT_RETENTION_MS = 7 * 86_400_000;\n\n/**\n * Local storage shape. Bump on any change to the events or peers tables.\n *\n * Distinct from `SCHEMA_VERSION` in export.ts, which versions the *wire*: a\n * node can change how it stores events without changing what it sends, and a\n * wire change should not throw away local history.\n */\nexport const STORE_VERSION = 2;\n\n/**\n * Migration strategy: there isn't one. A version mismatch deletes the database\n * and starts again.\n *\n * This is only acceptable because nothing in events.db is authoritative or\n * irreplaceable. It is a bounded-retention observability log: remote events\n * re-sync from their authoring peer on the next collect, local agents re-report\n * on their next state change, and node identity deliberately lives in a\n * separate file. If anything durable is ever added here, this stops being safe\n * and a real migration is required.\n *\n * Peers survive, because they are the one thing a human typed. Watermarks are\n * reset with the events they indexed -- keeping them would skip the events the\n * new database no longer has -- and re-reading a peer from zero is free, since\n * ingest is idempotent.\n */\nfunction resetIfStale(path: string): Peer[] {\n let salvaged: Peer[] = [];\n try {\n const existing = new Database(path, { fileMustExist: true });\n const version = (existing.pragma(\"user_version\", { simple: true }) as number) ?? 0;\n if (version === STORE_VERSION) {\n existing.close();\n return salvaged;\n }\n try {\n salvaged = existing\n .prepare(\"SELECT name, target, host_id, display_name FROM peers\")\n .all() as Peer[];\n } catch {\n // Old enough not to have the table, or unreadable. Nothing to save.\n }\n existing.close();\n } catch {\n // No database yet, or one too broken to open. Either way, recreate.\n return salvaged;\n }\n\n // -wal and -shm must go too: a stale sidecar against a fresh main file is a\n // documented way to corrupt sqlite.\n for (const suffix of [\"\", \"-wal\", \"-shm\"]) rmSync(`${path}${suffix}`, { force: true });\n return salvaged;\n}\n\n// The name fields are optional on the way in: a caller that has no name for a\n// thing should not have to say `null` four times, and a non-tmux harness has\n// none of them. They are non-optional on `Event` itself, so a reader never has\n// to distinguish absent from null.\nexport type NewEvent = Omit<\n Event,\n \"host_id\" | \"seq\" | \"ts\" | \"session_name\" | \"window_name\" | \"agent_name\" | \"pi_session\"\n> & {\n ts?: number;\n session_name?: string | null;\n window_name?: string | null;\n agent_name?: string | null;\n pi_session?: string | null;\n};\n\ntype EventRow = Omit<Event, \"synthetic\" | \"extra\"> & {\n synthetic: number;\n extra: string;\n};\n\nfunction eventValues(event: Event): unknown[] {\n return [\n event.host_id,\n event.seq,\n event.ts,\n event.agent_id,\n event.session,\n event.window,\n event.pane,\n event.session_name,\n event.window_name,\n event.agent_name,\n event.pi_session,\n event.workstream,\n event.role,\n event.cli,\n event.driver,\n event.kind,\n event.state,\n event.message,\n event.pid,\n Number(event.synthetic),\n event.reason,\n JSON.stringify(event.extra),\n ];\n}\n\nfunction toEvent(row: EventRow): Event {\n return {\n ...row,\n driver: row.driver as Driver | null,\n synthetic: row.synthetic === 1,\n extra: JSON.parse(row.extra) as Record<string, unknown>,\n };\n}\n\nexport interface Store {\n append(event: NewEvent): Event;\n ingest(events: Event[]): number;\n eventsSince(hostId: string, seq: number): Event[];\n allEvents(): Event[];\n maxSeq(hostId: string): number;\n prune(horizonMs?: number): number;\n peers(): Peer[];\n /**\n * Drop every event for one agent from this node's replica.\n *\n * For a remote agent this is a replica eviction, not a claim about truth: the\n * authoring node still owns it, and a collect re-reads from the watermark if\n * it is still alive.\n */\n forgetAgent(agentId: string): number;\n forgetHost(hostId: string): number;\n upsertPeer(peer: Partial<Peer> & { name: string; target: string }): void;\n removePeer(name: string): boolean;\n close(): void;\n}\n\nexport function openStore(): Store {\n const identity = ensureIdentity();\n const path = dbPath();\n const salvagedPeers = resetIfStale(path);\n const database = new Database(path);\n database.pragma(\"journal_mode = WAL\");\n database.pragma(`user_version = ${STORE_VERSION}`);\n database.exec(`\n CREATE TABLE IF NOT EXISTS events (\n host_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n ts INTEGER NOT NULL,\n agent_id TEXT NOT NULL,\n session TEXT NOT NULL,\n window TEXT NOT NULL,\n pane TEXT NOT NULL,\n session_name TEXT,\n window_name TEXT,\n agent_name TEXT,\n pi_session TEXT,\n workstream TEXT,\n role TEXT,\n cli TEXT,\n driver TEXT,\n kind TEXT NOT NULL,\n state TEXT NOT NULL,\n message TEXT NOT NULL,\n pid INTEGER,\n synthetic INTEGER NOT NULL,\n reason TEXT NOT NULL,\n extra TEXT NOT NULL,\n PRIMARY KEY (host_id, seq)\n );\n CREATE INDEX IF NOT EXISTS events_agent_seq ON events (agent_id, seq);\n CREATE TABLE IF NOT EXISTS peers (\n name TEXT PRIMARY KEY,\n target TEXT NOT NULL,\n host_id TEXT,\n display_name TEXT,\n watermark INTEGER NOT NULL,\n fetched_at INTEGER,\n -- When a jump last proved this peer's tmux was not answering. Reader\n -- state, not an event: this node cannot author facts about another\n -- node's agents, and a jump is a local observation, not something the\n -- peer said. Cleared by the next successful collect.\n tmux_down_at INTEGER\n );\n `);\n\n // Additive migration: an existing peers table predates tmux_down_at.\n try {\n database.exec(\"ALTER TABLE peers ADD COLUMN tmux_down_at INTEGER\");\n } catch {\n // Already present.\n }\n\n // Put back the peers the wipe took, at watermark 0 so the next collect\n // re-reads each one from the start.\n if (salvagedPeers.length > 0) {\n const restore = database.prepare(\n `INSERT OR IGNORE INTO peers (name, target, host_id, display_name, watermark, fetched_at)\n VALUES (?, ?, ?, ?, 0, NULL)`,\n );\n for (const peer of salvagedPeers) {\n restore.run(peer.name, peer.target, peer.host_id ?? null, peer.display_name ?? null);\n }\n }\n\n const eventColumns = `\n host_id, seq, ts, agent_id, session, window, pane,\n session_name, window_name, agent_name, pi_session,\n workstream, role, cli, driver, kind, state, message, pid,\n synthetic, reason, extra`;\n const eventPlaceholders = new Array(22).fill(\"?\").join(\", \");\n const insertEvent = database.prepare(\n `INSERT INTO events (${eventColumns}) VALUES (${eventPlaceholders})`,\n );\n const ingestEvent = database.prepare(\n `INSERT OR IGNORE INTO events (${eventColumns}) VALUES (${eventPlaceholders})`,\n );\n const selectMaxSeq = database.prepare(\n \"SELECT COALESCE(MAX(seq), 0) AS seq FROM events WHERE host_id = ?\",\n );\n const append = database.transaction((event: NewEvent): Event => {\n const row = selectMaxSeq.get(identity.host_id) as { seq: number };\n const stored: Event = {\n ...event,\n host_id: identity.host_id,\n seq: row.seq + 1,\n ts: event.ts ?? Date.now(),\n session_name: event.session_name ?? null,\n window_name: event.window_name ?? null,\n agent_name: event.agent_name ?? null,\n pi_session: event.pi_session ?? null,\n };\n insertEvent.run(...eventValues(stored));\n return stored;\n });\n const ingest = database.transaction((events: Event[]): number => {\n let inserted = 0;\n for (const event of events) inserted += ingestEvent.run(...eventValues(event)).changes;\n return inserted;\n });\n\n return {\n append,\n ingest,\n eventsSince(hostId, seq) {\n const rows = database\n .prepare(\"SELECT * FROM events WHERE host_id = ? AND seq > ? ORDER BY seq\")\n .all(hostId, seq) as EventRow[];\n return rows.map(toEvent);\n },\n allEvents() {\n const rows = database\n .prepare(\"SELECT * FROM events ORDER BY ts, host_id, seq\")\n .all() as EventRow[];\n return rows.map(toEvent);\n },\n maxSeq(hostId) {\n return (selectMaxSeq.get(hostId) as { seq: number }).seq;\n },\n prune(horizonMs = Number(process.env.MURMUR_RETENTION_MS ?? DEFAULT_RETENTION_MS)) {\n return database\n .prepare(`\n DELETE FROM events\n WHERE ts < ?\n AND (host_id, seq) NOT IN (\n SELECT host_id, seq FROM (\n SELECT host_id, seq,\n ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY ts DESC, seq DESC) rn\n FROM events\n ) WHERE rn = 1\n )\n `)\n .run(Date.now() - horizonMs).changes;\n },\n peers() {\n return database.prepare(\"SELECT * FROM peers ORDER BY name\").all() as Peer[];\n },\n forgetAgent(agentId) {\n return database.prepare(\"DELETE FROM events WHERE agent_id = ?\").run(agentId).changes;\n },\n forgetHost(hostId) {\n // Every replicated row for one origin node. Only ever called about a\n // REMOTE host: the local host's rows are this node's own authorship and\n // the retention horizon owns them.\n return database.prepare(\"DELETE FROM events WHERE host_id = ?\").run(hostId).changes;\n },\n upsertPeer(peer) {\n const current = database.prepare(\"SELECT * FROM peers WHERE name = ?\").get(peer.name) as\n | Peer\n | undefined;\n database\n .prepare(`\n INSERT INTO peers (name, target, host_id, display_name, watermark, fetched_at, tmux_down_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n target = excluded.target,\n host_id = excluded.host_id,\n display_name = excluded.display_name,\n watermark = excluded.watermark,\n fetched_at = excluded.fetched_at,\n tmux_down_at = excluded.tmux_down_at\n `)\n .run(\n peer.name,\n peer.target,\n peer.host_id !== undefined ? peer.host_id : (current?.host_id ?? null),\n peer.display_name !== undefined ? peer.display_name : (current?.display_name ?? null),\n peer.watermark !== undefined ? peer.watermark : (current?.watermark ?? 0),\n peer.fetched_at !== undefined ? peer.fetched_at : (current?.fetched_at ?? null),\n peer.tmux_down_at !== undefined ? peer.tmux_down_at : (current?.tmux_down_at ?? null),\n );\n },\n removePeer(name) {\n // Drops the peer and its watermark. Replicated events stay: they are\n // real history authored elsewhere, and the retention horizon already\n // ages them out. Re-adding the peer re-syncs from zero, which ingest\n // makes free.\n return database.prepare(\"DELETE FROM peers WHERE name = ?\").run(name).changes > 0;\n },\n close() {\n database.close();\n },\n };\n}\n"],"mappings":"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","names":["join","join"]}
|
|
1
|
+
{"version":3,"sources":["../../src/identity.ts","../../src/paths.ts","../../src/store.ts","../../src/ids.ts","../../src/mux.ts","../../src/version.ts","../../src/view.ts"],"sourcesContent":["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 { 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","/**\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 */\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}\",\n ]);\n const [session, window, sessionName, windowName] = 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: windowName || null,\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 { 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","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"],"mappings":"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","names":["join","join","randomUUID","mkdirSync","require","mkdirSync","agentId","randomUUID"]}
|