@scp-dev/core 0.1.4 → 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 +23 -2
- package/dist/index.js +76 -19
- package/package.json +1 -1
- package/src/core.ts +81 -19
- package/src/protocol.ts +16 -1
package/dist/index.d.ts
CHANGED
|
@@ -346,7 +346,7 @@ declare const LIMITS: {
|
|
|
346
346
|
readonly hookEndedGraceS: 30;
|
|
347
347
|
readonly restartGraceS: 60;
|
|
348
348
|
readonly injectMessages: 5;
|
|
349
|
-
readonly injectBytes:
|
|
349
|
+
readonly injectBytes: 7168;
|
|
350
350
|
readonly pushBytes: 20480;
|
|
351
351
|
/** the stops a session's push may continue between two prompts */
|
|
352
352
|
readonly pushes: 20;
|
|
@@ -422,6 +422,12 @@ declare const MESSAGE_KINDS: readonly MessageKind[];
|
|
|
422
422
|
declare const STATUS_STATES: readonly StatusState[];
|
|
423
423
|
declare function validHandle(handle: string): boolean;
|
|
424
424
|
declare function validHarness(harness: string): boolean;
|
|
425
|
+
/** harness names of the path a claim came by (a bare CLI, hook or MCP shim), not of the agent behind it */
|
|
426
|
+
declare const GENERIC_HARNESSES: ReadonlySet<string>;
|
|
427
|
+
/** whether a claim of harness `claimed` may take a row of harness `held`: the same harness, or a generic row, which heals to the claim's */
|
|
428
|
+
declare function harnessFits(held: string | null, claimed: string): boolean;
|
|
429
|
+
/** the harness a claimed row keeps: the claim's, except that a generic claim never replaces a known harness */
|
|
430
|
+
declare function keptHarness(held: string | null, claimed: string): string;
|
|
425
431
|
interface Claim {
|
|
426
432
|
handle?: string;
|
|
427
433
|
harness?: string;
|
|
@@ -1103,6 +1109,10 @@ interface EndpointRow {
|
|
|
1103
1109
|
linked: number | null;
|
|
1104
1110
|
/** how the session that holds the endpoint wakes it: a {@link Wake} other than none, or null */
|
|
1105
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;
|
|
1106
1116
|
}
|
|
1107
1117
|
interface MessageRow {
|
|
1108
1118
|
id: string;
|
|
@@ -1165,6 +1175,8 @@ declare class RelayCore {
|
|
|
1165
1175
|
constructor(sql: Sql, options: CoreOptions);
|
|
1166
1176
|
/** a `machine` label naming this host by its hostname, as clients default it, names it by its machine name when one is set */
|
|
1167
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;
|
|
1168
1180
|
migrate(): void;
|
|
1169
1181
|
/** every stored list that is exactly `tree:self`, the tree preset's old list, narrows once to the preset's list */
|
|
1170
1182
|
private migrateTreePreset;
|
|
@@ -1250,6 +1262,8 @@ declare class RelayCore {
|
|
|
1250
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
|
|
1251
1263
|
* `row` itself from `row`'s own process; never under `row` or elsewhere in its tree
|
|
1252
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;
|
|
1253
1267
|
private restarts;
|
|
1254
1268
|
/**
|
|
1255
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
|
|
@@ -1338,6 +1352,13 @@ declare class RelayCore {
|
|
|
1338
1352
|
transient?: boolean;
|
|
1339
1353
|
process?: string;
|
|
1340
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;
|
|
1341
1362
|
/**
|
|
1342
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.
|
|
1343
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
|
|
@@ -2492,4 +2513,4 @@ declare function sha256Hex(text: string): string;
|
|
|
2492
2513
|
/** the UTF-8 byte length of text, the unit every byte limit counts in */
|
|
2493
2514
|
declare function utf8Bytes(text: string): number;
|
|
2494
2515
|
|
|
2495
|
-
export { ACCOUNT_HANDLE, ACTIVITY_KINDS, type ActivityKind, type ActivityRow, type Admission, type AskResult, type Attachment, type AuditRow, BROKER_CAPABILITIES, type BreakerAdmission, CAPABILITIES, CLI_NAME, type Claim, type CoreOptions, type CrossAccountGate, type CrossAccountLink, DEFAULT_IMPLEMENTATION_PREFIXES, DEFAULT_JUDGE_BUDGET, DEFAULT_JUDGE_EFFORT, type DataClass, ERROR_CODES, type EndpointKind, type EndpointRow, type EndpointView, type Envelope, type ErrorCode, FakeJudgeClient, GONE_PREFIX, GROUP_NAME, type GateContext, type GateVerdict, HANDBACK_CUT, HUMAN_HANDLE, HttpJudgeClient, type HttpJudgeOptions, JUDGE_BREAKER_DEFAULTS, JUDGE_CATEGORIES, JUDGE_EFFORTS, JUDGE_FALLBACK_NOTE, JUDGE_MAX_OUTPUT_TOKENS, JUDGE_PROMPT_VERSION, JUDGE_PROVIDERS, JUDGE_VERDICTS, type JudgeAuditDetail, type JudgeAuditEvent, type JudgeBoundary, JudgeBreaker, type JudgeBreakerOptions, type JudgeBudget, type JudgeCategory, type JudgeClient, type JudgeContext, type JudgeEffort, type JudgeHeld, type JudgeModelConfig, type JudgePolicy, type JudgeProvider, JudgeProviderError, type JudgeRedacted, type JudgeSide, type JudgeStats, type JudgeVerdict, LIMITS, MESSAGE_KINDS, type MessageKind, type MessageReceipts, type MessageRow, OMITTED, PASS_GATE, PEEK, PREAMBLE, PREAMBLE_BRIEF, PRESET_LISTS, PROTOCOL, type PeekLevel, type PeekResult, type PeekSeen, type PeekState, type PeekTurn, type PeerView, type PiiHit, type Policy, type PolicyDefaults, type PolicyGroup, type PolicyRule, type PolicySubject, type Prefiltered, type Presence, REDACTION_KINDS, type Reach, type Redaction, RelayCore, RelayError, type RunJudgeInput, SCOPE_PRESETS, SERVER_FIELDS, SHORT_ID, STATUS_STATES, SYSTEM_HANDLE, type ScopeActor, type ScopeCap, type ScopeCaps, type ScopeChange, type ScopeDecision, type ScopeDirection, type ScopeEntry, type ScopeGroupIdentity, type ScopeGroupRules, type ScopeList, type ScopeNode, type ScopePolicyCap, type ScopePreset, type ScopeRules, type ScopeSubject, type ScopeView, type ScopeWorld, type SecretScanner, type Selector, type SendParams, type SendResult, type Sql, type SqlValue, type StatusState, type StopNudge, type StopWaitResult, TITLE_CHARS, type ThreadMessage, type Verdict, VerdictCache, WAKES, type Wake, type WhyResult, type XAllow, type XContact, XCounters, type XDelivered, type XEnvelopeFrom, XLIMITS, type XLimitConfig, type XLimits, type XListed, type XMessage, type XRelease, admits, allowlisted, applyRedactions, canonicalPolicy, cleanLabels, cleanText, clip, clipPasses, clipped, clock, collapseTools, createSecretScanner, crossFleet, dayStart, declaredLimits, deliveryText, digestText, dropQuoted, effectiveSelector, expandRules, findPii, findSecret, gateRefusal, groupCapKey, handbackOutput, hasScope, honestSender, isLeaving, joinRedacted, judgeBoundary, judgeDetailForAgents, judgeFellBack, judgeModel, judgePrompt, keepLast, leavesFleet, matches, mergeVerdicts, narrower, newId, newJudgeStats, one, parseJudgePolicy, parsePolicy, parseVerdict, parseXMessage, peekLevel, policyRefusal, prefilter, presenceText, presetOf, randomHex, reachLines, readText, receiptLines, redactPii, redactSecrets, redactionKind, relation, relationText, replyGrant, runJudge, sameSecret, scopeDecision, scopeLists, seqList, setImplementationPrefixes, sha256Hex, shortId, steerNote, stripControl, stripRelaySpans, stripSteerNote, unreachable, untilTomorrow, utf8Bytes, validHandle, validHarness, validSelector, wakeText, widenText, wrapMessage, wrapPeek, xlimits };
|
|
2516
|
+
export { ACCOUNT_HANDLE, ACTIVITY_KINDS, type ActivityKind, type ActivityRow, type Admission, type AskResult, type Attachment, type AuditRow, BROKER_CAPABILITIES, type BreakerAdmission, CAPABILITIES, CLI_NAME, type Claim, type CoreOptions, type CrossAccountGate, type CrossAccountLink, DEFAULT_IMPLEMENTATION_PREFIXES, DEFAULT_JUDGE_BUDGET, DEFAULT_JUDGE_EFFORT, type DataClass, ERROR_CODES, type EndpointKind, type EndpointRow, type EndpointView, type Envelope, type ErrorCode, FakeJudgeClient, GENERIC_HARNESSES, GONE_PREFIX, GROUP_NAME, type GateContext, type GateVerdict, HANDBACK_CUT, HUMAN_HANDLE, HttpJudgeClient, type HttpJudgeOptions, JUDGE_BREAKER_DEFAULTS, JUDGE_CATEGORIES, JUDGE_EFFORTS, JUDGE_FALLBACK_NOTE, JUDGE_MAX_OUTPUT_TOKENS, JUDGE_PROMPT_VERSION, JUDGE_PROVIDERS, JUDGE_VERDICTS, type JudgeAuditDetail, type JudgeAuditEvent, type JudgeBoundary, JudgeBreaker, type JudgeBreakerOptions, type JudgeBudget, type JudgeCategory, type JudgeClient, type JudgeContext, type JudgeEffort, type JudgeHeld, type JudgeModelConfig, type JudgePolicy, type JudgeProvider, JudgeProviderError, type JudgeRedacted, type JudgeSide, type JudgeStats, type JudgeVerdict, LIMITS, MESSAGE_KINDS, type MessageKind, type MessageReceipts, type MessageRow, OMITTED, PASS_GATE, PEEK, PREAMBLE, PREAMBLE_BRIEF, PRESET_LISTS, PROTOCOL, type PeekLevel, type PeekResult, type PeekSeen, type PeekState, type PeekTurn, type PeerView, type PiiHit, type Policy, type PolicyDefaults, type PolicyGroup, type PolicyRule, type PolicySubject, type Prefiltered, type Presence, REDACTION_KINDS, type Reach, type Redaction, RelayCore, RelayError, type RunJudgeInput, SCOPE_PRESETS, SERVER_FIELDS, SHORT_ID, STATUS_STATES, SYSTEM_HANDLE, type ScopeActor, type ScopeCap, type ScopeCaps, type ScopeChange, type ScopeDecision, type ScopeDirection, type ScopeEntry, type ScopeGroupIdentity, type ScopeGroupRules, type ScopeList, type ScopeNode, type ScopePolicyCap, type ScopePreset, type ScopeRules, type ScopeSubject, type ScopeView, type ScopeWorld, type SecretScanner, type Selector, type SendParams, type SendResult, type Sql, type SqlValue, type StatusState, type StopNudge, type StopWaitResult, TITLE_CHARS, type ThreadMessage, type Verdict, VerdictCache, WAKES, type Wake, type WhyResult, type XAllow, type XContact, XCounters, type XDelivered, type XEnvelopeFrom, XLIMITS, type XLimitConfig, type XLimits, type XListed, type XMessage, type XRelease, admits, allowlisted, applyRedactions, canonicalPolicy, cleanLabels, cleanText, clip, clipPasses, clipped, clock, collapseTools, createSecretScanner, crossFleet, dayStart, declaredLimits, deliveryText, digestText, dropQuoted, effectiveSelector, expandRules, findPii, findSecret, gateRefusal, groupCapKey, handbackOutput, harnessFits, hasScope, honestSender, isLeaving, joinRedacted, judgeBoundary, judgeDetailForAgents, judgeFellBack, judgeModel, judgePrompt, keepLast, keptHarness, leavesFleet, matches, mergeVerdicts, narrower, newId, newJudgeStats, one, parseJudgePolicy, parsePolicy, parseVerdict, parseXMessage, peekLevel, policyRefusal, prefilter, presenceText, presetOf, randomHex, reachLines, readText, receiptLines, redactPii, redactSecrets, redactionKind, relation, relationText, replyGrant, runJudge, sameSecret, scopeDecision, scopeLists, seqList, setImplementationPrefixes, sha256Hex, shortId, steerNote, stripControl, stripRelaySpans, stripSteerNote, unreachable, untilTomorrow, utf8Bytes, validHandle, validHarness, validSelector, wakeText, widenText, wrapMessage, wrapPeek, xlimits };
|
package/dist/index.js
CHANGED
|
@@ -126,7 +126,7 @@ var LIMITS = {
|
|
|
126
126
|
hookEndedGraceS: 30,
|
|
127
127
|
restartGraceS: 60,
|
|
128
128
|
injectMessages: 5,
|
|
129
|
-
injectBytes:
|
|
129
|
+
injectBytes: 7168,
|
|
130
130
|
pushBytes: 20480,
|
|
131
131
|
/** the stops a session's push may continue between two prompts */
|
|
132
132
|
pushes: 20,
|
|
@@ -192,6 +192,14 @@ function validHandle(handle) {
|
|
|
192
192
|
function validHarness(harness) {
|
|
193
193
|
return HARNESS.test(harness);
|
|
194
194
|
}
|
|
195
|
+
var GENERIC_HARNESSES = /* @__PURE__ */ new Set(["cli", "hook", "mcp", "shell"]);
|
|
196
|
+
var generic = (harness) => harness === null || GENERIC_HARNESSES.has(harness);
|
|
197
|
+
function harnessFits(held, claimed) {
|
|
198
|
+
return held === claimed || generic(held);
|
|
199
|
+
}
|
|
200
|
+
function keptHarness(held, claimed) {
|
|
201
|
+
return generic(claimed) && !generic(held) ? held : claimed;
|
|
202
|
+
}
|
|
195
203
|
var SCOPE_PRESETS = ["open", "tree", "parent", "none"];
|
|
196
204
|
var SERVER_FIELDS = ["from", "id", "seq", "hop", "created_at", "auth", "expires_at", "relation"];
|
|
197
205
|
|
|
@@ -1086,6 +1094,7 @@ function utf8Bytes(text) {
|
|
|
1086
1094
|
}
|
|
1087
1095
|
|
|
1088
1096
|
// src/core.ts
|
|
1097
|
+
var GENERIC_SQL = `harness IN (${[...GENERIC_HARNESSES].map((h) => `'${h}'`).join(", ")})`;
|
|
1089
1098
|
var HUMAN_HANDLE = "_human";
|
|
1090
1099
|
var TITLE_CHARS = 64;
|
|
1091
1100
|
var SYSTEM_HANDLE = "_relay";
|
|
@@ -1297,6 +1306,19 @@ var RelayCore = class {
|
|
|
1297
1306
|
if (this.namedMachine !== null && labels.machine !== void 0 && labels.machine === this.hostname) labels.machine = this.namedMachine;
|
|
1298
1307
|
return labels;
|
|
1299
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
|
+
}
|
|
1300
1322
|
migrate() {
|
|
1301
1323
|
for (const statement of MIGRATION) this.sql.all(statement);
|
|
1302
1324
|
const columns = this.sql.all(`PRAGMA table_info(idempotency)`).map((c) => c.name);
|
|
@@ -1335,7 +1357,9 @@ var RelayCore = class {
|
|
|
1335
1357
|
["tldr_turn", "INTEGER"],
|
|
1336
1358
|
["named", "INTEGER"],
|
|
1337
1359
|
["linked", "INTEGER"],
|
|
1338
|
-
["wake", "TEXT"]
|
|
1360
|
+
["wake", "TEXT"],
|
|
1361
|
+
["wakers", "INTEGER NOT NULL DEFAULT 0"],
|
|
1362
|
+
["waker_left", "INTEGER"]
|
|
1339
1363
|
]) {
|
|
1340
1364
|
if (!endpointColumns.includes(column)) this.sql.all(`ALTER TABLE endpoints ADD COLUMN ${column} ${type}`);
|
|
1341
1365
|
}
|
|
@@ -1595,7 +1619,7 @@ var RelayCore = class {
|
|
|
1595
1619
|
const bare = transient && fields.session !== null && BARE_PID_KEY.test(fields.session);
|
|
1596
1620
|
let row = fields.session ? one(
|
|
1597
1621
|
this.sql,
|
|
1598
|
-
`SELECT * FROM endpoints WHERE kind = 'agent' AND harness = ?
|
|
1622
|
+
`SELECT * FROM endpoints WHERE kind = 'agent' AND (harness = ? OR ${GENERIC_SQL})
|
|
1599
1623
|
AND (session = ? OR (? = 1 AND substr(session, 1, ?) = ? AND (connections = 0 OR hook_seen >= ?)))
|
|
1600
1624
|
AND substr(handle, 1, ${GONE_PREFIX.length}) != '${GONE_PREFIX}' ORDER BY COALESCE(last_seen, created_at) DESC LIMIT 1`,
|
|
1601
1625
|
harness,
|
|
@@ -1612,7 +1636,7 @@ var RelayCore = class {
|
|
|
1612
1636
|
if (row && presenceOf(row, now) === "offline") {
|
|
1613
1637
|
this.sql.all(`UPDATE endpoints SET session = NULL WHERE id = ?`, row.id);
|
|
1614
1638
|
row = void 0;
|
|
1615
|
-
} else if (row && !transient && !sameProcess(row, fields.process)) {
|
|
1639
|
+
} else if (row && !transient && !sameProcess(row, fields.process) && !(row.handle === wanted && reclaims(row, fields.process))) {
|
|
1616
1640
|
row = void 0;
|
|
1617
1641
|
session = null;
|
|
1618
1642
|
} else if (row && !transient && row.token_hash === null && row.named === null && row.handle !== wanted && !row.handle.startsWith(`${wanted.slice(0, 58)}-`)) {
|
|
@@ -1624,7 +1648,7 @@ var RelayCore = class {
|
|
|
1624
1648
|
if (!held) {
|
|
1625
1649
|
this.sql.all(`UPDATE endpoints SET handle = ? WHERE id = ?`, handle, hooks.id);
|
|
1626
1650
|
settled = true;
|
|
1627
|
-
} else if (held.kind === "agent" && held.connections === 0 && presenceOf(held, now) === "offline" && held.harness
|
|
1651
|
+
} else if (held.kind === "agent" && held.connections === 0 && presenceOf(held, now) === "offline" && harnessFits(held.harness, harness)) {
|
|
1628
1652
|
if (this.restarts(held, this.placement(scope, now).parent, fields.process) || this.redeems(held, scope, { harness, ...fields })) {
|
|
1629
1653
|
;
|
|
1630
1654
|
({ moved, stopBlocks } = this.leaveHooks(hooks, held.id));
|
|
@@ -1654,12 +1678,12 @@ var RelayCore = class {
|
|
|
1654
1678
|
this.sql.all(`INSERT INTO endpoints (id, handle, kind, harness, labels, created_at) VALUES (?, ?, 'agent', ?, '{}', ?)`, id, handle, harness, now);
|
|
1655
1679
|
row = this.byId(id);
|
|
1656
1680
|
created = true;
|
|
1657
|
-
} else if (held.kind === "agent" && held.connections === 0 && presenceOf(held, now) === "offline" && held.harness
|
|
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)) {
|
|
1658
1682
|
own = transient && held.process !== null && fields.process !== null && sameHarness(held.process, fields.process);
|
|
1659
1683
|
const ended = held.hook_ended !== null || (held.last_seen ?? held.created_at) < now - LIMITS.restartGraceS * 1e3;
|
|
1660
1684
|
if ((!transient || own || ended) && (this.restarts(held, this.placement(scope, now).parent, fields.process) || this.redeems(held, scope, { harness, ...fields })))
|
|
1661
1685
|
row = held;
|
|
1662
|
-
} else if (scope.exact && held.kind === "agent" && held.process === null && held.session === null && held.harness
|
|
1686
|
+
} else if (scope.exact && held.kind === "agent" && held.process === null && held.session === null && harnessFits(held.harness, harness) && this.restarts(held, this.placement(scope, now).parent, null)) {
|
|
1663
1687
|
row = held;
|
|
1664
1688
|
joined = true;
|
|
1665
1689
|
live = presenceOf(held, now) !== "offline";
|
|
@@ -1684,7 +1708,7 @@ var RelayCore = class {
|
|
|
1684
1708
|
stop_blocks = CASE WHEN ? IS NOT NULL THEN ? WHEN ? IS NOT NULL AND session IS NOT ? THEN 0 ELSE stop_blocks END,
|
|
1685
1709
|
briefed = CASE WHEN ? IS NOT NULL AND session IS NOT ? THEN NULL ELSE briefed END, introduced = CASE WHEN ? IS NOT NULL AND session IS NOT ? THEN NULL ELSE introduced END,
|
|
1686
1710
|
directive_holds = CASE WHEN ? IS NOT NULL AND session IS NOT ? THEN NULL ELSE directive_holds END WHERE id = ?`,
|
|
1687
|
-
harness,
|
|
1711
|
+
keptHarness(row.harness, harness),
|
|
1688
1712
|
fields.model,
|
|
1689
1713
|
fields.cwd,
|
|
1690
1714
|
fields.parent,
|
|
@@ -1813,7 +1837,7 @@ var RelayCore = class {
|
|
|
1813
1837
|
if ((held.session !== null || held.process !== null) && presenceOf(held, now) !== "offline") {
|
|
1814
1838
|
return new RelayError("unauthorized", `${handle} is held by a live ${held.harness} session: act as it from that session's own shell, no --as needed`);
|
|
1815
1839
|
}
|
|
1816
|
-
if (held.harness
|
|
1840
|
+
if (!harnessFits(held.harness, harness)) return new RelayError("unauthorized", `${handle} is a ${held.harness} endpoint: pass --harness ${held.harness}`);
|
|
1817
1841
|
return new RelayError("unauthorized", `${handle} is placed in another tree or under another parent than this shell`);
|
|
1818
1842
|
}
|
|
1819
1843
|
/**
|
|
@@ -1863,6 +1887,11 @@ var RelayCore = class {
|
|
|
1863
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
|
|
1864
1888
|
* `row` itself from `row`'s own process; never under `row` or elsewhere in its tree
|
|
1865
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
|
+
}
|
|
1866
1895
|
restarts(row, parent, process) {
|
|
1867
1896
|
if (parent?.id === row.id) return process !== null && row.process !== null && sameHarness(row.process, process);
|
|
1868
1897
|
return (parent?.id ?? null) === row.parent_ep || parent === null && row.linked !== null;
|
|
@@ -1875,7 +1904,7 @@ var RelayCore = class {
|
|
|
1875
1904
|
const spent = this.spentGrant(scope);
|
|
1876
1905
|
if (spent?.used_by !== row.id) return false;
|
|
1877
1906
|
const lineage = this.lineageParent(scope.lineage);
|
|
1878
|
-
if (!lineage) return row.harness
|
|
1907
|
+
if (!lineage) return harnessFits(row.harness, claimant.harness) && spent.spent_cwd !== null && spent.spent_cwd === claimant.cwd;
|
|
1879
1908
|
return this.restarts(row, lineage, claimant.process) || this.ancestorsOf(row).includes(lineage.id);
|
|
1880
1909
|
}
|
|
1881
1910
|
/** the claim's grant, when an endpoint spent it */
|
|
@@ -2027,7 +2056,7 @@ var RelayCore = class {
|
|
|
2027
2056
|
endpoint
|
|
2028
2057
|
);
|
|
2029
2058
|
const row = this.mustEndpoint(endpoint);
|
|
2030
|
-
if (!options.settle ||
|
|
2059
|
+
if (!options.settle || shownPresence(row, now) !== "offline" || row.status_state !== "working" && row.status_state !== "waiting") return false;
|
|
2031
2060
|
this.sql.all(`UPDATE endpoints SET status_state = NULL, status_note = NULL WHERE id = ?`, endpoint);
|
|
2032
2061
|
return true;
|
|
2033
2062
|
}
|
|
@@ -2042,6 +2071,7 @@ var RelayCore = class {
|
|
|
2042
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`,
|
|
2043
2072
|
this.now() - (LIMITS.hookIdleS - LIMITS.restartGraceS) * 1e3
|
|
2044
2073
|
);
|
|
2074
|
+
this.sql.all(`UPDATE endpoints SET wakers = 0, waker_left = ? WHERE wakers > 0`, this.now());
|
|
2045
2075
|
}
|
|
2046
2076
|
whoami(endpoint) {
|
|
2047
2077
|
return this.view(this.mustEndpoint(endpoint));
|
|
@@ -2059,7 +2089,7 @@ var RelayCore = class {
|
|
|
2059
2089
|
const viewer = filters.viewer === void 0 ? void 0 : this.mustEndpoint(filters.viewer);
|
|
2060
2090
|
const shown = viewer ? new Set(this.visibleRows(viewer).map((row) => row.id)) : void 0;
|
|
2061
2091
|
const peeker = viewer ?? (filters.peekable === true ? this.byHandle(HUMAN_HANDLE) : void 0);
|
|
2062
|
-
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) => {
|
|
2063
2093
|
const view = this.view(row, shown);
|
|
2064
2094
|
return peeker && this.peekableBy(peeker, row, viewer?.kind === "human") ? { ...view, peekable: row.peek } : view;
|
|
2065
2095
|
}).filter((view) => !filters.presence || view.presence === filters.presence);
|
|
@@ -2159,6 +2189,20 @@ var RelayCore = class {
|
|
|
2159
2189
|
this.sql.all(`UPDATE endpoints SET wake = ? WHERE id = ?`, wake, endpoint);
|
|
2160
2190
|
return true;
|
|
2161
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
|
+
}
|
|
2162
2206
|
/**
|
|
2163
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.
|
|
2164
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
|
|
@@ -2460,7 +2504,7 @@ var RelayCore = class {
|
|
|
2460
2504
|
const recipients = [...new Map(resolved.map((row) => [row.id, row])).values()];
|
|
2461
2505
|
if (params.live !== void 0 && typeof params.live !== "boolean") throw new RelayError("invalid_params", "live must be true or false");
|
|
2462
2506
|
const dark = recipients.filter(
|
|
2463
|
-
(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)
|
|
2464
2508
|
);
|
|
2465
2509
|
if (params.live === true && dark.length)
|
|
2466
2510
|
throw new RelayError("not_live", `no live session holds ${dark.map((r) => this.address(r)).join(", ")}: nothing was queued`);
|
|
@@ -2705,7 +2749,7 @@ var RelayCore = class {
|
|
|
2705
2749
|
reach(sender, recipient, offline, now) {
|
|
2706
2750
|
if (recipient.kind !== "agent" || !this.localRow(recipient)) return void 0;
|
|
2707
2751
|
const done = recipient.status_state === "done";
|
|
2708
|
-
if ((done ||
|
|
2752
|
+
if ((done || shownPresence(recipient, now) === "offline") && this.unjoinedSpawn(sender, recipient.handle, now)) return "starting";
|
|
2709
2753
|
if (done) return "ended";
|
|
2710
2754
|
return offline ? "offline" : "running";
|
|
2711
2755
|
}
|
|
@@ -3104,7 +3148,7 @@ var RelayCore = class {
|
|
|
3104
3148
|
continue;
|
|
3105
3149
|
}
|
|
3106
3150
|
const row = this.byId(grant.used_by);
|
|
3107
|
-
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;
|
|
3108
3152
|
workers.set(row.id, row);
|
|
3109
3153
|
if (grant.pushed_at !== null && grant.pushed_at > now - LIMITS.pushGapS * 1e3) paced.add(row.id);
|
|
3110
3154
|
}
|
|
@@ -3467,9 +3511,9 @@ var RelayCore = class {
|
|
|
3467
3511
|
request,
|
|
3468
3512
|
endpoint
|
|
3469
3513
|
).filter(
|
|
3470
|
-
(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
|
|
3471
3515
|
);
|
|
3472
|
-
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);
|
|
3473
3517
|
if (!rows.length || !rows.every(gone)) return null;
|
|
3474
3518
|
return rows.map((row) => ({ address: this.address(row), state: row.status_state }));
|
|
3475
3519
|
}
|
|
@@ -3564,7 +3608,7 @@ var RelayCore = class {
|
|
|
3564
3608
|
for (const row of this.sql.all(
|
|
3565
3609
|
`SELECT * FROM endpoints WHERE kind = 'agent' AND connections = 0 AND status_state IN ('working', 'waiting')`
|
|
3566
3610
|
)) {
|
|
3567
|
-
if (this.localRow(row) &&
|
|
3611
|
+
if (this.localRow(row) && shownPresence(row, now) === "offline")
|
|
3568
3612
|
this.sql.all(`UPDATE endpoints SET status_state = NULL, status_note = NULL WHERE id = ?`, row.id);
|
|
3569
3613
|
}
|
|
3570
3614
|
}
|
|
@@ -5373,7 +5417,7 @@ ${stderr}` : "";
|
|
|
5373
5417
|
}
|
|
5374
5418
|
/** with `shown`, the ids a viewer sees: a parent outside it is null */
|
|
5375
5419
|
view(row, shown) {
|
|
5376
|
-
const presence =
|
|
5420
|
+
const presence = shownPresence(row, this.now());
|
|
5377
5421
|
return {
|
|
5378
5422
|
id: row.id,
|
|
5379
5423
|
address: this.address(row),
|
|
@@ -5566,6 +5610,9 @@ function unreachable(address) {
|
|
|
5566
5610
|
var PROCESS_KEY = /^pid:\d+:[A-Za-z0-9:-]{1,64}$/;
|
|
5567
5611
|
var BARE_PID_KEY = /^pid:\d+$/;
|
|
5568
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
|
+
}
|
|
5569
5616
|
function sameProcess(row, key) {
|
|
5570
5617
|
if (row.connections > 0 || key === null || row.process === null) return false;
|
|
5571
5618
|
return row.token_hash === null ? sameHarness(row.process, key) : row.process === key && PROCESS_KEY.test(key);
|
|
@@ -5606,6 +5653,13 @@ function presenceOf(row, now) {
|
|
|
5606
5653
|
if (row.hook_seen >= now - LIMITS.hookOnlineS * 1e3) return "online";
|
|
5607
5654
|
return row.hook_seen >= now - LIMITS.hookIdleS * 1e3 ? "idle" : "offline";
|
|
5608
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
|
+
}
|
|
5609
5663
|
function pairKey(a, b) {
|
|
5610
5664
|
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
5611
5665
|
}
|
|
@@ -6387,6 +6441,7 @@ export {
|
|
|
6387
6441
|
DEFAULT_JUDGE_EFFORT,
|
|
6388
6442
|
ERROR_CODES,
|
|
6389
6443
|
FakeJudgeClient,
|
|
6444
|
+
GENERIC_HARNESSES,
|
|
6390
6445
|
GONE_PREFIX,
|
|
6391
6446
|
GROUP_NAME,
|
|
6392
6447
|
HANDBACK_CUT,
|
|
@@ -6449,6 +6504,7 @@ export {
|
|
|
6449
6504
|
gateRefusal,
|
|
6450
6505
|
groupCapKey,
|
|
6451
6506
|
handbackOutput,
|
|
6507
|
+
harnessFits,
|
|
6452
6508
|
hasScope,
|
|
6453
6509
|
honestSender,
|
|
6454
6510
|
isLeaving,
|
|
@@ -6459,6 +6515,7 @@ export {
|
|
|
6459
6515
|
judgeModel,
|
|
6460
6516
|
judgePrompt,
|
|
6461
6517
|
keepLast,
|
|
6518
|
+
keptHarness,
|
|
6462
6519
|
leavesFleet,
|
|
6463
6520
|
matches,
|
|
6464
6521
|
mergeVerdicts,
|
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -38,6 +38,9 @@ import {
|
|
|
38
38
|
type StopNudge,
|
|
39
39
|
type StopWaitResult,
|
|
40
40
|
type ThreadMessage,
|
|
41
|
+
GENERIC_HARNESSES,
|
|
42
|
+
harnessFits,
|
|
43
|
+
keptHarness,
|
|
41
44
|
validHandle,
|
|
42
45
|
validHarness,
|
|
43
46
|
type Wake,
|
|
@@ -70,6 +73,8 @@ import { findSecret, redactSecrets } from "./scrub"
|
|
|
70
73
|
import { one, type Sql, type SqlValue } from "./sql"
|
|
71
74
|
import { utf8Bytes } from "./utf8"
|
|
72
75
|
|
|
76
|
+
const GENERIC_SQL = `harness IN (${[...GENERIC_HARNESSES].map((h) => `'${h}'`).join(", ")})`
|
|
77
|
+
|
|
73
78
|
export const HUMAN_HANDLE = "_human"
|
|
74
79
|
/** an agent title's length cap */
|
|
75
80
|
export const TITLE_CHARS = 64
|
|
@@ -262,6 +267,10 @@ export interface EndpointRow {
|
|
|
262
267
|
linked: number | null
|
|
263
268
|
/** how the session that holds the endpoint wakes it: a {@link Wake} other than none, or null */
|
|
264
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
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
export interface MessageRow {
|
|
@@ -491,6 +500,20 @@ export class RelayCore {
|
|
|
491
500
|
return labels
|
|
492
501
|
}
|
|
493
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
|
+
|
|
494
517
|
migrate(): void {
|
|
495
518
|
for (const statement of MIGRATION) this.sql.all(statement)
|
|
496
519
|
const columns = this.sql.all<{ name: string }>(`PRAGMA table_info(idempotency)`).map((c) => c.name)
|
|
@@ -530,6 +553,8 @@ export class RelayCore {
|
|
|
530
553
|
["named", "INTEGER"],
|
|
531
554
|
["linked", "INTEGER"],
|
|
532
555
|
["wake", "TEXT"],
|
|
556
|
+
["wakers", "INTEGER NOT NULL DEFAULT 0"],
|
|
557
|
+
["waker_left", "INTEGER"],
|
|
533
558
|
] as const) {
|
|
534
559
|
if (!endpointColumns.includes(column)) this.sql.all(`ALTER TABLE endpoints ADD COLUMN ${column} ${type}`)
|
|
535
560
|
}
|
|
@@ -811,7 +836,7 @@ export class RelayCore {
|
|
|
811
836
|
let row = fields.session
|
|
812
837
|
? one<EndpointRow>(
|
|
813
838
|
this.sql,
|
|
814
|
-
`SELECT * FROM endpoints WHERE kind = 'agent' AND harness = ?
|
|
839
|
+
`SELECT * FROM endpoints WHERE kind = 'agent' AND (harness = ? OR ${GENERIC_SQL})
|
|
815
840
|
AND (session = ? OR (? = 1 AND substr(session, 1, ?) = ? AND (connections = 0 OR hook_seen >= ?)))
|
|
816
841
|
AND substr(handle, 1, ${GONE_PREFIX.length}) != '${GONE_PREFIX}' ORDER BY COALESCE(last_seen, created_at) DESC LIMIT 1`,
|
|
817
842
|
harness,
|
|
@@ -829,7 +854,7 @@ export class RelayCore {
|
|
|
829
854
|
if (row && presenceOf(row, now) === "offline") {
|
|
830
855
|
this.sql.all(`UPDATE endpoints SET session = NULL WHERE id = ?`, row.id)
|
|
831
856
|
row = undefined
|
|
832
|
-
} else if (row && !transient && !sameProcess(row, fields.process)) {
|
|
857
|
+
} else if (row && !transient && !sameProcess(row, fields.process) && !(row.handle === wanted && reclaims(row, fields.process))) {
|
|
833
858
|
row = undefined
|
|
834
859
|
session = null
|
|
835
860
|
} else if (
|
|
@@ -848,7 +873,7 @@ export class RelayCore {
|
|
|
848
873
|
if (!held) {
|
|
849
874
|
this.sql.all(`UPDATE endpoints SET handle = ? WHERE id = ?`, handle, hooks.id)
|
|
850
875
|
settled = true
|
|
851
|
-
} else if (held.kind === "agent" && held.connections === 0 && presenceOf(held, now) === "offline" && held.harness
|
|
876
|
+
} else if (held.kind === "agent" && held.connections === 0 && presenceOf(held, now) === "offline" && harnessFits(held.harness, harness)) {
|
|
852
877
|
if (this.restarts(held, this.placement(scope, now).parent, fields.process) || this.redeems(held, scope, { harness, ...fields })) {
|
|
853
878
|
;({ moved, stopBlocks } = this.leaveHooks(hooks, held.id))
|
|
854
879
|
row = undefined
|
|
@@ -879,9 +904,8 @@ export class RelayCore {
|
|
|
879
904
|
created = true
|
|
880
905
|
} else if (
|
|
881
906
|
held.kind === "agent" &&
|
|
882
|
-
held.connections === 0 &&
|
|
883
|
-
|
|
884
|
-
held.harness === harness &&
|
|
907
|
+
((held.connections === 0 && presenceOf(held, now) === "offline") || (!transient && this.outlived(held))) &&
|
|
908
|
+
harnessFits(held.harness, harness) &&
|
|
885
909
|
held.id !== scope.subagent?.root &&
|
|
886
910
|
!(scope.subagent && held.session?.startsWith("sub:") && held.session !== fields.session)
|
|
887
911
|
) {
|
|
@@ -897,7 +921,7 @@ export class RelayCore {
|
|
|
897
921
|
held.kind === "agent" &&
|
|
898
922
|
held.process === null &&
|
|
899
923
|
held.session === null &&
|
|
900
|
-
held.harness
|
|
924
|
+
harnessFits(held.harness, harness) &&
|
|
901
925
|
this.restarts(held, this.placement(scope, now).parent, null)
|
|
902
926
|
) {
|
|
903
927
|
row = held
|
|
@@ -924,7 +948,7 @@ export class RelayCore {
|
|
|
924
948
|
stop_blocks = CASE WHEN ? IS NOT NULL THEN ? WHEN ? IS NOT NULL AND session IS NOT ? THEN 0 ELSE stop_blocks END,
|
|
925
949
|
briefed = CASE WHEN ? IS NOT NULL AND session IS NOT ? THEN NULL ELSE briefed END, introduced = CASE WHEN ? IS NOT NULL AND session IS NOT ? THEN NULL ELSE introduced END,
|
|
926
950
|
directive_holds = CASE WHEN ? IS NOT NULL AND session IS NOT ? THEN NULL ELSE directive_holds END WHERE id = ?`,
|
|
927
|
-
harness,
|
|
951
|
+
keptHarness(row.harness, harness),
|
|
928
952
|
fields.model,
|
|
929
953
|
fields.cwd,
|
|
930
954
|
fields.parent,
|
|
@@ -1062,7 +1086,7 @@ export class RelayCore {
|
|
|
1062
1086
|
if ((held.session !== null || held.process !== null) && presenceOf(held, now) !== "offline") {
|
|
1063
1087
|
return new RelayError("unauthorized", `${handle} is held by a live ${held.harness} session: act as it from that session's own shell, no --as needed`)
|
|
1064
1088
|
}
|
|
1065
|
-
if (held.harness
|
|
1089
|
+
if (!harnessFits(held.harness, harness)) return new RelayError("unauthorized", `${handle} is a ${held.harness} endpoint: pass --harness ${held.harness}`)
|
|
1066
1090
|
return new RelayError("unauthorized", `${handle} is placed in another tree or under another parent than this shell`)
|
|
1067
1091
|
}
|
|
1068
1092
|
|
|
@@ -1119,6 +1143,12 @@ export class RelayCore {
|
|
|
1119
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
|
|
1120
1144
|
* `row` itself from `row`'s own process; never under `row` or elsewhere in its tree
|
|
1121
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
|
+
|
|
1122
1152
|
private restarts(row: EndpointRow, parent: EndpointRow | null, process: string | null): boolean {
|
|
1123
1153
|
if (parent?.id === row.id) return process !== null && row.process !== null && sameHarness(row.process, process)
|
|
1124
1154
|
return (parent?.id ?? null) === row.parent_ep || (parent === null && row.linked !== null)
|
|
@@ -1132,7 +1162,7 @@ export class RelayCore {
|
|
|
1132
1162
|
const spent = this.spentGrant(scope)
|
|
1133
1163
|
if (spent?.used_by !== row.id) return false
|
|
1134
1164
|
const lineage = this.lineageParent(scope.lineage)
|
|
1135
|
-
if (!lineage) return row.harness
|
|
1165
|
+
if (!lineage) return harnessFits(row.harness, claimant.harness) && spent.spent_cwd !== null && spent.spent_cwd === claimant.cwd
|
|
1136
1166
|
return this.restarts(row, lineage, claimant.process) || this.ancestorsOf(row).includes(lineage.id)
|
|
1137
1167
|
}
|
|
1138
1168
|
|
|
@@ -1300,7 +1330,7 @@ export class RelayCore {
|
|
|
1300
1330
|
endpoint,
|
|
1301
1331
|
)
|
|
1302
1332
|
const row = this.mustEndpoint(endpoint)
|
|
1303
|
-
if (!options.settle ||
|
|
1333
|
+
if (!options.settle || shownPresence(row, now) !== "offline" || (row.status_state !== "working" && row.status_state !== "waiting")) return false
|
|
1304
1334
|
this.sql.all(`UPDATE endpoints SET status_state = NULL, status_note = NULL WHERE id = ?`, endpoint)
|
|
1305
1335
|
return true
|
|
1306
1336
|
}
|
|
@@ -1317,6 +1347,7 @@ export class RelayCore {
|
|
|
1317
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`,
|
|
1318
1348
|
this.now() - (LIMITS.hookIdleS - LIMITS.restartGraceS) * 1000,
|
|
1319
1349
|
)
|
|
1350
|
+
this.sql.all(`UPDATE endpoints SET wakers = 0, waker_left = ? WHERE wakers > 0`, this.now())
|
|
1320
1351
|
}
|
|
1321
1352
|
|
|
1322
1353
|
whoami(endpoint: string): EndpointView {
|
|
@@ -1346,7 +1377,7 @@ export class RelayCore {
|
|
|
1346
1377
|
const shown = viewer ? new Set(this.visibleRows(viewer).map((row) => row.id)) : undefined
|
|
1347
1378
|
const peeker = viewer ?? (filters.peekable === true ? this.byHandle(HUMAN_HANDLE)! : undefined)
|
|
1348
1379
|
return this.listed()
|
|
1349
|
-
.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))
|
|
1350
1381
|
.filter((row) => !filters.harness || row.harness === filters.harness)
|
|
1351
1382
|
.filter((row) => !filters.kind || row.kind === filters.kind)
|
|
1352
1383
|
.filter((row) => !viewer || row.id === viewer.id || this.mayMessage(viewer, row, viewer.kind === "human"))
|
|
@@ -1463,6 +1494,21 @@ export class RelayCore {
|
|
|
1463
1494
|
return true
|
|
1464
1495
|
}
|
|
1465
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
|
+
|
|
1466
1512
|
/**
|
|
1467
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.
|
|
1468
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
|
|
@@ -1782,7 +1828,7 @@ export class RelayCore {
|
|
|
1782
1828
|
const dark = recipients.filter(
|
|
1783
1829
|
(r) =>
|
|
1784
1830
|
r.kind === "agent" &&
|
|
1785
|
-
|
|
1831
|
+
shownPresence(r, now) === "offline" &&
|
|
1786
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)) &&
|
|
1787
1833
|
!this.unjoinedSpawn(sender, r.handle, now),
|
|
1788
1834
|
)
|
|
@@ -2042,7 +2088,7 @@ export class RelayCore {
|
|
|
2042
2088
|
private reach(sender: EndpointRow, recipient: EndpointRow, offline: boolean, now: number): Reach | undefined {
|
|
2043
2089
|
if (recipient.kind !== "agent" || !this.localRow(recipient)) return undefined
|
|
2044
2090
|
const done = recipient.status_state === "done"
|
|
2045
|
-
if ((done ||
|
|
2091
|
+
if ((done || shownPresence(recipient, now) === "offline") && this.unjoinedSpawn(sender, recipient.handle, now)) return "starting"
|
|
2046
2092
|
if (done) return "ended"
|
|
2047
2093
|
return offline ? "offline" : "running"
|
|
2048
2094
|
}
|
|
@@ -2495,7 +2541,7 @@ export class RelayCore {
|
|
|
2495
2541
|
continue
|
|
2496
2542
|
}
|
|
2497
2543
|
const row = this.byId(grant.used_by)
|
|
2498
|
-
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
|
|
2499
2545
|
workers.set(row.id, row)
|
|
2500
2546
|
if (grant.pushed_at !== null && grant.pushed_at > now - LIMITS.pushGapS * 1000) paced.add(row.id)
|
|
2501
2547
|
}
|
|
@@ -2897,10 +2943,10 @@ export class RelayCore {
|
|
|
2897
2943
|
.filter((row) =>
|
|
2898
2944
|
present
|
|
2899
2945
|
? present.includes(row.id)
|
|
2900
|
-
:
|
|
2946
|
+
: shownPresence(row, now) !== "offline" || Math.max(row.last_seen ?? 0, row.hook_seen ?? 0, row.hook_ended ?? 0) >= asked.created_at,
|
|
2901
2947
|
)
|
|
2902
2948
|
const gone = (row: EndpointRow): boolean =>
|
|
2903
|
-
|
|
2949
|
+
shownPresence(row, now) === "offline" &&
|
|
2904
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)
|
|
2905
2951
|
if (!rows.length || !rows.every(gone)) return null
|
|
2906
2952
|
return rows.map((row) => ({ address: this.address(row), state: row.status_state as StatusState | null }))
|
|
@@ -3014,7 +3060,7 @@ export class RelayCore {
|
|
|
3014
3060
|
for (const row of this.sql.all<EndpointRow>(
|
|
3015
3061
|
`SELECT * FROM endpoints WHERE kind = 'agent' AND connections = 0 AND status_state IN ('working', 'waiting')`,
|
|
3016
3062
|
)) {
|
|
3017
|
-
if (this.localRow(row) &&
|
|
3063
|
+
if (this.localRow(row) && shownPresence(row, now) === "offline")
|
|
3018
3064
|
this.sql.all(`UPDATE endpoints SET status_state = NULL, status_note = NULL WHERE id = ?`, row.id)
|
|
3019
3065
|
}
|
|
3020
3066
|
}
|
|
@@ -5074,7 +5120,7 @@ export class RelayCore {
|
|
|
5074
5120
|
|
|
5075
5121
|
/** with `shown`, the ids a viewer sees: a parent outside it is null */
|
|
5076
5122
|
protected view(row: EndpointRow, shown?: Set<string>): EndpointView {
|
|
5077
|
-
const presence =
|
|
5123
|
+
const presence = shownPresence(row, this.now())
|
|
5078
5124
|
return {
|
|
5079
5125
|
id: row.id,
|
|
5080
5126
|
address: this.address(row),
|
|
@@ -5310,6 +5356,11 @@ const ANY_PROCESS_KEY = /^pid:\d+(:[A-Za-z0-9:-]{1,64})?$/
|
|
|
5310
5356
|
* whether a connecting claim from the harness process `key` may join a session endpoint: no connection holds it, the harness process that last
|
|
5311
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
|
|
5312
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
|
+
|
|
5313
5364
|
function sameProcess(row: EndpointRow, key: string | null): boolean {
|
|
5314
5365
|
if (row.connections > 0 || key === null || row.process === null) return false
|
|
5315
5366
|
return row.token_hash === null ? sameHarness(row.process, key) : row.process === key && PROCESS_KEY.test(key)
|
|
@@ -5374,6 +5425,17 @@ function presenceOf(row: EndpointRow, now: number): Presence {
|
|
|
5374
5425
|
return row.hook_seen >= now - LIMITS.hookIdleS * 1000 ? "idle" : "offline"
|
|
5375
5426
|
}
|
|
5376
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
|
+
|
|
5377
5439
|
function pairKey(a: string, b: string): string {
|
|
5378
5440
|
return a < b ? `${a}|${b}` : `${b}|${a}`
|
|
5379
5441
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -34,7 +34,7 @@ export const LIMITS = {
|
|
|
34
34
|
hookEndedGraceS: 30,
|
|
35
35
|
restartGraceS: 60,
|
|
36
36
|
injectMessages: 5,
|
|
37
|
-
injectBytes:
|
|
37
|
+
injectBytes: 7168,
|
|
38
38
|
pushBytes: 20480,
|
|
39
39
|
/** the stops a session's push may continue between two prompts */
|
|
40
40
|
pushes: 20,
|
|
@@ -144,6 +144,21 @@ export function validHarness(harness: string): boolean {
|
|
|
144
144
|
return HARNESS.test(harness)
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/** harness names of the path a claim came by (a bare CLI, hook or MCP shim), not of the agent behind it */
|
|
148
|
+
export const GENERIC_HARNESSES: ReadonlySet<string> = new Set(["cli", "hook", "mcp", "shell"])
|
|
149
|
+
|
|
150
|
+
const generic = (harness: string | null): boolean => harness === null || GENERIC_HARNESSES.has(harness)
|
|
151
|
+
|
|
152
|
+
/** whether a claim of harness `claimed` may take a row of harness `held`: the same harness, or a generic row, which heals to the claim's */
|
|
153
|
+
export function harnessFits(held: string | null, claimed: string): boolean {
|
|
154
|
+
return held === claimed || generic(held)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** the harness a claimed row keeps: the claim's, except that a generic claim never replaces a known harness */
|
|
158
|
+
export function keptHarness(held: string | null, claimed: string): string {
|
|
159
|
+
return generic(claimed) && !generic(held) ? held! : claimed
|
|
160
|
+
}
|
|
161
|
+
|
|
147
162
|
export interface Claim {
|
|
148
163
|
handle?: string
|
|
149
164
|
harness?: string
|