@llblab/pi-actors 0.43.1 → 0.45.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/AGENTS.md +6 -6
- package/BACKLOG.md +1 -189
- package/CHANGELOG.md +419 -402
- package/README.md +8 -6
- package/dist/index.js +7 -3
- package/dist/lib/async-runs.d.ts +2 -0
- package/dist/lib/async-runs.js +41 -34
- package/dist/lib/config.js +1 -0
- package/dist/lib/file-state.d.ts +3 -1
- package/dist/lib/file-state.js +25 -7
- package/dist/lib/inspector-overlay.js +46 -30
- package/dist/lib/inspector.js +8 -2
- package/dist/lib/limits.d.ts +10 -0
- package/dist/lib/limits.js +10 -0
- package/dist/lib/observability.d.ts +4 -2
- package/dist/lib/observability.js +43 -36
- package/dist/lib/recipes-discovery.js +20 -3
- package/dist/lib/recipes-references.d.ts +10 -0
- package/dist/lib/recipes-references.js +168 -5
- package/dist/lib/run-evidence-policy.d.ts +95 -0
- package/dist/lib/run-evidence-policy.js +177 -0
- package/dist/lib/run-ui-runtime.js +2 -0
- package/dist/lib/runs-controls.d.ts +7 -5
- package/dist/lib/runs-controls.js +186 -49
- package/dist/lib/runs-retention.js +27 -14
- package/dist/lib/runs-trace.d.ts +25 -1
- package/dist/lib/runs-trace.js +410 -21
- package/dist/lib/runtime-triage.js +6 -22
- package/dist/lib/schema.d.ts +3 -0
- package/dist/lib/schema.js +34 -1
- package/dist/lib/tools-inspect.js +58 -14
- package/dist/lib/tools-local.js +27 -10
- package/dist/lib/tools-spawn.js +2 -2
- package/dist/lib/trace-projection.js +90 -44
- package/dist/scripts/conformance.mjs +5 -0
- package/dist/scripts/locker.mjs +31 -74
- package/dist/scripts/music-player.mjs +41 -129
- package/dist/scripts/release-gates.mjs +26 -4
- package/dist/skills/actors/SKILL.md +9 -8
- package/dist/skills/swarm/SKILL.md +2 -2
- package/docs/README.md +0 -1
- package/docs/actor-inspector.md +3 -2
- package/docs/async-runs.md +10 -8
- package/docs/command-templates.md +2 -2
- package/docs/recipe-library.md +4 -2
- package/docs/template-recipes.md +10 -4
- package/docs/tool-registry.md +1 -1
- package/index.ts +9 -3
- package/lib/async-runs.ts +51 -54
- package/lib/config.ts +4 -0
- package/lib/file-state.ts +28 -7
- package/lib/inspector-overlay.ts +29 -17
- package/lib/inspector.ts +16 -2
- package/lib/limits.ts +10 -0
- package/lib/observability.ts +55 -57
- package/lib/recipes-discovery.ts +23 -3
- package/lib/recipes-references.ts +227 -7
- package/lib/run-evidence-policy.ts +242 -0
- package/lib/run-ui-runtime.ts +2 -0
- package/lib/runs-controls.ts +177 -108
- package/lib/runs-retention.ts +28 -20
- package/lib/runs-trace.ts +496 -21
- package/lib/runtime-triage.ts +11 -25
- package/lib/schema.ts +53 -1
- package/lib/tools-inspect.ts +56 -15
- package/lib/tools-local.ts +49 -19
- package/lib/tools-spawn.ts +2 -2
- package/lib/trace-projection.ts +137 -75
- package/package.json +1 -1
- package/scripts/conformance.mjs +5 -0
- package/scripts/locker.mjs +31 -74
- package/scripts/music-player.mjs +41 -129
- package/scripts/release-gates.mjs +26 -4
- package/skills/actors/SKILL.md +9 -8
- package/skills/swarm/SKILL.md +2 -2
- package/dist/lib/runtime-notifier.d.ts +0 -48
- package/dist/lib/runtime-notifier.js +0 -138
- package/docs/0.43-baseline.md +0 -39
- package/lib/runtime-notifier.ts +0 -211
package/lib/async-runs.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import * as Paths from "./paths.ts";
|
|
34
34
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
35
35
|
import * as RecipesUsage from "./recipes-usage.ts";
|
|
36
|
+
import * as Schema from "./schema.ts";
|
|
36
37
|
import {
|
|
37
38
|
resolveArtifactManifest,
|
|
38
39
|
resolveArtifactPaths,
|
|
@@ -64,10 +65,6 @@ import * as RunsStart from "./runs-start.ts";
|
|
|
64
65
|
import { appendRunTraceEvent } from "./runs-trace.ts";
|
|
65
66
|
import * as RunsIndex from "./runs-index.ts";
|
|
66
67
|
import * as RunsParentTeardown from "./runs-parent-teardown.ts";
|
|
67
|
-
import {
|
|
68
|
-
appendRunControlInStateDir,
|
|
69
|
-
updateRunControlStatusInStateDir,
|
|
70
|
-
} from "./runs-controls.ts";
|
|
71
68
|
import {
|
|
72
69
|
deliverRunControl,
|
|
73
70
|
type DeliverRunControlOptions,
|
|
@@ -132,6 +129,8 @@ export interface AsyncRunStartParams {
|
|
|
132
129
|
template?: CommandTemplateValue;
|
|
133
130
|
args?: string[];
|
|
134
131
|
defaults?: Record<string, unknown>;
|
|
132
|
+
recipe_dir?: string;
|
|
133
|
+
skill_dir?: string;
|
|
135
134
|
parallel?: boolean;
|
|
136
135
|
concurrency?: number | string;
|
|
137
136
|
min_successful?: number | string;
|
|
@@ -313,7 +312,14 @@ function resolveStartParams(params: AsyncRunStartParams): AsyncRunStartParams {
|
|
|
313
312
|
fileParams.run_id ||
|
|
314
313
|
fileParams.name ||
|
|
315
314
|
getRunIdFromFile(fileParams.file),
|
|
316
|
-
values: {
|
|
315
|
+
values: {
|
|
316
|
+
...(fileParams.values ?? {}),
|
|
317
|
+
...(params.values ?? {}),
|
|
318
|
+
...(fileParams.recipe_dir
|
|
319
|
+
? { recipe_dir: fileParams.recipe_dir }
|
|
320
|
+
: {}),
|
|
321
|
+
...(fileParams.skill_dir ? { skill_dir: fileParams.skill_dir } : {}),
|
|
322
|
+
},
|
|
317
323
|
};
|
|
318
324
|
}
|
|
319
325
|
|
|
@@ -481,13 +487,37 @@ export function startRun(
|
|
|
481
487
|
!isAbsolute(recipeRelation)
|
|
482
488
|
? dirname(packagedRecipeRoot)
|
|
483
489
|
: undefined;
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
490
|
+
const argSpec = Schema.parseToolArgDeclarationList(startParams.args ?? []);
|
|
491
|
+
if (argSpec.error) throw new Error(argSpec.error);
|
|
492
|
+
const declaredArgs = new Set(argSpec.args);
|
|
493
|
+
for (const key of Object.keys(startParams.defaults ?? {})) {
|
|
494
|
+
if (declaredArgs.size > 0 && !declaredArgs.has(key))
|
|
495
|
+
throw new Error(`Unknown Recipe default argument: ${key}`);
|
|
496
|
+
}
|
|
497
|
+
const values = Schema.normalizeRuntimeValues(
|
|
498
|
+
{
|
|
499
|
+
...argSpec.defaults,
|
|
500
|
+
...(startParams.defaults || {}),
|
|
501
|
+
...(packagedRepo ? { repo: packagedRepo } : {}),
|
|
502
|
+
...(startParams.values || {}),
|
|
503
|
+
run_id: run,
|
|
504
|
+
state_dir: stateDir,
|
|
505
|
+
trace_file: join(stateDir, "trace.jsonl"),
|
|
506
|
+
},
|
|
507
|
+
argSpec.argTypes,
|
|
508
|
+
);
|
|
509
|
+
const runtimeTemplate =
|
|
510
|
+
resolved.template &&
|
|
511
|
+
typeof resolved.template === "object" &&
|
|
512
|
+
!Array.isArray(resolved.template)
|
|
513
|
+
? {
|
|
514
|
+
...resolved.template,
|
|
515
|
+
defaults: {
|
|
516
|
+
...(resolved.template.defaults ?? {}),
|
|
517
|
+
...values,
|
|
518
|
+
},
|
|
519
|
+
}
|
|
520
|
+
: resolved.template;
|
|
491
521
|
const modelPolicy = describeCurrentPolicyProvenance({
|
|
492
522
|
defaults: startParams.defaults,
|
|
493
523
|
template: resolved.template,
|
|
@@ -561,7 +591,7 @@ export function startRun(
|
|
|
561
591
|
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
562
592
|
status: "running",
|
|
563
593
|
...(startParams.tool ? { tool: startParams.tool } : {}),
|
|
564
|
-
template:
|
|
594
|
+
template: runtimeTemplate,
|
|
565
595
|
values,
|
|
566
596
|
model_policy: modelPolicy,
|
|
567
597
|
...(artifacts ? { artifacts } : {}),
|
|
@@ -933,37 +963,9 @@ function stopRun(
|
|
|
933
963
|
) {
|
|
934
964
|
return { stopped: false, reason: "run generation changed", status };
|
|
935
965
|
}
|
|
936
|
-
const control =
|
|
937
|
-
event === "run.kill" && typeof status.run_instance_id === "string"
|
|
938
|
-
? appendRunControlInStateDir(stateDir, {
|
|
939
|
-
action: "kill",
|
|
940
|
-
run_instance_id: status.run_instance_id,
|
|
941
|
-
})
|
|
942
|
-
: undefined;
|
|
943
|
-
if (control) {
|
|
944
|
-
updateRunControlStatusInStateDir(
|
|
945
|
-
stateDir,
|
|
946
|
-
control.id,
|
|
947
|
-
"claimed",
|
|
948
|
-
{},
|
|
949
|
-
["queued"],
|
|
950
|
-
);
|
|
951
|
-
}
|
|
952
|
-
const finish = (result: Record<string, unknown>): Record<string, unknown> => {
|
|
953
|
-
if (!control) return result;
|
|
954
|
-
const handled = result.stopped === true;
|
|
955
|
-
updateRunControlStatusInStateDir(
|
|
956
|
-
stateDir,
|
|
957
|
-
control.id,
|
|
958
|
-
handled ? "handled" : "failed",
|
|
959
|
-
handled ? {} : { error: String(result.reason ?? "kill rejected") },
|
|
960
|
-
["claimed"],
|
|
961
|
-
);
|
|
962
|
-
return { ...result, control_id: control.id };
|
|
963
|
-
};
|
|
964
966
|
const pid = Number(status.pid || 0);
|
|
965
967
|
if (status.status !== "running" && status.status !== "exited") {
|
|
966
|
-
return
|
|
968
|
+
return { stopped: false, reason: "not running", status };
|
|
967
969
|
}
|
|
968
970
|
const identity = verifyRunProcessIdentity(
|
|
969
971
|
pid,
|
|
@@ -974,22 +976,22 @@ function stopRun(
|
|
|
974
976
|
identity.status === "owner_mismatch" ||
|
|
975
977
|
identity.status === "unsupported_proof"
|
|
976
978
|
) {
|
|
977
|
-
return
|
|
979
|
+
return {
|
|
978
980
|
stopped: false,
|
|
979
981
|
reason: identity.status.replaceAll("_", " "),
|
|
980
982
|
process_identity_status: identity.status,
|
|
981
983
|
status,
|
|
982
|
-
}
|
|
984
|
+
};
|
|
983
985
|
}
|
|
984
|
-
return
|
|
986
|
+
return { stopped: false, reason: "not running", status };
|
|
985
987
|
}
|
|
986
988
|
if (!identity.valid) {
|
|
987
|
-
return
|
|
989
|
+
return {
|
|
988
990
|
stopped: false,
|
|
989
991
|
reason: identity.status.replaceAll("_", " "),
|
|
990
992
|
process_identity_status: identity.status,
|
|
991
993
|
status,
|
|
992
|
-
}
|
|
994
|
+
};
|
|
993
995
|
}
|
|
994
996
|
let signalResult: RunProcessSignalPlan;
|
|
995
997
|
try {
|
|
@@ -999,11 +1001,6 @@ function stopRun(
|
|
|
999
1001
|
status.process_identity as RunProcessIdentity,
|
|
1000
1002
|
);
|
|
1001
1003
|
} catch (error) {
|
|
1002
|
-
if (control) {
|
|
1003
|
-
updateRunControlStatusInStateDir(stateDir, control.id, "failed", {
|
|
1004
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
1004
|
throw error;
|
|
1008
1005
|
}
|
|
1009
1006
|
appendRunTraceEvent(stateDir, {
|
|
@@ -1020,13 +1017,13 @@ function stopRun(
|
|
|
1020
1017
|
finalizeInterruptedExecution(stateDir, "cancelled", signal);
|
|
1021
1018
|
markTerminalProgress(stateDir, "cancelled");
|
|
1022
1019
|
}
|
|
1023
|
-
return
|
|
1020
|
+
return {
|
|
1024
1021
|
stopped: true,
|
|
1025
1022
|
pid,
|
|
1026
1023
|
signal,
|
|
1027
1024
|
...signalResult,
|
|
1028
1025
|
state_dir: stateDir,
|
|
1029
|
-
}
|
|
1026
|
+
};
|
|
1030
1027
|
} finally {
|
|
1031
1028
|
releaseControlLock();
|
|
1032
1029
|
}
|
package/lib/config.ts
CHANGED
|
@@ -188,6 +188,10 @@ export function normalizeStoredTool(
|
|
|
188
188
|
template: argTemplate,
|
|
189
189
|
};
|
|
190
190
|
const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
191
|
+
Schema.assertCompatibleToolArgTypes(
|
|
192
|
+
inferredArgTypes,
|
|
193
|
+
declarations.argTypes,
|
|
194
|
+
);
|
|
191
195
|
const argTypes = { ...inferredArgTypes, ...declarations.argTypes };
|
|
192
196
|
const cfg = {
|
|
193
197
|
name,
|
package/lib/file-state.ts
CHANGED
|
@@ -10,8 +10,11 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSyn
|
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { basename, dirname, join, parse, resolve } from "node:path";
|
|
12
12
|
|
|
13
|
-
const FILE_MUTATION_LOCK_TIMEOUT_MS = process.platform === "win32" ?
|
|
13
|
+
const FILE_MUTATION_LOCK_TIMEOUT_MS = process.platform === "win32" ? 30000 : 15000;
|
|
14
14
|
const FILE_MUTATION_LOCK_STALE_MS = 30000;
|
|
15
|
+
const FILE_MUTATION_LOCK_RECLAIM_POLL_MS = 100;
|
|
16
|
+
const FILE_MUTATION_LOCK_REMOVAL_GRACE_MS = 250;
|
|
17
|
+
const FILE_MUTATION_LOCK_MAX_WAIT_MS = 50;
|
|
15
18
|
const FILE_MUTATION_LOCK_ROOT = join(tmpdir(), "pi-actors-file-locks");
|
|
16
19
|
|
|
17
20
|
function canonicalMutationPath(path: string): string {
|
|
@@ -65,7 +68,8 @@ function lockOwnerStatus(lockPath: string): "alive" | "dead" | "unknown" {
|
|
|
65
68
|
if (!Number.isInteger(pid) || pid <= 0) return "unknown";
|
|
66
69
|
try {
|
|
67
70
|
process.kill(pid, 0);
|
|
68
|
-
if (Date.now() - statSync(lockPath).mtimeMs >
|
|
71
|
+
if (Date.now() - statSync(lockPath).mtimeMs > FILE_MUTATION_LOCK_REMOVAL_GRACE_MS &&
|
|
72
|
+
isZombieProcess(pid)) return "dead";
|
|
69
73
|
return "alive";
|
|
70
74
|
} catch (error) {
|
|
71
75
|
return (error as NodeJS.ErrnoException).code === "ESRCH"
|
|
@@ -120,8 +124,9 @@ function removeLockBoundary(lockPath: string, token: string | undefined): boolea
|
|
|
120
124
|
function tryReclaimRemovalBoundary(reclaimPath: string): void {
|
|
121
125
|
if (!existsSync(reclaimPath)) return;
|
|
122
126
|
try {
|
|
123
|
-
const inspectedToken = readLockToken(reclaimPath);
|
|
124
127
|
const age = Date.now() - statSync(reclaimPath).mtimeMs;
|
|
128
|
+
if (age <= FILE_MUTATION_LOCK_REMOVAL_GRACE_MS) return;
|
|
129
|
+
const inspectedToken = readLockToken(reclaimPath);
|
|
125
130
|
const ownerStatus = lockOwnerStatus(reclaimPath);
|
|
126
131
|
if (
|
|
127
132
|
(ownerStatus === "dead" ||
|
|
@@ -195,6 +200,8 @@ export function acquireFileMutationLock(
|
|
|
195
200
|
const token = randomUUID();
|
|
196
201
|
const pendingPath = prepareLockBoundary(lockPath, token, options.onBeforeLockPublish);
|
|
197
202
|
let contentionReported = false;
|
|
203
|
+
let nextReclaimAt = 0;
|
|
204
|
+
let waitMs = 10;
|
|
198
205
|
try {
|
|
199
206
|
for (;;) {
|
|
200
207
|
try {
|
|
@@ -205,13 +212,22 @@ export function acquireFileMutationLock(
|
|
|
205
212
|
contentionReported = true;
|
|
206
213
|
options.onContention?.();
|
|
207
214
|
}
|
|
208
|
-
|
|
209
|
-
if (
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
if (now >= nextReclaimAt) {
|
|
217
|
+
nextReclaimAt = now + FILE_MUTATION_LOCK_RECLAIM_POLL_MS;
|
|
218
|
+
if (tryReclaimMutationLock(lockPath, options)) {
|
|
219
|
+
waitMs = 10;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (now >= deadline) {
|
|
210
224
|
throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
|
|
211
225
|
cause: error,
|
|
212
226
|
});
|
|
213
227
|
}
|
|
214
|
-
|
|
228
|
+
const jitter = Math.floor(Math.random() * Math.max(1, Math.floor(waitMs / 2)));
|
|
229
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs + jitter);
|
|
230
|
+
waitMs = Math.min(FILE_MUTATION_LOCK_MAX_WAIT_MS, waitMs + 5);
|
|
215
231
|
}
|
|
216
232
|
}
|
|
217
233
|
} finally {
|
|
@@ -251,11 +267,16 @@ export function withFileMutationLock<T>(
|
|
|
251
267
|
}
|
|
252
268
|
}
|
|
253
269
|
|
|
254
|
-
export function writeTextAtomic(
|
|
270
|
+
export function writeTextAtomic(
|
|
271
|
+
path: string,
|
|
272
|
+
content: string,
|
|
273
|
+
options: { onBeforeReplace?(): void } = {},
|
|
274
|
+
): void {
|
|
255
275
|
mkdirSync(dirname(path), { recursive: true });
|
|
256
276
|
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
257
277
|
try {
|
|
258
278
|
writeFileSync(tempPath, content, "utf8");
|
|
279
|
+
options.onBeforeReplace?.();
|
|
259
280
|
renameSync(tempPath, path);
|
|
260
281
|
} catch (error) {
|
|
261
282
|
try {
|
package/lib/inspector-overlay.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns manual kernel navigation; evidence parsing and lifecycle mutation stay in ports.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
7
|
+
import { readFileSync, statSync } from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
@@ -19,6 +19,10 @@ import * as ActorInspector from "./inspector.ts";
|
|
|
19
19
|
import * as ControlProjection from "./control-projection.ts";
|
|
20
20
|
import * as RunsControls from "./runs-controls.ts";
|
|
21
21
|
import * as RunControlDelivery from "./runs-control-delivery.ts";
|
|
22
|
+
import * as RunsTrace from "./runs-trace.ts";
|
|
23
|
+
import * as Limits from "./limits.ts";
|
|
24
|
+
import { computeRunControlCapacity } from "./run-evidence-policy.ts";
|
|
25
|
+
import * as RuntimeTriage from "./runtime-triage.ts";
|
|
22
26
|
import * as TraceProjection from "./trace-projection.ts";
|
|
23
27
|
|
|
24
28
|
export type ActorInspectorTab = "recipe" | "trace" | "control";
|
|
@@ -59,12 +63,6 @@ const TRACE_SOURCES: TraceProjection.TraceSourceFilter[] = [
|
|
|
59
63
|
"runtime",
|
|
60
64
|
];
|
|
61
65
|
|
|
62
|
-
function newestTraceItems(items: readonly TraceProjection.TraceItem[]): TraceProjection.TraceItem[] {
|
|
63
|
-
return [...items].sort((left, right) =>
|
|
64
|
-
right.ts.localeCompare(left.ts) || right.id.localeCompare(left.id),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
66
|
export class ActorInspectorOverlay {
|
|
69
67
|
private readonly done: () => void;
|
|
70
68
|
private readonly killRun?: (run: string, runInstanceId: string) => ActorInspectorActionResult;
|
|
@@ -442,7 +440,7 @@ export class ActorInspectorOverlay {
|
|
|
442
440
|
`${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
|
|
443
441
|
`${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
|
|
444
442
|
"",
|
|
445
|
-
this.theme.fg("text", "This
|
|
443
|
+
this.theme.fg("text", "This invokes generation-fenced runtime kill."),
|
|
446
444
|
"",
|
|
447
445
|
`${cancel} ${kill}`,
|
|
448
446
|
"",
|
|
@@ -544,7 +542,11 @@ export class ActorInspectorOverlay {
|
|
|
544
542
|
|
|
545
543
|
private renderTraceList(run: ActorInspector.ActorInspectorRunItem): string[] {
|
|
546
544
|
const items = this.traceItems(run);
|
|
547
|
-
|
|
545
|
+
const summary = RunsTrace.summarizeRunTraceJournal(
|
|
546
|
+
RunsTrace.readRunTraceJournal(path.join(this.stateRoot, run.run)));
|
|
547
|
+
const banner = summary.history_complete ? [] : [this.theme.fg("warning",
|
|
548
|
+
` ! Trace history incomplete: ${summary.compacted ? `${summary.compactions_total} compaction(s), ` : ""}${summary.dropped_event_count_exact ? summary.dropped_events : "unknown"} dropped event(s), ${summary.dropped_bytes} bytes`)];
|
|
549
|
+
if (items.length === 0) return [...banner, this.theme.fg("muted", " No Trace evidence")];
|
|
548
550
|
if (this.selectedTraceId) {
|
|
549
551
|
const selectedIndex = items.findIndex((item) => item.id === this.selectedTraceId);
|
|
550
552
|
if (selectedIndex >= 0) this.rowIndex = selectedIndex;
|
|
@@ -560,7 +562,7 @@ export class ActorInspectorOverlay {
|
|
|
560
562
|
this.contentStripeIndices = items
|
|
561
563
|
.slice(start, start + viewportRows)
|
|
562
564
|
.map((_item, offset) => start + offset);
|
|
563
|
-
return items.slice(start, start + viewportRows).map((item, offset) => {
|
|
565
|
+
return [...banner, ...items.slice(start, start + viewportRows - banner.length).map((item, offset) => {
|
|
564
566
|
const index = start + offset;
|
|
565
567
|
const detail = item.detail && typeof item.detail === "object"
|
|
566
568
|
? item.detail as Record<string, unknown>
|
|
@@ -577,7 +579,7 @@ export class ActorInspectorOverlay {
|
|
|
577
579
|
return this.focus === "list" && index === this.rowIndex
|
|
578
580
|
? this.theme.fg("accent", row)
|
|
579
581
|
: row;
|
|
580
|
-
});
|
|
582
|
+
})];
|
|
581
583
|
}
|
|
582
584
|
|
|
583
585
|
private renderTraceDetail(width: number): string[] {
|
|
@@ -603,16 +605,26 @@ export class ActorInspectorOverlay {
|
|
|
603
605
|
? RunControlDelivery.readRunControlEndpoint(stateDir, run.runInstanceId)
|
|
604
606
|
: undefined;
|
|
605
607
|
const meta = this.readRunMeta(stateDir);
|
|
608
|
+
const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
|
|
609
|
+
const capacity = computeRunControlCapacity(journal.records);
|
|
610
|
+
const stalePending = journal.records.reduce<number>((count, control) => count + Number(Boolean(
|
|
611
|
+
RuntimeTriage.classifyRuntimeControl({ run: run.run,
|
|
612
|
+
runInstanceId: run.runInstanceId, status: run.status }, control, Date.now()).stale)), 0);
|
|
606
613
|
return {
|
|
607
614
|
status: run.status,
|
|
608
615
|
run_instance_id: run.runInstanceId,
|
|
616
|
+
pending: capacity.pending, pending_limit: capacity.limit,
|
|
617
|
+
available: capacity.available, backpressured: capacity.backpressured,
|
|
618
|
+
journal_bytes: (() => { try { return statSync(RunsControls.runControlsFile(stateDir)).size; } catch { return 0; } })(),
|
|
619
|
+
journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
|
|
620
|
+
stale_pending: stalePending,
|
|
621
|
+
diagnostics: journal.diagnostics.map((item) => item.line === undefined
|
|
622
|
+
? { reason: "unreadable_control_journal" } : { line: item.line, reason: "invalid_control_json" }),
|
|
609
623
|
actor_actions: Array.isArray(meta.control) ? meta.control : [],
|
|
610
624
|
runtime_actions: run.status === "running" ? ["kill"] : ["archive", "prune"],
|
|
611
625
|
endpoint,
|
|
612
|
-
recent_controls:
|
|
613
|
-
.
|
|
614
|
-
.reverse()
|
|
615
|
-
.map(ControlProjection.projectRunControl),
|
|
626
|
+
recent_controls: journal.records.slice(-20).reverse().map((control) =>
|
|
627
|
+
ControlProjection.projectRunControl(control as RunsControls.RunControlRecord)),
|
|
616
628
|
};
|
|
617
629
|
}
|
|
618
630
|
|
|
@@ -624,10 +636,10 @@ export class ActorInspectorOverlay {
|
|
|
624
636
|
) {
|
|
625
637
|
return this.traceCache.items;
|
|
626
638
|
}
|
|
627
|
-
const items =
|
|
639
|
+
const items = this.readTrace(path.join(this.stateRoot, run.run), {
|
|
628
640
|
limit: 100,
|
|
629
641
|
source: "all",
|
|
630
|
-
})
|
|
642
|
+
});
|
|
631
643
|
this.traceCache = { run: run.run, runInstanceId: run.runInstanceId, items };
|
|
632
644
|
return items;
|
|
633
645
|
}
|
package/lib/inspector.ts
CHANGED
|
@@ -105,9 +105,22 @@ export function readActorInspectorRecipe(
|
|
|
105
105
|
const redactedRecord = (value: Record<string, unknown>): Record<string, unknown> =>
|
|
106
106
|
SessionEvidence.redactSessionEvidenceValue(
|
|
107
107
|
Object.fromEntries(
|
|
108
|
-
Object.entries(value).filter(
|
|
108
|
+
Object.entries(value).filter(
|
|
109
|
+
([key, entry]) =>
|
|
110
|
+
entry !== undefined &&
|
|
111
|
+
key !== "recipe_dir" &&
|
|
112
|
+
key !== "skill_dir",
|
|
113
|
+
),
|
|
109
114
|
),
|
|
110
115
|
) as Record<string, unknown>;
|
|
116
|
+
const launchValues =
|
|
117
|
+
meta.values && typeof meta.values === "object" && !Array.isArray(meta.values)
|
|
118
|
+
? Object.fromEntries(
|
|
119
|
+
Object.entries(meta.values as Record<string, unknown>).filter(
|
|
120
|
+
([key]) => key !== "recipe_dir" && key !== "skill_dir",
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
: meta.values;
|
|
111
124
|
return {
|
|
112
125
|
composition: contexts.map((entry) =>
|
|
113
126
|
redactedRecord({
|
|
@@ -116,6 +129,7 @@ export function readActorInspectorRecipe(
|
|
|
116
129
|
file: entry.file,
|
|
117
130
|
import_path: entry.import_path,
|
|
118
131
|
name: entry.name,
|
|
132
|
+
qualified_name: entry.qualified_name,
|
|
119
133
|
}),
|
|
120
134
|
),
|
|
121
135
|
...(definition ? { definition: redactedRecord(definition) } : {}),
|
|
@@ -129,7 +143,7 @@ export function readActorInspectorRecipe(
|
|
|
129
143
|
launch: redactedRecord({
|
|
130
144
|
cwd: meta.cwd,
|
|
131
145
|
template: meta.template,
|
|
132
|
-
values:
|
|
146
|
+
values: launchValues,
|
|
133
147
|
model_policy: meta.model_policy,
|
|
134
148
|
control: meta.control,
|
|
135
149
|
artifacts: meta.artifacts,
|
package/lib/limits.ts
CHANGED
|
@@ -17,3 +17,13 @@ export const SESSION_EVIDENCE_TEXT_CHARS = 4_000;
|
|
|
17
17
|
export const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
|
|
18
18
|
export const TRACE_EVENT_MAX_BYTES = 64 * 1024;
|
|
19
19
|
export const TRACE_EVENT_MAX_READ = 200;
|
|
20
|
+
export const TRACE_JOURNAL_MAX_EVENTS = 2_048;
|
|
21
|
+
export const TRACE_JOURNAL_TARGET_EVENTS = 1_536;
|
|
22
|
+
export const TRACE_JOURNAL_MAX_BYTES = 4 * 1024 * 1024;
|
|
23
|
+
export const TRACE_JOURNAL_TARGET_BYTES = 3 * 1024 * 1024;
|
|
24
|
+
export const RUN_CONTROL_PENDING_LIMIT = 64;
|
|
25
|
+
export const RUN_CONTROL_TERMINAL_LIMIT = 128;
|
|
26
|
+
export const RUN_CONTROL_ERROR_MAX_BYTES = 4 * 1024;
|
|
27
|
+
export const RUN_CONTROL_JOURNAL_MAX_BYTES = 1024 * 1024;
|
|
28
|
+
export const RUN_RETENTION_MAX_RECORDS = 256;
|
|
29
|
+
export const RUN_RETENTION_MAX_BYTES = 1024 * 1024;
|
package/lib/observability.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
|
|
26
26
|
import * as AsyncRuns from "./async-runs.ts";
|
|
27
27
|
import * as Paths from "./paths.ts";
|
|
28
|
+
import * as RunsTrace from "./runs-trace.ts";
|
|
28
29
|
import { readJsonlFileResilient } from "./state-readers.ts";
|
|
29
30
|
|
|
30
31
|
export type RunObservedStatus =
|
|
@@ -73,7 +74,7 @@ export interface RunSummary {
|
|
|
73
74
|
|
|
74
75
|
export interface RunUiObservationState {
|
|
75
76
|
attentionEventIds: Map<string, Set<string>>;
|
|
76
|
-
|
|
77
|
+
legacyEventLines: Map<string, number>;
|
|
77
78
|
frame: number;
|
|
78
79
|
observed: Map<string, RunObservedStatus>;
|
|
79
80
|
}
|
|
@@ -98,12 +99,19 @@ export interface RunUiNotificationSink {
|
|
|
98
99
|
export function createRunUiObservationState(): RunUiObservationState {
|
|
99
100
|
return {
|
|
100
101
|
attentionEventIds: new Map<string, Set<string>>(),
|
|
101
|
-
|
|
102
|
+
legacyEventLines: new Map<string, number>(),
|
|
102
103
|
frame: 0,
|
|
103
104
|
observed: new Map<string, RunObservedStatus>(),
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
export function primeRunAttentionState(
|
|
109
|
+
state: RunUiObservationState, ownerId: string, stateRoot?: string,
|
|
110
|
+
): void {
|
|
111
|
+
detectRunAttentionEvents(state.legacyEventLines, summarizeRuns(stateRoot, ownerId),
|
|
112
|
+
state.attentionEventIds, true);
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
export function readRunUiSnapshot(
|
|
108
116
|
state: RunUiObservationState,
|
|
109
117
|
ownerId: string,
|
|
@@ -116,7 +124,7 @@ export function readRunUiSnapshot(
|
|
|
116
124
|
options.includeAttention === false
|
|
117
125
|
? []
|
|
118
126
|
: detectRunAttentionEvents(
|
|
119
|
-
state.
|
|
127
|
+
state.legacyEventLines,
|
|
120
128
|
summary,
|
|
121
129
|
state.attentionEventIds,
|
|
122
130
|
),
|
|
@@ -132,7 +140,7 @@ export function pruneRunUiObservationState(
|
|
|
132
140
|
): void {
|
|
133
141
|
pruneRunObservationState(
|
|
134
142
|
state.observed,
|
|
135
|
-
state.
|
|
143
|
+
state.legacyEventLines,
|
|
136
144
|
snapshot.summary,
|
|
137
145
|
snapshot.transitions.map(
|
|
138
146
|
(transition) => transition.stateDir ?? transition.run,
|
|
@@ -192,16 +200,15 @@ export function reconcileRunTerminalNotifications(input: {
|
|
|
192
200
|
sink: RunUiNotificationSink;
|
|
193
201
|
state: RunUiObservationState;
|
|
194
202
|
stateRoot?: string;
|
|
203
|
+
includeAttention?: boolean;
|
|
195
204
|
}): RunUiSnapshot {
|
|
196
205
|
const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
|
|
197
|
-
includeAttention:
|
|
206
|
+
includeAttention: input.includeAttention,
|
|
198
207
|
stateRoot: input.stateRoot,
|
|
199
208
|
});
|
|
200
|
-
|
|
201
|
-
snapshot.
|
|
202
|
-
|
|
203
|
-
input.inFlight,
|
|
204
|
-
);
|
|
209
|
+
if (input.includeAttention)
|
|
210
|
+
deliverRunAttentionNotifications(snapshot.attentionEvents, input.sink);
|
|
211
|
+
deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
|
|
205
212
|
pruneRunUiObservationState(input.state, snapshot);
|
|
206
213
|
return snapshot;
|
|
207
214
|
}
|
|
@@ -1114,15 +1121,17 @@ function parseAttentionRecord(
|
|
|
1114
1121
|
};
|
|
1115
1122
|
}
|
|
1116
1123
|
|
|
1117
|
-
function readTraceAttentionRecords(run: RunObservation):
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1124
|
+
function readTraceAttentionRecords(run: RunObservation): {
|
|
1125
|
+
canonical: boolean; records: Record<string, unknown>[];
|
|
1126
|
+
} {
|
|
1127
|
+
if (!run.stateDir) return { canonical: true, records: [] };
|
|
1128
|
+
if (existsSync(join(run.stateDir, "trace.jsonl"))) return {
|
|
1129
|
+
canonical: true,
|
|
1130
|
+
records: RunsTrace.readRunTraceJournal(run.stateDir).events.map(
|
|
1131
|
+
({ event }) => event as unknown as Record<string, unknown>),
|
|
1132
|
+
};
|
|
1133
|
+
return { canonical: false, records: readJsonlFileResilient<Record<string, unknown>>(
|
|
1134
|
+
join(run.stateDir, "outbox.jsonl")).records };
|
|
1126
1135
|
}
|
|
1127
1136
|
|
|
1128
1137
|
export function pruneRunObservationState(
|
|
@@ -1134,55 +1143,44 @@ export function pruneRunObservationState(
|
|
|
1134
1143
|
): void {
|
|
1135
1144
|
const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
|
|
1136
1145
|
const terminalRunSet = new Set(terminalRuns);
|
|
1137
|
-
const terminalLineKeys = new Set(
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
);
|
|
1142
|
-
const activeLineKeys = new Set(
|
|
1143
|
-
summary.runs.map((run) => run.stateDir ?? run.run),
|
|
1144
|
-
);
|
|
1146
|
+
const terminalLineKeys = new Set(summary.runs
|
|
1147
|
+
.filter((run) => terminalRunSet.has(runObservationKey(run)))
|
|
1148
|
+
.map((run) => runObservationKey(run)));
|
|
1149
|
+
const activeLineKeys = new Set(summary.runs.map((run) => run.stateDir ?? run.run));
|
|
1145
1150
|
for (const run of terminalRunSet) previousStatuses.delete(run);
|
|
1146
|
-
for (const run of previousStatuses.keys())
|
|
1151
|
+
for (const run of previousStatuses.keys())
|
|
1147
1152
|
if (!activeRuns.has(run)) previousStatuses.delete(run);
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
for (const key of seenEventIds.keys()) {
|
|
1155
|
-
if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) {
|
|
1156
|
-
seenEventIds.delete(key);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1153
|
+
for (const key of previousLineCounts.keys())
|
|
1154
|
+
if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) previousLineCounts.delete(key);
|
|
1155
|
+
for (const key of seenEventIds.keys())
|
|
1156
|
+
if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) seenEventIds.delete(key);
|
|
1159
1157
|
}
|
|
1160
1158
|
|
|
1161
1159
|
export function detectRunAttentionEvents(
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
seenEventIds: Map<string, Set<string>> = new Map(),
|
|
1160
|
+
legacyLineCounts: Map<string, number>, summary: RunSummary,
|
|
1161
|
+
seenEventIds: Map<string, Set<string>> = new Map(), prime = false,
|
|
1165
1162
|
): RunAttentionEvent[] {
|
|
1166
1163
|
const events: RunAttentionEvent[] = [];
|
|
1167
1164
|
for (const run of summary.runs) {
|
|
1168
1165
|
const key = run.stateDir ?? run.run;
|
|
1169
|
-
const
|
|
1170
|
-
const
|
|
1171
|
-
if (run.notificationPolicy === "silent") {
|
|
1172
|
-
previousLineCounts.set(key, records.length);
|
|
1173
|
-
seenEventIds.set(key, seenEventIds.get(key) ?? new Set<string>());
|
|
1174
|
-
continue;
|
|
1175
|
-
}
|
|
1176
|
-
const start = Math.min(previousCount, records.length);
|
|
1166
|
+
const read = readTraceAttentionRecords(run);
|
|
1167
|
+
const retained = new Set<string>();
|
|
1177
1168
|
const seen = seenEventIds.get(key) ?? new Set<string>();
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1169
|
+
const start = read.canonical ? 0
|
|
1170
|
+
: Math.min(legacyLineCounts.get(key) ?? 0, read.records.length);
|
|
1171
|
+
for (const [index, record] of read.records.entries()) {
|
|
1172
|
+
const event = parseAttentionRecord(record, run, index);
|
|
1173
|
+
if (!event || !shouldNotifyRunAttentionEvent(event) ||
|
|
1174
|
+
event.kind === "runtime.trace_compacted") continue;
|
|
1175
|
+
retained.add(event.id);
|
|
1176
|
+
if (prime || run.notificationPolicy === "silent") seen.add(event.id);
|
|
1177
|
+
else if (index >= start && !seen.has(event.id)) {
|
|
1178
|
+
events.push(event); seen.add(event.id);
|
|
1179
|
+
}
|
|
1183
1180
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1181
|
+
seenEventIds.set(key, new Set([...retained].filter((id) => seen.has(id))));
|
|
1182
|
+
if (read.canonical) legacyLineCounts.delete(key);
|
|
1183
|
+
else legacyLineCounts.set(key, read.records.length);
|
|
1186
1184
|
}
|
|
1187
1185
|
return events;
|
|
1188
1186
|
}
|