agent-coord-mcp 0.26.21 → 0.26.22
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/capabilities.js +250 -2
- package/dist/capabilities.js.map +1 -1
- package/dist/closing-line.js +83 -0
- package/dist/closing-line.js.map +1 -0
- package/dist/commit-cite.js +55 -0
- package/dist/commit-cite.js.map +1 -0
- package/dist/gated-head.js +67 -20
- package/dist/gated-head.js.map +1 -1
- package/dist/server-spread.js +195 -0
- package/dist/server-spread.js.map +1 -0
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/store.js +32 -0
- package/dist/store.js.map +1 -1
- package/dist/tools/away.js +67 -7
- package/dist/tools/away.js.map +1 -1
- package/dist/tools/board-ref.js +44 -4
- package/dist/tools/board-ref.js.map +1 -1
- package/dist/tools/event-kinds.js +5 -1
- package/dist/tools/event-kinds.js.map +1 -1
- package/dist/tools/events.js +31 -2
- package/dist/tools/events.js.map +1 -1
- package/dist/tools/messaging.js +64 -6
- package/dist/tools/messaging.js.map +1 -1
- package/dist/tools/record-events.js +85 -5
- package/dist/tools/record-events.js.map +1 -1
- package/dist/tools/records.js +231 -38
- package/dist/tools/records.js.map +1 -1
- package/dist/tools/registry.js +52 -2
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/seat-build.js +173 -0
- package/dist/tools/seat-build.js.map +1 -0
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/stall.js +1095 -18
- package/dist/tools/stall.js.map +1 -1
- package/dist/tools/transport.js +21 -2
- package/dist/tools/transport.js.map +1 -1
- package/dist/tools/worktrees.js +14 -0
- package/dist/tools/worktrees.js.map +1 -1
- package/package.json +1 -1
- package/scripts/coord-attention-clock.mjs +2 -0
- package/scripts/coord-stall-clock.mjs +52 -11
- package/src/capabilities.ts +264 -2
- package/src/closing-line.ts +85 -0
- package/src/commit-cite.ts +58 -0
- package/src/gated-head.ts +128 -26
- package/src/server-spread.ts +233 -0
- package/src/server.ts +2 -2
- package/src/store.ts +32 -0
- package/src/tools/away.ts +82 -9
- package/src/tools/board-ref.ts +70 -3
- package/src/tools/event-kinds.ts +17 -1
- package/src/tools/events.ts +33 -2
- package/src/tools/messaging.ts +63 -6
- package/src/tools/record-events.ts +78 -5
- package/src/tools/records.ts +248 -38
- package/src/tools/registry.ts +54 -3
- package/src/tools/seat-build.ts +194 -0
- package/src/tools/shared.ts +22 -0
- package/src/tools/stall.ts +1266 -23
- package/src/tools/transport.ts +21 -2
- package/src/tools/worktrees.ts +13 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ⟨q-8a3f1c05⟩ — A HALF-RESTARTED SEAT IS A STATEMENT, NOT AN INFERENCE.
|
|
3
|
+
*
|
|
4
|
+
* Each seat runs TWO long-lived processes from the installed tree: the MCP server
|
|
5
|
+
* (answers `capabilities`) and the tmux pusher (`hooks/tmux-pusher.mjs --agent <id>`,
|
|
6
|
+
* pastes into the pane). A restart cycles the server and leaves the pusher, so a seat
|
|
7
|
+
* comes back running new server code behind an old pusher — fully responsive, `online:
|
|
8
|
+
* true`, and invisible to every instrument that reads the server. Measured 2026-09-14
|
|
9
|
+
* during the staged restart: stage 1's pusher was three days older than its server and
|
|
10
|
+
* was found only because that seat happened to look; a stale pusher still delivers.
|
|
11
|
+
*
|
|
12
|
+
* THE INSTRUMENT ALREADY EXISTED AND NOBODY CALLED IT: pushers self-identify on the
|
|
13
|
+
* command line (`--agent <id>`), so `ps` + `--agent` + the installed HOOK's mtime is a
|
|
14
|
+
* per-seat pusher-staleness probe. This module states it, per seat, beside the server
|
|
15
|
+
* half — in words, not a pair of timestamps a reader has to subtract.
|
|
16
|
+
*
|
|
17
|
+
* THE HOOK'S OWN MTIME, NEVER A SIBLING'S: `hooks/tmux-pusher.mjs` is the file the
|
|
18
|
+
* pusher loads. `package.json`, `dist/…` and the hook all carry the same instant under
|
|
19
|
+
* a whole-tree install, but an install that rewrites some files and not others breaks
|
|
20
|
+
* that, and nothing checks it. So the pusher half reads the hook file directly.
|
|
21
|
+
*
|
|
22
|
+
* THE LIMIT THIS CARRIES RATHER THAN SOLVES: servers carry no `--agent`, so the server
|
|
23
|
+
* side cannot be keyed to a seat from `ps`. Only the ANSWERING process knows its own
|
|
24
|
+
* start time (`answeredBy.pid`). For every other seat the server half is reported as
|
|
25
|
+
* UNOBSERVABLE from here — said, not inferred from the marker's attach-time stamp.
|
|
26
|
+
*/
|
|
27
|
+
import { execFileSync } from "node:child_process";
|
|
28
|
+
import { statSync } from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import type { TransportMarker } from "../transports/types.js";
|
|
31
|
+
|
|
32
|
+
export type ProcessFacts = { pid: number; startedAt: number | null; command: string | null; alive: boolean };
|
|
33
|
+
export type PsReader = (pid: number) => ProcessFacts;
|
|
34
|
+
export type StatReader = (p: string) => { mtimeMs: number };
|
|
35
|
+
export type Installed = { module: string; hookPath: string; hookMtime: number | null; buildMtime: number | null };
|
|
36
|
+
export type ServerFacts = { pid: number; startedAt: number; buildMtime: number | null } | null;
|
|
37
|
+
|
|
38
|
+
export type SeatBuild = {
|
|
39
|
+
agentId: string;
|
|
40
|
+
verdict: "current" | "half-restarted" | "stale" | "unknown";
|
|
41
|
+
statement: string;
|
|
42
|
+
server: { observable: boolean; pid: number | null; startedAt: string | null; buildMtime: string | null; current: boolean | null; note: string };
|
|
43
|
+
pusher: {
|
|
44
|
+
pid: number | null;
|
|
45
|
+
alive: boolean | null;
|
|
46
|
+
keyedByAgentArg: boolean | null;
|
|
47
|
+
startedAt: string | null;
|
|
48
|
+
/** The hook file THIS pusher loads, read off its own command line — never a sibling, never the server's tree. */
|
|
49
|
+
hookPath: string | null;
|
|
50
|
+
hookMtime: string | null;
|
|
51
|
+
/** false when the pusher runs from a different tree than the answering server (a dev checkout vs the global install, or two installs). */
|
|
52
|
+
sameTreeAsServer: boolean | null;
|
|
53
|
+
current: boolean | null;
|
|
54
|
+
note: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const iso = (ms: number | null | undefined) => (typeof ms === "number" && Number.isFinite(ms) ? new Date(ms).toISOString() : null);
|
|
59
|
+
|
|
60
|
+
/** `ps` for one pid: start time and command line. `lstart` is portable across macOS and Linux. */
|
|
61
|
+
export const psReader: PsReader = (pid) => {
|
|
62
|
+
try {
|
|
63
|
+
const out = execFileSync("ps", ["-o", "lstart=,command=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
64
|
+
if (!out) return { pid, startedAt: null, command: null, alive: false };
|
|
65
|
+
// lstart is 24 chars ("Mon Sep 14 14:38:24 2026"), then the command.
|
|
66
|
+
const m = /^(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(.*)$/.exec(out);
|
|
67
|
+
const startedAt = m ? Date.parse(m[1]) : NaN;
|
|
68
|
+
return { pid, startedAt: Number.isFinite(startedAt) ? startedAt : null, command: m ? m[2] : out, alive: true };
|
|
69
|
+
} catch {
|
|
70
|
+
return { pid, startedAt: null, command: null, alive: false };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** The installed tree's hook and build stamps, read from the files themselves. */
|
|
75
|
+
export function installedFrom(moduleRoot: string, stat: (p: string) => { mtimeMs: number } = statSync): Installed {
|
|
76
|
+
const hookPath = path.join(moduleRoot, "hooks", "tmux-pusher.mjs");
|
|
77
|
+
const read = (p: string) => {
|
|
78
|
+
try {
|
|
79
|
+
return stat(p).mtimeMs;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
return { module: moduleRoot, hookPath, hookMtime: read(hookPath), buildMtime: read(path.join(moduleRoot, "package.json")) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const day = (ms: number) => new Date(ms).toISOString().slice(0, 10);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* One seat's build state: the pusher half from its marker's pid via `ps`, keyed by the
|
|
91
|
+
* `--agent` argument on that process's own command line; the server half from the
|
|
92
|
+
* answering process when the seat IS the answering process, else unobservable.
|
|
93
|
+
*/
|
|
94
|
+
/** The hook file a pusher loads, read off its own command line: `node <path>/hooks/tmux-pusher.mjs --agent <id>`. */
|
|
95
|
+
export function hookPathOf(command: string): string | null {
|
|
96
|
+
const m = /(\S*\/hooks\/tmux-pusher\.mjs)(?=\s|$)/.exec(command);
|
|
97
|
+
return m ? m[1] : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function seatBuildOf(input: { agentId: string; marker: TransportMarker | undefined; installed: Installed; ps: PsReader; server: ServerFacts; stat?: StatReader }): SeatBuild {
|
|
101
|
+
const { agentId, marker, installed, ps, server } = input;
|
|
102
|
+
const stat = input.stat ?? statSync;
|
|
103
|
+
|
|
104
|
+
// ---- pusher half
|
|
105
|
+
let pusher: SeatBuild["pusher"];
|
|
106
|
+
const empty = { pid: null, alive: null, keyedByAgentArg: null, startedAt: null, hookPath: null, hookMtime: null, sameTreeAsServer: null, current: null };
|
|
107
|
+
if (!marker || typeof marker.pid !== "number") {
|
|
108
|
+
pusher = { ...empty, note: "no transport marker for this seat — no pusher pid to read" };
|
|
109
|
+
} else {
|
|
110
|
+
const facts = ps(marker.pid);
|
|
111
|
+
if (!facts.alive) {
|
|
112
|
+
pusher = { ...empty, pid: marker.pid, alive: false, note: `the marker names pid ${marker.pid} and no such process is running — the pusher is dead, not stale` };
|
|
113
|
+
} else {
|
|
114
|
+
const cmd = facts.command ?? "";
|
|
115
|
+
const keyed = /tmux-pusher\.mjs/.test(cmd) && new RegExp(`--agent\\s+${agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`).test(cmd);
|
|
116
|
+
if (!keyed) {
|
|
117
|
+
pusher = { ...empty, pid: marker.pid, alive: true, keyedByAgentArg: false, startedAt: iso(facts.startedAt), note: `pid ${marker.pid} is alive but its command line does not say \`tmux-pusher.mjs --agent ${agentId}\` — the marker's pid is not this seat's pusher, so nothing is claimed about it` };
|
|
118
|
+
} else {
|
|
119
|
+
// THE VERY FILE THIS PUSHER LOADS. Its path is on the command line; the mtime is
|
|
120
|
+
// read from that path, not from the answering server's tree — a dev checkout
|
|
121
|
+
// answering for a fleet of global-install pushers would otherwise call every
|
|
122
|
+
// pusher stale against a file none of them ever loaded (measured while building
|
|
123
|
+
// this: 6 of 6 read stale against a worktree's hook, all 6 current against their own).
|
|
124
|
+
const hookPath = hookPathOf(cmd) ?? installed.hookPath;
|
|
125
|
+
const fromOwnCmd = hookPathOf(cmd) !== null;
|
|
126
|
+
let hookMtime: number | null = null;
|
|
127
|
+
try {
|
|
128
|
+
hookMtime = stat(hookPath).mtimeMs;
|
|
129
|
+
} catch {
|
|
130
|
+
hookMtime = null;
|
|
131
|
+
}
|
|
132
|
+
const sameTree = path.resolve(path.dirname(path.dirname(hookPath))) === path.resolve(installed.module);
|
|
133
|
+
if (facts.startedAt === null || hookMtime === null) {
|
|
134
|
+
pusher = { ...empty, pid: marker.pid, alive: true, keyedByAgentArg: true, startedAt: iso(facts.startedAt), hookPath, sameTreeAsServer: sameTree, note: facts.startedAt === null ? "ps gave no start time for the pusher" : `the hook ${hookPath} could not be read` };
|
|
135
|
+
} else {
|
|
136
|
+
const current = facts.startedAt >= hookMtime;
|
|
137
|
+
pusher = {
|
|
138
|
+
pid: marker.pid,
|
|
139
|
+
alive: true,
|
|
140
|
+
keyedByAgentArg: true,
|
|
141
|
+
startedAt: iso(facts.startedAt),
|
|
142
|
+
hookPath,
|
|
143
|
+
hookMtime: iso(hookMtime),
|
|
144
|
+
sameTreeAsServer: sameTree,
|
|
145
|
+
current,
|
|
146
|
+
note:
|
|
147
|
+
(current
|
|
148
|
+
? `pusher started ${iso(facts.startedAt)}, after its hook ${hookPath} (${iso(hookMtime)}) — it loaded that installed code`
|
|
149
|
+
: `pusher started ${iso(facts.startedAt)}, BEFORE its hook ${hookPath} (${iso(hookMtime)}) — it runs the code it loaded then, and still delivers`) +
|
|
150
|
+
(fromOwnCmd ? "" : " (hook path not on the command line; the answering server's tree was used)") +
|
|
151
|
+
(sameTree ? "" : `; NOTE the pusher's tree differs from the answering server's (${installed.module})`),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---- server half
|
|
159
|
+
let srv: SeatBuild["server"];
|
|
160
|
+
if (!server) {
|
|
161
|
+
srv = { observable: false, pid: null, startedAt: null, buildMtime: iso(installed.buildMtime), current: null, note: "the server side cannot be keyed to a seat from ps (servers carry no --agent); only the answering process knows its own start time (answeredBy.pid). Ask this seat's own `capabilities`." };
|
|
162
|
+
} else if (server.buildMtime === null) {
|
|
163
|
+
srv = { observable: true, pid: server.pid, startedAt: iso(server.startedAt), buildMtime: null, current: null, note: "the installed package.json could not be read, so the server's build cannot be placed" };
|
|
164
|
+
} else {
|
|
165
|
+
const current = server.startedAt >= server.buildMtime;
|
|
166
|
+
srv = { observable: true, pid: server.pid, startedAt: iso(server.startedAt), buildMtime: iso(server.buildMtime), current, note: current ? `server started ${iso(server.startedAt)}, on the ${day(server.buildMtime)} install` : `server started ${iso(server.startedAt)}, BEFORE the ${day(server.buildMtime)} install` };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---- the statement
|
|
170
|
+
let verdict: SeatBuild["verdict"];
|
|
171
|
+
let statement: string;
|
|
172
|
+
const p = pusher.current;
|
|
173
|
+
const s = srv.current;
|
|
174
|
+
if (s === true && p === true) {
|
|
175
|
+
verdict = "current";
|
|
176
|
+
statement = `${agentId}: current — server and pusher both started after the ${day(server!.buildMtime!)} install.`;
|
|
177
|
+
} else if (s === false && p === false) {
|
|
178
|
+
verdict = "stale";
|
|
179
|
+
statement = `${agentId}: stale — server and pusher both predate the install; nothing on this seat runs the installed code.`;
|
|
180
|
+
} else if (s === true && p === false) {
|
|
181
|
+
verdict = "half-restarted";
|
|
182
|
+
statement = `${agentId}: HALF-RESTARTED — server on the ${day(server!.buildMtime!)} install, pusher started before it (${pusher.startedAt}). The seat sends through new code and receives through old; it looks healthy. Cycle the pusher: detach_agent, then attach_agent, from this seat.`;
|
|
183
|
+
} else if (s === false && p === true) {
|
|
184
|
+
verdict = "half-restarted";
|
|
185
|
+
statement = `${agentId}: HALF-RESTARTED the other way — pusher on the installed hook, server started before the ${day(server!.buildMtime!)} install. Reload the server (relaunch the MCP client for this seat).`;
|
|
186
|
+
} else if (s === null && p !== null) {
|
|
187
|
+
verdict = "unknown";
|
|
188
|
+
statement = `${agentId}: pusher ${p ? "current" : "STALE — started before the installed hook"}; server UNOBSERVABLE from here (${srv.note}).`;
|
|
189
|
+
} else {
|
|
190
|
+
verdict = "unknown";
|
|
191
|
+
statement = `${agentId}: unknown — ${pusher.note}${srv.observable ? "" : `; ${srv.note}`}`;
|
|
192
|
+
}
|
|
193
|
+
return { agentId, verdict, statement, server: srv, pusher };
|
|
194
|
+
}
|
package/src/tools/shared.ts
CHANGED
|
@@ -86,6 +86,28 @@ export type AgentEntry = {
|
|
|
86
86
|
// OTHER agents' context on every multi-line send. That is why it is stamped,
|
|
87
87
|
// counted, and reviewed rather than self-service.
|
|
88
88
|
proseOnly?: { since: number; reason?: string };
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
* ⛔⛆ THE ANSWERING PROCESS, PUBLISHED — `⟨q-cec42e20⟩`. ADDITIVE AND OPTIONAL:
|
|
92
|
+
* absent on every pre-existing entry, and no reader that ignores unknown keys changes
|
|
93
|
+
* behaviour.
|
|
94
|
+
*
|
|
95
|
+
* Two seats ran the same verb, got `153` and `138`, and both servers honestly reported
|
|
96
|
+
* `versionLabel 0.26.20`. Nothing detected it — each seat had to VOLUNTEER it.
|
|
97
|
+
*
|
|
98
|
+
* ⭐ WHY THESE TWO FIELDS AND NOT A VERSION: a server loads its code once, at spawn,
|
|
99
|
+
* so START TIME against the installed build is the axis that separates two processes
|
|
100
|
+
* running different code. A label cannot: it AGREED while the behaviour differed, and
|
|
101
|
+
* `serverBuildMtime` cannot either — it is stamped at ATTACH and read IDENTICAL across
|
|
102
|
+
* all six live transports while the spread was live.
|
|
103
|
+
*
|
|
104
|
+
* ⚠ AND IT IS THE SERVER, NEVER THE PUSHER. `transports/<agent>.json` already carries a
|
|
105
|
+
* pid and it is the pusher's — a different process, on different code, which is the
|
|
106
|
+
* distinction this fleet paid for twice on 2026-09-12.
|
|
107
|
+
*/
|
|
108
|
+
serverPid?: number;
|
|
109
|
+
serverStartedAt?: number;
|
|
110
|
+
serverModule?: string;
|
|
89
111
|
};
|
|
90
112
|
|
|
91
113
|
/**
|