hub-runner 0.2.2 → 0.2.4
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/check.mjs +151 -187
- package/main.mjs +202 -205
- package/package.json +1 -1
package/check.mjs
CHANGED
|
@@ -6,18 +6,18 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { join as join3, resolve } from "node:path";
|
|
7
7
|
import { setTimeout as delay } from "node:timers/promises";
|
|
8
8
|
|
|
9
|
-
// ../../lib/clean-env.mjs
|
|
9
|
+
// ../../hosted-hub/lib/clean-env.mjs
|
|
10
10
|
var INHERITED_ENV = /^(EVE_|WORKFLOW_)/;
|
|
11
11
|
function cleanEnv(env = process.env) {
|
|
12
12
|
return Object.fromEntries(Object.entries(env).filter(([key]) => !INHERITED_ENV.test(key)));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// ../../lib/processes.mjs
|
|
16
|
-
import { spawn
|
|
17
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync,
|
|
15
|
+
// ../../hosted-hub/lib/processes.mjs
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { closeSync as closeSync2, existsSync, mkdirSync, openSync as openSync2, readFileSync, renameSync, rmSync, readdirSync, statSync as statSync2, writeFileSync, writeSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
|
|
20
|
-
// ../../lib/fingerprint.mjs
|
|
20
|
+
// ../../hosted-hub/lib/fingerprint.mjs
|
|
21
21
|
var TREE_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".DS_Store"]);
|
|
22
22
|
function fingerprintDiff(recorded, current) {
|
|
23
23
|
if (!recorded?.hash) return "it was started without a fingerprint (by an older registry)";
|
|
@@ -36,22 +36,44 @@ function fingerprintDiff(recorded, current) {
|
|
|
36
36
|
}
|
|
37
37
|
var BUILD_SKIP = /* @__PURE__ */ new Set([...TREE_SKIP, ".eve", ".output"]);
|
|
38
38
|
|
|
39
|
-
// ../../lib/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
// ../../hosted-hub/lib/log-tail.mjs
|
|
40
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
41
|
+
function createLogTail(log) {
|
|
42
|
+
const tails = /* @__PURE__ */ new Map();
|
|
43
|
+
function follow(name, file, offset) {
|
|
44
|
+
tails.set(name, { file, offset, carry: "", prefix: `[${name}]`.padEnd(20) });
|
|
45
|
+
}
|
|
46
|
+
function pump() {
|
|
47
|
+
for (const tail of tails.values()) {
|
|
48
|
+
let size;
|
|
49
|
+
try {
|
|
50
|
+
size = statSync(tail.file).size;
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (size < tail.offset) tail.offset = 0;
|
|
55
|
+
if (size === tail.offset) continue;
|
|
56
|
+
const length = Math.min(size - tail.offset, 1e6);
|
|
57
|
+
const buffer = Buffer.alloc(length);
|
|
58
|
+
const fd = openSync(tail.file, "r");
|
|
59
|
+
try {
|
|
60
|
+
readSync(fd, buffer, 0, length, tail.offset);
|
|
61
|
+
} finally {
|
|
62
|
+
closeSync(fd);
|
|
63
|
+
}
|
|
64
|
+
tail.offset += length;
|
|
65
|
+
const lines = (tail.carry + buffer.toString("utf8")).split("\n");
|
|
66
|
+
tail.carry = lines.pop();
|
|
67
|
+
for (const line of lines) if (line.trim() && !line.startsWith("Migration file of version")) log(`${tail.prefix} ${line}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { follow, pump };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ../../hosted-hub/lib/process-tree.mjs
|
|
74
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
44
75
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
45
|
-
var CRASH_LIMIT = num("ROSTER_CRASH_LIMIT", 5);
|
|
46
|
-
var CRASH_WINDOW_MS = num("ROSTER_CRASH_WINDOW_MS", 10 * 6e4);
|
|
47
|
-
var BACKOFF_BASE_MS = num("ROSTER_BACKOFF_BASE_MS", 1e3);
|
|
48
|
-
var BACKOFF_MAX_MS = num("ROSTER_BACKOFF_MAX_MS", 6e4);
|
|
49
|
-
var HEALTH_TIMEOUT_MS = num("ROSTER_START_HEALTH_TIMEOUT_MS", 12e4);
|
|
50
|
-
var PORT_RECHECK_MS = num("ROSTER_PORT_RECHECK_MS", 5e3);
|
|
51
76
|
var KILL_GRACE_MS = 3e3;
|
|
52
|
-
var WATCH_MS = 1e3;
|
|
53
|
-
var TREE_MS = 5e3;
|
|
54
|
-
var LOG_ROTATE_BYTES = 2e7;
|
|
55
77
|
function groupAlive(pgid) {
|
|
56
78
|
if (!(pgid > 1)) return false;
|
|
57
79
|
try {
|
|
@@ -74,7 +96,7 @@ var C = { ...process.env, LC_ALL: "C" };
|
|
|
74
96
|
var SLOW_MS = 1e3;
|
|
75
97
|
function slow(what, started) {
|
|
76
98
|
const ms = Date.now() - started;
|
|
77
|
-
if (ms >= SLOW_MS) console.warn(`[
|
|
99
|
+
if (ms >= SLOW_MS) console.warn(`[runner] ${what} took ${ms}ms`);
|
|
78
100
|
}
|
|
79
101
|
var LSTART = String.raw`(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})`;
|
|
80
102
|
function table() {
|
|
@@ -137,6 +159,73 @@ async function healthy(port, ms = 1e3) {
|
|
|
137
159
|
}
|
|
138
160
|
}
|
|
139
161
|
var merge = (...lists) => [...new Map(lists.flat().map((m) => [m.pid, m])).values()];
|
|
162
|
+
var treeAlive = (pgid, tree) => groupAlive(pgid) || liveMembers(tree, table()).length > 0;
|
|
163
|
+
async function untilTreeGone(pgid, tree, ms) {
|
|
164
|
+
const deadline = Date.now() + ms;
|
|
165
|
+
while (treeAlive(pgid, tree)) {
|
|
166
|
+
if (Date.now() >= deadline) return false;
|
|
167
|
+
await sleep(200);
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
function killTree(pgid, tree) {
|
|
172
|
+
signalGroup(pgid, "SIGKILL");
|
|
173
|
+
for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) signalGroup(g, "SIGKILL");
|
|
174
|
+
}
|
|
175
|
+
async function stopTree(pgid, tree, { name, timeoutMs, warn }) {
|
|
176
|
+
if (!treeAlive(pgid, tree)) return true;
|
|
177
|
+
let membersTermed = false;
|
|
178
|
+
const termMembers = () => {
|
|
179
|
+
membersTermed = true;
|
|
180
|
+
for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) if (g !== pgid) signalGroup(g, "SIGTERM");
|
|
181
|
+
};
|
|
182
|
+
if (groupAlive(pgid)) signalGroup(pgid, "SIGTERM");
|
|
183
|
+
else termMembers();
|
|
184
|
+
const deadline = Date.now() + timeoutMs;
|
|
185
|
+
let gone = true;
|
|
186
|
+
while (treeAlive(pgid, tree)) {
|
|
187
|
+
if (!membersTermed && !groupAlive(pgid)) termMembers();
|
|
188
|
+
if (Date.now() >= deadline) {
|
|
189
|
+
gone = false;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
await sleep(200);
|
|
193
|
+
}
|
|
194
|
+
if (gone) return true;
|
|
195
|
+
warn(`[runner] ${name} didn't stop within ${timeoutMs / 1e3}s of SIGTERM; sending SIGKILL`);
|
|
196
|
+
killTree(pgid, tree);
|
|
197
|
+
return untilTreeGone(pgid, tree, KILL_GRACE_MS);
|
|
198
|
+
}
|
|
199
|
+
function ownsLeader(record, rows, signature) {
|
|
200
|
+
if (!(record.pid > 1) || record.pgid !== record.pid) return false;
|
|
201
|
+
const row = rows.find((r) => r.pid === record.pid);
|
|
202
|
+
if (!row || row.pgid !== record.pgid) return false;
|
|
203
|
+
if (!` ${row.command} `.includes(signature)) return false;
|
|
204
|
+
const member = record.tree?.find((m) => m.pid === record.pid);
|
|
205
|
+
if (member) return member.startedAt === row.startedAt;
|
|
206
|
+
const recorded = Date.parse(record.startedAt);
|
|
207
|
+
return !(Number.isFinite(row.startedAt) && Number.isFinite(recorded) && Math.abs(row.startedAt - recorded) > 5e3);
|
|
208
|
+
}
|
|
209
|
+
function ourTree(record, rows, signature) {
|
|
210
|
+
const leader = ownsLeader(record, rows, signature);
|
|
211
|
+
return { leader, tree: merge(leader ? treeOf(record.pid, rows) : [], liveMembers(record.tree, rows)) };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ../../hosted-hub/lib/processes.mjs
|
|
215
|
+
var num = (key, fallback) => {
|
|
216
|
+
const raw = process.env[key];
|
|
217
|
+
return raw === void 0 || raw === "" || !Number.isFinite(Number(raw)) ? fallback : Number(raw);
|
|
218
|
+
};
|
|
219
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
220
|
+
var CRASH_LIMIT = num("ROSTER_CRASH_LIMIT", 5);
|
|
221
|
+
var CRASH_WINDOW_MS = num("ROSTER_CRASH_WINDOW_MS", 10 * 6e4);
|
|
222
|
+
var BACKOFF_BASE_MS = num("ROSTER_BACKOFF_BASE_MS", 1e3);
|
|
223
|
+
var BACKOFF_MAX_MS = num("ROSTER_BACKOFF_MAX_MS", 6e4);
|
|
224
|
+
var HEALTH_TIMEOUT_MS = num("ROSTER_START_HEALTH_TIMEOUT_MS", 12e4);
|
|
225
|
+
var PORT_RECHECK_MS = num("ROSTER_PORT_RECHECK_MS", 5e3);
|
|
226
|
+
var WATCH_MS = 1e3;
|
|
227
|
+
var TREE_MS = 5e3;
|
|
228
|
+
var LOG_ROTATE_BYTES = 2e7;
|
|
140
229
|
function createSupervisor({
|
|
141
230
|
root,
|
|
142
231
|
// the workspace: eve dev's cwd
|
|
@@ -149,18 +238,12 @@ function createSupervisor({
|
|
|
149
238
|
isCurrent = () => true,
|
|
150
239
|
// is this agent object still on the roster?
|
|
151
240
|
// ({ agent, env, argv }) -> what a server spawned now runs with
|
|
152
|
-
// (lib/fingerprint.mjs), or null. A previous
|
|
241
|
+
// (lib/fingerprint.mjs), or null. A previous runner's server is
|
|
153
242
|
// adopted only if its recorded fingerprint matches.
|
|
154
243
|
fingerprint = null,
|
|
155
244
|
onChange = () => {
|
|
156
245
|
},
|
|
157
246
|
// status changed (broadcast)
|
|
158
|
-
onUsage = () => {
|
|
159
|
-
},
|
|
160
|
-
// a running tree's memory changed (status() reports rssKb)
|
|
161
|
-
onRecovered = () => {
|
|
162
|
-
},
|
|
163
|
-
// healthy again after a crash restart
|
|
164
247
|
onUp = () => {
|
|
165
248
|
},
|
|
166
249
|
// a server is serving: adopted, spawned, or respawned ({ startedAt, adopted, recovered })
|
|
@@ -188,10 +271,7 @@ function createSupervisor({
|
|
|
188
271
|
status: e.status,
|
|
189
272
|
...e.tree?.length ? { tree: e.tree } : {},
|
|
190
273
|
...e.lastExit ? { lastExit: e.lastExit } : {},
|
|
191
|
-
...e.fingerprint ? { fingerprint: e.fingerprint } : {}
|
|
192
|
-
// The daemon token it was spawned with (HUB_INSTANCE): an adopted bot
|
|
193
|
-
// keeps its spawner's, and lib/daemon.mjs selfStopRefusal checks it here.
|
|
194
|
-
...e.instance ? { instance: e.instance } : {}
|
|
274
|
+
...e.fingerprint ? { fingerprint: e.fingerprint } : {}
|
|
195
275
|
}));
|
|
196
276
|
mkdirSync(stateDir, { recursive: true });
|
|
197
277
|
const tmp = `${recordsPath}.${process.pid}.tmp`;
|
|
@@ -212,34 +292,7 @@ function createSupervisor({
|
|
|
212
292
|
persist();
|
|
213
293
|
onChange(entry.name);
|
|
214
294
|
}
|
|
215
|
-
const
|
|
216
|
-
function follow(name, file, offset) {
|
|
217
|
-
tails.set(name, { file, offset, carry: "", prefix: `[${name}]`.padEnd(20) });
|
|
218
|
-
}
|
|
219
|
-
function pump() {
|
|
220
|
-
for (const tail of tails.values()) {
|
|
221
|
-
let size;
|
|
222
|
-
try {
|
|
223
|
-
size = statSync(tail.file).size;
|
|
224
|
-
} catch {
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (size < tail.offset) tail.offset = 0;
|
|
228
|
-
if (size === tail.offset) continue;
|
|
229
|
-
const length = Math.min(size - tail.offset, 1e6);
|
|
230
|
-
const buffer = Buffer.alloc(length);
|
|
231
|
-
const fd = openSync(tail.file, "r");
|
|
232
|
-
try {
|
|
233
|
-
readSync(fd, buffer, 0, length, tail.offset);
|
|
234
|
-
} finally {
|
|
235
|
-
closeSync(fd);
|
|
236
|
-
}
|
|
237
|
-
tail.offset += length;
|
|
238
|
-
const lines = (tail.carry + buffer.toString("utf8")).split("\n");
|
|
239
|
-
tail.carry = lines.pop();
|
|
240
|
-
for (const line of lines) if (line.trim() && !line.startsWith("Migration file of version")) log(`${tail.prefix} ${line}`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
295
|
+
const { follow, pump } = createLogTail(log);
|
|
243
296
|
const pumpTimer = setInterval(pump, 250);
|
|
244
297
|
pumpTimer.unref();
|
|
245
298
|
function refreshTree(entry, rows = table()) {
|
|
@@ -247,10 +300,8 @@ function createSupervisor({
|
|
|
247
300
|
const before = JSON.stringify(entry.tree ?? []);
|
|
248
301
|
entry.tree = merge(treeOf(entry.pid, rows), liveMembers(entry.tree, rows));
|
|
249
302
|
const own = rows.filter((r) => r.pid === entry.pid || r.ppid === entry.pid);
|
|
250
|
-
const rss = entry.rssKb;
|
|
251
303
|
if (own.length) entry.rssKb = own.reduce((sum, r) => sum + (r.rssKb || 0), 0);
|
|
252
304
|
if (JSON.stringify(entry.tree) !== before) persist();
|
|
253
|
-
if (entry.rssKb !== rss) onUsage(entry.name);
|
|
254
305
|
}
|
|
255
306
|
const treeTimer = setInterval(() => {
|
|
256
307
|
const live = [...entries.values()].filter((e) => e.pid && !e.stopping);
|
|
@@ -259,45 +310,8 @@ function createSupervisor({
|
|
|
259
310
|
for (const e of live) refreshTree(e, rows);
|
|
260
311
|
}, TREE_MS);
|
|
261
312
|
treeTimer.unref();
|
|
262
|
-
const treeAlive = (pgid, tree) => groupAlive(pgid) || liveMembers(tree, table()).length > 0;
|
|
263
|
-
async function untilTreeGone(pgid, tree, ms) {
|
|
264
|
-
const deadline = Date.now() + ms;
|
|
265
|
-
while (treeAlive(pgid, tree)) {
|
|
266
|
-
if (Date.now() >= deadline) return false;
|
|
267
|
-
await sleep(200);
|
|
268
|
-
}
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
function killTree(pgid, tree) {
|
|
272
|
-
signalGroup(pgid, "SIGKILL");
|
|
273
|
-
for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) signalGroup(g, "SIGKILL");
|
|
274
|
-
}
|
|
275
|
-
async function stopTree(pgid, tree, name) {
|
|
276
|
-
if (!treeAlive(pgid, tree)) return true;
|
|
277
|
-
let membersTermed = false;
|
|
278
|
-
const termMembers = () => {
|
|
279
|
-
membersTermed = true;
|
|
280
|
-
for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) if (g !== pgid) signalGroup(g, "SIGTERM");
|
|
281
|
-
};
|
|
282
|
-
if (groupAlive(pgid)) signalGroup(pgid, "SIGTERM");
|
|
283
|
-
else termMembers();
|
|
284
|
-
const deadline = Date.now() + stopTimeoutMs;
|
|
285
|
-
let gone = true;
|
|
286
|
-
while (treeAlive(pgid, tree)) {
|
|
287
|
-
if (!membersTermed && !groupAlive(pgid)) termMembers();
|
|
288
|
-
if (Date.now() >= deadline) {
|
|
289
|
-
gone = false;
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
await sleep(200);
|
|
293
|
-
}
|
|
294
|
-
if (gone) return true;
|
|
295
|
-
warn(`[registry] ${name} didn't stop within ${stopTimeoutMs / 1e3}s of SIGTERM; sending SIGKILL`);
|
|
296
|
-
killTree(pgid, tree);
|
|
297
|
-
return untilTreeGone(pgid, tree, KILL_GRACE_MS);
|
|
298
|
-
}
|
|
299
313
|
const logFile = (name) => join(logDir, `${name}.log`);
|
|
300
|
-
const sizeOf = (file) => existsSync(file) ?
|
|
314
|
+
const sizeOf = (file) => existsSync(file) ? statSync2(file).size : 0;
|
|
301
315
|
function entryFor(agent) {
|
|
302
316
|
let entry = entries.get(agent.name);
|
|
303
317
|
if (!entry) {
|
|
@@ -308,33 +322,19 @@ function createSupervisor({
|
|
|
308
322
|
entry.port = agent.port;
|
|
309
323
|
return entry;
|
|
310
324
|
}
|
|
311
|
-
function ownsLeader(record, rows) {
|
|
312
|
-
if (!(record.pid > 1) || record.pgid !== record.pid) return false;
|
|
313
|
-
const row = rows.find((r) => r.pid === record.pid);
|
|
314
|
-
if (!row || row.pgid !== record.pgid) return false;
|
|
315
|
-
if (!` ${row.command} `.includes(signature(record.agent, record.port))) return false;
|
|
316
|
-
const member = record.tree?.find((m) => m.pid === record.pid);
|
|
317
|
-
if (member) return member.startedAt === row.startedAt;
|
|
318
|
-
const recorded = Date.parse(record.startedAt);
|
|
319
|
-
return !(Number.isFinite(row.startedAt) && Number.isFinite(recorded) && Math.abs(row.startedAt - recorded) > 5e3);
|
|
320
|
-
}
|
|
321
|
-
function ourTree(record, rows) {
|
|
322
|
-
const leader = ownsLeader(record, rows);
|
|
323
|
-
return { leader, tree: merge(leader ? treeOf(record.pid, rows) : [], liveMembers(record.tree, rows)) };
|
|
324
|
-
}
|
|
325
325
|
async function killStale(record, why) {
|
|
326
326
|
const rows = table();
|
|
327
|
-
const { leader, tree } = ourTree(record, rows);
|
|
327
|
+
const { leader, tree } = ourTree(record, rows, signature(record.agent, record.port));
|
|
328
328
|
if (!leader && !tree.length) {
|
|
329
|
-
if (groupAlive(record.pgid)) warn(`[
|
|
329
|
+
if (groupAlive(record.pgid)) warn(`[runner] ! ${record.agent}: process group ${record.pgid} from processes.json is no longer its server; leaving it alone`);
|
|
330
330
|
return true;
|
|
331
331
|
}
|
|
332
|
-
warn(`[
|
|
333
|
-
return stopTree(leader ? record.pgid : null, tree, record.agent);
|
|
332
|
+
warn(`[runner] ${record.agent}: stopping its stale server (pid ${record.pid}): ${why}`);
|
|
333
|
+
return stopTree(leader ? record.pgid : null, tree, { name: record.agent, timeoutMs: stopTimeoutMs, warn });
|
|
334
334
|
}
|
|
335
335
|
async function vetAdoption(agent, record) {
|
|
336
336
|
const rows = table();
|
|
337
|
-
const { leader, tree } = ourTree(record, rows);
|
|
337
|
+
const { leader, tree } = ourTree(record, rows, signature(record.agent, record.port));
|
|
338
338
|
if (!leader && !tree.length) return { adopted: false };
|
|
339
339
|
if (!leader) return { adopted: false, stale: true, why: "its leader is gone" };
|
|
340
340
|
if (record.port !== agent.port) return { adopted: false, stale: true, why: `its port moved from ${record.port} to ${agent.port}` };
|
|
@@ -366,7 +366,7 @@ function createSupervisor({
|
|
|
366
366
|
const { tree } = vetted;
|
|
367
367
|
if (closed) return carry(agent, record, tree);
|
|
368
368
|
const entry = entryFor(agent);
|
|
369
|
-
Object.assign(entry, { pid: record.pid, pgid: record.pgid, startedAt: record.startedAt, restarts: record.restarts ?? 0, status: "up", adopted: true, child: null, stopping: false, error: void 0, tree, fingerprint: record.fingerprint,
|
|
369
|
+
Object.assign(entry, { pid: record.pid, pgid: record.pgid, startedAt: record.startedAt, restarts: record.restarts ?? 0, status: "up", adopted: true, child: null, stopping: false, error: void 0, tree, fingerprint: record.fingerprint, rssKb: void 0 });
|
|
370
370
|
const gen = ++entry.gen;
|
|
371
371
|
follow(agent.name, logFile(agent.name), sizeOf(logFile(agent.name)));
|
|
372
372
|
entry.watch = setInterval(() => {
|
|
@@ -377,14 +377,14 @@ function createSupervisor({
|
|
|
377
377
|
}
|
|
378
378
|
}, WATCH_MS);
|
|
379
379
|
entry.watch.unref();
|
|
380
|
-
log(`[
|
|
380
|
+
log(`[runner] adopted ${agent.name} (pid ${record.pid}, port ${agent.port})`);
|
|
381
381
|
changed(entry);
|
|
382
382
|
onUp(agent, { startedAt: entry.startedAt, adopted: true, recovered: false });
|
|
383
383
|
return { adopted: true };
|
|
384
384
|
}
|
|
385
385
|
function carry(agent, record, tree = record.tree ?? []) {
|
|
386
386
|
const entry = entryFor(agent);
|
|
387
|
-
Object.assign(entry, { pid: record.pid, pgid: record.pgid, startedAt: record.startedAt, restarts: record.restarts ?? 0, status: record.status ?? "up", adopted: true, child: null, stopping: false, tree, fingerprint: record.fingerprint
|
|
387
|
+
Object.assign(entry, { pid: record.pid, pgid: record.pgid, startedAt: record.startedAt, restarts: record.restarts ?? 0, status: record.status ?? "up", adopted: true, child: null, stopping: false, tree, fingerprint: record.fingerprint });
|
|
388
388
|
entry.gen++;
|
|
389
389
|
return { adopted: true, carried: true };
|
|
390
390
|
}
|
|
@@ -410,7 +410,7 @@ function createSupervisor({
|
|
|
410
410
|
return spawnAgent(entry, { recovered });
|
|
411
411
|
}
|
|
412
412
|
const error = `port ${port} is held by another process (${holders.map((pid) => describe(pid, rows)).join("; ")}), not by ${name}'s server; not touching it. ${name} starts once the port is free.`;
|
|
413
|
-
if (entry.error !== error || entry.status !== "blocked") warn(`[
|
|
413
|
+
if (entry.error !== error || entry.status !== "blocked") warn(`[runner] ! ${error}`);
|
|
414
414
|
Object.assign(entry, { status: "blocked", error, pid: null, pgid: null });
|
|
415
415
|
entry.timer = setTimeout(() => spawnAgent(entry, { recovered }), PORT_RECHECK_MS);
|
|
416
416
|
entry.timer.unref();
|
|
@@ -419,7 +419,7 @@ function createSupervisor({
|
|
|
419
419
|
mkdirSync(logDir, { recursive: true });
|
|
420
420
|
const file = logFile(name);
|
|
421
421
|
if (sizeOf(file) > LOG_ROTATE_BYTES) renameSync(file, `${file}.1`);
|
|
422
|
-
const fd =
|
|
422
|
+
const fd = openSync2(file, "a");
|
|
423
423
|
writeSync(fd, `
|
|
424
424
|
--- ${(/* @__PURE__ */ new Date()).toISOString()} hub: starting ${name} on port ${port}
|
|
425
425
|
`);
|
|
@@ -429,7 +429,7 @@ function createSupervisor({
|
|
|
429
429
|
try {
|
|
430
430
|
fp = fingerprintFor(agent, spawnEnv);
|
|
431
431
|
} catch (error) {
|
|
432
|
-
warn(`[
|
|
432
|
+
warn(`[runner] ! ${name}: couldn't fingerprint its inputs (${error.message}); the next runner respawns it instead of adopting it`);
|
|
433
433
|
}
|
|
434
434
|
let child;
|
|
435
435
|
try {
|
|
@@ -437,17 +437,17 @@ function createSupervisor({
|
|
|
437
437
|
cwd: root,
|
|
438
438
|
env: spawnEnv,
|
|
439
439
|
detached: true,
|
|
440
|
-
// own process group; survives the
|
|
440
|
+
// own process group; survives the runner
|
|
441
441
|
stdio: ["ignore", fd, fd]
|
|
442
442
|
});
|
|
443
443
|
} finally {
|
|
444
|
-
|
|
444
|
+
closeSync2(fd);
|
|
445
445
|
}
|
|
446
446
|
const gen = ++entry.gen;
|
|
447
|
-
Object.assign(entry, { child, pid: child.pid ?? null, pgid: child.pid ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "starting", adopted: false, stopping: false, error: void 0, nextRestartAt: void 0, tree: [], slowWarned: false, fingerprint: fp ?? void 0,
|
|
447
|
+
Object.assign(entry, { child, pid: child.pid ?? null, pgid: child.pid ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "starting", adopted: false, stopping: false, error: void 0, nextRestartAt: void 0, tree: [], slowWarned: false, fingerprint: fp ?? void 0, rssKb: void 0 });
|
|
448
448
|
follow(name, file, sizeOf(file));
|
|
449
449
|
child.once("error", (error) => {
|
|
450
|
-
warn(`[
|
|
450
|
+
warn(`[runner] ! couldn't start ${name}: ${error.message}`);
|
|
451
451
|
exited(entry, gen, { code: null, signal: null, error: error.message });
|
|
452
452
|
});
|
|
453
453
|
child.once("exit", (code, signal) => exited(entry, gen, { code, signal }));
|
|
@@ -472,15 +472,14 @@ function createSupervisor({
|
|
|
472
472
|
entry.status = "up";
|
|
473
473
|
changed(entry);
|
|
474
474
|
onUp(entry.agent, { startedAt: entry.startedAt, adopted: false, recovered });
|
|
475
|
-
if (recovered) onRecovered(entry.agent);
|
|
476
475
|
return;
|
|
477
476
|
}
|
|
478
477
|
}
|
|
479
478
|
if (Date.now() - started > HEALTH_TIMEOUT_MS && !entry.slowWarned) {
|
|
480
|
-
warn(`[
|
|
479
|
+
warn(`[runner] ! ${entry.name} isn't healthy ${HEALTH_TIMEOUT_MS / 1e3}s after starting; see ${logFile(entry.name)}`);
|
|
481
480
|
entry.slowWarned = true;
|
|
482
481
|
}
|
|
483
|
-
await
|
|
482
|
+
await sleep2(500);
|
|
484
483
|
}
|
|
485
484
|
}
|
|
486
485
|
function exited(entry, gen, { code, signal, error }) {
|
|
@@ -506,13 +505,13 @@ function createSupervisor({
|
|
|
506
505
|
if (entry.crashes.length >= CRASH_LIMIT) {
|
|
507
506
|
entry.status = "crashed";
|
|
508
507
|
entry.error = `exited ${entry.crashes.length} times within ${Math.round(CRASH_WINDOW_MS / 6e4)} min (last: ${how}); not restarting until restart_agent. Log: ${logFile(entry.name)}`;
|
|
509
|
-
warn(`[
|
|
508
|
+
warn(`[runner] ! ${entry.name} crashed: ${entry.error}`);
|
|
510
509
|
return changed(entry);
|
|
511
510
|
}
|
|
512
511
|
const delay2 = Math.min(BACKOFF_BASE_MS * 2 ** (entry.crashes.length - 1), BACKOFF_MAX_MS);
|
|
513
512
|
entry.status = "down";
|
|
514
513
|
entry.nextRestartAt = new Date(now + delay2).toISOString();
|
|
515
|
-
warn(`[
|
|
514
|
+
warn(`[runner] ${entry.name} exited unexpectedly (${how}); restarting in ${delay2 / 1e3}s`);
|
|
516
515
|
entry.timer = setTimeout(async () => {
|
|
517
516
|
await untilTreeGone(pgid, tree, KILL_GRACE_MS);
|
|
518
517
|
if (entry.status !== "down" || entry.stopping) return;
|
|
@@ -524,7 +523,7 @@ function createSupervisor({
|
|
|
524
523
|
changed(entry);
|
|
525
524
|
}
|
|
526
525
|
return {
|
|
527
|
-
// Startup: adopt what's still running from a previous
|
|
526
|
+
// Startup: adopt what's still running from a previous runner, start the rest.
|
|
528
527
|
startAll(agents) {
|
|
529
528
|
starting = (async () => {
|
|
530
529
|
const records = readRecords();
|
|
@@ -542,7 +541,7 @@ function createSupervisor({
|
|
|
542
541
|
const result = results.get(agent.name);
|
|
543
542
|
if (result.adopted) return;
|
|
544
543
|
if (closed && !result.superseded) {
|
|
545
|
-
if (ourTree(record, table()).tree.length) carry(agent, record);
|
|
544
|
+
if (ourTree(record, table(), signature(record.agent, record.port)).tree.length) carry(agent, record);
|
|
546
545
|
return;
|
|
547
546
|
}
|
|
548
547
|
await killStale(record, result.why ?? "it isn't serving");
|
|
@@ -586,7 +585,7 @@ function createSupervisor({
|
|
|
586
585
|
}
|
|
587
586
|
entry.status = "stopping";
|
|
588
587
|
changed(entry);
|
|
589
|
-
const gone = await stopTree(pgid, tree, agent.name);
|
|
588
|
+
const gone = await stopTree(pgid, tree, { name: agent.name, timeoutMs: stopTimeoutMs, warn });
|
|
590
589
|
if (gone) {
|
|
591
590
|
if (entry.pgid === pgid) {
|
|
592
591
|
entry.gen++;
|
|
@@ -608,42 +607,6 @@ function createSupervisor({
|
|
|
608
607
|
pump();
|
|
609
608
|
persist();
|
|
610
609
|
},
|
|
611
|
-
// Exit without stopping any bot (a `hub restart` that keeps them): nothing
|
|
612
|
-
// new starts, pending (re)starts are cancelled, each tree is re-read and
|
|
613
|
-
// saved, and no signal is sent. The next registry vets every record
|
|
614
|
-
// (vetAdoption) and adopts what still matches. Returns the bots left running.
|
|
615
|
-
async detach() {
|
|
616
|
-
closed = true;
|
|
617
|
-
await starting.catch(() => {
|
|
618
|
-
});
|
|
619
|
-
clearInterval(pumpTimer);
|
|
620
|
-
clearInterval(treeTimer);
|
|
621
|
-
const rows = table();
|
|
622
|
-
for (const e of entries.values()) {
|
|
623
|
-
clearTimeout(e.timer);
|
|
624
|
-
e.timer = void 0;
|
|
625
|
-
e.claim = (e.claim ?? 0) + 1;
|
|
626
|
-
clearInterval(e.watch);
|
|
627
|
-
e.gen++;
|
|
628
|
-
if (e.pid) refreshTree(e, rows);
|
|
629
|
-
}
|
|
630
|
-
pump();
|
|
631
|
-
persist();
|
|
632
|
-
return [...entries.values()].filter((e) => e.pid).map((e) => ({ name: e.name, pid: e.pid }));
|
|
633
|
-
},
|
|
634
|
-
// Drop a removed bot's record (after its server is confirmed down).
|
|
635
|
-
forget(name) {
|
|
636
|
-
const entry = entries.get(name);
|
|
637
|
-
if (entry) {
|
|
638
|
-
clearTimeout(entry.timer);
|
|
639
|
-
clearInterval(entry.watch);
|
|
640
|
-
entry.gen++;
|
|
641
|
-
entry.claim = (entry.claim ?? 0) + 1;
|
|
642
|
-
}
|
|
643
|
-
entries.delete(name);
|
|
644
|
-
tails.delete(name);
|
|
645
|
-
persist();
|
|
646
|
-
},
|
|
647
610
|
// Resolves true once the bot is up (spawned by us and healthy); false
|
|
648
611
|
// once it can't be: it crashed, left the roster, or this supervisor closed.
|
|
649
612
|
async waitUp(agent, timeoutMs) {
|
|
@@ -653,21 +616,10 @@ function createSupervisor({
|
|
|
653
616
|
if (closed || !isCurrent(agent)) return false;
|
|
654
617
|
if (entry?.agent === agent && entry.status === "up") return true;
|
|
655
618
|
if (entry?.status === "crashed") return false;
|
|
656
|
-
await
|
|
619
|
+
await sleep2(250);
|
|
657
620
|
}
|
|
658
621
|
return false;
|
|
659
622
|
},
|
|
660
|
-
// A running bot's memory as of the last tree refresh (eve dev + its
|
|
661
|
-
// server) and its uptime (an adopted one keeps its start). Kept out of
|
|
662
|
-
// status(): that's in every state snapshot, and these change constantly.
|
|
663
|
-
usage(name) {
|
|
664
|
-
const e = entries.get(name);
|
|
665
|
-
if (!e?.pid || e.status !== "up") return void 0;
|
|
666
|
-
return {
|
|
667
|
-
...e.rssKb ? { rssMb: Math.round(e.rssKb / 1024) } : {},
|
|
668
|
-
...e.startedAt ? { uptimeS: Math.max(0, Math.round((Date.now() - Date.parse(e.startedAt)) / 1e3)) } : {}
|
|
669
|
-
};
|
|
670
|
-
},
|
|
671
623
|
status(name) {
|
|
672
624
|
const e = entries.get(name);
|
|
673
625
|
if (!e) return null;
|
|
@@ -742,7 +694,7 @@ async function createSession(fetchFn, eve, operationId) {
|
|
|
742
694
|
return sessionId;
|
|
743
695
|
}
|
|
744
696
|
|
|
745
|
-
// ../../lib/v26-facts.mjs
|
|
697
|
+
// ../../hosted-hub/lib/v26-facts.mjs
|
|
746
698
|
function factsOfV26(event) {
|
|
747
699
|
if (!event || typeof event.type !== "string") return { facts: [], progress: false };
|
|
748
700
|
const data = isRecord(event.data) ? event.data : {};
|
|
@@ -962,7 +914,8 @@ function fact(type, data, source) {
|
|
|
962
914
|
}
|
|
963
915
|
var NO_RUN_SCOPE = /* @__PURE__ */ new Set(["context.started", "context.settled", "interaction.opened", "interaction.settled", "response.submitted", "response.settled"]);
|
|
964
916
|
function content(data, kind, phase, value) {
|
|
965
|
-
|
|
917
|
+
const run = typeof data.stepIndex === "number" ? { runId: runIdOf(data) } : {};
|
|
918
|
+
return fact("content.completed", { partId: partIdOf(data, kind), ...run, kind, phase, value }, data);
|
|
966
919
|
}
|
|
967
920
|
function turnSettled(data, settled) {
|
|
968
921
|
const turnId = str(data.turnId);
|
|
@@ -1072,7 +1025,7 @@ var str = (value) => typeof value === "string" ? value : void 0;
|
|
|
1072
1025
|
var num2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1073
1026
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1074
1027
|
|
|
1075
|
-
// ../../lib/session-stream.mjs
|
|
1028
|
+
// ../../hosted-hub/lib/session-stream.mjs
|
|
1076
1029
|
async function* readSessionLines(body, startIndex) {
|
|
1077
1030
|
let line = startIndex;
|
|
1078
1031
|
let lastAt = null;
|
|
@@ -1155,9 +1108,19 @@ async function* streamLines(eve, sessionId, o) {
|
|
|
1155
1108
|
}
|
|
1156
1109
|
const idle = new AbortController(), signal = o.signal ? AbortSignal.any([o.signal, idle.signal]) : idle.signal;
|
|
1157
1110
|
let timer = setTimeout(() => idle.abort(), idleMs);
|
|
1111
|
+
const opened = Date.now();
|
|
1112
|
+
let firstByte = false, lineAt = opened;
|
|
1158
1113
|
const quiet = () => {
|
|
1159
1114
|
clearTimeout(timer);
|
|
1160
1115
|
timer = setTimeout(() => idle.abort(), idleMs);
|
|
1116
|
+
if (!firstByte) {
|
|
1117
|
+
firstByte = true;
|
|
1118
|
+
console.log(JSON.stringify({ event: "hub.stream_first_byte", sessionId, startIndex: next, ms: Date.now() - opened }));
|
|
1119
|
+
}
|
|
1120
|
+
if (Date.now() - lineAt >= idleMs) {
|
|
1121
|
+
console.log(JSON.stringify({ event: "hub.stream_no_lines", sessionId, next, ms: Date.now() - lineAt }));
|
|
1122
|
+
lineAt = Date.now();
|
|
1123
|
+
}
|
|
1161
1124
|
};
|
|
1162
1125
|
let renewed = false;
|
|
1163
1126
|
try {
|
|
@@ -1185,6 +1148,7 @@ async function* streamLines(eve, sessionId, o) {
|
|
|
1185
1148
|
break;
|
|
1186
1149
|
}
|
|
1187
1150
|
yield record;
|
|
1151
|
+
lineAt = Date.now();
|
|
1188
1152
|
next = record.line + 1;
|
|
1189
1153
|
reopens = o.reopens ?? 3;
|
|
1190
1154
|
}
|
package/main.mjs
CHANGED
|
@@ -496,14 +496,14 @@ var require_graceful_fs = __commonJS({
|
|
|
496
496
|
return close;
|
|
497
497
|
})(fs.close);
|
|
498
498
|
fs.closeSync = (function(fs$closeSync) {
|
|
499
|
-
function
|
|
499
|
+
function closeSync4(fd) {
|
|
500
500
|
fs$closeSync.apply(fs, arguments);
|
|
501
501
|
resetQueue();
|
|
502
502
|
}
|
|
503
|
-
Object.defineProperty(
|
|
503
|
+
Object.defineProperty(closeSync4, previousSymbol, {
|
|
504
504
|
value: fs$closeSync
|
|
505
505
|
});
|
|
506
|
-
return
|
|
506
|
+
return closeSync4;
|
|
507
507
|
})(fs.closeSync);
|
|
508
508
|
if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || "")) {
|
|
509
509
|
process.on("exit", function() {
|
|
@@ -1619,7 +1619,7 @@ import { randomUUID } from "node:crypto";
|
|
|
1619
1619
|
import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync10, realpathSync as realpathSync4, writeFileSync as writeFileSync7 } from "node:fs";
|
|
1620
1620
|
import { join as join16 } from "node:path";
|
|
1621
1621
|
|
|
1622
|
-
// ../../lib/clean-env.mjs
|
|
1622
|
+
// ../../hosted-hub/lib/clean-env.mjs
|
|
1623
1623
|
var INHERITED_ENV = /^(EVE_|WORKFLOW_)/;
|
|
1624
1624
|
function cleanEnv(env = process.env) {
|
|
1625
1625
|
return Object.fromEntries(Object.entries(env).filter(([key]) => !INHERITED_ENV.test(key)));
|
|
@@ -1629,12 +1629,12 @@ function cleanGitEnv(env = process.env) {
|
|
|
1629
1629
|
return Object.fromEntries(Object.entries(env).filter(([key]) => !GIT_REPO_ENV.test(key)));
|
|
1630
1630
|
}
|
|
1631
1631
|
|
|
1632
|
-
// ../../lib/processes.mjs
|
|
1633
|
-
import { spawn
|
|
1634
|
-
import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2,
|
|
1632
|
+
// ../../hosted-hub/lib/processes.mjs
|
|
1633
|
+
import { spawn } from "node:child_process";
|
|
1634
|
+
import { closeSync as closeSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync2, renameSync, rmSync, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync2, writeSync } from "node:fs";
|
|
1635
1635
|
import { join as join2 } from "node:path";
|
|
1636
1636
|
|
|
1637
|
-
// ../../lib/fingerprint.mjs
|
|
1637
|
+
// ../../hosted-hub/lib/fingerprint.mjs
|
|
1638
1638
|
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
1639
1639
|
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
1640
1640
|
import { join, relative } from "node:path";
|
|
@@ -1701,22 +1701,44 @@ function fingerprintDiff(recorded, current) {
|
|
|
1701
1701
|
}
|
|
1702
1702
|
var BUILD_SKIP = /* @__PURE__ */ new Set([...TREE_SKIP, ".eve", ".output"]);
|
|
1703
1703
|
|
|
1704
|
-
// ../../lib/
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1704
|
+
// ../../hosted-hub/lib/log-tail.mjs
|
|
1705
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
1706
|
+
function createLogTail(log2) {
|
|
1707
|
+
const tails = /* @__PURE__ */ new Map();
|
|
1708
|
+
function follow(name, file, offset) {
|
|
1709
|
+
tails.set(name, { file, offset, carry: "", prefix: `[${name}]`.padEnd(20) });
|
|
1710
|
+
}
|
|
1711
|
+
function pump() {
|
|
1712
|
+
for (const tail of tails.values()) {
|
|
1713
|
+
let size;
|
|
1714
|
+
try {
|
|
1715
|
+
size = statSync(tail.file).size;
|
|
1716
|
+
} catch {
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
if (size < tail.offset) tail.offset = 0;
|
|
1720
|
+
if (size === tail.offset) continue;
|
|
1721
|
+
const length = Math.min(size - tail.offset, 1e6);
|
|
1722
|
+
const buffer = Buffer.alloc(length);
|
|
1723
|
+
const fd = openSync(tail.file, "r");
|
|
1724
|
+
try {
|
|
1725
|
+
readSync(fd, buffer, 0, length, tail.offset);
|
|
1726
|
+
} finally {
|
|
1727
|
+
closeSync(fd);
|
|
1728
|
+
}
|
|
1729
|
+
tail.offset += length;
|
|
1730
|
+
const lines = (tail.carry + buffer.toString("utf8")).split("\n");
|
|
1731
|
+
tail.carry = lines.pop();
|
|
1732
|
+
for (const line of lines) if (line.trim() && !line.startsWith("Migration file of version")) log2(`${tail.prefix} ${line}`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return { follow, pump };
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// ../../hosted-hub/lib/process-tree.mjs
|
|
1739
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
1709
1740
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1710
|
-
var CRASH_LIMIT = num("ROSTER_CRASH_LIMIT", 5);
|
|
1711
|
-
var CRASH_WINDOW_MS = num("ROSTER_CRASH_WINDOW_MS", 10 * 6e4);
|
|
1712
|
-
var BACKOFF_BASE_MS = num("ROSTER_BACKOFF_BASE_MS", 1e3);
|
|
1713
|
-
var BACKOFF_MAX_MS = num("ROSTER_BACKOFF_MAX_MS", 6e4);
|
|
1714
|
-
var HEALTH_TIMEOUT_MS = num("ROSTER_START_HEALTH_TIMEOUT_MS", 12e4);
|
|
1715
|
-
var PORT_RECHECK_MS = num("ROSTER_PORT_RECHECK_MS", 5e3);
|
|
1716
1741
|
var KILL_GRACE_MS = 3e3;
|
|
1717
|
-
var WATCH_MS = 1e3;
|
|
1718
|
-
var TREE_MS = 5e3;
|
|
1719
|
-
var LOG_ROTATE_BYTES = 2e7;
|
|
1720
1742
|
function groupAlive(pgid) {
|
|
1721
1743
|
if (!(pgid > 1)) return false;
|
|
1722
1744
|
try {
|
|
@@ -1739,7 +1761,7 @@ var C = { ...process.env, LC_ALL: "C" };
|
|
|
1739
1761
|
var SLOW_MS = 1e3;
|
|
1740
1762
|
function slow(what, started2) {
|
|
1741
1763
|
const ms = Date.now() - started2;
|
|
1742
|
-
if (ms >= SLOW_MS) console.warn(`[
|
|
1764
|
+
if (ms >= SLOW_MS) console.warn(`[runner] ${what} took ${ms}ms`);
|
|
1743
1765
|
}
|
|
1744
1766
|
var LSTART = String.raw`(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})`;
|
|
1745
1767
|
function table() {
|
|
@@ -1802,6 +1824,73 @@ async function healthy(port, ms = 1e3) {
|
|
|
1802
1824
|
}
|
|
1803
1825
|
}
|
|
1804
1826
|
var merge = (...lists) => [...new Map(lists.flat().map((m) => [m.pid, m])).values()];
|
|
1827
|
+
var treeAlive = (pgid, tree) => groupAlive(pgid) || liveMembers(tree, table()).length > 0;
|
|
1828
|
+
async function untilTreeGone(pgid, tree, ms) {
|
|
1829
|
+
const deadline = Date.now() + ms;
|
|
1830
|
+
while (treeAlive(pgid, tree)) {
|
|
1831
|
+
if (Date.now() >= deadline) return false;
|
|
1832
|
+
await sleep(200);
|
|
1833
|
+
}
|
|
1834
|
+
return true;
|
|
1835
|
+
}
|
|
1836
|
+
function killTree(pgid, tree) {
|
|
1837
|
+
signalGroup(pgid, "SIGKILL");
|
|
1838
|
+
for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) signalGroup(g, "SIGKILL");
|
|
1839
|
+
}
|
|
1840
|
+
async function stopTree(pgid, tree, { name, timeoutMs, warn }) {
|
|
1841
|
+
if (!treeAlive(pgid, tree)) return true;
|
|
1842
|
+
let membersTermed = false;
|
|
1843
|
+
const termMembers = () => {
|
|
1844
|
+
membersTermed = true;
|
|
1845
|
+
for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) if (g !== pgid) signalGroup(g, "SIGTERM");
|
|
1846
|
+
};
|
|
1847
|
+
if (groupAlive(pgid)) signalGroup(pgid, "SIGTERM");
|
|
1848
|
+
else termMembers();
|
|
1849
|
+
const deadline = Date.now() + timeoutMs;
|
|
1850
|
+
let gone = true;
|
|
1851
|
+
while (treeAlive(pgid, tree)) {
|
|
1852
|
+
if (!membersTermed && !groupAlive(pgid)) termMembers();
|
|
1853
|
+
if (Date.now() >= deadline) {
|
|
1854
|
+
gone = false;
|
|
1855
|
+
break;
|
|
1856
|
+
}
|
|
1857
|
+
await sleep(200);
|
|
1858
|
+
}
|
|
1859
|
+
if (gone) return true;
|
|
1860
|
+
warn(`[runner] ${name} didn't stop within ${timeoutMs / 1e3}s of SIGTERM; sending SIGKILL`);
|
|
1861
|
+
killTree(pgid, tree);
|
|
1862
|
+
return untilTreeGone(pgid, tree, KILL_GRACE_MS);
|
|
1863
|
+
}
|
|
1864
|
+
function ownsLeader(record, rows, signature) {
|
|
1865
|
+
if (!(record.pid > 1) || record.pgid !== record.pid) return false;
|
|
1866
|
+
const row = rows.find((r) => r.pid === record.pid);
|
|
1867
|
+
if (!row || row.pgid !== record.pgid) return false;
|
|
1868
|
+
if (!` ${row.command} `.includes(signature)) return false;
|
|
1869
|
+
const member = record.tree?.find((m) => m.pid === record.pid);
|
|
1870
|
+
if (member) return member.startedAt === row.startedAt;
|
|
1871
|
+
const recorded = Date.parse(record.startedAt);
|
|
1872
|
+
return !(Number.isFinite(row.startedAt) && Number.isFinite(recorded) && Math.abs(row.startedAt - recorded) > 5e3);
|
|
1873
|
+
}
|
|
1874
|
+
function ourTree(record, rows, signature) {
|
|
1875
|
+
const leader = ownsLeader(record, rows, signature);
|
|
1876
|
+
return { leader, tree: merge(leader ? treeOf(record.pid, rows) : [], liveMembers(record.tree, rows)) };
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// ../../hosted-hub/lib/processes.mjs
|
|
1880
|
+
var num = (key, fallback) => {
|
|
1881
|
+
const raw = process.env[key];
|
|
1882
|
+
return raw === void 0 || raw === "" || !Number.isFinite(Number(raw)) ? fallback : Number(raw);
|
|
1883
|
+
};
|
|
1884
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1885
|
+
var CRASH_LIMIT = num("ROSTER_CRASH_LIMIT", 5);
|
|
1886
|
+
var CRASH_WINDOW_MS = num("ROSTER_CRASH_WINDOW_MS", 10 * 6e4);
|
|
1887
|
+
var BACKOFF_BASE_MS = num("ROSTER_BACKOFF_BASE_MS", 1e3);
|
|
1888
|
+
var BACKOFF_MAX_MS = num("ROSTER_BACKOFF_MAX_MS", 6e4);
|
|
1889
|
+
var HEALTH_TIMEOUT_MS = num("ROSTER_START_HEALTH_TIMEOUT_MS", 12e4);
|
|
1890
|
+
var PORT_RECHECK_MS = num("ROSTER_PORT_RECHECK_MS", 5e3);
|
|
1891
|
+
var WATCH_MS = 1e3;
|
|
1892
|
+
var TREE_MS = 5e3;
|
|
1893
|
+
var LOG_ROTATE_BYTES = 2e7;
|
|
1805
1894
|
function createSupervisor({
|
|
1806
1895
|
root,
|
|
1807
1896
|
// the workspace: eve dev's cwd
|
|
@@ -1814,18 +1903,12 @@ function createSupervisor({
|
|
|
1814
1903
|
isCurrent = () => true,
|
|
1815
1904
|
// is this agent object still on the roster?
|
|
1816
1905
|
// ({ agent, env, argv }) -> what a server spawned now runs with
|
|
1817
|
-
// (lib/fingerprint.mjs), or null. A previous
|
|
1906
|
+
// (lib/fingerprint.mjs), or null. A previous runner's server is
|
|
1818
1907
|
// adopted only if its recorded fingerprint matches.
|
|
1819
1908
|
fingerprint = null,
|
|
1820
1909
|
onChange = () => {
|
|
1821
1910
|
},
|
|
1822
1911
|
// status changed (broadcast)
|
|
1823
|
-
onUsage = () => {
|
|
1824
|
-
},
|
|
1825
|
-
// a running tree's memory changed (status() reports rssKb)
|
|
1826
|
-
onRecovered = () => {
|
|
1827
|
-
},
|
|
1828
|
-
// healthy again after a crash restart
|
|
1829
1912
|
onUp = () => {
|
|
1830
1913
|
},
|
|
1831
1914
|
// a server is serving: adopted, spawned, or respawned ({ startedAt, adopted, recovered })
|
|
@@ -1853,10 +1936,7 @@ function createSupervisor({
|
|
|
1853
1936
|
status: e.status,
|
|
1854
1937
|
...e.tree?.length ? { tree: e.tree } : {},
|
|
1855
1938
|
...e.lastExit ? { lastExit: e.lastExit } : {},
|
|
1856
|
-
...e.fingerprint ? { fingerprint: e.fingerprint } : {}
|
|
1857
|
-
// The daemon token it was spawned with (HUB_INSTANCE): an adopted bot
|
|
1858
|
-
// keeps its spawner's, and lib/daemon.mjs selfStopRefusal checks it here.
|
|
1859
|
-
...e.instance ? { instance: e.instance } : {}
|
|
1939
|
+
...e.fingerprint ? { fingerprint: e.fingerprint } : {}
|
|
1860
1940
|
}));
|
|
1861
1941
|
mkdirSync2(stateDir, { recursive: true });
|
|
1862
1942
|
const tmp = `${recordsPath}.${process.pid}.tmp`;
|
|
@@ -1877,34 +1957,7 @@ function createSupervisor({
|
|
|
1877
1957
|
persist();
|
|
1878
1958
|
onChange(entry.name);
|
|
1879
1959
|
}
|
|
1880
|
-
const
|
|
1881
|
-
function follow(name, file, offset) {
|
|
1882
|
-
tails.set(name, { file, offset, carry: "", prefix: `[${name}]`.padEnd(20) });
|
|
1883
|
-
}
|
|
1884
|
-
function pump() {
|
|
1885
|
-
for (const tail of tails.values()) {
|
|
1886
|
-
let size;
|
|
1887
|
-
try {
|
|
1888
|
-
size = statSync(tail.file).size;
|
|
1889
|
-
} catch {
|
|
1890
|
-
continue;
|
|
1891
|
-
}
|
|
1892
|
-
if (size < tail.offset) tail.offset = 0;
|
|
1893
|
-
if (size === tail.offset) continue;
|
|
1894
|
-
const length = Math.min(size - tail.offset, 1e6);
|
|
1895
|
-
const buffer = Buffer.alloc(length);
|
|
1896
|
-
const fd = openSync(tail.file, "r");
|
|
1897
|
-
try {
|
|
1898
|
-
readSync(fd, buffer, 0, length, tail.offset);
|
|
1899
|
-
} finally {
|
|
1900
|
-
closeSync(fd);
|
|
1901
|
-
}
|
|
1902
|
-
tail.offset += length;
|
|
1903
|
-
const lines = (tail.carry + buffer.toString("utf8")).split("\n");
|
|
1904
|
-
tail.carry = lines.pop();
|
|
1905
|
-
for (const line of lines) if (line.trim() && !line.startsWith("Migration file of version")) log2(`${tail.prefix} ${line}`);
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1960
|
+
const { follow, pump } = createLogTail(log2);
|
|
1908
1961
|
const pumpTimer = setInterval(pump, 250);
|
|
1909
1962
|
pumpTimer.unref();
|
|
1910
1963
|
function refreshTree(entry, rows = table()) {
|
|
@@ -1912,10 +1965,8 @@ function createSupervisor({
|
|
|
1912
1965
|
const before = JSON.stringify(entry.tree ?? []);
|
|
1913
1966
|
entry.tree = merge(treeOf(entry.pid, rows), liveMembers(entry.tree, rows));
|
|
1914
1967
|
const own = rows.filter((r) => r.pid === entry.pid || r.ppid === entry.pid);
|
|
1915
|
-
const rss = entry.rssKb;
|
|
1916
1968
|
if (own.length) entry.rssKb = own.reduce((sum, r) => sum + (r.rssKb || 0), 0);
|
|
1917
1969
|
if (JSON.stringify(entry.tree) !== before) persist();
|
|
1918
|
-
if (entry.rssKb !== rss) onUsage(entry.name);
|
|
1919
1970
|
}
|
|
1920
1971
|
const treeTimer = setInterval(() => {
|
|
1921
1972
|
const live = [...entries.values()].filter((e) => e.pid && !e.stopping);
|
|
@@ -1924,45 +1975,8 @@ function createSupervisor({
|
|
|
1924
1975
|
for (const e of live) refreshTree(e, rows);
|
|
1925
1976
|
}, TREE_MS);
|
|
1926
1977
|
treeTimer.unref();
|
|
1927
|
-
const treeAlive = (pgid, tree) => groupAlive(pgid) || liveMembers(tree, table()).length > 0;
|
|
1928
|
-
async function untilTreeGone(pgid, tree, ms) {
|
|
1929
|
-
const deadline = Date.now() + ms;
|
|
1930
|
-
while (treeAlive(pgid, tree)) {
|
|
1931
|
-
if (Date.now() >= deadline) return false;
|
|
1932
|
-
await sleep(200);
|
|
1933
|
-
}
|
|
1934
|
-
return true;
|
|
1935
|
-
}
|
|
1936
|
-
function killTree(pgid, tree) {
|
|
1937
|
-
signalGroup(pgid, "SIGKILL");
|
|
1938
|
-
for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) signalGroup(g, "SIGKILL");
|
|
1939
|
-
}
|
|
1940
|
-
async function stopTree(pgid, tree, name) {
|
|
1941
|
-
if (!treeAlive(pgid, tree)) return true;
|
|
1942
|
-
let membersTermed = false;
|
|
1943
|
-
const termMembers = () => {
|
|
1944
|
-
membersTermed = true;
|
|
1945
|
-
for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) if (g !== pgid) signalGroup(g, "SIGTERM");
|
|
1946
|
-
};
|
|
1947
|
-
if (groupAlive(pgid)) signalGroup(pgid, "SIGTERM");
|
|
1948
|
-
else termMembers();
|
|
1949
|
-
const deadline = Date.now() + stopTimeoutMs;
|
|
1950
|
-
let gone = true;
|
|
1951
|
-
while (treeAlive(pgid, tree)) {
|
|
1952
|
-
if (!membersTermed && !groupAlive(pgid)) termMembers();
|
|
1953
|
-
if (Date.now() >= deadline) {
|
|
1954
|
-
gone = false;
|
|
1955
|
-
break;
|
|
1956
|
-
}
|
|
1957
|
-
await sleep(200);
|
|
1958
|
-
}
|
|
1959
|
-
if (gone) return true;
|
|
1960
|
-
warn(`[registry] ${name} didn't stop within ${stopTimeoutMs / 1e3}s of SIGTERM; sending SIGKILL`);
|
|
1961
|
-
killTree(pgid, tree);
|
|
1962
|
-
return untilTreeGone(pgid, tree, KILL_GRACE_MS);
|
|
1963
|
-
}
|
|
1964
1978
|
const logFile = (name) => join2(logDir, `${name}.log`);
|
|
1965
|
-
const sizeOf = (file) => existsSync2(file) ?
|
|
1979
|
+
const sizeOf = (file) => existsSync2(file) ? statSync2(file).size : 0;
|
|
1966
1980
|
function entryFor(agent) {
|
|
1967
1981
|
let entry = entries.get(agent.name);
|
|
1968
1982
|
if (!entry) {
|
|
@@ -1973,33 +1987,19 @@ function createSupervisor({
|
|
|
1973
1987
|
entry.port = agent.port;
|
|
1974
1988
|
return entry;
|
|
1975
1989
|
}
|
|
1976
|
-
function ownsLeader(record, rows) {
|
|
1977
|
-
if (!(record.pid > 1) || record.pgid !== record.pid) return false;
|
|
1978
|
-
const row = rows.find((r) => r.pid === record.pid);
|
|
1979
|
-
if (!row || row.pgid !== record.pgid) return false;
|
|
1980
|
-
if (!` ${row.command} `.includes(signature(record.agent, record.port))) return false;
|
|
1981
|
-
const member = record.tree?.find((m) => m.pid === record.pid);
|
|
1982
|
-
if (member) return member.startedAt === row.startedAt;
|
|
1983
|
-
const recorded = Date.parse(record.startedAt);
|
|
1984
|
-
return !(Number.isFinite(row.startedAt) && Number.isFinite(recorded) && Math.abs(row.startedAt - recorded) > 5e3);
|
|
1985
|
-
}
|
|
1986
|
-
function ourTree(record, rows) {
|
|
1987
|
-
const leader = ownsLeader(record, rows);
|
|
1988
|
-
return { leader, tree: merge(leader ? treeOf(record.pid, rows) : [], liveMembers(record.tree, rows)) };
|
|
1989
|
-
}
|
|
1990
1990
|
async function killStale(record, why) {
|
|
1991
1991
|
const rows = table();
|
|
1992
|
-
const { leader, tree } = ourTree(record, rows);
|
|
1992
|
+
const { leader, tree } = ourTree(record, rows, signature(record.agent, record.port));
|
|
1993
1993
|
if (!leader && !tree.length) {
|
|
1994
|
-
if (groupAlive(record.pgid)) warn(`[
|
|
1994
|
+
if (groupAlive(record.pgid)) warn(`[runner] ! ${record.agent}: process group ${record.pgid} from processes.json is no longer its server; leaving it alone`);
|
|
1995
1995
|
return true;
|
|
1996
1996
|
}
|
|
1997
|
-
warn(`[
|
|
1998
|
-
return stopTree(leader ? record.pgid : null, tree, record.agent);
|
|
1997
|
+
warn(`[runner] ${record.agent}: stopping its stale server (pid ${record.pid}): ${why}`);
|
|
1998
|
+
return stopTree(leader ? record.pgid : null, tree, { name: record.agent, timeoutMs: stopTimeoutMs, warn });
|
|
1999
1999
|
}
|
|
2000
2000
|
async function vetAdoption(agent, record) {
|
|
2001
2001
|
const rows = table();
|
|
2002
|
-
const { leader, tree } = ourTree(record, rows);
|
|
2002
|
+
const { leader, tree } = ourTree(record, rows, signature(record.agent, record.port));
|
|
2003
2003
|
if (!leader && !tree.length) return { adopted: false };
|
|
2004
2004
|
if (!leader) return { adopted: false, stale: true, why: "its leader is gone" };
|
|
2005
2005
|
if (record.port !== agent.port) return { adopted: false, stale: true, why: `its port moved from ${record.port} to ${agent.port}` };
|
|
@@ -2031,7 +2031,7 @@ function createSupervisor({
|
|
|
2031
2031
|
const { tree } = vetted;
|
|
2032
2032
|
if (closed) return carry(agent, record, tree);
|
|
2033
2033
|
const entry = entryFor(agent);
|
|
2034
|
-
Object.assign(entry, { pid: record.pid, pgid: record.pgid, startedAt: record.startedAt, restarts: record.restarts ?? 0, status: "up", adopted: true, child: null, stopping: false, error: void 0, tree, fingerprint: record.fingerprint,
|
|
2034
|
+
Object.assign(entry, { pid: record.pid, pgid: record.pgid, startedAt: record.startedAt, restarts: record.restarts ?? 0, status: "up", adopted: true, child: null, stopping: false, error: void 0, tree, fingerprint: record.fingerprint, rssKb: void 0 });
|
|
2035
2035
|
const gen = ++entry.gen;
|
|
2036
2036
|
follow(agent.name, logFile(agent.name), sizeOf(logFile(agent.name)));
|
|
2037
2037
|
entry.watch = setInterval(() => {
|
|
@@ -2042,14 +2042,14 @@ function createSupervisor({
|
|
|
2042
2042
|
}
|
|
2043
2043
|
}, WATCH_MS);
|
|
2044
2044
|
entry.watch.unref();
|
|
2045
|
-
log2(`[
|
|
2045
|
+
log2(`[runner] adopted ${agent.name} (pid ${record.pid}, port ${agent.port})`);
|
|
2046
2046
|
changed(entry);
|
|
2047
2047
|
onUp(agent, { startedAt: entry.startedAt, adopted: true, recovered: false });
|
|
2048
2048
|
return { adopted: true };
|
|
2049
2049
|
}
|
|
2050
2050
|
function carry(agent, record, tree = record.tree ?? []) {
|
|
2051
2051
|
const entry = entryFor(agent);
|
|
2052
|
-
Object.assign(entry, { pid: record.pid, pgid: record.pgid, startedAt: record.startedAt, restarts: record.restarts ?? 0, status: record.status ?? "up", adopted: true, child: null, stopping: false, tree, fingerprint: record.fingerprint
|
|
2052
|
+
Object.assign(entry, { pid: record.pid, pgid: record.pgid, startedAt: record.startedAt, restarts: record.restarts ?? 0, status: record.status ?? "up", adopted: true, child: null, stopping: false, tree, fingerprint: record.fingerprint });
|
|
2053
2053
|
entry.gen++;
|
|
2054
2054
|
return { adopted: true, carried: true };
|
|
2055
2055
|
}
|
|
@@ -2075,7 +2075,7 @@ function createSupervisor({
|
|
|
2075
2075
|
return spawnAgent(entry, { recovered });
|
|
2076
2076
|
}
|
|
2077
2077
|
const error = `port ${port} is held by another process (${holders.map((pid) => describe(pid, rows)).join("; ")}), not by ${name}'s server; not touching it. ${name} starts once the port is free.`;
|
|
2078
|
-
if (entry.error !== error || entry.status !== "blocked") warn(`[
|
|
2078
|
+
if (entry.error !== error || entry.status !== "blocked") warn(`[runner] ! ${error}`);
|
|
2079
2079
|
Object.assign(entry, { status: "blocked", error, pid: null, pgid: null });
|
|
2080
2080
|
entry.timer = setTimeout(() => spawnAgent(entry, { recovered }), PORT_RECHECK_MS);
|
|
2081
2081
|
entry.timer.unref();
|
|
@@ -2084,7 +2084,7 @@ function createSupervisor({
|
|
|
2084
2084
|
mkdirSync2(logDir, { recursive: true });
|
|
2085
2085
|
const file = logFile(name);
|
|
2086
2086
|
if (sizeOf(file) > LOG_ROTATE_BYTES) renameSync(file, `${file}.1`);
|
|
2087
|
-
const fd =
|
|
2087
|
+
const fd = openSync2(file, "a");
|
|
2088
2088
|
writeSync(fd, `
|
|
2089
2089
|
--- ${(/* @__PURE__ */ new Date()).toISOString()} hub: starting ${name} on port ${port}
|
|
2090
2090
|
`);
|
|
@@ -2094,7 +2094,7 @@ function createSupervisor({
|
|
|
2094
2094
|
try {
|
|
2095
2095
|
fp = fingerprintFor(agent, spawnEnv);
|
|
2096
2096
|
} catch (error) {
|
|
2097
|
-
warn(`[
|
|
2097
|
+
warn(`[runner] ! ${name}: couldn't fingerprint its inputs (${error.message}); the next runner respawns it instead of adopting it`);
|
|
2098
2098
|
}
|
|
2099
2099
|
let child;
|
|
2100
2100
|
try {
|
|
@@ -2102,17 +2102,17 @@ function createSupervisor({
|
|
|
2102
2102
|
cwd: root,
|
|
2103
2103
|
env: spawnEnv,
|
|
2104
2104
|
detached: true,
|
|
2105
|
-
// own process group; survives the
|
|
2105
|
+
// own process group; survives the runner
|
|
2106
2106
|
stdio: ["ignore", fd, fd]
|
|
2107
2107
|
});
|
|
2108
2108
|
} finally {
|
|
2109
|
-
|
|
2109
|
+
closeSync2(fd);
|
|
2110
2110
|
}
|
|
2111
2111
|
const gen = ++entry.gen;
|
|
2112
|
-
Object.assign(entry, { child, pid: child.pid ?? null, pgid: child.pid ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "starting", adopted: false, stopping: false, error: void 0, nextRestartAt: void 0, tree: [], slowWarned: false, fingerprint: fp ?? void 0,
|
|
2112
|
+
Object.assign(entry, { child, pid: child.pid ?? null, pgid: child.pid ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "starting", adopted: false, stopping: false, error: void 0, nextRestartAt: void 0, tree: [], slowWarned: false, fingerprint: fp ?? void 0, rssKb: void 0 });
|
|
2113
2113
|
follow(name, file, sizeOf(file));
|
|
2114
2114
|
child.once("error", (error) => {
|
|
2115
|
-
warn(`[
|
|
2115
|
+
warn(`[runner] ! couldn't start ${name}: ${error.message}`);
|
|
2116
2116
|
exited(entry, gen, { code: null, signal: null, error: error.message });
|
|
2117
2117
|
});
|
|
2118
2118
|
child.once("exit", (code, signal) => exited(entry, gen, { code, signal }));
|
|
@@ -2137,15 +2137,14 @@ function createSupervisor({
|
|
|
2137
2137
|
entry.status = "up";
|
|
2138
2138
|
changed(entry);
|
|
2139
2139
|
onUp(entry.agent, { startedAt: entry.startedAt, adopted: false, recovered });
|
|
2140
|
-
if (recovered) onRecovered(entry.agent);
|
|
2141
2140
|
return;
|
|
2142
2141
|
}
|
|
2143
2142
|
}
|
|
2144
2143
|
if (Date.now() - started2 > HEALTH_TIMEOUT_MS && !entry.slowWarned) {
|
|
2145
|
-
warn(`[
|
|
2144
|
+
warn(`[runner] ! ${entry.name} isn't healthy ${HEALTH_TIMEOUT_MS / 1e3}s after starting; see ${logFile(entry.name)}`);
|
|
2146
2145
|
entry.slowWarned = true;
|
|
2147
2146
|
}
|
|
2148
|
-
await
|
|
2147
|
+
await sleep2(500);
|
|
2149
2148
|
}
|
|
2150
2149
|
}
|
|
2151
2150
|
function exited(entry, gen, { code, signal, error }) {
|
|
@@ -2171,25 +2170,25 @@ function createSupervisor({
|
|
|
2171
2170
|
if (entry.crashes.length >= CRASH_LIMIT) {
|
|
2172
2171
|
entry.status = "crashed";
|
|
2173
2172
|
entry.error = `exited ${entry.crashes.length} times within ${Math.round(CRASH_WINDOW_MS / 6e4)} min (last: ${how}); not restarting until restart_agent. Log: ${logFile(entry.name)}`;
|
|
2174
|
-
warn(`[
|
|
2173
|
+
warn(`[runner] ! ${entry.name} crashed: ${entry.error}`);
|
|
2175
2174
|
return changed(entry);
|
|
2176
2175
|
}
|
|
2177
|
-
const
|
|
2176
|
+
const delay3 = Math.min(BACKOFF_BASE_MS * 2 ** (entry.crashes.length - 1), BACKOFF_MAX_MS);
|
|
2178
2177
|
entry.status = "down";
|
|
2179
|
-
entry.nextRestartAt = new Date(now +
|
|
2180
|
-
warn(`[
|
|
2178
|
+
entry.nextRestartAt = new Date(now + delay3).toISOString();
|
|
2179
|
+
warn(`[runner] ${entry.name} exited unexpectedly (${how}); restarting in ${delay3 / 1e3}s`);
|
|
2181
2180
|
entry.timer = setTimeout(async () => {
|
|
2182
2181
|
await untilTreeGone(pgid, tree, KILL_GRACE_MS);
|
|
2183
2182
|
if (entry.status !== "down" || entry.stopping) return;
|
|
2184
2183
|
entry.restarts++;
|
|
2185
2184
|
changed(entry);
|
|
2186
2185
|
void spawnAgent(entry, { recovered: true });
|
|
2187
|
-
},
|
|
2186
|
+
}, delay3);
|
|
2188
2187
|
entry.timer.unref();
|
|
2189
2188
|
changed(entry);
|
|
2190
2189
|
}
|
|
2191
2190
|
return {
|
|
2192
|
-
// Startup: adopt what's still running from a previous
|
|
2191
|
+
// Startup: adopt what's still running from a previous runner, start the rest.
|
|
2193
2192
|
startAll(agents) {
|
|
2194
2193
|
starting = (async () => {
|
|
2195
2194
|
const records = readRecords();
|
|
@@ -2207,7 +2206,7 @@ function createSupervisor({
|
|
|
2207
2206
|
const result = results.get(agent.name);
|
|
2208
2207
|
if (result.adopted) return;
|
|
2209
2208
|
if (closed && !result.superseded) {
|
|
2210
|
-
if (ourTree(record, table()).tree.length) carry(agent, record);
|
|
2209
|
+
if (ourTree(record, table(), signature(record.agent, record.port)).tree.length) carry(agent, record);
|
|
2211
2210
|
return;
|
|
2212
2211
|
}
|
|
2213
2212
|
await killStale(record, result.why ?? "it isn't serving");
|
|
@@ -2251,7 +2250,7 @@ function createSupervisor({
|
|
|
2251
2250
|
}
|
|
2252
2251
|
entry.status = "stopping";
|
|
2253
2252
|
changed(entry);
|
|
2254
|
-
const gone = await stopTree(pgid, tree, agent.name);
|
|
2253
|
+
const gone = await stopTree(pgid, tree, { name: agent.name, timeoutMs: stopTimeoutMs, warn });
|
|
2255
2254
|
if (gone) {
|
|
2256
2255
|
if (entry.pgid === pgid) {
|
|
2257
2256
|
entry.gen++;
|
|
@@ -2273,42 +2272,6 @@ function createSupervisor({
|
|
|
2273
2272
|
pump();
|
|
2274
2273
|
persist();
|
|
2275
2274
|
},
|
|
2276
|
-
// Exit without stopping any bot (a `hub restart` that keeps them): nothing
|
|
2277
|
-
// new starts, pending (re)starts are cancelled, each tree is re-read and
|
|
2278
|
-
// saved, and no signal is sent. The next registry vets every record
|
|
2279
|
-
// (vetAdoption) and adopts what still matches. Returns the bots left running.
|
|
2280
|
-
async detach() {
|
|
2281
|
-
closed = true;
|
|
2282
|
-
await starting.catch(() => {
|
|
2283
|
-
});
|
|
2284
|
-
clearInterval(pumpTimer);
|
|
2285
|
-
clearInterval(treeTimer);
|
|
2286
|
-
const rows = table();
|
|
2287
|
-
for (const e of entries.values()) {
|
|
2288
|
-
clearTimeout(e.timer);
|
|
2289
|
-
e.timer = void 0;
|
|
2290
|
-
e.claim = (e.claim ?? 0) + 1;
|
|
2291
|
-
clearInterval(e.watch);
|
|
2292
|
-
e.gen++;
|
|
2293
|
-
if (e.pid) refreshTree(e, rows);
|
|
2294
|
-
}
|
|
2295
|
-
pump();
|
|
2296
|
-
persist();
|
|
2297
|
-
return [...entries.values()].filter((e) => e.pid).map((e) => ({ name: e.name, pid: e.pid }));
|
|
2298
|
-
},
|
|
2299
|
-
// Drop a removed bot's record (after its server is confirmed down).
|
|
2300
|
-
forget(name) {
|
|
2301
|
-
const entry = entries.get(name);
|
|
2302
|
-
if (entry) {
|
|
2303
|
-
clearTimeout(entry.timer);
|
|
2304
|
-
clearInterval(entry.watch);
|
|
2305
|
-
entry.gen++;
|
|
2306
|
-
entry.claim = (entry.claim ?? 0) + 1;
|
|
2307
|
-
}
|
|
2308
|
-
entries.delete(name);
|
|
2309
|
-
tails.delete(name);
|
|
2310
|
-
persist();
|
|
2311
|
-
},
|
|
2312
2275
|
// Resolves true once the bot is up (spawned by us and healthy); false
|
|
2313
2276
|
// once it can't be: it crashed, left the roster, or this supervisor closed.
|
|
2314
2277
|
async waitUp(agent, timeoutMs) {
|
|
@@ -2318,21 +2281,10 @@ function createSupervisor({
|
|
|
2318
2281
|
if (closed || !isCurrent(agent)) return false;
|
|
2319
2282
|
if (entry?.agent === agent && entry.status === "up") return true;
|
|
2320
2283
|
if (entry?.status === "crashed") return false;
|
|
2321
|
-
await
|
|
2284
|
+
await sleep2(250);
|
|
2322
2285
|
}
|
|
2323
2286
|
return false;
|
|
2324
2287
|
},
|
|
2325
|
-
// A running bot's memory as of the last tree refresh (eve dev + its
|
|
2326
|
-
// server) and its uptime (an adopted one keeps its start). Kept out of
|
|
2327
|
-
// status(): that's in every state snapshot, and these change constantly.
|
|
2328
|
-
usage(name) {
|
|
2329
|
-
const e = entries.get(name);
|
|
2330
|
-
if (!e?.pid || e.status !== "up") return void 0;
|
|
2331
|
-
return {
|
|
2332
|
-
...e.rssKb ? { rssMb: Math.round(e.rssKb / 1024) } : {},
|
|
2333
|
-
...e.startedAt ? { uptimeS: Math.max(0, Math.round((Date.now() - Date.parse(e.startedAt)) / 1e3)) } : {}
|
|
2334
|
-
};
|
|
2335
|
-
},
|
|
2336
2288
|
status(name) {
|
|
2337
2289
|
const e = entries.get(name);
|
|
2338
2290
|
if (!e) return null;
|
|
@@ -2575,7 +2527,7 @@ async function sendToBot(fetchFn, eve, d, context, loadImage) {
|
|
|
2575
2527
|
}
|
|
2576
2528
|
|
|
2577
2529
|
// ../../hosted-hub/runner/hub-runner.ts
|
|
2578
|
-
import { setTimeout as
|
|
2530
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
2579
2531
|
|
|
2580
2532
|
// ../../hosted-hub/runner/status.ts
|
|
2581
2533
|
import { renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -2606,7 +2558,7 @@ var status = (state, extra = {}) => {
|
|
|
2606
2558
|
console.log(JSON.stringify({ state, ...extra }));
|
|
2607
2559
|
};
|
|
2608
2560
|
|
|
2609
|
-
// ../../lib/v26-facts.mjs
|
|
2561
|
+
// ../../hosted-hub/lib/v26-facts.mjs
|
|
2610
2562
|
function isV26Boundary(event) {
|
|
2611
2563
|
const type = event?.type;
|
|
2612
2564
|
return type === "session.waiting" || type === "session.completed" || type === "session.failed";
|
|
@@ -2830,7 +2782,8 @@ function fact(type, data, source) {
|
|
|
2830
2782
|
}
|
|
2831
2783
|
var NO_RUN_SCOPE = /* @__PURE__ */ new Set(["context.started", "context.settled", "interaction.opened", "interaction.settled", "response.submitted", "response.settled"]);
|
|
2832
2784
|
function content(data, kind, phase, value) {
|
|
2833
|
-
|
|
2785
|
+
const run4 = typeof data.stepIndex === "number" ? { runId: runIdOf(data) } : {};
|
|
2786
|
+
return fact("content.completed", { partId: partIdOf(data, kind), ...run4, kind, phase, value }, data);
|
|
2834
2787
|
}
|
|
2835
2788
|
function turnSettled(data, settled) {
|
|
2836
2789
|
const turnId = str(data.turnId);
|
|
@@ -2940,7 +2893,7 @@ var str = (value) => typeof value === "string" ? value : void 0;
|
|
|
2940
2893
|
var num2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2941
2894
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2942
2895
|
|
|
2943
|
-
// ../../lib/session-stream.mjs
|
|
2896
|
+
// ../../hosted-hub/lib/session-stream.mjs
|
|
2944
2897
|
async function* readSessionLines(body, startIndex) {
|
|
2945
2898
|
let line = startIndex;
|
|
2946
2899
|
let lastAt = null;
|
|
@@ -3023,9 +2976,19 @@ async function* streamLines(eve, sessionId, o) {
|
|
|
3023
2976
|
}
|
|
3024
2977
|
const idle = new AbortController(), signal = o.signal ? AbortSignal.any([o.signal, idle.signal]) : idle.signal;
|
|
3025
2978
|
let timer = setTimeout(() => idle.abort(), idleMs);
|
|
2979
|
+
const opened = Date.now();
|
|
2980
|
+
let firstByte = false, lineAt = opened;
|
|
3026
2981
|
const quiet = () => {
|
|
3027
2982
|
clearTimeout(timer);
|
|
3028
2983
|
timer = setTimeout(() => idle.abort(), idleMs);
|
|
2984
|
+
if (!firstByte) {
|
|
2985
|
+
firstByte = true;
|
|
2986
|
+
console.log(JSON.stringify({ event: "hub.stream_first_byte", sessionId, startIndex: next, ms: Date.now() - opened }));
|
|
2987
|
+
}
|
|
2988
|
+
if (Date.now() - lineAt >= idleMs) {
|
|
2989
|
+
console.log(JSON.stringify({ event: "hub.stream_no_lines", sessionId, next, ms: Date.now() - lineAt }));
|
|
2990
|
+
lineAt = Date.now();
|
|
2991
|
+
}
|
|
3029
2992
|
};
|
|
3030
2993
|
let renewed = false;
|
|
3031
2994
|
try {
|
|
@@ -3053,6 +3016,7 @@ async function* streamLines(eve, sessionId, o) {
|
|
|
3053
3016
|
break;
|
|
3054
3017
|
}
|
|
3055
3018
|
yield record;
|
|
3019
|
+
lineAt = Date.now();
|
|
3056
3020
|
next = record.line + 1;
|
|
3057
3021
|
reopens = o.reopens ?? 3;
|
|
3058
3022
|
}
|
|
@@ -3192,7 +3156,7 @@ async function downloadWorkspace(res, dir2) {
|
|
|
3192
3156
|
}
|
|
3193
3157
|
|
|
3194
3158
|
// ../../hosted-hub/runner/bot-state.ts
|
|
3195
|
-
import { closeSync as
|
|
3159
|
+
import { closeSync as closeSync3, openSync as openSync3, readSync as readSync2, statSync as statSync3 } from "node:fs";
|
|
3196
3160
|
var MARKER = "hub: starting";
|
|
3197
3161
|
var WINDOW = 16 * 1024;
|
|
3198
3162
|
var LINES = 5;
|
|
@@ -3200,13 +3164,13 @@ var CHARS = 500;
|
|
|
3200
3164
|
function latestRun(file) {
|
|
3201
3165
|
let text;
|
|
3202
3166
|
try {
|
|
3203
|
-
const size =
|
|
3167
|
+
const size = statSync3(file).size, fd = openSync3(file, "r");
|
|
3204
3168
|
try {
|
|
3205
3169
|
const buf = Buffer.alloc(Math.min(size, WINDOW));
|
|
3206
3170
|
readSync2(fd, buf, 0, buf.length, size - buf.length);
|
|
3207
3171
|
text = buf.toString("utf8");
|
|
3208
3172
|
} finally {
|
|
3209
|
-
|
|
3173
|
+
closeSync3(fd);
|
|
3210
3174
|
}
|
|
3211
3175
|
} catch {
|
|
3212
3176
|
return void 0;
|
|
@@ -3363,7 +3327,7 @@ async function diskUsage(path) {
|
|
|
3363
3327
|
|
|
3364
3328
|
// ../../workspaces/snapshot.mjs
|
|
3365
3329
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
3366
|
-
import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as
|
|
3330
|
+
import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync4 } from "node:fs";
|
|
3367
3331
|
import { copyFile, mkdir, rm } from "node:fs/promises";
|
|
3368
3332
|
import { tmpdir } from "node:os";
|
|
3369
3333
|
import { dirname as dirname2, join as join6, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
@@ -3471,7 +3435,7 @@ async function saveCheckout(info, { threadId, name, snapshotRoot, root, label =
|
|
|
3471
3435
|
const target = existsSync5(bundle) ? bundle.replace(/\.bundle$/, `-${sha2.slice(0, 8)}.bundle`) : bundle;
|
|
3472
3436
|
await git(checkout, ["bundle", "create", "-q", target, tmpRef, "--branches", "--not", "--remotes"]);
|
|
3473
3437
|
await git(checkout, ["bundle", "verify", "-q", target]);
|
|
3474
|
-
return { checkout, kind: "bundle", bundle: target, bytes:
|
|
3438
|
+
return { checkout, kind: "bundle", bundle: target, bytes: statSync4(target).size, sha: sha2, dirty: info.dirty, unsaved: info.unsaved };
|
|
3475
3439
|
} finally {
|
|
3476
3440
|
await git(checkout, ["update-ref", "-d", tmpRef], { allowFail: true });
|
|
3477
3441
|
}
|
|
@@ -3938,11 +3902,11 @@ function fileRecords(dir2) {
|
|
|
3938
3902
|
}
|
|
3939
3903
|
|
|
3940
3904
|
// ../../workspaces/bare-guard.mjs
|
|
3941
|
-
import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as
|
|
3905
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync5 } from "node:fs";
|
|
3942
3906
|
import { join as join9 } from "node:path";
|
|
3943
3907
|
async function repairBareFlag(repo) {
|
|
3944
3908
|
const dotgit = join9(repo, ".git");
|
|
3945
|
-
if (!existsSync7(dotgit) || !
|
|
3909
|
+
if (!existsSync7(dotgit) || !statSync5(dotgit).isDirectory()) return false;
|
|
3946
3910
|
const file = join9(dotgit, "config");
|
|
3947
3911
|
try {
|
|
3948
3912
|
const text = readFileSync6(file, "utf8");
|
|
@@ -4547,6 +4511,38 @@ function modelCredential(o) {
|
|
|
4547
4511
|
} };
|
|
4548
4512
|
}
|
|
4549
4513
|
|
|
4514
|
+
// ../../hosted-hub/runner/held.ts
|
|
4515
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4516
|
+
function heldReports(send, failed) {
|
|
4517
|
+
const told = /* @__PURE__ */ new Map();
|
|
4518
|
+
const now = (bot) => told.has(bot) ? told.get(bot) : null;
|
|
4519
|
+
let sending = Promise.resolve();
|
|
4520
|
+
const tell = (bot, reason) => sending = sending.catch(() => {
|
|
4521
|
+
}).then(async () => {
|
|
4522
|
+
if (now(bot) === reason) return;
|
|
4523
|
+
told.set(bot, void 0);
|
|
4524
|
+
await send({ bot, reason });
|
|
4525
|
+
if (reason) told.set(bot, reason);
|
|
4526
|
+
else told.delete(bot);
|
|
4527
|
+
});
|
|
4528
|
+
return (waiting, why) => {
|
|
4529
|
+
for (const bot of /* @__PURE__ */ new Set([...waiting, ...told.keys()])) {
|
|
4530
|
+
const reason = why(bot);
|
|
4531
|
+
if (now(bot) !== reason) tell(bot, reason).catch(failed);
|
|
4532
|
+
}
|
|
4533
|
+
};
|
|
4534
|
+
}
|
|
4535
|
+
async function stopWithin(stop, ms, signal) {
|
|
4536
|
+
const stopped = new AbortController();
|
|
4537
|
+
await Promise.race([
|
|
4538
|
+
stop.finally(() => stopped.abort()),
|
|
4539
|
+
delay(ms, void 0, { signal: AbortSignal.any([signal, stopped.signal]) }).then(() => {
|
|
4540
|
+
throw new Error(`it did not stop in ${ms / 1e3}s`);
|
|
4541
|
+
}, () => {
|
|
4542
|
+
})
|
|
4543
|
+
]);
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4550
4546
|
// ../../hosted-hub/runner/hub-runner.ts
|
|
4551
4547
|
var realBots = { stage: stageLocal, supervise: createSupervisor };
|
|
4552
4548
|
var HeldElsewhere = class extends Error {
|
|
@@ -4679,7 +4675,7 @@ async function run3(config, bots = realBots) {
|
|
|
4679
4675
|
const open = work.sessions.filter((s) => s.bot === name);
|
|
4680
4676
|
for (const s of open) {
|
|
4681
4677
|
const res = await cancelTurn(net, eve(name, work.human), s.eveSessionId);
|
|
4682
|
-
if (!res.ok) throw new Error(`cancel ${s.sessionId}: HTTP ${res.status}`);
|
|
4678
|
+
if (!res.ok && !sessionEnded(res.status, await res.text())) throw new Error(`cancel ${s.sessionId}: HTTP ${res.status}`);
|
|
4683
4679
|
}
|
|
4684
4680
|
for (const s of open) await upload(s, name, work.human, s.foldedIndex + 1, { toTail: true });
|
|
4685
4681
|
const sessionIds = open.map((s) => s.sessionId);
|
|
@@ -4824,7 +4820,7 @@ async function run3(config, bots = realBots) {
|
|
|
4824
4820
|
if (restarted.get(name)?.requestedAt !== requestedAt) {
|
|
4825
4821
|
if (agent) {
|
|
4826
4822
|
try {
|
|
4827
|
-
await supervisor.stop(agent);
|
|
4823
|
+
await stopWithin(supervisor.stop(agent), config.stopTimeoutMs ?? 6e4, shutdown.signal);
|
|
4828
4824
|
shutdown.signal.throwIfAborted();
|
|
4829
4825
|
supervisor.start(agent);
|
|
4830
4826
|
if (!await supervisor.waitUp(agent, 12e4)) throw new Error("it did not come back up");
|
|
@@ -4869,7 +4865,7 @@ async function run3(config, bots = realBots) {
|
|
|
4869
4865
|
}
|
|
4870
4866
|
}
|
|
4871
4867
|
let syncing = null, again = false, drained = null;
|
|
4872
|
-
const unsettled = /* @__PURE__ */ new Set();
|
|
4868
|
+
const unsettled = /* @__PURE__ */ new Set(), held = heldReports((body) => hubJson("/api/local-runner/setup", { method: "POST", body: JSON.stringify(body), signal: AbortSignal.timeout(ACK_MS) }), (e) => failed("held report", e));
|
|
4873
4869
|
async function sync() {
|
|
4874
4870
|
const work = await hubJson("/api/local-runner/work");
|
|
4875
4871
|
if (drained && !work.sessions.length && !work.children.length && !work.deliveries.some(([d]) => unsettled.has(d.id))) {
|
|
@@ -4897,6 +4893,7 @@ async function run3(config, bots = realBots) {
|
|
|
4897
4893
|
for (const [id, abort] of following) if (![...sessions, ...work.children].some((s) => s.sessionId === id)) abort.abort();
|
|
4898
4894
|
restart(work);
|
|
4899
4895
|
compact2(work);
|
|
4896
|
+
held(work.deliveries.map(([d]) => d.bot), (bot) => drained ? "draining" : restarting.has(bot) ? "restarting" : !running.has(bot) ? "stopped" : null);
|
|
4900
4897
|
await Promise.all([
|
|
4901
4898
|
...sessions.filter((s) => s.cancel.length).map((s) => cancel(s, work.human).catch((e) => failed("cancel", e))),
|
|
4902
4899
|
...work.deliveries.filter(([d]) => (!drained || unsettled.has(d.id)) && running.has(d.bot) && !restarting.has(d.bot) && !dead.has(d.bot) && !retired.has(d.sessionId)).map((q) => deliver(q, work).catch((e) => failed("delivery", e)))
|
|
@@ -4953,7 +4950,7 @@ async function run3(config, bots = realBots) {
|
|
|
4953
4950
|
} finally {
|
|
4954
4951
|
clearTimeout(watchdog);
|
|
4955
4952
|
}
|
|
4956
|
-
await
|
|
4953
|
+
await delay2(RETRY_MS, void 0, { signal: shutdown.signal }).catch(() => {
|
|
4957
4954
|
});
|
|
4958
4955
|
}
|
|
4959
4956
|
})();
|