@volter/supercode-teams 0.3.100 → 0.3.102
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 +40 -5
- package/mail/send.mjs +42 -7
- package/mail/store.mjs +11 -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 unread ones always, and a first line says how
|
|
306
|
+
// many older read 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 read 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);
|
|
@@ -3468,7 +3490,13 @@ export class TeamsMachine extends MachineCore {
|
|
|
3468
3490
|
address = found.address;
|
|
3469
3491
|
}
|
|
3470
3492
|
const machine = address.split(':')[1];
|
|
3471
|
-
if (machine === machineName(this.env))
|
|
3493
|
+
if (machine === machineName(this.env)) {
|
|
3494
|
+
const rows = await this.mail.inboxRows(address, last);
|
|
3495
|
+
// an unread message to it is always listed, as its own inbox lists its claimed ones
|
|
3496
|
+
const kept = inboxKept(rows.length, (index) => Buffer.byteLength(JSON.stringify(rows[index])), (index) => rows[index].direction === 'in' && rows[index].state === 'unread');
|
|
3497
|
+
const left = rows.length - kept.length;
|
|
3498
|
+
return { code: 0, address, rows: [...(left ? [{ direction: 'note', omitted: left, rendered: inboxNote(left) }] : []), ...kept.map((index) => rows[index])] };
|
|
3499
|
+
}
|
|
3472
3500
|
try {
|
|
3473
3501
|
const answer = await this.#askMailDoor(machine, { op: 'inbox', session: address, recent: last });
|
|
3474
3502
|
return answer?.code === 0 ? { code: 0, address, rows: Array.isArray(answer.rows) ? answer.rows : [] } : { code: 3, text: answer?.text ?? 'its machine refused' };
|
|
@@ -3491,7 +3519,14 @@ export class TeamsMachine extends MachineCore {
|
|
|
3491
3519
|
this.inboxClaims.set(token, { address: caller, claimed: read.claimed });
|
|
3492
3520
|
}
|
|
3493
3521
|
// `plain`: each message as its reader is shown it (the envelope rendered), with no listing's line about when
|
|
3494
|
-
|
|
3522
|
+
const plain = read.stored.map((item) => render(item.envelope));
|
|
3523
|
+
const claimedIds = new Set(read.claimed.map((item) => item.envelope.id));
|
|
3524
|
+
const kept = inboxKept(read.stored.length,
|
|
3525
|
+
(index) => Buffer.byteLength(read.rendered[index] ?? '') + Buffer.byteLength(plain[index] ?? '') + Buffer.byteLength(JSON.stringify(read.rows[index] ?? null)),
|
|
3526
|
+
(index) => claimedIds.has(read.stored[index].envelope.id));
|
|
3527
|
+
const left = read.stored.length - kept.length;
|
|
3528
|
+
const note = left ? [inboxNote(left)] : [];
|
|
3529
|
+
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 };
|
|
3495
3530
|
}
|
|
3496
3531
|
// Who a local client is, from its claim (live-sessions.mjs callerOf): what a command acting on another machine sends
|
|
3497
3532
|
// 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
|
@@ -335,6 +335,12 @@ export class MailSend {
|
|
|
335
335
|
throw Object.assign(new Error(relayed.detail), { unanswered: true });
|
|
336
336
|
}
|
|
337
337
|
if (!relayed.delivered && !received) { if (filedHere) { try { unlinkSync(filed); } catch { /* gone */ } } throw new Error(relayed.detail); }
|
|
338
|
+
// relayed: a copy filed unread before this delivery (an earlier unanswered or queued one, held for the receiver's
|
|
339
|
+
// own read) is moved to cur/, so that read never hands it over a second time
|
|
340
|
+
if (relayed.delivered && !received) {
|
|
341
|
+
const now = this.store.findIn(to, handed.id);
|
|
342
|
+
if (now?.state === 'unread') { try { this.store.markRead(to, now.path); } catch { /* moved meanwhile */ } }
|
|
343
|
+
}
|
|
338
344
|
delivered = busy ? 'native_busy' : 'native_idle';
|
|
339
345
|
} else if (door.kind === 'hook') {
|
|
340
346
|
this.store.deliver(to, sent);
|
|
@@ -368,9 +374,11 @@ export class MailSend {
|
|
|
368
374
|
|
|
369
375
|
async #deliverPlanned(caller, sent, plan, { queue, notify_when_idle }) {
|
|
370
376
|
const reached = [], copied = [], failed = [];
|
|
377
|
+
let recorded = false;
|
|
378
|
+
const held = []; // recipients whose own copy is held unread with its wake (an unanswered relay)
|
|
371
379
|
if (plan.agent) {
|
|
372
380
|
// the agent's own mailbox keeps the record of what was addressed to it
|
|
373
|
-
try { this.store.deliverRead(plan.agent, sent); } catch (error) { failed.push(`the agent's mailbox (${error.message})`); }
|
|
381
|
+
try { this.store.deliverRead(plan.agent, sent); recorded = true; } catch (error) { failed.push(`the agent's mailbox (${error.message})`); }
|
|
374
382
|
}
|
|
375
383
|
const sessions = this.live.sessions();
|
|
376
384
|
for (const recipient of plan.recipients.filter((item) => item.address !== caller.address)) {
|
|
@@ -381,15 +389,42 @@ export class MailSend {
|
|
|
381
389
|
continue;
|
|
382
390
|
}
|
|
383
391
|
try { reached.push(`${label} (${await this.#deliverWoken(sent, recipient.address, sessions, queue, notify_when_idle)})`); }
|
|
384
|
-
catch (error) {
|
|
392
|
+
catch (error) {
|
|
393
|
+
// held only when its copy is here, unread: its wake hands it over, or (queued, no wake) the receiver's own read
|
|
394
|
+
// does. An unanswered request to another machine's door holds nothing here, so it is a failure, and the agent's
|
|
395
|
+
// own copy waits with its wake (filing there again is filing the same id, which files nothing new)
|
|
396
|
+
const kept = this.store.findIn(recipient.address, sent.id);
|
|
397
|
+
if (error.unanswered && kept?.state === 'unread') held.push(`${label} (unanswered: ${error.message}; ${this.store.hasWake(recipient.address, sent.id) ? 'its wake hands it over' : 'queued: its own mailbox read gives it'})`);
|
|
398
|
+
else failed.push(error.unanswered ? `${label} (unanswered, whether it arrived is unknown: ${error.message})` : `${label} (${error.message})`);
|
|
399
|
+
}
|
|
385
400
|
}
|
|
386
401
|
const thread = plan.thread ? ` Thread ${shortId(plan.thread.id)}.` : '';
|
|
402
|
+
// agent mail that reached nobody is never left only as a read record: its copy in the agent's mailbox goes back
|
|
403
|
+
// unread with a wake, and the carrier hands it over again (handOver plans it anew) after its backoff, until a
|
|
404
|
+
// recipient has it or the wake expires. Not when a recipient's own copy is held unread with its wake (an unanswered
|
|
405
|
+
// relay), which hands it over itself: two wakes would deliver it twice
|
|
406
|
+
let waiting = false;
|
|
407
|
+
if (recorded && (reached.length || copied.length || held.length)) {
|
|
408
|
+
// reached, or held by a recipient's own wake (which hands it over, once): its record is read (one a retry handed
|
|
409
|
+
// over was unread), so its own wake, if any, is settled and never hands it over beside the recipient's
|
|
410
|
+
try { const record = this.store.findIn(plan.agent, sent.id); if (record?.state === 'unread') this.store.markRead(plan.agent, record.path); } catch { /* kept unread */ }
|
|
411
|
+
} else if (recorded) {
|
|
412
|
+
try {
|
|
413
|
+
const record = this.store.findIn(plan.agent, sent.id);
|
|
414
|
+
if (record?.state === 'read') this.store.markUnread(plan.agent, record.path);
|
|
415
|
+
this.store.requestWake(plan.agent, sent.id, { keep: true });
|
|
416
|
+
waiting = true;
|
|
417
|
+
} catch (error) { failed.push(`its retry (${error.message})`); }
|
|
418
|
+
}
|
|
387
419
|
let text;
|
|
388
|
-
|
|
389
|
-
|
|
420
|
+
// a held copy is not said not sent: it is handed over by its wake, so a sender that sent it again would duplicate it
|
|
421
|
+
const heldText = held.length ? ` Held, unanswered, unread in ${held.length === 1 ? 'its receiver\'s' : 'their receivers\''} mailbox, which gives it to the receiver once (don't send it again): ${held.join('; ')}.` : '';
|
|
422
|
+
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.`;
|
|
423
|
+
else if (!reached.length && !copied.length && !held.length) text = `Not sent: nobody in its thread could be reached.${thread}`;
|
|
424
|
+
else text = `sent to ${reached.length ? reached.join(', ') : 'no one woken yet'}.${copied.length ? ` CC (filed, not woken): ${copied.join(', ')}.` : ''}${heldText} Message id ${shortId(sent.id)}.${thread} Don't poll; carry on.`;
|
|
390
425
|
if (failed.length) text += ` Not delivered to: ${failed.join('; ')}.`;
|
|
391
|
-
const code = reached.length || copied.length ? 0 : EXIT_FAILED;
|
|
392
|
-
return outcome(code, text, code === 0 ? { delivered: true, message_ids: [sent.id], receipt: 'agent-mailbox' } : null);
|
|
426
|
+
const code = reached.length || copied.length || held.length || waiting ? 0 : EXIT_FAILED;
|
|
427
|
+
return { ...outcome(code, text, code === 0 ? { delivered: true, message_ids: [sent.id], receipt: 'agent-mailbox', ...(waiting ? { waiting } : {}), ...(held.length && !reached.length && !copied.length ? { unanswered: true } : {}) } : null), waiting };
|
|
393
428
|
}
|
|
394
429
|
|
|
395
430
|
/** `sent` filed unread in `address`'s mailbox on this machine's mail, without waking it (mail_agent.rs `file_unread`):
|
|
@@ -692,7 +727,7 @@ export class MailSend {
|
|
|
692
727
|
const plan = await this.#plan(sent, address, {});
|
|
693
728
|
if (!plan.plan) return { outcome: 'filed', detail: plan.outcome?.text ?? 'no plan' };
|
|
694
729
|
const done = await this.#deliverPlanned({ address: sent.from, name: sent.from_name }, sent, plan.plan, { queue: false, notify_when_idle: false });
|
|
695
|
-
return done.code === 0 ? { outcome: 'delivered', detail: done.text } : { outcome: 'later', detail: done.text };
|
|
730
|
+
return done.code === 0 && !done.waiting ? { outcome: 'delivered', detail: done.text } : { outcome: 'later', detail: done.text };
|
|
696
731
|
}
|
|
697
732
|
const door = this.#door(address, this.live.sessions());
|
|
698
733
|
if (!door) return { outcome: 'later', detail: 'its session is not running' };
|
package/mail/store.mjs
CHANGED
|
@@ -403,12 +403,20 @@ 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; }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Whether a wake is on file for `id` in `address`'s mailbox. */
|
|
417
|
+
hasWake(address, id) {
|
|
418
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id)) return false;
|
|
419
|
+
return existsSync(join(this.open(address).dir, 'wake', id));
|
|
412
420
|
}
|
|
413
421
|
|
|
414
422
|
/** 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.102",
|
|
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",
|