@llblab/pi-actors 0.22.2 → 0.22.4
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/AGENTS.md +1 -0
- package/BACKLOG.md +456 -81
- package/CHANGELOG.md +25 -25
- package/dist/index.js +1 -1
- package/dist/lib/actor-rooms.d.ts +8 -2
- package/dist/lib/actor-rooms.js +58 -9
- package/dist/lib/async-runs.js +32 -10
- package/dist/lib/observability.js +28 -10
- package/dist/lib/recipe-discovery.js +121 -16
- package/dist/lib/recipe-references.d.ts +1 -0
- package/dist/lib/recipe-references.js +40 -1
- package/dist/lib/registry.js +5 -1
- package/dist/lib/tools.js +25 -8
- package/index.ts +1 -1
- package/lib/actor-rooms.ts +79 -11
- package/lib/async-runs.ts +124 -88
- package/lib/observability.ts +85 -24
- package/lib/recipe-discovery.ts +186 -41
- package/lib/recipe-references.ts +88 -14
- package/lib/registry.ts +7 -1
- package/lib/tools.ts +42 -15
- package/package.json +1 -1
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
package/lib/async-runs.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Owns detached run state, observation, log tailing, listing, and cancellation safety
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
9
|
import {
|
|
10
10
|
closeSync,
|
|
11
11
|
constants,
|
|
@@ -133,7 +133,10 @@ const DEFAULT_RECIPE_ROOT = Paths.getRecipeRoot();
|
|
|
133
133
|
|
|
134
134
|
function packageRoot(): string {
|
|
135
135
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
136
|
-
if (
|
|
136
|
+
if (
|
|
137
|
+
basename(moduleDir) === "lib" &&
|
|
138
|
+
basename(dirname(moduleDir)) === "dist"
|
|
139
|
+
) {
|
|
137
140
|
return dirname(dirname(moduleDir));
|
|
138
141
|
}
|
|
139
142
|
return dirname(moduleDir);
|
|
@@ -409,84 +412,88 @@ export function startRun(
|
|
|
409
412
|
try {
|
|
410
413
|
assertNoActiveRunState(stateDir);
|
|
411
414
|
prepareStateDirForStart(stateDir);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
? {
|
|
462
|
-
: {}),
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
415
|
+
const stdout = join(stateDir, "stdout.log");
|
|
416
|
+
const stderr = join(stateDir, "stderr.log");
|
|
417
|
+
const recipeFile = startParams.file
|
|
418
|
+
? resolveRecipeFile(startParams.file)
|
|
419
|
+
: undefined;
|
|
420
|
+
const recipe = startParams.name || getRunIdFromFile(recipeFile);
|
|
421
|
+
const includeActorRecipeContext =
|
|
422
|
+
startParams.actor_context !== false &&
|
|
423
|
+
startParams.actor_context !== "off";
|
|
424
|
+
const recipeContextRecords =
|
|
425
|
+
recipeFile && includeActorRecipeContext
|
|
426
|
+
? RecipeReferences.buildRecipeContextRecords(recipeFile)
|
|
427
|
+
: undefined;
|
|
428
|
+
if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
|
|
429
|
+
RecipeUsage.recordRecipeLaunch(recipeFile);
|
|
430
|
+
}
|
|
431
|
+
const outFd = openSync(stdout, "a");
|
|
432
|
+
const errFd = openSync(stderr, "a");
|
|
433
|
+
const argv = asyncRunnerArgv(stateDir);
|
|
434
|
+
const values = {
|
|
435
|
+
...(startParams.values || {}),
|
|
436
|
+
actor_address: `run:${run}`,
|
|
437
|
+
communication_file: join(stateDir, "communication.json"),
|
|
438
|
+
default_room: `room:${run}`,
|
|
439
|
+
run_id: run,
|
|
440
|
+
state_dir: stateDir,
|
|
441
|
+
};
|
|
442
|
+
const outputValues = {
|
|
443
|
+
...(startParams.defaults || {}),
|
|
444
|
+
...values,
|
|
445
|
+
};
|
|
446
|
+
const artifacts = resolveArtifactPaths(startParams.artifacts, outputValues);
|
|
447
|
+
const meta: AsyncRunMeta = {
|
|
448
|
+
argv: [process.execPath, ...argv],
|
|
449
|
+
createdAt: new Date().toISOString(),
|
|
450
|
+
cwd,
|
|
451
|
+
...(startParams.launch_source
|
|
452
|
+
? { launch_source: startParams.launch_source }
|
|
453
|
+
: {}),
|
|
454
|
+
...(startParams.ownerId ? { ownerId: startParams.ownerId } : {}),
|
|
455
|
+
pid: 0,
|
|
456
|
+
...(recipe ? { recipe } : {}),
|
|
457
|
+
...(recipeFile ? { recipe_file: recipeFile } : {}),
|
|
458
|
+
run,
|
|
459
|
+
state_dir: stateDir,
|
|
460
|
+
status: "running",
|
|
461
|
+
...(startParams.tool ? { tool: startParams.tool } : {}),
|
|
462
|
+
template: resolved.template,
|
|
463
|
+
values,
|
|
464
|
+
...(artifacts ? { artifacts } : {}),
|
|
465
|
+
...(startParams.control ? { control: startParams.control } : {}),
|
|
466
|
+
...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
|
|
467
|
+
...(recipeContextRecords && recipeContextRecords.length > 0
|
|
468
|
+
? { recipe_context_records: recipeContextRecords }
|
|
469
|
+
: {}),
|
|
470
|
+
...(startParams.retire_when === "children_terminal"
|
|
471
|
+
? { retire_when: "children_terminal" as const }
|
|
472
|
+
: {}),
|
|
473
|
+
};
|
|
474
|
+
writeJsonAtomic(join(stateDir, "run.json"), meta);
|
|
475
|
+
const child = spawn(process.execPath, argv, {
|
|
476
|
+
cwd,
|
|
477
|
+
detached: true,
|
|
478
|
+
stdio: ["ignore", outFd, errFd],
|
|
479
|
+
});
|
|
480
|
+
closeSync(outFd);
|
|
481
|
+
closeSync(errFd);
|
|
482
|
+
meta.pid = child.pid ?? 0;
|
|
483
|
+
writeJsonAtomic(join(stateDir, "run.json"), meta);
|
|
484
|
+
writeJsonAtomic(join(stateDir, "progress.json"), {
|
|
485
|
+
completed: 0,
|
|
486
|
+
failures: [],
|
|
487
|
+
phase: "starting",
|
|
488
|
+
updatedAt: new Date().toISOString(),
|
|
489
|
+
});
|
|
490
|
+
writeFileSync(
|
|
491
|
+
join(stateDir, "events.jsonl"),
|
|
492
|
+
`${JSON.stringify({ event: "run.start", run, pid: meta.pid, ts: new Date().toISOString() })}\n`,
|
|
493
|
+
{ flag: "a" },
|
|
494
|
+
);
|
|
495
|
+
child.unref();
|
|
496
|
+
return meta;
|
|
490
497
|
} finally {
|
|
491
498
|
releaseStartLock();
|
|
492
499
|
}
|
|
@@ -806,7 +813,10 @@ export function claimRunInboxMessage(
|
|
|
806
813
|
...messages[index],
|
|
807
814
|
claimed_at: new Date().toISOString(),
|
|
808
815
|
claimed_by: owner,
|
|
809
|
-
id:
|
|
816
|
+
id:
|
|
817
|
+
typeof messages[index].id === "string"
|
|
818
|
+
? messages[index].id
|
|
819
|
+
: randomUUID(),
|
|
810
820
|
status: "claimed",
|
|
811
821
|
} satisfies RunInboxMessage;
|
|
812
822
|
messages[index] = claimed;
|
|
@@ -928,9 +938,10 @@ function appendRunInboxMessage(stateDir: string, message: string): string {
|
|
|
928
938
|
let record: Record<string, unknown>;
|
|
929
939
|
try {
|
|
930
940
|
const parsed = JSON.parse(message) as unknown;
|
|
931
|
-
record =
|
|
932
|
-
|
|
933
|
-
|
|
941
|
+
record =
|
|
942
|
+
parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
943
|
+
? (parsed as Record<string, unknown>)
|
|
944
|
+
: { body: parsed, type: "run.message" };
|
|
934
945
|
} catch {
|
|
935
946
|
record = { body: message, type: "run.message" };
|
|
936
947
|
}
|
|
@@ -1057,7 +1068,13 @@ export async function sendRunMessage(
|
|
|
1057
1068
|
const payload = message.endsWith("\n") ? message : `${message}\n`;
|
|
1058
1069
|
const payloadBytes = Buffer.byteLength(payload);
|
|
1059
1070
|
const inboxId = appendRunInboxMessage(stateDir, message);
|
|
1060
|
-
const wake = notifyRunMessageWake(
|
|
1071
|
+
const wake = notifyRunMessageWake(
|
|
1072
|
+
stateDir,
|
|
1073
|
+
run,
|
|
1074
|
+
payloadBytes,
|
|
1075
|
+
endpoint,
|
|
1076
|
+
inboxId,
|
|
1077
|
+
);
|
|
1061
1078
|
const runtimePlatform = options.platform ?? process.platform;
|
|
1062
1079
|
if (endpoint.type === "mailbox") {
|
|
1063
1080
|
return {
|
|
@@ -1066,6 +1083,7 @@ export async function sendRunMessage(
|
|
|
1066
1083
|
control: "inbox.jsonl",
|
|
1067
1084
|
control_path: endpoint.path,
|
|
1068
1085
|
control_type: endpoint.type,
|
|
1086
|
+
inbox_id: inboxId,
|
|
1069
1087
|
queued: true,
|
|
1070
1088
|
run,
|
|
1071
1089
|
sent: true,
|
|
@@ -1087,6 +1105,7 @@ export async function sendRunMessage(
|
|
|
1087
1105
|
control: "control.fifo",
|
|
1088
1106
|
control_path: endpoint.path,
|
|
1089
1107
|
control_type: endpoint.type,
|
|
1108
|
+
inbox_id: inboxId,
|
|
1090
1109
|
run,
|
|
1091
1110
|
sent: true,
|
|
1092
1111
|
state_dir: stateDir,
|
|
@@ -1104,13 +1123,28 @@ export async function sendRunMessage(
|
|
|
1104
1123
|
control: endpoint.path,
|
|
1105
1124
|
control_path: endpoint.path,
|
|
1106
1125
|
control_type: endpoint.type,
|
|
1126
|
+
inbox_id: inboxId,
|
|
1107
1127
|
run,
|
|
1108
1128
|
sent: true,
|
|
1109
1129
|
state_dir: stateDir,
|
|
1110
1130
|
};
|
|
1111
1131
|
} catch (error) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1132
|
+
const deliveryError =
|
|
1133
|
+
error instanceof Error ? error.message : String(error);
|
|
1134
|
+
throw Object.assign(
|
|
1135
|
+
new Error(
|
|
1136
|
+
`Run control endpoint is not ready: ${endpoint.path}: ${deliveryError}`,
|
|
1137
|
+
),
|
|
1138
|
+
{
|
|
1139
|
+
control_path: endpoint.path,
|
|
1140
|
+
control_type: endpoint.type,
|
|
1141
|
+
delivery_error: deliveryError,
|
|
1142
|
+
inbox_id: inboxId,
|
|
1143
|
+
queued: true,
|
|
1144
|
+
run,
|
|
1145
|
+
sent: false,
|
|
1146
|
+
state_dir: stateDir,
|
|
1147
|
+
},
|
|
1114
1148
|
);
|
|
1115
1149
|
}
|
|
1116
1150
|
}
|
|
@@ -1150,7 +1184,9 @@ function signalOwnedRunProcess(
|
|
|
1150
1184
|
const result = spawnSync(plan.command, plan.args, { encoding: "utf8" });
|
|
1151
1185
|
if (result.status !== 0) {
|
|
1152
1186
|
throw new Error(
|
|
1153
|
-
result.stderr?.trim() ||
|
|
1187
|
+
result.stderr?.trim() ||
|
|
1188
|
+
result.stdout?.trim() ||
|
|
1189
|
+
`${plan.command} failed`,
|
|
1154
1190
|
);
|
|
1155
1191
|
}
|
|
1156
1192
|
return plan;
|
package/lib/observability.ts
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
basename,
|
|
10
|
+
dirname,
|
|
11
|
+
isAbsolute,
|
|
12
|
+
join,
|
|
13
|
+
relative,
|
|
14
|
+
resolve,
|
|
15
|
+
} from "node:path";
|
|
9
16
|
|
|
10
17
|
import * as AsyncRuns from "./async-runs.ts";
|
|
11
18
|
import * as Paths from "./paths.ts";
|
|
@@ -229,13 +236,25 @@ export function summarizeRuns(
|
|
|
229
236
|
const descendantSubagents = processSubagentsByRun.get(run.run) ?? 0;
|
|
230
237
|
return descendantSubagents > 0 ? { ...run, descendantSubagents } : run;
|
|
231
238
|
});
|
|
232
|
-
const runningRuns = runsWithDescendants.filter(
|
|
239
|
+
const runningRuns = runsWithDescendants.filter(
|
|
240
|
+
(run) => run.status === "running",
|
|
241
|
+
);
|
|
233
242
|
const running = runningRuns.length;
|
|
234
|
-
const done = runsWithDescendants.filter(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
243
|
+
const done = runsWithDescendants.filter(
|
|
244
|
+
(run) => run.status === "done",
|
|
245
|
+
).length;
|
|
246
|
+
const exited = runsWithDescendants.filter(
|
|
247
|
+
(run) => run.status === "exited",
|
|
248
|
+
).length;
|
|
249
|
+
const failed = runsWithDescendants.filter(
|
|
250
|
+
(run) => run.status === "failed",
|
|
251
|
+
).length;
|
|
252
|
+
const cancelled = runsWithDescendants.filter(
|
|
253
|
+
(run) => run.status === "cancelled",
|
|
254
|
+
).length;
|
|
255
|
+
const killed = runsWithDescendants.filter(
|
|
256
|
+
(run) => run.status === "killed",
|
|
257
|
+
).length;
|
|
239
258
|
const progressSubagents = runningRuns.reduce(
|
|
240
259
|
(sum, run) => sum + Math.max(1, Math.floor(run.activeSubagents ?? 0)),
|
|
241
260
|
0,
|
|
@@ -244,7 +263,10 @@ export function summarizeRuns(
|
|
|
244
263
|
(sum, count) => sum + count,
|
|
245
264
|
0,
|
|
246
265
|
);
|
|
247
|
-
const runningSubagents = Math.max(
|
|
266
|
+
const runningSubagents = Math.max(
|
|
267
|
+
progressSubagents,
|
|
268
|
+
running + processSubagents,
|
|
269
|
+
);
|
|
248
270
|
return {
|
|
249
271
|
cancelled,
|
|
250
272
|
done,
|
|
@@ -278,11 +300,16 @@ function getProcCommand(pid: string): string {
|
|
|
278
300
|
return (readProcFile(`/proc/${pid}/cmdline`) ?? "").replaceAll("\0", " ");
|
|
279
301
|
}
|
|
280
302
|
|
|
281
|
-
function getRunningRunPidMap(
|
|
303
|
+
function getRunningRunPidMap(
|
|
304
|
+
stateRoot: string,
|
|
305
|
+
ownerId?: string,
|
|
306
|
+
): Map<string, string> {
|
|
282
307
|
const pids = new Map<string, string>();
|
|
283
308
|
for (const run of summarizeRunsWithoutSubagents(stateRoot, ownerId).runs) {
|
|
284
309
|
if (run.status !== "running") continue;
|
|
285
|
-
const status = AsyncRuns.getRunStatus(
|
|
310
|
+
const status = AsyncRuns.getRunStatus(
|
|
311
|
+
run.stateDir ?? join(stateRoot, run.run),
|
|
312
|
+
);
|
|
286
313
|
const pid = Number(status.pid || 0);
|
|
287
314
|
if (pid > 0) pids.set(String(pid), run.run);
|
|
288
315
|
}
|
|
@@ -420,7 +447,10 @@ export function findRunRetirementCandidates(
|
|
|
420
447
|
return summary.runs
|
|
421
448
|
.map((run) => {
|
|
422
449
|
const activeSubagents = Math.max(0, Math.floor(run.activeSubagents ?? 0));
|
|
423
|
-
const descendantSubagents = Math.max(
|
|
450
|
+
const descendantSubagents = Math.max(
|
|
451
|
+
0,
|
|
452
|
+
Math.floor(run.descendantSubagents ?? 0),
|
|
453
|
+
);
|
|
424
454
|
const childRuns = run.stateDir
|
|
425
455
|
? summary.runs.filter(
|
|
426
456
|
(child) =>
|
|
@@ -429,7 +459,9 @@ export function findRunRetirementCandidates(
|
|
|
429
459
|
isNestedStateDir(run.stateDir!, child.stateDir),
|
|
430
460
|
)
|
|
431
461
|
: [];
|
|
432
|
-
const runningChildRuns = childRuns.filter(
|
|
462
|
+
const runningChildRuns = childRuns.filter(
|
|
463
|
+
(child) => child.status === "running",
|
|
464
|
+
).length;
|
|
433
465
|
return {
|
|
434
466
|
activeSubagents,
|
|
435
467
|
childRuns: childRuns.length,
|
|
@@ -441,7 +473,9 @@ export function findRunRetirementCandidates(
|
|
|
441
473
|
!run.terminalHandled &&
|
|
442
474
|
activeSubagents + descendantSubagents + runningChildRuns <= 0,
|
|
443
475
|
run,
|
|
444
|
-
terminalChildRuns: childRuns.filter((child) =>
|
|
476
|
+
terminalChildRuns: childRuns.filter((child) =>
|
|
477
|
+
TERMINAL.has(child.status),
|
|
478
|
+
).length,
|
|
445
479
|
};
|
|
446
480
|
})
|
|
447
481
|
.filter((item) => item.ready)
|
|
@@ -462,7 +496,11 @@ export async function executeRunRetirements(
|
|
|
462
496
|
const results: RunRetirementExecution[] = [];
|
|
463
497
|
for (const candidate of findRunRetirementCandidates(summary)) {
|
|
464
498
|
if (options.attempted?.has(candidate.stateDir)) {
|
|
465
|
-
results.push({
|
|
499
|
+
results.push({
|
|
500
|
+
action: "skip",
|
|
501
|
+
run: candidate.run,
|
|
502
|
+
stateDir: candidate.stateDir,
|
|
503
|
+
});
|
|
466
504
|
continue;
|
|
467
505
|
}
|
|
468
506
|
options.attempted?.add(candidate.stateDir);
|
|
@@ -472,12 +510,18 @@ export async function executeRunRetirements(
|
|
|
472
510
|
`Retiring actor ${candidate.run} after child runs reached terminal state`,
|
|
473
511
|
"info",
|
|
474
512
|
);
|
|
475
|
-
results.push({
|
|
513
|
+
results.push({
|
|
514
|
+
action: "stop",
|
|
515
|
+
run: candidate.run,
|
|
516
|
+
stateDir: candidate.stateDir,
|
|
517
|
+
});
|
|
476
518
|
continue;
|
|
477
519
|
} catch (error) {
|
|
478
520
|
try {
|
|
479
521
|
const cancelResult = options.cancelRun(candidate);
|
|
480
|
-
const cancelled = Boolean(
|
|
522
|
+
const cancelled = Boolean(
|
|
523
|
+
(cancelResult as { cancelled?: unknown }).cancelled,
|
|
524
|
+
);
|
|
481
525
|
options.notify?.(
|
|
482
526
|
cancelled
|
|
483
527
|
? `Retiring actor ${candidate.run} by cancellation after graceful stop failed`
|
|
@@ -486,13 +530,23 @@ export async function executeRunRetirements(
|
|
|
486
530
|
);
|
|
487
531
|
results.push({
|
|
488
532
|
action: cancelled ? "cancel" : "skip",
|
|
489
|
-
...(cancelled
|
|
533
|
+
...(cancelled
|
|
534
|
+
? {}
|
|
535
|
+
: {
|
|
536
|
+
error: error instanceof Error ? error.message : String(error),
|
|
537
|
+
}),
|
|
490
538
|
run: candidate.run,
|
|
491
539
|
stateDir: candidate.stateDir,
|
|
492
540
|
});
|
|
493
541
|
} catch (cancelError) {
|
|
494
|
-
const message =
|
|
495
|
-
|
|
542
|
+
const message =
|
|
543
|
+
cancelError instanceof Error
|
|
544
|
+
? cancelError.message
|
|
545
|
+
: String(cancelError);
|
|
546
|
+
options.notify?.(
|
|
547
|
+
`Actor retirement failed for ${candidate.run}: ${message}`,
|
|
548
|
+
"error",
|
|
549
|
+
);
|
|
496
550
|
results.push({
|
|
497
551
|
action: "failed",
|
|
498
552
|
error: message,
|
|
@@ -505,13 +559,20 @@ export async function executeRunRetirements(
|
|
|
505
559
|
return results;
|
|
506
560
|
}
|
|
507
561
|
|
|
562
|
+
function runObservationKey(
|
|
563
|
+
run: Pick<RunObservation, "run" | "stateDir">,
|
|
564
|
+
): string {
|
|
565
|
+
return run.stateDir ?? run.run;
|
|
566
|
+
}
|
|
567
|
+
|
|
508
568
|
export function detectRunTransitions(
|
|
509
569
|
previous: Map<string, RunObservedStatus>,
|
|
510
570
|
summary: RunSummary,
|
|
511
571
|
): RunTransition[] {
|
|
512
572
|
const transitions: RunTransition[] = [];
|
|
513
573
|
for (const run of summary.runs) {
|
|
514
|
-
const
|
|
574
|
+
const key = runObservationKey(run);
|
|
575
|
+
const old = previous.get(key);
|
|
515
576
|
if (old && old !== run.status && TERMINAL.has(run.status)) {
|
|
516
577
|
transitions.push({
|
|
517
578
|
from: old,
|
|
@@ -525,7 +586,7 @@ export function detectRunTransitions(
|
|
|
525
586
|
...(run.tool ? { tool: run.tool } : {}),
|
|
526
587
|
});
|
|
527
588
|
}
|
|
528
|
-
previous.set(
|
|
589
|
+
previous.set(key, run.status);
|
|
529
590
|
}
|
|
530
591
|
return transitions;
|
|
531
592
|
}
|
|
@@ -599,12 +660,12 @@ export function pruneRunObservationState(
|
|
|
599
660
|
summary: RunSummary,
|
|
600
661
|
terminalRuns: Iterable<string> = [],
|
|
601
662
|
): void {
|
|
602
|
-
const activeRuns = new Set(summary.runs.map((run) => run
|
|
663
|
+
const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
|
|
603
664
|
const terminalRunSet = new Set(terminalRuns);
|
|
604
665
|
const terminalLineKeys = new Set(
|
|
605
666
|
summary.runs
|
|
606
|
-
.filter((run) => terminalRunSet.has(run
|
|
607
|
-
.map((run) => run
|
|
667
|
+
.filter((run) => terminalRunSet.has(runObservationKey(run)))
|
|
668
|
+
.map((run) => runObservationKey(run)),
|
|
608
669
|
);
|
|
609
670
|
const activeLineKeys = new Set(
|
|
610
671
|
summary.runs.map((run) => run.stateDir ?? run.run),
|