pi-straw-boss 0.58.3 → 0.59.1
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/server/main.mjs +359 -41
- package/package.json +1 -1
- package/prompts/apps-config.md +6 -3
- package/prompts/integrator.md +18 -9
- package/prompts/planning.md +3 -3
- package/prompts/tower-run.md +1 -1
- package/src/link/dialogs-out.ts +180 -25
- package/src/link/holder.ts +30 -2
- package/src/link/scope.ts +14 -0
- package/src/link/service.ts +7 -0
- package/src/link/unitstatus.ts +15 -9
- package/src/server/allyport.ts +17 -1
- package/src/server/allyroute.ts +2 -0
- package/src/server/dispatcher.ts +43 -0
- package/src/server/guihost.ts +1 -1
- package/src/server/main.ts +8 -7
- package/src/server/revoke.ts +89 -0
- package/src/server/runs.ts +31 -7
- package/src/server/towers.ts +16 -3
package/dist/server/main.mjs
CHANGED
|
@@ -1923,9 +1923,25 @@ function allyPort(link) {
|
|
|
1923
1923
|
async taken(id, identity2) {
|
|
1924
1924
|
await link()?.holder?.taken(id, identity2);
|
|
1925
1925
|
},
|
|
1926
|
+
delivered(id, run3) {
|
|
1927
|
+
link()?.holder?.delivered(id, run3);
|
|
1928
|
+
},
|
|
1929
|
+
// A device is taken as gone only while the box can tell who is in the team: the policy file reads (a refused one answers `reply` for everybody, g346-F003) and the holder reads the roster. A team the unattended policy
|
|
1930
|
+
// no longer names has no holder run and never will: nobody is in it, so its controllers are gone (g346-F005b).
|
|
1931
|
+
known(team) {
|
|
1932
|
+
const service = link();
|
|
1933
|
+
if (!service) return false;
|
|
1934
|
+
const { mode, config } = service.policyLook();
|
|
1935
|
+
if (mode === "refused") return false;
|
|
1936
|
+
if (mode === "unattended" && !config.teams.has(team)) return service.holder?.active === true;
|
|
1937
|
+
return service.holder?.rosterKnown(team) ?? false;
|
|
1938
|
+
},
|
|
1926
1939
|
standing(device, team) {
|
|
1927
1940
|
const service = link();
|
|
1928
|
-
|
|
1941
|
+
if (!service) return void 0;
|
|
1942
|
+
const { mode, config } = service.policyLook();
|
|
1943
|
+
if (mode === "unattended" && !config.teams.has(team)) return void 0;
|
|
1944
|
+
return service.holder?.isMember(team, device) ? { level: service.levelOf(device) } : void 0;
|
|
1929
1945
|
},
|
|
1930
1946
|
project: (team) => link()?.config().teams.get(team)?.project,
|
|
1931
1947
|
thread: (device) => link()?.store.readThread(device).slice(-THREAD_ROWS).map((r) => ({ dir: r.dir, at: r.at, kind: r.kind, text: r.text })) ?? []
|
|
@@ -8017,7 +8033,12 @@ function createGuiHost(o) {
|
|
|
8017
8033
|
let forwardNet;
|
|
8018
8034
|
return {
|
|
8019
8035
|
/** The hub and a net reading that binds its dialogs, for the forwarder an unattended device runs. */
|
|
8020
|
-
dialogs: () => ({
|
|
8036
|
+
dialogs: () => ({
|
|
8037
|
+
hub: theHub(),
|
|
8038
|
+
read: async () => void await (forwardNet ??= netSource({ ...o.tower ? { services: (workspace) => o.tower.view(workspace) } : {}, run: cfg.run, world: cfg.world, sessions, hub: theHub() })).read(),
|
|
8039
|
+
/** The unit the hub files dialogs under `key` (known after a `read`): its pane for the dispatcher, its name and role for the label a controller is told. */
|
|
8040
|
+
lookup: (key2) => forwardNet?.lookup(key2)
|
|
8041
|
+
}),
|
|
8021
8042
|
/** At the server's start: the GUI on the recorded ports unless it was stopped on purpose (which only the boss's own command undoes). */
|
|
8022
8043
|
async start() {
|
|
8023
8044
|
theHub().track();
|
|
@@ -8411,12 +8432,23 @@ function decodePlaintext(kind, bytes) {
|
|
|
8411
8432
|
}
|
|
8412
8433
|
}
|
|
8413
8434
|
|
|
8435
|
+
// src/link/scope.ts
|
|
8436
|
+
var EVERYONE = "all";
|
|
8437
|
+
function reaches(scope, c, admin) {
|
|
8438
|
+
if (scope === "all") return true;
|
|
8439
|
+
if (admin(c.team, c.device) && (scope.team === void 0 || scope.team === c.team)) return true;
|
|
8440
|
+
return scope.device !== void 0 && scope.device === c.device && (scope.team === void 0 || scope.team === c.team);
|
|
8441
|
+
}
|
|
8442
|
+
|
|
8414
8443
|
// src/link/dialogs-out.ts
|
|
8415
8444
|
var MAX_OPEN2 = 32;
|
|
8416
8445
|
var MAX_SEEN = 1024;
|
|
8417
8446
|
var UNANSWERED_MS = 30 * 6e4;
|
|
8418
8447
|
var REFRESH_MS = 10 * 6e4;
|
|
8419
8448
|
var UNKNOWN_RECHECK_MS = 15e3;
|
|
8449
|
+
var PASS_STUCK_MS = 6e4;
|
|
8450
|
+
var CLOSE_TRIES = 5;
|
|
8451
|
+
var MAX_CLOSES = 256;
|
|
8420
8452
|
var DialogForwarder = class {
|
|
8421
8453
|
d;
|
|
8422
8454
|
open = /* @__PURE__ */ new Map();
|
|
@@ -8424,12 +8456,29 @@ var DialogForwarder = class {
|
|
|
8424
8456
|
seen = /* @__PURE__ */ new Set();
|
|
8425
8457
|
off;
|
|
8426
8458
|
polling;
|
|
8459
|
+
/** The closes not posted yet, per controller and dialog, drained by every pass until each is posted. */
|
|
8460
|
+
closes = /* @__PURE__ */ new Map();
|
|
8461
|
+
/** The passes let go as stuck that have not returned; at most one is outstanding, so a call that never returns costs one pending pass and not one a minute. */
|
|
8462
|
+
released = /* @__PURE__ */ new Set();
|
|
8463
|
+
/** The line that a second stuck pass is not let go was written for this stretch. */
|
|
8464
|
+
hung = false;
|
|
8427
8465
|
again = false;
|
|
8466
|
+
/** The pass that counts: a pass let go as stuck has an older number and stops at its next turn. */
|
|
8467
|
+
gen = 0;
|
|
8468
|
+
/** When the pass in flight began, and the call it waits at (`dialog-pass-stuck` names it). */
|
|
8469
|
+
passAt = 0;
|
|
8470
|
+
step = "idle";
|
|
8471
|
+
/** What the last `dialog-controllers` line said, so a line is written when it changes and not at every pass. */
|
|
8472
|
+
counted;
|
|
8473
|
+
/** The room for open questions was used up, and the line that says so was written. */
|
|
8474
|
+
full = false;
|
|
8428
8475
|
constructor(d) {
|
|
8429
8476
|
this.d = d;
|
|
8430
8477
|
}
|
|
8431
8478
|
start() {
|
|
8432
|
-
this.off
|
|
8479
|
+
if (this.off) return;
|
|
8480
|
+
this.off = this.d.hub.onChange(() => void this.poll());
|
|
8481
|
+
this.d.log({ event: "dialog-forwarder", state: "started" });
|
|
8433
8482
|
}
|
|
8434
8483
|
stop() {
|
|
8435
8484
|
this.off?.();
|
|
@@ -8439,25 +8488,69 @@ var DialogForwarder = class {
|
|
|
8439
8488
|
tick() {
|
|
8440
8489
|
return this.poll();
|
|
8441
8490
|
}
|
|
8442
|
-
/** Looks at the hub now; calls that overlap run once more after the one in flight. */
|
|
8491
|
+
/** Looks at the hub now; calls that overlap run once more after the one in flight, and a pass in flight for `PASS_STUCK_MS` is let go (its next look is a new pass). */
|
|
8443
8492
|
poll() {
|
|
8444
|
-
if (this.polling)
|
|
8493
|
+
if (this.polling) {
|
|
8494
|
+
if (this.now() - this.passAt < PASS_STUCK_MS) return this.again = true, this.polling;
|
|
8495
|
+
if (this.released.size) {
|
|
8496
|
+
if (!this.hung) this.d.log({ event: "dialog-pass-hung", step: this.step, ms: this.now() - this.passAt });
|
|
8497
|
+
this.hung = true;
|
|
8498
|
+
return this.again = true, this.polling;
|
|
8499
|
+
}
|
|
8500
|
+
this.d.log({ event: "dialog-pass-stuck", step: this.step, ms: this.now() - this.passAt });
|
|
8501
|
+
this.gen++;
|
|
8502
|
+
this.released.add(this.polling);
|
|
8503
|
+
this.polling = void 0;
|
|
8504
|
+
for (const d of this.open.values()) d.sent.clear();
|
|
8505
|
+
}
|
|
8506
|
+
const gen = this.gen;
|
|
8445
8507
|
const run3 = (async () => {
|
|
8446
8508
|
do {
|
|
8447
8509
|
this.again = false;
|
|
8510
|
+
this.passAt = this.now();
|
|
8448
8511
|
try {
|
|
8449
|
-
await this.pass();
|
|
8512
|
+
await this.pass(gen);
|
|
8450
8513
|
} catch (error) {
|
|
8451
|
-
this.d.log({ event: "dialog-forward-failed", error: String(error.message).slice(0, 200) });
|
|
8514
|
+
this.d.log({ event: "dialog-forward-failed", step: this.step, error: String(error.message).slice(0, 200) });
|
|
8452
8515
|
}
|
|
8453
|
-
} while (this.again);
|
|
8454
|
-
})().finally(() =>
|
|
8516
|
+
} while (this.again && gen === this.gen);
|
|
8517
|
+
})().finally(() => {
|
|
8518
|
+
if (this.polling === run3) this.polling = void 0;
|
|
8519
|
+
this.released.delete(run3);
|
|
8520
|
+
if (!this.released.size) this.hung = false;
|
|
8521
|
+
});
|
|
8455
8522
|
return this.polling = run3;
|
|
8456
8523
|
}
|
|
8457
|
-
|
|
8524
|
+
/** Whose unit asked: looked up until it is known, then kept. A lookup that fails leaves it unknown, which is the admins' alone. */
|
|
8525
|
+
async whose(d) {
|
|
8526
|
+
if (!this.d.dispatcher || d.who) return;
|
|
8527
|
+
d.who = await this.d.dispatcher(d.key).catch(() => void 0);
|
|
8528
|
+
}
|
|
8529
|
+
/** What the question's controllers are: every controller when the box cannot tell whose units are whose, else the dispatcher and the admins. */
|
|
8530
|
+
scopeOf(d) {
|
|
8531
|
+
return this.d.dispatcher ? d.who ? { device: d.who.device, ...d.who.team !== void 0 ? { team: d.who.team } : {} } : {} : EVERYONE;
|
|
8532
|
+
}
|
|
8533
|
+
/** Records where the pass in flight waits (`dialog-pass-stuck` names it); a pass let go as stuck no longer writes it. */
|
|
8534
|
+
at(gen, step) {
|
|
8535
|
+
if (gen === this.gen) this.step = step;
|
|
8536
|
+
}
|
|
8537
|
+
/** One line when the controllers the forwarder sees change in number or in whether the list is complete, so a box with nobody to tell says so once (and the first pass says how it starts). */
|
|
8538
|
+
count(controllers, known2) {
|
|
8539
|
+
const line2 = `${controllers}/${known2}`;
|
|
8540
|
+
if (line2 === this.counted) return;
|
|
8541
|
+
this.counted = line2;
|
|
8542
|
+
this.d.log({ event: "dialog-controllers", count: controllers, known: known2, open: this.open.size });
|
|
8543
|
+
}
|
|
8544
|
+
async pass(gen) {
|
|
8458
8545
|
const to = this.d.controllers();
|
|
8546
|
+
this.count(to.length, this.d.controllersKnown?.() ?? true);
|
|
8547
|
+
if (this.closes.size && !await this.drain(gen)) return;
|
|
8459
8548
|
if (!to.length && !this.open.size) return;
|
|
8460
|
-
if (to.length)
|
|
8549
|
+
if (to.length) {
|
|
8550
|
+
this.at(gen, "read");
|
|
8551
|
+
await this.d.read();
|
|
8552
|
+
if (gen !== this.gen) return;
|
|
8553
|
+
}
|
|
8461
8554
|
for (const o of to.length ? this.d.hub.answerable() : []) {
|
|
8462
8555
|
const id = o.target.answer.dialog;
|
|
8463
8556
|
if (this.open.has(id) || this.skipped.has(id)) continue;
|
|
@@ -8465,17 +8558,33 @@ var DialogForwarder = class {
|
|
|
8465
8558
|
if (Buffer.byteLength(text5) > LIMITS2.dialog) {
|
|
8466
8559
|
this.skipped.add(id);
|
|
8467
8560
|
this.d.log({ event: "dialog-oversize", dialog: id, bytes: Buffer.byteLength(text5) });
|
|
8468
|
-
|
|
8561
|
+
const unit = { key: o.key };
|
|
8562
|
+
this.at(gen, "dispatcher");
|
|
8563
|
+
await this.whose(unit);
|
|
8564
|
+
this.at(gen, "notice");
|
|
8565
|
+
await this.d.notice?.(`oversize/${id}`, { kind: "dialog-oversize", unit: o.key, text: `A question of ${this.labelOf(o.key, o.owner)} was not forwarded: it is ${Buffer.byteLength(text5)} bytes, over the ${LIMITS2.dialog} the link carries. It waits at the device.` }, this.scopeOf(unit));
|
|
8566
|
+
if (gen !== this.gen) return;
|
|
8567
|
+
continue;
|
|
8568
|
+
}
|
|
8569
|
+
if (this.open.size >= MAX_OPEN2) {
|
|
8570
|
+
if (!this.full) this.d.log({ event: "dialog-full", open: this.open.size });
|
|
8571
|
+
this.full = true;
|
|
8469
8572
|
continue;
|
|
8470
8573
|
}
|
|
8471
|
-
|
|
8472
|
-
this.
|
|
8574
|
+
this.open.set(id, { key: o.key, target: o.target, owner: o.owner, label: this.labelOf(o.key, o.owner), text: text5, sent: /* @__PURE__ */ new Map(), away: /* @__PURE__ */ new Set(), claimed: false, since: this.now(), told: false });
|
|
8575
|
+
this.d.log({ event: "dialog-seen", dialog: id, unit: o.key, owner: o.owner });
|
|
8473
8576
|
}
|
|
8474
8577
|
for (const [id, d] of [...this.open]) {
|
|
8578
|
+
this.at(gen, "dispatcher");
|
|
8579
|
+
await this.whose(d);
|
|
8580
|
+
if (gen !== this.gen) return;
|
|
8475
8581
|
const state = this.d.hub.outcome(id);
|
|
8476
8582
|
if (!state.open) {
|
|
8477
8583
|
this.open.delete(id);
|
|
8478
|
-
|
|
8584
|
+
this.full = false;
|
|
8585
|
+
this.d.log({ event: "dialog-ended", dialog: id, by: state.by, sent: d.sent.size });
|
|
8586
|
+
for (const k of d.sent.keys()) this.hold(k, id, state.by);
|
|
8587
|
+
if (!await this.drain(gen)) return;
|
|
8479
8588
|
continue;
|
|
8480
8589
|
}
|
|
8481
8590
|
if (d.claimed && d.unknownAt !== void 0 && this.now() - d.unknownAt >= UNKNOWN_RECHECK_MS) {
|
|
@@ -8483,17 +8592,25 @@ var DialogForwarder = class {
|
|
|
8483
8592
|
d.unknownAt = void 0;
|
|
8484
8593
|
d.sent.clear();
|
|
8485
8594
|
}
|
|
8595
|
+
const mine = to.filter((c) => reaches(this.scopeOf(d), c, this.d.admin ?? (() => false)));
|
|
8486
8596
|
for (const k of this.d.controllersKnown?.() === false ? [] : [...d.sent.keys()]) {
|
|
8487
|
-
if (
|
|
8597
|
+
if (mine.some((c) => `${c.team}
|
|
8488
8598
|
${c.device}` === k)) continue;
|
|
8489
8599
|
d.sent.delete(k);
|
|
8490
|
-
|
|
8600
|
+
this.hold(k, id, "withdrawn");
|
|
8601
|
+
if (!await this.drain(gen)) return;
|
|
8491
8602
|
}
|
|
8492
8603
|
if (!d.told && !d.claimed && to.length && this.now() - d.since >= UNANSWERED_MS) {
|
|
8493
|
-
|
|
8494
|
-
await this.d.notice?.(`unanswered/${id}`, { kind: "dialog-unanswered", unit: d.key, text: `A question of ${d.
|
|
8604
|
+
this.at(gen, "notice");
|
|
8605
|
+
const sent = await this.d.notice?.(`unanswered/${id}`, { kind: "dialog-unanswered", unit: d.key, text: `A question of ${d.label} has waited ${Math.round((this.now() - d.since) / 6e4)} minutes for an answer.` }, this.scopeOf(d));
|
|
8606
|
+
if (gen !== this.gen) return;
|
|
8607
|
+
d.told = sent !== false;
|
|
8608
|
+
}
|
|
8609
|
+
if (!mine.length && !d.unrouted) {
|
|
8610
|
+
d.unrouted = true;
|
|
8611
|
+
this.d.log({ event: "dialog-unrouted", dialog: id, unit: d.key, owner: d.owner });
|
|
8495
8612
|
}
|
|
8496
|
-
for (const c of
|
|
8613
|
+
for (const c of mine) {
|
|
8497
8614
|
const k = `${c.team}
|
|
8498
8615
|
${c.device}`;
|
|
8499
8616
|
const at = d.sent.get(k), up = this.d.online?.(c.team, c.device) ?? true;
|
|
@@ -8502,16 +8619,53 @@ ${c.device}`;
|
|
|
8502
8619
|
if (at !== void 0) continue;
|
|
8503
8620
|
} else if (at !== void 0 && !d.away.delete(k) && this.now() - at < REFRESH_MS) continue;
|
|
8504
8621
|
d.sent.set(k, this.now());
|
|
8505
|
-
|
|
8622
|
+
this.at(gen, "post");
|
|
8623
|
+
const posted = await this.send(k, "dialog-open", { dialogId: id, digest: d.target.answer.digest, owner: d.label, dialog: d.text });
|
|
8624
|
+
if (gen !== this.gen) return;
|
|
8625
|
+
if (!posted) d.sent.delete(k);
|
|
8506
8626
|
}
|
|
8507
8627
|
}
|
|
8508
8628
|
for (const id of [...this.skipped]) if (!this.d.hub.outcome(id).open) this.skipped.delete(id);
|
|
8509
8629
|
}
|
|
8630
|
+
/** Keeps a close to post: the oldest is given up when the room is used up. */
|
|
8631
|
+
hold(k, dialogId, by) {
|
|
8632
|
+
this.closes.set(`${k}
|
|
8633
|
+
${dialogId}`, { k, dialogId, by, at: this.now(), tries: 0 });
|
|
8634
|
+
for (const [key2, c] of this.closes) {
|
|
8635
|
+
if (this.closes.size <= MAX_CLOSES) break;
|
|
8636
|
+
this.closes.delete(key2);
|
|
8637
|
+
this.d.log({ event: "dialog-close-dropped", dialog: c.dialogId, reason: "full" });
|
|
8638
|
+
}
|
|
8639
|
+
}
|
|
8640
|
+
/** Posts the held closes, each removed once posted. One that failed `CLOSE_TRIES` times, or whose card has expired at the controller (`OPEN_STALE_MS`), is given up with a line. False when this pass was let go meanwhile. */
|
|
8641
|
+
async drain(gen) {
|
|
8642
|
+
for (const [key2, c] of [...this.closes]) {
|
|
8643
|
+
if (this.now() - c.at > OPEN_STALE_MS) {
|
|
8644
|
+
this.closes.delete(key2);
|
|
8645
|
+
this.d.log({ event: "dialog-close-dropped", dialog: c.dialogId, reason: "stale" });
|
|
8646
|
+
continue;
|
|
8647
|
+
}
|
|
8648
|
+
this.at(gen, "post");
|
|
8649
|
+
const posted = await this.send(c.k, "dialog-close", { dialogId: c.dialogId, by: c.by });
|
|
8650
|
+
if (posted) this.closes.delete(key2);
|
|
8651
|
+
else if (++c.tries >= CLOSE_TRIES) {
|
|
8652
|
+
this.closes.delete(key2);
|
|
8653
|
+
this.d.log({ event: "dialog-close-dropped", dialog: c.dialogId, reason: "failed" });
|
|
8654
|
+
}
|
|
8655
|
+
if (gen !== this.gen) return false;
|
|
8656
|
+
}
|
|
8657
|
+
return true;
|
|
8658
|
+
}
|
|
8510
8659
|
now = () => this.d.now?.() ?? Date.now();
|
|
8660
|
+
/** What the controllers are told asks: the unit's name when it is known, else the owner of the dialog. */
|
|
8661
|
+
labelOf(key2, owner) {
|
|
8662
|
+
return this.d.unit?.(key2) || owner;
|
|
8663
|
+
}
|
|
8511
8664
|
async send(k, kind, payload) {
|
|
8512
8665
|
const [team, device] = k.split("\n");
|
|
8513
8666
|
try {
|
|
8514
8667
|
await this.d.post(team, device, kind, payload);
|
|
8668
|
+
this.d.log({ event: "dialog-sent", kind, dialog: payload.dialogId, team, to: device });
|
|
8515
8669
|
return true;
|
|
8516
8670
|
} catch (error) {
|
|
8517
8671
|
this.d.log({ event: "dialog-post-failed", kind, dialog: payload.dialogId, error: String(error.message).slice(0, 200) });
|
|
@@ -8532,6 +8686,8 @@ ${device}`);
|
|
|
8532
8686
|
const drop = (reason) => this.d.log({ event: "drop", reason, kind: "dialog-answer", from: a.from, dialog: a.dialogId });
|
|
8533
8687
|
const d = this.open.get(a.dialogId);
|
|
8534
8688
|
if (!d) return drop("dialog-answer-unknown");
|
|
8689
|
+
await this.whose(d);
|
|
8690
|
+
if (!reaches(this.scopeOf(d), { team: a.team, device: a.from }, this.d.admin ?? (() => false))) return drop("dialog-answer-not-recipient");
|
|
8535
8691
|
if (d.target.answer.digest !== a.digest) return drop("dialog-answer-digest");
|
|
8536
8692
|
if (!ANSWER_ID2.test(a.answerId) || this.seen.has(a.answerId)) return drop("dialog-answer-id");
|
|
8537
8693
|
this.seen.add(a.answerId);
|
|
@@ -8597,6 +8753,33 @@ ${dialogId}`;
|
|
|
8597
8753
|
};
|
|
8598
8754
|
var newAnswerId = () => randomBytes12(16).toString("hex");
|
|
8599
8755
|
|
|
8756
|
+
// src/server/dispatcher.ts
|
|
8757
|
+
function dispatcherOf(pane, listed2, allyPanes) {
|
|
8758
|
+
const mine = listed2.find((p) => p.pane_id === pane);
|
|
8759
|
+
if (!mine) return void 0;
|
|
8760
|
+
const ally = allyPanes.find((a) => a.pane === pane);
|
|
8761
|
+
if (ally) return { device: ally.device, ...ally.team ? { team: ally.team } : {} };
|
|
8762
|
+
const device = listed2.find((p) => p.tab_id === mine.tab_id && p.tokens?.sb_opened_by?.startsWith("link:"))?.tokens?.sb_opened_by?.replace(/^(link:)+/, "");
|
|
8763
|
+
return device && /^[a-z2-7]{26}$/.test(device) ? { device } : void 0;
|
|
8764
|
+
}
|
|
8765
|
+
function dispatcherLookup(d) {
|
|
8766
|
+
let listed2;
|
|
8767
|
+
return async (key2) => {
|
|
8768
|
+
const pane = d.paneOf(key2);
|
|
8769
|
+
if (!pane) return void 0;
|
|
8770
|
+
if (!listed2 || d.now() - listed2.at > 2e3) listed2 = { at: d.now(), panes: d.list().catch(() => void 0) };
|
|
8771
|
+
const panes3 = await listed2.panes;
|
|
8772
|
+
return panes3 ? dispatcherOf(pane, panes3, d.allyPanes()) : void 0;
|
|
8773
|
+
};
|
|
8774
|
+
}
|
|
8775
|
+
var KIND = { main: "main", member: "member", subagent: "subagent", ally: "ally run", run: "run" };
|
|
8776
|
+
function unitLabel(found) {
|
|
8777
|
+
const name2 = allyLabel(found?.name);
|
|
8778
|
+
if (!found || !/[\p{L}\p{N}]/u.test(name2)) return void 0;
|
|
8779
|
+
const kind = KIND[found.role];
|
|
8780
|
+
return kind ? `${kind} - ${name2}` : name2;
|
|
8781
|
+
}
|
|
8782
|
+
|
|
8600
8783
|
// src/server/opentower.ts
|
|
8601
8784
|
import { realpathSync as realpathSync7 } from "node:fs";
|
|
8602
8785
|
import { basename as basename8 } from "node:path";
|
|
@@ -10708,8 +10891,14 @@ var LinkHolder = class {
|
|
|
10708
10891
|
...message.awaitsReply !== void 0 ? { awaitsReply: message.awaitsReply } : {}
|
|
10709
10892
|
};
|
|
10710
10893
|
this.o.store.putInbox(item);
|
|
10894
|
+
this.o.store.log({ event: "tower-in", kind: "tower", team: run3.team, from: header2.from, id: item.id, ...message.awaitsReply !== void 0 ? { ask: message.awaitsReply } : {} });
|
|
10711
10895
|
this.thread(header2.from, { dir: "in", at: item.received, id: item.id, kind: "tower", ...message.awaitsReply !== void 0 ? { awaitsReply: message.awaitsReply } : {}, text: message.text });
|
|
10712
10896
|
}
|
|
10897
|
+
/** An ally run took the inbox message `id` into its session: one `link.log` line names the run, the sender, the team and the ask, never the text (docs/specs/2026-11-05-link-shared-box.md, Audit). */
|
|
10898
|
+
delivered(id, run3) {
|
|
10899
|
+
const item = this.o.store.readInbox(id);
|
|
10900
|
+
if (item) this.o.store.log({ event: "tower-run", run: run3, team: item.team, from: item.from, id, ...item.awaitsReply !== void 0 ? { ask: item.awaitsReply } : {} });
|
|
10901
|
+
}
|
|
10713
10902
|
/** A row of a device's thread, under the lock; a store that cannot write it does not lose the message. */
|
|
10714
10903
|
thread(device, row2) {
|
|
10715
10904
|
try {
|
|
@@ -10742,6 +10931,7 @@ var LinkHolder = class {
|
|
|
10742
10931
|
...taken ? { reply: { ask: message.inReplyTo, main: taken.main, mainName: taken.mainName, tower: taken.tower } } : {}
|
|
10743
10932
|
};
|
|
10744
10933
|
this.o.store.putInbox(item);
|
|
10934
|
+
this.o.store.log({ event: "tower-in", kind: "tower-reply", team: run3.team, from: header2.from, id: item.id, ask: message.inReplyTo });
|
|
10745
10935
|
this.thread(header2.from, { dir: "in", at: item.received, id: item.id, kind: "tower-reply", inReplyTo: message.inReplyTo, text: message.text });
|
|
10746
10936
|
}
|
|
10747
10937
|
/** The election's record no longer names this session: nothing is acted on from here, and every stream ends. */
|
|
@@ -10895,7 +11085,7 @@ var LinkHolder = class {
|
|
|
10895
11085
|
store.log({ event: "tower-open", project, from });
|
|
10896
11086
|
const attempt = (async () => {
|
|
10897
11087
|
try {
|
|
10898
|
-
await openTower2(project, toWorkspace);
|
|
11088
|
+
await openTower2(project, toWorkspace, from);
|
|
10899
11089
|
store.log({ event: "tower-opened", project });
|
|
10900
11090
|
} catch (error) {
|
|
10901
11091
|
store.log({ event: "tower-open-failed", project, error: error.message });
|
|
@@ -11078,6 +11268,17 @@ ${target.id}`, { text: req.text, at: this.now() });
|
|
|
11078
11268
|
}
|
|
11079
11269
|
return items.sort((a, b) => a.received - b.received || (a.id < b.id ? -1 : 1));
|
|
11080
11270
|
}
|
|
11271
|
+
/** Whether this holder can tell who is in `team` right now: it runs, the team runs and has not died, and its roster reads. A device is taken as gone only while this is true (the revocation of a controller's mains). */
|
|
11272
|
+
rosterKnown(team) {
|
|
11273
|
+
const run3 = this.runs.get(team);
|
|
11274
|
+
if (!this.running || !run3 || run3.dead) return false;
|
|
11275
|
+
try {
|
|
11276
|
+
this.loadTeam(team);
|
|
11277
|
+
return true;
|
|
11278
|
+
} catch {
|
|
11279
|
+
return false;
|
|
11280
|
+
}
|
|
11281
|
+
}
|
|
11081
11282
|
/** Whether `device` is a member of `team` on this computer's roster (a run whose sender left is ended). */
|
|
11082
11283
|
isMember(team, device) {
|
|
11083
11284
|
try {
|
|
@@ -11187,6 +11388,14 @@ ${target.id}`, { text: req.text, at: this.now() });
|
|
|
11187
11388
|
dialogControllers() {
|
|
11188
11389
|
return this.controllerRead().list;
|
|
11189
11390
|
}
|
|
11391
|
+
/** Whether `device` is an admin of `team`'s roster now (an admin is told every question and notice of the box). False for a team that is not running or a roster that cannot be read. */
|
|
11392
|
+
isAdmin(team, device) {
|
|
11393
|
+
try {
|
|
11394
|
+
return this.loadTeam(team).state.members.get(device)?.role === "admin";
|
|
11395
|
+
} catch {
|
|
11396
|
+
return false;
|
|
11397
|
+
}
|
|
11398
|
+
}
|
|
11190
11399
|
/** False while the controller list cannot be trusted to be complete (the holder is not running, or a team's roster could not be read): a caller does not withdraw anything then. */
|
|
11191
11400
|
controllersKnown() {
|
|
11192
11401
|
return this.controllerRead().known;
|
|
@@ -11259,6 +11468,7 @@ ${Buffer5.from(header2.id).toString("hex")}`).digest("hex").slice(0, 16);
|
|
|
11259
11468
|
const sender = roster.members.get(header2.from);
|
|
11260
11469
|
if (!(this.o.attended?.() ?? true) || !sender || sender.attended) return void this.o.store.log({ event: "drop", team: run3.team, reason: "dialog-open-unexpected", from: header2.from, kind: "dialog-open" });
|
|
11261
11470
|
const fresh = this.dialogViews.put({ team: run3.team, device: header2.from, dialogId: p.dialogId, digest: p.digest, owner: p.owner, dialog: p.dialog, at: this.now() });
|
|
11471
|
+
this.o.store.log({ event: "dialog-in", team: run3.team, from: header2.from, dialog: p.dialogId, fresh });
|
|
11262
11472
|
if (fresh) this.o.notify(`Straw Link: ${sender.label} asks a question (${restrictLabel(p.owner)})`, "warning");
|
|
11263
11473
|
}
|
|
11264
11474
|
takeDialogClose(header2, p) {
|
|
@@ -12288,6 +12498,13 @@ function createLinkService({ kit, stateDir, version, started, pid = process.pid,
|
|
|
12288
12498
|
return store;
|
|
12289
12499
|
},
|
|
12290
12500
|
attended,
|
|
12501
|
+
/** How the policy file reads now: a refused one means the box cannot tell who may do what (g346-F003). */
|
|
12502
|
+
policyMode: () => policy().mode,
|
|
12503
|
+
/** The policy mode and the effective config from one read of the policy file, so the two cannot disagree. */
|
|
12504
|
+
policyLook() {
|
|
12505
|
+
const p = policy();
|
|
12506
|
+
return { mode: p.mode, config: readEffectiveConfig(p, stateDir, { home, agentDir }) };
|
|
12507
|
+
},
|
|
12291
12508
|
/** The teams and projects of config.json, read fresh. */
|
|
12292
12509
|
config,
|
|
12293
12510
|
/** The level this computer gives a device (the policy file's, else `link.trust`, else `reply`): an ally run is claimed at it. */
|
|
@@ -13595,11 +13812,11 @@ ${c.device}`, units = mine.map((u) => ({ ...u, report: cutBytes(this.d.report(u.
|
|
|
13595
13812
|
}
|
|
13596
13813
|
}
|
|
13597
13814
|
}
|
|
13598
|
-
/** Tells
|
|
13599
|
-
async notify(key2, notice) {
|
|
13600
|
-
if (this.told.has(key2)) return;
|
|
13601
|
-
const to = this.d.controllers();
|
|
13602
|
-
if (!to.length) return;
|
|
13815
|
+
/** Tells the controllers `scope` covers (the dispatcher and the admins; `EVERYONE` for all), once per `key`; nothing is remembered when there was nobody to tell, so the next trigger with the same key still goes out. Resolves whether anybody was addressed. */
|
|
13816
|
+
async notify(key2, notice, scope) {
|
|
13817
|
+
if (this.told.has(key2)) return false;
|
|
13818
|
+
const to = this.d.controllers().filter((c) => reaches(scope, c, this.d.admin ?? (() => false)));
|
|
13819
|
+
if (!to.length) return false;
|
|
13603
13820
|
this.told.add(key2);
|
|
13604
13821
|
while (this.told.size > TOLD_MAX) this.told.delete(this.told.values().next().value);
|
|
13605
13822
|
this.d.log({ event: "notice", key: key2, kind: notice.kind, unit: notice.unit });
|
|
@@ -13610,14 +13827,17 @@ ${c.device}`, units = mine.map((u) => ({ ...u, report: cutBytes(this.d.report(u.
|
|
|
13610
13827
|
this.d.log({ event: "notice-post-failed", device: c.device, kind: notice.kind, error: String(error.message).slice(0, 200) });
|
|
13611
13828
|
}
|
|
13612
13829
|
}
|
|
13830
|
+
return true;
|
|
13613
13831
|
}
|
|
13614
13832
|
};
|
|
13615
13833
|
function runNotice(s2) {
|
|
13834
|
+
const scope = s2.device !== void 0 ? { device: s2.device, ...s2.team !== void 0 ? { team: s2.team } : {} } : {};
|
|
13616
13835
|
const who = s2.ally ? "An ally run" : "A run";
|
|
13617
|
-
if (s2.kind === "
|
|
13836
|
+
if (s2.kind === "revoked") return { key: `revoked/${s2.main}/${s2.ended ? "ended" : s2.manual ? "manual" : "pending"}`, notice: { kind: s2.ended ? "main-revoked" : "main-revoke-pending", unit: allyLabel(s2.label), text: s2.text }, scope };
|
|
13837
|
+
if (s2.kind === "refused") return { key: `refused/${s2.workspace}/${createHash12("sha256").update(s2.text).digest("hex").slice(0, 16)}`, notice: { kind: "run-refused", unit: allyLabel(s2.label), text: s2.text }, scope };
|
|
13618
13838
|
const trust = s2.detail?.startsWith("No trust decision");
|
|
13619
13839
|
const kind = trust ? "trust-unanswered" : s2.outcome === "quit" ? "run-quit" : s2.outcome === "timeout" ? "run-timeout" : "run-crashed";
|
|
13620
|
-
return { key: `run/${s2.run}`, notice: { kind, unit: allyLabel(s2.label), text: `${who} ${s2.outcome === "quit" ? "was quit" : s2.outcome === "timeout" ? "timed out" : "crashed"}${s2.detail ? `: ${s2.detail}` : ""}` } };
|
|
13840
|
+
return { key: `run/${s2.run}`, notice: { kind, unit: allyLabel(s2.label), text: `${who} ${s2.outcome === "quit" ? "was quit" : s2.outcome === "timeout" ? "timed out" : "crashed"}${s2.detail ? `: ${s2.detail}` : ""}` }, scope };
|
|
13621
13841
|
}
|
|
13622
13842
|
|
|
13623
13843
|
// src/server/towerrunstore.ts
|
|
@@ -15461,6 +15681,62 @@ function removeAnswer(slot, workspace, id, now) {
|
|
|
15461
15681
|
if (was.some((a) => a.id === id)) write(slot, workspace, ANSWERS, was.filter((a) => a.id !== id));
|
|
15462
15682
|
}
|
|
15463
15683
|
|
|
15684
|
+
// src/server/revoke.ts
|
|
15685
|
+
var GONE = /No live session is named|is not in this workspace|is not a main agent|was not opened by a tower/;
|
|
15686
|
+
function createRevoker(d) {
|
|
15687
|
+
const told = /* @__PURE__ */ new Set();
|
|
15688
|
+
let low = /* @__PURE__ */ new Set();
|
|
15689
|
+
let goneSeen = /* @__PURE__ */ new Map();
|
|
15690
|
+
let running;
|
|
15691
|
+
const pass = async () => {
|
|
15692
|
+
const now = /* @__PURE__ */ new Set(), gone = /* @__PURE__ */ new Map();
|
|
15693
|
+
for (const row2 of d.opened()) {
|
|
15694
|
+
if (row2.level !== void 0 && row2.level !== "dispatch") continue;
|
|
15695
|
+
if (!d.known(row2.team)) continue;
|
|
15696
|
+
const standing = d.standing(row2.device, row2.team);
|
|
15697
|
+
if (standing?.level === "dispatch") continue;
|
|
15698
|
+
now.add(row2.main);
|
|
15699
|
+
if (!low.has(row2.main)) continue;
|
|
15700
|
+
const why = standing ? "the device lost dispatch" : "the device left the team", who = `The main "${row2.name}" opened for ${row2.deviceLabel}`;
|
|
15701
|
+
try {
|
|
15702
|
+
await d.close(row2.workspace, row2.main);
|
|
15703
|
+
} catch (cause) {
|
|
15704
|
+
const reason = cause.message;
|
|
15705
|
+
if (GONE.test(reason)) {
|
|
15706
|
+
const seen = (goneSeen.get(row2.main) ?? 0) + 1;
|
|
15707
|
+
gone.set(row2.main, seen);
|
|
15708
|
+
if (seen < 2) continue;
|
|
15709
|
+
if (await d.paneGone(row2.workspace, row2.pane)) {
|
|
15710
|
+
d.log({ event: "revoke-gone", workspace: row2.workspace, main: row2.main });
|
|
15711
|
+
d.forget(row2.workspace, row2.main);
|
|
15712
|
+
gone.delete(row2.main);
|
|
15713
|
+
continue;
|
|
15714
|
+
}
|
|
15715
|
+
}
|
|
15716
|
+
const manual = /was not opened by a tower/.test(reason), key2 = `${row2.main}/${manual ? "manual" : "pending"}`;
|
|
15717
|
+
d.log({ event: "revoke-waiting", workspace: row2.workspace, main: row2.main, why });
|
|
15718
|
+
if (!told.has(key2)) {
|
|
15719
|
+
const sent = await d.signal({ kind: "revoked", workspace: row2.workspace, label: row2.label, ally: true, device: row2.device, ...row2.team ? { team: row2.team } : {}, main: row2.main, ended: false, ...manual ? { manual: true } : {}, text: manual ? `${who} still runs and cannot be ended by the box (${reason}). Close it by hand.` : `${who} could not be ended yet (${reason}). The box asks again until it quits.` });
|
|
15720
|
+
if (sent !== false) {
|
|
15721
|
+
told.add(key2);
|
|
15722
|
+
while (told.size > 256) told.delete(told.values().next().value);
|
|
15723
|
+
}
|
|
15724
|
+
}
|
|
15725
|
+
continue;
|
|
15726
|
+
}
|
|
15727
|
+
d.log({ event: "revoke-ended", workspace: row2.workspace, main: row2.main, why });
|
|
15728
|
+
d.forget(row2.workspace, row2.main);
|
|
15729
|
+
d.signal({ kind: "revoked", workspace: row2.workspace, label: row2.label, ally: true, device: row2.device, ...row2.team ? { team: row2.team } : {}, main: row2.main, ended: true, text: `${who} was ended: ${why}.` });
|
|
15730
|
+
}
|
|
15731
|
+
low = now;
|
|
15732
|
+
goneSeen = gone;
|
|
15733
|
+
};
|
|
15734
|
+
return {
|
|
15735
|
+
/** One look at every opened main; looks that overlap share the one in flight. A failure is logged and the next sweep tries again. */
|
|
15736
|
+
sweep: () => running ??= pass().catch((cause) => d.log({ event: "revoke-failed", error: cause.message.slice(0, 200) })).finally(() => running = void 0)
|
|
15737
|
+
};
|
|
15738
|
+
}
|
|
15739
|
+
|
|
15464
15740
|
// src/server/runs.ts
|
|
15465
15741
|
import { randomBytes as randomBytes17 } from "node:crypto";
|
|
15466
15742
|
import { lstatSync as lstatSync7, realpathSync as realpathSync10 } from "node:fs";
|
|
@@ -15991,7 +16267,7 @@ function createRuns2(o) {
|
|
|
15991
16267
|
release();
|
|
15992
16268
|
svc.refusedAt = now();
|
|
15993
16269
|
noticeOf(svc, { id: "settings", level: "error", kind: "settings", text: `A run could not start: ${cause.message}` });
|
|
15994
|
-
o.signal?.({ kind: "refused", workspace: svc.ref.workspace, label: svc.ref.record().label, ally: true, text: `A run could not start: ${cause.message}
|
|
16270
|
+
o.signal?.({ kind: "refused", workspace: svc.ref.workspace, label: svc.ref.record().label, ally: true, text: `A run could not start: ${cause.message}`, device, team: first2.team });
|
|
15995
16271
|
log.line("run-refused", { workspace: svc.ref.workspace, why: "settings", kind: "ally" });
|
|
15996
16272
|
return;
|
|
15997
16273
|
}
|
|
@@ -16202,7 +16478,7 @@ function createRuns2(o) {
|
|
|
16202
16478
|
a.rec.outcome = outcome;
|
|
16203
16479
|
a.rec.endedAt = iso2(now());
|
|
16204
16480
|
if (detail) a.rec.detail = phrase4(detail, 200);
|
|
16205
|
-
if (outcome === "crashed" || outcome === "timeout" || outcome === "quit") o.signal?.({ kind: "ended", run: a.rec.id, workspace: ws, label: svc.ref.record().label, ally: a.rec.kind === "ally", outcome, ...detail ? { detail: phrase4(detail, 200) } : {} });
|
|
16481
|
+
if (outcome === "crashed" || outcome === "timeout" || outcome === "quit") o.signal?.({ kind: "ended", run: a.rec.id, workspace: ws, label: svc.ref.record().label, ally: a.rec.kind === "ally", outcome, ...detail ? { detail: phrase4(detail, 200) } : {}, ...a.rec.kind === "ally" && a.rec.device ? { device: a.rec.device, ...a.rec.team ? { team: a.rec.team } : {} } : {} });
|
|
16206
16482
|
try {
|
|
16207
16483
|
writeRun(slot, a.rec);
|
|
16208
16484
|
pruneRuns(slot, ws);
|
|
@@ -16324,7 +16600,7 @@ function createRuns2(o) {
|
|
|
16324
16600
|
const orphan = a.rec.hand === true && !a.polled && a.pending === void 0 && a.closing === void 0;
|
|
16325
16601
|
const outcome = a.pending ?? (a.closing !== void 0 ? "done" : orphan ? "quit" : "crashed");
|
|
16326
16602
|
if (outcome === "crashed" && diedWithHerdr(a)) continue;
|
|
16327
|
-
await finish(a, outcome, outcome === "crashed" ? "pi exited without tower_done" : orphan ? "the pi left before its first poll" :
|
|
16603
|
+
await finish(a, outcome, outcome === "crashed" ? "pi exited without tower_done" : orphan ? "the pi left before its first poll" : a.why, a.left === true);
|
|
16328
16604
|
continue;
|
|
16329
16605
|
}
|
|
16330
16606
|
if (a.recovering !== void 0) {
|
|
@@ -16353,9 +16629,18 @@ function createRuns2(o) {
|
|
|
16353
16629
|
if (!a.token) continue;
|
|
16354
16630
|
if (a.rec.kind === "ally" && !standingOf(a)) {
|
|
16355
16631
|
log.line("ally-ended", { workspace: a.rec.workspace, run: a.rec.id, why: "the sender left the roster" });
|
|
16632
|
+
a.why = "the sender left the team";
|
|
16356
16633
|
terminate(a, "quit");
|
|
16357
16634
|
continue;
|
|
16358
16635
|
}
|
|
16636
|
+
const low = a.rec.kind === "ally" && a.rec.level === "dispatch" && o.ally?.known?.(a.rec.team ?? "") !== false && standingOf(a)?.level !== "dispatch";
|
|
16637
|
+
if (low && a.lowSeen) {
|
|
16638
|
+
log.line("ally-ended", { workspace: a.rec.workspace, run: a.rec.id, why: "the sender lost dispatch" });
|
|
16639
|
+
a.why = "the sender lost dispatch";
|
|
16640
|
+
terminate(a, "quit");
|
|
16641
|
+
continue;
|
|
16642
|
+
}
|
|
16643
|
+
a.lowSeen = low;
|
|
16359
16644
|
if (now() - a.lastPoll > t.lost) {
|
|
16360
16645
|
log.line("run-lost", { run: a.rec.id });
|
|
16361
16646
|
terminate(a, "crashed");
|
|
@@ -16526,6 +16811,7 @@ function createRuns2(o) {
|
|
|
16526
16811
|
a.offered.delete(event.id);
|
|
16527
16812
|
a.rec.events.push(event.id);
|
|
16528
16813
|
writeRun(slot, a.rec);
|
|
16814
|
+
if (a.rec.kind === "ally") o.ally?.delivered?.(event.id, a.rec.id);
|
|
16529
16815
|
if (event.kind === "boss") bossTurn(a);
|
|
16530
16816
|
return { ok: true };
|
|
16531
16817
|
}
|
|
@@ -16790,7 +17076,7 @@ function createRuns2(o) {
|
|
|
16790
17076
|
const a = of(token);
|
|
16791
17077
|
if (a && !a.ended && a.rec.kind === "ally") {
|
|
16792
17078
|
const standing = standingOf(a);
|
|
16793
|
-
if (!standing) terminate(a, "quit");
|
|
17079
|
+
if (!standing) a.why ??= "the sender left the team", terminate(a, "quit");
|
|
16794
17080
|
return { id: a.rec.id, workspace: a.rec.workspace, kind: "ally", ...standing ? { level: standing.level } : { gone: true }, ...a.rec.device ? { device: a.rec.device } : {}, ...a.rec.deviceLabel ? { deviceLabel: a.rec.deviceLabel } : {}, ...a.rec.deviceShort ? { deviceShort: a.rec.deviceShort } : {}, ...a.rec.team ? { team: a.rec.team } : {}, ...a.rec.teamName ? { teamName: a.rec.teamName } : {}, pane: a.rec.pane, asks: itemsOf(a, true).flatMap((i) => i.awaitsReply ? [i.awaitsReply] : []) };
|
|
16795
17081
|
}
|
|
16796
17082
|
return a && !a.ended ? { id: a.rec.id, workspace: a.rec.workspace, kind: headRun(a) ? "head" : a.rec.kind, ...a.rec.level ? { level: a.rec.level } : {}, ...a.rec.device ? { device: a.rec.device } : {}, ...a.rec.deviceLabel ? { deviceLabel: a.rec.deviceLabel } : {}, ...a.rec.team ? { team: a.rec.team } : {}, pane: a.rec.pane, asks: itemsOf(a, true).flatMap((i) => i.awaitsReply ? [i.awaitsReply] : []) } : void 0;
|
|
@@ -16911,6 +17197,8 @@ function createRuns2(o) {
|
|
|
16911
17197
|
parked: readQueue(slot, workspace, true).map((e) => ({ id: e.id, kind: e.kind, ...e.from?.name ?? e.from?.id ? { from: e.from?.name ?? e.from?.id } : {}, first: phrase4(e.text.split("\n").find((l) => l.trim()) ?? "", 120) }))
|
|
16912
17198
|
};
|
|
16913
17199
|
},
|
|
17200
|
+
/** The panes of the live ally runs, with the device each serves and its team: the units a controller dispatched (a question asked in one is that controller's). */
|
|
17201
|
+
allyPanes: () => [...services.values()].flatMap((svc) => [...svc.allies.values()].filter((a) => !a.ended && a.rec.pane && a.rec.device).map((a) => ({ pane: a.rec.pane, device: a.rec.device, ...a.rec.team ? { team: a.rec.team } : {} }))),
|
|
16914
17202
|
/** The waiting events of a workspace for the GUI's queue rows. */
|
|
16915
17203
|
queue: (workspace) => {
|
|
16916
17204
|
const s2 = find2(workspace);
|
|
@@ -17087,6 +17375,7 @@ var UNKNOWN = "(unknown)";
|
|
|
17087
17375
|
var RUN_SEND_CAP_MS = 2e4;
|
|
17088
17376
|
var RETRY_MS3 = [1e3, 2e3, 5e3, 1e4, 3e4];
|
|
17089
17377
|
var REPUBLISH_MS = 300;
|
|
17378
|
+
var REVOKE_MS = 15e3;
|
|
17090
17379
|
var REFRESH_MS2 = 4 * 6e4;
|
|
17091
17380
|
var RECOVER_MS = 6e4;
|
|
17092
17381
|
var RECOVER_POLL_MS = 2e3;
|
|
@@ -17216,10 +17505,14 @@ function createTowers(o) {
|
|
|
17216
17505
|
standing: (device, team) => o.ally.standing(device, team),
|
|
17217
17506
|
project: (team) => o.ally.project(team),
|
|
17218
17507
|
thread: (device) => o.ally.thread(device),
|
|
17508
|
+
known: (team) => o.ally.known?.(team) ?? true,
|
|
17219
17509
|
waiting: (identity2) => {
|
|
17220
17510
|
const ws = workspaceOf(identity2), answers2 = ws ? readAnswers(slot, ws, now()).map((a) => ({ id: `${LOCAL}${a.id}`, device: a.device, label: a.label, short: a.short, team: a.team, teamName: a.teamName, fromWorkspace: "", replyExpected: false, text: a.text, at: a.at, local: a.from, ...a.ask ? { awaitsReply: a.ask } : {} })) : [];
|
|
17221
17511
|
return [...o.ally.waiting(identity2), ...answers2].sort((a, b) => a.at - b.at);
|
|
17222
17512
|
},
|
|
17513
|
+
delivered: (id, run4) => {
|
|
17514
|
+
if (!id.startsWith(LOCAL)) o.ally.delivered?.(id, run4);
|
|
17515
|
+
},
|
|
17223
17516
|
taken: async (id, identity2) => {
|
|
17224
17517
|
const ws = workspaceOf(identity2);
|
|
17225
17518
|
if (!id.startsWith(LOCAL)) return o.ally.taken(id, identity2);
|
|
@@ -17691,6 +17984,7 @@ function createTowers(o) {
|
|
|
17691
17984
|
error("purge", cause.message);
|
|
17692
17985
|
}
|
|
17693
17986
|
}, o.refreshMs ?? REFRESH_MS2));
|
|
17987
|
+
timers2.push(setInterval(() => void revoker.sweep(), o.revokeMs ?? REVOKE_MS));
|
|
17694
17988
|
const poll = o.recoverPollMs ?? RECOVER_POLL_MS;
|
|
17695
17989
|
timers2.push(setInterval(() => {
|
|
17696
17990
|
if (now() < recoverUntil + poll) look2();
|
|
@@ -17856,6 +18150,25 @@ function createTowers(o) {
|
|
|
17856
18150
|
}
|
|
17857
18151
|
return closed;
|
|
17858
18152
|
};
|
|
18153
|
+
const revoker = createRevoker({
|
|
18154
|
+
opened: () => o.ally ? [...services.values()].filter((l) => !l.stopped && l.state === "live" && l.control && !isHeadL(l)).flatMap((l) => readRoutes(slot, l.record.workspace, now()).filter((r) => r.kind === "opened").map((r) => ({ workspace: l.record.workspace, label: l.record.label, main: r.to, name: r.name, device: r.device, deviceLabel: r.label, team: r.team, ...r.level ? { level: r.level } : {}, ...r.pane ? { pane: r.pane } : {} }))) : [],
|
|
18155
|
+
known: (team) => o.ally?.known?.(team) ?? true,
|
|
18156
|
+
standing: (device, team) => o.ally?.standing(device, team),
|
|
18157
|
+
close: async (workspace, main) => void await need(workspace).control.closeMain(main),
|
|
18158
|
+
paneGone: async (workspace, pane) => {
|
|
18159
|
+
if (!pane) return false;
|
|
18160
|
+
try {
|
|
18161
|
+
const net = await need(workspace).reach.read();
|
|
18162
|
+
return !net.ps.some((p) => p.pane_id === pane);
|
|
18163
|
+
} catch {
|
|
18164
|
+
return false;
|
|
18165
|
+
}
|
|
18166
|
+
},
|
|
18167
|
+
// Herdr's listing is the second witness that a main is gone; no pane or a failed read confirms nothing
|
|
18168
|
+
forget: (workspace, main) => void forgetClosed(workspace, { main }),
|
|
18169
|
+
signal: (s2) => o.runs?.signal?.(s2),
|
|
18170
|
+
log: ({ event, ...rest }) => log.line(String(event), rest)
|
|
18171
|
+
});
|
|
17859
18172
|
const answered = answeredAsks(now);
|
|
17860
18173
|
const runToolFor = async (request) => {
|
|
17861
18174
|
const who = runs2.runOf(request.token);
|
|
@@ -17865,7 +18178,7 @@ function createTowers(o) {
|
|
|
17865
18178
|
const peer = who.kind === "ally" && who.device && who.team ? { device: who.device, label: who.deviceLabel ?? who.device.slice(0, 6), short: who.deviceShort ?? who.device.slice(0, 6), team: who.team, teamName: who.teamName ?? who.team.slice(0, 6) } : void 0;
|
|
17866
18179
|
const route = (to, name3, kind, ask, live, panes3, pane) => {
|
|
17867
18180
|
try {
|
|
17868
|
-
addRoute(slot, ws, { ...peer, to, name: name3, kind, at: now(), ...ask ? { ask } : {}, ...pane ? { pane } : {} }, now(), live, panes3);
|
|
18181
|
+
addRoute(slot, ws, { ...peer, to, name: name3, kind, at: now(), ...ask ? { ask } : {}, ...pane ? { pane } : {}, ...kind === "opened" && who.level ? { level: who.level } : {} }, now(), live, panes3);
|
|
17869
18182
|
} catch (cause) {
|
|
17870
18183
|
error("ally-route", cause.message);
|
|
17871
18184
|
}
|
|
@@ -18627,9 +18940,9 @@ async function runServer(o) {
|
|
|
18627
18940
|
...o.towers,
|
|
18628
18941
|
// A seam that watches the runs (tests) sees every signal too, and does not take the place of the notice to the controllers.
|
|
18629
18942
|
runs: { ...o.towers?.runs, signal: (sig) => {
|
|
18630
|
-
const r = runNotice(sig);
|
|
18631
|
-
void reporter?.notify(r.key, r.notice);
|
|
18943
|
+
const r = runNotice(sig), sent = reporter?.notify(r.key, r.notice, r.scope);
|
|
18632
18944
|
o.towers?.runs?.signal?.(sig);
|
|
18945
|
+
return sent;
|
|
18633
18946
|
} }
|
|
18634
18947
|
});
|
|
18635
18948
|
stops.push(cfg.watch(() => {
|
|
@@ -18648,21 +18961,26 @@ async function runServer(o) {
|
|
|
18648
18961
|
}
|
|
18649
18962
|
}), o.herdrPollMs ?? HERDR_POLL_MS));
|
|
18650
18963
|
const dialogsOut = () => forwarder ??= new DialogForwarder({
|
|
18651
|
-
|
|
18964
|
+
hub: guiHost.dialogs().hub,
|
|
18965
|
+
read: guiHost.dialogs().read,
|
|
18966
|
+
dispatcher: dispatcherLookup({ paneOf: (key2) => guiHost.dialogs().lookup(key2)?.pane, list: () => panes(cfg.run), allyPanes: () => towers?.runs.allyPanes() ?? [], now }),
|
|
18967
|
+
admin: (team, device) => link?.holder?.isAdmin(team, device) ?? false,
|
|
18652
18968
|
controllers: () => link?.holder?.dialogControllers() ?? [],
|
|
18653
18969
|
online: (team, device) => link?.holder?.deviceOnline(team, device) ?? false,
|
|
18654
18970
|
controllersKnown: () => link?.holder?.controllersKnown() ?? false,
|
|
18971
|
+
unit: (key2) => unitLabel(guiHost.dialogs().lookup(key2)),
|
|
18655
18972
|
post: async (team, device, kind, payload) => {
|
|
18656
18973
|
if (!link?.holder) throw new Error("The link holder is not running");
|
|
18657
18974
|
await link.holder.sendDialog(team, device, kind, payload);
|
|
18658
18975
|
},
|
|
18659
18976
|
log: ({ event, ...rest }) => log.line(String(event), Object.fromEntries(Object.entries(rest).map(([k, v]) => [k, typeof v === "number" || typeof v === "boolean" ? v : phrase6(v, 200)]))),
|
|
18660
18977
|
now,
|
|
18661
|
-
notice: (key2, notice) => unitsOut().notify(key2, notice)
|
|
18978
|
+
notice: (key2, notice, scope) => unitsOut().notify(key2, notice, scope)
|
|
18662
18979
|
});
|
|
18663
18980
|
const unitWorkspaces = /* @__PURE__ */ new Map();
|
|
18664
18981
|
const unitsOut = () => reporter ??= new UnitReporter({
|
|
18665
18982
|
controllers: () => link?.holder?.dialogControllers() ?? [],
|
|
18983
|
+
admin: (team, device) => link?.holder?.isAdmin(team, device) ?? false,
|
|
18666
18984
|
// Per team: the units whose workspace has a pane inside that team's project, when its policy shares status. Labels and monitor commands leave this computer, so they pass the same scrub as a board's.
|
|
18667
18985
|
units: async (team) => {
|
|
18668
18986
|
const policy = link?.holder?.policyOf(team), rows = towers?.status() ?? [];
|
|
@@ -18714,11 +19032,11 @@ ${unit}`), note = ws ? readNotes(slot, ws).at(-1) : void 0, sent = ws ? link?.ho
|
|
|
18714
19032
|
asks: { take: (q) => asks.take(q) },
|
|
18715
19033
|
served: (identity2) => towers?.allyWaiting(identity2),
|
|
18716
19034
|
confirmed: (to, dialogId, allow) => towers?.runs.confirmed(dialogId, allow) ?? false,
|
|
18717
|
-
openTower: async (project, toWorkspace) => {
|
|
19035
|
+
openTower: async (project, toWorkspace, from) => {
|
|
18718
19036
|
try {
|
|
18719
19037
|
await openTower({ run: cfg.run, towers: () => towers, now, home: o.link?.home ?? homedir12(), agentDir: env.PI_CODING_AGENT_DIR ?? join44(o.link?.home ?? homedir12(), ".pi", "agent") }, project, toWorkspace);
|
|
18720
19038
|
} catch (error) {
|
|
18721
|
-
void unitsOut().notify(`tower-open/${project}/${Math.floor(now() / 6e5)}`, { kind: "tower-open-failed", unit: allyLabel(project), text: `Could not open a tower for ${project}: ${error.message}` });
|
|
19039
|
+
void unitsOut().notify(`tower-open/${from}/${project}/${Math.floor(now() / 6e5)}`, { kind: "tower-open-failed", unit: allyLabel(project), text: `Could not open a tower for ${project}: ${error.message}` }, { device: from });
|
|
18722
19040
|
throw error;
|
|
18723
19041
|
}
|
|
18724
19042
|
}
|
|
@@ -18770,7 +19088,7 @@ ${unit}`), note = ws ? readNotes(slot, ws).at(-1) : void 0, sent = ws ? link?.ho
|
|
|
18770
19088
|
if (slot.primary && !leaving && phase === "starting") {
|
|
18771
19089
|
dialogsOut().start();
|
|
18772
19090
|
timers2.push(setInterval(() => void Promise.all([dialogsOut().tick(), unitsOut().tick()]), o.unitsMs ?? 15e3));
|
|
18773
|
-
}
|
|
19091
|
+
} else log.line("dialog-forwarder", { state: "skipped", primary: slot.primary, leaving: Boolean(leaving), phase });
|
|
18774
19092
|
if (phase === "starting") {
|
|
18775
19093
|
phase = "ready";
|
|
18776
19094
|
const stale = own.lastError !== void 0 && own.lastError === prior?.lastError;
|