@volter/supercode-teams 0.3.100 → 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 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: an index it opened is registered in serve with nobody reading
1162
- // it (one more at each retry), so it is let go at once
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
- if (typeof late === 'string' && late.startsWith('index-sub-')) this.#toServe(`${JSON.stringify({ jsonrpc: '2.0', id: joinId(INTERNAL_CONNECTION, `late-${late}`), method: 'harness.v1.sessions.index.unsubscribe', params: { subscription: late } })}\n`);
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,12 @@ 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)) return { code: 0, address, rows: await this.mail.inboxRows(address, last) };
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
+ }
3472
3499
  try {
3473
3500
  const answer = await this.#askMailDoor(machine, { op: 'inbox', session: address, recent: last });
3474
3501
  return answer?.code === 0 ? { code: 0, address, rows: Array.isArray(answer.rows) ? answer.rows : [] } : { code: 3, text: answer?.text ?? 'its machine refused' };
@@ -3491,7 +3518,14 @@ export class TeamsMachine extends MachineCore {
3491
3518
  this.inboxClaims.set(token, { address: caller, claimed: read.claimed });
3492
3519
  }
3493
3520
  // `plain`: each message as its reader is shown it (the envelope rendered), with no listing's line about when
3494
- return { code: 0, address: caller, rendered: read.rendered, plain: read.stored.map((item) => render(item.envelope)), rows: read.rows, token };
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 };
3495
3529
  }
3496
3530
  // Who a local client is, from its claim (live-sessions.mjs callerOf): what a command acting on another machine sends
3497
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
@@ -368,9 +368,10 @@ export class MailSend {
368
368
 
369
369
  async #deliverPlanned(caller, sent, plan, { queue, notify_when_idle }) {
370
370
  const reached = [], copied = [], failed = [];
371
+ let recorded = false, held = false;
371
372
  if (plan.agent) {
372
373
  // 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})`); }
374
+ try { this.store.deliverRead(plan.agent, sent); recorded = true; } catch (error) { failed.push(`the agent's mailbox (${error.message})`); }
374
375
  }
375
376
  const sessions = this.live.sessions();
376
377
  for (const recipient of plan.recipients.filter((item) => item.address !== caller.address)) {
@@ -381,15 +382,35 @@ export class MailSend {
381
382
  continue;
382
383
  }
383
384
  try { reached.push(`${label} (${await this.#deliverWoken(sent, recipient.address, sessions, queue, notify_when_idle)})`); }
384
- catch (error) { failed.push(error.unanswered ? `${label} (unanswered, whether it arrived is unknown: ${error.message})` : `${label} (${error.message})`); }
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
+ }
385
389
  }
386
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
+ }
387
407
  let text;
388
- if (!reached.length && !copied.length) text = `Not sent: nobody in its thread could be reached.${thread}`;
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}`;
389
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.`;
390
411
  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);
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 };
393
414
  }
394
415
 
395
416
  /** `sent` filed unread in `address`'s mailbox on this machine's mail, without waking it (mail_agent.rs `file_unread`):
@@ -692,7 +713,7 @@ export class MailSend {
692
713
  const plan = await this.#plan(sent, address, {});
693
714
  if (!plan.plan) return { outcome: 'filed', detail: plan.outcome?.text ?? 'no plan' };
694
715
  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 };
716
+ return done.code === 0 && !done.waiting ? { outcome: 'delivered', detail: done.text } : { outcome: 'later', detail: done.text };
696
717
  }
697
718
  const door = this.#door(address, this.live.sessions());
698
719
  if (!door) return { outcome: 'later', detail: 'its session is not running' };
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
- requestWake(address, id) {
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.100",
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",