@volter/supercode-teams 0.3.139 → 0.3.141

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/machine.mjs +44 -13
  2. package/package.json +1 -1
package/machine.mjs CHANGED
@@ -194,17 +194,20 @@ async function claudeTranscriptHolds(projectsDirs, sessionId, landed) {
194
194
  return false;
195
195
  }
196
196
  /**
197
- * The doors whose work waits on a session, with the bound each answers within (the node door's own is 30 s): a send or
197
+ * The doors whose work waits on a session or mailbox, with the bound each answers within (the node door's own is 30 s): a send or
198
198
  * a hand-over waits on its receiver's door (a relay's receipt, up to 90 s; another machine's door, MAIL_SEND_MS), a
199
199
  * delegation or a declaration that starts a session waits for it to appear (up to a minute). Past its bound a call is
200
- * answered 504, which its client reads as unanswered: the work may still finish.
200
+ * answered 504, which its client reads as unanswered: the work may still finish. A board's mailbox long poll gets
201
+ * its maximum requested wait plus time to return the answer, rather than the ordinary door's shorter bound.
201
202
  */
203
+ const BOARD_WAIT_MAX_SECONDS = 120;
202
204
  const DOOR_BOUNDS_MS = {
203
205
  'harness.v1.mail.send': 170_000,
204
206
  'harness.v1.mail.message': 170_000,
205
207
  'harness.v1.mail.handover': 140_000,
206
208
  'harness.v1.mail.delegate': 110_000,
207
209
  'harness.v1.agent.declare': 110_000,
210
+ 'harness.v1.mail.board_wait': (BOARD_WAIT_MAX_SECONDS + 10) * 1000,
208
211
  };
209
212
  /** How many typed lines' keys a machine remembers; a server retries a line for minutes, not thousands of lines. */
210
213
  const TURN_KEYS_KEPT = 1000;
@@ -2387,6 +2390,13 @@ export class TeamsMachine extends MachineCore {
2387
2390
  */
2388
2391
  #deliverInitialInput({ key, contextKey, session, kind, by, input, act, nativeInput, executable = {}, resumed = {} }) {
2389
2392
  if (this.pendingInitialInputs.has(key)) return;
2393
+ // Input confirmation can finish after the child. Keep the launch's latest
2394
+ // status and completion; a late input receipt must not make it ready again.
2395
+ const recordInput = facts => {
2396
+ const launch = this.launches.find(key);
2397
+ if (launch?.pane !== contextKey) return;
2398
+ this.launches.record({ ...launch, ...facts, status: launch.status });
2399
+ };
2390
2400
  const abort = new AbortController();
2391
2401
  const task = (async () => {
2392
2402
  let nativeReadyBy = 'native-prompt';
@@ -2396,7 +2406,7 @@ export class TeamsMachine extends MachineCore {
2396
2406
  const result = nativeReadyBy === 'pane-gone' || nativeReadyBy === 'deadline'
2397
2407
  ? { delivered: false, refused: true, reason: nativeReadyBy }
2398
2408
  : await this.#deliverClaudeInitialInput(session, contextKey, nativeInput, abort.signal);
2399
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: result.delivered,
2409
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: result.delivered,
2400
2410
  inputReadyBy: nativeReadyBy, ...(result.delivered ? { inputReceipt: result.receipt } : { inputError: result.reason }),
2401
2411
  inputRefused: result.refused === true, inputPending: result.queued === true,
2402
2412
  ...(result.native_session ? { native_session: result.native_session } : {}) });
@@ -2412,7 +2422,7 @@ export class TeamsMachine extends MachineCore {
2412
2422
  const typed = nativeReadyBy === 'pane-gone' || nativeReadyBy === 'deadline' ? { delivered: false, reason: nativeReadyBy }
2413
2423
  : await this.#typeCodexFirstInput(session, input, abort.signal);
2414
2424
  if (!typed.delivered) {
2415
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: false,
2425
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: false,
2416
2426
  inputReadyBy: nativeReadyBy, inputError: typed.reason });
2417
2427
  act('launch', `pane:${contextKey}`, 'failed', `first input undelivered: ${typed.reason}`);
2418
2428
  return;
@@ -2421,7 +2431,7 @@ export class TeamsMachine extends MachineCore {
2421
2431
  if (nativeInput) {
2422
2432
  const result = await confirmNativeInitialInput(nativeInput, { signal: abort.signal,
2423
2433
  deadlineMs: Date.now() + INITIAL_INPUT_READY_DEADLINE_MS, live: async () => Boolean(await this.#paneByContext(contextKey)) });
2424
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: result.delivered,
2434
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: result.delivered,
2425
2435
  inputReadyBy: nativeReadyBy, ...(result.delivered ? { inputReceipt: result.receipt } : { inputError: result.reason }),
2426
2436
  ...(result.native_session ? { native_session: result.native_session } : {}) });
2427
2437
  act('launch', `pane:${contextKey}`, result.delivered ? 'ok' : result.queued ? 'input-pending' : 'failed',
@@ -2431,19 +2441,19 @@ export class TeamsMachine extends MachineCore {
2431
2441
  const readyBy = await this.#awaitInputReady(session, contextKey, kind, abort.signal);
2432
2442
  if (readyBy === 'aborted') return;
2433
2443
  if (readyBy === 'pane-gone' || readyBy === 'deadline') {
2434
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: false, inputReadyBy: readyBy });
2444
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: false, inputReadyBy: readyBy });
2435
2445
  act('launch', `pane:${contextKey}`, 'failed', `first input undelivered: ${readyBy}`);
2436
2446
  return;
2437
2447
  }
2438
2448
  const result = await this.#deliverPaneText(session, contextKey, input, { signal: abort.signal });
2439
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: result.delivered,
2449
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: result.delivered,
2440
2450
  inputReadyBy: readyBy, ...(!result.delivered ? { inputError: result.reason } : {}) });
2441
2451
  act('launch', `pane:${contextKey}`, result.delivered ? 'ok' : result.queued ? 'input-pending' : 'failed',
2442
2452
  result.delivered ? `first input delivered (${readyBy})` : `first input unconfirmed: ${result.reason}`);
2443
2453
  })().catch((error) => {
2444
2454
  // Nothing above is expected to throw; if something does, the launch record
2445
2455
  // is where it is answerable, not a crashed daemon.
2446
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: false, inputError: error?.message ?? String(error) });
2456
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: false, inputError: error?.message ?? String(error) });
2447
2457
  }).finally(() => {
2448
2458
  if (this.pendingInitialInputs.get(key)?.task === task) this.pendingInitialInputs.delete(key);
2449
2459
  });
