@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.
- package/bin/workspace/agent-session.mjs +38 -0
- package/bin/workspace.mjs +5 -17
- package/fleet.mjs +8 -3
- package/machine.mjs +67 -20
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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",
|