@dougbots/avenor-core 0.8.3 → 0.10.0

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/README.md CHANGED
@@ -50,7 +50,33 @@ const result = await spawnTool({
50
50
  // { run_id: '...', label: 'reverse-string', supervisor_id: '/path/to/socket' }
51
51
  ```
52
52
 
53
- Accepts `agent`, `prompt`, `promptFile`, `label`, `dir`, `timeout`, `model`, `backend`, `serverUrl`, `sessionId`, and `supervisorId` (to target a specific socket).
53
+ Accepts `agent`, `prompt`, `promptFile`, `label`, `dir`, `timeout`, `model`, `thinking`, `backend`, `rosterFile`, `rosterEntry`, `serverUrl`, `sessionId`, and `supervisorId` (to target a specific socket).
54
+
55
+ ### Roster selection
56
+
57
+ `rosterFile` is the path to a top-level JSON roster map and `rosterEntry` is the key selected from it. Each entry must contain `backend` and at least one of `agent` or `model`:
58
+
59
+ ```json
60
+ {
61
+ "planner": {
62
+ "backend": "opencode-acp",
63
+ "model": "provider/planner"
64
+ }
65
+ }
66
+ ```
67
+
68
+ A direct roster spawn forwards both selectors and must not also provide `agent`, `model`, or `backend`. Direct mode without a roster allows each of those identity fields to be omitted independently, including a backend-only request; the selected runtime supplies defaults for omitted values. `thinking` is run-level, not a roster field, and is checked against the effective backend. The shared selector validation rejects missing selector pairs, mixed direct/roster fields, and unknown selector keys. Roster-file loading rejects deferred entry fields such as `system`; `thinking` remains a valid run-level option but is never read from a roster entry.
69
+
70
+ ```ts
71
+ await spawnTool({
72
+ prompt: 'Analyze the repository',
73
+ dir: '/repo',
74
+ rosterFile: '/repo/roster.json',
75
+ rosterEntry: 'planner',
76
+ })
77
+ ```
78
+
79
+ Roster phase selection is resolved by Avenor's Go workflow runners. A roster entry may override backend/agent/model per phase. CLI workflow fallback and nested roster inheritance are CLI-only; stable nesting keeps its existing behavior. A resumed session must use the same effective backend as the original session.
54
80
 
55
81
  ### `statusTool`
56
82
 
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ interface HistoryResult<TEvent = Event> {
24
24
  events: TEvent[];
25
25
  latest_seq?: number;
26
26
  }
27
+ type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
27
28
  interface SpawnParams {
28
29
  agent?: string;
29
30
  prompt_file?: string;
@@ -32,8 +33,11 @@ interface SpawnParams {
32
33
  label?: string;
33
34
  timeout?: number;
34
35
  model?: string;
36
+ thinking?: ThinkingLevel;
35
37
  agent_profile?: string;
36
38
  backend?: string;
39
+ roster_file?: string;
40
+ roster_entry?: string;
37
41
  server_url?: string;
38
42
  session_id?: string;
39
43
  sentinel_file?: string;
@@ -41,9 +45,28 @@ interface SpawnParams {
41
45
  auto_approve?: boolean;
42
46
  [key: string]: unknown;
43
47
  }
48
+ interface SpawnResult {
49
+ runtime_id?: string;
50
+ session_id?: string;
51
+ on_event?: string;
52
+ sentinel_file?: string;
53
+ broker_url?: string;
54
+ parent_token?: string;
55
+ agent?: string;
56
+ agent_profile?: string;
57
+ model?: string;
58
+ backend?: string;
59
+ roster_file?: string;
60
+ roster_entry?: string;
61
+ effective_agent?: string;
62
+ effective_model?: string;
63
+ effective_backend?: string;
64
+ [key: string]: unknown;
65
+ }
44
66
  declare class Client {
45
67
  private socket;
46
68
  private rl;
69
+ private closed;
47
70
  private nextID;
48
71
  private pending;
49
72
  private started;
@@ -66,12 +89,13 @@ declare class Client {
66
89
  status(runtimeId?: string): Promise<Record<string, unknown>>;
67
90
  result(runtimeId?: string): Promise<Record<string, unknown>>;
68
91
  list(): Promise<Array<Record<string, unknown>>>;
69
- spawn(params: SpawnParams): Promise<Record<string, unknown>>;
92
+ spawn(params: SpawnParams): Promise<SpawnResult>;
70
93
  shutdown(mode: string): Promise<void>;
71
94
  answerPermission(runtimeId: string, requestId: string, optionId: string, message?: string): Promise<void>;
72
95
  prompt(runtimeId: string, text: string): Promise<void>;
73
96
  interruptAndPrompt(runtimeId: string, text: string, keepQueue?: boolean): Promise<void>;
74
97
  cancel(runtimeId?: string): Promise<void>;
98
+ isClosed(): boolean;
75
99
  close(): void;
76
100
  }
77
101
  declare function dial(socketPath: string, opts?: {
@@ -241,6 +265,12 @@ interface RunInfo {
241
265
  agentProfile?: string;
242
266
  backend?: string;
243
267
  model?: string;
268
+ effectiveAgent?: string;
269
+ effectiveModel?: string;
270
+ effectiveBackend?: string;
271
+ rosterFile?: string;
272
+ rosterEntry?: string;
273
+ thinking?: ThinkingLevel;
244
274
  dir?: string;
245
275
  brokerUrl?: string;
246
276
  parentToken?: string;
@@ -250,6 +280,14 @@ interface SupervisorOptions {
250
280
  binaryPath?: string;
251
281
  callTimeoutMs?: number;
252
282
  }
283
+ /** Metadata retained locally when a follow-up uses a resolved direct identity. */
284
+ interface SpawnMetadata {
285
+ rosterFile?: string;
286
+ rosterEntry?: string;
287
+ effectiveAgent?: string;
288
+ effectiveModel?: string;
289
+ effectiveBackend?: string;
290
+ }
253
291
  declare function findAvenorBinary(): string;
254
292
  declare class Supervisor {
255
293
  private static instance;
@@ -262,6 +300,7 @@ declare class Supervisor {
262
300
  private crashed;
263
301
  private crashCode;
264
302
  private runs;
303
+ private aliases;
265
304
  private binaryPath;
266
305
  private callTimeoutMs;
267
306
  private constructor();
@@ -272,7 +311,7 @@ declare class Supervisor {
272
311
  static isCurrentInstance(supervisorId: string): boolean;
273
312
  static currentInstance(): Supervisor | null;
274
313
  getClient(): Client;
275
- spawn(params: SpawnParams, runId?: `${string}-${string}-${string}-${string}-${string}`): Promise<RunInfo>;
314
+ spawn(params: SpawnParams, runId?: `${string}-${string}-${string}-${string}-${string}`, metadata?: SpawnMetadata): Promise<RunInfo>;
276
315
  close(options?: {
277
316
  skipShutdown?: boolean;
278
317
  }): Promise<void>;
@@ -282,7 +321,7 @@ declare class Supervisor {
282
321
  declare function socketsRoot(): string;
283
322
  //#endregion
284
323
  //#region src/tools/spawn.d.ts
285
- declare function spawnTool(args: {
324
+ interface SpawnToolArgs {
286
325
  agent?: string;
287
326
  prompt?: string;
288
327
  promptFile?: string;
@@ -290,20 +329,36 @@ declare function spawnTool(args: {
290
329
  dir?: string;
291
330
  timeout?: string;
292
331
  model?: string;
332
+ thinking?: ThinkingLevel;
293
333
  agentProfile?: string;
294
334
  backend?: string;
335
+ rosterFile?: string;
336
+ rosterEntry?: string;
295
337
  serverUrl?: string;
296
338
  sessionId?: string;
297
339
  supervisorId?: string;
298
340
  parent_run_id?: string;
299
- }): Promise<{
341
+ }
342
+ interface SpawnToolResult {
300
343
  run_id: string;
301
344
  label: string;
302
345
  supervisor_id: string;
303
346
  runtime_id?: string;
304
347
  broker_url?: string;
305
348
  parent_token?: string;
306
- }>;
349
+ }
350
+ declare const spawnTool: (args: SpawnToolArgs) => Promise<SpawnToolResult>;
351
+ //#endregion
352
+ //#region src/spawn-selection.d.ts
353
+ interface SpawnSelectionInput {
354
+ agent?: string;
355
+ model?: string;
356
+ backend?: string;
357
+ roster_file?: string;
358
+ roster_entry?: string;
359
+ }
360
+ /** Validate the direct-versus-roster selector contract. */
361
+ declare function validateSpawnSelection(input: unknown, rosterConfigured?: boolean): void;
307
362
  //#endregion
308
363
  //#region src/tools/status.d.ts
309
364
  type StatusView = 'lifecycle' | 'full';
@@ -331,9 +386,15 @@ interface StatusResult {
331
386
  };
332
387
  session_id?: string;
333
388
  stop_reason?: string;
389
+ roster_file?: string;
390
+ roster_entry?: string;
334
391
  backend?: string;
335
392
  agent?: string;
336
393
  model?: string;
394
+ agent_profile?: string;
395
+ effective_backend?: string;
396
+ effective_agent?: string;
397
+ effective_model?: string;
337
398
  dir?: string;
338
399
  parent_id?: string;
339
400
  children?: string[];
@@ -430,6 +491,7 @@ interface FollowUpToolArgs {
430
491
  interface FollowUpToolResult {
431
492
  run_id: string;
432
493
  label: string;
494
+ runtime_id?: string;
433
495
  }
434
496
  declare const followUpTool: (args: FollowUpToolArgs) => Promise<FollowUpToolResult>;
435
497
  //#endregion
@@ -442,4 +504,4 @@ declare function shutdownTool(args: {
442
504
  cleaned_up: string[];
443
505
  }>;
444
506
  //#endregion
445
- export { type Client, DEFAULT_RUN_REDUCER_LIMITS, type Event, type HistoryOptions, type HistoryResult, type InspectResult, type NormalizedRunEvent, type ObserveRunOptions, type ResultResult, type ResultToolArgs, type RunChunkRole, type RunIdentity, type RunInfo, type RunObserver, type RunObserverClient, type RunPermissionOption, type RunPermissionSummary, RunReducer, type RunReducerLimits, type RunSnapshot, type RunSnapshotSeed, type RunToolState, type RunToolSummary, type RunTranscriptEntry, type SpawnParams, type StatusResult, type StatusView, type SubscribeOptions, Supervisor, type SupervisorOptions, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, findAvenorBinary, followUpTool, getInstallDir, getVersion, inspectTool, installerBinaryPath, normalizeRunEvent, observeRun, reduceRunSnapshot, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool };
507
+ export { type Client, DEFAULT_RUN_REDUCER_LIMITS, type Event, type FollowUpToolArgs, type FollowUpToolResult, type HistoryOptions, type HistoryResult, type InspectResult, type NormalizedRunEvent, type ObserveRunOptions, type ResultResult, type ResultToolArgs, type RunChunkRole, type RunIdentity, type RunInfo, type RunObserver, type RunObserverClient, type RunPermissionOption, type RunPermissionSummary, RunReducer, type RunReducerLimits, type RunSnapshot, type RunSnapshotSeed, type RunToolState, type RunToolSummary, type RunTranscriptEntry, type SpawnMetadata, type SpawnParams, type SpawnResult, type SpawnSelectionInput, type SpawnToolArgs, type SpawnToolResult, type StatusResult, type StatusToolArgs, type StatusView, type SubscribeOptions, Supervisor, type SupervisorOptions, type ThinkingLevel, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, findAvenorBinary, followUpTool, getInstallDir, getVersion, inspectTool, installerBinaryPath, normalizeRunEvent, observeRun, reduceRunSnapshot, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection };
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import * as crypto from "node:crypto";
9
+ import { umpire } from "@umpire/core";
10
+ import { fromJson } from "@umpire/json";
9
11
  //#region src/client.ts
10
12
  function normalizeSubscribeOptions(options) {
11
13
  if (!options) return void 0;
@@ -26,6 +28,7 @@ function removeResolver(resolvers, target) {
26
28
  var Client = class {
27
29
  socket;
28
30
  rl;
31
+ closed = false;
29
32
  nextID = 0;
30
33
  pending = /* @__PURE__ */ new Map();
31
34
  started = false;
@@ -44,11 +47,15 @@ var Client = class {
44
47
  });
45
48
  this.callTimeout = opts?.callTimeoutMs ?? 3e4;
46
49
  this.socket.on("error", (err) => {
50
+ this.closed = true;
47
51
  this.pushEvent({
48
52
  event: "protocol-error",
49
53
  message: err.message
50
54
  });
51
55
  });
56
+ this.socket.on("close", () => {
57
+ this.closed = true;
58
+ });
52
59
  }
53
60
  startReadLoop() {
54
61
  if (this.started) return;
@@ -85,6 +92,7 @@ var Client = class {
85
92
  }
86
93
  });
87
94
  this.rl.on("close", () => {
95
+ this.closed = true;
88
96
  for (const [, pc] of this.pending) {
89
97
  clearTimeout(pc.timer);
90
98
  pc.reject(/* @__PURE__ */ new Error("read response: connection closed"));
@@ -116,6 +124,7 @@ var Client = class {
116
124
  else this.dropped++;
117
125
  }
118
126
  async call(method, params) {
127
+ if (this.isClosed()) throw new Error("control socket is closed");
119
128
  this.startReadLoop();
120
129
  const id = ++this.nextID;
121
130
  const req = {
@@ -137,6 +146,7 @@ var Client = class {
137
146
  });
138
147
  this.socket.write(data, (err) => {
139
148
  if (err) {
149
+ this.closed = true;
140
150
  clearTimeout(timer);
141
151
  this.pending.delete(id);
142
152
  reject(/* @__PURE__ */ new Error(`write request: ${err.message}`));
@@ -272,7 +282,11 @@ var Client = class {
272
282
  const params = runtimeId ? { runtime_id: runtimeId } : void 0;
273
283
  await this.call("cancel", params);
274
284
  }
285
+ isClosed() {
286
+ return this.closed || this.socket.destroyed || this.socket.writableEnded;
287
+ }
275
288
  close() {
289
+ this.closed = true;
276
290
  this.rl.close();
277
291
  this.socket.destroy();
278
292
  }
@@ -1062,6 +1076,95 @@ function observeRun(client, options = {}) {
1062
1076
  };
1063
1077
  }
1064
1078
  //#endregion
1079
+ //#region ../../schemas/spawn_selection.umpire.json
1080
+ var spawn_selection_umpire_default = {
1081
+ version: 1,
1082
+ conditions: { "rosterConfigured": {
1083
+ "type": "boolean",
1084
+ "description": "Whether the caller has a configured roster context"
1085
+ } },
1086
+ fields: {
1087
+ "agent": { "isEmpty": "string" },
1088
+ "model": { "isEmpty": "string" },
1089
+ "backend": { "isEmpty": "string" },
1090
+ "roster_file": { "isEmpty": "string" },
1091
+ "roster_entry": { "isEmpty": "string" }
1092
+ },
1093
+ rules: [
1094
+ {
1095
+ "type": "requires",
1096
+ "field": "roster_file",
1097
+ "dependency": "roster_entry",
1098
+ "reason": "roster_file requires roster_entry"
1099
+ },
1100
+ {
1101
+ "type": "fairWhen",
1102
+ "field": "roster_entry",
1103
+ "when": {
1104
+ "op": "or",
1105
+ "exprs": [{
1106
+ "op": "present",
1107
+ "field": "roster_file"
1108
+ }, {
1109
+ "op": "cond",
1110
+ "condition": "rosterConfigured"
1111
+ }]
1112
+ },
1113
+ "reason": "roster_entry requires roster_file unless a roster context is configured"
1114
+ },
1115
+ {
1116
+ "type": "disables",
1117
+ "source": "roster_entry",
1118
+ "targets": [
1119
+ "agent",
1120
+ "model",
1121
+ "backend"
1122
+ ],
1123
+ "reason": "direct identity fields are disabled in roster mode"
1124
+ }
1125
+ ]
1126
+ };
1127
+ //#endregion
1128
+ //#region src/spawn-selection.ts
1129
+ const selectorFields = [
1130
+ "agent",
1131
+ "model",
1132
+ "backend",
1133
+ "roster_file",
1134
+ "roster_entry"
1135
+ ];
1136
+ const selectorFieldSet = new Set(selectorFields);
1137
+ const parsedSchema = fromJson(spawn_selection_umpire_default);
1138
+ const spawnSelectionUmpire = umpire({
1139
+ fields: parsedSchema.fields,
1140
+ rules: parsedSchema.rules
1141
+ });
1142
+ function assertInput(input) {
1143
+ if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("invalid spawn selector: expected an object");
1144
+ for (const key of Object.keys(input)) {
1145
+ if (!selectorFieldSet.has(key)) throw new Error(`invalid spawn selector: unknown field "${key}"`);
1146
+ const value = input[key];
1147
+ if (value !== void 0 && typeof value !== "string") throw new Error(`invalid spawn selector: ${key} must be a string`);
1148
+ }
1149
+ }
1150
+ /** Validate the direct-versus-roster selector contract. */
1151
+ function validateSpawnSelection(input, rosterConfigured = false) {
1152
+ assertInput(input);
1153
+ if (typeof rosterConfigured !== "boolean") throw new Error("invalid spawn selector: rosterConfigured must be a boolean");
1154
+ const values = {};
1155
+ for (const field of selectorFields) {
1156
+ const value = input[field];
1157
+ if (value !== void 0 && value !== "") values[field] = value;
1158
+ }
1159
+ const availability = spawnSelectionUmpire.check(values, { rosterConfigured });
1160
+ for (const field of selectorFields) {
1161
+ const value = values[field];
1162
+ const status = availability[field];
1163
+ if (value === void 0 || status?.enabled && status.fair) continue;
1164
+ throw new Error(`invalid spawn selector: ${status?.reason ?? `${field} is disabled`}`);
1165
+ }
1166
+ }
1167
+ //#endregion
1065
1168
  //#region src/paths.ts
1066
1169
  function avenorHome() {
1067
1170
  return process.env.AVENOR_HOME || path.join(os.homedir(), ".avenor");
@@ -1086,6 +1189,37 @@ function ensureRunPaths(runId) {
1086
1189
  }
1087
1190
  //#endregion
1088
1191
  //#region src/supervisor.ts
1192
+ function retainLiveIdentity(runInfo, liveStatus) {
1193
+ const value = (candidate) => typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
1194
+ const liveIdentity = (effectiveKey, directKey) => {
1195
+ if (typeof liveStatus[effectiveKey] === "string") return {
1196
+ present: true,
1197
+ value: value(liveStatus[effectiveKey])
1198
+ };
1199
+ if (typeof liveStatus[directKey] === "string") return {
1200
+ present: true,
1201
+ value: value(liveStatus[directKey])
1202
+ };
1203
+ return { present: false };
1204
+ };
1205
+ const effectiveAgent = liveIdentity("effective_agent", "agent");
1206
+ const effectiveModel = liveIdentity("effective_model", "model");
1207
+ const effectiveBackend = liveIdentity("effective_backend", "backend");
1208
+ const sessionId = value(liveStatus.session_id);
1209
+ const rosterFilePresent = typeof liveStatus.roster_file === "string";
1210
+ const rosterFile = value(liveStatus.roster_file);
1211
+ const rosterEntryPresent = typeof liveStatus.roster_entry === "string";
1212
+ const rosterEntry = value(liveStatus.roster_entry);
1213
+ const agentProfilePresent = typeof liveStatus.agent_profile === "string";
1214
+ const agentProfile = value(liveStatus.agent_profile);
1215
+ if (effectiveAgent.present) runInfo.agent = runInfo.effectiveAgent = effectiveAgent.value;
1216
+ if (effectiveModel.present) runInfo.model = runInfo.effectiveModel = effectiveModel.value;
1217
+ if (effectiveBackend.present) runInfo.backend = runInfo.effectiveBackend = effectiveBackend.value;
1218
+ if (sessionId) runInfo.sessionId = sessionId;
1219
+ if (rosterFilePresent) runInfo.rosterFile = rosterFile;
1220
+ if (rosterEntryPresent) runInfo.rosterEntry = rosterEntry;
1221
+ if (agentProfilePresent) runInfo.agentProfile = agentProfile;
1222
+ }
1089
1223
  function findAvenorBinary() {
1090
1224
  const envBin = process.env.AVENOR_BIN;
1091
1225
  if (envBin) try {
@@ -1142,6 +1276,7 @@ var Supervisor = class Supervisor {
1142
1276
  crashed = false;
1143
1277
  crashCode = null;
1144
1278
  runs = /* @__PURE__ */ new Map();
1279
+ aliases = /* @__PURE__ */ new Map();
1145
1280
  binaryPath;
1146
1281
  callTimeoutMs;
1147
1282
  constructor(binaryPath, socketPath, opts) {
@@ -1150,7 +1285,14 @@ var Supervisor = class Supervisor {
1150
1285
  this.callTimeoutMs = opts?.callTimeoutMs ?? 3e4;
1151
1286
  }
1152
1287
  static get(opts) {
1153
- if (Supervisor.instance && !Supervisor.instance.crashed) return Promise.resolve(Supervisor.instance);
1288
+ const current = Supervisor.instance;
1289
+ if (current && !current.crashed) {
1290
+ if (current.client && !current.client.isClosed()) return Promise.resolve(current);
1291
+ current.crashed = true;
1292
+ current.crashCode = 0;
1293
+ current.client?.close();
1294
+ current.client = null;
1295
+ }
1154
1296
  if (Supervisor.starting) return Supervisor.starting;
1155
1297
  const starting = Supervisor.start(opts);
1156
1298
  Supervisor.starting = starting;
@@ -1216,15 +1358,13 @@ var Supervisor = class Supervisor {
1216
1358
  reject(/* @__PURE__ */ new Error(`avenor exited with code ${code}${signal ? " signal " + signal : ""} during startup`));
1217
1359
  return;
1218
1360
  }
1219
- if (code !== 0 || signal) {
1220
- this.crashed = true;
1221
- this.crashCode = code ?? -1;
1222
- if (this.client) {
1223
- try {
1224
- this.client.close();
1225
- } catch {}
1226
- this.client = null;
1227
- }
1361
+ this.crashed = true;
1362
+ this.crashCode = code ?? -1;
1363
+ if (this.client) {
1364
+ try {
1365
+ this.client.close();
1366
+ } catch {}
1367
+ this.client = null;
1228
1368
  }
1229
1369
  });
1230
1370
  this.childProcess.on("error", (err) => {
@@ -1262,11 +1402,27 @@ var Supervisor = class Supervisor {
1262
1402
  return Supervisor.instance;
1263
1403
  }
1264
1404
  getClient() {
1265
- if (this.crashed) throw new Error(`avenor supervisor crashed (exit code ${this.crashCode}). Call avenor_shutdown to clean up and retry.`);
1405
+ if (this.crashed) {
1406
+ const state = this.crashCode === 0 ? "exited" : "crashed";
1407
+ throw new Error(`avenor supervisor ${state} (exit code ${this.crashCode}). Call avenor_shutdown to clean up and retry.`);
1408
+ }
1266
1409
  if (!this.client) throw new Error("supervisor not started");
1410
+ if (this.client.isClosed()) {
1411
+ this.crashed = true;
1412
+ this.crashCode = 0;
1413
+ this.client = null;
1414
+ throw new Error("avenor supervisor connection closed");
1415
+ }
1267
1416
  return this.client;
1268
1417
  }
1269
- async spawn(params, runId = crypto.randomUUID()) {
1418
+ async spawn(params, runId = crypto.randomUUID(), metadata) {
1419
+ if (!(typeof params.loop_file === "string" || typeof params.team_file === "string")) validateSpawnSelection({
1420
+ agent: params.agent,
1421
+ model: params.model,
1422
+ backend: params.backend,
1423
+ roster_file: params.roster_file,
1424
+ roster_entry: params.roster_entry
1425
+ });
1270
1426
  const client = this.getClient();
1271
1427
  const { sentinelPath, eventLogPath } = ensureRunPaths(runId);
1272
1428
  const spawnParams = {
@@ -1275,25 +1431,60 @@ var Supervisor = class Supervisor {
1275
1431
  sentinel_file: sentinelPath,
1276
1432
  on_event: eventLogPath
1277
1433
  };
1434
+ if (spawnParams.roster_entry) {
1435
+ for (const field of [
1436
+ "agent",
1437
+ "model",
1438
+ "backend"
1439
+ ]) if (spawnParams[field] === "") delete spawnParams[field];
1440
+ }
1278
1441
  const result = await client.spawn(spawnParams);
1442
+ let identityResult = result;
1443
+ if (spawnParams.roster_entry && result.runtime_id) {
1444
+ const hasBackend = [result.effective_backend, result.backend].some((value) => typeof value === "string" && value.length > 0);
1445
+ const hasAgentOrModel = [
1446
+ result.effective_agent,
1447
+ result.agent,
1448
+ result.effective_model,
1449
+ result.model
1450
+ ].some((value) => typeof value === "string" && value.length > 0);
1451
+ if (!(hasBackend && hasAgentOrModel)) try {
1452
+ identityResult = {
1453
+ ...result,
1454
+ ...await client.status(result.runtime_id)
1455
+ };
1456
+ } catch {}
1457
+ }
1458
+ const value = (candidate) => typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
1459
+ const effectiveAgent = metadata?.effectiveAgent ?? value(identityResult.effective_agent) ?? value(identityResult.agent) ?? value(spawnParams.agent);
1460
+ const effectiveModel = metadata?.effectiveModel ?? value(identityResult.effective_model) ?? value(identityResult.model) ?? value(spawnParams.model);
1461
+ const effectiveBackend = metadata?.effectiveBackend ?? value(identityResult.effective_backend) ?? value(identityResult.backend) ?? value(spawnParams.backend);
1462
+ const rosterFile = metadata?.rosterFile ?? value(identityResult.roster_file) ?? value(spawnParams.roster_file);
1463
+ const rosterEntry = metadata?.rosterEntry ?? value(identityResult.roster_entry) ?? value(spawnParams.roster_entry);
1279
1464
  const runInfo = {
1280
1465
  runId,
1281
1466
  label: spawnParams.label ?? runId,
1282
1467
  sentinelPath: result.sentinel_file ?? sentinelPath,
1283
1468
  eventLogPath: result.on_event ?? eventLogPath,
1284
- runtimeId: result.runtime_id,
1285
- sessionId: result.session_id,
1286
- agent: spawnParams.agent ?? params.agent,
1287
- agentProfile: spawnParams.agent_profile,
1288
- backend: spawnParams.backend,
1289
- model: spawnParams.model,
1469
+ runtimeId: value(identityResult.runtime_id),
1470
+ sessionId: value(identityResult.session_id),
1471
+ agent: effectiveAgent,
1472
+ agentProfile: value(identityResult.agent_profile) ?? spawnParams.agent_profile,
1473
+ backend: effectiveBackend,
1474
+ model: effectiveModel,
1475
+ effectiveAgent,
1476
+ effectiveModel,
1477
+ effectiveBackend,
1478
+ rosterFile,
1479
+ rosterEntry,
1480
+ thinking: spawnParams.thinking,
1290
1481
  dir: spawnParams.dir,
1291
1482
  brokerUrl: result.broker_url,
1292
1483
  parentToken: result.parent_token,
1293
1484
  autoApprove: spawnParams.auto_approve
1294
1485
  };
1295
- this.runs.set(runInfo.label, runInfo);
1296
1486
  this.runs.set(runId, runInfo);
1487
+ this.aliases.set(runInfo.label, runInfo);
1297
1488
  return runInfo;
1298
1489
  }
1299
1490
  async close(options = {}) {
@@ -1316,6 +1507,7 @@ var Supervisor = class Supervisor {
1316
1507
  this.crashed = false;
1317
1508
  this.crashCode = null;
1318
1509
  this.runs.clear();
1510
+ this.aliases.clear();
1319
1511
  Supervisor.instance = null;
1320
1512
  }
1321
1513
  };
@@ -1361,8 +1553,125 @@ async function getSupervisorClient(supervisorId) {
1361
1553
  };
1362
1554
  }
1363
1555
  //#endregion
1556
+ //#region src/tools/run-registry.ts
1557
+ const externalRuns = /* @__PURE__ */ new Map();
1558
+ function runsFor(supervisorId) {
1559
+ let runs = externalRuns.get(supervisorId);
1560
+ if (!runs) {
1561
+ runs = {
1562
+ byRunId: /* @__PURE__ */ new Map(),
1563
+ byAlias: /* @__PURE__ */ new Map()
1564
+ };
1565
+ externalRuns.set(supervisorId, runs);
1566
+ }
1567
+ return runs;
1568
+ }
1569
+ function indexRun(runInfo) {
1570
+ const runs = runsFor(runInfo.supervisorId);
1571
+ runs.byRunId.set(runInfo.runId, runInfo);
1572
+ runs.byAlias.set(runInfo.label, runInfo);
1573
+ if (runInfo.runtimeId) runs.byAlias.set(runInfo.runtimeId, runInfo);
1574
+ }
1575
+ function isSafeRunReference(reference) {
1576
+ return /^[a-zA-Z0-9_-]+$/.test(reference);
1577
+ }
1578
+ function externalRunMetadataPath(runId) {
1579
+ if (!isSafeRunReference(runId)) throw new Error(`unsafe external run reference: ${runId}`);
1580
+ return path.join(runsRoot(), runId, "run.json");
1581
+ }
1582
+ function optionalString(value) {
1583
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1584
+ }
1585
+ function readPersistedRun(supervisorId, runId) {
1586
+ if (!isSafeRunReference(runId)) return void 0;
1587
+ try {
1588
+ const parsed = JSON.parse(fs.readFileSync(externalRunMetadataPath(runId), "utf-8"));
1589
+ if (parsed.schemaVersion !== 1 || parsed.runId !== runId || parsed.supervisorId !== supervisorId || typeof parsed.label !== "string") return;
1590
+ const runDir = path.join(runsRoot(), runId);
1591
+ const runInfo = {
1592
+ runId,
1593
+ label: parsed.label,
1594
+ supervisorId,
1595
+ sentinelPath: path.join(runDir, "sentinel.done"),
1596
+ eventLogPath: path.join(runDir, "events.log"),
1597
+ runtimeId: optionalString(parsed.runtimeId),
1598
+ sessionId: optionalString(parsed.sessionId),
1599
+ agent: optionalString(parsed.agent),
1600
+ agentProfile: optionalString(parsed.agentProfile),
1601
+ backend: optionalString(parsed.backend),
1602
+ model: optionalString(parsed.model),
1603
+ effectiveAgent: optionalString(parsed.effectiveAgent),
1604
+ effectiveModel: optionalString(parsed.effectiveModel),
1605
+ effectiveBackend: optionalString(parsed.effectiveBackend),
1606
+ rosterFile: optionalString(parsed.rosterFile),
1607
+ rosterEntry: optionalString(parsed.rosterEntry),
1608
+ thinking: parsed.thinking,
1609
+ dir: optionalString(parsed.dir),
1610
+ brokerUrl: optionalString(parsed.brokerUrl),
1611
+ parentToken: optionalString(parsed.parentToken),
1612
+ autoApprove: typeof parsed.autoApprove === "boolean" ? parsed.autoApprove : void 0
1613
+ };
1614
+ indexRun(runInfo);
1615
+ return runInfo;
1616
+ } catch {
1617
+ return;
1618
+ }
1619
+ }
1620
+ /** Persist the public-to-runtime identity for runs spawned through an external supervisor. */
1621
+ function registerExternalRun(runInfo) {
1622
+ const persisted = {
1623
+ schemaVersion: 1,
1624
+ ...runInfo,
1625
+ parentToken: void 0
1626
+ };
1627
+ const metadataPath = externalRunMetadataPath(runInfo.runId);
1628
+ const temporaryPath = `${metadataPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
1629
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(persisted, null, 2)}\n`, {
1630
+ mode: 384,
1631
+ flag: "wx"
1632
+ });
1633
+ try {
1634
+ fs.renameSync(temporaryPath, metadataPath);
1635
+ } catch (error) {
1636
+ try {
1637
+ fs.unlinkSync(temporaryPath);
1638
+ } catch {}
1639
+ throw error;
1640
+ }
1641
+ indexRun(runInfo);
1642
+ }
1643
+ function findExternalRun(supervisorId, reference) {
1644
+ const exact = externalRuns.get(supervisorId)?.byRunId.get(reference);
1645
+ if (exact) return exact;
1646
+ const persisted = readPersistedRun(supervisorId, reference);
1647
+ if (persisted) return persisted;
1648
+ listExternalRuns(supervisorId);
1649
+ return externalRuns.get(supervisorId)?.byAlias.get(reference);
1650
+ }
1651
+ function listExternalRuns(supervisorId) {
1652
+ try {
1653
+ for (const entry of fs.readdirSync(runsRoot(), { withFileTypes: true })) if (entry.isDirectory()) readPersistedRun(supervisorId, entry.name);
1654
+ } catch {}
1655
+ return [...externalRuns.get(supervisorId)?.byRunId.values() ?? []];
1656
+ }
1657
+ function forgetExternalRuns(supervisorId) {
1658
+ externalRuns.delete(supervisorId);
1659
+ }
1660
+ function clearExternalRuns(supervisorId) {
1661
+ const runs = listExternalRuns(supervisorId);
1662
+ forgetExternalRuns(supervisorId);
1663
+ return runs;
1664
+ }
1665
+ //#endregion
1364
1666
  //#region src/tools/spawn.ts
1365
- async function spawnTool(args) {
1667
+ async function executeSpawnTool(args, getSupervisorClient) {
1668
+ validateSpawnSelection({
1669
+ agent: args.agent,
1670
+ model: args.model,
1671
+ backend: args.backend,
1672
+ roster_file: args.rosterFile,
1673
+ roster_entry: args.rosterEntry
1674
+ });
1366
1675
  const runId = crypto.randomUUID();
1367
1676
  const label = args.label ?? runId;
1368
1677
  const baseParams = {
@@ -1378,9 +1687,23 @@ async function spawnTool(args) {
1378
1687
  session_id: args.sessionId,
1379
1688
  parent_run_id: args.parent_run_id
1380
1689
  };
1690
+ if (args.rosterFile) baseParams.roster_file = args.rosterFile;
1691
+ if (args.rosterEntry) baseParams.roster_entry = args.rosterEntry;
1381
1692
  if (args.agent) baseParams.agent = args.agent;
1693
+ if (args.thinking !== void 0) baseParams.thinking = args.thinking;
1382
1694
  if (args.supervisorId) {
1383
- const { client, isSingleton, supervisorId } = await getSupervisorClient(args.supervisorId);
1695
+ const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId);
1696
+ if (isSingleton && sup) {
1697
+ const runInfo = await sup.spawn(baseParams, runId);
1698
+ return {
1699
+ run_id: runInfo.runId,
1700
+ label: runInfo.label,
1701
+ supervisor_id: supervisorId,
1702
+ runtime_id: runInfo.runtimeId,
1703
+ broker_url: runInfo.brokerUrl,
1704
+ parent_token: runInfo.parentToken
1705
+ };
1706
+ }
1384
1707
  const { sentinelPath, eventLogPath } = ensureRunPaths(runId);
1385
1708
  try {
1386
1709
  const result = await client.spawn({
@@ -1388,13 +1711,37 @@ async function spawnTool(args) {
1388
1711
  sentinel_file: sentinelPath,
1389
1712
  on_event: eventLogPath
1390
1713
  });
1714
+ const stringValue = (candidate) => typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
1715
+ const runtimeId = stringValue(result.runtime_id);
1716
+ registerExternalRun({
1717
+ runId,
1718
+ label,
1719
+ supervisorId,
1720
+ sentinelPath,
1721
+ eventLogPath,
1722
+ runtimeId,
1723
+ sessionId: stringValue(result.session_id),
1724
+ agent: stringValue(result.effective_agent) ?? stringValue(result.agent) ?? baseParams.agent,
1725
+ model: stringValue(result.effective_model) ?? stringValue(result.model) ?? baseParams.model,
1726
+ backend: stringValue(result.effective_backend) ?? stringValue(result.backend) ?? baseParams.backend,
1727
+ effectiveAgent: stringValue(result.effective_agent) ?? stringValue(result.agent) ?? baseParams.agent,
1728
+ effectiveModel: stringValue(result.effective_model) ?? stringValue(result.model) ?? baseParams.model,
1729
+ effectiveBackend: stringValue(result.effective_backend) ?? stringValue(result.backend) ?? baseParams.backend,
1730
+ agentProfile: stringValue(result.agent_profile) ?? baseParams.agent_profile,
1731
+ rosterFile: stringValue(result.roster_file) ?? baseParams.roster_file,
1732
+ rosterEntry: stringValue(result.roster_entry) ?? baseParams.roster_entry,
1733
+ thinking: baseParams.thinking,
1734
+ dir: baseParams.dir,
1735
+ brokerUrl: stringValue(result.broker_url),
1736
+ parentToken: stringValue(result.parent_token)
1737
+ });
1391
1738
  return {
1392
- run_id: result.runtime_id ?? runId,
1739
+ run_id: runId,
1393
1740
  label,
1394
1741
  supervisor_id: supervisorId,
1395
- runtime_id: result.runtime_id,
1396
- broker_url: result.broker_url,
1397
- parent_token: result.parent_token
1742
+ runtime_id: runtimeId,
1743
+ broker_url: stringValue(result.broker_url),
1744
+ parent_token: stringValue(result.parent_token)
1398
1745
  };
1399
1746
  } finally {
1400
1747
  if (!isSingleton) client.close();
@@ -1411,14 +1758,25 @@ async function spawnTool(args) {
1411
1758
  parent_token: runInfo.parentToken
1412
1759
  };
1413
1760
  }
1761
+ function createSpawnTool(getSupervisorClient) {
1762
+ return (args) => executeSpawnTool(args, getSupervisorClient);
1763
+ }
1764
+ const spawnTool = createSpawnTool(getSupervisorClient);
1414
1765
  //#endregion
1415
- //#region src/tools/status.ts
1416
- function findRunByLabel$3(sup, runId) {
1417
- const runs = sup.runs;
1418
- const byKey = runs.get(runId);
1419
- if (byKey) return byKey;
1420
- for (const info of runs.values()) if (info.label === runId) return info;
1766
+ //#region src/tools/run-resolution.ts
1767
+ /** Resolve a local public run ID before considering a label alias. */
1768
+ function findLocalRunByReference(sup, reference) {
1769
+ const { runs, aliases } = sup;
1770
+ return runs.get(reference) ?? aliases.get(reference);
1771
+ }
1772
+ /** Match a supervisor list entry to its stored runtime identity. */
1773
+ function findLocalRunByRuntimeId(sup, runtimeId) {
1774
+ if (!runtimeId) return void 0;
1775
+ const { runs } = sup;
1776
+ for (const runInfo of runs.values()) if (runInfo.runtimeId === runtimeId) return runInfo;
1421
1777
  }
1778
+ //#endregion
1779
+ //#region src/tools/status.ts
1422
1780
  async function parseSentinel$1(filePath) {
1423
1781
  try {
1424
1782
  const content = (await fs.promises.readFile(filePath, "utf-8")).trim();
@@ -1506,6 +1864,15 @@ function extractPendingPermission(liveStatus) {
1506
1864
  return normalizePermission(asRecord(pendingPermission));
1507
1865
  }
1508
1866
  function buildBaseStatus(source, fallback, translatedStatus) {
1867
+ const identity = fallback.identity;
1868
+ const authoritativeValue = (effectiveKey, directKey, fallbackEffective, fallbackDirect) => {
1869
+ if (typeof source[effectiveKey] === "string") return stringField(source, effectiveKey);
1870
+ if (typeof source[directKey] === "string") return stringField(source, directKey);
1871
+ return fallbackEffective ?? fallbackDirect;
1872
+ };
1873
+ const effectiveAgent = authoritativeValue("effective_agent", "agent", identity?.effective_agent, identity?.agent);
1874
+ const effectiveModel = authoritativeValue("effective_model", "model", identity?.effective_model, identity?.model);
1875
+ const effectiveBackend = authoritativeValue("effective_backend", "backend", identity?.effective_backend, identity?.backend);
1509
1876
  return {
1510
1877
  run_id: fallback.prefer_fallback_run_id ? fallback.run_id : stringField(source, "run_id") ?? fallback.run_id,
1511
1878
  label: stringField(source, "label") ?? fallback.label,
@@ -1516,9 +1883,15 @@ function buildBaseStatus(source, fallback, translatedStatus) {
1516
1883
  pending_permission: extractPendingPermission(source),
1517
1884
  session_id: stringField(source, "session_id") ?? fallback.session_id,
1518
1885
  stop_reason: stringField(source, "stop_reason") ?? fallback.stop_reason,
1519
- backend: stringField(source, "backend"),
1520
- agent: stringField(source, "agent"),
1521
- model: stringField(source, "model"),
1886
+ roster_file: typeof source.roster_file === "string" ? stringField(source, "roster_file") : identity?.roster_file,
1887
+ roster_entry: typeof source.roster_entry === "string" ? stringField(source, "roster_entry") : identity?.roster_entry,
1888
+ backend: effectiveBackend,
1889
+ agent: effectiveAgent,
1890
+ model: effectiveModel,
1891
+ agent_profile: typeof source.agent_profile === "string" ? stringField(source, "agent_profile") : identity?.agent_profile,
1892
+ effective_backend: effectiveBackend,
1893
+ effective_agent: effectiveAgent,
1894
+ effective_model: effectiveModel,
1522
1895
  dir: stringField(source, "dir"),
1523
1896
  parent_id: stringField(source, "parent_id", "parentId"),
1524
1897
  children: stringArrayField(source, "children"),
@@ -1545,6 +1918,7 @@ function shapeStatusResult(result, view = "full") {
1545
1918
  };
1546
1919
  }
1547
1920
  async function buildRunStatus(runInfo, liveStatus) {
1921
+ if (liveStatus) retainLiveIdentity(runInfo, liveStatus);
1548
1922
  let sentinel = null;
1549
1923
  if (await sentinelExists(runInfo.sentinelPath)) sentinel = await parseSentinel$1(runInfo.sentinelPath);
1550
1924
  const translated = translateStatus(rawStatusPhase(liveStatus), sentinel);
@@ -1554,7 +1928,18 @@ async function buildRunStatus(runInfo, liveStatus) {
1554
1928
  prefer_fallback_run_id: true,
1555
1929
  runtime_id: runInfo.runtimeId,
1556
1930
  session_id: sentinel?.SESSION ?? liveStatus?.session_id ?? runInfo.sessionId,
1557
- stop_reason: sentinel?._status
1931
+ stop_reason: sentinel?.STOP_REASON,
1932
+ identity: {
1933
+ roster_file: runInfo.rosterFile,
1934
+ roster_entry: runInfo.rosterEntry,
1935
+ agent: runInfo.agent,
1936
+ model: runInfo.model,
1937
+ backend: runInfo.backend,
1938
+ agent_profile: runInfo.agentProfile,
1939
+ effective_agent: runInfo.effectiveAgent,
1940
+ effective_model: runInfo.effectiveModel,
1941
+ effective_backend: runInfo.effectiveBackend
1942
+ }
1558
1943
  }, translated);
1559
1944
  }
1560
1945
  async function queryLiveStatus(client, runtimeId) {
@@ -1567,11 +1952,11 @@ async function queryLiveStatus(client, runtimeId) {
1567
1952
  }
1568
1953
  async function executeStatusTool(args, getSupervisorClient) {
1569
1954
  if (args.supervisorId) {
1570
- const { client, isSingleton, sup } = await getSupervisorClient(args.supervisorId);
1955
+ const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId);
1571
1956
  try {
1572
1957
  if (args.runId) {
1573
1958
  validateRunId(args.runId);
1574
- const runInfo = isSingleton && sup ? findRunByLabel$3(sup, args.runId) : void 0;
1959
+ const runInfo = isSingleton && sup ? findLocalRunByReference(sup, args.runId) : findExternalRun(supervisorId, args.runId);
1575
1960
  if (runInfo) return shapeStatusResult(await buildRunStatus(runInfo, await queryLiveStatus(client, runInfo.runtimeId)), args.view);
1576
1961
  let liveStatus = await queryLiveStatus(client, args.runId);
1577
1962
  if (!liveStatus) try {
@@ -1589,17 +1974,30 @@ async function executeStatusTool(args, getSupervisorClient) {
1589
1974
  return shapeStatusResult(buildBaseStatus(liveStatus ?? {}, {
1590
1975
  run_id: args.runId,
1591
1976
  label: args.runId,
1977
+ prefer_fallback_run_id: true,
1592
1978
  runtime_id: stringField(liveStatus ?? {}, "runtime_id", "id"),
1593
1979
  session_id: sentinel?.SESSION ?? liveStatus?.session_id,
1594
- stop_reason: sentinel?._status
1980
+ stop_reason: sentinel?.STOP_REASON
1595
1981
  }, translateStatus(rawStatusPhase(liveStatus), sentinel)), args.view);
1596
1982
  }
1597
- return (await client.list()).map((entry) => shapeStatusResult(buildBaseStatus(entry, {
1598
- run_id: String(entry.run_id ?? entry.runtime_id ?? entry.id ?? ""),
1599
- label: String(entry.label ?? entry.runtime_id ?? entry.id ?? ""),
1600
- runtime_id: String(entry.runtime_id ?? entry.id ?? ""),
1601
- session_id: entry.session_id
1602
- }, translateStatus(rawStatusPhase(entry), null)), args.view));
1983
+ const list = await client.list();
1984
+ if (!isSingleton) listExternalRuns(supervisorId);
1985
+ const results = [];
1986
+ for (const entry of list) {
1987
+ const entryId = String(entry.runtime_id ?? entry.id ?? "");
1988
+ const runInfo = !isSingleton ? findExternalRun(supervisorId, entryId) ?? findExternalRun(supervisorId, String(entry.run_id ?? "")) ?? findExternalRun(supervisorId, String(entry.label ?? "")) : void 0;
1989
+ if (runInfo) {
1990
+ results.push(shapeStatusResult(await buildRunStatus(runInfo, entry), args.view));
1991
+ continue;
1992
+ }
1993
+ results.push(shapeStatusResult(buildBaseStatus(entry, {
1994
+ run_id: String(entry.run_id ?? entryId),
1995
+ label: String(entry.label ?? entryId),
1996
+ runtime_id: entryId,
1997
+ session_id: entry.session_id
1998
+ }, translateStatus(rawStatusPhase(entry), null)), args.view));
1999
+ }
2000
+ return results;
1603
2001
  } finally {
1604
2002
  if (!isSingleton) client.close();
1605
2003
  }
@@ -1607,7 +2005,7 @@ async function executeStatusTool(args, getSupervisorClient) {
1607
2005
  const sup = await Supervisor.get();
1608
2006
  const client = sup.getClient();
1609
2007
  if (args.runId) {
1610
- const runInfo = findRunByLabel$3(sup, args.runId);
2008
+ const runInfo = findLocalRunByReference(sup, args.runId);
1611
2009
  if (runInfo) return shapeStatusResult(await buildRunStatus(runInfo, await queryLiveStatus(client, runInfo.runtimeId)), args.view);
1612
2010
  validateRunId(args.runId);
1613
2011
  const liveStatus = await queryLiveStatus(client, args.runId);
@@ -1619,7 +2017,7 @@ async function executeStatusTool(args, getSupervisorClient) {
1619
2017
  label: args.runId,
1620
2018
  runtime_id: stringField(liveStatus ?? {}, "runtime_id", "id"),
1621
2019
  session_id: sentinel?.SESSION ?? liveStatus?.session_id,
1622
- stop_reason: sentinel?._status
2020
+ stop_reason: sentinel?.STOP_REASON
1623
2021
  }, translateStatus(rawStatusPhase(liveStatus), sentinel)), args.view);
1624
2022
  }
1625
2023
  const list = await client.list();
@@ -1627,9 +2025,9 @@ async function executeStatusTool(args, getSupervisorClient) {
1627
2025
  for (const entry of list) {
1628
2026
  const entryId = String(entry.runtime_id ?? entry.id ?? "");
1629
2027
  const entryLabel = String(entry.label ?? entryId);
1630
- const runInfo = findRunByLabel$3(sup, entryId) ?? findRunByLabel$3(sup, entryLabel);
2028
+ const runInfo = findLocalRunByRuntimeId(sup, entryId) ?? findLocalRunByReference(sup, entryId) ?? findLocalRunByReference(sup, entryLabel);
1631
2029
  const liveStatus = runInfo ? await queryLiveStatus(client, runInfo.runtimeId) : null;
1632
- if (runInfo && liveStatus) results.push(shapeStatusResult(await buildRunStatus(runInfo, liveStatus), args.view));
2030
+ if (runInfo) results.push(shapeStatusResult(await buildRunStatus(runInfo, liveStatus), args.view));
1633
2031
  else results.push(shapeStatusResult(buildBaseStatus(entry, {
1634
2032
  run_id: String(entry.run_id ?? entryId),
1635
2033
  label: entryLabel,
@@ -1649,12 +2047,6 @@ const MAX_EVENT_LIMIT = 1e3;
1649
2047
  const MAX_STRING_CHARS = 4e3;
1650
2048
  const MAX_ARRAY_ITEMS = 32;
1651
2049
  const MAX_OBJECT_KEYS = 64;
1652
- function findRunByLabel$2(sup, runId) {
1653
- const runs = sup.runs;
1654
- const byKey = runs.get(runId);
1655
- if (byKey) return byKey;
1656
- for (const info of runs.values()) if (info.label === runId) return info;
1657
- }
1658
2050
  function normalizeLimit(limit) {
1659
2051
  return Math.max(1, Math.min(MAX_EVENT_LIMIT, Math.trunc(limit ?? 50)));
1660
2052
  }
@@ -1724,8 +2116,8 @@ async function readNdjsonEvents(filePath, args) {
1724
2116
  }
1725
2117
  async function resolveRun(args, getSupervisorClient) {
1726
2118
  if (args.supervisorId) {
1727
- const { client, isSingleton, sup } = await getSupervisorClient(args.supervisorId);
1728
- const runInfo = isSingleton && sup ? findRunByLabel$2(sup, args.runId) : void 0;
2119
+ const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId);
2120
+ const runInfo = isSingleton && sup ? findLocalRunByReference(sup, args.runId) : findExternalRun(supervisorId, args.runId);
1729
2121
  if (runInfo) return {
1730
2122
  client,
1731
2123
  isSingleton,
@@ -1768,7 +2160,7 @@ async function resolveRun(args, getSupervisorClient) {
1768
2160
  };
1769
2161
  }
1770
2162
  const sup = await Supervisor.get();
1771
- const runInfo = findRunByLabel$2(sup, args.runId);
2163
+ const runInfo = findLocalRunByReference(sup, args.runId);
1772
2164
  if (runInfo) return {
1773
2165
  client: sup.getClient(),
1774
2166
  isSingleton: true,
@@ -1794,6 +2186,7 @@ async function resolveRun(args, getSupervisorClient) {
1794
2186
  };
1795
2187
  }
1796
2188
  async function executeEventsTool(args, getSupervisorClient) {
2189
+ validateRunId(args.runId);
1797
2190
  const limit = normalizeLimit(args.limit);
1798
2191
  const resolved = await resolveRun(args, getSupervisorClient);
1799
2192
  try {
@@ -2002,12 +2395,6 @@ async function resultTool(args) {
2002
2395
  }
2003
2396
  //#endregion
2004
2397
  //#region src/tools/answer-permission.ts
2005
- function findRunByLabel$1(sup, runId) {
2006
- const runs = sup.runs;
2007
- const byKey = runs.get(runId);
2008
- if (byKey) return byKey;
2009
- for (const info of runs.values()) if (info.label === runId) return info;
2010
- }
2011
2398
  function pendingPermissionRequestID(liveStatus) {
2012
2399
  const status = asRecord(liveStatus);
2013
2400
  if (!status) return void 0;
@@ -2027,7 +2414,7 @@ async function answerPermissionTool(args) {
2027
2414
  }
2028
2415
  try {
2029
2416
  validateRunId(args.runId);
2030
- const runtimeId = (sup ? findRunByLabel$1(sup, args.runId) : void 0)?.runtimeId ?? args.runId;
2417
+ const runtimeId = (sup ? findLocalRunByReference(sup, args.runId) : supervisorId ? findExternalRun(supervisorId, args.runId) : void 0)?.runtimeId ?? args.runId;
2031
2418
  if (!requestId) {
2032
2419
  requestId = pendingPermissionRequestID(await client.status(runtimeId));
2033
2420
  if (!requestId) throw new Error("no pending permission request for this run");
@@ -2040,20 +2427,18 @@ async function answerPermissionTool(args) {
2040
2427
  }
2041
2428
  //#endregion
2042
2429
  //#region src/tools/follow-up.ts
2043
- function findRunByLabel(sup, runId) {
2044
- const runs = sup.runs;
2045
- const byKey = runs.get(runId);
2046
- if (byKey) return byKey;
2047
- for (const info of runs.values()) if (info.label === runId) return info;
2048
- }
2049
2430
  function resolveAutoApprove(liveStatus, runInfo) {
2050
2431
  const liveAutoApprove = liveStatus?.auto_approve;
2051
2432
  if (typeof liveAutoApprove === "boolean") return liveAutoApprove;
2052
2433
  return typeof runInfo?.autoApprove === "boolean" ? runInfo.autoApprove : void 0;
2053
2434
  }
2054
- function requireResumableAgent(agent) {
2055
- if (agent === void 0) throw new Error("run has no agent to resume");
2056
- return agent;
2435
+ function value(candidate) {
2436
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
2437
+ }
2438
+ function resolvedIdentity(liveStatus, effectiveKey, directKey, fallbackEffective, fallbackDirect) {
2439
+ if (liveStatus && typeof liveStatus[effectiveKey] === "string") return value(liveStatus[effectiveKey]);
2440
+ if (liveStatus && typeof liveStatus[directKey] === "string") return value(liveStatus[directKey]);
2441
+ return fallbackEffective ?? fallbackDirect;
2057
2442
  }
2058
2443
  async function parseSentinel(filePath) {
2059
2444
  try {
@@ -2073,81 +2458,144 @@ async function parseSentinel(filePath) {
2073
2458
  return null;
2074
2459
  }
2075
2460
  }
2461
+ function rejectSessionConflict(sentinel, liveStatus) {
2462
+ const sentinelStopReason = value(sentinel?.STOP_REASON);
2463
+ const liveStopReason = value(liveStatus?.stop_reason);
2464
+ if (sentinelStopReason === "session_id_conflict" || liveStopReason === "session_id_conflict") throw new Error("run is not resumable: session_id_conflict");
2465
+ }
2076
2466
  async function executeFollowUpTool(args, getSupervisorClient) {
2077
2467
  if (args.supervisorId) {
2078
- const { client, isSingleton, sup } = await getSupervisorClient(args.supervisorId);
2468
+ const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId);
2079
2469
  try {
2080
2470
  validateRunId(args.runId);
2081
2471
  let runInfo;
2082
- if (isSingleton && sup) runInfo = findRunByLabel(sup, args.runId);
2472
+ if (isSingleton && sup) runInfo = findLocalRunByReference(sup, args.runId);
2473
+ else runInfo = findExternalRun(supervisorId, args.runId);
2083
2474
  const fallbackSessionId = runInfo?.sessionId;
2084
2475
  let liveStatus = null;
2085
2476
  try {
2086
2477
  liveStatus = await client.status(runInfo?.runtimeId ?? args.runId);
2087
2478
  } catch {}
2088
- const sessionId = (await parseSentinel(liveStatus?.sentinel_file ?? path.join(runsRoot(), args.runId, "sentinel.done")))?.SESSION ?? liveStatus?.session_id ?? fallbackSessionId;
2479
+ const sentinel = await parseSentinel(liveStatus?.sentinel_file ?? runInfo?.sentinelPath ?? path.join(runsRoot(), args.runId, "sentinel.done"));
2480
+ rejectSessionConflict(sentinel, liveStatus);
2481
+ const sessionId = sentinel?.SESSION ?? liveStatus?.session_id ?? fallbackSessionId;
2089
2482
  if (!sessionId) throw new Error("run has no session to resume");
2090
- const agent = requireResumableAgent(liveStatus?.agent ?? runInfo?.agent);
2091
- const backend = liveStatus?.backend ?? runInfo?.backend;
2092
- const model = liveStatus?.model ?? runInfo?.model;
2483
+ const agent = resolvedIdentity(liveStatus, "effective_agent", "agent", runInfo?.effectiveAgent, runInfo?.agent);
2484
+ const model = resolvedIdentity(liveStatus, "effective_model", "model", runInfo?.effectiveModel, runInfo?.model);
2485
+ const backend = resolvedIdentity(liveStatus, "effective_backend", "backend", runInfo?.effectiveBackend, runInfo?.backend);
2486
+ if (runInfo && liveStatus) retainLiveIdentity(runInfo, liveStatus);
2487
+ const thinking = liveStatus?.thinking ?? runInfo?.thinking;
2093
2488
  const dir = liveStatus?.dir ?? runInfo?.dir;
2094
2489
  const agentProfile = liveStatus?.agent_profile ?? runInfo?.agentProfile;
2095
2490
  const autoApprove = resolveAutoApprove(liveStatus, runInfo);
2096
2491
  const followUpRunId = crypto.randomUUID();
2097
2492
  const followUpLabel = args.label ?? `${args.runId}-followup`;
2098
- const { sentinelPath: followUpSentinelPath, eventLogPath } = ensureRunPaths(followUpRunId);
2099
2493
  const spawnParams = {
2100
- agent,
2101
2494
  prompt: args.message,
2102
2495
  label: followUpLabel,
2103
- session_id: sessionId,
2104
- sentinel_file: followUpSentinelPath,
2105
- on_event: eventLogPath
2496
+ session_id: sessionId
2106
2497
  };
2498
+ if (agent) spawnParams.agent = agent;
2107
2499
  if (backend) spawnParams.backend = backend;
2108
2500
  if (model) spawnParams.model = model;
2501
+ if (thinking) spawnParams.thinking = thinking;
2109
2502
  if (dir) spawnParams.dir = dir;
2110
2503
  if (agentProfile) spawnParams.agent_profile = agentProfile;
2111
2504
  if (autoApprove === true) spawnParams.auto_approve = true;
2505
+ if (isSingleton && sup) {
2506
+ const followUpRun = await sup.spawn(spawnParams, followUpRunId, {
2507
+ rosterFile: runInfo?.rosterFile,
2508
+ rosterEntry: runInfo?.rosterEntry,
2509
+ effectiveAgent: agent,
2510
+ effectiveModel: model,
2511
+ effectiveBackend: backend
2512
+ });
2513
+ return {
2514
+ run_id: followUpRun.runId,
2515
+ label: followUpRun.label,
2516
+ runtime_id: followUpRun.runtimeId
2517
+ };
2518
+ }
2519
+ const { sentinelPath: followUpSentinelPath, eventLogPath } = ensureRunPaths(followUpRunId);
2520
+ spawnParams.sentinel_file = followUpSentinelPath;
2521
+ spawnParams.on_event = eventLogPath;
2522
+ const result = await client.spawn(spawnParams);
2523
+ const runtimeId = value(result.runtime_id);
2524
+ registerExternalRun({
2525
+ runId: followUpRunId,
2526
+ label: followUpLabel,
2527
+ supervisorId,
2528
+ sentinelPath: followUpSentinelPath,
2529
+ eventLogPath,
2530
+ runtimeId,
2531
+ sessionId: value(result.session_id) ?? sessionId,
2532
+ agent,
2533
+ model,
2534
+ backend,
2535
+ effectiveAgent: agent,
2536
+ effectiveModel: model,
2537
+ effectiveBackend: backend,
2538
+ rosterFile: runInfo?.rosterFile,
2539
+ rosterEntry: runInfo?.rosterEntry,
2540
+ thinking,
2541
+ dir,
2542
+ agentProfile,
2543
+ autoApprove
2544
+ });
2112
2545
  return {
2113
- run_id: (await client.spawn(spawnParams)).runtime_id ?? followUpRunId,
2114
- label: followUpLabel
2546
+ run_id: followUpRunId,
2547
+ label: followUpLabel,
2548
+ runtime_id: runtimeId
2115
2549
  };
2116
2550
  } finally {
2117
2551
  if (!isSingleton) client.close();
2118
2552
  }
2119
2553
  }
2120
2554
  const sup = await Supervisor.get();
2121
- const runInfo = findRunByLabel(sup, args.runId);
2555
+ const runInfo = findLocalRunByReference(sup, args.runId);
2122
2556
  if (!runInfo) throw new Error(`run not found: ${args.runId}`);
2123
- const sessionId = (await parseSentinel(runInfo.sentinelPath))?.SESSION ?? runInfo.sessionId;
2557
+ const sentinel = await parseSentinel(runInfo.sentinelPath);
2558
+ rejectSessionConflict(sentinel, null);
2559
+ const sessionId = sentinel?.SESSION ?? runInfo.sessionId;
2124
2560
  if (!sessionId) throw new Error("run has no session to resume");
2125
2561
  const client = sup.getClient();
2126
2562
  let liveStatus = null;
2127
2563
  if (runInfo.runtimeId) try {
2128
2564
  liveStatus = await client.status(runInfo.runtimeId);
2129
2565
  } catch {}
2130
- const agent = requireResumableAgent(liveStatus?.agent ?? runInfo.agent);
2131
- const backend = liveStatus?.backend ?? runInfo.backend;
2132
- const model = liveStatus?.model ?? runInfo.model;
2566
+ rejectSessionConflict(sentinel, liveStatus);
2567
+ const agent = resolvedIdentity(liveStatus, "effective_agent", "agent", runInfo.effectiveAgent, runInfo.agent);
2568
+ const model = resolvedIdentity(liveStatus, "effective_model", "model", runInfo.effectiveModel, runInfo.model);
2569
+ const backend = resolvedIdentity(liveStatus, "effective_backend", "backend", runInfo.effectiveBackend, runInfo.backend);
2570
+ if (liveStatus) retainLiveIdentity(runInfo, liveStatus);
2571
+ const thinking = liveStatus?.thinking ?? runInfo.thinking;
2133
2572
  const dir = liveStatus?.dir ?? runInfo.dir;
2134
2573
  const agentProfile = liveStatus?.agent_profile ?? runInfo.agentProfile;
2135
2574
  const autoApprove = resolveAutoApprove(liveStatus, runInfo);
2136
2575
  const followUpLabel = args.label ?? `${runInfo.label}-followup`;
2137
- const followUpRun = await sup.spawn({
2138
- agent,
2139
- backend,
2140
- model,
2141
- dir,
2142
- agent_profile: agentProfile,
2576
+ const followUpParams = {
2577
+ ...agent ? { agent } : {},
2578
+ ...backend ? { backend } : {},
2579
+ ...model ? { model } : {},
2580
+ ...thinking ? { thinking } : {},
2581
+ ...dir ? { dir } : {},
2582
+ ...agentProfile ? { agent_profile: agentProfile } : {},
2143
2583
  prompt: args.message,
2144
2584
  session_id: sessionId,
2145
2585
  label: followUpLabel,
2146
2586
  ...autoApprove === true ? { auto_approve: true } : {}
2587
+ };
2588
+ const followUpRun = await sup.spawn(followUpParams, void 0, {
2589
+ rosterFile: runInfo.rosterFile,
2590
+ rosterEntry: runInfo.rosterEntry,
2591
+ effectiveAgent: agent,
2592
+ effectiveModel: model,
2593
+ effectiveBackend: backend
2147
2594
  });
2148
2595
  return {
2149
2596
  run_id: followUpRun.runId,
2150
- label: followUpRun.label
2597
+ label: followUpRun.label,
2598
+ runtime_id: followUpRun.runtimeId
2151
2599
  };
2152
2600
  }
2153
2601
  function createFollowUpTool(getSupervisorClient) {
@@ -2180,9 +2628,24 @@ async function shutdownTool(args) {
2180
2628
  const client = await dial(supervisorId);
2181
2629
  try {
2182
2630
  await client.shutdown(args.force ? "force" : "graceful");
2183
- } catch {} finally {
2631
+ } finally {
2184
2632
  client.close();
2185
2633
  }
2634
+ for (const info of clearExternalRuns(supervisorId)) {
2635
+ try {
2636
+ await fs.promises.unlink(info.sentinelPath);
2637
+ cleanedUp.push(info.sentinelPath);
2638
+ } catch {}
2639
+ try {
2640
+ await fs.promises.unlink(info.eventLogPath);
2641
+ cleanedUp.push(info.eventLogPath);
2642
+ } catch {}
2643
+ const metadataPath = externalRunMetadataPath(info.runId);
2644
+ try {
2645
+ await fs.promises.unlink(metadataPath);
2646
+ cleanedUp.push(metadataPath);
2647
+ } catch {}
2648
+ }
2186
2649
  }
2187
2650
  return {
2188
2651
  ok: true,
@@ -2211,4 +2674,4 @@ async function shutdownTool(args) {
2211
2674
  };
2212
2675
  }
2213
2676
  //#endregion
2214
- export { DEFAULT_RUN_REDUCER_LIMITS, RunReducer, Supervisor, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, findAvenorBinary, followUpTool, getInstallDir, getVersion, inspectTool, installerBinaryPath, normalizeRunEvent, observeRun, reduceRunSnapshot, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool };
2677
+ export { DEFAULT_RUN_REDUCER_LIMITS, RunReducer, Supervisor, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, findAvenorBinary, followUpTool, getInstallDir, getVersion, inspectTool, installerBinaryPath, normalizeRunEvent, observeRun, reduceRunSnapshot, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-core",
3
- "version": "0.8.3",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,5 +34,9 @@
34
34
  "tsdown": "^0.22.0",
35
35
  "typescript": "^6.0.3",
36
36
  "unrun": "^0.3.0"
37
+ },
38
+ "dependencies": {
39
+ "@umpire/core": "1.0.1",
40
+ "@umpire/json": "1.0.0"
37
41
  }
38
42
  }