@@ -3199,7 +3209,8 @@ export class TeamsMachine extends MachineCore {
3199
3209
  cancelled_at: found.cancelled_at ?? null,
3200
3210
  live: Boolean(session && !session.dead), label: session?.label ?? null, activeCommand: session?.activeCommand ?? null,
3201
3211
  exit_output: found.pane && (!session || session.dead) ? paneExitOutput(found.pane) : null,
3202
- exit: session?.dead ? deadPaneExit(session) : found.pane && !session ? paneExit(found.pane) : null,
3212
+ exit: found.completion ?? (found.status === 'finished' ? { failed: false, how: 'status 0' }
3213
+ : session?.dead ? deadPaneExit(session) : found.pane && !session ? paneExit(found.pane) : null),
3203
3214
  ...initialInputFacts(found),
3204
3215
  };
3205
3216
  }
@@ -3499,6 +3510,11 @@ export class TeamsMachine extends MachineCore {
3499
3510
  let existing = this.launches.find(key);
3500
3511
  while (existing) {
3501
3512
  if (existing.status === 'cancelled') throw Object.assign(new Error(`launch key ${key} was cancelled before issuance; use a new key`), { code: 409, reason: 'launch_key_cancelled' });
3513
+ // Finished itself records an observed clean exit, including older
3514
+ // journals that did not keep a separate completion field.
3515
+ if (existing.status === 'finished') {
3516
+ return { key, pane: existing.pane, reused: true, status: 'finished', exit: existing.completion?.how ?? 'status 0' };
3517
+ }
3502
3518
  if (existing.status === 'issued') return { key, pane: existing.pane, reused: true, status: existing.status };
3503
3519
  if (existing.status === 'ready') {
3504
3520
  const session = await this.#paneByContext(existing.pane);
@@ -3508,6 +3524,14 @@ export class TeamsMachine extends MachineCore {
3508
3524
  this.#recoverClaudeInitialInputs([session], key);
3509
3525
  return { key, pane: existing.pane, reused: true, status: existing.status };
3510
3526
  }
3527
+ // The child can finish after creation but before the original
3528
+ // caller's settlement read. Retain its observed exit before a
3529
+ // concurrent or restarted caller can issue this key again.
3530
+ const exit = session?.dead ? deadPaneExit(session) : paneExit(existing.pane);
3531
+ if (exit?.failed === false) {
3532
+ this.launches.record({ ...existing, status: 'finished', completion: exit });
3533
+ return { key, pane: existing.pane, reused: true, status: 'finished', exit: exit.how };
3534
+ }
3511
3535
  }
3512
3536
  break;
3513
3537
  }
@@ -3558,7 +3582,13 @@ export class TeamsMachine extends MachineCore {
3558
3582
  // The agent the launch's environment names is kept with the launch: the pane's session is bound to it once the
3559
3583
  // session is seen in the pane (sessionBindings).
3560
3584
  const agent = launchAgent(params.env);
3561
- this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...resumed, ...executable, status: 'ready', ...(agent ? { agent } : {}) });
3585
+ // createSession can return a retained exit descriptor with no pane.
3586
+ // Its first journal write must already contain that completion, before
3587
+ // any title/input/settlement await lets another caller see the key.
3588
+ const createdExit = session.dead ? deadPaneExit(session) : null;
3589
+ this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...resumed, ...executable,
3590
+ status: createdExit ? createdExit.failed ? 'exited' : 'finished' : 'ready',
3591
+ ...(createdExit?.failed === false ? { completion: createdExit } : {}), ...(agent ? { agent } : {}) });
3562
3592
  if (params.kind === 'codex' && (params.arguments ?? []).includes('--dangerously-bypass-hook-trust')) this.#settleCodexStartup(key, contextKey, session, act);
