@volter/supercode-teams 0.3.140 → 0.3.142

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.
@@ -1,6 +1,9 @@
1
1
  // Native session hints choose the nearest actual harness when terminal servers inherited both variables.
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { basename } from 'node:path';
4
+ import { openLocalMachine, nodeRpc } from '../../client.mjs';
5
+ import { teamsHome, paths } from '@volter/teams/home';
6
+ import { ownClaim } from '../../live-sessions.mjs';
4
7
  export function agentSessionHint(env=process.env){
5
8
  if(env.SUPERCODE_AGENT_SESSION)return env.SUPERCODE_AGENT_SESSION;
6
9
  const claude=env.CLAUDE_CODE_SESSION_ID,codex=env.CODEX_THREAD_ID;
@@ -18,3 +21,38 @@ export function agentSessionHint(env=process.env){
18
21
  }
19
22
  throw new Error('native session hints are ambiguous; name --session SESSION');
20
23
  }
24
+
25
+ /** The daemon's retained binding is authoritative even when a launch has an old or empty context. */
26
+ export async function agentSessionEnvironment(env = process.env, session = agentSessionHint(env)) {
27
+ if (!session) return env;
28
+ let channel;
29
+ try {
30
+ channel = await openLocalMachine(paths(teamsHome(env)).socket);
31
+ const rpc = await nodeRpc(channel);
32
+ const identity = await rpc.call('harness.v1.agents.session', { session, claim: ownClaim(env) });
33
+ if (!identity) return env;
34
+ if (env.SUPERCODE_AGENT_ID && env.SUPERCODE_AGENT_ID !== identity.agent)
35
+ throw Object.assign(new Error('session ownership does not match its launch identity'), { code: 'forbidden' });
36
+ if (env.SUPERCODE_AGENT_ORG && env.SUPERCODE_AGENT_ORG !== identity.org)
37
+ throw Object.assign(new Error('session Org does not match its launch identity'), { code: 'forbidden' });
38
+ return { ...env, SUPERCODE_AGENT_CONTEXT: identity.context ?? '', SUPERCODE_AGENT_ID: identity.agent,
39
+ SUPERCODE_AGENT_ORG: identity.org, SUPERCODE_AGENT_ASSIGNMENTS: JSON.stringify(identity.assignments) };
40
+ } catch (error) {
41
+ // An offline/older daemon does not authorize a fallback to a person's context.
42
+ // An existing launch reader can still be checked by the server itself.
43
+ if (!['ENOENT', 'ECONNREFUSED', 'machine_offline', -32601].includes(error.code)) throw error;
44
+ return env;
45
+ } finally { channel?.close(); }
46
+ }
47
+
48
+ /** Choosing a context never changes an agent into the connected person. */
49
+ export function agentContextName(contexts, env = process.env, explicit = null) {
50
+ if (!env.SUPERCODE_AGENT_ID) return explicit ?? env.SUPERCODE_CONTEXT ?? contexts.read().current;
51
+ const name = explicit ?? (env.SUPERCODE_AGENT_CONTEXT || null);
52
+ if (!name) throw Object.assign(new Error('agent delegation is unavailable; no custodian context fallback is permitted'), { code: 'authentication_required' });
53
+ const selected = contexts.read().contexts[name];
54
+ if (!selected || selected.principal_id !== env.SUPERCODE_AGENT_ID
55
+ || (env.SUPERCODE_AGENT_ORG && selected.team_id !== env.SUPERCODE_AGENT_ORG))
56
+ throw Object.assign(new Error('selected context does not belong to this agent and Org'), { code: 'forbidden' });
57
+ return name;
58
+ }
package/bin/workspace.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { StringDecoder } from 'node:string_decoder';
3
3
  import { hostname } from 'node:os';
4
- import { agentSessionHint } from './workspace/agent-session.mjs';
4
+ import { agentSessionHint, agentSessionEnvironment, agentContextName } from './workspace/agent-session.mjs';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import { existsSync, readFileSync, realpathSync, rmSync } from 'node:fs';
7
7
  import { basename, dirname, join, resolve } from 'node:path';
@@ -60,20 +60,8 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
60
60
  if (verb === 'login') return loginCommand({ flags, origin: sub, rest, home, output });
61
61
  if (verb === 'auth') return authCommand({ flags, sub, rest, home, contexts, output, env, dependencies });
62
62
  if (verb === 'connect' && flags.enrollment_stdin) adoptEnrollment(contexts, home, flags);
