@volter/supercode-teams 0.3.125 → 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/bin/teams.mjs CHANGED
@@ -55,7 +55,7 @@ import { APP_DOOR_PATH, appDoorFile, appDoorLink, normalOrigin, readAppDoor, rea
55
55
 
56
56
  const MACHINE_VERBS = new Set(['machine', 'slow', 'describe', 'records', 'panes', 'input', 'suspend', 'resume', 'files', 'ports', 'port', 'browser', 'stream', 'jobs', 'workspace', 'title', 'attention', 'watch', 'rpc', 'launches', 'log', 'mail', 'board', 'health', 'app-door']);
57
57
  // Every flag a machine verb reads; any other is refused rather than silently ignored.
58
- const KNOWN_FLAGS = new Set(['home', 'since', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'processes', 'panes', 'clear-draft', 'message-ids', 'harness', 'observe', 'attach-context', 'json', 'all', 'origin', 'port', 'remove-origin', 'open']);
58
+ const KNOWN_FLAGS = new Set(['home', 'since', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'processes', 'panes', 'clear-draft', 'message-ids', 'harness', 'observe', 'attach-context', 'json', 'all', 'origin', 'port', 'remove-origin', 'open', 'foreign']);
59
59
 
60
60
  const argv = process.argv.slice(2);
61
61
  const flags = {};
@@ -247,7 +247,13 @@ async function main() {
247
247
  flags.processes ? 'panes.processes' : flags['presence-only'] ? 'panes.present' : 'panes.list',
248
248
  flags['presence-only'] && typeof flags.panes === 'string' ? { panes: flags.panes.split(',').filter(Boolean) } : {})));
249
249
  if (sub === 'capture') return withNode(async (rpc) => out((await rpc.call('panes.capture', { pane: rest[0], lines: Number(flags.lines ?? 200) })).text));
250
- if (sub === 'close') return withNode(async (rpc) => out(await rpc.call('panes.close', { pane: rest[0] })));
250
+ // One pane, named: `--foreign` closes a pane another session started, and is said for that pane alone.
251
+ if (sub === 'close') {
252
+ // the flag parser takes `--foreign p_…` as the flag's value: that pane is the one named
253
+ const named = typeof flags.foreign === 'string' ? [flags.foreign, ...rest] : rest;
254
+ if (named.length !== 1) die('panes close <pane> [--foreign]: one pane per close');
255
+ return withNode(async (rpc) => out(await rpc.call('panes.close', { pane: named[0], foreign: flags.foreign !== undefined, ...(await acting()) })));
256
+ }
251
257
  // `window <pane> [machine]`: a terminal window on this machine's screen showing the pane, this machine's or, named,
252
258
  // another machine's (attached through Teams, in that machine's Teams context `--attach-context`); `--observe` shows it
253
259
  // read-only.
package/codex-startup.mjs CHANGED
@@ -42,7 +42,16 @@ export function codexStartupArguments(args = []) {
42
42
  }
43
43
  normalized.push(argument);
44
44
  }
45
- return ['-c', 'check_for_update_on_startup=false', ...normalized];
45
+ // Codex 0.160's `resume` does not take this override from before the subcommand: a resumed TUI with it there still
46
+ // stopped on "Update available" once another Codex had cached a newer version, and `codex resume <id> -c …` did not
47
+ // (yuerans, 2026-10-06). It goes after the subcommand and its session id (readers of the process take the word after
48
+ // `resume` as the id: codex_peer.rs `resumed_session_in`), in front when there is no subcommand. `fork` is placed the
49
+ // same way by extension; only `resume` was measured.
50
+ const subcommand = normalized.findIndex((argument, index) => ['resume', 'fork'].includes(argument)
51
+ && !['-c', '--config'].includes(normalized[index - 1]) && !normalized.slice(0, index).includes('--'));
52
+ const id = subcommand >= 0 && normalized[subcommand + 1] !== undefined && normalized[subcommand + 1] !== '--' && !normalized[subcommand + 1].startsWith('-');
53
+ const at = subcommand < 0 ? 0 : subcommand + (id ? 2 : 1);
54
+ return [...normalized.slice(0, at), '-c', 'check_for_update_on_startup=false', ...normalized.slice(at)];
46
55
  }
47
56
 
48
57
  export function codexUpdatePrompt(screen) {
package/machine.mjs CHANGED
@@ -2079,16 +2079,22 @@ export class TeamsMachine extends MachineCore {
2079
2079
  }
2080
2080
 
2081
2081
  #refuseForeign(pane, caller) {
2082
- if (typeof caller !== 'string' || !caller || /^sc:[^:]+:board:/.test(caller)) return;
2082
+ const starter = this.#foreignStarter(pane, caller);
2083
+ if (starter) throw Object.assign(new Error(`pane ${pane} ${starter}, not by ${caller}: a session acts only on the panes it started. Nothing was done; reach that session through its own doors (supercode message send)`), { code: 403 });
2084
+ }
2085
+
2086
+ /** Who started `pane`, said, when `caller` is a session that did not start it; null when the caller may act on it (a
2087
+ * person, a board, its starter). */
2088
+ #foreignStarter(pane, caller) {
2089
+ if (typeof caller !== 'string' || !caller || /^sc:[^:]+:board:/.test(caller)) return null;
2083
2090
  const launch = this.launches.all().find((entry) => entry.pane === pane);
