agent-coord-mcp 0.26.21 → 0.26.23
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 +270 -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 +40 -4
- package/dist/tools/events.js.map +1 -1
- package/dist/tools/herdr-delivery.js +99 -0
- package/dist/tools/herdr-delivery.js.map +1 -0
- package/dist/tools/messaging.js +72 -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 +310 -44
- package/dist/tools/records.js.map +1 -1
- package/dist/tools/registry.js +67 -3
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/seat-build.js +182 -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 +71 -3
- package/dist/tools/transport.js.map +1 -1
- package/dist/tools/worktrees.js +14 -0
- package/dist/tools/worktrees.js.map +1 -1
- package/dist/transports/herdr.js +297 -0
- package/dist/transports/herdr.js.map +1 -0
- package/dist/transports/index.js +10 -4
- package/dist/transports/index.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 +284 -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 +19 -2
- package/src/tools/events.ts +42 -4
- package/src/tools/herdr-delivery.ts +87 -0
- package/src/tools/messaging.ts +71 -6
- package/src/tools/record-events.ts +78 -5
- package/src/tools/records.ts +316 -44
- package/src/tools/registry.ts +68 -4
- package/src/tools/seat-build.ts +201 -0
- package/src/tools/shared.ts +22 -0
- package/src/tools/stall.ts +1266 -23
- package/src/tools/transport.ts +69 -2
- package/src/tools/worktrees.ts +13 -0
- package/src/transports/herdr.ts +311 -0
- package/src/transports/index.ts +10 -4
package/src/tools/registry.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { detachAgentTool } from "./transport.js";
|
|
2
|
-
import { isLocallyProbeable, isRemoteTmuxKind, targetOf } from "../transports/index.js";
|
|
2
|
+
import { isLocallyProbeable, isRemoteTmuxKind, targetOf, HERDR, HerdrTransport, activeTransport } from "../transports/index.js";
|
|
3
3
|
import { readAway, secondCoordinatorRefusal } from "./away.js";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { existsSync, openSync, watch } from "node:fs";
|
|
5
|
+
import { existsSync, openSync, statSync, watch } from "node:fs";
|
|
6
6
|
import { promises as fsp } from "node:fs";
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { detectSpread, installedBuild } from "../server-spread.js";
|
|
9
10
|
import { z } from "zod";
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import {
|
|
@@ -53,7 +54,8 @@ import {
|
|
|
53
54
|
type RoomRegistry,
|
|
54
55
|
type SessionBinding,
|
|
55
56
|
} from "../store.js";
|
|
56
|
-
import { recordAuthorityFor, resolveRole, CANONICAL_ROLE_IDS, roleInputSchema, type RoleArg } from "../roles.js";
|
|
57
|
+
import { recordAuthorityFor, resolveRole, CANONICAL_ROLE_IDS, roleInputSchema, isHuman, type RoleArg } from "../roles.js";
|
|
58
|
+
import { readHumans, recordHuman } from "../store.js";
|
|
57
59
|
import {
|
|
58
60
|
type AgentEntry,
|
|
59
61
|
type AgentRegistry,
|
|
@@ -180,6 +182,10 @@ export async function registerTool(args: {
|
|
|
180
182
|
return current;
|
|
181
183
|
});
|
|
182
184
|
const entry = reg[args.agentId];
|
|
185
|
+
// ⟨q-178878aa⟩ — a HUMAN registration is recorded durably: the registry entry itself is
|
|
186
|
+
// evicted after EVICT_MS (no heartbeat, no transport), and the send path's exemption
|
|
187
|
+
// must still see the human after that.
|
|
188
|
+
if (isHuman(entry)) await recordHuman(args.agentId, { displayName: entry.role, by: `register:${args.agentId}` });
|
|
183
189
|
// Echo the record types this role may and may not emit. Record authority is
|
|
184
190
|
// otherwise invisible until the first typed send is refused mid-work — this
|
|
185
191
|
// is how an agent whose role owns `go`/`scope`/`verdict` finds out at
|
|
@@ -287,6 +293,23 @@ export async function heartbeatTool(args: { agentId: string }) {
|
|
|
287
293
|
return current;
|
|
288
294
|
}
|
|
289
295
|
current[args.agentId].lastHeartbeat = Date.now();
|
|
296
|
+
/*
|
|
297
|
+
* ⛔ STAMP THE ANSWERING PROCESS — `⟨q-cec42e20⟩`. This runs INSIDE the server that
|
|
298
|
+
* is answering, which is the only place these two facts exist: `capabilities`
|
|
299
|
+
* reports them solely to its own caller, and no seat can query another seat's
|
|
300
|
+
* server at all. Publishing them here is what makes a cross-seat comparison
|
|
301
|
+
* possible without every seat having to volunteer it in prose.
|
|
302
|
+
*
|
|
303
|
+
* `startedAt` is derived from uptime rather than read from a file: an mtime tracks
|
|
304
|
+
* writes (a reinstall of identical bytes moves it) while uptime is a fact about
|
|
305
|
+
* THIS process.
|
|
306
|
+
*/
|
|
307
|
+
current[args.agentId].serverPid = process.pid;
|
|
308
|
+
current[args.agentId].serverStartedAt = Date.now() - Math.round(process.uptime() * 1000);
|
|
309
|
+
// ⛔ WHAT this process is executing, not what it is labelled. Resolved from this
|
|
310
|
+
// module's own URL, so a server running a dev `dist/` says so instead of inheriting
|
|
311
|
+
// the installed path — the case that made a 3-build fleet read AGREED.
|
|
312
|
+
current[args.agentId].serverModule = installedBuild(import.meta.url, statSync)?.module;
|
|
290
313
|
return current;
|
|
291
314
|
});
|
|
292
315
|
if (missing) return { ok: false, error: `agent '${args.agentId}' not registered` };
|
|
@@ -331,6 +354,8 @@ export async function listAgentsTool() {
|
|
|
331
354
|
// heartbeat. This stamp only ever overwrote a true value with a
|
|
332
355
|
// simultaneous one.
|
|
333
356
|
if (liveTransports.has(id)) continue;
|
|
357
|
+
// A human has no heartbeat to go stale; evicting one is how the exemption went blind.
|
|
358
|
+
if (isHuman(entry)) continue;
|
|
334
359
|
if (now - entry.lastHeartbeat > EVICT_MS) {
|
|
335
360
|
evicted.push(id);
|
|
336
361
|
delete current[id];
|
|
@@ -417,7 +442,33 @@ export async function listAgentsTool() {
|
|
|
417
442
|
"granted to the SENDER and paid by every READER: their multi-line messages cannot be slimmed. " +
|
|
418
443
|
"A rising share means the rule is decaying.",
|
|
419
444
|
};
|
|
420
|
-
|
|
445
|
+
/*
|
|
446
|
+
* ⛔⛆ AND WHETHER THOSE SEATS AGREE ABOUT WHAT THEY ARE RUNNING — `⟨q-cec42e20⟩`.
|
|
447
|
+
*
|
|
448
|
+
* Reported HERE because this is the verb that already answers "who is live", and the
|
|
449
|
+
* spread is a property of that same population: a reader asking who is on the bus is
|
|
450
|
+
* exactly the reader who needs to know their servers are not the same build.
|
|
451
|
+
*
|
|
452
|
+
* ⚠ IT NEVER REPORTS AGREEMENT IT DID NOT ESTABLISH. A seat that has not stamped its
|
|
453
|
+
* identity is UNCOMPARABLE and poisons the verdict to CANNOT_COMPARE rather than being
|
|
454
|
+
* dropped from the population — because "every seat I could read agrees" and "the
|
|
455
|
+
* fleet agrees" are different claims, and only the second is what a reader will act on.
|
|
456
|
+
*/
|
|
457
|
+
const spread = detectSpread(
|
|
458
|
+
Object.values(reg)
|
|
459
|
+
.filter((a) => liveTransports.has(a.agentId) || now - a.lastHeartbeat < STALE_MS)
|
|
460
|
+
.map((a) => ({
|
|
461
|
+
agentId: a.agentId,
|
|
462
|
+
serverPid: a.serverPid,
|
|
463
|
+
serverStartedAt: a.serverStartedAt,
|
|
464
|
+
serverModule: a.serverModule,
|
|
465
|
+
})),
|
|
466
|
+
installedBuild(import.meta.url, statSync),
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// ⟨q-178878aa⟩ — the humans the bus knows: visible here, read at send time, never evicted.
|
|
470
|
+
const humans = Object.entries(await readHumans()).map(([id, h]) => ({ id, ...h }));
|
|
471
|
+
return { agents, evicted, proseOnly, humans, serverSpread: spread };
|
|
421
472
|
}
|
|
422
473
|
|
|
423
474
|
/**
|
|
@@ -474,6 +525,19 @@ export function isMarkerLive(marker: TransportMarker, reg: AgentRegistry, now: n
|
|
|
474
525
|
const entry = reg[marker.agentId];
|
|
475
526
|
return !!entry && now - entry.lastHeartbeat < STALE_MS;
|
|
476
527
|
}
|
|
528
|
+
// Phase 5.4 Task 4 — a herdr marker has no pid (0): liveness is the PANE, asked of
|
|
529
|
+
// herdr itself. "Could not ask" keeps the marker (unknown is not dead); only herdr
|
|
530
|
+
// saying pane_not_found lets the registry drop it.
|
|
531
|
+
if (marker.transport === HERDR) {
|
|
532
|
+
const t = targetOf(marker);
|
|
533
|
+
if (!t) return false;
|
|
534
|
+
// Ask through the WIRED herdr transport (its runner is what tests inject and what the
|
|
535
|
+
// fleet configured). A server with no herdr transport wired cannot ask, and "cannot
|
|
536
|
+
// ask" keeps the marker: the herdr-configured server reaps its own dead panes.
|
|
537
|
+
const active = activeTransport();
|
|
538
|
+
const exists = active?.kind === HERDR && active instanceof HerdrTransport ? active.paneExists(t) : null;
|
|
539
|
+
return exists !== false;
|
|
540
|
+
}
|
|
477
541
|
return isPidAlive(marker.pid);
|
|
478
542
|
}
|
|
479
543
|
|
|
@@ -0,0 +1,201 @@
|
|
|
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 && marker.transport === "herdr") {
|
|
108
|
+
// Phase 5.4 Task 4 — a herdr seat has NO pusher: delivery is in-process by the server,
|
|
109
|
+
// so the seat's build is the server half alone, and this says so instead of reading pid 0.
|
|
110
|
+
pusher = { ...empty, note: `herdr socket transport (pane ${marker.target ?? marker.tmuxTarget ?? "?"}): no pusher process — delivery is in-process by the answering server, so the server half is the whole seat` };
|
|
111
|
+
} else if (!marker || typeof marker.pid !== "number") {
|
|
112
|
+
pusher = { ...empty, note: "no transport marker for this seat — no pusher pid to read" };
|
|
113
|
+
} else {
|
|
114
|
+
const facts = ps(marker.pid);
|
|
115
|
+
if (!facts.alive) {
|
|
116
|
+
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` };
|
|
117
|
+
} else {
|
|
118
|
+
const cmd = facts.command ?? "";
|
|
119
|
+
const keyed = /tmux-pusher\.mjs/.test(cmd) && new RegExp(`--agent\\s+${agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`).test(cmd);
|
|
120
|
+
if (!keyed) {
|
|
121
|
+
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` };
|
|
122
|
+
} else {
|
|
123
|
+
// THE VERY FILE THIS PUSHER LOADS. Its path is on the command line; the mtime is
|
|
124
|
+
// read from that path, not from the answering server's tree — a dev checkout
|
|
125
|
+
// answering for a fleet of global-install pushers would otherwise call every
|
|
126
|
+
// pusher stale against a file none of them ever loaded (measured while building
|
|
127
|
+
// this: 6 of 6 read stale against a worktree's hook, all 6 current against their own).
|
|
128
|
+
const hookPath = hookPathOf(cmd) ?? installed.hookPath;
|
|
129
|
+
const fromOwnCmd = hookPathOf(cmd) !== null;
|
|
130
|
+
let hookMtime: number | null = null;
|
|
131
|
+
try {
|
|
132
|
+
hookMtime = stat(hookPath).mtimeMs;
|
|
133
|
+
} catch {
|
|
134
|
+
hookMtime = null;
|
|
135
|
+
}
|
|
136
|
+
const sameTree = path.resolve(path.dirname(path.dirname(hookPath))) === path.resolve(installed.module);
|
|
137
|
+
if (facts.startedAt === null || hookMtime === null) {
|
|
138
|
+
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` };
|
|
139
|
+
} else {
|
|
140
|
+
const current = facts.startedAt >= hookMtime;
|
|
141
|
+
pusher = {
|
|
142
|
+
pid: marker.pid,
|
|
143
|
+
alive: true,
|
|
144
|
+
keyedByAgentArg: true,
|
|
145
|
+
startedAt: iso(facts.startedAt),
|
|
146
|
+
hookPath,
|
|
147
|
+
hookMtime: iso(hookMtime),
|
|
148
|
+
sameTreeAsServer: sameTree,
|
|
149
|
+
current,
|
|
150
|
+
note:
|
|
151
|
+
(current
|
|
152
|
+
? `pusher started ${iso(facts.startedAt)}, after its hook ${hookPath} (${iso(hookMtime)}) — it loaded that installed code`
|
|
153
|
+
: `pusher started ${iso(facts.startedAt)}, BEFORE its hook ${hookPath} (${iso(hookMtime)}) — it runs the code it loaded then, and still delivers`) +
|
|
154
|
+
(fromOwnCmd ? "" : " (hook path not on the command line; the answering server's tree was used)") +
|
|
155
|
+
(sameTree ? "" : `; NOTE the pusher's tree differs from the answering server's (${installed.module})`),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---- server half
|
|
163
|
+
let srv: SeatBuild["server"];
|
|
164
|
+
if (!server) {
|
|
165
|
+
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`." };
|
|
166
|
+
} else if (server.buildMtime === null) {
|
|
167
|
+
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" };
|
|
168
|
+
} else {
|
|
169
|
+
const current = server.startedAt >= server.buildMtime;
|
|
170
|
+
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` };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---- the statement
|
|
174
|
+
let verdict: SeatBuild["verdict"];
|
|
175
|
+
let statement: string;
|
|
176
|
+
const p = pusher.current;
|
|
177
|
+
const s = srv.current;
|
|
178
|
+
if (s === true && p === true) {
|
|
179
|
+
verdict = "current";
|
|
180
|
+
statement = `${agentId}: current — server and pusher both started after the ${day(server!.buildMtime!)} install.`;
|
|
181
|
+
} else if (s === false && p === false) {
|
|
182
|
+
verdict = "stale";
|
|
183
|
+
statement = `${agentId}: stale — server and pusher both predate the install; nothing on this seat runs the installed code.`;
|
|
184
|
+
} else if (s === true && p === false) {
|
|
185
|
+
verdict = "half-restarted";
|
|
186
|
+
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.`;
|
|
187
|
+
} else if (s === false && p === true) {
|
|
188
|
+
verdict = "half-restarted";
|
|
189
|
+
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).`;
|
|
190
|
+
} else if (marker?.transport === "herdr" && s !== null) {
|
|
191
|
+
verdict = s ? "current" : "stale";
|
|
192
|
+
statement = `${agentId}: ${s ? "current" : "stale"} — herdr socket transport, no pusher; the server ${s ? "started after" : "predates"} the ${day(server!.buildMtime!)} install and is the whole seat.`;
|
|
193
|
+
} else if (s === null && p !== null) {
|
|
194
|
+
verdict = "unknown";
|
|
195
|
+
statement = `${agentId}: pusher ${p ? "current" : "STALE — started before the installed hook"}; server UNOBSERVABLE from here (${srv.note}).`;
|
|
196
|
+
} else {
|
|
197
|
+
verdict = "unknown";
|
|
198
|
+
statement = `${agentId}: unknown — ${pusher.note}${srv.observable ? "" : `; ${srv.note}`}`;
|
|
199
|
+
}
|
|
200
|
+
return { agentId, verdict, statement, server: srv, pusher };
|
|
201
|
+
}
|
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
|
/**
|