@volter/supercode-teams 0.3.99 → 0.3.101
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/machine.mjs +41 -6
- package/mail/send.mjs +52 -11
- package/mail/store.mjs +5 -3
- package/package.json +1 -1
package/machine.mjs
CHANGED
|
@@ -300,6 +300,25 @@ function instructionsFile({ harness, scope = 'user', cwd } = {}) {
|
|
|
300
300
|
}
|
|
301
301
|
// A board verb's answer goes in pieces of this many characters (JSON, so about as many bytes).
|
|
302
302
|
const BOARD_ANSWER_PIECE = 512 * 1024;
|
|
303
|
+
// What an inbox answer carries at most: the channel's frame is 8 MiB (channel.mjs MAX_FRAME), and an answer holds each
|
|
304
|
+
// message three ways (rendered, plain, its row). A mailbox's whole history did not fit (yuerans, 2026-10-06: 6,496 read
|
|
305
|
+
// envelopes, a 43 MB frame refused); the newest that fit are listed, its claimed (unread) ones always, and a first line
|
|
306
|
+
// says how many older ones are not.
|
|
307
|
+
const INBOX_ANSWER_BYTES = 4 * 1024 * 1024;
|
|
308
|
+
|
|
309
|
+
/** The indexes of `count` messages kept in an answer, newest first until `sizeOf` passes the bound; `always` kept. */
|
|
310
|
+
function inboxKept(count, sizeOf, always = () => false) {
|
|
311
|
+
const kept = [];
|
|
312
|
+
let bytes = 0, full = false;
|
|
313
|
+
for (let index = count - 1; index >= 0; index -= 1) {
|
|
314
|
+
const size = sizeOf(index);
|
|
315
|
+
if (always(index) || (!full && bytes + size <= INBOX_ANSWER_BYTES)) { kept.push(index); bytes += size; }
|
|
316
|
+
else full = true;
|
|
317
|
+
}
|
|
318
|
+
return kept.reverse();
|
|
319
|
+
}
|
|
320
|
+
const inboxNote = (left) => `(${left} older message${left === 1 ? ' is' : 's are'} not listed: an answer holds at most ${INBOX_ANSWER_BYTES / 1024 / 1024} MiB. --recent <N> lists the newest N.)`;
|
|
321
|
+
|
|
303
322
|
|
|
304
323
|
export class TeamsMachine extends MachineCore {
|
|
305
324
|
/** Typed lines by key → the answer their first attempt gives (see #serveTurn). */
|
|
@@ -1158,10 +1177,13 @@ export class TeamsMachine extends MachineCore {
|
|
|
1158
1177
|
if (conn === INTERNAL_CONNECTION) {
|
|
1159
1178
|
const pending = this.internalRequests?.get(original);
|
|
1160
1179
|
if (!pending) {
|
|
1161
|
-
// an answer that came after its request gave up:
|
|
1162
|
-
// it
|
|
1180
|
+
// an answer that came after its request gave up: a subscription it opened (an index, or an activity follow of up
|
|
1181
|
+
// to 2048 sessions) is registered in serve with nobody reading it, one more at each retry, and serve resolves
|
|
1182
|
+
// every activity subscription on every poll round; so it is let go at once (yuerans, 2026-10-06: leaked activity
|
|
1183
|
+
// follows held serve's loop in poll rounds 294 s of 420, and its relay starts timed out)
|
|
1163
1184
|
const late = message.result?.subscription;
|
|
1164
|
-
|
|
1185
|
+
const kind = typeof late === 'string' ? /^(index|activity)-sub-/.exec(late)?.[1] : null;
|
|
1186
|
+
if (kind) this.#toServe(`${JSON.stringify({ jsonrpc: '2.0', id: joinId(INTERNAL_CONNECTION, `late-${late}`), method: `harness.v1.sessions.${kind}.unsubscribe`, params: { subscription: late } })}\n`);
|
|
1165
1187
|
return;
|
|
1166
1188
|
}
|
|
1167
1189
|
this.internalRequests.delete(original);
|
|
@@ -3419,7 +3441,8 @@ export class TeamsMachine extends MachineCore {
|
|
|
3419
3441
|
case 'harness.v1.mail.handover': {
|
|
3420
3442
|
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is handed over here by this machine\'s own processes'), { code: 403 });
|
|
3421
3443
|
if (typeof params.address !== 'string' || typeof params.id !== 'string' || !parseAddress(params.address)) throw Object.assign(new Error('a hand-over names an address and a message id'), { code: 400 });
|
|
3422
|
-
|
|
3444
|
+
// a hand-over is moved to cur/ once delivered whatever `mark_read` says (MailSend.handOver)
|
|
3445
|
+
return this.mailSend.handOver(params.address, params.id, { wake: params.wake === true });
|
|
3423
3446
|
}
|
|
3424
3447
|
// harness serve's `sessions.message` (its mail to a session or an agent, not its `as_user` turn), asked here by
|
|
3425
3448
|
// harness serve itself: one implementation of a send, whatever the entry.
|
|
@@ -3467,7 +3490,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
3467
3490
|
address = found.address;
|
|
3468
3491
|
}
|
|
3469
3492
|
const machine = address.split(':')[1];
|
|
3470
|
-
if (machine === machineName(this.env))
|
|
3493
|
+
if (machine === machineName(this.env)) {
|
|
3494
|
+
const rows = await this.mail.inboxRows(address, last);
|
|
3495
|
+
const kept = inboxKept(rows.length, (index) => Buffer.byteLength(JSON.stringify(rows[index])));
|
|
3496
|
+
const left = rows.length - kept.length;
|
|
3497
|
+
return { code: 0, address, rows: [...(left ? [{ direction: 'note', omitted: left, rendered: inboxNote(left) }] : []), ...kept.map((index) => rows[index])] };
|
|
3498
|
+
}
|
|
3471
3499
|
try {
|
|
3472
3500
|
const answer = await this.#askMailDoor(machine, { op: 'inbox', session: address, recent: last });
|
|
3473
3501
|
return answer?.code === 0 ? { code: 0, address, rows: Array.isArray(answer.rows) ? answer.rows : [] } : { code: 3, text: answer?.text ?? 'its machine refused' };
|
|
@@ -3490,7 +3518,14 @@ export class TeamsMachine extends MachineCore {
|
|
|
3490
3518
|
this.inboxClaims.set(token, { address: caller, claimed: read.claimed });
|
|
3491
3519
|
}
|
|
3492
3520
|
// `plain`: each message as its reader is shown it (the envelope rendered), with no listing's line about when
|
|
3493
|
-
|
|
3521
|
+
const plain = read.stored.map((item) => render(item.envelope));
|
|
3522
|
+
const claimedIds = new Set(read.claimed.map((item) => item.envelope.id));
|
|
3523
|
+
const kept = inboxKept(read.stored.length,
|
|
3524
|
+
(index) => Buffer.byteLength(read.rendered[index] ?? '') + Buffer.byteLength(plain[index] ?? '') + Buffer.byteLength(JSON.stringify(read.rows[index] ?? null)),
|
|
3525
|
+
(index) => claimedIds.has(read.stored[index].envelope.id));
|
|
3526
|
+
const left = read.stored.length - kept.length;
|
|
3527
|
+
const note = left ? [inboxNote(left)] : [];
|
|
3528
|
+
return { code: 0, address: caller, rendered: [...note, ...kept.map((index) => read.rendered[index])], plain: [...note, ...kept.map((index) => plain[index])], rows: kept.map((index) => read.rows[index]), omitted: left, token };
|
|
3494
3529
|
}
|
|
3495
3530
|
// Who a local client is, from its claim (live-sessions.mjs callerOf): what a command acting on another machine sends
|
|
3496
3531
|
// there as its caller, resolved here, where the map that holds it is. Local only; never the app door's.
|
package/mail/send.mjs
CHANGED
|
@@ -52,6 +52,7 @@ export class MailSend {
|
|
|
52
52
|
*/
|
|
53
53
|
constructor({ machine, mailRoot, live, store, agents, relays, askMachine, deliverRuntime, codexAnswer, typeTurn, log = () => {} }) {
|
|
54
54
|
Object.assign(this, { machine, mailRoot, live, store, agents, relays, askMachine, deliverRuntime, codexAnswer, typeTurn, log });
|
|
55
|
+
this.handing = new Set(); // `<address>\n<id>` of each hand-over running here (handOver)
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// ---- one send ----------------------------------------------------------------------------------------------------
|
|
@@ -367,9 +368,10 @@ export class MailSend {
|
|
|
367
368
|
|
|
368
369
|
async #deliverPlanned(caller, sent, plan, { queue, notify_when_idle }) {
|
|
369
370
|
const reached = [], copied = [], failed = [];
|
|
371
|
+
let recorded = false, held = false;
|
|
370
372
|
if (plan.agent) {
|
|
371
373
|
// the agent's own mailbox keeps the record of what was addressed to it
|
|
372
|
-
try { this.store.deliverRead(plan.agent, sent); } catch (error) { failed.push(`the agent's mailbox (${error.message})`); }
|
|
374
|
+
try { this.store.deliverRead(plan.agent, sent); recorded = true; } catch (error) { failed.push(`the agent's mailbox (${error.message})`); }
|
|
373
375
|
}
|
|
374
376
|
const sessions = this.live.sessions();
|
|
375
377
|
for (const recipient of plan.recipients.filter((item) => item.address !== caller.address)) {
|
|
@@ -380,15 +382,35 @@ export class MailSend {
|
|
|
380
382
|
continue;
|
|
381
383
|
}
|
|
382
384
|
try { reached.push(`${label} (${await this.#deliverWoken(sent, recipient.address, sessions, queue, notify_when_idle)})`); }
|
|
383
|
-
catch (error) {
|
|
385
|
+
catch (error) {
|
|
386
|
+
held ||= error.unanswered === true;
|
|
387
|
+
failed.push(error.unanswered ? `${label} (unanswered, whether it arrived is unknown: ${error.message})` : `${label} (${error.message})`);
|
|
388
|
+
}
|
|
384
389
|
}
|
|
385
390
|
const thread = plan.thread ? ` Thread ${shortId(plan.thread.id)}.` : '';
|
|
391
|
+
// agent mail that reached nobody is never left only as a read record: its copy in the agent's mailbox goes back
|
|
392
|
+
// unread with a wake, and the carrier hands it over again (handOver plans it anew) after its backoff, until a
|
|
393
|
+
// recipient has it or the wake expires. Not when a recipient's own copy is held unread with its wake (an unanswered
|
|
394
|
+
// relay), which hands it over itself: two wakes would deliver it twice
|
|
395
|
+
let waiting = false;
|
|
396
|
+
if (recorded && (reached.length || copied.length)) {
|
|
397
|
+
// reached: its record is read (one a retry handed over was unread)
|
|
398
|
+
try { const record = this.store.findIn(plan.agent, sent.id); if (record?.state === 'unread') this.store.markRead(plan.agent, record.path); } catch { /* kept unread */ }
|
|
399
|
+
} else if (recorded && !held) {
|
|
400
|
+
try {
|
|
401
|
+
const record = this.store.findIn(plan.agent, sent.id);
|
|
402
|
+
if (record?.state === 'read') this.store.markUnread(plan.agent, record.path);
|
|
403
|
+
this.store.requestWake(plan.agent, sent.id, { keep: true });
|
|
404
|
+
waiting = true;
|
|
405
|
+
} catch (error) { failed.push(`its retry (${error.message})`); }
|
|
406
|
+
}
|
|
386
407
|
let text;
|
|
387
|
-
if (
|
|
408
|
+
if (waiting) text = `not sent yet: nobody in its thread could be reached, so it waits unread in ${plan.agent}'s mailbox and is handed over again when one can be. Message id ${shortId(sent.id)}.${thread} Don't poll; carry on.`;
|
|
409
|
+
else if (!reached.length && !copied.length) text = `Not sent: nobody in its thread could be reached.${thread}`;
|
|
388
410
|
else text = `sent to ${reached.length ? reached.join(', ') : 'no one woken'}.${copied.length ? ` CC (filed, not woken): ${copied.join(', ')}.` : ''} Message id ${shortId(sent.id)}.${thread} Don't poll; carry on.`;
|
|
389
411
|
if (failed.length) text += ` Not delivered to: ${failed.join('; ')}.`;
|
|
390
|
-
const code = reached.length || copied.length ? 0 : EXIT_FAILED;
|
|
391
|
-
return outcome(code, text, code === 0 ? { delivered: true, message_ids: [sent.id], receipt: 'agent-mailbox' } : null);
|
|
412
|
+
const code = reached.length || copied.length || waiting ? 0 : EXIT_FAILED;
|
|
413
|
+
return { ...outcome(code, text, code === 0 ? { delivered: true, message_ids: [sent.id], receipt: 'agent-mailbox', ...(waiting ? { waiting } : {}) } : null), waiting };
|
|
392
414
|
}
|
|
393
415
|
|
|
394
416
|
/** `sent` filed unread in `address`'s mailbox on this machine's mail, without waking it (mail_agent.rs `file_unread`):
|
|
@@ -665,22 +687,33 @@ export class MailSend {
|
|
|
665
687
|
/**
|
|
666
688
|
* A message filed unread in `address`'s mailbox handed to its session's door, once (mail_watch.rs `carry` and message.rs
|
|
667
689
|
* `push`, and the native watch's notices): only a door that hands it to the session (a relay, a runtime) takes it, a
|
|
668
|
-
* hook's is woken when `wake`, and an agent's mail is planned
|
|
669
|
-
*
|
|
690
|
+
* hook's is woken when `wake`, and an agent's mail is planned. One handed over is moved to `cur/` here, whoever asked:
|
|
691
|
+
* its receiver has it, so its next inbox read never shows it again (the carrier marks it read only when its own call
|
|
692
|
+
* answers delivered, which a hand-over past its bound never does). The
|
|
693
|
+
* hand-over is recorded on its wake before it starts, so one interrupted mid-way (its daemon ended) is never handed over
|
|
694
|
+
* again; one still running here, its carrier past its bound, answers 'later' until it settles. Answers
|
|
670
695
|
* `{ outcome: 'delivered' | 'filed' | 'later', detail }`.
|
|
671
696
|
*/
|
|
672
|
-
async handOver(address, id, {
|
|
697
|
+
async handOver(address, id, { wake = false } = {}) {
|
|
673
698
|
const found = this.store.findIn(address, id);
|
|
674
699
|
if (!found) return { outcome: 'filed', detail: 'not in its mailbox' };
|
|
675
700
|
const sent = readJson(found.path);
|
|
676
701
|
if (!sent) return { outcome: 'later', detail: 'its envelope could not be read' };
|
|
677
702
|
if (found.state !== 'unread') return { outcome: 'filed', detail: 'already read' };
|
|
703
|
+
// one already handed to its receiver's door (that delivery's claim, the door's or the carrier's: a daemon that ended
|
|
704
|
+
// between the delivery and moving the copy left it unread) is moved to `cur/`, never handed over again. A reader's
|
|
705
|
+
// claim is not one: a reader that ended before reading put the copy back unread, and it is handed over
|
|
706
|
+
const claim = this.store.claimOf(address, id);
|
|
707
|
+
if (claim?.recipient === true && (claim.via === 'door' || claim.via === 'carrier')) {
|
|
708
|
+
this.store.markRead(address, found.path);
|
|
709
|
+
return { outcome: 'filed', detail: 'already handed over' };
|
|
710
|
+
}
|
|
678
711
|
const parsed = parseAddress(address);
|
|
679
712
|
if (parsed?.harness === AGENT_HARNESS) {
|
|
680
713
|
const plan = await this.#plan(sent, address, {});
|
|
681
714
|
if (!plan.plan) return { outcome: 'filed', detail: plan.outcome?.text ?? 'no plan' };
|
|
682
715
|
const done = await this.#deliverPlanned({ address: sent.from, name: sent.from_name }, sent, plan.plan, { queue: false, notify_when_idle: false });
|
|
683
|
-
return done.code === 0 ? { outcome: 'delivered', detail: done.text } : { outcome: 'later', detail: done.text };
|
|
716
|
+
return done.code === 0 && !done.waiting ? { outcome: 'delivered', detail: done.text } : { outcome: 'later', detail: done.text };
|
|
684
717
|
}
|
|
685
718
|
const door = this.#door(address, this.live.sessions());
|
|
686
719
|
if (!door) return { outcome: 'later', detail: 'its session is not running' };
|
|
@@ -691,11 +724,18 @@ export class MailSend {
|
|
|
691
724
|
return { outcome: 'filed', detail: `its door is ${door.kind}` };
|
|
692
725
|
}
|
|
693
726
|
const state = this.store.wakeState(address, id);
|
|
727
|
+
// one still running here (its carrier gave up on its answer, past its bound) is waited for: the wake stays and is
|
|
728
|
+
// tried again after its backoff, settled by that hand-over's own outcome
|
|
729
|
+
const key = `${address}\n${id}`;
|
|
730
|
+
if (this.handing.has(key)) return { outcome: 'later', detail: 'its hand-over is still in progress' };
|
|
694
731
|
if (state.handover_at_ms != null) return { outcome: 'filed', detail: 'an earlier hand-over never settled' };
|
|
695
732
|
this.store.setWakeState(address, id, { ...state, handover_at_ms: Date.now() });
|
|
733
|
+
this.handing.add(key);
|
|
696
734
|
try {
|
|
697
|
-
await this.route(sent, address, door, true, false);
|
|
698
|
-
|
|
735
|
+
const how = await this.route(sent, address, door, true, false);
|
|
736
|
+
// handed to the session (or already its): moved to cur/ (one only queued for a runtime not woken would stay unread)
|
|
737
|
+
const now = this.store.findIn(address, id);
|
|
738
|
+
if (how !== 'queued' && now?.state === 'unread') this.store.markRead(address, now.path);
|
|
699
739
|
return { outcome: 'delivered', detail: null };
|
|
700
740
|
} catch (error) {
|
|
701
741
|
// a message its door can never take (too long for a relay) stays filed for the session to read
|
|
@@ -704,6 +744,7 @@ export class MailSend {
|
|
|
704
744
|
if (error.unanswered) return { outcome: 'later', detail: `unanswered: ${error.message}` };
|
|
705
745
|
return error instanceof Refused ? { outcome: 'filed', detail: error.kind } : { outcome: 'later', detail: error.message };
|
|
706
746
|
} finally {
|
|
747
|
+
this.handing.delete(key);
|
|
707
748
|
const after = this.store.wakeState(address, id);
|
|
708
749
|
this.store.setWakeState(address, id, { ...after, handover_at_ms: null });
|
|
709
750
|
}
|
package/mail/store.mjs
CHANGED
|
@@ -403,12 +403,14 @@ export class MailStore extends EventEmitter {
|
|
|
403
403
|
renameSync(path, join(box.dir, 'new', path.split(/[\\/]/).pop()));
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
/** A wake kept beside the envelope (mailbox.rs `request_wake`): its watcher hands it to the session's door.
|
|
407
|
-
|
|
406
|
+
/** A wake kept beside the envelope (mailbox.rs `request_wake`): its watcher hands it to the session's door. With
|
|
407
|
+
* `keep`, a wake already there is kept as it is (its attempts and backoff so far), never made fresh again. */
|
|
408
|
+
requestWake(address, id, { keep = false } = {}) {
|
|
408
409
|
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('invalid wake message id');
|
|
409
410
|
const box = this.open(address);
|
|
410
411
|
mkdirSync(join(box.dir, 'wake'), { recursive: true });
|
|
411
|
-
writeFileSync(join(box.dir, 'wake', id), '');
|
|
412
|
+
try { writeFileSync(join(box.dir, 'wake', id), '', { flag: keep ? 'wx' : 'w' }); }
|
|
413
|
+
catch (error) { if (!keep || error.code !== 'EEXIST') throw error; }
|
|
412
414
|
}
|
|
413
415
|
|
|
414
416
|
/** A wake's delivery so far (mailbox.rs `wake_state`): an empty or absent file is a fresh wake. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.101",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|