@scp-dev/core 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +15 -0
- package/dist/index.js +57 -12
- package/package.json +1 -1
- package/src/core.ts +69 -12
package/dist/index.d.ts
CHANGED
|
@@ -1109,6 +1109,10 @@ interface EndpointRow {
|
|
|
1109
1109
|
linked: number | null;
|
|
1110
1110
|
/** how the session that holds the endpoint wakes it: a {@link Wake} other than none, or null */
|
|
1111
1111
|
wake: string | null;
|
|
1112
|
+
/** the live connections that set the wake */
|
|
1113
|
+
wakers: number;
|
|
1114
|
+
/** when the last hook connection that set the wake closed */
|
|
1115
|
+
waker_left: number | null;
|
|
1112
1116
|
}
|
|
1113
1117
|
interface MessageRow {
|
|
1114
1118
|
id: string;
|
|
@@ -1171,6 +1175,8 @@ declare class RelayCore {
|
|
|
1171
1175
|
constructor(sql: Sql, options: CoreOptions);
|
|
1172
1176
|
/** a `machine` label naming this host by its hostname, as clients default it, names it by its machine name when one is set */
|
|
1173
1177
|
private machineLabel;
|
|
1178
|
+
/** this host's own agent rows labelled with another machine (its name before a rename, or its hostname) take its machine name */
|
|
1179
|
+
relabelMachine(): number;
|
|
1174
1180
|
migrate(): void;
|
|
1175
1181
|
/** every stored list that is exactly `tree:self`, the tree preset's old list, narrows once to the preset's list */
|
|
1176
1182
|
private migrateTreePreset;
|
|
@@ -1256,6 +1262,8 @@ declare class RelayCore {
|
|
|
1256
1262
|
* whether a claim placed under `parent` restarts `row`: it lands where `row` sits (under `row`'s parent, or a root with no placement), or on
|
|
1257
1263
|
* `row` itself from `row`'s own process; never under `row` or elsewhere in its tree
|
|
1258
1264
|
*/
|
|
1265
|
+
/** a local row held only by connections whose harness process exited, as a shim that outlived its harness leaves it */
|
|
1266
|
+
private outlived;
|
|
1259
1267
|
private restarts;
|
|
1260
1268
|
/**
|
|
1261
1269
|
* whether the claim re-claims `row` with the grant `row` spent: from `row`'s own process or one of its ancestors, or with no lineage from
|
|
@@ -1344,6 +1352,13 @@ declare class RelayCore {
|
|
|
1344
1352
|
transient?: boolean;
|
|
1345
1353
|
process?: string;
|
|
1346
1354
|
}): boolean;
|
|
1355
|
+
/**
|
|
1356
|
+
* a connection that set the endpoint's wake starts (`live`) or stops counting as its waker. A hook's (`grace`) still counts for
|
|
1357
|
+
* LIMITS.restartGraceS after it closes, while its harness starts the next one
|
|
1358
|
+
*/
|
|
1359
|
+
waker(endpoint: string, live: boolean, options?: {
|
|
1360
|
+
grace?: boolean;
|
|
1361
|
+
}): void;
|
|
1347
1362
|
/**
|
|
1348
1363
|
* a title for the caller's in-process subagent `id` from the description its parent gave it; the subagent's endpoint id when it changed one.
|
|
1349
1364
|
* It fills only a title nobody set, as an inferred one, and waits for the subagent's claim when it has no endpoint yet
|
package/dist/index.js
CHANGED
|
@@ -1306,6 +1306,19 @@ var RelayCore = class {
|
|
|
1306
1306
|
if (this.namedMachine !== null && labels.machine !== void 0 && labels.machine === this.hostname) labels.machine = this.namedMachine;
|
|
1307
1307
|
return labels;
|
|
1308
1308
|
}
|
|
1309
|
+
/** this host's own agent rows labelled with another machine (its name before a rename, or its hostname) take its machine name */
|
|
1310
|
+
relabelMachine() {
|
|
1311
|
+
const machine = this.namedMachine;
|
|
1312
|
+
if (machine === null) return 0;
|
|
1313
|
+
let changed = 0;
|
|
1314
|
+
for (const row of this.sql.all(`SELECT * FROM endpoints WHERE kind = 'agent' AND labels LIKE '%"machine"%'`)) {
|
|
1315
|
+
const labels = json(row.labels, {});
|
|
1316
|
+
if (labels.machine === void 0 || labels.machine === machine || !this.localRow(row)) continue;
|
|
1317
|
+
this.sql.all(`UPDATE endpoints SET labels = ? WHERE id = ?`, JSON.stringify({ ...labels, machine }), row.id);
|
|
1318
|
+
changed++;
|
|
1319
|
+
}
|
|
1320
|
+
return changed;
|
|
1321
|
+
}
|
|
1309
1322
|
migrate() {
|
|
1310
1323
|
for (const statement of MIGRATION) this.sql.all(statement);
|
|
1311
1324
|
const columns = this.sql.all(`PRAGMA table_info(idempotency)`).map((c) => c.name);
|
|
@@ -1344,7 +1357,9 @@ var RelayCore = class {
|
|
|
1344
1357
|
["tldr_turn", "INTEGER"],
|
|
1345
1358
|
["named", "INTEGER"],
|
|
1346
1359
|
["linked", "INTEGER"],
|
|
1347
|
-
["wake", "TEXT"]
|
|
1360
|
+
["wake", "TEXT"],
|
|
1361
|
+
["wakers", "INTEGER NOT NULL DEFAULT 0"],
|
|
1362
|
+
["waker_left", "INTEGER"]
|
|
1348
1363
|
]) {
|
|
1349
1364
|
if (!endpointColumns.includes(column)) this.sql.all(`ALTER TABLE endpoints ADD COLUMN ${column} ${type}`);
|
|
1350
1365
|
}
|
|
@@ -1621,7 +1636,7 @@ var RelayCore = class {
|
|
|
1621
1636
|
if (row && presenceOf(row, now) === "offline") {
|
|
1622
1637
|
this.sql.all(`UPDATE endpoints SET session = NULL WHERE id = ?`, row.id);
|
|
1623
1638
|
row = void 0;
|
|
1624
|
-
} else if (row && !transient && !sameProcess(row, fields.process)) {
|
|
1639
|
+
} else if (row && !transient && !sameProcess(row, fields.process) && !(row.handle === wanted && reclaims(row, fields.process))) {
|
|
1625
1640
|
row = void 0;
|
|
1626
1641
|
session = null;
|
|
1627
1642
|
} else if (row && !transient && row.token_hash === null && row.named === null && row.handle !== wanted && !row.handle.startsWith(`${wanted.slice(0, 58)}-`)) {
|
|
@@ -1663,7 +1678,7 @@ var RelayCore = class {
|
|
|
1663
1678
|
this.sql.all(`INSERT INTO endpoints (id, handle, kind, harness, labels, created_at) VALUES (?, ?, 'agent', ?, '{}', ?)`, id, handle, harness, now);
|
|
1664
1679
|
row = this.byId(id);
|
|
1665
1680
|
created = true;
|
|
1666
|
-
} else if (held.kind === "agent" && held.connections === 0 && presenceOf(held, now) === "offline" && harnessFits(held.harness, harness) && held.id !== scope.subagent?.root && !(scope.subagent && held.session?.startsWith("sub:") && held.session !== fields.session)) {
|
|
1681
|
+
} else if (held.kind === "agent" && (held.connections === 0 && presenceOf(held, now) === "offline" || !transient && this.outlived(held)) && harnessFits(held.harness, harness) && held.id !== scope.subagent?.root && !(scope.subagent && held.session?.startsWith("sub:") && held.session !== fields.session)) {
|
|
1667
1682
|
own = transient && held.process !== null && fields.process !== null && sameHarness(held.process, fields.process);
|
|
1668
1683
|
const ended = held.hook_ended !== null || (held.last_seen ?? held.created_at) < now - LIMITS.restartGraceS * 1e3;
|
|
1669
1684
|
if ((!transient || own || ended) && (this.restarts(held, this.placement(scope, now).parent, fields.process) || this.redeems(held, scope, { harness, ...fields })))
|
|
@@ -1872,6 +1887,11 @@ var RelayCore = class {
|
|
|
1872
1887
|
* whether a claim placed under `parent` restarts `row`: it lands where `row` sits (under `row`'s parent, or a root with no placement), or on
|
|
1873
1888
|
* `row` itself from `row`'s own process; never under `row` or elsewhere in its tree
|
|
1874
1889
|
*/
|
|
1890
|
+
/** a local row held only by connections whose harness process exited, as a shim that outlived its harness leaves it */
|
|
1891
|
+
outlived(row) {
|
|
1892
|
+
const pid = Number(/^pid:(\d+):/.exec(row.process ?? "")?.[1]);
|
|
1893
|
+
return row.connections > 0 && pid > 0 && this.localRow(row) && !this.alive(pid);
|
|
1894
|
+
}
|
|
1875
1895
|
restarts(row, parent, process) {
|
|
1876
1896
|
if (parent?.id === row.id) return process !== null && row.process !== null && sameHarness(row.process, process);
|
|
1877
1897
|
return (parent?.id ?? null) === row.parent_ep || parent === null && row.linked !== null;
|
|
@@ -2036,7 +2056,7 @@ var RelayCore = class {
|
|
|
2036
2056
|
endpoint
|
|
2037
2057
|
);
|
|
2038
2058
|
const row = this.mustEndpoint(endpoint);
|
|
2039
|
-
if (!options.settle ||
|
|
2059
|
+
if (!options.settle || shownPresence(row, now) !== "offline" || row.status_state !== "working" && row.status_state !== "waiting") return false;
|
|
2040
2060
|
this.sql.all(`UPDATE endpoints SET status_state = NULL, status_note = NULL WHERE id = ?`, endpoint);
|
|
2041
2061
|
return true;
|
|
2042
2062
|
}
|
|
@@ -2051,6 +2071,7 @@ var RelayCore = class {
|
|
|
2051
2071
|
`UPDATE endpoints SET connections = 0, hook_seen = CASE WHEN kind = 'agent' AND hook_ended IS NULL THEN MAX(COALESCE(hook_seen, 0), ?) ELSE hook_seen END WHERE connections > 0`,
|
|
2052
2072
|
this.now() - (LIMITS.hookIdleS - LIMITS.restartGraceS) * 1e3
|
|
2053
2073
|
);
|
|
2074
|
+
this.sql.all(`UPDATE endpoints SET wakers = 0, waker_left = ? WHERE wakers > 0`, this.now());
|
|
2054
2075
|
}
|
|
2055
2076
|
whoami(endpoint) {
|
|
2056
2077
|
return this.view(this.mustEndpoint(endpoint));
|
|
@@ -2068,7 +2089,7 @@ var RelayCore = class {
|
|
|
2068
2089
|
const viewer = filters.viewer === void 0 ? void 0 : this.mustEndpoint(filters.viewer);
|
|
2069
2090
|
const shown = viewer ? new Set(this.visibleRows(viewer).map((row) => row.id)) : void 0;
|
|
2070
2091
|
const peeker = viewer ?? (filters.peekable === true ? this.byHandle(HUMAN_HANDLE) : void 0);
|
|
2071
|
-
return this.listed().filter((row) => filters.all || row.connections > 0 || row.kind === "human" || (row.last_seen ?? 0) >= cutoff).filter((row) => !filters.harness || row.harness === filters.harness).filter((row) => !filters.kind || row.kind === filters.kind).filter((row) => !viewer || row.id === viewer.id || this.mayMessage(viewer, row, viewer.kind === "human")).map((row) => {
|
|
2092
|
+
return this.listed().filter((row) => filters.all || row.connections > 0 || row.kind === "human" || (row.last_seen ?? 0) >= cutoff || waking(row, now)).filter((row) => !filters.harness || row.harness === filters.harness).filter((row) => !filters.kind || row.kind === filters.kind).filter((row) => !viewer || row.id === viewer.id || this.mayMessage(viewer, row, viewer.kind === "human")).map((row) => {
|
|
2072
2093
|
const view = this.view(row, shown);
|
|
2073
2094
|
return peeker && this.peekableBy(peeker, row, viewer?.kind === "human") ? { ...view, peekable: row.peek } : view;
|
|
2074
2095
|
}).filter((view) => !filters.presence || view.presence === filters.presence);
|
|
@@ -2168,6 +2189,20 @@ var RelayCore = class {
|
|
|
2168
2189
|
this.sql.all(`UPDATE endpoints SET wake = ? WHERE id = ?`, wake, endpoint);
|
|
2169
2190
|
return true;
|
|
2170
2191
|
}
|
|
2192
|
+
/**
|
|
2193
|
+
* a connection that set the endpoint's wake starts (`live`) or stops counting as its waker. A hook's (`grace`) still counts for
|
|
2194
|
+
* LIMITS.restartGraceS after it closes, while its harness starts the next one
|
|
2195
|
+
*/
|
|
2196
|
+
waker(endpoint, live, options = {}) {
|
|
2197
|
+
if (live) this.sql.all(`UPDATE endpoints SET wakers = wakers + 1 WHERE id = ?`, endpoint);
|
|
2198
|
+
else
|
|
2199
|
+
this.sql.all(
|
|
2200
|
+
`UPDATE endpoints SET wakers = MAX(wakers - 1, 0), waker_left = CASE WHEN ? THEN ? ELSE waker_left END WHERE id = ?`,
|
|
2201
|
+
options.grace ? 1 : 0,
|
|
2202
|
+
this.now(),
|
|
2203
|
+
endpoint
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2171
2206
|
/**
|
|
2172
2207
|
* a title for the caller's in-process subagent `id` from the description its parent gave it; the subagent's endpoint id when it changed one.
|
|
2173
2208
|
* It fills only a title nobody set, as an inferred one, and waits for the subagent's claim when it has no endpoint yet
|
|
@@ -2469,7 +2504,7 @@ var RelayCore = class {
|
|
|
2469
2504
|
const recipients = [...new Map(resolved.map((row) => [row.id, row])).values()];
|
|
2470
2505
|
if (params.live !== void 0 && typeof params.live !== "boolean") throw new RelayError("invalid_params", "live must be true or false");
|
|
2471
2506
|
const dark = recipients.filter(
|
|
2472
|
-
(r) => r.kind === "agent" &&
|
|
2507
|
+
(r) => r.kind === "agent" && shownPresence(r, now) === "offline" && (now - Math.max(r.last_seen ?? 0, r.hook_seen ?? 0) >= ASK_ENDED_GRACE_MS || r.session === null && r.token_hash === null && r.hook_ended !== null) && !this.unjoinedSpawn(sender, r.handle, now)
|
|
2473
2508
|
);
|
|
2474
2509
|
if (params.live === true && dark.length)
|
|
2475
2510
|
throw new RelayError("not_live", `no live session holds ${dark.map((r) => this.address(r)).join(", ")}: nothing was queued`);
|
|
@@ -2714,7 +2749,7 @@ var RelayCore = class {
|
|
|
2714
2749
|
reach(sender, recipient, offline, now) {
|
|
2715
2750
|
if (recipient.kind !== "agent" || !this.localRow(recipient)) return void 0;
|
|
2716
2751
|
const done = recipient.status_state === "done";
|
|
2717
|
-
if ((done ||
|
|
2752
|
+
if ((done || shownPresence(recipient, now) === "offline") && this.unjoinedSpawn(sender, recipient.handle, now)) return "starting";
|
|
2718
2753
|
if (done) return "ended";
|
|
2719
2754
|
return offline ? "offline" : "running";
|
|
2720
2755
|
}
|
|
@@ -3113,7 +3148,7 @@ var RelayCore = class {
|
|
|
3113
3148
|
continue;
|
|
3114
3149
|
}
|
|
3115
3150
|
const row = this.byId(grant.used_by);
|
|
3116
|
-
if (!row || row.status_state === "done" ||
|
|
3151
|
+
if (!row || row.status_state === "done" || shownPresence(row, now) === "offline" && (grant.handback !== null || !pid || !this.alive(pid))) continue;
|
|
3117
3152
|
workers.set(row.id, row);
|
|
3118
3153
|
if (grant.pushed_at !== null && grant.pushed_at > now - LIMITS.pushGapS * 1e3) paced.add(row.id);
|
|
3119
3154
|
}
|
|
@@ -3476,9 +3511,9 @@ var RelayCore = class {
|
|
|
3476
3511
|
request,
|
|
3477
3512
|
endpoint
|
|
3478
3513
|
).filter(
|
|
3479
|
-
(row) => present ? present.includes(row.id) :
|
|
3514
|
+
(row) => present ? present.includes(row.id) : shownPresence(row, now) !== "offline" || Math.max(row.last_seen ?? 0, row.hook_seen ?? 0, row.hook_ended ?? 0) >= asked.created_at
|
|
3480
3515
|
);
|
|
3481
|
-
const gone = (row) =>
|
|
3516
|
+
const gone = (row) => shownPresence(row, now) === "offline" && ((row.hook_ended ?? 0) >= asked.created_at || row.status_state === "done" || now - Math.max(row.last_seen ?? 0, row.hook_seen ?? 0) >= ASK_ENDED_GRACE_MS);
|
|
3482
3517
|
if (!rows.length || !rows.every(gone)) return null;
|
|
3483
3518
|
return rows.map((row) => ({ address: this.address(row), state: row.status_state }));
|
|
3484
3519
|
}
|
|
@@ -3573,7 +3608,7 @@ var RelayCore = class {
|
|
|
3573
3608
|
for (const row of this.sql.all(
|
|
3574
3609
|
`SELECT * FROM endpoints WHERE kind = 'agent' AND connections = 0 AND status_state IN ('working', 'waiting')`
|
|
3575
3610
|
)) {
|
|
3576
|
-
if (this.localRow(row) &&
|
|
3611
|
+
if (this.localRow(row) && shownPresence(row, now) === "offline")
|
|
3577
3612
|
this.sql.all(`UPDATE endpoints SET status_state = NULL, status_note = NULL WHERE id = ?`, row.id);
|
|
3578
3613
|
}
|
|
3579
3614
|
}
|
|
@@ -5382,7 +5417,7 @@ ${stderr}` : "";
|
|
|
5382
5417
|
}
|
|
5383
5418
|
/** with `shown`, the ids a viewer sees: a parent outside it is null */
|
|
5384
5419
|
view(row, shown) {
|
|
5385
|
-
const presence =
|
|
5420
|
+
const presence = shownPresence(row, this.now());
|
|
5386
5421
|
return {
|
|
5387
5422
|
id: row.id,
|
|
5388
5423
|
address: this.address(row),
|
|
@@ -5575,6 +5610,9 @@ function unreachable(address) {
|
|
|
5575
5610
|
var PROCESS_KEY = /^pid:\d+:[A-Za-z0-9:-]{1,64}$/;
|
|
5576
5611
|
var BARE_PID_KEY = /^pid:\d+$/;
|
|
5577
5612
|
var ANY_PROCESS_KEY = /^pid:\d+(:[A-Za-z0-9:-]{1,64})?$/;
|
|
5613
|
+
function reclaims(row, key) {
|
|
5614
|
+
return row.connections > 0 && key !== null && row.process === key && PROCESS_KEY.test(key);
|
|
5615
|
+
}
|
|
5578
5616
|
function sameProcess(row, key) {
|
|
5579
5617
|
if (row.connections > 0 || key === null || row.process === null) return false;
|
|
5580
5618
|
return row.token_hash === null ? sameHarness(row.process, key) : row.process === key && PROCESS_KEY.test(key);
|
|
@@ -5615,6 +5653,13 @@ function presenceOf(row, now) {
|
|
|
5615
5653
|
if (row.hook_seen >= now - LIMITS.hookOnlineS * 1e3) return "online";
|
|
5616
5654
|
return row.hook_seen >= now - LIMITS.hookIdleS * 1e3 ? "idle" : "offline";
|
|
5617
5655
|
}
|
|
5656
|
+
function waking(row, now) {
|
|
5657
|
+
return wakeOf(row) !== null && (row.wakers > 0 || (row.waker_left ?? 0) >= now - LIMITS.restartGraceS * 1e3);
|
|
5658
|
+
}
|
|
5659
|
+
function shownPresence(row, now) {
|
|
5660
|
+
const presence = presenceOf(row, now);
|
|
5661
|
+
return presence === "offline" && waking(row, now) ? "idle" : presence;
|
|
5662
|
+
}
|
|
5618
5663
|
function pairKey(a, b) {
|
|
5619
5664
|
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
5620
5665
|
}
|
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -267,6 +267,10 @@ export interface EndpointRow {
|
|
|
267
267
|
linked: number | null
|
|
268
268
|
/** how the session that holds the endpoint wakes it: a {@link Wake} other than none, or null */
|
|
269
269
|
wake: string | null
|
|
270
|
+
/** the live connections that set the wake */
|
|
271
|
+
wakers: number
|
|
272
|
+
/** when the last hook connection that set the wake closed */
|
|
273
|
+
waker_left: number | null
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
export interface MessageRow {
|
|
@@ -496,6 +500,20 @@ export class RelayCore {
|
|
|
496
500
|
return labels
|
|
497
501
|
}
|
|
498
502
|
|
|
503
|
+
/** this host's own agent rows labelled with another machine (its name before a rename, or its hostname) take its machine name */
|
|
504
|
+
relabelMachine(): number {
|
|
505
|
+
const machine = this.namedMachine
|
|
506
|
+
if (machine === null) return 0
|
|
507
|
+
let changed = 0
|
|
508
|
+
for (const row of this.sql.all<EndpointRow>(`SELECT * FROM endpoints WHERE kind = 'agent' AND labels LIKE '%"machine"%'`)) {
|
|
509
|
+
const labels = json<Record<string, string>>(row.labels, {})
|
|
510
|
+
if (labels.machine === undefined || labels.machine === machine || !this.localRow(row)) continue
|
|
511
|
+
this.sql.all(`UPDATE endpoints SET labels = ? WHERE id = ?`, JSON.stringify({ ...labels, machine }), row.id)
|
|
512
|
+
changed++
|
|
513
|
+
}
|
|
514
|
+
return changed
|
|
515
|
+
}
|
|
516
|
+
|
|
499
517
|
migrate(): void {
|
|
500
518
|
for (const statement of MIGRATION) this.sql.all(statement)
|
|
501
519
|
const columns = this.sql.all<{ name: string }>(`PRAGMA table_info(idempotency)`).map((c) => c.name)
|
|
@@ -535,6 +553,8 @@ export class RelayCore {
|
|
|
535
553
|
["named", "INTEGER"],
|
|
536
554
|
["linked", "INTEGER"],
|
|
537
555
|
["wake", "TEXT"],
|
|
556
|
+
["wakers", "INTEGER NOT NULL DEFAULT 0"],
|
|
557
|
+
["waker_left", "INTEGER"],
|
|
538
558
|
] as const) {
|
|
539
559
|
if (!endpointColumns.includes(column)) this.sql.all(`ALTER TABLE endpoints ADD COLUMN ${column} ${type}`)
|
|
540
560
|
}
|
|
@@ -834,7 +854,7 @@ export class RelayCore {
|
|
|
834
854
|
if (row && presenceOf(row, now) === "offline") {
|
|
835
855
|
this.sql.all(`UPDATE endpoints SET session = NULL WHERE id = ?`, row.id)
|
|
836
856
|
row = undefined
|
|
837
|
-
} else if (row && !transient && !sameProcess(row, fields.process)) {
|
|
857
|
+
} else if (row && !transient && !sameProcess(row, fields.process) && !(row.handle === wanted && reclaims(row, fields.process))) {
|
|
838
858
|
row = undefined
|
|
839
859
|
session = null
|
|
840
860
|
} else if (
|
|
@@ -884,8 +904,7 @@ export class RelayCore {
|
|
|
884
904
|
created = true
|
|
885
905
|
} else if (
|
|
886
906
|
held.kind === "agent" &&
|
|
887
|
-
held.connections === 0 &&
|
|
888
|
-
presenceOf(held, now) === "offline" &&
|
|
907
|
+
((held.connections === 0 && presenceOf(held, now) === "offline") || (!transient && this.outlived(held))) &&
|
|
889
908
|
harnessFits(held.harness, harness) &&
|
|
890
909
|
held.id !== scope.subagent?.root &&
|
|
891
910
|
!(scope.subagent && held.session?.startsWith("sub:") && held.session !== fields.session)
|
|
@@ -1124,6 +1143,12 @@ export class RelayCore {
|
|
|
1124
1143
|
* whether a claim placed under `parent` restarts `row`: it lands where `row` sits (under `row`'s parent, or a root with no placement), or on
|
|
1125
1144
|
* `row` itself from `row`'s own process; never under `row` or elsewhere in its tree
|
|
1126
1145
|
*/
|
|
1146
|
+
/** a local row held only by connections whose harness process exited, as a shim that outlived its harness leaves it */
|
|
1147
|
+
private outlived(row: EndpointRow): boolean {
|
|
1148
|
+
const pid = Number(/^pid:(\d+):/.exec(row.process ?? "")?.[1])
|
|
1149
|
+
return row.connections > 0 && pid > 0 && this.localRow(row) && !this.alive(pid)
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1127
1152
|
private restarts(row: EndpointRow, parent: EndpointRow | null, process: string | null): boolean {
|
|
1128
1153
|
if (parent?.id === row.id) return process !== null && row.process !== null && sameHarness(row.process, process)
|
|
1129
1154
|
return (parent?.id ?? null) === row.parent_ep || (parent === null && row.linked !== null)
|
|
@@ -1305,7 +1330,7 @@ export class RelayCore {
|
|
|
1305
1330
|
endpoint,
|
|
1306
1331
|
)
|
|
1307
1332
|
const row = this.mustEndpoint(endpoint)
|
|
1308
|
-
if (!options.settle ||
|
|
1333
|
+
if (!options.settle || shownPresence(row, now) !== "offline" || (row.status_state !== "working" && row.status_state !== "waiting")) return false
|
|
1309
1334
|
this.sql.all(`UPDATE endpoints SET status_state = NULL, status_note = NULL WHERE id = ?`, endpoint)
|
|
1310
1335
|
return true
|
|
1311
1336
|
}
|
|
@@ -1322,6 +1347,7 @@ export class RelayCore {
|
|
|
1322
1347
|
`UPDATE endpoints SET connections = 0, hook_seen = CASE WHEN kind = 'agent' AND hook_ended IS NULL THEN MAX(COALESCE(hook_seen, 0), ?) ELSE hook_seen END WHERE connections > 0`,
|
|
1323
1348
|
this.now() - (LIMITS.hookIdleS - LIMITS.restartGraceS) * 1000,
|
|
1324
1349
|
)
|
|
1350
|
+
this.sql.all(`UPDATE endpoints SET wakers = 0, waker_left = ? WHERE wakers > 0`, this.now())
|
|
1325
1351
|
}
|
|
1326
1352
|
|
|
1327
1353
|
whoami(endpoint: string): EndpointView {
|
|
@@ -1351,7 +1377,7 @@ export class RelayCore {
|
|
|
1351
1377
|
const shown = viewer ? new Set(this.visibleRows(viewer).map((row) => row.id)) : undefined
|
|
1352
1378
|
const peeker = viewer ?? (filters.peekable === true ? this.byHandle(HUMAN_HANDLE)! : undefined)
|
|
1353
1379
|
return this.listed()
|
|
1354
|
-
.filter((row) => filters.all || row.connections > 0 || row.kind === "human" || (row.last_seen ?? 0) >= cutoff)
|
|
1380
|
+
.filter((row) => filters.all || row.connections > 0 || row.kind === "human" || (row.last_seen ?? 0) >= cutoff || waking(row, now))
|
|
1355
1381
|
.filter((row) => !filters.harness || row.harness === filters.harness)
|
|
1356
1382
|
.filter((row) => !filters.kind || row.kind === filters.kind)
|
|
1357
1383
|
.filter((row) => !viewer || row.id === viewer.id || this.mayMessage(viewer, row, viewer.kind === "human"))
|
|
@@ -1468,6 +1494,21 @@ export class RelayCore {
|
|
|
1468
1494
|
return true
|
|
1469
1495
|
}
|
|
1470
1496
|
|
|
1497
|
+
/**
|
|
1498
|
+
* a connection that set the endpoint's wake starts (`live`) or stops counting as its waker. A hook's (`grace`) still counts for
|
|
1499
|
+
* LIMITS.restartGraceS after it closes, while its harness starts the next one
|
|
1500
|
+
*/
|
|
1501
|
+
waker(endpoint: string, live: boolean, options: { grace?: boolean } = {}): void {
|
|
1502
|
+
if (live) this.sql.all(`UPDATE endpoints SET wakers = wakers + 1 WHERE id = ?`, endpoint)
|
|
1503
|
+
else
|
|
1504
|
+
this.sql.all(
|
|
1505
|
+
`UPDATE endpoints SET wakers = MAX(wakers - 1, 0), waker_left = CASE WHEN ? THEN ? ELSE waker_left END WHERE id = ?`,
|
|
1506
|
+
options.grace ? 1 : 0,
|
|
1507
|
+
this.now(),
|
|
1508
|
+
endpoint,
|
|
1509
|
+
)
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1471
1512
|
/**
|
|
1472
1513
|
* a title for the caller's in-process subagent `id` from the description its parent gave it; the subagent's endpoint id when it changed one.
|
|
1473
1514
|
* It fills only a title nobody set, as an inferred one, and waits for the subagent's claim when it has no endpoint yet
|
|
@@ -1787,7 +1828,7 @@ export class RelayCore {
|
|
|
1787
1828
|
const dark = recipients.filter(
|
|
1788
1829
|
(r) =>
|
|
1789
1830
|
r.kind === "agent" &&
|
|
1790
|
-
|
|
1831
|
+
shownPresence(r, now) === "offline" &&
|
|
1791
1832
|
(now - Math.max(r.last_seen ?? 0, r.hook_seen ?? 0) >= ASK_ENDED_GRACE_MS || (r.session === null && r.token_hash === null && r.hook_ended !== null)) &&
|
|
1792
1833
|
!this.unjoinedSpawn(sender, r.handle, now),
|
|
1793
1834
|
)
|
|
@@ -2047,7 +2088,7 @@ export class RelayCore {
|
|
|
2047
2088
|
private reach(sender: EndpointRow, recipient: EndpointRow, offline: boolean, now: number): Reach | undefined {
|
|
2048
2089
|
if (recipient.kind !== "agent" || !this.localRow(recipient)) return undefined
|
|
2049
2090
|
const done = recipient.status_state === "done"
|
|
2050
|
-
if ((done ||
|
|
2091
|
+
if ((done || shownPresence(recipient, now) === "offline") && this.unjoinedSpawn(sender, recipient.handle, now)) return "starting"
|
|
2051
2092
|
if (done) return "ended"
|
|
2052
2093
|
return offline ? "offline" : "running"
|
|
2053
2094
|
}
|
|
@@ -2500,7 +2541,7 @@ export class RelayCore {
|
|
|
2500
2541
|
continue
|
|
2501
2542
|
}
|
|
2502
2543
|
const row = this.byId(grant.used_by)
|
|
2503
|
-
if (!row || row.status_state === "done" || (
|
|
2544
|
+
if (!row || row.status_state === "done" || (shownPresence(row, now) === "offline" && (grant.handback !== null || !pid || !this.alive(pid)))) continue
|
|
2504
2545
|
workers.set(row.id, row)
|
|
2505
2546
|
if (grant.pushed_at !== null && grant.pushed_at > now - LIMITS.pushGapS * 1000) paced.add(row.id)
|
|
2506
2547
|
}
|
|
@@ -2902,10 +2943,10 @@ export class RelayCore {
|
|
|
2902
2943
|
.filter((row) =>
|
|
2903
2944
|
present
|
|
2904
2945
|
? present.includes(row.id)
|
|
2905
|
-
:
|
|
2946
|
+
: shownPresence(row, now) !== "offline" || Math.max(row.last_seen ?? 0, row.hook_seen ?? 0, row.hook_ended ?? 0) >= asked.created_at,
|
|
2906
2947
|
)
|
|
2907
2948
|
const gone = (row: EndpointRow): boolean =>
|
|
2908
|
-
|
|
2949
|
+
shownPresence(row, now) === "offline" &&
|
|
2909
2950
|
((row.hook_ended ?? 0) >= asked.created_at || row.status_state === "done" || now - Math.max(row.last_seen ?? 0, row.hook_seen ?? 0) >= ASK_ENDED_GRACE_MS)
|
|
2910
2951
|
if (!rows.length || !rows.every(gone)) return null
|
|
2911
2952
|
return rows.map((row) => ({ address: this.address(row), state: row.status_state as StatusState | null }))
|
|
@@ -3019,7 +3060,7 @@ export class RelayCore {
|
|
|
3019
3060
|
for (const row of this.sql.all<EndpointRow>(
|
|
3020
3061
|
`SELECT * FROM endpoints WHERE kind = 'agent' AND connections = 0 AND status_state IN ('working', 'waiting')`,
|
|
3021
3062
|
)) {
|
|
3022
|
-
if (this.localRow(row) &&
|
|
3063
|
+
if (this.localRow(row) && shownPresence(row, now) === "offline")
|
|
3023
3064
|
this.sql.all(`UPDATE endpoints SET status_state = NULL, status_note = NULL WHERE id = ?`, row.id)
|
|
3024
3065
|
}
|
|
3025
3066
|
}
|
|
@@ -5079,7 +5120,7 @@ export class RelayCore {
|
|
|
5079
5120
|
|
|
5080
5121
|
/** with `shown`, the ids a viewer sees: a parent outside it is null */
|
|
5081
5122
|
protected view(row: EndpointRow, shown?: Set<string>): EndpointView {
|
|
5082
|
-
const presence =
|
|
5123
|
+
const presence = shownPresence(row, this.now())
|
|
5083
5124
|
return {
|
|
5084
5125
|
id: row.id,
|
|
5085
5126
|
address: this.address(row),
|
|
@@ -5315,6 +5356,11 @@ const ANY_PROCESS_KEY = /^pid:\d+(:[A-Za-z0-9:-]{1,64})?$/
|
|
|
5315
5356
|
* whether a connecting claim from the harness process `key` may join a session endpoint: no connection holds it, the harness process that last
|
|
5316
5357
|
* claimed it claims again, and, once a connection held it (a token was handed out), with the same full key, which a recycled pid cannot match
|
|
5317
5358
|
*/
|
|
5359
|
+
/** a claim from the very harness process that holds the live `row`, such as a session it restarted in process */
|
|
5360
|
+
function reclaims(row: EndpointRow, key: string | null): boolean {
|
|
5361
|
+
return row.connections > 0 && key !== null && row.process === key && PROCESS_KEY.test(key)
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5318
5364
|
function sameProcess(row: EndpointRow, key: string | null): boolean {
|
|
5319
5365
|
if (row.connections > 0 || key === null || row.process === null) return false
|
|
5320
5366
|
return row.token_hash === null ? sameHarness(row.process, key) : row.process === key && PROCESS_KEY.test(key)
|
|
@@ -5379,6 +5425,17 @@ function presenceOf(row: EndpointRow, now: number): Presence {
|
|
|
5379
5425
|
return row.hook_seen >= now - LIMITS.hookIdleS * 1000 ? "idle" : "offline"
|
|
5380
5426
|
}
|
|
5381
5427
|
|
|
5428
|
+
/** a wake with a live waker, or a hook waker that left within LIMITS.restartGraceS */
|
|
5429
|
+
function waking(row: EndpointRow, now: number): boolean {
|
|
5430
|
+
return wakeOf(row) !== null && (row.wakers > 0 || (row.waker_left ?? 0) >= now - LIMITS.restartGraceS * 1000)
|
|
5431
|
+
}
|
|
5432
|
+
|
|
5433
|
+
/** the presence an endpoint shows: one its waker can wake is at least idle, though its session's own presence decayed */
|
|
5434
|
+
function shownPresence(row: EndpointRow, now: number): Presence {
|
|
5435
|
+
const presence = presenceOf(row, now)
|
|
5436
|
+
return presence === "offline" && waking(row, now) ? "idle" : presence
|
|
5437
|
+
}
|
|
5438
|
+
|
|
5382
5439
|
function pairKey(a: string, b: string): string {
|
|
5383
5440
|
return a < b ? `${a}|${b}` : `${b}|${a}`
|
|
5384
5441
|
}
|