3563
3593
  act('launch', `pane:${contextKey}`, 'ok', params.kind ?? params.program);
3564
3594
  if (typeof params.name === 'string' && params.name.trim()) {
@@ -3582,9 +3612,10 @@ export class TeamsMachine extends MachineCore {
3582
3612
  const settled = await this.#paneByContext(contextKey);
3583
3613
  if (!settled || settled.dead) {
3584
3614
  // Finished (it ran to a clean end) is an answer; exited (it failed at once) is the caller's error.
3585
- const exit = settled?.dead ? deadPaneExit(settled) : paneExit(contextKey);
3615
+ const exit = settled?.dead ? deadPaneExit(settled) : session.dead ? deadPaneExit(session) : paneExit(contextKey);
3586
3616
  const status = exit && !exit.failed ? 'finished' : 'exited';
3587
- this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...resumed, ...executable, status, error: exit?.failed ? exit.how : undefined });
3617
+ this.launches.record({ ...this.launches.find(key), key, pane: contextKey, kind: params.kind ?? null, by, ...resumed, ...executable, status,
3618
+ ...(exit && !exit.failed ? { completion: exit } : {}), error: exit?.failed ? exit.how : undefined });
3588
3619
  this.#panesChanged();
3589
3620
  return { key, pane: contextKey, reused: false, status, exit: exit?.how ?? null };
3590
3621
  }
@@ -4133,7 +4164,7 @@ export class TeamsMachine extends MachineCore {
4133
4164
  const board = new RegExp(`^sc:${machineName(this.env).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:board:[^:\\s]+$`);
4134
4165
  if (method === 'harness.v1.mail.board_wait') {
4135
4166
  const boards = (Array.isArray(params.boards) ? params.boards : []).filter((address) => typeof address === 'string' && board.test(address)).slice(0, 256);
4136
- const seconds = Math.min(120, Math.max(0, Number.isFinite(Number(params.timeout)) ? Math.floor(Number(params.timeout)) : 60));
4167
+ const seconds = Math.min(BOARD_WAIT_MAX_SECONDS, Math.max(0, Number.isFinite(Number(params.timeout)) ? Math.floor(Number(params.timeout)) : 60));
4137
4168
  const waiting = () => boards.filter((address) => this.mailStore.hasClaimable(address));
4138
4169
  let woken = waiting();
4139
4170
  if (!woken.length && boards.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.139",
3
+ "version": "0.3.141",
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",