@dougbots/avenor-core 0.9.0 → 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
@@ -36,6 +36,8 @@ interface SpawnParams {
36
36
  thinking?: ThinkingLevel;
37
37
  agent_profile?: string;
38
38
  backend?: string;
39
+ roster_file?: string;
40
+ roster_entry?: string;
39
41
  server_url?: string;
40
42
  session_id?: string;
41
43
  sentinel_file?: string;
@@ -43,6 +45,24 @@ interface SpawnParams {
43
45
  auto_approve?: boolean;
44
46
  [key: string]: unknown;
45
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
+ }
46
66
  declare class Client {
47
67
  private socket;
48
68
  private rl;
@@ -69,7 +89,7 @@ declare class Client {
69
89
  status(runtimeId?: string): Promise<Record<string, unknown>>;
70
90
  result(runtimeId?: string): Promise<Record<string, unknown>>;
71
91
  list(): Promise<Array<Record<string, unknown>>>;
72
- spawn(params: SpawnParams): Promise<Record<string, unknown>>;
92
+ spawn(params: SpawnParams): Promise<SpawnResult>;
73
93
  shutdown(mode: string): Promise<void>;
74
94
  answerPermission(runtimeId: string, requestId: string, optionId: string, message?: string): Promise<void>;
75
95
  prompt(runtimeId: string, text: string): Promise<void>;
@@ -245,6 +265,11 @@ interface RunInfo {
245
265
  agentProfile?: string;
246
266
  backend?: string;
247
267
  model?: string;
268
+ effectiveAgent?: string;
269
+ effectiveModel?: string;
270
+ effectiveBackend?: string;
271
+ rosterFile?: string;
272
+ rosterEntry?: string;
248
273
  thinking?: ThinkingLevel;
249
274
  dir?: string;
250
275
  brokerUrl?: string;
@@ -255,6 +280,14 @@ interface SupervisorOptions {
255
280
  binaryPath?: string;
256
281
  callTimeoutMs?: number;
257
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
+ }
258
291
  declare function findAvenorBinary(): string;
259
292
  declare class Supervisor {
260
293
  private static instance;
@@ -267,6 +300,7 @@ declare class Supervisor {
267
300
  private crashed;
268
301
  private crashCode;
269
302
  private runs;
303
+ private aliases;
270
304
  private binaryPath;
271
305
  private callTimeoutMs;
272
306
  private constructor();
@@ -277,7 +311,7 @@ declare class Supervisor {
277
311
  static isCurrentInstance(supervisorId: string): boolean;
278
312
  static currentInstance(): Supervisor | null;
279
313
  getClient(): Client;
280
- 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>;
281
315
  close(options?: {
282
316
  skipShutdown?: boolean;
283
317
  }): Promise<void>;
@@ -287,7 +321,7 @@ declare class Supervisor {
287
321
  declare function socketsRoot(): string;
288
322
  //#endregion
289
323
  //#region src/tools/spawn.d.ts
290
- declare function spawnTool(args: {
324
+ interface SpawnToolArgs {
291
325
  agent?: string;
292
326
  prompt?: string;
293
327
  promptFile?: string;
@@ -298,18 +332,33 @@ declare function spawnTool(args: {
298
332
  thinking?: ThinkingLevel;
299
333
  agentProfile?: string;
300
334
  backend?: string;
335
+ rosterFile?: string;
336
+ rosterEntry?: string;
301
337
  serverUrl?: string;
302
338
  sessionId?: string;
303
339
  supervisorId?: string;
304
340
  parent_run_id?: string;
305
- }): Promise<{
341
+ }
342
+ interface SpawnToolResult {
306
343
  run_id: string;
307
344
  label: string;
308
345
  supervisor_id: string;
309
346
  runtime_id?: string;
310
347
  broker_url?: string;
311
348
  parent_token?: string;
312
- }>;
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;
313
362
  //#endregion
314
363
  //#region src/tools/status.d.ts
315
364
  type StatusView = 'lifecycle' | 'full';
@@ -337,9 +386,15 @@ interface StatusResult {
337
386
  };
338
387
  session_id?: string;
339
388
  stop_reason?: string;
389
+ roster_file?: string;
390
+ roster_entry?: string;
340
391
  backend?: string;
341
392
  agent?: string;
342
393
  model?: string;
394
+ agent_profile?: string;
395
+ effective_backend?: string;
396
+ effective_agent?: string;
397
+ effective_model?: string;
343
398
  dir?: string;
344
399
  parent_id?: string;
345
400
  children?: string[];
@@ -436,6 +491,7 @@ interface FollowUpToolArgs {
436
491
  interface FollowUpToolResult {
437
492
  run_id: string;
438
493
  label: string;
494
+ runtime_id?: string;
439
495
  }
440
496
  declare const followUpTool: (args: FollowUpToolArgs) => Promise<FollowUpToolResult>;
441
497
  //#endregion
@@ -448,4 +504,4 @@ declare function shutdownTool(args: {
448
504
  cleaned_up: string[];
449
505
  }>;
450
506
  //#endregion
451
- 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, type ThinkingLevel, 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;
@@ -1074,6 +1076,95 @@ function observeRun(client, options = {}) {
1074
1076
  };
1075
1077
  }
1076
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
1077
1168
  //#region src/paths.ts
1078
1169
  function avenorHome() {
1079
1170
  return process.env.AVENOR_HOME || path.join(os.homedir(), ".avenor");
@@ -1098,6 +1189,37 @@ function ensureRunPaths(runId) {
1098
1189
  }
1099
1190
  //#endregion
1100
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
+ }
1101
1223
  function findAvenorBinary() {
1102
1224
  const envBin = process.env.AVENOR_BIN;
1103
1225
  if (envBin) try {
@@ -1154,6 +1276,7 @@ var Supervisor = class Supervisor {
1154
1276
  crashed = false;
1155
1277
  crashCode = null;
1156
1278
  runs = /* @__PURE__ */ new Map();
1279
+ aliases = /* @__PURE__ */ new Map();
1157
1280
  binaryPath;
1158
1281
  callTimeoutMs;
1159
1282
  constructor(binaryPath, socketPath, opts) {
@@ -1292,7 +1415,14 @@ var Supervisor = class Supervisor {
1292
1415
  }
1293
1416
  return this.client;
1294
1417
  }
1295
- 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
+ });
1296
1426
  const client = this.getClient();
1297
1427
  const { sentinelPath, eventLogPath } = ensureRunPaths(runId);
1298
1428
  const spawnParams = {
@@ -1301,26 +1431,60 @@ var Supervisor = class Supervisor {
1301
1431
  sentinel_file: sentinelPath,
1302
1432
  on_event: eventLogPath
1303
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
+ }
1304
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);
1305
1464
  const runInfo = {
1306
1465
  runId,
1307
1466
  label: spawnParams.label ?? runId,
1308
1467
  sentinelPath: result.sentinel_file ?? sentinelPath,
1309
1468
  eventLogPath: result.on_event ?? eventLogPath,
1310
- runtimeId: result.runtime_id,
1311
- sessionId: result.session_id,
1312
- agent: spawnParams.agent ?? params.agent,
1313
- agentProfile: spawnParams.agent_profile,
1314
- backend: spawnParams.backend,
1315
- 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,
1316
1480
  thinking: spawnParams.thinking,
1317
1481
  dir: spawnParams.dir,
1318
1482
  brokerUrl: result.broker_url,
1319
1483
  parentToken: result.parent_token,
1320
1484
  autoApprove: spawnParams.auto_approve
1321
1485
  };
1322
- this.runs.set(runInfo.label, runInfo);
1323
1486
  this.runs.set(runId, runInfo);
1487
+ this.aliases.set(runInfo.label, runInfo);
1324
1488
  return runInfo;
1325
1489
  }
1326
1490
  async close(options = {}) {
@@ -1343,6 +1507,7 @@ var Supervisor = class Supervisor {
1343
1507
  this.crashed = false;
1344
1508
  this.crashCode = null;
1345
1509
  this.runs.clear();
1510
+ this.aliases.clear();
1346
1511
  Supervisor.instance = null;
1347
1512
  }
1348
1513
  };
@@ -1388,8 +1553,125 @@ async function getSupervisorClient(supervisorId) {
1388
1553
  };
1389
1554
  }
1390
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
1391
1666
  //#region src/tools/spawn.ts
1392
- 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
+ });
1393
1675
  const runId = crypto.randomUUID();
1394
1676
  const label = args.label ?? runId;
1395
1677
  const baseParams = {
@@ -1405,10 +1687,23 @@ async function spawnTool(args) {
1405
1687
  session_id: args.sessionId,
1406
1688
  parent_run_id: args.parent_run_id
1407
1689
  };
1690
+ if (args.rosterFile) baseParams.roster_file = args.rosterFile;
1691
+ if (args.rosterEntry) baseParams.roster_entry = args.rosterEntry;
1408
1692
  if (args.agent) baseParams.agent = args.agent;
1409
1693
  if (args.thinking !== void 0) baseParams.thinking = args.thinking;
1410
1694
  if (args.supervisorId) {
1411
- 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
+ }
1412
1707
  const { sentinelPath, eventLogPath } = ensureRunPaths(runId);
1413
1708
  try {
1414
1709
  const result = await client.spawn({
@@ -1416,13 +1711,37 @@ async function spawnTool(args) {
1416
1711
  sentinel_file: sentinelPath,
1417
1712
  on_event: eventLogPath
1418
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
+ });
1419
1738
  return {
1420
- run_id: result.runtime_id ?? runId,
1739
+ run_id: runId,
1421
1740
  label,
1422
1741
  supervisor_id: supervisorId,
1423
- runtime_id: result.runtime_id,
1424
- broker_url: result.broker_url,
1425
- parent_token: result.parent_token
1742
+ runtime_id: runtimeId,
1743
+ broker_url: stringValue(result.broker_url),
1744
+ parent_token: stringValue(result.parent_token)
1426
1745
  };
1427
1746
  } finally {
1428
1747
  if (!isSingleton) client.close();
@@ -1439,14 +1758,25 @@ async function spawnTool(args) {
1439
1758
  parent_token: runInfo.parentToken
1440
1759
  };
1441
1760
  }
1761
+ function createSpawnTool(getSupervisorClient) {
1762
+ return (args) => executeSpawnTool(args, getSupervisorClient);
1763
+ }
1764
+ const spawnTool = createSpawnTool(getSupervisorClient);
1442
1765
  //#endregion
1443
- //#region src/tools/status.ts
1444
- function findRunByLabel$3(sup, runId) {
1445
- const runs = sup.runs;
1446
- const byKey = runs.get(runId);
1447
- if (byKey) return byKey;
1448
- 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;
1449
1777
  }
1778
+ //#endregion
1779
+ //#region src/tools/status.ts
1450
1780
  async function parseSentinel$1(filePath) {
1451
1781
  try {
1452
1782
  const content = (await fs.promises.readFile(filePath, "utf-8")).trim();
@@ -1534,6 +1864,15 @@ function extractPendingPermission(liveStatus) {
1534
1864
  return normalizePermission(asRecord(pendingPermission));
1535
1865
  }
1536
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);
1537
1876
  return {
1538
1877
  run_id: fallback.prefer_fallback_run_id ? fallback.run_id : stringField(source, "run_id") ?? fallback.run_id,
1539
1878
  label: stringField(source, "label") ?? fallback.label,
@@ -1544,9 +1883,15 @@ function buildBaseStatus(source, fallback, translatedStatus) {
1544
1883
  pending_permission: extractPendingPermission(source),
1545
1884
  session_id: stringField(source, "session_id") ?? fallback.session_id,
1546
1885
  stop_reason: stringField(source, "stop_reason") ?? fallback.stop_reason,
1547
- backend: stringField(source, "backend"),
1548
- agent: stringField(source, "agent"),
1549
- 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,
1550
1895
  dir: stringField(source, "dir"),
1551
1896
  parent_id: stringField(source, "parent_id", "parentId"),
1552
1897
  children: stringArrayField(source, "children"),
@@ -1573,6 +1918,7 @@ function shapeStatusResult(result, view = "full") {
1573
1918
  };
1574
1919
  }
1575
1920
  async function buildRunStatus(runInfo, liveStatus) {
1921
+ if (liveStatus) retainLiveIdentity(runInfo, liveStatus);
1576
1922
  let sentinel = null;
1577
1923
  if (await sentinelExists(runInfo.sentinelPath)) sentinel = await parseSentinel$1(runInfo.sentinelPath);
1578
1924
  const translated = translateStatus(rawStatusPhase(liveStatus), sentinel);
@@ -1582,7 +1928,18 @@ async function buildRunStatus(runInfo, liveStatus) {
1582
1928
  prefer_fallback_run_id: true,
1583
1929
  runtime_id: runInfo.runtimeId,
1584
1930
  session_id: sentinel?.SESSION ?? liveStatus?.session_id ?? runInfo.sessionId,
1585
- 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
+ }
1586
1943
  }, translated);
1587
1944
  }
1588
1945
  async function queryLiveStatus(client, runtimeId) {
@@ -1595,11 +1952,11 @@ async function queryLiveStatus(client, runtimeId) {
1595
1952
  }
1596
1953
  async function executeStatusTool(args, getSupervisorClient) {
1597
1954
  if (args.supervisorId) {
1598
- const { client, isSingleton, sup } = await getSupervisorClient(args.supervisorId);
1955
+ const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId);
1599
1956
  try {
1600
1957
  if (args.runId) {
1601
1958
  validateRunId(args.runId);
1602
- const runInfo = isSingleton && sup ? findRunByLabel$3(sup, args.runId) : void 0;
1959
+ const runInfo = isSingleton && sup ? findLocalRunByReference(sup, args.runId) : findExternalRun(supervisorId, args.runId);
1603
1960
  if (runInfo) return shapeStatusResult(await buildRunStatus(runInfo, await queryLiveStatus(client, runInfo.runtimeId)), args.view);
1604
1961
  let liveStatus = await queryLiveStatus(client, args.runId);
1605
1962
  if (!liveStatus) try {
@@ -1617,17 +1974,30 @@ async function executeStatusTool(args, getSupervisorClient) {
1617
1974
  return shapeStatusResult(buildBaseStatus(liveStatus ?? {}, {
1618
1975
  run_id: args.runId,
1619
1976
  label: args.runId,
1977
+ prefer_fallback_run_id: true,
1620
1978
  runtime_id: stringField(liveStatus ?? {}, "runtime_id", "id"),
1621
1979
  session_id: sentinel?.SESSION ?? liveStatus?.session_id,
1622
- stop_reason: sentinel?._status
1980
+ stop_reason: sentinel?.STOP_REASON
1623
1981
  }, translateStatus(rawStatusPhase(liveStatus), sentinel)), args.view);
1624
1982
  }
1625
- return (await client.list()).map((entry) => shapeStatusResult(buildBaseStatus(entry, {
1626
- run_id: String(entry.run_id ?? entry.runtime_id ?? entry.id ?? ""),
1627
- label: String(entry.label ?? entry.runtime_id ?? entry.id ?? ""),
1628
- runtime_id: String(entry.runtime_id ?? entry.id ?? ""),
1629
- session_id: entry.session_id
1630
- }, 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;
1631
2001
  } finally {
1632
2002
  if (!isSingleton) client.close();
1633
2003
  }
@@ -1635,7 +2005,7 @@ async function executeStatusTool(args, getSupervisorClient) {
1635
2005
  const sup = await Supervisor.get();
1636
2006
  const client = sup.getClient();
1637
2007
  if (args.runId) {
1638
- const runInfo = findRunByLabel$3(sup, args.runId);
2008
+ const runInfo = findLocalRunByReference(sup, args.runId);
1639
2009
  if (runInfo) return shapeStatusResult(await buildRunStatus(runInfo, await queryLiveStatus(client, runInfo.runtimeId)), args.view);
1640
2010
  validateRunId(args.runId);
1641
2011
  const liveStatus = await queryLiveStatus(client, args.runId);
@@ -1647,7 +2017,7 @@ async function executeStatusTool(args, getSupervisorClient) {
1647
2017
  label: args.runId,
1648
2018
  runtime_id: stringField(liveStatus ?? {}, "runtime_id", "id"),
1649
2019
  session_id: sentinel?.SESSION ?? liveStatus?.session_id,
1650
- stop_reason: sentinel?._status
2020
+ stop_reason: sentinel?.STOP_REASON
1651
2021
  }, translateStatus(rawStatusPhase(liveStatus), sentinel)), args.view);
1652
2022
  }
1653
2023
  const list = await client.list();
@@ -1655,9 +2025,9 @@ async function executeStatusTool(args, getSupervisorClient) {
1655
2025
  for (const entry of list) {
1656
2026
  const entryId = String(entry.runtime_id ?? entry.id ?? "");
1657
2027
  const entryLabel = String(entry.label ?? entryId);
1658
- const runInfo = findRunByLabel$3(sup, entryId) ?? findRunByLabel$3(sup, entryLabel);
2028
+ const runInfo = findLocalRunByRuntimeId(sup, entryId) ?? findLocalRunByReference(sup, entryId) ?? findLocalRunByReference(sup, entryLabel);
1659
2029
  const liveStatus = runInfo ? await queryLiveStatus(client, runInfo.runtimeId) : null;
1660
- if (runInfo && liveStatus) results.push(shapeStatusResult(await buildRunStatus(runInfo, liveStatus), args.view));
2030
+ if (runInfo) results.push(shapeStatusResult(await buildRunStatus(runInfo, liveStatus), args.view));
1661
2031
  else results.push(shapeStatusResult(buildBaseStatus(entry, {
1662
2032
  run_id: String(entry.run_id ?? entryId),
1663
2033
  label: entryLabel,
@@ -1677,12 +2047,6 @@ const MAX_EVENT_LIMIT = 1e3;
1677
2047
  const MAX_STRING_CHARS = 4e3;
1678
2048
  const MAX_ARRAY_ITEMS = 32;
1679
2049
  const MAX_OBJECT_KEYS = 64;
1680
- function findRunByLabel$2(sup, runId) {
1681
- const runs = sup.runs;
1682
- const byKey = runs.get(runId);
1683
- if (byKey) return byKey;
1684
- for (const info of runs.values()) if (info.label === runId) return info;
1685
- }
1686
2050
  function normalizeLimit(limit) {
1687
2051
  return Math.max(1, Math.min(MAX_EVENT_LIMIT, Math.trunc(limit ?? 50)));
1688
2052
  }
@@ -1752,8 +2116,8 @@ async function readNdjsonEvents(filePath, args) {
1752
2116
  }
1753
2117
  async function resolveRun(args, getSupervisorClient) {
1754
2118
  if (args.supervisorId) {
1755
- const { client, isSingleton, sup } = await getSupervisorClient(args.supervisorId);
1756
- 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);
1757
2121
  if (runInfo) return {
1758
2122
  client,
1759
2123
  isSingleton,
@@ -1796,7 +2160,7 @@ async function resolveRun(args, getSupervisorClient) {
1796
2160
  };
1797
2161
  }
1798
2162
  const sup = await Supervisor.get();
1799
- const runInfo = findRunByLabel$2(sup, args.runId);
2163
+ const runInfo = findLocalRunByReference(sup, args.runId);
1800
2164
  if (runInfo) return {
1801
2165
  client: sup.getClient(),
1802
2166
  isSingleton: true,
@@ -1822,6 +2186,7 @@ async function resolveRun(args, getSupervisorClient) {
1822
2186
  };
1823
2187
  }
1824
2188
  async function executeEventsTool(args, getSupervisorClient) {
2189
+ validateRunId(args.runId);
1825
2190
  const limit = normalizeLimit(args.limit);
1826
2191
  const resolved = await resolveRun(args, getSupervisorClient);
1827
2192
  try {
@@ -2030,12 +2395,6 @@ async function resultTool(args) {
2030
2395
  }
2031
2396
  //#endregion
2032
2397
  //#region src/tools/answer-permission.ts
2033
- function findRunByLabel$1(sup, runId) {
2034
- const runs = sup.runs;
2035
- const byKey = runs.get(runId);
2036
- if (byKey) return byKey;
2037
- for (const info of runs.values()) if (info.label === runId) return info;
2038
- }
2039
2398
  function pendingPermissionRequestID(liveStatus) {
2040
2399
  const status = asRecord(liveStatus);
2041
2400
  if (!status) return void 0;
@@ -2055,7 +2414,7 @@ async function answerPermissionTool(args) {
2055
2414
  }
2056
2415
  try {
2057
2416
  validateRunId(args.runId);
2058
- 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;
2059
2418
  if (!requestId) {
2060
2419
  requestId = pendingPermissionRequestID(await client.status(runtimeId));
2061
2420
  if (!requestId) throw new Error("no pending permission request for this run");
@@ -2068,20 +2427,18 @@ async function answerPermissionTool(args) {
2068
2427
  }
2069
2428
  //#endregion
2070
2429
  //#region src/tools/follow-up.ts
2071
- function findRunByLabel(sup, runId) {
2072
- const runs = sup.runs;
2073
- const byKey = runs.get(runId);
2074
- if (byKey) return byKey;
2075
- for (const info of runs.values()) if (info.label === runId) return info;
2076
- }
2077
2430
  function resolveAutoApprove(liveStatus, runInfo) {
2078
2431
  const liveAutoApprove = liveStatus?.auto_approve;
2079
2432
  if (typeof liveAutoApprove === "boolean") return liveAutoApprove;
2080
2433
  return typeof runInfo?.autoApprove === "boolean" ? runInfo.autoApprove : void 0;
2081
2434
  }
2082
- function requireResumableAgent(agent) {
2083
- if (agent === void 0) throw new Error("run has no agent to resume");
2084
- 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;
2085
2442
  }
2086
2443
  async function parseSentinel(filePath) {
2087
2444
  try {
@@ -2101,85 +2458,144 @@ async function parseSentinel(filePath) {
2101
2458
  return null;
2102
2459
  }
2103
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
+ }
2104
2466
  async function executeFollowUpTool(args, getSupervisorClient) {
2105
2467
  if (args.supervisorId) {
2106
- const { client, isSingleton, sup } = await getSupervisorClient(args.supervisorId);
2468
+ const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId);
2107
2469
  try {
2108
2470
  validateRunId(args.runId);
2109
2471
  let runInfo;
2110
- if (isSingleton && sup) runInfo = findRunByLabel(sup, args.runId);
2472
+ if (isSingleton && sup) runInfo = findLocalRunByReference(sup, args.runId);
2473
+ else runInfo = findExternalRun(supervisorId, args.runId);
2111
2474
  const fallbackSessionId = runInfo?.sessionId;
2112
2475
  let liveStatus = null;
2113
2476
  try {
2114
2477
  liveStatus = await client.status(runInfo?.runtimeId ?? args.runId);
2115
2478
  } catch {}
2116
- 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;
2117
2482
  if (!sessionId) throw new Error("run has no session to resume");
2118
- const agent = requireResumableAgent(liveStatus?.agent ?? runInfo?.agent);
2119
- const backend = liveStatus?.backend ?? runInfo?.backend;
2120
- 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);
2121
2487
  const thinking = liveStatus?.thinking ?? runInfo?.thinking;
2122
2488
  const dir = liveStatus?.dir ?? runInfo?.dir;
2123
2489
  const agentProfile = liveStatus?.agent_profile ?? runInfo?.agentProfile;
2124
2490
  const autoApprove = resolveAutoApprove(liveStatus, runInfo);
2125
2491
  const followUpRunId = crypto.randomUUID();
2126
2492
  const followUpLabel = args.label ?? `${args.runId}-followup`;
2127
- const { sentinelPath: followUpSentinelPath, eventLogPath } = ensureRunPaths(followUpRunId);
2128
2493
  const spawnParams = {
2129
- agent,
2130
2494
  prompt: args.message,
2131
2495
  label: followUpLabel,
2132
- session_id: sessionId,
2133
- sentinel_file: followUpSentinelPath,
2134
- on_event: eventLogPath
2496
+ session_id: sessionId
2135
2497
  };
2498
+ if (agent) spawnParams.agent = agent;
2136
2499
  if (backend) spawnParams.backend = backend;
2137
2500
  if (model) spawnParams.model = model;
2138
2501
  if (thinking) spawnParams.thinking = thinking;
2139
2502
  if (dir) spawnParams.dir = dir;
2140
2503
  if (agentProfile) spawnParams.agent_profile = agentProfile;
2141
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
+ });
2142
2545
  return {
2143
- run_id: (await client.spawn(spawnParams)).runtime_id ?? followUpRunId,
2144
- label: followUpLabel
2546
+ run_id: followUpRunId,
2547
+ label: followUpLabel,
2548
+ runtime_id: runtimeId
2145
2549
  };
2146
2550
  } finally {
2147
2551
  if (!isSingleton) client.close();
2148
2552
  }
2149
2553
  }
2150
2554
  const sup = await Supervisor.get();
2151
- const runInfo = findRunByLabel(sup, args.runId);
2555
+ const runInfo = findLocalRunByReference(sup, args.runId);
2152
2556
  if (!runInfo) throw new Error(`run not found: ${args.runId}`);
2153
- 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;
2154
2560
  if (!sessionId) throw new Error("run has no session to resume");
2155
2561
  const client = sup.getClient();
2156
2562
  let liveStatus = null;
2157
2563
  if (runInfo.runtimeId) try {
2158
2564
  liveStatus = await client.status(runInfo.runtimeId);
2159
2565
  } catch {}
2160
- const agent = requireResumableAgent(liveStatus?.agent ?? runInfo.agent);
2161
- const backend = liveStatus?.backend ?? runInfo.backend;
2162
- 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);
2163
2571
  const thinking = liveStatus?.thinking ?? runInfo.thinking;
2164
2572
  const dir = liveStatus?.dir ?? runInfo.dir;
2165
2573
  const agentProfile = liveStatus?.agent_profile ?? runInfo.agentProfile;
2166
2574
  const autoApprove = resolveAutoApprove(liveStatus, runInfo);
2167
2575
  const followUpLabel = args.label ?? `${runInfo.label}-followup`;
2168
- const followUpRun = await sup.spawn({
2169
- agent,
2170
- backend,
2171
- model,
2172
- thinking,
2173
- dir,
2174
- 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 } : {},
2175
2583
  prompt: args.message,
2176
2584
  session_id: sessionId,
2177
2585
  label: followUpLabel,
2178
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
2179
2594
  });
2180
2595
  return {
2181
2596
  run_id: followUpRun.runId,
2182
- label: followUpRun.label
2597
+ label: followUpRun.label,
2598
+ runtime_id: followUpRun.runtimeId
2183
2599
  };
2184
2600
  }
2185
2601
  function createFollowUpTool(getSupervisorClient) {
@@ -2212,9 +2628,24 @@ async function shutdownTool(args) {
2212
2628
  const client = await dial(supervisorId);
2213
2629
  try {
2214
2630
  await client.shutdown(args.force ? "force" : "graceful");
2215
- } catch {} finally {
2631
+ } finally {
2216
2632
  client.close();
2217
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
+ }
2218
2649
  }
2219
2650
  return {
2220
2651
  ok: true,
@@ -2243,4 +2674,4 @@ async function shutdownTool(args) {
2243
2674
  };
2244
2675
  }
2245
2676
  //#endregion
2246
- 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.9.0",
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
  }