/** * THE TRANSPORT SEAM (Phase 5.4 Task 2). * * This interface is DERIVED from the 12 `"tmux-push"` literals and the 7 * `spawnSync("tmux", …)` shell-outs that were spread across `tools/`, not * designed top-down. Where the phase doc's sketch and the code disagreed, the * code won — three times, each recorded below, because the gate for this task is * that a running fleet cannot tell the difference. * * 1. `tmux-push-remote` IS A LIVE KIND. The doc's sketch had two kinds; the code * has three. `registry.ts` gives remote markers heartbeat-based liveness * because their pid is 0 on a foreign host, and `server.ts` documents the * value as the wire contract for `scripts/coord-pusher.mjs`. A union without * it silently reclassifies every remote agent as not-tmux. * 2. THE MARKER HAS NINE FIELDS, NOT SIX. `scriptMtime`, `serverBuildMtime` and * `rooms` are absent from the sketch and each one closes a defect that * actually shipped. They are kept verbatim, including the rule they share: * ABSENT MEANS UNKNOWN, NEVER "ON" — the same principle as `Liveness.unknown` * below, one field deeper. * 3. `target` IS ADDITIVE, NOT A RENAME. See `targetOf`. */ /** * Every transport value that can appear in a marker on disk. * * Not `string`: the whole defect being fixed is code branching on a literal * instead of reading the field, and a `string` discriminant cannot tell the * compiler that a branch was missed. Not a two-member union either — see (1). */ /** * ⟨q-ec020f6a⟩ LOCAL tmux-push was DELETED outright (0 of 13 live seats used it; keeping * it meant a second delivery mechanism — hooks/tmux-pusher.mjs, now removed — plus a * role-card precondition that halted every live herdr seat on a false requirement). * * `TMUX_PUSH` stays DEFINED AND EXPORTED, but OUT of `TransportKind`/`TRANSPORT_KINDS`, for * exactly one reason: a marker written to disk before this change can still literally read * `"tmux-push"`, and `isLocallyProbeable`/`isTmuxKind` below must keep classifying that * HISTORICAL value correctly (never `"dead"` where it used to read `"unknown"`) even * though nothing can construct a NEW one — `resolveTransport` has no case for it and * `config.ts` refuses it. Classifying old data and constructing new data are different * questions; removing it from the union answers only the second. */ export const TMUX_PUSH = "tmux-push" as const; export const TMUX_PUSH_REMOTE = "tmux-push-remote" as const; export const HERDR = "herdr" as const; export type TransportKind = typeof TMUX_PUSH_REMOTE | typeof HERDR; export const TRANSPORT_KINDS: readonly TransportKind[] = [TMUX_PUSH_REMOTE, HERDR] as const; /** * The tmux family, for LIVENESS CLASSIFICATION of whatever a marker on disk says — * including the historical, no-longer-constructible local kind (`TMUX_PUSH`, above). * Both members are delivered by a pusher process typing into a pane; they differed in * WHERE that pane was, which is why liveness splits below. * * THIS IS THE ONE PLACE THE TMUX LITERALS LIVE. Twelve call sites used to spell * them; they now ask. */ const TMUX_KINDS = new Set([TMUX_PUSH, TMUX_PUSH_REMOTE]); /** Is this marker carried by the tmux family (local OR remote)? */ export function isTmuxKind(transport: string | undefined): boolean { return transport !== undefined && TMUX_KINDS.has(transport); } /** * Is this marker's pane on THIS host, so that a local probe means anything? * * The distinction every `marker.transport !== "tmux-push"` site was making by * hand, with a comment explaining "remote = can't verify". Naming it stops the * next person re-deriving it — and re-deriving it wrongly is how a remote agent * gets reported dead because a pane that was never local did not answer. */ export function isLocallyProbeable(transport: string | undefined): boolean { return transport === TMUX_PUSH; } /** * ⟨q-f995c3c7⟩ THE PANE THIS PROCESS IS RUNNING IN, read from ITS OWN ENVIRONMENT — never from a * caller's argument, which is what would make it spoofable. * * The first-claim guard's same-pane exception asked `process.env.TMUX_PANE` only, and a herdr seat * has no TMUX_PANE: it has HERDR_PANE_ID. So a herdr seat restarting into its OWN pane could never * satisfy the exception, and every one of them was refused against its own leftover marker. Measured * 2026-09-23: 6 of 6 seats, cleared only by moving the markers aside by hand. */ export function ownPaneTarget(transport: string | undefined, env: NodeJS.ProcessEnv = process.env): string | undefined { if (transport === HERDR) return env.HERDR_PANE_ID || undefined; if (isLocallyProbeable(transport)) return env.TMUX_PANE || undefined; return undefined; } /** * Is liveness for this marker decided by the registry heartbeat rather than by a * local pid? True only for the remote kind, whose pid is 0 on a foreign host. */ export function isRemoteTmuxKind(transport: string | undefined): boolean { return transport === TMUX_PUSH_REMOTE; } export type TransportMarker = { agentId: string; transport: string; pid: number; /** * Transport-agnostic address: a tmux pane id, later a herdr session id. * * ADDITIVE AND DUAL-WRITTEN, not a rename of `tmuxTarget`. Measured before * choosing: every marker on the live fleet's disk carries `tmuxTarget` and * none carries `target`. The phase doc asks for a read-migration and its own * Rollback Plan asks for no format change until the migration is proven on a * live fleet; writing both satisfies each. A marker written with `target` * alone is unreadable to the code a merge-revert restores, and that failure * does not degrade — it silences every lane at once. */ target?: string; /** The original field. Still written. See `target`. */ tmuxTarget?: string; /** * ⟨q-abd88dd4⟩ herdr only: why `pid` holds what it holds (see `herdrMarkerPid`). A herdr * marker's pid is addressed to PRE-HERDR readers and is never liveness for this build. */ pidWhy?: string; since: number; /** * Remote pushers run on a different machine; the local pid is meaningless, * so we tag the host and use heartbeat-based liveness instead of pidAlive. */ host?: string; /** * mtime of the pusher script the daemon loaded into memory at spawn time * (epoch ms). When the on-disk script is upgraded but the daemon isn't * restarted, doctor() compares this to the current mtime to flag a stale * pusher — the class of bug that silently dropped /clear /compact in v0.8.1. * Absent on markers written by older versions (treated as "unknown, skip"). */ scriptMtime?: number; /** * Build identity of the MCP server whose attach_agent stamped this marker. * A marker stamped by a server predating the current on-disk build was * written by attach/stamp logic the rebuild replaced. Absent means unknown. */ serverBuildMtime?: number; /** * Does this transport carry ROOM traffic, or DMs only? * * ABSENT MEANS UNKNOWN, NEVER "ON". A `--no-room` pusher used to look * identical to a full one, so `status` said `attached: true` while an agent * sat with its room feed off — that is how a worker missed its channel * traffic. A marker from an older pusher cannot tell us, and reporting an * unasked question as full capability is the defect this field removes. */ rooms?: boolean; /** * ⟨q-ec020f6a⟩(A) herdr ONLY: the `agent_session.value` herdr itself reported for this * marker's pane at ATTACH TIME (`herdr pane get ` — populated from the pane's own * live process, e.g. a Claude Code resume-session-id; never asserted by the attaching * caller). This is what makes the pane a checkable identity rather than a trusted claim: * `resolveBoundAgent`'s pane path re-reads the pane's CURRENT `agent_session.value` on * every request and refuses if it no longer matches what was recorded here — the same * pane_id with a DIFFERENT live session behind it (a restart, a reused pane, a swap) is * exactly the class ⟨q-e439e4ad⟩/⟨q-8c23e5a3⟩ named. Absent means unknown (herdr had no * session for that pane at attach time, or this marker predates the field) — never treated * as a match. */ herdrAgentSession?: string; }; /** * Read a marker's address, preferring the generic field and falling back to the * tmux-specific one. * * This is the read half of the dual-write. Call it rather than touching either * field: a consumer that reads only `tmuxTarget` goes blind the day a herdr * marker appears, and one that reads only `target` is blind to every marker on * disk today. */ export function targetOf(marker: Pick): string | undefined { return marker.target ?? marker.tmuxTarget; } /** * Stamp an address into BOTH fields, so the marker is legible to the current * code and to whatever a revert restores. */ export function withTarget(marker: T, target: string | undefined): T & { target?: string; tmuxTarget?: string } { if (target === undefined) return marker; return { ...marker, target, tmuxTarget: target }; } /** * THE THIRD STATE IS NOT A COURTESY, IT IS THE POINT. * * "Cannot probe" must stay expressible and distinct from "dead": a transport * that cannot answer is not evidence of a dead agent, and collapsing the two * reaps live sessions. PRODUCTION_ROADMAP Phase 5.3 states the rule; the * wedged-pusher reaping in `doctor` depends on it. */ export type Liveness = | { state: "live" } | { state: "dead"; reason: string } | { state: "unknown"; reason: string }; /** * ⟨q-1c95f7d4⟩ Phase 5.4 Task 5 — THE EXTERNAL TICK'S VOCABULARY, TAKEN FROM THE TOOL THAT * OWNS IT. Measured 2026-09-15 in a workspace of my own: `herdr pane report-agent --help` * enumerates exactly `idle, working, blocked, unknown`, and a pane carries the state an * external source published (`"agent":"