agent-coord-mcp 0.26.5 → 0.26.7
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/prefix.js +64 -0
- package/dist/prefix.js.map +1 -0
- package/dist/server.js +32 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/attention.js +73 -0
- package/dist/tools/attention.js.map +1 -0
- package/dist/tools/away.js +122 -0
- package/dist/tools/away.js.map +1 -0
- package/dist/tools/events.js +171 -0
- package/dist/tools/events.js.map +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/records.js +651 -0
- package/dist/tools/records.js.map +1 -0
- package/dist/tools/registry.js +45 -5
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/rotate.js +143 -0
- package/dist/tools/rotate.js.map +1 -0
- package/dist/tools/shared.js +9 -0
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/stall.js +294 -0
- package/dist/tools/stall.js.map +1 -0
- package/dist/tools/transport.js +92 -16
- package/dist/tools/transport.js.map +1 -1
- package/dist/tools/worktrees.js +294 -0
- package/dist/tools/worktrees.js.map +1 -0
- package/package.json +2 -2
- package/scripts/check-test-count.mjs +1 -1
- package/scripts/coord-attention-clock.mjs +122 -0
- package/scripts/coord-stall-clock.mjs +125 -0
- package/src/prefix.ts +72 -0
- package/src/server.ts +138 -3
- package/src/tools/attention.ts +91 -0
- package/src/tools/away.ts +121 -0
- package/src/tools/events.ts +199 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/records.ts +747 -0
- package/src/tools/registry.ts +45 -5
- package/src/tools/rotate.ts +180 -0
- package/src/tools/shared.ts +24 -0
- package/src/tools/stall.ts +311 -0
- package/src/tools/transport.ts +99 -3
- package/src/tools/worktrees.ts +311 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `stall_check` — a predicate over the board and the bus, plus a HALT flag.
|
|
3
|
+
*
|
|
4
|
+
* Stall has been "notice the room" until now, which means it is noticed when
|
|
5
|
+
* somebody happens to look. The measured cost elsewhere: ~42 hours of dead merge
|
|
6
|
+
* path across four incidents in three days, with `main` advancing throughout so
|
|
7
|
+
* every commit-based liveness check read green.
|
|
8
|
+
*
|
|
9
|
+
* A MISS IS SILENT TO THE DUTY OFFICER AND NEVER SILENT TO THE RECORD.
|
|
10
|
+
*
|
|
11
|
+
* This is the whole design constraint (3.3b). "HIT DMs, MISS silent" answers the
|
|
12
|
+
* noise question and makes a DEAD CLOCK look exactly like a healthy fleet — the
|
|
13
|
+
* absence-read-as-evidence rule, inside the verb built to watch for absence. So
|
|
14
|
+
* every run leaves a mark, HIT or MISS, and "no alert" becomes distinguishable
|
|
15
|
+
* from "nothing ran". A check that only speaks when it fires cannot be told from
|
|
16
|
+
* a broken one.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
19
|
+
import { execFileSync } from "node:child_process";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { parseWorkDoc, workstreamsV1RowsOf } from "@davidbalzan/groundwork-seam";
|
|
23
|
+
import { ROOT, AGENTS_FILE, readJson } from "../store.js";
|
|
24
|
+
import { loadLiveTransports } from "./registry.js";
|
|
25
|
+
const BOARD_DOC = "docs/WORKSTREAMS.md";
|
|
26
|
+
const STALL_MS = 30 * 60 * 1000;
|
|
27
|
+
const runFile = () => path.join(ROOT, "stall-check.json");
|
|
28
|
+
const haltFile = () => path.join(ROOT, "halt.json");
|
|
29
|
+
// ---------- halt ----------
|
|
30
|
+
export const setHaltSchema = {
|
|
31
|
+
reason: z.string().min(1),
|
|
32
|
+
by: z.string().min(1),
|
|
33
|
+
clear: z.boolean().optional(),
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A NAMED state, never a mood.
|
|
37
|
+
*
|
|
38
|
+
* "Production feels down" is not a halt: a halt blocks every claim in the fleet,
|
|
39
|
+
* so the thing that sets it must be nameable and therefore arguable — a board
|
|
40
|
+
* cutover, a cited `BLOCKER:`, a documented red pipeline. The reason is required
|
|
41
|
+
* for that reason, not for the log.
|
|
42
|
+
*/
|
|
43
|
+
export async function setHaltTool(args) {
|
|
44
|
+
mkdirSync(ROOT, { recursive: true });
|
|
45
|
+
if (args.clear) {
|
|
46
|
+
writeFileSync(haltFile(), JSON.stringify({ halted: false, clearedBy: args.by, clearedAt: Date.now(), lastReason: args.reason }, null, 2));
|
|
47
|
+
return { ok: true, halted: false, clearedBy: args.by };
|
|
48
|
+
}
|
|
49
|
+
const state = { halted: true, reason: args.reason, by: args.by, at: Date.now() };
|
|
50
|
+
writeFileSync(haltFile(), JSON.stringify(state, null, 2));
|
|
51
|
+
return { ok: true, ...state };
|
|
52
|
+
}
|
|
53
|
+
export function haltState() {
|
|
54
|
+
try {
|
|
55
|
+
const raw = JSON.parse(readFileSync(haltFile(), "utf8"));
|
|
56
|
+
return raw?.halted ? raw : { halted: false };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { halted: false };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ---------- the run mark ----------
|
|
63
|
+
/** Every run leaves this, HIT or MISS. It is what makes a dead clock visible. */
|
|
64
|
+
/**
|
|
65
|
+
* Record a run that FAILED.
|
|
66
|
+
*
|
|
67
|
+
* Acceptance from the queue item, and the clause most easily skipped: "a
|
|
68
|
+
* scheduled check reports its FETCH FAILURES, or a broken check is
|
|
69
|
+
* indistinguishable from a quiet registry". Without this, a clock that fires
|
|
70
|
+
* every 30 minutes and throws every time leaves NO marks at all — identical on
|
|
71
|
+
* disk to a clock that was never installed.
|
|
72
|
+
*/
|
|
73
|
+
export function markRunFailure(reason) {
|
|
74
|
+
mkdirSync(ROOT, { recursive: true });
|
|
75
|
+
let history = [];
|
|
76
|
+
try {
|
|
77
|
+
history = JSON.parse(readFileSync(runFile(), "utf8")).history ?? [];
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* first run */
|
|
81
|
+
}
|
|
82
|
+
history.push({ at: Date.now(), hits: 0, checked: 0, failed: reason });
|
|
83
|
+
writeFileSync(runFile(), JSON.stringify({ history: history.slice(-200) }, null, 2));
|
|
84
|
+
}
|
|
85
|
+
function markRun(result) {
|
|
86
|
+
mkdirSync(ROOT, { recursive: true });
|
|
87
|
+
let history = [];
|
|
88
|
+
try {
|
|
89
|
+
history = JSON.parse(readFileSync(runFile(), "utf8")).history ?? [];
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* first run */
|
|
93
|
+
}
|
|
94
|
+
history.push({ at: Date.now(), hits: result.hits.length, checked: result.checked });
|
|
95
|
+
// A RUN OF MISSES MUST BE VISIBLE AS RUNS, not as absence — so the marks are a
|
|
96
|
+
// list, not a single timestamp. "Ten quiet checks" and "one check ten hours ago"
|
|
97
|
+
// are different states and only the first is a healthy fleet.
|
|
98
|
+
writeFileSync(runFile(), JSON.stringify({ history: history.slice(-200) }, null, 2));
|
|
99
|
+
}
|
|
100
|
+
export const lastRanSchema = { maxAgeMinutes: z.number().optional() };
|
|
101
|
+
/**
|
|
102
|
+
* Is the clock alive? Readable by a human or another check, which is the point —
|
|
103
|
+
* MISS is silent to the duty officer, not to the record.
|
|
104
|
+
*/
|
|
105
|
+
export async function stallClockStatusTool(args) {
|
|
106
|
+
const maxAge = (args.maxAgeMinutes ?? 60) * 60 * 1000;
|
|
107
|
+
let history = [];
|
|
108
|
+
try {
|
|
109
|
+
history = JSON.parse(readFileSync(runFile(), "utf8")).history ?? [];
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: "stall_check has NEVER run — no run mark exists. That is not a quiet fleet, it is an unwatched one: a check that only speaks when it fires cannot be told from a broken one.",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const last = history[history.length - 1];
|
|
118
|
+
// A RUN THAT FAILED IS NOT A RUN THAT PASSED. Without this the clock reads
|
|
119
|
+
// "fresh" off a mark it wrote while erroring — the age is honest and the
|
|
120
|
+
// health is not, which is the same shape as a fresh heartbeat from a stuck
|
|
121
|
+
// agent.
|
|
122
|
+
const failures = history.filter((h) => h.failed);
|
|
123
|
+
const lastFailed = last?.failed;
|
|
124
|
+
const age = Date.now() - (last?.at ?? 0);
|
|
125
|
+
// `>=`, NOT `>`, AND THE DIFFERENCE IS A REAL RACE RATHER THAN PEDANTRY.
|
|
126
|
+
//
|
|
127
|
+
// With `>`, a window of 0 and a mark written in the SAME MILLISECOND gives
|
|
128
|
+
// `0 > 0` = false: the clock reads FRESH at the instant it was asked to treat
|
|
129
|
+
// everything as stale. It passed locally and on one CI run and failed on
|
|
130
|
+
// another — green in two environments, red in one — because it depended on at
|
|
131
|
+
// least a millisecond elapsing.
|
|
132
|
+
//
|
|
133
|
+
// The window having ELAPSED is the condition, so equality is inside it: a
|
|
134
|
+
// 0-minute window means nothing is ever fresh, which is what a caller asking
|
|
135
|
+
// for one means.
|
|
136
|
+
const stale = age >= maxAge;
|
|
137
|
+
const misses = history.filter((h) => !h.failed && h.hits === 0).length;
|
|
138
|
+
const hits = history.filter((h) => !h.failed && h.hits > 0).length;
|
|
139
|
+
return {
|
|
140
|
+
// A FAILING CLOCK IS NOT A HEALTHY ONE. It writes marks on schedule, so the
|
|
141
|
+
// age looks fresh while nothing is being measured — a fresh heartbeat from
|
|
142
|
+
// a stuck agent, one level up.
|
|
143
|
+
ok: !stale && !lastFailed,
|
|
144
|
+
...(stale
|
|
145
|
+
? {
|
|
146
|
+
error: `stall_check last ran ${Math.round(age / 60000)}m ago, past the ${args.maxAgeMinutes ?? 60}m window — THE CLOCK IS STOPPED. No alerts is not the same as no stalls.`,
|
|
147
|
+
}
|
|
148
|
+
: lastFailed
|
|
149
|
+
? {
|
|
150
|
+
error: `the clock is RUNNING but its last run FAILED: ${lastFailed}. It is firing on schedule and measuring nothing, which reads as fresh and is not.`,
|
|
151
|
+
}
|
|
152
|
+
: {}),
|
|
153
|
+
lastRanMinutesAgo: Math.round(age / 60000),
|
|
154
|
+
runs: history.length,
|
|
155
|
+
misses,
|
|
156
|
+
hits,
|
|
157
|
+
failures: failures.length,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// ---------- the predicate ----------
|
|
161
|
+
export const stallCheckSchema = { repo: z.string().optional(), stallMinutes: z.number().optional() };
|
|
162
|
+
export async function stallCheckTool(args) {
|
|
163
|
+
const repo = args.repo ?? process.cwd();
|
|
164
|
+
const limit = (args.stallMinutes ?? 30) * 60 * 1000;
|
|
165
|
+
const board = path.join(repo, BOARD_DOC);
|
|
166
|
+
if (!existsSync(board))
|
|
167
|
+
return { ok: false, error: `no ${BOARD_DOC} under '${repo}'` };
|
|
168
|
+
const liveTransports = await loadLiveTransports();
|
|
169
|
+
const rows = workstreamsV1RowsOf(parseWorkDoc(readFileSync(board, "utf8")));
|
|
170
|
+
const inFlight = rows.filter((r) => /🚧/.test(r.status));
|
|
171
|
+
const reg = await readJson(AGENTS_FILE, {});
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const hits = [];
|
|
174
|
+
/** Rows whose VCS activity could not be measured. NOT stall claims. */
|
|
175
|
+
const unmeasurable = [];
|
|
176
|
+
for (const row of inFlight) {
|
|
177
|
+
const agentId = row.owner.replace(/[`*]/g, "").trim();
|
|
178
|
+
const entry = reg[agentId];
|
|
179
|
+
if (entry) {
|
|
180
|
+
const age = now - entry.lastHeartbeat;
|
|
181
|
+
// A HEARTBEAT IS ONLY EVIDENCE WHERE SOMETHING WRITES ONE.
|
|
182
|
+
//
|
|
183
|
+
// `heartbeat` is called by the PUSHER, never by the agent. Only
|
|
184
|
+
// `coord-pusher.mjs` (the REMOTE pusher) calls it, and it must: a remote
|
|
185
|
+
// marker cannot be pid-probed across machines, so its liveness IS the
|
|
186
|
+
// heartbeat. `hooks/tmux-pusher.mjs` — what this fleet actually runs —
|
|
187
|
+
// never calls it, because a LOCAL marker's liveness is `isPidAlive`.
|
|
188
|
+
//
|
|
189
|
+
// So for a local transport there is no heartbeat SOURCE at all. Before
|
|
190
|
+
// kit#137, `list_agents` stamped these agents and that fabrication was
|
|
191
|
+
// the only thing keeping the field moving; removing it left the field
|
|
192
|
+
// honest and empty. Measured: three attached, working agents at an
|
|
193
|
+
// IDENTICAL 44.7m — the uniform signature of one shared cause, not three
|
|
194
|
+
// stalls.
|
|
195
|
+
//
|
|
196
|
+
// WHY NOT JUST MAKE tmux-pusher HEARTBEAT: because the signal would mean
|
|
197
|
+
// "the pusher process is alive", which `isPidAlive` already answers for
|
|
198
|
+
// local markers. During the 17-hour stall every transport was live the
|
|
199
|
+
// whole time, so a pusher heartbeat would have read FRESH for all 17
|
|
200
|
+
// hours. It would restore a field without restoring a detector — and the
|
|
201
|
+
// case this verb exists for is exactly the one it would miss. The vcs
|
|
202
|
+
// half is what catches "alive and not progressing"; saying so is more
|
|
203
|
+
// honest than a green field.
|
|
204
|
+
//
|
|
205
|
+
// Unknown is not stalled (kit#138). This does NOT narrow Task 3.5: an
|
|
206
|
+
// agent with no transport, or a REMOTE one where the heartbeat genuinely
|
|
207
|
+
// is the liveness mechanism, still HITs on a dead heartbeat.
|
|
208
|
+
const marker = liveTransports.get(agentId);
|
|
209
|
+
if (age > limit && marker && marker.transport === "tmux-push") {
|
|
210
|
+
unmeasurable.push({
|
|
211
|
+
agentId,
|
|
212
|
+
value: marker.transport,
|
|
213
|
+
why: `heartbeat is ${Math.round(age / 60000)}m old, but nothing writes heartbeats for a local 'tmux-push' transport — hooks/tmux-pusher.mjs does not call heartbeat, and this marker's liveness is its pid. There is no heartbeat SOURCE here, so the age measures nothing about this agent`,
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (age > limit) {
|
|
218
|
+
hits.push({ kind: "no-heartbeat", agentId, stream: row.stream.slice(0, 60), minutes: Math.round(age / 60000) });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// A FRESH HEARTBEAT IS NOT PROGRESS. An agent can be alive and stuck, which is
|
|
223
|
+
// the case "notice the room" never catches: the pane is responsive, so nobody
|
|
224
|
+
// looks. Ask the branch instead.
|
|
225
|
+
//
|
|
226
|
+
// RESOLVE A REF, AND SAY SO WHEN THE VALUE IS NOT ONE.
|
|
227
|
+
//
|
|
228
|
+
// `git log -1 <value>` accepts a PATHSPEC exactly as readily as a ref, and
|
|
229
|
+
// the old guard only required a `/` — which every path has. So the board's
|
|
230
|
+
// `Branch · Worktree` cells, which hold PATHS, were fed to git and silently
|
|
231
|
+
// measured as paths: the aide was reported at 2,271 minutes, the age of
|
|
232
|
+
// `docs/phases/phase5`'s last commit, not of any activity by that agent.
|
|
233
|
+
// Verified: `docs/phases/phase5` does not resolve as a ref, yet
|
|
234
|
+
// `git log -1 --format=%cI docs/phases/phase5` returns a date.
|
|
235
|
+
//
|
|
236
|
+
// Other rows read plausibly only by coincidence — a path that happens to be
|
|
237
|
+
// committed often looks like an active branch.
|
|
238
|
+
//
|
|
239
|
+
// UNKNOWN IS NOT STALLED, which this verb already gets right for
|
|
240
|
+
// heartbeats. An unresolvable value is REPORTED as unmeasurable rather than
|
|
241
|
+
// skipped in silence: a silent `continue` and a healthy agent produce the
|
|
242
|
+
// same output, which is the failure this whole verb exists to avoid.
|
|
243
|
+
const raw = (row.branchWorktree.match(/`([^`]+)`/)?.[1] ?? "").trim();
|
|
244
|
+
if (!raw) {
|
|
245
|
+
unmeasurable.push({ agentId, value: "", why: "no value in the Branch · Worktree cell" });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
let sha = "";
|
|
249
|
+
try {
|
|
250
|
+
sha = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${raw}^{commit}`], {
|
|
251
|
+
cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
|
|
252
|
+
}).trim();
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
sha = "";
|
|
256
|
+
}
|
|
257
|
+
if (!sha) {
|
|
258
|
+
unmeasurable.push({
|
|
259
|
+
agentId,
|
|
260
|
+
value: raw,
|
|
261
|
+
why: `'${raw}' does not resolve as a git ref — it is a path or glob, and \`git log <path>\` would silently report that PATH's last commit as this agent's activity`,
|
|
262
|
+
});
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
// The resolved SHA, and `--`: a ref can then never be re-read as a
|
|
267
|
+
// pathspec, which is the ambiguity that produced the wrong number.
|
|
268
|
+
const iso = execFileSync("git", ["log", "-1", "--format=%cI", sha, "--"], {
|
|
269
|
+
cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
|
|
270
|
+
}).trim();
|
|
271
|
+
const age = now - Date.parse(iso);
|
|
272
|
+
if (age > limit) {
|
|
273
|
+
hits.push({ kind: "no-vcs-activity", agentId, branch: raw, minutes: Math.round(age / 60000) });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
unmeasurable.push({ agentId, value: raw, why: "resolved as a ref but its log could not be read" });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const result = { hits, checked: inFlight.length, unmeasurable };
|
|
281
|
+
markRun(result);
|
|
282
|
+
return {
|
|
283
|
+
ok: true,
|
|
284
|
+
...result,
|
|
285
|
+
halted: haltState().halted,
|
|
286
|
+
// MISS is silent to the DUTY OFFICER — the caller decides whether to DM — and
|
|
287
|
+
// never silent to the record, which markRun just wrote.
|
|
288
|
+
dm: hits.length > 0,
|
|
289
|
+
note: hits.length === 0
|
|
290
|
+
? `MISS — ${inFlight.length} in-flight row(s), none stalled${unmeasurable.length ? `; ${unmeasurable.length} row(s) UNMEASURABLE for VCS activity (${unmeasurable.map((u) => u.agentId).join(", ")}) — reported, not counted as healthy` : ""}. No DM. The run IS recorded: read it with stall_clock_status, because no alert and nothing running look identical from here.`
|
|
291
|
+
: undefined,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=stall.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stall.js","sourceRoot":"","sources":["../../src/tools/stall.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/transport.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { loadLiveTransports, isMarkerLive, isPidAlive } from "./registry.js";
|
|
2
2
|
import { newestMtimeUnder, onDiskBuildMtime, onDiskSourceMtime, SERVER_BUILD_MTIME, SERVER_BUILD_SHA, BUILD_DIR } from "../build.js";
|
|
3
|
+
import { prefixOf, prefixVerdict } from "../prefix.js";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
3
5
|
import { registerTool } from "./registry.js";
|
|
4
6
|
import { roleInputSchema } from "../roles.js";
|
|
5
7
|
import { attributeWriter, isGitRepo, lastWriterOf, loadScopes, ownsDocument } from "./scopes.js";
|
|
@@ -208,27 +210,72 @@ function defaultReminderText(agentId) {
|
|
|
208
210
|
`list_rooms() for the channels you're in. Any DM or channel post you receive ` +
|
|
209
211
|
`next is your new task context.`);
|
|
210
212
|
}
|
|
213
|
+
const pendingReminders = new Set();
|
|
214
|
+
/** Reminders whose delivery FAILED, kept so a drop is observable, not stderr-only. */
|
|
215
|
+
export const reminderFailures = [];
|
|
216
|
+
/**
|
|
217
|
+
* Resolves once every scheduled reminder has been delivered or has failed.
|
|
218
|
+
*
|
|
219
|
+
* Re-`ref`s the pending timers for the duration of the wait. The reminder timer
|
|
220
|
+
* is deliberately `unref`d so it never holds the MCP server open on its own —
|
|
221
|
+
* but that also means the event loop will not wait for it, and awaiting an
|
|
222
|
+
* unref'd timer's promise hangs until the loop drains. A caller that has
|
|
223
|
+
* explicitly asked to wait is stating the opposite intent, so the ref is
|
|
224
|
+
* restored for exactly that window and dropped again afterwards.
|
|
225
|
+
*
|
|
226
|
+
* (The unref has a production consequence worth naming: if the server's
|
|
227
|
+
* transport closes before the timer fires, the reminder is dropped. That is a
|
|
228
|
+
* separate defect from the one this function fixes — reported, not silently
|
|
229
|
+
* papered over.)
|
|
230
|
+
*/
|
|
231
|
+
export function remindersSettled() {
|
|
232
|
+
const pending = [...pendingReminders];
|
|
233
|
+
for (const p of pending)
|
|
234
|
+
p.timer.ref();
|
|
235
|
+
return Promise.all(pending.map((p) => p.done)).then(() => {
|
|
236
|
+
for (const p of pending)
|
|
237
|
+
p.timer.unref();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
211
240
|
function scheduleReminders(from, recipients, delayMs, override) {
|
|
241
|
+
// Registered BEFORE the timer fires, so awaiting settlement covers the delay
|
|
242
|
+
// as well as the write — otherwise a caller could observe "nothing pending"
|
|
243
|
+
// during the window between scheduling and firing.
|
|
244
|
+
let markDone = () => { };
|
|
245
|
+
const done = new Promise((resolve) => { markDone = resolve; });
|
|
246
|
+
let entry;
|
|
212
247
|
const t = setTimeout(async () => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
248
|
+
try {
|
|
249
|
+
for (const r of recipients) {
|
|
250
|
+
try {
|
|
251
|
+
const reminder = {
|
|
252
|
+
id: randomUUID(),
|
|
253
|
+
ts: Date.now(),
|
|
254
|
+
from,
|
|
255
|
+
to: r,
|
|
256
|
+
text: override ?? defaultReminderText(r),
|
|
257
|
+
// A just-cleared agent is contextless until this lands — it must
|
|
258
|
+
// push immediately, never queue behind the routine tier.
|
|
259
|
+
urgent: true,
|
|
260
|
+
};
|
|
261
|
+
await appendJsonl(inboxFile(r), reminder);
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
const error = e?.message ?? String(e);
|
|
265
|
+
// Recorded, not just printed: a dropped reminder leaves a just-cleared
|
|
266
|
+
// agent contextless, and stderr is not somewhere anyone looks for that.
|
|
267
|
+
reminderFailures.push({ to: r, error, at: Date.now() });
|
|
268
|
+
process.stderr.write(`[send_command] post-/clear reminder to '${r}' failed: ${error}\n`);
|
|
269
|
+
}
|
|
229
270
|
}
|
|
230
271
|
}
|
|
272
|
+
finally {
|
|
273
|
+
pendingReminders.delete(entry);
|
|
274
|
+
markDone();
|
|
275
|
+
}
|
|
231
276
|
}, delayMs);
|
|
277
|
+
entry = { done, timer: t };
|
|
278
|
+
pendingReminders.add(entry);
|
|
232
279
|
// Don't keep the event loop alive solely for the reminder — the MCP server's
|
|
233
280
|
// transport already holds it open as long as it's connected.
|
|
234
281
|
if (typeof t.unref === "function")
|
|
@@ -510,6 +557,11 @@ export async function attachAgentTool(args) {
|
|
|
510
557
|
// code, and after an in-place rebuild the two differ (that difference is
|
|
511
558
|
// exactly what doctor's provenance check exists to surface).
|
|
512
559
|
serverBuildMtime: SERVER_BUILD_MTIME,
|
|
560
|
+
// WHAT this transport carries, recorded by the code that decides it.
|
|
561
|
+
// `includeRoom` is what the pusher is actually spawned with a few lines
|
|
562
|
+
// above, so the marker cannot claim a capability the process was not
|
|
563
|
+
// given — the marker and the spawn come from one value, not two.
|
|
564
|
+
rooms: includeRoom,
|
|
513
565
|
};
|
|
514
566
|
// Use updateJson so it lockfile-protects and creates the file atomically.
|
|
515
567
|
await updateJson(transportFile(args.agentId), marker, () => marker);
|
|
@@ -786,6 +838,10 @@ export const reportTransportSchema = {
|
|
|
786
838
|
// counterpart has been upgraded since. The remote pusher passes
|
|
787
839
|
// `(await fsp.stat(__filename)).mtimeMs`; absent → doctor skips the check.
|
|
788
840
|
scriptMtime: z.number().optional(),
|
|
841
|
+
// Whether this pusher carries room traffic as well as DMs. Absent → the
|
|
842
|
+
// marker cannot say, and every reader reports UNKNOWN rather than assuming
|
|
843
|
+
// a full transport (the worker-2 shape).
|
|
844
|
+
rooms: z.boolean().optional(),
|
|
789
845
|
};
|
|
790
846
|
// Called by an external push daemon (typically scripts/coord-pusher.mjs on a
|
|
791
847
|
// remote machine) to publish a transport marker so list_agents reflects the
|
|
@@ -800,6 +856,7 @@ export async function reportTransportTool(args) {
|
|
|
800
856
|
host: args.host,
|
|
801
857
|
since: args.since ?? Date.now(),
|
|
802
858
|
scriptMtime: args.scriptMtime,
|
|
859
|
+
rooms: args.rooms,
|
|
803
860
|
};
|
|
804
861
|
await updateJson(transportFile(args.agentId), marker, () => marker);
|
|
805
862
|
return { ok: true, marker };
|
|
@@ -1026,6 +1083,25 @@ export async function doctorTool(args) {
|
|
|
1026
1083
|
fixable: false,
|
|
1027
1084
|
});
|
|
1028
1085
|
}
|
|
1086
|
+
// 1b³. WHERE A NEW GLOBAL INSTALL WOULD LAND, which is a different question
|
|
1087
|
+
// from which copy is running (1b² above) and neither substitutes. On
|
|
1088
|
+
// this box `npm prefix -g` and the fleet's actual load path pointed at
|
|
1089
|
+
// two different nvm versions, so an install that printed success would
|
|
1090
|
+
// have updated a copy nothing loads.
|
|
1091
|
+
{
|
|
1092
|
+
const loadPrefix = prefixOf(BUILD_DIR);
|
|
1093
|
+
let installPrefix = null;
|
|
1094
|
+
let npmPath;
|
|
1095
|
+
try {
|
|
1096
|
+
installPrefix = execFileSync("npm", ["prefix", "-g"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
1097
|
+
npmPath = execFileSync("which", ["npm"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
|
|
1098
|
+
}
|
|
1099
|
+
catch {
|
|
1100
|
+
installPrefix = null;
|
|
1101
|
+
}
|
|
1102
|
+
const v = prefixVerdict(loadPrefix, installPrefix, npmPath);
|
|
1103
|
+
findings.push({ check: "install-prefix", level: v.level, detail: v.detail, fixable: false });
|
|
1104
|
+
}
|
|
1029
1105
|
// 1b²ᵇ. The affirmative catch for merged-but-never-rebuilt: src/ newer than
|
|
1030
1106
|
// the compiled build means no restart can help — the artifact every
|
|
1031
1107
|
// future session will load is already behind the code. Distinct from
|