@volter/supercode-teams 0.3.195 → 0.3.197

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.
Files changed (2) hide show
  1. package/mail/send.mjs +70 -42
  2. package/package.json +1 -1
package/mail/send.mjs CHANGED
@@ -357,10 +357,9 @@ export class MailSend {
357
357
  const session = sessions.find((row) => row.address === address);
358
358
  if (!session) return null;
359
359
  if (session.door === 'runtime') return { kind: 'runtime', session };
360
- if (session.door === 'native') {
361
- const pane = paneOf(session);
362
- return pane ? { kind: 'pane', session, pane } : { kind: 'native', session };
363
- }
360
+ // a native Claude session is reached by its relay; its daemon pane, when it has one, is the fallback for a message
361
+ // the relay could not deliver
362
+ if (session.door === 'native') return { kind: 'native', session, pane: paneOf(session) };
364
363
  if (session.door === 'hook') return { kind: 'hook', session, pane: session.pane ?? null, idle: session.status === 'idle' };
365
364
  // a session with no hook and no relay (Grok) in a daemon pane: its mail is typed there, into an empty composer
366
365
  if (session.door === 'pane' && session.pane) return { kind: 'pane', session, pane: session.pane };
@@ -428,7 +427,8 @@ export class MailSend {
428
427
  const [code, what] = {
429
428
  steered: [0, 'steered into its running turn'],
430
429
  started: [0, 'it was idle, so the message started a turn'],
431
- native_waits: [EXIT_STORED, 'Stored: its native session is not known to be idle, so the message waits unread until it is idle. Do not resend'],
430
+ native_busy: [0, 'it will read it at its next tool call'],
431
+ native_waits: [EXIT_STORED, 'Stored: its relay could not deliver it and its session is busy, so its pane fallback waits unread until it is idle. Do not resend'],
432
432
  native_idle: [0, 'it was idle, so the message starts its next turn'],
433
433
  hooked: [0, 'queued in its mailbox; terminal delivery waits for native transcript confirmation'],
434
434
  hook_woken: [0, 'it was idle, so its pane was told to read its mailbox; its hook shows it the message in that turn'],
@@ -441,12 +441,13 @@ export class MailSend {
441
441
  stored: [EXIT_STORED, `Stored (not failed): it has no delivery door, so it sees this only if it runs supercode message inbox${parseAddress(address).harness === 'codex' ? ' (a Codex session gets one with: supercode message setup codex)' : ''}. Don't resend, and don't wait for a reply`],
442
442
  }[delivered];
443
443
  this.#recordSend(caller.address, address, messageId, sentIn, receiver.continuation);
444
+ const viaPane = terminal || (door.kind === 'native' && ['typed', 'pane_held', 'pane_waits'].includes(delivered));
444
445
  const harness = parseAddress(address).harness;
445
446
  const text = code === 0
446
447
  ? `sent to ${name} (${harness}, ${door.kind}): ${what}. Message id ${shortId(messageId)}.${receiver.alias ?? ''}${idleNote} Don't poll; carry on.`
447
448
  : `${name} (${harness}): ${what}. Message id ${shortId(messageId)}.${idleNote}`;
448
- if ((code === 0 || (terminal && code === EXIT_STORED)) && id != null) { try { this.store.setSentMarker(caller.address, messageId, text); } catch { /* the marker is best kept */ } }
449
- return outcome(code, text, code === 0 || terminal ? this.#deliveryReceipt(address, messageId, terminal) : null);
449
+ if ((code === 0 || (viaPane && code === EXIT_STORED)) && id != null) { try { this.store.setSentMarker(caller.address, messageId, text); } catch { /* the marker is best kept */ } }
450
+ return outcome(code, text, code === 0 || viaPane ? this.#deliveryReceipt(address, messageId, viaPane) : null);
450
451
  }
451
452
 
452
453
  /** An agent's plan for `sent` (its thread store decides), or an outcome when the plan refused it. */
@@ -485,17 +486,20 @@ export class MailSend {
485
486
  if (how === 'not_woken') { this.store.deliver(to, sent); delivered = 'queued'; }
486
487
  else { this.store.deliverRead(to, handed); finalReply = answers; delivered = how === 'steered' ? 'steered' : 'started'; }
487
488
  } else if (door.kind === 'native') {
489
+ // one its pane fallback already holds (typed, or queued for its composer) is never handed to the relay as well
490
+ const held = door.pane ? terminalReceipt(this.store, to, sent.id) : null;
491
+ if (held?.delivered) {
492
+ attempt?.begin('existing-native-delivery');
493
+ try { this.store.claimFor(to, sent.id, { pid: process.pid, via: 'door', caller: null, recipient: true, at_ms: Date.now() }); } catch { /* best kept */ }
494
+ const now = this.store.findIn(to, sent.id);
495
+ if (now?.state === 'unread') { try { this.store.markRead(to, now.path); } catch { /* moved meanwhile */ } }
496
+ return 'already';
497
+ }
498
+ if (held?.queued) return 'pane_held';
488
499
  const busy = door.session.status !== 'idle' || door.session.panes_current === false;
489
500
  if (!wake && !busy) throw new Refused('queue');
490
- if (busy) {
491
- attempt?.begin('native-mailbox');
492
- this.store.deliver(to, sent);
493
- this.store.requestWake(to, sent.id, { keep: true });
494
- const state = this.store.wakeState(to, sent.id);
495
- this.store.setWakeState(to, sent.id, { ...state, waiting_for_idle: true, next_at_ms: Date.now() + 300_000 });
496
- if (notifyWhenIdle && !cameThroughLink(sent)) this.store.subscribeIdle(to, { message_id: sent.id, subscriber: sent.from, notice: true, final_reply: false });
497
- return 'native_waits';
498
- }
501
+ // a busy session is handed it at once, as an idle one is: Claude Code's own cross-session message surfaces at the
502
+ // receiver's next tool call (D391). Only the pane fallback below waits for idle, since a busy pane is never typed into
499
503
  // its reply line names the door this session really has: its tools when it runs supercode's MCP server
500
504
  const handed = sent.reply_via?.mode === 'command' && door.session.tools ? { ...sent, reply_via: { mode: 'tool' } } : sent;
501
505
  const text = render(handed);
@@ -543,43 +547,43 @@ export class MailSend {
543
547
  // gone, so its gate refuses a late send) and it is not in the receiver's transcript: a copy this delivery filed
544
548
  // goes back unread, for the receiver's own mailbox read and the mailbox's wake, which hands it over again
545
549
  const received = this.store.claimOf(to, handed.id)?.recipient === true;
546
- if (!relayed.delivered && relayed.unknown && !received) {
550
+ // the relay produced no receipt and cannot send it late (known not made, or not in the receiver's transcript after
551
+ // its bound, its queue gone): a session with a daemon pane has the same copy typed there instead
552
+ if (!relayed.delivered && !received && wake && door.pane && (!relayed.unknown || relayed.landed === false) && busy) {
553
+ // its pane is typed into only once its turn is idle: the copy waits unread, its wake resumed when it goes idle
554
+ // (carrier.mjs), which hands it to the relay again first
555
+ this.log(`relay could not deliver ${shortId(handed.id)} to busy ${to} (${relayed.detail}); its pane fallback waits for idle`);
556
+ try { this.store.requestWake(to, handed.id, { keep: true }); } catch { /* the wake filed above stands */ }
557
+ const state = this.store.wakeState(to, handed.id);
558
+ this.store.setWakeState(to, handed.id, { ...state, waiting_for_idle: true, next_at_ms: Date.now() + 300_000 });
559
+ delivered = 'native_waits';
560
+ } else if (!relayed.delivered && !received && wake && door.pane && (!relayed.unknown || relayed.landed === false)) {
561
+ this.log(`relay could not deliver ${shortId(handed.id)} to ${to} (${relayed.detail}); typing it into its pane ${door.pane}`);
562
+ this.relaying.add(key);
563
+ try { delivered = await this.#typeIntoPane(handed, to, door.pane, wake, attempt); }
564
+ finally { this.relaying.delete(key); }
565
+ } else if (!relayed.delivered && relayed.unknown && !received) {
547
566
  // kept unread, its wake due now: the carrier hands it over again after its backoff
548
567
  if (filedHere && wake) { try { this.store.requestWake(to, handed.id); } catch { /* the wake filed above stands */ } }
549
568
  throw Object.assign(new Error(relayed.detail), { unanswered: true });
550
569
  }
551
- if (!relayed.delivered && !received) { if (filedHere) { try { this.store.remove(to, filed); } catch { /* gone */ } } throw new Error(relayed.detail); }
570
+ else if (!relayed.delivered && !received) { if (filedHere) { try { this.store.remove(to, filed); } catch { /* gone */ } } throw new Error(relayed.detail); }
552
571
  // relayed: a copy filed unread before this delivery (an earlier unanswered or queued one, held for the receiver's
553
572
  // own read) is moved to cur/, so that read never hands it over a second time
554
- if (relayed.delivered && !received) {
555
- const now = this.store.findIn(to, handed.id);
556
- if (now?.state === 'unread') { try { this.store.markRead(to, now.path); } catch { /* moved meanwhile */ } }
573
+ else {
574
+ if (relayed.delivered && !received) {
575
+ const now = this.store.findIn(to, handed.id);
576
+ if (now?.state === 'unread') { try { this.store.markRead(to, now.path); } catch { /* moved meanwhile */ } }
577
+ }
578
+ delivered = busy ? 'native_busy' : 'native_idle';
557
579
  }
558
- delivered = 'native_idle';
559
580
  } else if (door.kind === 'hook') {
560
581
  attempt?.begin('hook-mailbox');
561
582
  this.store.deliver(to, sent);
562
583
  if (wake) this.store.requestWake(to, sent.id);
563
584
  delivered = 'hooked';
564
585
  } else if (door.kind === 'pane') {
565
- // typed as its next turn only into an empty composer (machine.mjs #paneTurn); otherwise it waits in its mailbox
566
- let typed = null;
567
- attempt?.begin(wake ? 'terminal-input' : 'terminal-mailbox');
568
- if (wake) { try { typed = await this.typeTurn(door.pane, parseAddress(to).harness, render(sent), [sent.id], to); } catch (error) { typed = { reason: error.message }; } }
569
- if (typed?.delivered) { this.store.deliverRead(to, sent); delivered = 'typed'; }
570
- // held by the daemon's queue for the pane, typed once its composer frees: not typed yet, so kept unread
571
- else if (typed?.queued) { this.store.deliver(to, sent); if (wake) this.store.requestWake(to, sent.id, { keep: true }); delivered = 'pane_held'; }
572
- // not typed (its composer held a draft or a turn ran): it waits unread with a wake, so the carrier types it once the
573
- // composer is empty (mail/carrier.mjs), never left for a read a session with no hook never makes
574
- else {
575
- this.store.deliver(to, sent);
576
- if (wake) {
577
- this.store.requestWake(to, sent.id, { keep: true });
578
- const state = this.store.wakeState(to, sent.id);
579
- this.store.setWakeState(to, sent.id, { ...state, next_at_ms: Date.now() + 2_000, last_error: paneWaitReason(typed) });
580
- }
581
- delivered = wake ? 'pane_waits' : 'queued';
582
- }
586
+ delivered = await this.#typeIntoPane(sent, to, door.pane, wake, attempt);
583
587
  } else if (door.kind === 'stored') { attempt?.begin('stored-mailbox'); this.store.deliver(to, sent); delivered = 'stored'; }
584
588
  else { attempt?.begin('operator-mailbox'); this.store.deliver(to, sent); delivered = 'operator'; }
585
589
  // a hand-over to the recipient's own door is its delivery, recorded as such
@@ -592,6 +596,30 @@ export class MailSend {
592
596
  return delivered;
593
597
  }
594
598
 
599
+ /** Type `sent` into `pane` as its next turn, only into an empty composer (machine.mjs #paneTurn); otherwise it waits
600
+ * in its mailbox. Answers 'typed', 'pane_held', 'pane_waits' or 'queued'. */
601
+ async #typeIntoPane(sent, to, pane, wake, attempt) {
602
+ let typed = null;
603
+ attempt?.begin(wake ? 'terminal-input' : 'terminal-mailbox');
604
+ if (wake) { try { typed = await this.typeTurn(pane, parseAddress(to).harness, render(sent), [sent.id], to); } catch (error) { typed = { reason: error.message }; } }
605
+ if (typed?.delivered) {
606
+ this.store.deliverRead(to, sent);
607
+ const now = this.store.findIn(to, sent.id);
608
+ if (now?.state === 'unread') { try { this.store.markRead(to, now.path); } catch { /* moved meanwhile */ } }
609
+ return 'typed';
610
+ }
611
+ // held by the daemon's queue for the pane, typed once its composer frees: not typed yet, so kept unread
612
+ if (typed?.queued) { this.store.deliver(to, sent); if (wake) this.store.requestWake(to, sent.id, { keep: true }); return 'pane_held'; }
613
+ // not typed (its composer held a draft or a turn ran): it waits unread with a wake, so the carrier tries it again once
614
+ // the composer is empty (mail/carrier.mjs), never left for a read a session with no hook never makes
615
+ this.store.deliver(to, sent);
616
+ if (!wake) return 'queued';
617
+ this.store.requestWake(to, sent.id, { keep: true });
618
+ const state = this.store.wakeState(to, sent.id);
619
+ this.store.setWakeState(to, sent.id, { ...state, next_at_ms: Date.now() + 2_000, last_error: paneWaitReason(typed) });
620
+ return 'pane_waits';
621
+ }
622
+
595
623
  /**
596
624
  * Deliver `sent` to every receiver `plan` names: a `to` receiver through its door (a stopped holder the mailbox
597
625
  * launched is resumed first), a `cc` receiver filed unread and not woken (mail_send.rs `deliver_planned`).
@@ -719,7 +747,7 @@ export class MailSend {
719
747
  if (error instanceof Refused) throw new Error(`${error.bytes} bytes; the limit is ${MAX_RELAYED_BYTES}`);
720
748
  throw error;
721
749
  }
722
- return { how, text: { steered: 'steered into its running turn', started: 'started a turn', native_waits: 'waits unread until its native turn is idle', 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] };
750
+ return { how, text: { steered: 'steered into its running turn', started: 'started a turn', native_busy: 'read at its next tool call', native_waits: 'waits unread until its native turn is idle', 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] };
723
751
  }
724
752
 
725
753
  // ---- a filing --------------------------------------------------------------------------------------------------
@@ -822,7 +850,7 @@ export class MailSend {
822
850
  return {
823
851
  delivered_to_bus: door.kind !== 'stored' && !['pane_waits', 'pane_held', 'native_waits'].includes(how), message_id: sent.id, reply_to: sender,
824
852
  target: { harness, session_id: sessionId, name: door.kind === 'native' ? door.session.name.split('@')[0] : null },
825
- delivery: { door: door.kind, how: { steered: 'steered', started: 'started', native_waits: 'waiting_for_idle', native_idle: 'started', hooked: 'hook', queued: 'queued', stored: 'stored', operator: 'filed', already: 'already', typed: 'typed', pane_held: 'held', pane_waits: 'stored' }[how] },
853
+ delivery: { door: door.kind, how: { steered: 'steered', started: 'started', native_busy: 'next_tool_call', native_waits: 'waiting_for_idle', native_idle: 'started', hooked: 'hook', queued: 'queued', stored: 'stored', operator: 'filed', already: 'already', typed: 'typed', pane_held: 'held', pane_waits: 'stored' }[how] },
826
854
  };
827
855
  }
828
856
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.195",
3
+ "version": "0.3.197",
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",