63
- if(['agent','sessions'].includes(verb)){
64
- const session=flags.session??agentSessionHint(env);
65
- if(session){
66
- let channel;
67
- try{
68
- channel=await openLocalMachine(paths(teamsHome()).socket);const rpc=await nodeRpc(channel);
69
- const identity=await rpc.call('harness.v1.agents.session',{session});
70
- if(identity&&!flags.context&&!env.SUPERCODE_AGENT_CONTEXT)env={...env,SUPERCODE_AGENT_CONTEXT:identity.context,SUPERCODE_AGENT_ID:identity.agent,SUPERCODE_AGENT_ASSIGNMENTS:JSON.stringify(identity.assignments)};
71
- }catch(error){
72
- // Only absence of an older/offline daemon permits the ordinary plain-session context.
73
- if(!['ENOENT','ECONNREFUSED','machine_offline',-32601].includes(error.code))throw error;
74
- }finally{channel?.close();}
75
- }
76
- }
63
+ if(env.SUPERCODE_AGENT_ID||['agent','sessions'].includes(verb))
64
+ env=await agentSessionEnvironment(env,flags.session??agentSessionHint(env));
77
65
  // Every door called from an agent starts under its delegated context. A sibling product verb
78
66
  // must not silently recover the custodian's ordinary login when an agent's reader is bounded.
79
67
  const agentContext = env.SUPERCODE_AGENT_CONTEXT || null;
@@ -87,7 +75,7 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
87
75
  throw new Error('agent delegation is unavailable; no custodian context fallback is permitted');
88
76
  }
89
77
  const explicitContext = flags.context ?? agentContext ?? env.SUPERCODE_CONTEXT;
