hub-runner 0.2.3 → 0.2.5
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 +244 -266
- 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
|
}
|