2084
2091
  const by = launch?.by ?? null;
2085
- if (by === caller) return;
2092
+ if (by === caller) return null;
2086
2093
  // A pane that resumes a session is that session's as well: its first starter acts on it as before the resume
2087
2094
  // (t_09a795fd item 18). The starter is the oldest harness launch whose own transcript confirmed it started that
2088
2095
  // session, never a launch row's claim (any caller can launch a program with --session-id S).
2089
- if (launch?.resumes && this.#sessionOrigin(launch.resumes)?.by === caller) return;
2090
- const starter = by ? `was started by ${by}` : 'was not started by a session (a person or an earlier daemon started it)';
2091
- throw Object.assign(new Error(`pane ${pane} ${starter}, not by ${caller}: a session acts only on the panes it started. Nothing was done; reach that session through its own doors (supercode message send)`), { code: 403 });
2096
+ if (launch?.resumes && this.#sessionOrigin(launch.resumes)?.by === caller) return null;
2097
+ return by ? `was started by ${by}` : 'was not started by a session (a person or an earlier daemon started it)';
2092
2098
  }
2093
2099
 
2094
2100
  /** The launch that started native Claude session `session`: the oldest journal row of a Claude launch whose own
@@ -3307,7 +3313,7 @@ export class TeamsMachine extends MachineCore {
3307
3313
  }
3308
3314
  const contextKey = params.pane ?? `p_${randomBytes(9).toString('base64url')}`;
3309
3315
  // Who started the pane: the launching session's (or board's) address, null for a person. A session's later
3310
- // input on the pane is refused unless it is this one (#refuseForeign); close is not creator-restricted.
3316
+ // input on the pane is refused unless it is this one (#refuseForeign); its close needs `foreign` (panes.close).
3311
3317
  // Who to tell when the pane ends (`open --new … --notify-when-ended`): a session's mail address, and this
3312
3318
  // machine's name as the address a notice from its pane comes from.
3313
3319
  if (params.notify !== undefined && !(params.notify && typeof params.notify.to === 'string' && /^sc:[^:\s]+:[^:\s]+:\S+$/.test(params.notify.to) && typeof params.notify.machine === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(params.notify.machine))) throw Object.assign(new Error('launch notify is { to: <sc: address>, machine: <this machine\'s mail name> }'), { code: 400 });
@@ -3459,14 +3465,28 @@ export class TeamsMachine extends MachineCore {
3459
3465
  case 'harness.v1.panes.close':
3460
3466
  case 'panes.close': {
3461
3467
  need('control', `pane:${params.pane}`);
3462
- for (const pending of this.pendingStartups.values()) if (pending.pane === params.pane) pending.abort.abort();
3468
+ // A session closes the panes it started and the one it runs in. Another's pane is closed only when the caller
3469
+ // says so (`foreign`), one pane at a time, by name: a listing piped into close is refused pane by pane rather
3470
+ // than ending every session on the machine (docs/adr/0004-pane-close-authority.md).
3471
+ const starting = [...this.pendingStartups.values()].filter((pending) => pending.pane === params.pane);
3472
+ // only a pane here (open, or still starting) is anyone's: one this machine lacks is answered as absent, before
3473
+ // the caller is asked for
3474
+ const here = starting.length > 0 || Boolean(await this.#paneByContext(params.pane));
3475
+ const closeBy = here ? await this.#callerOf(entry, params) : null;
3476
+ const starter = params.foreign === true || !here ? null : this.#foreignStarter(params.pane, closeBy);
3477
+ // the pane the caller runs in: a Claude row names it as its tmux locator (`p_…:@window.%pane`), the others as the pane
3478
+ if (starter && !this.live.sessions().some((live) => live.address === closeBy && String(live.pane ?? '').split(':')[0] === params.pane)) {
3479
+ act('close', `pane:${params.pane}`, 'refused', `foreign: ${starter}`);
3480
+ throw Object.assign(new Error(`pane ${params.pane} ${starter}, not by ${closeBy}. Nothing was closed. A session closes the panes it started; closing another's is deliberate: read what the pane holds, settle it with whoever started it, then close that one pane with --foreign`), { code: 403 });
3481
+ }
3482
+ for (const pending of starting) pending.abort.abort();
3463
3483
  const session = await this.#paneByContext(params.pane);
3464
3484
  if (!session) {
3465
3485
  act('close', `pane:${params.pane}`, 'ok', 'already absent');
3466
3486
  return { closed: params.pane, alreadyAbsent: true };
3467
3487
  }
3468
3488
  await this.host.closeSession(session.id);
3469
- act('close', `pane:${params.pane}`);
3489
+ act('close', `pane:${params.pane}`, 'ok', params.foreign === true ? `foreign, by ${closeBy ?? 'a person'}` : null);
3470
3490
  this.#panesChanged();
3471
3491
  return { closed: params.pane };
3472
3492
  }
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.125",
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",