@volter/supercode-teams 0.3.126 → 0.3.127

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/mail/relay.mjs CHANGED
@@ -371,6 +371,18 @@ export class Relays {
371
371
  * (claude_relay.rs `send_through_relay`): the receiver is named by its own socket (`uds:<path>`), unique to it, never
372
372
  * by its name. One send at a time per relay; a relay whose process is proven dead is started again once.
373
373
  */
374
+ /** Whether `sender`'s relay still holds a send queued (its queue file, written within SEND_TIMEOUT_MS twice over): a
375
+ * relay runtime outlives a daemon that ended mid-send and may still make it. */
376
+ sending(sender) {
377
+ try { return Date.now() - statSync(this.spec(sender).queue).mtimeMs < 2 * SEND_TIMEOUT_MS; } catch { return false; }
378
+ }
379
+
380
+ /** Whether message `messageId` from `sender` landed in Claude session `sessionId`'s transcript, as a relay send reads
381
+ * an unconfirmed outcome (send.mjs handOver asks it of a hand-over the daemon did not live to settle). */
382
+ async landed(sessionId, messageId, sender) {
383
+ return this.arrived(sessionId, `cross-session-message id=\\"${shortId(messageId)}\\" from=\\"${sender}\\"`);
384
+ }
385
+
374
386
  async send(sender, senderName, receiver, message, messageId) {
375
387
  const spec = this.spec(sender, senderName);
376
388
  const asked = performance.now();
package/mail/send.mjs CHANGED
@@ -25,6 +25,12 @@ export function unansweredText(where, detail, id) {
25
25
 
26
26
  /** How long a send waits, in a daemon's first moments, for its Codex sessions to be known before it resolves a receiver. */
27
27
  const CODEX_KNOWN_WAIT_MS = 20_000;
28
+ // A relayed message's wake is due this long after its relay send begins: a send still running here holds it (handOver),
29
+ // so it fires only for a send the daemon did not live to finish (t_b0f9a2a0)
30
+ const RELAY_GRACE_MS = 30_000;
31
+ /** The kinds of delivery that hand nothing over (#deliverWoken's `how`): the copy waits filed. An operator's mailbox,
32
+ * which its operator reads, and a session's hook, which shows it, are deliveries. */
33
+ const LEFT_FILED = new Set(['queued', 'stored', 'pane_held', 'pane_waits', 'not_running']);
28
34
 
29
35
  /** Replies in one unbroken chain between two sessions beyond which another is refused. */
30
36
  const REPLY_CHAIN_LIMIT = 32;
@@ -53,6 +59,7 @@ export class MailSend {
53
59
  constructor({ machine, mailRoot, live, store, agents, relays, askMachine, deliverRuntime, codexAnswer, typeTurn, log = () => {} }) {
54
60
  Object.assign(this, { machine, mailRoot, live, store, agents, relays, askMachine, deliverRuntime, codexAnswer, typeTurn, log });
55
61
  this.handing = new Set(); // `<address>\n<id>` of each hand-over running here (handOver)
62
+ this.relaying = new Set(); // `<address>\n<id>` of each relay send running here (route): its wake waits for it
56
63
  }
57
64
 
58
65
  // ---- one send ----------------------------------------------------------------------------------------------------
@@ -319,11 +326,21 @@ export class MailSend {
319
326
  const handed = sent.reply_via?.mode === 'command' && door.session.tools ? { ...sent, reply_via: { mode: 'tool' } } : sent;
320
327
  const text = render(handed);
321
328
  if (Buffer.byteLength(text) > MAX_RELAYED_BYTES) throw new Refused('too_long', Buffer.byteLength(text));
322
- // filed before it is handed over: a session that reads its mailbox at its first tool call finds it; a relay that
323
- // fails takes back the copy this delivery filed
329
+ // filed before it is handed over, unread, with a wake due RELAY_GRACE_MS from now: a session that reads its mailbox
330
+ // at its first tool call finds it, and a daemon that ends before the relay answers (an upgrade's restart, a crash)
331
+ // leaves it for the carrier, which hands it over again (t_b0f9a2a0: filed read before, such a send was lost). It is
332
+ // read once the relay has made it; a relay that fails takes back the copy this delivery filed
324
333
  const filedHere = this.store.findIn(to, handed.id) == null;
325
- const filed = this.store.deliverRead(to, handed);
326
- const relayed = await this.relays.send(handed.from, handed.from_name, { sessionId: door.session.address.split(':').slice(3).join(':'), name: door.session.name.split('@')[0], socket: door.session.socket }, text, handed.id);
334
+ const filed = this.store.deliver(to, handed);
335
+ if (wake && !this.store.hasWake(to, handed.id)) {
336
+ // its hand-over begins now: a daemon that restarts reads the receiver's transcript before sending it again
337
+ try { this.store.requestWake(to, handed.id); this.store.setWakeState(to, handed.id, { attempts: 0, next_at_ms: Date.now() + RELAY_GRACE_MS, handover_at_ms: Date.now() }); } catch { /* the relay's own outcome stands */ }
338
+ }
339
+ const key = `${to}\n${handed.id}`;
340
+ this.relaying.add(key);
341
+ let relayed;
342
+ try { relayed = await this.relays.send(handed.from, handed.from_name, { sessionId: door.session.address.split(':').slice(3).join(':'), name: door.session.name.split('@')[0], socket: door.session.socket }, text, handed.id); }
343
+ finally { this.relaying.delete(key); }
327
344
  // a send the relay may have made keeps its copy and is unanswered, never taken back and said not sent; only one the
328
345
  // relay is known not to have made (refused, never handed in) takes its copy back. One its receiver read from its
329
346
  // mailbox meanwhile (its recipient claim) has reached it. Otherwise the relay can no longer make it (its queue is
@@ -331,7 +348,8 @@ export class MailSend {
331
348
  // goes back unread, for the receiver's own mailbox read and the mailbox's wake, which hands it over again
332
349
  const received = this.store.claimOf(to, handed.id)?.recipient === true;
333
350
  if (!relayed.delivered && relayed.unknown && !received) {
334
- if (filedHere) { try { this.store.markUnread(to, filed); if (wake) this.store.requestWake(to, handed.id); } catch { /* kept read */ } }
351
+ // kept unread, its wake due now: the carrier hands it over again after its backoff
352
+ if (filedHere && wake) { try { this.store.requestWake(to, handed.id); } catch { /* the wake filed above stands */ } }
335
353
  throw Object.assign(new Error(relayed.detail), { unanswered: true });
336
354
  }
337
355
  if (!relayed.delivered && !received) { if (filedHere) { try { unlinkSync(filed); } catch { /* gone */ } } throw new Error(relayed.detail); }
@@ -378,10 +396,19 @@ export class MailSend {
378
396
  const reached = [], copied = [], failed = [];
379
397
  let recorded = false;
380
398
  const held = []; // recipients whose own copy is held unread with its wake (an unanswered relay)
399
+ let agentKey = null;
381
400
  if (plan.agent) {
382
- // the agent's own mailbox keeps the record of what was addressed to it
383
- try { this.store.deliverRead(plan.agent, sent); recorded = true; } catch (error) { failed.push(`the agent's mailbox (${error.message})`); }
401
+ // the agent's own mailbox keeps the record of what was addressed to it: unread, with a wake due RELAY_GRACE_MS on,
402
+ // until a recipient has it, so a daemon that ends while the plan runs (a resume, a relay send, another machine
403
+ // asked) leaves it for the carrier to plan again (#1264 review P1). While this plan runs its wake waits for it
404
+ try {
405
+ const fresh = this.store.findIn(plan.agent, sent.id) == null;
406
+ this.store.deliver(plan.agent, sent); recorded = true;
407
+ if (fresh && !this.store.hasWake(plan.agent, sent.id)) { this.store.requestWake(plan.agent, sent.id); this.store.setWakeState(plan.agent, sent.id, { attempts: 0, next_at_ms: Date.now() + RELAY_GRACE_MS }); }
408
+ agentKey = `${plan.agent}\n${sent.id}`; this.relaying.add(agentKey);
409
+ } catch (error) { failed.push(`the agent's mailbox (${error.message})`); }
384
410
  }
411
+ try {
385
412
  const sessions = this.live.sessions();
386
413
  for (const recipient of plan.recipients.filter((item) => item.address !== caller.address)) {
387
414
  const label = this.#label(sessions, recipient.address);
@@ -390,7 +417,16 @@ export class MailSend {
390
417
  try { this.#fileUnread(recipient.address, sent); copied.push(label); } catch (error) { failed.push(`${label} (${error.message})`); }
391
418
  continue;
392
419
  }
393
- try { reached.push(`${label} (${await this.#deliverWoken(sent, recipient.address, sessions, queue, notify_when_idle)})`); }
420
+ try {
421
+ const { how, text: said } = await this.#deliverWoken(sent, recipient.address, sessions, queue, notify_when_idle);
422
+ // only a hand-over is a reach: a copy left filed with nothing to carry it (queued, stored, held in the daemon's
423
+ // memory for a pane, or for a session not running) is held when its own wake carries it, and otherwise the
424
+ // agent's copy keeps its retry (t_b0f9a2a0: such a send was counted reached, and nothing carried it). One the
425
+ // sender asked to queue (--queue) is held as asked: its receiver reads it at its next turn
426
+ if (!LEFT_FILED.has(how)) reached.push(`${label} (${said})`);
427
+ else if (this.store.hasWake(recipient.address, sent.id) || (queue && how === 'queued')) held.push(`${label} (${said})`);
428
+ else failed.push(`${label} (${said}; handed over again when it can be)`);
429
+ }
394
430
  catch (error) {
395
431
  // held only when its copy is here, unread: its wake hands it over, or (queued, no wake) the receiver's own read
396
432
  // does. An unanswered request to another machine's door holds nothing here, so it is a failure, and the agent's
@@ -400,6 +436,7 @@ export class MailSend {
400
436
  else failed.push(error.unanswered ? `${label} (unanswered, whether it arrived is unknown: ${error.message})` : `${label} (${error.message})`);
401
437
  }
402
438
  }
439
+ } finally { if (agentKey) this.relaying.delete(agentKey); }
403
440
  const thread = plan.thread ? ` Thread ${shortId(plan.thread.id)}.` : '';
404
441
  // agent mail that reached nobody is never left only as a read record: its copy in the agent's mailbox goes back
405
442
  // unread with a wake, and the carrier hands it over again (handOver plans it anew) after its backoff, until a
@@ -450,18 +487,18 @@ export class MailSend {
450
487
  if (cameThroughLink(sent)) throw new Error(`not carried on to ${door.machine}: a message that came to this machine through a link is for its own sessions only`);
451
488
  const answer = await this.askMachine(door.machine, { op: 'file', to, envelope: sent });
452
489
  if (answer?.code !== 0) throw new Error(typeof answer?.text === 'string' ? answer.text : 'the other machine refused the message');
453
- return 'filed on its machine';
490
+ return { how: 'other_machine', text: 'filed on its machine' };
454
491
  }
455
492
  if (!door) {
456
493
  // a reply to a closed session resumes it; only one the mailbox launched, the way it was launched
457
494
  this.store.deliver(to, sent);
458
- if (!(await this.agents.resumable(to))) return 'stopped; not resumed, since the mailbox did not launch it: the message waits in its mailbox';
495
+ if (!(await this.agents.resumable(to))) return { how: 'not_running', text: 'stopped; not resumed, since the mailbox did not launch it: the message waits in its mailbox' };
459
496
  try { await this.agents.resume(to); } catch (error) { throw new Error(`not resumed: ${error.message}`); }
460
497
  const started = Date.now();
461
498
  for (;;) {
462
499
  door = this.#door(to, this.live.sessions());
463
500
  if (door) break;
464
- if (Date.now() - started > RESUME_WAIT_MS) return 'resumed; the message waits in its mailbox';
501
+ if (Date.now() - started > RESUME_WAIT_MS) return { how: 'not_running', text: 'resumed; the message waits in its mailbox' };
465
502
  await sleep(1000);
466
503
  }
467
504
  }
@@ -472,7 +509,7 @@ export class MailSend {
472
509
  if (error instanceof Refused) throw new Error(`${error.bytes} bytes; the limit is ${MAX_RELAYED_BYTES}`);
473
510
  throw error;
474
511
  }
475
- return { steered: 'steered into its running turn', started: 'started a turn', native_busy: 'read at its next tool call', native_idle: 'starts its next turn', hooked: 'in its mailbox, its hook shows it', queued: 'waits in its mailbox', operator: 'filed', already: 'it already had it', stored: 'stored; it has no delivery door', typed: 'typed into its pane', pane_held: 'held for its pane until its composer is free', pane_waits: 'waits in its mailbox (its composer was not empty)' }[how];
512
+ return { how, text: { steered: 'steered into its running turn', started: 'started a turn', native_busy: 'read at its next tool call', native_idle: 'starts its next turn', hooked: 'in its mailbox, its hook shows it', queued: 'waits in its mailbox', operator: 'filed', already: 'it already had it', stored: 'stored; it has no delivery door', typed: 'typed into its pane', pane_held: 'held for its pane until its composer is free', pane_waits: 'waits in its mailbox (its composer was not empty)' }[how] };
476
513
  }
477
514
 
478
515
  // ---- a filing --------------------------------------------------------------------------------------------------
@@ -724,6 +761,8 @@ export class MailSend {
724
761
  this.store.markRead(address, found.path);
725
762
  return { outcome: 'filed', detail: 'already handed over' };
726
763
  }
764
+ // a plan or a relay send still running here holds its wake (#1264 review P1)
765
+ if (this.relaying.has(`${address}\n${id}`)) return { outcome: 'later', detail: 'its delivery is still in progress' };
727
766
  const parsed = parseAddress(address);
728
767
  if (parsed?.harness === AGENT_HARNESS) {
729
768
  const plan = await this.#plan(sent, address, {});
@@ -743,8 +782,20 @@ export class MailSend {
743
782
  // one still running here (its carrier gave up on its answer, past its bound) is waited for: the wake stays and is
744
783
  // tried again after its backoff, settled by that hand-over's own outcome
745
784
  const key = `${address}\n${id}`;
746
- if (this.handing.has(key)) return { outcome: 'later', detail: 'its hand-over is still in progress' };
747
- if (state.handover_at_ms != null) return { outcome: 'filed', detail: 'an earlier hand-over never settled' };
785
+ if (this.handing.has(key) || this.relaying.has(key)) return { outcome: 'later', detail: 'its hand-over is still in progress' };
786
+ // an earlier hand-over never settled (the daemon ended mid-way): one its receiver's transcript holds landed, and is
787
+ // read; any other is handed over again, never given up (t_b0f9a2a0)
788
+ if (state.handover_at_ms != null && door.kind === 'native') {
789
+ // a relay that outlived its daemon may still be making that send (its queue still there, fresh): waited for, never
790
+ // sent beside it (#1264 review P2a)
791
+ if (this.relays.sending?.(sent.from, id)) return { outcome: 'later', detail: 'its relay may still be making it' };
792
+ let landed = false;
793
+ try { landed = await this.relays.landed(door.session.address.split(':').slice(3).join(':'), id, sent.from); } catch { /* unread: hand it over */ }
794
+ if (landed) {
795
+ try { this.store.claimFor(address, id, { pid: process.pid, via: 'door', caller: null, recipient: true, at_ms: Date.now() }); this.store.markRead(address, found.path); } catch { /* moved meanwhile */ }
796
+ return { outcome: 'filed', detail: 'an earlier hand-over landed' };
797
+ }
798
+ }
748
799
  this.store.setWakeState(address, id, { ...state, handover_at_ms: Date.now() });
749
800
  this.handing.add(key);
750
801
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.126",
3
+ "version": "0.3.127",
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",