90
- let selected = explicitContext ?? contexts.read().current;
78
+ let selected = agentContextName(contexts,env,flags.context??null);
91
79
  const urlReference = verb === 'sessions' ? rest.find(value => /^https?:\/\//.test(value)) : null;
92
80
  if (!explicitContext && urlReference) selected = contextForUrl(contexts.read(), urlReference) ?? selected;
93
81
  if (!selected) usage('no workspace context selected');
@@ -871,7 +859,7 @@ async function machinesCommand(sub, { flags, rest, output, client, context, team
871
859
  }
872
860
  if (action === 'grant') {
873
861
  for (const key of ['to', 'caps', 'ttl']) if (!flags[key]) usage(`--${key} is required`);
874
- const subject = /^(user|service|group):(.+)$/.exec(flags.to)?.[2]; if (!subject) usage('--to must be user:ID, service:ID or group:ID');
862
+ const subject = /^(user|service|agent|group):(.+)$/.exec(flags.to)?.[2]; if (!subject) usage('--to must be user:ID, service:ID, agent:ID or group:ID');
875
863
  const ttl = duration(flags.ttl);
876
864
  if (flags.session || flags.binding) {
877
865
  // One session binding's native live actions (§5).
package/fleet.mjs CHANGED
@@ -8,9 +8,14 @@ import { harnessTransport, nodeRpc, openRemoteMachine } from './client.mjs';
8
8
  import { openMachine } from './machine-door.mjs';
9
9
  import { WorkspaceContexts, fromContext } from './context.mjs';
10
10
  import { teamsHome } from '@volter/teams/home';
11
+ import { agentSessionEnvironment, agentContextName } from './bin/workspace/agent-session.mjs';
11
12
 
12
13
  /** Open the channel to the machine `name` names in `context` (or this box's daemon when name is null). */
13
- export function openOn(name = null, { context = null, env = process.env } = {}) {
14
+ export async function openOn(name = null, { context = null, env = process.env } = {}) {
15
+ if (name && env.SUPERCODE_AGENT_ID) {
16
+ env = await agentSessionEnvironment(env);
17
+ context = agentContextName(new WorkspaceContexts(env.SUPERCODE_HOME ?? resolve(teamsHome(env), '..')), env, context);
18
+ }
14
19
  return openMachine({ machine: name, context, env });
15
20
  }
16
21
 
@@ -44,6 +49,7 @@ export async function nodeRequest(channel, method, params = {}) {
44
49
  * that refused or is offline carries its reason. Nothing is stored.
45
50
  */
46
51
  export async function fleetDiscover({ context = null, machines = null, params = {}, env = process.env } = {}) {
52
+ if (env.SUPERCODE_AGENT_ID) env = await agentSessionEnvironment(env);
47
53
  // One page from each machine, never its whole catalog (yuerans' was 5,620 sessions, 20 MB, 13.5 s): the caller's
48
54
  // limit, or FLEET_PAGE; each machine answers it from its daemon's held index.
49
55
  // `--limit 0` is every session, as it always was
@@ -51,7 +57,7 @@ export async function fleetDiscover({ context = null, machines = null, params =
51
57
  const home = teamsHome(env);
52
58
  const supercodeHome = env.SUPERCODE_HOME ?? resolve(home, '..');
53
59
  const contexts = new WorkspaceContexts(supercodeHome);
54
- const name = context ?? env.SUPERCODE_CONTEXT ?? contexts.read().current;
60
+ const name = agentContextName(contexts, env, context);
55
61
  if (!name || name === 'local') throw new Error('--fleet discovers the machines of a Teams context: `supercode teams login …`, then `supercode context use NAME` or --context NAME');
56
62
  const { client, context: selected } = fromContext({ supercodeHome, name, fetch: globalThis.fetch });
57
63
  const token = contexts.credential(selected.credential_ref).token;
@@ -105,4 +111,3 @@ export async function fleetDiscover({ context = null, machines = null, params =
105
111
 
106
112
  /** Sessions `discover --fleet` asks each machine for when the caller names no limit. */
107
113
  const FLEET_PAGE = 50;
108
-
package/machine.mjs CHANGED
@@ -682,13 +682,15 @@ export class TeamsMachine extends MachineCore {
682
682
  // one follow at a time: a change while one is in flight is followed again when it ends, never beside it (two in flight
683
683
  // would leave the earlier subscription unsubscribed)
684
684
  let pending = null, followed = '', backoffMs = 0, running = false, again = false;
685
+ this.stopLiveFollow = () => { clearTimeout(pending); pending = null; again = false; this.live.stop(); };
685
686
  const follow = async () => {
686
687
  pending = null;
688
+ if (this.stopping) return;
687
689
  if (running) { again = true; return; }
688
690
  running = true;
689
691
  try { await followNow(); } finally {
690
692
  running = false;
691
- if (again) { again = false; if (!pending) { pending = setTimeout(follow, 0); pending.unref?.(); } }
693
+ if (again && !this.stopping) { again = false; if (!pending) { pending = setTimeout(follow, 0); pending.unref?.(); } }
692
694
  }
693
695
  };
694
696
  const followNow = async () => {
@@ -699,6 +701,7 @@ export class TeamsMachine extends MachineCore {
699
701
  try {
700
702
  if (locators.length) {
701
703
  const opened = await this.#serveRequest('harness.v1.sessions.activity.subscribe', { locators }, 30_000);
704
+ if (this.stopping) return;
702
705
  this.liveActivity = opened?.subscription ?? null;
703
706
  this.live.codexActivity(opened?.initial ?? [], { replace: true });
704
707
  } else { this.liveActivity = null; this.live.codexActivity([], { replace: true }); }
@@ -706,6 +709,7 @@ export class TeamsMachine extends MachineCore {
706
709
  this.live.codexFollowedNow();
707
710
  if (previous) void this.#serveRequest('harness.v1.sessions.activity.unsubscribe', { subscription: previous }, 30_000).catch(() => {});
708
711
  } catch (error) {
712
+ if (this.stopping) return;
709
713
  backoffMs = Math.min(300_000, Math.max(2000, (backoffMs || 1000) * 2));
710
714
  this.emit('records-log', `live sessions: the Codex sessions' activity could not be followed (${error.message}); tried again in ${Math.round(backoffMs / 1000)} s`);
711
715
  pending = setTimeout(follow, backoffMs); pending.unref?.();
@@ -713,7 +717,7 @@ export class TeamsMachine extends MachineCore {
713
717
  };
714
718
  // a change is followed after 2 s (changes come in bursts); the first follow after the index's first answer at once,
715
719
  // as sends wait on it (the Codex sessions are not known until it is done)
716
- this.live.on('codex-followed', () => { if (!pending) { pending = setTimeout(follow, this.live.codexKnown ? 2000 : 0); pending.unref?.(); } });
720
+ this.live.on('codex-followed', () => { if (!this.stopping && !pending) { pending = setTimeout(follow, this.live.codexKnown ? 2000 : 0); pending.unref?.(); } });
717
721
  this.on('internal-notification', (message) => {
718
722
  const params = message.params ?? {};
719
723
  if (params.subscription && params.subscription === this.liveActivity) this.live.codexActivity(params.activities);
@@ -1158,6 +1162,8 @@ export class TeamsMachine extends MachineCore {
1158
1162
  this.jobs?.stopAll();
1159
1163
  void this.relayEndpoint?.then((endpoint) => endpoint?.close()).catch(() => {});
1160
1164
  this.carrier?.stop();
1165
+ this.stopLiveFollow?.();
1166
+ this.mailStore?.stop();
1161
1167
  clearInterval(this.hookSpoolTimer);
1162
1168
  this.idleNotices?.stop();
1163
1169
  this.agentUpkeep?.stop();
@@ -1353,17 +1359,30 @@ export class TeamsMachine extends MachineCore {
1353
1359
  accept();
1354
1360
  this.log.append({ principal, cap: 'harness', target: '*', outcome: 'opened' });
1355
1361
  const lines = lineSplitter();
1362
+ let forwarding = Promise.resolve();
1356
1363
  stream.on('data', (chunk) => {
1357
1364
  for (const line of lines(chunk)) {
1358
1365
  if (!line) continue;
1359
- let message;
1360
- try { message = JSON.parse(line); } catch { continue; }
1361
- const asked = message.id !== undefined && message.id !== null ? message.id : null;
1362
- if (asked !== null) message.id = joinId(index, message.id);
1363
- // a serve that is not there to take it answers the request at once, on this stream only
1364
- if (!this.#toServe(JSON.stringify(message) + '\n') && asked !== null && !stream.closed) {
1365
- try { stream.write(`${JSON.stringify({ jsonrpc: '2.0', id: asked, error: { code: -32000, message: 'harness serve is not running; it is being started again' } })}\n`); } catch { /* the stream went */ }
1366
- }
1366
+ forwarding = forwarding.then(async () => {
1367
+ if (stream.closed) return;
1368
+ if (entry.verify) {
1369
+ const fresh = await entry.verify();
1370
+ if (fresh?.class !== 'operator') {
1371
+ this.log.append({ principal, cap: 'harness', target: '*', outcome: 'refused', detail: 'no live operator grant' });
1372
+ return stream.close('no live operator grant');
1373
+ }
1374
+ entry.admitted = fresh;
1375
+ }
1376
+ if (stream.closed) return;
1377
+ let message;
1378
+ try { message = JSON.parse(line); } catch { return; }
1379
+ const asked = message.id !== undefined && message.id !== null ? message.id : null;
1380
+ if (asked !== null) message.id = joinId(index, message.id);
1381
+ // a serve that is not there to take it answers the request at once, on this stream only
1382
+ if (!this.#toServe(JSON.stringify(message) + '\n') && asked !== null && !stream.closed) {
1383
+ try { stream.write(`${JSON.stringify({ jsonrpc: '2.0', id: asked, error: { code: -32000, message: 'harness serve is not running; it is being started again' } })}\n`); } catch { /* the stream went */ }
1384
+ }
1385
+ }).catch(() => { if (!stream.closed) stream.close('machine authority could not be verified'); });
1367
1386
  }
1368
1387
  });
1369
1388
  stream.on('close', () => { entry.harness = null; });
@@ -2390,6 +2409,13 @@ export class TeamsMachine extends MachineCore {
2390
2409
  */
2391
2410
  #deliverInitialInput({ key, contextKey, session, kind, by, input, act, nativeInput, executable = {}, resumed = {} }) {
2392
2411
  if (this.pendingInitialInputs.has(key)) return;
2412
+ // Input confirmation can finish after the child. Keep the launch's latest
2413
+ // status and completion; a late input receipt must not make it ready again.
2414
+ const recordInput = facts => {
2415
+ const launch = this.launches.find(key);
2416
+ if (launch?.pane !== contextKey) return;
2417
+ this.launches.record({ ...launch, ...facts, status: launch.status });
2418
+ };
2393
2419
  const abort = new AbortController();
2394
2420
  const task = (async () => {
2395
2421
  let nativeReadyBy = 'native-prompt';
@@ -2399,7 +2425,7 @@ export class TeamsMachine extends MachineCore {
2399
2425
  const result = nativeReadyBy === 'pane-gone' || nativeReadyBy === 'deadline'
2400
2426
  ? { delivered: false, refused: true, reason: nativeReadyBy }
2401
2427
  : await this.#deliverClaudeInitialInput(session, contextKey, nativeInput, abort.signal);
2402
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: result.delivered,
2428
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: result.delivered,
2403
2429
  inputReadyBy: nativeReadyBy, ...(result.delivered ? { inputReceipt: result.receipt } : { inputError: result.reason }),
2404
2430
  inputRefused: result.refused === true, inputPending: result.queued === true,
2405
2431
  ...(result.native_session ? { native_session: result.native_session } : {}) });
@@ -2415,7 +2441,7 @@ export class TeamsMachine extends MachineCore {
2415
2441
  const typed = nativeReadyBy === 'pane-gone' || nativeReadyBy === 'deadline' ? { delivered: false, reason: nativeReadyBy }
2416
2442
  : await this.#typeCodexFirstInput(session, input, abort.signal);
2417
2443
  if (!typed.delivered) {
2418
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: false,
2444
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: false,
2419
2445
  inputReadyBy: nativeReadyBy, inputError: typed.reason });
2420
2446
  act('launch', `pane:${contextKey}`, 'failed', `first input undelivered: ${typed.reason}`);
2421
2447
  return;
@@ -2424,7 +2450,7 @@ export class TeamsMachine extends MachineCore {
2424
2450
  if (nativeInput) {
2425
2451
  const result = await confirmNativeInitialInput(nativeInput, { signal: abort.signal,
2426
2452
  deadlineMs: Date.now() + INITIAL_INPUT_READY_DEADLINE_MS, live: async () => Boolean(await this.#paneByContext(contextKey)) });
2427
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: result.delivered,
2453
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: result.delivered,
2428
2454
  inputReadyBy: nativeReadyBy, ...(result.delivered ? { inputReceipt: result.receipt } : { inputError: result.reason }),
2429
2455
  ...(result.native_session ? { native_session: result.native_session } : {}) });
2430
2456
  act('launch', `pane:${contextKey}`, result.delivered ? 'ok' : result.queued ? 'input-pending' : 'failed',
@@ -2434,19 +2460,19 @@ export class TeamsMachine extends MachineCore {
2434
2460
  const readyBy = await this.#awaitInputReady(session, contextKey, kind, abort.signal);
2435
2461
  if (readyBy === 'aborted') return;
2436
2462
  if (readyBy === 'pane-gone' || readyBy === 'deadline') {
2437
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: false, inputReadyBy: readyBy });
2463
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: false, inputReadyBy: readyBy });
2438
2464
  act('launch', `pane:${contextKey}`, 'failed', `first input undelivered: ${readyBy}`);
2439
2465
  return;
2440
2466
  }
2441
2467
  const result = await this.#deliverPaneText(session, contextKey, input, { signal: abort.signal });
2442
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: result.delivered,
2468
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: result.delivered,
2443
2469
  inputReadyBy: readyBy, ...(!result.delivered ? { inputError: result.reason } : {}) });
2444
2470
  act('launch', `pane:${contextKey}`, result.delivered ? 'ok' : result.queued ? 'input-pending' : 'failed',
2445
2471
  result.delivered ? `first input delivered (${readyBy})` : `first input unconfirmed: ${result.reason}`);
2446
2472
  })().catch((error) => {
2447
2473
  // Nothing above is expected to throw; if something does, the launch record
2448
2474
  // is where it is answerable, not a crashed daemon.
2449
- this.launches.record({ key, pane: contextKey, kind, by, ...resumed, ...executable, status: 'ready', inputDelivered: false, inputError: error?.message ?? String(error) });
2475
+ recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: false, inputError: error?.message ?? String(error) });
2450
2476
  }).finally(() => {
2451
2477
  if (this.pendingInitialInputs.get(key)?.task === task) this.pendingInitialInputs.delete(key);
2452
2478
  });
@@ -3202,7 +3228,8 @@ export class TeamsMachine extends MachineCore {
3202
3228
  cancelled_at: found.cancelled_at ?? null,
3203
3229
  live: Boolean(session && !session.dead), label: session?.label ?? null, activeCommand: session?.activeCommand ?? null,
3204
3230
  exit_output: found.pane && (!session || session.dead) ? paneExitOutput(found.pane) : null,
3205
- exit: session?.dead ? deadPaneExit(session) : found.pane && !session ? paneExit(found.pane) : null,
3231
+ exit: found.completion ?? (found.status === 'finished' ? { failed: false, how: 'status 0' }
3232
+ : session?.dead ? deadPaneExit(session) : found.pane && !session ? paneExit(found.pane) : null),
3206
3233
  ...initialInputFacts(found),
3207
3234
  };
3208
3235
  }
@@ -3502,6 +3529,11 @@ export class TeamsMachine extends MachineCore {
3502
3529
  let existing = this.launches.find(key);
3503
3530
  while (existing) {
3504
3531
  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' });
3532
+ // Finished itself records an observed clean exit, including older
3533
+ // journals that did not keep a separate completion field.
3534
+ if (existing.status === 'finished') {
3535
+ return { key, pane: existing.pane, reused: true, status: 'finished', exit: existing.completion?.how ?? 'status 0' };
3536
+ }
3505
3537
  if (existing.status === 'issued') return { key, pane: existing.pane, reused: true, status: existing.status };
3506
3538
  if (existing.status === 'ready') {
3507
3539
  const session = await this.#paneByContext(existing.pane);
@@ -3511,6 +3543,14 @@ export class TeamsMachine extends MachineCore {
3511
3543
  this.#recoverClaudeInitialInputs([session], key);
3512
3544
  return { key, pane: existing.pane, reused: true, status: existing.status };
3513
3545
  }
3546
+ // The child can finish after creation but before the original
3547
+ // caller's settlement read. Retain its observed exit before a
3548
+ // concurrent or restarted caller can issue this key again.
3549
+ const exit = session?.dead ? deadPaneExit(session) : paneExit(existing.pane);
3550
+ if (exit?.failed === false) {
3551
+ this.launches.record({ ...existing, status: 'finished', completion: exit });
3552
+ return { key, pane: existing.pane, reused: true, status: 'finished', exit: exit.how };
3553
+ }
3514
3554
  }
3515
3555
  break;
3516
3556
  }
@@ -3561,7 +3601,13 @@ export class TeamsMachine extends MachineCore {
3561
3601
  // The agent the launch's environment names is kept with the launch: the pane's session is bound to it once the
3562
3602
  // session is seen in the pane (sessionBindings).
3563
3603
  const agent = launchAgent(params.env);
3564
- this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...resumed, ...executable, status: 'ready', ...(agent ? { agent } : {}) });
3604
+ // createSession can return a retained exit descriptor with no pane.
3605
+ // Its first journal write must already contain that completion, before
3606
+ // any title/input/settlement await lets another caller see the key.
3607
+ const createdExit = session.dead ? deadPaneExit(session) : null;
3608
+ this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...resumed, ...executable,
3609
+ status: createdExit ? createdExit.failed ? 'exited' : 'finished' : 'ready',
3610
+ ...(createdExit?.failed === false ? { completion: createdExit } : {}), ...(agent ? { agent } : {}) });
3565
3611
  if (params.kind === 'codex' && (params.arguments ?? []).includes('--dangerously-bypass-hook-trust')) this.#settleCodexStartup(key, contextKey, session, act);
3566
3612
  act('launch', `pane:${contextKey}`, 'ok', params.kind ?? params.program);
3567
3613
  if (typeof params.name === 'string' && params.name.trim()) {
@@ -3585,9 +3631,10 @@ export class TeamsMachine extends MachineCore {
3585
3631
  const settled = await this.#paneByContext(contextKey);
3586
3632
  if (!settled || settled.dead) {
3587
3633
  // Finished (it ran to a clean end) is an answer; exited (it failed at once) is the caller's error.
3588
- const exit = settled?.dead ? deadPaneExit(settled) : paneExit(contextKey);
3634
+ const exit = settled?.dead ? deadPaneExit(settled) : session.dead ? deadPaneExit(session) : paneExit(contextKey);
3589
3635
  const status = exit && !exit.failed ? 'finished' : 'exited';
3590
- this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...resumed, ...executable, status, error: exit?.failed ? exit.how : undefined });
3636
+ this.launches.record({ ...this.launches.find(key), key, pane: contextKey, kind: params.kind ?? null, by, ...resumed, ...executable, status,
3637
+ ...(exit && !exit.failed ? { completion: exit } : {}), error: exit?.failed ? exit.how : undefined });
3591
3638
  this.#panesChanged();
3592
3639
  return { key, pane: contextKey, reused: false, status, exit: exit?.how ?? null };
3593
3640
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.140",
3
+ "version": "0.3.142",
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",
@@ -70,7 +70,7 @@
70
70
  "@volter/supercode-health": "^0.1.0",
71
71
  "@volter/supercode-terminal": "^0.2.54",
72
72
  "@volter/supercode-trace": "^0.1.0",
73
- "@volter/teams": "0.2.36",
73
+ "@volter/teams": "0.2.37",
74
74
  "@xterm/addon-serialize": "^0.14.0",
75
75
  "@xterm/headless": "^6.0.0",
76
76
  "protobufjs": "8.8.0",