@scp-dev/broker 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -681,6 +681,7 @@ function startBridge(options) {
681
681
  const hello = await request("hello", { protocol: PROTOCOL2, machine, bridge: true });
682
682
  account = hello.account;
683
683
  core.setAliases([account]);
684
+ core.setOthers(account, Array.isArray(hello.others) ? hello.others.filter((name) => typeof name === "string") : []);
684
685
  ready = true;
685
686
  applyRemote(hello.endpoints, true);
686
687
  await Promise.all(fullMirror().map((params) => request("mirror", params)));
@@ -763,7 +764,7 @@ function startBridge(options) {
763
764
  function localAddress(address) {
764
765
  if (typeof address !== "string" || !account || !address.startsWith(`${account}/`)) return address;
765
766
  const rest = address.slice(account.length + 1);
766
- return rest.includes("/") ? `${core.user}/${rest}` : `${core.user}/${account}/${rest}`;
767
+ return rest.includes("/") ? core.remoteAddress(rest) : `${core.user}/${account}/${rest}`;
767
768
  }
768
769
  function cursor() {
769
770
  return inflight.size ? Math.min(...inflight) - 1 : ackedMax;
@@ -796,6 +797,7 @@ function startBridge(options) {
796
797
  }
797
798
  case "endpoints": {
798
799
  if (typeof params.machine !== "string" || params.machine === machine) return;
800
+ if (typeof params.own === "boolean") core.setOther(params.machine, !params.own);
799
801
  try {
800
802
  core.mirror(params.machine, params.endpoints, { full: params.full === true });
801
803
  } catch (error) {
@@ -987,6 +989,17 @@ function startBridge(options) {
987
989
  }
988
990
  if (mirrored(view)) fireAndForget("mirror", { endpoints: [outward(view)], full: false });
989
991
  }
992
+ function renamed(endpoint, was) {
993
+ if (!ready) return;
994
+ let view;
995
+ try {
996
+ view = core.whoami(endpoint);
997
+ } catch {
998
+ return;
999
+ }
1000
+ if (mirrored(view)) fireAndForget("mirror", { endpoints: [{ ...outward(view), was }], full: false });
1001
+ else resync();
1002
+ }
990
1003
  function resync() {
991
1004
  if (!ready) return;
992
1005
  let frames;
@@ -1034,24 +1047,24 @@ function startBridge(options) {
1034
1047
  if (socket && socket.readyState <= 1) socket.close(1e3, "the broker stopped");
1035
1048
  }
1036
1049
  connect();
1037
- return { forward, changed, resync, activity, contacts, status, relink, stop };
1050
+ return { forward, changed, resync, renamed, activity, contacts, status, relink, stop };
1038
1051
  }
1039
1052
 
1040
1053
  // src/server.ts
1041
- import { createHash as createHash2 } from "crypto";
1042
- import { chmodSync as chmodSync2, existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync5, realpathSync, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
1054
+ import { createHash as createHash3 } from "crypto";
1055
+ import { chmodSync as chmodSync2, existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync6, realpathSync, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
1043
1056
  import { createServer } from "net";
1044
1057
  import { homedir as homedir2 } from "os";
1045
- import { dirname as dirname2, isAbsolute, join as join3, sep } from "path";
1058
+ import { dirname as dirname2, isAbsolute, join as join4, sep } from "path";
1046
1059
  import { setTimeout as sleep2 } from "timers/promises";
1047
1060
  import {
1048
1061
  BROKER_CAPABILITIES,
1049
1062
  CLI_NAME,
1050
- LIMITS as LIMITS2,
1063
+ LIMITS,
1051
1064
  parsePolicy,
1052
1065
  PREAMBLE,
1053
1066
  PROTOCOL as PROTOCOL3,
1054
- RelayError as RelayError4,
1067
+ RelayError as RelayError3,
1055
1068
  SERVER_FIELDS,
1056
1069
  stripControl
1057
1070
  } from "@scp-dev/core";
@@ -1059,459 +1072,47 @@ import { FederatedCore } from "@scp-dev/hub";
1059
1072
 
1060
1073
  // src/judge-gate.ts
1061
1074
  import { accessSync, constants, readFileSync as readFileSync4 } from "fs";
1062
- import {
1063
- applyRedactions,
1064
- DEFAULT_JUDGE_BUDGET,
1065
- findSecret as findSecret2,
1066
- HttpJudgeClient,
1067
- JUDGE_PROMPT_VERSION,
1068
- judgeBoundary,
1069
- LIMITS,
1070
- mergeVerdicts,
1071
- newId as newId2,
1072
- newJudgeStats,
1073
- one,
1074
- RelayError as RelayError3,
1075
- runJudge,
1076
- sha256Hex,
1077
- VerdictCache
1078
- } from "@scp-dev/core";
1079
- var HELD_PER_SENDER = 100;
1080
- var iso = (ms) => ms === null ? null : new Date(ms).toISOString();
1081
- var JudgeGate = class {
1082
- constructor(o) {
1083
- this.o = o;
1084
- o.sql.all(
1085
- `CREATE TABLE IF NOT EXISTS judge_holds (id TEXT PRIMARY KEY, from_ep TEXT NOT NULL, params TEXT NOT NULL, origin TEXT NOT NULL, verdict TEXT NOT NULL,
1086
- created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, state TEXT NOT NULL, by TEXT, resolved_at INTEGER)`
1087
- );
1088
- o.sql.all(
1089
- `CREATE TABLE IF NOT EXISTS judge_sent (from_ep TEXT NOT NULL, client_id TEXT NOT NULL, fingerprint TEXT NOT NULL, result TEXT NOT NULL, created_at INTEGER NOT NULL,
1090
- PRIMARY KEY (from_ep, client_id))`
1091
- );
1092
- o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE state = 'releasing'`);
1093
- }
1094
- o;
1095
- stats = newJudgeStats();
1096
- policy;
1097
- version = "";
1098
- client = null;
1099
- cache = new VerdictCache();
1100
- shadows = /* @__PURE__ */ new Set();
1101
- /** judged sends in the last hour per budget: `ep:<id>`, `tree:<root id>` and `*` for the whole broker */
1102
- spent = /* @__PURE__ */ new Map();
1103
- /** the policy's judge section; a model client only when its key is readable, else the prefilter alone */
1104
- configure(policy) {
1105
- this.policy = policy;
1106
- this.version = policy ? sha256Hex(JSON.stringify(policy)) : "";
1107
- this.client = null;
1108
- const model = policy?.model;
1109
- if (!policy || policy.mode === "off" || !model) return;
1110
- const key = this.keyReader(model);
1111
- if (!key) {
1112
- this.o.log(
1113
- `relay broker: the judge model's key (${model.key_env ? `key_env ${model.key_env}` : `key_file ${model.key_file}`}) is not readable; judging with the prefilter only`
1114
- );
1115
- return;
1116
- }
1117
- this.client = (this.o.client ?? ((m, k) => new HttpJudgeClient({ model: m.id, effort: m.effort, key: k })))(model, key);
1118
- }
1119
- keyReader(model) {
1120
- if (model.key_env) {
1121
- const name = model.key_env;
1122
- return this.o.env[name] ? async () => this.o.env[name] ?? "" : null;
1123
- }
1124
- const file = model.key_file;
1125
- try {
1126
- accessSync(file, constants.R_OK);
1127
- } catch {
1128
- return null;
1129
- }
1130
- return async () => readFileSync4(file, "utf8").trim();
1131
- }
1132
- /** resolves once every shadow judgement started so far finished */
1133
- async settled() {
1134
- await Promise.all([...this.shadows]);
1135
- }
1136
- /** a send or ask through the judge; an unjudged one goes straight to `deliver` */
1137
- async send(from, params, origin, deliver) {
1138
- const policy = this.policy;
1139
- const body = params.body;
1140
- if (!policy || policy.mode === "off" || typeof body !== "string" || new TextEncoder().encode(body).length > LIMITS.bodyBytes || findSecret2(body))
1141
- return deliver(from, params, origin);
1142
- const targets = Array.isArray(params.to) ? params.to : [params.to];
1143
- if (targets.some((to) => this.o.core.foreignAccount(to) !== null)) {
1144
- if (policy.mode === "enforce") {
1145
- this.o.core.auditJudge("judge_skipped", from, null, {
1146
- side: "local",
1147
- boundary: "other account",
1148
- prompt_version: JUDGE_PROMPT_VERSION,
1149
- categories: [],
1150
- sha256: sha256Hex(body),
1151
- verdict: "skipped",
1152
- reason: "other_account_judged_by_hub",
1153
- to: targets.map(String)
1154
- });
1155
- }
1156
- return deliver(from, params, origin);
1157
- }
1158
- if (policy.mode === "shadow") {
1159
- const result = await deliver(from, params, origin);
1160
- if (!result.duplicate) this.shadow(policy, from, params, origin, result.id);
1161
- return result;
1162
- }
1163
- const { groups, tree } = this.boundaries(policy, from, params, origin);
1164
- if (!groups.size) return deliver(from, params, origin);
1165
- const ttl = params.ttl_s ?? LIMITS.ttlDefaultS;
1166
- if (!Number.isInteger(ttl) || ttl < 1 || ttl > LIMITS.ttlMaxS) throw new RelayError3("invalid_params", `ttl_s must be 1 to ${LIMITS.ttlMaxS}`);
1167
- const seen = params.client_id === void 0 ? void 0 : this.seen(from, params);
1168
- if (seen) return seen;
1169
- const waiting = one(this.o.sql, `SELECT COUNT(*) AS n FROM judge_holds WHERE from_ep = ? AND state IN ('held', 'releasing')`, from).n;
1170
- if (waiting >= HELD_PER_SENDER)
1171
- throw new RelayError3("rate_limited", `${waiting} of your messages wait for review by your human; send more once they released or refused some`, 3600);
1172
- this.spend(policy, from, tree);
1173
- const verdict = await this.judge(policy, from, body, groups);
1174
- const detail = this.detail(verdict, body, groups);
1175
- switch (verdict.verdict) {
1176
- case "refuse":
1177
- this.o.core.auditJudge("judge_refuse", from, null, detail);
1178
- throw new RelayError3(
1179
- "judge_refused",
1180
- `the relay judge refused this message (local; ${verdict.categories.join(", ") || "no category"}): ${verdict.reason}`,
1181
- void 0,
1182
- {
1183
- side: "local",
1184
- categories: verdict.categories,
1185
- reason: verdict.reason
1186
- }
1187
- );
1188
- case "hold":
1189
- return this.hold(from, params, origin, verdict, groups, detail);
1190
- case "redact": {
1191
- const applied = applyRedactions(body, verdict.redactions);
1192
- const result = await deliver(from, { ...params, body: applied.text }, origin);
1193
- const redacted = { n: applied.n, kinds: applied.kinds };
1194
- this.o.core.auditJudge(verdict.source === "error" ? "judge_skipped" : "judge_redact", from, result.id, { ...detail, redacted });
1195
- return this.remember(from, params, { ...result, redacted });
1196
- }
1197
- case "pass": {
1198
- const result = await deliver(from, params, origin);
1199
- if (verdict.source === "error") this.o.core.auditJudge("judge_skipped", from, result.id, detail);
1200
- return this.remember(from, params, result);
1201
- }
1202
- }
1203
- }
1204
- /** an earlier judged send or waiting hold under the send's client_id: the same message is answered as before, another one refused as core refuses it */
1205
- seen(from, params) {
1206
- const clientId = params.client_id;
1207
- const held2 = one(
1208
- this.o.sql,
1209
- `SELECT id, params FROM judge_holds WHERE from_ep = ? AND json_extract(params, '$.client_id') = ? AND state IN ('held', 'releasing')`,
1210
- from,
1211
- clientId
1212
- );
1213
- const sent = held2 ? void 0 : one(
1214
- this.o.sql,
1215
- `SELECT fingerprint, result FROM judge_sent WHERE from_ep = ? AND client_id = ? AND created_at > ?`,
1216
- from,
1217
- clientId,
1218
- this.o.now() - LIMITS.idempotencyS * 1e3
1219
- );
1220
- const before = held2 ? fingerprint(JSON.parse(held2.params)) : sent?.fingerprint;
1221
- if (before === void 0) return void 0;
1222
- if (before !== fingerprint(params)) throw new RelayError3("invalid_params", `client_id ${clientId} was already used for a different message`);
1223
- return held2 ? heldResult(held2.id, params) : { ...JSON.parse(sent.result), duplicate: true };
1224
- }
1225
- remember(from, params, result) {
1226
- if (params.client_id !== void 0 && !result.duplicate) {
1227
- this.o.sql.all(
1228
- `INSERT OR REPLACE INTO judge_sent (from_ep, client_id, fingerprint, result, created_at) VALUES (?, ?, ?, ?, ?)`,
1229
- from,
1230
- params.client_id,
1231
- fingerprint(params),
1232
- JSON.stringify(result),
1233
- this.o.now()
1234
- );
1235
- }
1236
- return result;
1237
- }
1238
- /**
1239
- * a judged send counts before any model call against its sender's, its lineage tree's and the whole broker's budget, since refused and
1240
- * held sends never reach the send's own limits, and a new handle or a helper process must not bring a fresh budget
1241
- */
1242
- spend(policy, from, tree) {
1243
- const now = this.o.now();
1244
- const global = policy.budget ?? DEFAULT_JUDGE_BUDGET;
1245
- const budgets = [
1246
- [`ep:${from}`, LIMITS.pairPerMinute, LIMITS.pairPerHour, "from this endpoint"],
1247
- [`tree:${tree}`, LIMITS.pairPerMinute * 2, LIMITS.pairPerHour * 2, "from this lineage tree"],
1248
- ["*", global.per_minute, global.per_hour, "on this broker"]
1249
- ];
1250
- const recent = budgets.map(([key]) => (this.spent.get(key) ?? []).filter((at) => at > now - 36e5));
1251
- budgets.forEach(([key, perMinute, perHour, what], i) => {
1252
- for (const [limit, windowS] of [
1253
- [perMinute, 60],
1254
- [perHour, 3600]
1255
- ]) {
1256
- const inWindow = recent[i].filter((at) => at > now - windowS * 1e3);
1257
- if (inWindow.length >= limit) {
1258
- this.spent.set(key, recent[i]);
1259
- throw new RelayError3(
1260
- "rate_limited",
1261
- `${limit} judged messages ${what} in ${windowS} s`,
1262
- Math.max(1, Math.ceil((inWindow[0] + windowS * 1e3 - now) / 1e3))
1263
- );
1264
- }
1265
- }
1266
- });
1267
- for (const [i, [key]] of budgets.entries()) this.spent.set(key, [...recent[i], now]);
1268
- }
1269
- /** how many budgets are tracked (idle ones are evicted by the sweep) */
1270
- budgetKeys() {
1271
- return this.spent.size;
1272
- }
1273
- /** the held messages, newest first, without bodies */
1274
- holds(state) {
1275
- const rows = state ? this.o.sql.all(`SELECT * FROM judge_holds WHERE state = ? ORDER BY created_at DESC, rowid DESC LIMIT 200`, state) : this.o.sql.all(`SELECT * FROM judge_holds ORDER BY created_at DESC, rowid DESC LIMIT 200`);
1276
- return rows.map((row) => this.view(row));
1277
- }
1278
- /** one hold with its body and the judge's redactions while it waits */
1279
- show(id) {
1280
- const row = this.row(id);
1281
- const params = JSON.parse(row.params);
1282
- const verdict = JSON.parse(row.verdict);
1283
- return { ...this.view(row), ...typeof params.body === "string" ? { body: params.body } : {}, redactions: verdict.redactions };
1284
- }
1285
- /** delivers a held message as its sender, as stored or redacted: scope, policy and limits are checked again by the send itself */
1286
- async release(id, options, deliver) {
1287
- const row = one(
1288
- this.o.sql,
1289
- `UPDATE judge_holds SET state = 'releasing' WHERE id = ? AND state = 'held' AND expires_at > ? RETURNING *`,
1290
- id,
1291
- this.o.now()
1292
- );
1293
- if (!row) throw new RelayError3("not_found", `no held message ${id}`);
1294
- const params = JSON.parse(row.params);
1295
- const verdict = JSON.parse(row.verdict);
1296
- let sent = params;
1297
- let redacted;
1298
- if (options.redacted) {
1299
- const applied = applyRedactions(params.body, verdict.redactions);
1300
- if (!applied.n) {
1301
- this.o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE id = ?`, id);
1302
- throw new RelayError3("invalid_params", `hold ${id} has no redactions: release it as it is or refuse it`);
1303
- }
1304
- sent = { ...params, body: applied.text };
1305
- redacted = { n: applied.n, kinds: applied.kinds };
1306
- }
1307
- let result;
1308
- try {
1309
- result = await deliver(row.from_ep, sent, JSON.parse(row.origin));
1310
- } catch (error) {
1311
- this.o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE id = ?`, id);
1312
- throw error;
1313
- }
1314
- this.remember(row.from_ep, params, { ...result, ...redacted ? { redacted } : {} });
1315
- this.resolve(row, "released", options.by);
1316
- this.o.core.auditJudge("judge_override", row.from_ep, id, {
1317
- ...this.storedDetail(row, verdict),
1318
- by: options.by,
1319
- action: redacted ? "release_redacted" : "release",
1320
- ...redacted ? { redacted } : {}
1321
- });
1322
- this.tell(
1323
- row.from_ep,
1324
- `relay: your message held for review as ${id} was released by your human${redacted ? " with redactions" : ""} and delivered as ${result.id}`
1325
- );
1326
- return { ...result, ...redacted ? { redacted } : {} };
1327
- }
1328
- /** refuses a held message: it is never delivered, and its sender hears the human's reason */
1329
- refuse(id, reason, by) {
1330
- this.expire(this.o.now());
1331
- const row = one(this.o.sql, `SELECT * FROM judge_holds WHERE id = ? AND state = 'held'`, id);
1332
- if (!row) throw new RelayError3("not_found", `no held message ${id}`);
1333
- const verdict = JSON.parse(row.verdict);
1334
- this.resolve(row, "refused", by);
1335
- this.o.core.auditJudge("judge_override", row.from_ep, id, { ...this.storedDetail(row, verdict), by, action: "refuse", reason });
1336
- this.tell(row.from_ep, `relay: your message held for review as ${id} was refused by your human: ${reason}`);
1337
- return this.view(this.row(id));
1338
- }
1339
- /** expires holds past their message TTL (body purged, sender told) and drops resolved ones past the audit window */
1340
- sweep() {
1341
- const now = this.o.now();
1342
- this.expire(now);
1343
- this.o.sql.all(`DELETE FROM judge_holds WHERE state IN ('released', 'refused', 'expired') AND resolved_at <= ?`, now - LIMITS.auditKeepS * 1e3);
1344
- this.o.sql.all(`DELETE FROM judge_sent WHERE created_at <= ?`, now - LIMITS.idempotencyS * 1e3);
1345
- for (const [key, times] of this.spent) if (!times.length || times[times.length - 1] <= now - 36e5) this.spent.delete(key);
1346
- }
1347
- expire(now) {
1348
- for (const row of this.o.sql.all(`SELECT * FROM judge_holds WHERE state = 'held' AND expires_at <= ?`, now)) {
1349
- const verdict = JSON.parse(row.verdict);
1350
- this.resolve(row, "expired", null);
1351
- this.o.core.auditJudge("judge_expire", row.from_ep, row.id, this.storedDetail(row, verdict));
1352
- this.tell(row.from_ep, `relay: your message held for review as ${row.id} expired without review and was not delivered`);
1353
- }
1354
- }
1355
- /** the recipients by the boundary their message crosses; throws as send does for a target the sender cannot reach */
1356
- boundaries(policy, from, params, origin) {
1357
- const { sender, recipients, tree } = this.o.core.judgeSubjects(from, params, origin);
1358
- const groups = /* @__PURE__ */ new Map();
1359
- for (const recipient of recipients) {
1360
- const label = judgeBoundary(policy, sender, recipient);
1361
- if (label !== null) groups.set(label, [...groups.get(label) ?? [], recipient.address]);
1362
- }
1363
- return { groups, tree };
1364
- }
1365
- async judge(policy, from, body, groups) {
1366
- const verdicts = await Promise.all(
1367
- [...groups].map(
1368
- ([boundary, to]) => runJudge({
1369
- body,
1370
- side: "local",
1371
- boundary,
1372
- configVersion: this.version,
1373
- client: this.client,
1374
- cache: this.cache,
1375
- timeoutMs: policy.timeout_s * 1e3,
1376
- failMode: policy.fail,
1377
- ...this.client && policy.model ? { model: policy.model.id } : {},
1378
- from: this.o.core.addressOf(from),
1379
- to,
1380
- now: this.o.now,
1381
- stats: this.stats
1382
- })
1383
- )
1384
- );
1385
- return mergeVerdicts(verdicts);
1386
- }
1387
- shadow(policy, from, params, origin, ref) {
1388
- const task = (async () => {
1389
- try {
1390
- const { groups, tree } = this.boundaries(policy, from, params, origin);
1391
- if (!groups.size) return;
1392
- this.spend(policy, from, tree);
1393
- const body = params.body;
1394
- const verdict = await this.judge(policy, from, body, groups);
1395
- this.o.core.auditJudge("judge_shadow", from, ref, this.detail(verdict, body, groups));
1396
- } catch (error) {
1397
- this.o.log(`relay broker: a shadow judgement failed: ${error instanceof Error ? error.message : String(error)}`);
1398
- }
1399
- })();
1400
- this.shadows.add(task);
1401
- void task.finally(() => this.shadows.delete(task));
1402
- }
1403
- hold(from, params, origin, verdict, groups, detail) {
1404
- const now = this.o.now();
1405
- const id = newId2("hold", now);
1406
- const stored = { ...params, client_id: params.client_id ?? id };
1407
- const to = [...groups.values()].flat();
1408
- const record = { ...verdict, boundary: [...groups.keys()].join(" | "), to };
1409
- this.o.sql.all(
1410
- `INSERT INTO judge_holds (id, from_ep, params, origin, verdict, created_at, expires_at, state) VALUES (?, ?, ?, ?, ?, ?, ?, 'held')`,
1411
- id,
1412
- from,
1413
- JSON.stringify(stored),
1414
- JSON.stringify(origin),
1415
- JSON.stringify(record),
1416
- now,
1417
- now + (params.ttl_s ?? LIMITS.ttlDefaultS) * 1e3
1418
- );
1419
- this.o.core.auditJudge("judge_hold", from, id, detail);
1420
- return heldResult(id, params);
1421
- }
1422
- detail(verdict, body, groups) {
1423
- return {
1424
- side: "local",
1425
- boundary: [...groups.keys()].join(" | "),
1426
- prompt_version: verdict.prompt_version,
1427
- categories: verdict.categories,
1428
- sha256: sha256Hex(body),
1429
- verdict: verdict.verdict,
1430
- source: verdict.source,
1431
- reason: verdict.reason,
1432
- to: [...groups.values()].flat(),
1433
- ...verdict.model ? { model: verdict.model } : {},
1434
- ...verdict.ms !== void 0 ? { ms: verdict.ms } : {}
1435
- };
1436
- }
1437
- storedDetail(row, verdict) {
1438
- const params = JSON.parse(row.params);
1439
- return {
1440
- side: "local",
1441
- boundary: verdict.boundary,
1442
- prompt_version: verdict.prompt_version,
1443
- categories: verdict.categories,
1444
- sha256: params.sha256 ?? sha256Hex(params.body ?? ""),
1445
- verdict: verdict.verdict,
1446
- to: verdict.to,
1447
- ...verdict.model ? { model: verdict.model } : {}
1448
- };
1075
+ import { JudgeGate as HubJudgeGate } from "@scp-dev/hub";
1076
+ function readKeyFile(file) {
1077
+ try {
1078
+ accessSync(file, constants.R_OK);
1079
+ } catch {
1080
+ return null;
1449
1081
  }
1450
- /** marks a hold resolved and purges its body and the judge's quotes of it */
1451
- resolve(row, state, by) {
1452
- const { body, ...params } = JSON.parse(row.params);
1453
- const verdict = JSON.parse(row.verdict);
1454
- const purged = { ...params, bytes: new TextEncoder().encode(body).length, sha256: sha256Hex(body) };
1455
- const kinds = { ...verdict, redactions: verdict.redactions.map((r) => ({ kind: r.kind })) };
1456
- this.o.sql.all(
1457
- `UPDATE judge_holds SET state = ?, by = ?, resolved_at = ?, params = ?, verdict = ? WHERE id = ?`,
1458
- state,
1459
- by,
1460
- this.o.now(),
1461
- JSON.stringify(purged),
1462
- JSON.stringify(kinds),
1463
- row.id
1464
- );
1465
- }
1466
- tell(endpoint, text) {
1467
- try {
1468
- this.o.core.judgeNotice(endpoint, text);
1469
- this.o.wake(endpoint);
1470
- } catch (error) {
1471
- if (!(error instanceof RelayError3)) throw error;
1472
- }
1082
+ return async () => readFileSync4(file, "utf8").trim();
1083
+ }
1084
+ var JudgeGate = class extends HubJudgeGate {
1085
+ constructor(options) {
1086
+ super({ keyFile: readKeyFile, ...options });
1473
1087
  }
1474
- row(id) {
1475
- const row = one(this.o.sql, `SELECT * FROM judge_holds WHERE id = ?`, id);
1476
- if (!row) throw new RelayError3("not_found", `no held message ${id}`);
1477
- return row;
1088
+ };
1089
+
1090
+ // src/sessions.ts
1091
+ import { createHash as createHash2, randomUUID } from "crypto";
1092
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync, writeFileSync } from "fs";
1093
+ import { join as join3 } from "path";
1094
+ function markerPath(dir, session) {
1095
+ return join3(dir, "sessions", `${createHash2("sha256").update(session).digest("hex").slice(0, 32)}.json`);
1096
+ }
1097
+ function readSessionMarker(dir, session) {
1098
+ try {
1099
+ const value = JSON.parse(readFileSync5(markerPath(dir, session), "utf8"));
1100
+ if (typeof value?.joined !== "boolean") return void 0;
1101
+ return { joined: value.joined, ...typeof value.handle === "string" ? { handle: value.handle } : {} };
1102
+ } catch {
1103
+ return void 0;
1478
1104
  }
1479
- view(row) {
1480
- const params = JSON.parse(row.params);
1481
- const verdict = JSON.parse(row.verdict);
1482
- return {
1483
- id: row.id,
1484
- from: this.o.core.addressOf(row.from_ep),
1485
- to: verdict.to,
1486
- state: row.state,
1487
- created_at: iso(row.created_at),
1488
- expires_at: iso(row.expires_at),
1489
- categories: verdict.categories,
1490
- reason: verdict.reason,
1491
- boundary: verdict.boundary,
1492
- source: verdict.source,
1493
- bytes: params.bytes ?? new TextEncoder().encode(params.body ?? "").length,
1494
- by: row.by,
1495
- resolved_at: iso(row.resolved_at)
1496
- };
1105
+ }
1106
+ function writeSessionMarker(dir, session, marker) {
1107
+ const path = markerPath(dir, session);
1108
+ mkdirSync2(join3(dir, "sessions"), { recursive: true, mode: 448 });
1109
+ const temp = `${path}.${randomUUID()}.tmp`;
1110
+ try {
1111
+ writeFileSync(temp, JSON.stringify(marker), { mode: 384 });
1112
+ renameSync2(temp, path);
1113
+ } finally {
1114
+ rmSync(temp, { force: true });
1497
1115
  }
1498
- };
1499
- var fingerprint = (p) => sha256Hex(
1500
- JSON.stringify([
1501
- [p.to].flat().map(String).sort(),
1502
- p.body,
1503
- p.kind ?? "message",
1504
- p.thread ?? null,
1505
- p.reply_to ?? null,
1506
- p.data_class ?? "public",
1507
- (p.attachments ?? []).map((a) => a?.path).sort(),
1508
- p.mime ?? null,
1509
- p.ttl_s ?? LIMITS.ttlDefaultS,
1510
- p.require_reply ?? null
1511
- ])
1512
- );
1513
- function heldResult(id, params) {
1514
- return { id, thread: params.thread ?? "", hop: 0, deliveries: [], held: { hold_id: id, side: "local" } };
1515
1116
  }
1516
1117
 
1517
1118
  // src/sqlite.ts
@@ -1609,7 +1210,7 @@ var SCOPE_METHODS = /* @__PURE__ */ new Set([
1609
1210
  var CHANGES_SCOPE = /* @__PURE__ */ new Set(["rules_set", "rules_widen", "rules_lift", "group"]);
1610
1211
  var ASK_RECHECK_MS = 1e3;
1611
1212
  var STOP_RECHECK_MS = 1e3;
1612
- var TRANSIENT_METHODS = /* @__PURE__ */ new Set(["event", "status", "whoami", "inject", "injected", "peek_turn", "stop_wait", "prompted"]);
1213
+ var TRANSIENT_METHODS = /* @__PURE__ */ new Set(["event", "status", "whoami", "inject", "injected", "peek_turn", "stop_wait", "prompted", "title_infer"]);
1613
1214
  var GRANT_UNBOUND = `a spawn-env grant binds only a new harness process: run the harness with it, or name the child (${CLI_NAME} spawn-env --name / --as)`;
1614
1215
  function workerResults(result) {
1615
1216
  return result.messages.length - (result.others_shown ?? 0) + result.more;
@@ -1631,7 +1232,7 @@ function hookOf(s) {
1631
1232
  }
1632
1233
  async function startBroker(options) {
1633
1234
  const dir = privateDir(options.dir);
1634
- const lock = takeBrokerLock(join3(dir, "broker.lock"));
1235
+ const lock = takeBrokerLock(join4(dir, "broker.lock"));
1635
1236
  const socket = socketPath(dir);
1636
1237
  const connections = /* @__PURE__ */ new Set();
1637
1238
  const waiters = /* @__PURE__ */ new Map();
@@ -1659,16 +1260,16 @@ async function startBroker(options) {
1659
1260
  let listener;
1660
1261
  try {
1661
1262
  privateDir(dirname2(socket));
1662
- db = openDatabase(join3(dir, "relay.db"));
1263
+ db = openDatabase(join4(dir, "relay.db"));
1663
1264
  db.exec("PRAGMA journal_mode = WAL");
1664
1265
  db.exec("PRAGMA busy_timeout = 5000");
1665
- chmodSync2(join3(dir, "relay.db"), 384);
1266
+ chmodSync2(join4(dir, "relay.db"), 384);
1666
1267
  const sql = db;
1667
1268
  const machine = options.machine ?? options.bridge?.machine;
1668
1269
  core = new FederatedCore(sql, {
1669
1270
  ...compact({ user: options.user, machine, now: options.now }),
1670
1271
  hostname: shortHostname(),
1671
- hash: (value) => createHash2("sha256").update(value).digest("hex"),
1272
+ hash: (value) => createHash3("sha256").update(value).digest("hex"),
1672
1273
  transaction: (fn) => db.transaction(fn),
1673
1274
  alive: running
1674
1275
  });
@@ -1724,6 +1325,7 @@ async function startBroker(options) {
1724
1325
  const known = /* @__PURE__ */ new Map();
1725
1326
  for (const view of localViews()) known.set(view.id, view.presence);
1726
1327
  const presenceTimer = setInterval(() => {
1328
+ core.endExited();
1727
1329
  const live = /* @__PURE__ */ new Set();
1728
1330
  for (const view of localViews()) {
1729
1331
  live.add(view.id);
@@ -1731,21 +1333,17 @@ async function startBroker(options) {
1731
1333
  }
1732
1334
  for (const id of known.keys()) if (!live.has(id)) known.delete(id);
1733
1335
  }, options.presenceEveryMs ?? PRESENCE_MS);
1734
- if (options.bridge) {
1735
- bridge = startBridge({
1736
- ...options.bridge,
1737
- core,
1738
- onRemote: {
1739
- wake,
1740
- fleet: notifyFleet,
1741
- policyError: () => {
1742
- loadPolicy();
1743
- return policyError;
1744
- },
1745
- log
1746
- }
1747
- });
1748
- }
1336
+ const remoteHooks = {
1337
+ wake,
1338
+ fleet: notifyFleet,
1339
+ policyError: () => {
1340
+ loadPolicy();
1341
+ return policyError;
1342
+ },
1343
+ log
1344
+ };
1345
+ if (options.bridge) bridge = startBridge({ ...options.bridge, core, onRemote: remoteHooks });
1346
+ else if (options.link) bridge = options.link({ core, hooks: remoteHooks, send: (from, params, origin) => judge.send(from, params, origin, deliver) });
1749
1347
  async function relink() {
1750
1348
  const config = options.bridgeConfig ? await options.bridgeConfig() : options.bridge;
1751
1349
  if (!config || bridge && config.url === options.bridge?.url && config.machine === options.bridge?.machine) {
@@ -1763,7 +1361,7 @@ async function startBroker(options) {
1763
1361
  function loadPolicy() {
1764
1362
  let text;
1765
1363
  try {
1766
- text = readFileSync5(policyFile, "utf8");
1364
+ text = readFileSync6(policyFile, "utf8");
1767
1365
  } catch (error) {
1768
1366
  const code = error.code;
1769
1367
  const dangling = code === "ENOENT" && lstatSync2(policyFile, { throwIfNoEntry: false }) !== void 0;
@@ -1828,6 +1426,20 @@ async function startBroker(options) {
1828
1426
  notifyFleet({ type: "presence", endpoint: view.address, presence: view.presence, harness: view.harness, labels: view.labels });
1829
1427
  bridge?.changed(view.id);
1830
1428
  }
1429
+ function membership(view, session, joined, was) {
1430
+ const handle = view.address.slice(view.address.lastIndexOf("/") + 1);
1431
+ if (session && !session.startsWith("sub:")) {
1432
+ try {
1433
+ writeSessionMarker(dir, session, { joined, handle });
1434
+ } catch (error) {
1435
+ log(`relay broker: the session marker was not written: ${error instanceof Error ? error.message : String(error)}`);
1436
+ }
1437
+ }
1438
+ known.set(view.id, view.presence);
1439
+ notifyFleet({ type: "presence", endpoint: view.address, presence: view.presence, harness: view.harness, labels: view.labels });
1440
+ if (was !== void 0 && was !== handle) bridge?.renamed(view.id, was);
1441
+ else bridge?.changed(view.id);
1442
+ }
1831
1443
  function notifyFleet(event) {
1832
1444
  const at = new Date((options.now ?? Date.now)()).toISOString();
1833
1445
  let line;
@@ -1887,7 +1499,7 @@ async function startBroker(options) {
1887
1499
  endpoint: core.whoami(endpoint),
1888
1500
  capabilities: BROKER_CAPABILITIES,
1889
1501
  server_time: (/* @__PURE__ */ new Date()).toISOString(),
1890
- limits: LIMITS2,
1502
+ limits: LIMITS,
1891
1503
  preamble: PREAMBLE,
1892
1504
  broker: brokerInfo()
1893
1505
  };
@@ -1917,16 +1529,16 @@ async function startBroker(options) {
1917
1529
  }
1918
1530
  async function gather(me, result, peek, s) {
1919
1531
  const gathering = (r) => workerResults(r) > 0 && !r.more && !r.prompted && othersRunning(r) > 0;
1920
- const cap = Date.now() + LIMITS2.pushGraceS * 1e3;
1921
- let quiet = Date.now() + LIMITS2.pushQuietS * 1e3;
1532
+ const cap = Date.now() + LIMITS.pushGraceS * 1e3;
1533
+ let quiet = Date.now() + LIMITS.pushQuietS * 1e3;
1922
1534
  let results = workerResults(result);
1923
1535
  while (gathering(result) && !s.data.closed && Date.now() < Math.min(quiet, cap)) {
1924
1536
  await waitFor(me, Math.min(quiet, cap) - Date.now(), s);
1925
- if (closing) throw new RelayError4("unauthorized", "the relay broker is stopping");
1537
+ if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
1926
1538
  result = core.stopWait(me, peek);
1927
1539
  if (workerResults(result) > results) {
1928
1540
  results = workerResults(result);
1929
- quiet = Date.now() + LIMITS2.pushQuietS * 1e3;
1541
+ quiet = Date.now() + LIMITS.pushQuietS * 1e3;
1930
1542
  }
1931
1543
  }
1932
1544
  }
@@ -2020,7 +1632,7 @@ async function startBroker(options) {
2020
1632
  if (hasId) write(s, { jsonrpc: "2.0", id, result });
2021
1633
  } catch (error) {
2022
1634
  if (!hasId || closing) return;
2023
- if (error instanceof RelayError4) {
1635
+ if (error instanceof RelayError3) {
2024
1636
  const code = error.code === "method_not_found" ? -32601 : error.code === "invalid_params" ? -32602 : -32e3;
2025
1637
  write(s, {
2026
1638
  jsonrpc: "2.0",
@@ -2041,59 +1653,70 @@ async function startBroker(options) {
2041
1653
  }
2042
1654
  }
2043
1655
  function endpointOf(s) {
2044
- if (closing) throw new RelayError4("unauthorized", "the relay broker is stopping");
1656
+ if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
2045
1657
  if (!s.data.endpoint)
2046
- throw new RelayError4("unauthorized", s.data.observer ? "this connection only observes: say hello as an agent or the human" : "call hello first");
1658
+ throw new RelayError3("unauthorized", s.data.observer ? "this connection only observes: say hello as an agent or the human" : "call hello first");
2047
1659
  core.touch(s.data.endpoint);
2048
1660
  return s.data.endpoint;
2049
1661
  }
2050
1662
  function waitSeconds(value) {
2051
1663
  if (value === void 0) return 0;
2052
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new RelayError4("invalid_params", "wait_s must be a number of seconds");
2053
- return Math.min(value, LIMITS2.waitMaxS) * 1e3;
1664
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new RelayError3("invalid_params", "wait_s must be a number of seconds");
1665
+ return Math.min(value, LIMITS.waitMaxS) * 1e3;
2054
1666
  }
2055
1667
  function checkAttachments(attachments, s) {
2056
1668
  if (attachments === void 0) return void 0;
2057
- if (!Array.isArray(attachments)) throw new RelayError4("invalid_params", "attachments must be a list");
1669
+ if (!Array.isArray(attachments)) throw new RelayError3("invalid_params", "attachments must be a list");
2058
1670
  const tooWide = /* @__PURE__ */ new Set(["/", realOrSelf(homedir2()), realOrSelf(accountHome())]);
2059
1671
  const roots = [s.data.cwd, ...options.roots ?? []].filter((r) => typeof r === "string" && isAbsolute(r)).map(realOrSelf).filter((r) => !tooWide.has(r));
2060
1672
  return attachments.map((attachment) => {
2061
1673
  if (attachment?.kind !== "path" || typeof attachment.path !== "string" || !isAbsolute(attachment.path)) {
2062
- throw new RelayError4("invalid_params", "an attachment is {kind: path, path: an absolute path}");
1674
+ throw new RelayError3("invalid_params", "an attachment is {kind: path, path: an absolute path}");
2063
1675
  }
2064
1676
  let real;
2065
1677
  try {
2066
1678
  real = realpathSync(attachment.path);
2067
1679
  } catch {
2068
- throw new RelayError4("not_found", `no file at ${attachment.path}`);
1680
+ throw new RelayError3("not_found", `no file at ${attachment.path}`);
2069
1681
  }
2070
1682
  if (!roots.some((root) => real === root || real.startsWith(root.endsWith(sep) ? root : root + sep))) {
2071
- throw new RelayError4("invalid_params", `${attachment.path} is outside the sender's working directory and the relay's allowed roots`);
1683
+ throw new RelayError3("invalid_params", `${attachment.path} is outside the sender's working directory and the relay's allowed roots`);
2072
1684
  }
2073
1685
  return { kind: "path", path: real };
2074
1686
  });
2075
1687
  }
2076
1688
  async function dispatch(s, method, params) {
2077
1689
  if (method === "hello") {
2078
- if (s.data.endpoint || s.data.observer) throw new RelayError4("invalid_params", "hello was already called on this connection");
2079
- if (params.protocol !== PROTOCOL3) throw new RelayError4("invalid_params", `this broker speaks ${PROTOCOL3}`);
1690
+ if (s.data.endpoint || s.data.observer) throw new RelayError3("invalid_params", "hello was already called on this connection");
1691
+ if (params.protocol !== PROTOCOL3) throw new RelayError3("invalid_params", `this broker speaks ${PROTOCOL3}`);
2080
1692
  if (params.transient === true) {
2081
1693
  if (params.human === true || params.observe === true || params.resume !== void 0) {
2082
- throw new RelayError4("invalid_params", "a hook connection claims an agent endpoint: transient does not go with human, observe or resume");
1694
+ throw new RelayError3("invalid_params", "a hook connection claims an agent endpoint: transient does not go with human, observe or resume");
2083
1695
  }
2084
- const before2 = new Map(core.list({ all: true }).map((view) => [view.id, view.presence]));
2085
- const { endpoint: endpoint2, subagent, created } = core.claim(params.claim ?? {}, { transient: true });
1696
+ const join5 = params.join;
1697
+ if (join5 !== void 0 && (typeof join5 !== "object" || join5 === null || Array.isArray(join5)))
1698
+ throw new RelayError3("invalid_params", "join is {handle?, under?}");
1699
+ const under = join5?.under;
1700
+ const before2 = new Map(core.list({ all: true }).map((view) => [view.id, view]));
1701
+ const { endpoint: endpoint2, subagent, created } = join5 === void 0 ? core.claim(params.claim ?? {}, { transient: true }) : {
1702
+ endpoint: core.joinSession(params.claim ?? {}, join5.handle, under),
1703
+ subagent: void 0,
1704
+ created: void 0
1705
+ };
2086
1706
  s.data.endpoint = endpoint2.id;
2087
1707
  s.data.transient = true;
2088
1708
  const key = params.claim?.process;
2089
1709
  if (typeof key === "string") s.data.process = key;
2090
- if ((before2.get(endpoint2.id) ?? "offline") !== endpoint2.presence) presenceChanged(endpoint2);
1710
+ const was2 = before2.get(endpoint2.id)?.address;
1711
+ if (join5 !== void 0) membership(endpoint2, core.sessionOf(endpoint2.id), true, was2?.slice(was2.lastIndexOf("/") + 1));
1712
+ if (under !== void 0 && before2.get(endpoint2.id)?.parent !== endpoint2.parent) bridge?.resync();
1713
+ else if ((before2.get(endpoint2.id)?.presence ?? "offline") !== endpoint2.presence) presenceChanged(endpoint2);
2091
1714
  return {
2092
1715
  endpoint: endpoint2,
2093
1716
  ...subagent ? { subagent, ...created ? { created } : {} } : {},
2094
1717
  capabilities: BROKER_CAPABILITIES,
2095
1718
  server_time: (/* @__PURE__ */ new Date()).toISOString(),
2096
- limits: LIMITS2,
1719
+ limits: LIMITS,
2097
1720
  preamble: PREAMBLE,
2098
1721
  broker: brokerInfo()
2099
1722
  };
@@ -2101,7 +1724,7 @@ async function startBroker(options) {
2101
1724
  if (params.observe === true) {
2102
1725
  const self = params.self === void 0 ? null : core.placeLineage(params.self);
2103
1726
  if (params.grant !== void 0) {
2104
- if (!core.grantBinds(params.grant, self)) throw new RelayError4("unauthorized", GRANT_UNBOUND);
1727
+ if (!core.grantBinds(params.grant, self)) throw new RelayError3("unauthorized", GRANT_UNBOUND);
2105
1728
  return { ...bindLineage(s, self, params, true), grant_bound: true };
2106
1729
  }
2107
1730
  if (self) return bindLineage(s, self, params, true);
@@ -2111,7 +1734,7 @@ async function startBroker(options) {
2111
1734
  endpoint: null,
2112
1735
  capabilities: BROKER_CAPABILITIES,
2113
1736
  server_time: (/* @__PURE__ */ new Date()).toISOString(),
2114
- limits: LIMITS2,
1737
+ limits: LIMITS,
2115
1738
  preamble: PREAMBLE,
2116
1739
  broker: brokerInfo()
2117
1740
  };
@@ -2119,7 +1742,10 @@ async function startBroker(options) {
2119
1742
  const claim = params.claim;
2120
1743
  if (params.human === true && params.resume === void 0 && params.lineage !== void 0) {
2121
1744
  const placed = core.placeLineage(params.lineage);
2122
- if (placed) return bindLineage(s, placed, params);
1745
+ if (placed) {
1746
+ s.data.terminal = params.tty === true;
1747
+ return bindLineage(s, placed, params);
1748
+ }
2123
1749
  } else if (params.human !== true && params.resume === void 0 && claim?.process === void 0 && typeof claim?.handle === "string") {
2124
1750
  const placed = claim.lineage === void 0 ? null : core.placeLineage(claim.lineage, claim.handle);
2125
1751
  if (placed && (claim.grant === void 0 || core.grantBinds(claim.grant, placed))) {
@@ -2154,17 +1780,17 @@ async function startBroker(options) {
2154
1780
  ...token ? { token } : {},
2155
1781
  capabilities: BROKER_CAPABILITIES,
2156
1782
  server_time: (/* @__PURE__ */ new Date()).toISOString(),
2157
- limits: LIMITS2,
1783
+ limits: LIMITS,
2158
1784
  preamble: PREAMBLE,
2159
1785
  broker: brokerInfo()
2160
1786
  };
2161
1787
  }
2162
- if (closing) throw new RelayError4("unauthorized", "the relay broker is stopping");
1788
+ if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
2163
1789
  if (s.data.observer && OBSERVER_METHODS.has(method)) return observe(s, method, params);
2164
1790
  if (s.data.observer && SCOPE_METHODS.has(method) && (s.data.lineage || method === "why")) return scope(s, method, params);
2165
- if (s.data.transient && SCOPE_METHODS.has(method)) throw new RelayError4("scope_refused", "a hook connection cannot read or change scopes, groups or grants");
1791
+ if (s.data.transient && SCOPE_METHODS.has(method)) throw new RelayError3("scope_refused", "a hook connection cannot read or change scopes, groups or grants");
2166
1792
  const me = endpointOf(s);
2167
- if (s.data.transient && !TRANSIENT_METHODS.has(method)) throw new RelayError4("unauthorized", "a hook connection only posts activity and status");
1793
+ if (s.data.transient && !TRANSIENT_METHODS.has(method)) throw new RelayError3("unauthorized", "a hook connection only posts activity and status");
2168
1794
  if (OBSERVER_METHODS.has(method)) return observe(s, method, params);
2169
1795
  if (SCOPE_METHODS.has(method)) return scope(s, method, params);
2170
1796
  switch (method) {
@@ -2172,13 +1798,13 @@ async function startBroker(options) {
2172
1798
  return s.data.transient ? core.whoami(me) : { ...core.whoami(me), ...core.peekState(me) };
2173
1799
  case "peek_turn":
2174
1800
  if (!s.data.transient || !core.hookOwns(me, s.data.process))
2175
- throw new RelayError4("unauthorized", "only a hook of the endpoint's own harness process posts digest turns");
1801
+ throw new RelayError3("unauthorized", "only a hook of the endpoint's own harness process posts digest turns");
2176
1802
  return core.peekTurn(me, { role: params.role, text: params.text });
2177
1803
  case "send":
2178
1804
  case "ask": {
2179
- for (const field of SERVER_FIELDS) if (field in params) throw new RelayError4("invalid_params", `${field} is set by the relay, not the sender`);
1805
+ for (const field of SERVER_FIELDS) if (field in params) throw new RelayError3("invalid_params", `${field} is set by the relay, not the sender`);
2180
1806
  loadPolicy();
2181
- if (policyError) throw new RelayError4("policy_refused", policyError);
1807
+ if (policyError) throw new RelayError3("policy_refused", policyError);
2182
1808
  const attachments = checkAttachments(params.attachments, s);
2183
1809
  const send = { ...params, ...attachments ? { attachments } : {} };
2184
1810
  if (method === "ask") send.kind = "request";
@@ -2194,16 +1820,16 @@ async function startBroker(options) {
2194
1820
  case "hold_release": {
2195
1821
  const id = required(reviewer(s, params), "id");
2196
1822
  loadPolicy();
2197
- if (policyError) throw new RelayError4("policy_refused", policyError);
1823
+ if (policyError) throw new RelayError3("policy_refused", policyError);
2198
1824
  return judge.release(id, { redacted: params.redacted === true, by: core.addressOf(me) }, deliver);
2199
1825
  }
2200
1826
  case "hold_refuse": {
2201
1827
  const reason = required(reviewer(s, params), "reason");
2202
- if (reason.length > LIMITS2.noteChars) throw new RelayError4("invalid_params", `reason takes at most ${LIMITS2.noteChars} characters`);
1828
+ if (reason.length > LIMITS.noteChars) throw new RelayError3("invalid_params", `reason takes at most ${LIMITS.noteChars} characters`);
2203
1829
  return judge.refuse(required(params, "id"), stripControl(reason), core.addressOf(me));
2204
1830
  }
2205
1831
  case "ask_wait": {
2206
- if (typeof params.id !== "string") throw new RelayError4("invalid_params", "id is the request's message id");
1832
+ if (typeof params.id !== "string") throw new RelayError3("invalid_params", "id is the request's message id");
2207
1833
  return awaitReply(s, me, params.id, waitSeconds(params.wait_s));
2208
1834
  }
2209
1835
  case "inbox": {
@@ -2213,7 +1839,7 @@ async function startBroker(options) {
2213
1839
  let page = read();
2214
1840
  while (!page.messages.length && !s.data.closed && Date.now() < deadline) {
2215
1841
  await waitFor(me, deadline - Date.now(), s);
2216
- if (closing) throw new RelayError4("unauthorized", "the relay broker is stopping");
1842
+ if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
2217
1843
  page = read();
2218
1844
  }
2219
1845
  const { messages } = page;
@@ -2226,6 +1852,37 @@ async function startBroker(options) {
2226
1852
  bridge?.changed(me);
2227
1853
  return view;
2228
1854
  }
1855
+ case "title_infer": {
1856
+ const changed = core.inferTitle(me, params, hookOf(s));
1857
+ if (changed) bridge?.changed(me);
1858
+ return { changed };
1859
+ }
1860
+ case "rename": {
1861
+ const renamed = core.rename(actorOf(s), { ...params.endpoint !== void 0 ? { endpoint: params.endpoint } : {}, handle: params.handle });
1862
+ membership(renamed.endpoint, renamed.session, true, renamed.from);
1863
+ return renamed.endpoint;
1864
+ }
1865
+ case "adopt": {
1866
+ const actor = actorOf(s);
1867
+ const adopted = core.adopt(
1868
+ actor,
1869
+ { endpoint: params.endpoint, ...params.parent !== void 0 ? { parent: params.parent } : {} },
1870
+ { tty: actor.kind === "endpoint" && s.data.terminal === true }
1871
+ );
1872
+ bridge?.resync();
1873
+ return adopted;
1874
+ }
1875
+ case "unlink": {
1876
+ const unlinked = core.unlink(actorOf(s), params.endpoint !== void 0 ? { endpoint: params.endpoint } : {});
1877
+ bridge?.resync();
1878
+ return unlinked;
1879
+ }
1880
+ case "leave": {
1881
+ const left = core.leaveSession(actorOf(s), params.endpoint !== void 0 ? { endpoint: params.endpoint } : {});
1882
+ for (const c of connections) if (c.data.endpoint === left.endpoint.id && !c.data.transient && !c.data.lineage && c !== s) c.end();
1883
+ membership(left.endpoint, left.session, false);
1884
+ return left.endpoint;
1885
+ }
2229
1886
  case "status": {
2230
1887
  core.status(me, params.state, params.note);
2231
1888
  const view = core.whoami(me);
@@ -2241,14 +1898,25 @@ async function startBroker(options) {
2241
1898
  if ((known.get(me) ?? "offline") !== after.presence) presenceChanged(after);
2242
1899
  return row;
2243
1900
  }
2244
- case "inject":
2245
- return core.inject(me, {
1901
+ case "inject": {
1902
+ const waitMs = waitSeconds(params.wait_s);
1903
+ const options2 = {
2246
1904
  stop: params.stop === true,
2247
1905
  count: params.count === true,
2248
1906
  holdUnread: params.hold_unread === true,
2249
1907
  directives: params.directives === true,
1908
+ wake: waitMs > 0 && params.stop !== true && params.count !== true,
2250
1909
  ...hookOf(s)
2251
- });
1910
+ };
1911
+ const deadline = Date.now() + (options2.wake ? waitMs : 0);
1912
+ let result = core.inject(me, options2);
1913
+ while (!result.offered.length && !s.data.closed && Date.now() < deadline) {
1914
+ await waitFor(me, deadline - Date.now(), s);
1915
+ if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
1916
+ result = core.inject(me, options2);
1917
+ }
1918
+ return result;
1919
+ }
2252
1920
  case "stop_wait": {
2253
1921
  const waitMs = waitSeconds(params.wait_s);
2254
1922
  const deadline = Date.now() + waitMs;
@@ -2258,7 +1926,7 @@ async function startBroker(options) {
2258
1926
  const idle = (r) => !r.messages.length && !r.more && r.workers.length + r.starting > 0 && r.pushes_left > 0 && !r.prompted;
2259
1927
  while (idle(result) && !s.data.closed && Date.now() < deadline) {
2260
1928
  await waitFor(me, Math.min(STOP_RECHECK_MS, deadline - Date.now()), s);
2261
- if (closing) throw new RelayError4("unauthorized", "the relay broker is stopping");
1929
+ if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
2262
1930
  result = core.stopWait(me, peek);
2263
1931
  }
2264
1932
  if (waitMs > 0) await gather(me, result, peek, s);
@@ -2267,7 +1935,8 @@ async function startBroker(options) {
2267
1935
  case "prompted": {
2268
1936
  const prompted = core.prompted(me, hookOf(s));
2269
1937
  if (prompted) wake(me);
2270
- return { prompted };
1938
+ const tldr = prompted && typeof params.tldr_every === "number" && core.tldrDue(me, params.tldr_every);
1939
+ return { prompted, ...tldr ? { tldr } : {} };
2271
1940
  }
2272
1941
  case "injected":
2273
1942
  return {
@@ -2284,13 +1953,13 @@ async function startBroker(options) {
2284
1953
  case "receipts":
2285
1954
  return core.receipts(me, params.id);
2286
1955
  }
2287
- throw new RelayError4("method_not_found", `no method ${method}`);
1956
+ throw new RelayError3("method_not_found", `no method ${method}`);
2288
1957
  }
2289
1958
  const deliver = async (from, send, origin) => {
2290
1959
  let result;
2291
1960
  if (remoteSend(core, send.to)) {
2292
1961
  if (!bridge)
2293
- throw new RelayError4("not_found", `no endpoint ${[send.to].flat().join(", ")}: this broker is not linked to a hub (${CLI_NAME} cloud enable)`);
1962
+ throw new RelayError3("not_found", `no endpoint ${[send.to].flat().join(", ")}: this broker is not linked to a hub (${CLI_NAME} cloud enable)`);
2294
1963
  result = await bridge.forward(from, send, origin);
2295
1964
  } else {
2296
1965
  result = core.send(from, send, origin);
@@ -2310,7 +1979,7 @@ async function startBroker(options) {
2310
1979
  return result;
2311
1980
  };
2312
1981
  function reviewer(s, params) {
2313
- if (!atTerminal(s)) throw new RelayError4("unauthorized", "only your human at a terminal reviews held messages");
1982
+ if (!atTerminal(s)) throw new RelayError3("unauthorized", "only your human at a terminal reviews held messages");
2314
1983
  return params;
2315
1984
  }
2316
1985
  function observe(s, method, params) {
@@ -2318,7 +1987,7 @@ async function startBroker(options) {
2318
1987
  switch (method) {
2319
1988
  case "list":
2320
1989
  if (params.contacts === true) {
2321
- if (!bridge) throw new RelayError4("unavailable", `this broker is not linked to a hub (${CLI_NAME} cloud enable), so it reaches no contacts`);
1990
+ if (!bridge) throw new RelayError3("unavailable", `this broker is not linked to a hub (${CLI_NAME} cloud enable), so it reaches no contacts`);
2322
1991
  return bridge.contacts().then((contacts) => ({ contacts }));
2323
1992
  }
2324
1993
  return { endpoints: core.list({ ...params, viewer, peekable: viewer === void 0 }) };
@@ -2359,7 +2028,7 @@ async function startBroker(options) {
2359
2028
  case "bridge_relink":
2360
2029
  return relink();
2361
2030
  }
2362
- throw new RelayError4("method_not_found", `no method ${method}`);
2031
+ throw new RelayError3("method_not_found", `no method ${method}`);
2363
2032
  }
2364
2033
  function scope(s, method, params) {
2365
2034
  const result = scopeCall(s, method, params);
@@ -2438,7 +2107,7 @@ async function startBroker(options) {
2438
2107
  return result;
2439
2108
  }
2440
2109
  }
2441
- throw new RelayError4("method_not_found", `no method ${method}`);
2110
+ throw new RelayError3("method_not_found", `no method ${method}`);
2442
2111
  }
2443
2112
  function realOrSelf(path) {
2444
2113
  try {
@@ -2451,7 +2120,7 @@ async function startBroker(options) {
2451
2120
  const deadline = Date.now() + ms;
2452
2121
  const present = sent?.deliveries.filter((d) => !d.offline).map((d) => d.endpoint);
2453
2122
  for (; ; ) {
2454
- if (closing) throw new RelayError4("unauthorized", "the relay broker is stopping");
2123
+ if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
2455
2124
  const reply = core.replyTo(me, request);
2456
2125
  if (reply) {
2457
2126
  core.ack(me, [reply.seq]);
@@ -2482,7 +2151,7 @@ async function startBroker(options) {
2482
2151
  unlinkSync2(socket);
2483
2152
  } catch {
2484
2153
  }
2485
- if (socket !== join3(dir, "relay.sock")) {
2154
+ if (socket !== join4(dir, "relay.sock")) {
2486
2155
  try {
2487
2156
  rmdirSync(dirname2(socket));
2488
2157
  } catch {
@@ -2503,18 +2172,18 @@ function compact(fields) {
2503
2172
  function optional(params, key) {
2504
2173
  const value = params[key];
2505
2174
  if (value === void 0 || value === null) return void 0;
2506
- if (typeof value !== "string") throw new RelayError4("invalid_params", `${key} must be a string`);
2175
+ if (typeof value !== "string") throw new RelayError3("invalid_params", `${key} must be a string`);
2507
2176
  return value;
2508
2177
  }
2509
2178
  function required(params, key) {
2510
2179
  const value = optional(params, key);
2511
- if (!value) throw new RelayError4("invalid_params", `${key} is required`);
2180
+ if (!value) throw new RelayError3("invalid_params", `${key} is required`);
2512
2181
  return value;
2513
2182
  }
2514
2183
  function selectors(params, key) {
2515
2184
  const value = params[key];
2516
2185
  if (value === void 0 || value === null) return void 0;
2517
- if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) throw new RelayError4("invalid_params", `${key} must be a list of selectors`);
2186
+ if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) throw new RelayError3("invalid_params", `${key} must be a list of selectors`);
2518
2187
  return value;
2519
2188
  }
2520
2189
  export {
@@ -2527,11 +2196,13 @@ export {
2527
2196
  localHostName,
2528
2197
  policyPath,
2529
2198
  privateDir,
2199
+ readSessionMarker,
2530
2200
  relayDir,
2531
2201
  relaySocketUrl,
2532
2202
  shortHostname,
2533
2203
  socketPath,
2534
2204
  startBroker,
2535
2205
  systemHostname,
2536
- thisHostname
2206
+ thisHostname,
2207
+ writeSessionMarker
2537
2208
  };