@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/README.md
CHANGED
|
@@ -84,7 +84,7 @@ Persist a trusted command template or Recipe-backed capability under `~/.pi/agen
|
|
|
84
84
|
|
|
85
85
|
Recipes can declare:
|
|
86
86
|
|
|
87
|
-
- args and
|
|
87
|
+
- named typed args, inline fallbacks, configuration `defaults`, and composition `values`;
|
|
88
88
|
- imports and command-template composition;
|
|
89
89
|
- retry, failure, recovery, repeat, concurrency, and timeout policy;
|
|
90
90
|
- artifact paths;
|
|
@@ -103,6 +103,8 @@ Example controlled Recipe:
|
|
|
103
103
|
|
|
104
104
|
Ordinary one-shot Recipes should omit `control`. Recipe imports compose definitions inside one Run; they do not create peer actors.
|
|
105
105
|
|
|
106
|
+
File-backed Recipes receive immutable `{recipe_dir}`; Recipes under a Pi-active Skill also receive `{skill_dir}`. Use `std:<recipe>` for an exact packaged component and `skill:<skill>/<recipe-path>` for an exact active-Skill component. Skill Recipes are namespaced reusable components and do not become tools automatically.
|
|
107
|
+
|
|
106
108
|
String command-template leaves execute directly without shell interpretation. Use template arrays for sequencing or an explicit trusted shell/script when shell semantics matter.
|
|
107
109
|
|
|
108
110
|
## Trace
|
|
@@ -121,13 +123,13 @@ String command-template leaves execute directly without shell interpretation. Us
|
|
|
121
123
|
}
|
|
122
124
|
```
|
|
123
125
|
|
|
124
|
-
Trace fields are exact: `id`, `ts`, `kind`, and optional `summary`, `data`, `level`, `attention`. Address, sender, recipient, reply, and routing fields fail validation.
|
|
126
|
+
Trace fields are exact: `id`, `ts`, `kind`, and optional `summary`, `data`, `level`, `attention`. Address, sender, recipient, reply, and routing fields fail validation. Trace is a bounded retained suffix, not an audit archive: the canonical authority appends within 2,048 events and 4 MiB or atomically keeps the newest suffix plus one warning-only `runtime.trace_compacted` marker. That marker means older history was discarded; `result.json`, `execution.json`, terminal state, and declared artifacts retain their own authority. `inspect view=trace` reports whether retained history is complete. Reads are newline-safe and deterministic; first-party scripts never write this file directly.
|
|
125
127
|
|
|
126
|
-
Use `attention: "notify"` for visible status and `attention: "followup"` only when the coordinator needs semantic follow-up context. Store large evidence in artifacts or bounded execution captures.
|
|
128
|
+
Attention is a live wake hint, not a durable queue: persist recovery state or an artifact first. Use `attention: "notify"` for visible status and `attention: "followup"` only when the coordinator needs semantic follow-up context; compaction may discard older hints. Store large evidence in artifacts or bounded execution captures.
|
|
127
129
|
|
|
128
130
|
## Control
|
|
129
131
|
|
|
130
|
-
Controls persist to `controls.jsonl` before transport.
|
|
132
|
+
Controls persist to `controls.jsonl` before transport. One token-owned lock rejects a 65th pending Control or 1 MiB rewrite before admission, fails closed on malformed or stale-generation evidence, and atomically admits one record. Canonical transitions retain a 128-terminal tail, expected-state fencing, and 4 KiB errors. Admitted nonterminal Controls never expire automatically. Every record carries immutable `run_instance_id`. `inspect view=control` reports pending capacity, saturation, stale work, journal bytes, and bounded diagnostics. If a stuck Run is saturated, use runtime-owned `kill`; it bypasses actor-local capacity and creates no synthetic Control.
|
|
131
133
|
|
|
132
134
|
Long-lived services publish `control-endpoint.json` only when ready:
|
|
133
135
|
|
|
@@ -159,7 +161,7 @@ Core files:
|
|
|
159
161
|
- `execution.json` — command/session provenance and complete-capture references;
|
|
160
162
|
- `result.json`, command logs, progress, and declared artifacts.
|
|
161
163
|
|
|
162
|
-
The runtime preserves owner filtering, process-identity verification, lifecycle locking, shutdown kill, terminal reconciliation, bounded captures, owned Pi sessions, path containment, and redaction.
|
|
164
|
+
The runtime preserves owner filtering, process-identity verification, lifecycle locking, shutdown kill independent of actor-local Control capacity, terminal reconciliation, bounded captures, owned Pi sessions, path containment, and redaction. Trace/Control quotas do not constrain user-declared artifacts, repositories, media sources, complete captures, or actor-owned workload state. Restart clears generation-local Trace, Control, endpoint, execution, and terminal evidence; archive preserves the bounded tree, while prune preserves only requested artifacts.
|
|
163
165
|
|
|
164
166
|
## Actor Inspector
|
|
165
167
|
|
|
@@ -197,7 +199,7 @@ npm run validate
|
|
|
197
199
|
npm run test:preservation
|
|
198
200
|
```
|
|
199
201
|
|
|
200
|
-
See the [documentation index](./docs/README.md), [Run lifecycle](./docs/async-runs.md), [Recipe library](./docs/recipe-library.md)
|
|
202
|
+
See the [documentation index](./docs/README.md), [Run lifecycle](./docs/async-runs.md), and [Recipe library](./docs/recipe-library.md).
|
|
201
203
|
|
|
202
204
|
Project context: [AGENTS.md](./AGENTS.md) · [BACKLOG.md](./BACKLOG.md) · [CHANGELOG.md](./CHANGELOG.md).
|
|
203
205
|
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as InspectorCommand from "./lib/inspector-command.js";
|
|
|
10
10
|
import * as Paths from "./lib/paths.js";
|
|
11
11
|
import * as Pi from "./lib/pi.js";
|
|
12
12
|
import * as Prompts from "./lib/prompts.js";
|
|
13
|
+
import * as RecipesReferences from "./lib/recipes-references.js";
|
|
13
14
|
import * as RunUiRuntime from "./lib/run-ui-runtime.js";
|
|
14
15
|
import * as Runtime from "./lib/runtime.js";
|
|
15
16
|
import * as Temp from "./lib/temp.js";
|
|
@@ -99,9 +100,12 @@ export default function toolRegistryExtension(pi) {
|
|
|
99
100
|
runUiRuntime.shutdown(event.reason, ctx);
|
|
100
101
|
});
|
|
101
102
|
InspectorCommand.registerActorInspectorCommand(pi, getRunOwnerId);
|
|
102
|
-
pi.on("before_agent_start", async (event) =>
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
pi.on("before_agent_start", async (event) => {
|
|
104
|
+
RecipesReferences.setActiveSkillRecipeSources(event.systemPromptOptions.skills ?? []);
|
|
105
|
+
return {
|
|
106
|
+
systemPrompt: `${event.systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
105
109
|
Pi.registerToolDefinitions(pi, Tools.createCoreActorToolDefinitions({
|
|
106
110
|
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
107
111
|
getActiveTools: () => pi.getActiveTools(),
|
package/dist/lib/async-runs.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface AsyncRunStartParams {
|
|
|
38
38
|
template?: CommandTemplateValue;
|
|
39
39
|
args?: string[];
|
|
40
40
|
defaults?: Record<string, unknown>;
|
|
41
|
+
recipe_dir?: string;
|
|
42
|
+
skill_dir?: string;
|
|
41
43
|
parallel?: boolean;
|
|
42
44
|
concurrency?: number | string;
|
|
43
45
|
min_successful?: number | string;
|
package/dist/lib/async-runs.js
CHANGED
|
@@ -12,6 +12,7 @@ import { CURRENT_MODEL_VALUE_KEY, CURRENT_THINKING_VALUE_KEY, describeCurrentPol
|
|
|
12
12
|
import * as Paths from "./paths.js";
|
|
13
13
|
import * as RecipesReferences from "./recipes-references.js";
|
|
14
14
|
import * as RecipesUsage from "./recipes-usage.js";
|
|
15
|
+
import * as Schema from "./schema.js";
|
|
15
16
|
import { resolveArtifactPaths, } from "./runs-artifacts.js";
|
|
16
17
|
import { safeRunId } from "./runs-identity.js";
|
|
17
18
|
import { buildTerminalProgress, markTerminalHandled, signalOwnedRunProcess, } from "./runs-control.js";
|
|
@@ -23,7 +24,6 @@ import * as RunsStart from "./runs-start.js";
|
|
|
23
24
|
import { appendRunTraceEvent } from "./runs-trace.js";
|
|
24
25
|
import * as RunsIndex from "./runs-index.js";
|
|
25
26
|
import * as RunsParentTeardown from "./runs-parent-teardown.js";
|
|
26
|
-
import { appendRunControlInStateDir, updateRunControlStatusInStateDir, } from "./runs-controls.js";
|
|
27
27
|
import { deliverRunControl, } from "./runs-control-delivery.js";
|
|
28
28
|
import { buildRunStatus, tailFile, } from "./runs-status.js";
|
|
29
29
|
import { readJsonFileResilient } from "./state-readers.js";
|
|
@@ -152,7 +152,14 @@ function resolveStartParams(params) {
|
|
|
152
152
|
fileParams.run_id ||
|
|
153
153
|
fileParams.name ||
|
|
154
154
|
getRunIdFromFile(fileParams.file),
|
|
155
|
-
values: {
|
|
155
|
+
values: {
|
|
156
|
+
...(fileParams.values ?? {}),
|
|
157
|
+
...(params.values ?? {}),
|
|
158
|
+
...(fileParams.recipe_dir
|
|
159
|
+
? { recipe_dir: fileParams.recipe_dir }
|
|
160
|
+
: {}),
|
|
161
|
+
...(fileParams.skill_dir ? { skill_dir: fileParams.skill_dir } : {}),
|
|
162
|
+
},
|
|
156
163
|
};
|
|
157
164
|
}
|
|
158
165
|
function readJson(path) {
|
|
@@ -250,13 +257,34 @@ export function startRun(params, cwd) {
|
|
|
250
257
|
!isAbsolute(recipeRelation)
|
|
251
258
|
? dirname(packagedRecipeRoot)
|
|
252
259
|
: undefined;
|
|
253
|
-
const
|
|
260
|
+
const argSpec = Schema.parseToolArgDeclarationList(startParams.args ?? []);
|
|
261
|
+
if (argSpec.error)
|
|
262
|
+
throw new Error(argSpec.error);
|
|
263
|
+
const declaredArgs = new Set(argSpec.args);
|
|
264
|
+
for (const key of Object.keys(startParams.defaults ?? {})) {
|
|
265
|
+
if (declaredArgs.size > 0 && !declaredArgs.has(key))
|
|
266
|
+
throw new Error(`Unknown Recipe default argument: ${key}`);
|
|
267
|
+
}
|
|
268
|
+
const values = Schema.normalizeRuntimeValues({
|
|
269
|
+
...argSpec.defaults,
|
|
270
|
+
...(startParams.defaults || {}),
|
|
254
271
|
...(packagedRepo ? { repo: packagedRepo } : {}),
|
|
255
272
|
...(startParams.values || {}),
|
|
256
273
|
run_id: run,
|
|
257
274
|
state_dir: stateDir,
|
|
258
275
|
trace_file: join(stateDir, "trace.jsonl"),
|
|
259
|
-
};
|
|
276
|
+
}, argSpec.argTypes);
|
|
277
|
+
const runtimeTemplate = resolved.template &&
|
|
278
|
+
typeof resolved.template === "object" &&
|
|
279
|
+
!Array.isArray(resolved.template)
|
|
280
|
+
? {
|
|
281
|
+
...resolved.template,
|
|
282
|
+
defaults: {
|
|
283
|
+
...(resolved.template.defaults ?? {}),
|
|
284
|
+
...values,
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
: resolved.template;
|
|
260
288
|
const modelPolicy = describeCurrentPolicyProvenance({
|
|
261
289
|
defaults: startParams.defaults,
|
|
262
290
|
template: resolved.template,
|
|
@@ -313,7 +341,7 @@ export function startRun(params, cwd) {
|
|
|
313
341
|
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
314
342
|
status: "running",
|
|
315
343
|
...(startParams.tool ? { tool: startParams.tool } : {}),
|
|
316
|
-
template:
|
|
344
|
+
template: runtimeTemplate,
|
|
317
345
|
values,
|
|
318
346
|
model_policy: modelPolicy,
|
|
319
347
|
...(artifacts ? { artifacts } : {}),
|
|
@@ -576,57 +604,36 @@ function stopRun(runOrDir, signal, event, expected = {}) {
|
|
|
576
604
|
status.run_instance_id !== expected.runInstanceId) {
|
|
577
605
|
return { stopped: false, reason: "run generation changed", status };
|
|
578
606
|
}
|
|
579
|
-
const control = event === "run.kill" && typeof status.run_instance_id === "string"
|
|
580
|
-
? appendRunControlInStateDir(stateDir, {
|
|
581
|
-
action: "kill",
|
|
582
|
-
run_instance_id: status.run_instance_id,
|
|
583
|
-
})
|
|
584
|
-
: undefined;
|
|
585
|
-
if (control) {
|
|
586
|
-
updateRunControlStatusInStateDir(stateDir, control.id, "claimed", {}, ["queued"]);
|
|
587
|
-
}
|
|
588
|
-
const finish = (result) => {
|
|
589
|
-
if (!control)
|
|
590
|
-
return result;
|
|
591
|
-
const handled = result.stopped === true;
|
|
592
|
-
updateRunControlStatusInStateDir(stateDir, control.id, handled ? "handled" : "failed", handled ? {} : { error: String(result.reason ?? "kill rejected") }, ["claimed"]);
|
|
593
|
-
return { ...result, control_id: control.id };
|
|
594
|
-
};
|
|
595
607
|
const pid = Number(status.pid || 0);
|
|
596
608
|
if (status.status !== "running" && status.status !== "exited") {
|
|
597
|
-
return
|
|
609
|
+
return { stopped: false, reason: "not running", status };
|
|
598
610
|
}
|
|
599
611
|
const identity = verifyRunProcessIdentity(pid, status.process_identity);
|
|
600
612
|
if (status.status === "exited") {
|
|
601
613
|
if (identity.status === "owner_mismatch" ||
|
|
602
614
|
identity.status === "unsupported_proof") {
|
|
603
|
-
return
|
|
615
|
+
return {
|
|
604
616
|
stopped: false,
|
|
605
617
|
reason: identity.status.replaceAll("_", " "),
|
|
606
618
|
process_identity_status: identity.status,
|
|
607
619
|
status,
|
|
608
|
-
}
|
|
620
|
+
};
|
|
609
621
|
}
|
|
610
|
-
return
|
|
622
|
+
return { stopped: false, reason: "not running", status };
|
|
611
623
|
}
|
|
612
624
|
if (!identity.valid) {
|
|
613
|
-
return
|
|
625
|
+
return {
|
|
614
626
|
stopped: false,
|
|
615
627
|
reason: identity.status.replaceAll("_", " "),
|
|
616
628
|
process_identity_status: identity.status,
|
|
617
629
|
status,
|
|
618
|
-
}
|
|
630
|
+
};
|
|
619
631
|
}
|
|
620
632
|
let signalResult;
|
|
621
633
|
try {
|
|
622
634
|
signalResult = signalOwnedRunProcess(pid, signal, status.process_identity);
|
|
623
635
|
}
|
|
624
636
|
catch (error) {
|
|
625
|
-
if (control) {
|
|
626
|
-
updateRunControlStatusInStateDir(stateDir, control.id, "failed", {
|
|
627
|
-
error: error instanceof Error ? error.message : String(error),
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
637
|
throw error;
|
|
631
638
|
}
|
|
632
639
|
appendRunTraceEvent(stateDir, {
|
|
@@ -643,13 +650,13 @@ function stopRun(runOrDir, signal, event, expected = {}) {
|
|
|
643
650
|
finalizeInterruptedExecution(stateDir, "cancelled", signal);
|
|
644
651
|
markTerminalProgress(stateDir, "cancelled");
|
|
645
652
|
}
|
|
646
|
-
return
|
|
653
|
+
return {
|
|
647
654
|
stopped: true,
|
|
648
655
|
pid,
|
|
649
656
|
signal,
|
|
650
657
|
...signalResult,
|
|
651
658
|
state_dir: stateDir,
|
|
652
|
-
}
|
|
659
|
+
};
|
|
653
660
|
}
|
|
654
661
|
finally {
|
|
655
662
|
releaseControlLock();
|
package/dist/lib/config.js
CHANGED
|
@@ -149,6 +149,7 @@ export function normalizeStoredTool(key, value, reservedToolNames) {
|
|
|
149
149
|
template: argTemplate,
|
|
150
150
|
};
|
|
151
151
|
const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
152
|
+
Schema.assertCompatibleToolArgTypes(inferredArgTypes, declarations.argTypes);
|
|
152
153
|
const argTypes = { ...inferredArgTypes, ...declarations.argTypes };
|
|
153
154
|
const cfg = {
|
|
154
155
|
name,
|
package/dist/lib/file-state.d.ts
CHANGED
|
@@ -12,5 +12,7 @@ export interface FileMutationLockOptions {
|
|
|
12
12
|
}
|
|
13
13
|
export declare function acquireFileMutationLock(path: string, options?: FileMutationLockOptions): () => void;
|
|
14
14
|
export declare function withFileMutationLock<T>(path: string, mutate: () => T, options?: FileMutationLockOptions): T;
|
|
15
|
-
export declare function writeTextAtomic(path: string, content: string
|
|
15
|
+
export declare function writeTextAtomic(path: string, content: string, options?: {
|
|
16
|
+
onBeforeReplace?(): void;
|
|
17
|
+
}): void;
|
|
16
18
|
export declare function writeJsonAtomic(path: string, value: unknown): void;
|
package/dist/lib/file-state.js
CHANGED
|
@@ -8,8 +8,11 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
8
8
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { basename, dirname, join, parse, resolve } from "node:path";
|
|
11
|
-
const FILE_MUTATION_LOCK_TIMEOUT_MS = process.platform === "win32" ?
|
|
11
|
+
const FILE_MUTATION_LOCK_TIMEOUT_MS = process.platform === "win32" ? 30000 : 15000;
|
|
12
12
|
const FILE_MUTATION_LOCK_STALE_MS = 30000;
|
|
13
|
+
const FILE_MUTATION_LOCK_RECLAIM_POLL_MS = 100;
|
|
14
|
+
const FILE_MUTATION_LOCK_REMOVAL_GRACE_MS = 250;
|
|
15
|
+
const FILE_MUTATION_LOCK_MAX_WAIT_MS = 50;
|
|
13
16
|
const FILE_MUTATION_LOCK_ROOT = join(tmpdir(), "pi-actors-file-locks");
|
|
14
17
|
function canonicalMutationPath(path) {
|
|
15
18
|
const absolute = resolve(path);
|
|
@@ -65,7 +68,8 @@ function lockOwnerStatus(lockPath) {
|
|
|
65
68
|
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))
|
|
69
73
|
return "dead";
|
|
70
74
|
return "alive";
|
|
71
75
|
}
|
|
@@ -132,8 +136,10 @@ function tryReclaimRemovalBoundary(reclaimPath) {
|
|
|
132
136
|
if (!existsSync(reclaimPath))
|
|
133
137
|
return;
|
|
134
138
|
try {
|
|
135
|
-
const inspectedToken = readLockToken(reclaimPath);
|
|
136
139
|
const age = Date.now() - statSync(reclaimPath).mtimeMs;
|
|
140
|
+
if (age <= FILE_MUTATION_LOCK_REMOVAL_GRACE_MS)
|
|
141
|
+
return;
|
|
142
|
+
const inspectedToken = readLockToken(reclaimPath);
|
|
137
143
|
const ownerStatus = lockOwnerStatus(reclaimPath);
|
|
138
144
|
if ((ownerStatus === "dead" ||
|
|
139
145
|
(ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
|
|
@@ -200,6 +206,8 @@ export function acquireFileMutationLock(path, options = {}) {
|
|
|
200
206
|
const token = randomUUID();
|
|
201
207
|
const pendingPath = prepareLockBoundary(lockPath, token, options.onBeforeLockPublish);
|
|
202
208
|
let contentionReported = false;
|
|
209
|
+
let nextReclaimAt = 0;
|
|
210
|
+
let waitMs = 10;
|
|
203
211
|
try {
|
|
204
212
|
for (;;) {
|
|
205
213
|
try {
|
|
@@ -211,13 +219,22 @@ export function acquireFileMutationLock(path, options = {}) {
|
|
|
211
219
|
contentionReported = true;
|
|
212
220
|
options.onContention?.();
|
|
213
221
|
}
|
|
214
|
-
|
|
215
|
-
if (
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
if (now >= nextReclaimAt) {
|
|
224
|
+
nextReclaimAt = now + FILE_MUTATION_LOCK_RECLAIM_POLL_MS;
|
|
225
|
+
if (tryReclaimMutationLock(lockPath, options)) {
|
|
226
|
+
waitMs = 10;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (now >= deadline) {
|
|
216
231
|
throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
|
|
217
232
|
cause: error,
|
|
218
233
|
});
|
|
219
234
|
}
|
|
220
|
-
|
|
235
|
+
const jitter = Math.floor(Math.random() * Math.max(1, Math.floor(waitMs / 2)));
|
|
236
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs + jitter);
|
|
237
|
+
waitMs = Math.min(FILE_MUTATION_LOCK_MAX_WAIT_MS, waitMs + 5);
|
|
221
238
|
}
|
|
222
239
|
}
|
|
223
240
|
}
|
|
@@ -256,11 +273,12 @@ export function withFileMutationLock(path, mutate, options = {}) {
|
|
|
256
273
|
release();
|
|
257
274
|
}
|
|
258
275
|
}
|
|
259
|
-
export function writeTextAtomic(path, content) {
|
|
276
|
+
export function writeTextAtomic(path, content, options = {}) {
|
|
260
277
|
mkdirSync(dirname(path), { recursive: true });
|
|
261
278
|
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
262
279
|
try {
|
|
263
280
|
writeFileSync(tempPath, content, "utf8");
|
|
281
|
+
options.onBeforeReplace?.();
|
|
264
282
|
renameSync(tempPath, path);
|
|
265
283
|
}
|
|
266
284
|
catch (error) {
|
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
* Zones: owned actor selection, Recipe/Trace/Control tabs, safe Run Kill confirmation
|
|
4
4
|
* Owns manual kernel navigation; evidence parsing and lifecycle mutation stay in ports.
|
|
5
5
|
*/
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
6
|
+
import { readFileSync, statSync } from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
9
9
|
import * as ActorInspector from "./inspector.js";
|
|
10
10
|
import * as ControlProjection from "./control-projection.js";
|
|
11
11
|
import * as RunsControls from "./runs-controls.js";
|
|
12
12
|
import * as RunControlDelivery from "./runs-control-delivery.js";
|
|
13
|
+
import * as RunsTrace from "./runs-trace.js";
|
|
14
|
+
import * as Limits from "./limits.js";
|
|
15
|
+
import { computeRunControlCapacity } from "./run-evidence-policy.js";
|
|
16
|
+
import * as RuntimeTriage from "./runtime-triage.js";
|
|
13
17
|
import * as TraceProjection from "./trace-projection.js";
|
|
14
18
|
const TABS = ["recipe", "trace", "control"];
|
|
15
19
|
const TRACE_SOURCES = [
|
|
@@ -21,9 +25,6 @@ const TRACE_SOURCES = [
|
|
|
21
25
|
"artifact",
|
|
22
26
|
"runtime",
|
|
23
27
|
];
|
|
24
|
-
function newestTraceItems(items) {
|
|
25
|
-
return [...items].sort((left, right) => right.ts.localeCompare(left.ts) || right.id.localeCompare(left.id));
|
|
26
|
-
}
|
|
27
28
|
export class ActorInspectorOverlay {
|
|
28
29
|
done;
|
|
29
30
|
killRun;
|
|
@@ -410,7 +411,7 @@ export class ActorInspectorOverlay {
|
|
|
410
411
|
`${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
|
|
411
412
|
`${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
|
|
412
413
|
"",
|
|
413
|
-
this.theme.fg("text", "This
|
|
414
|
+
this.theme.fg("text", "This invokes generation-fenced runtime kill."),
|
|
414
415
|
"",
|
|
415
416
|
`${cancel} ${kill}`,
|
|
416
417
|
"",
|
|
@@ -501,8 +502,10 @@ export class ActorInspectorOverlay {
|
|
|
501
502
|
}
|
|
502
503
|
renderTraceList(run) {
|
|
503
504
|
const items = this.traceItems(run);
|
|
505
|
+
const summary = RunsTrace.summarizeRunTraceJournal(RunsTrace.readRunTraceJournal(path.join(this.stateRoot, run.run)));
|
|
506
|
+
const banner = summary.history_complete ? [] : [this.theme.fg("warning", ` ! 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`)];
|
|
504
507
|
if (items.length === 0)
|
|
505
|
-
return [this.theme.fg("muted", " No Trace evidence")];
|
|
508
|
+
return [...banner, this.theme.fg("muted", " No Trace evidence")];
|
|
506
509
|
if (this.selectedTraceId) {
|
|
507
510
|
const selectedIndex = items.findIndex((item) => item.id === this.selectedTraceId);
|
|
508
511
|
if (selectedIndex >= 0)
|
|
@@ -520,24 +523,24 @@ export class ActorInspectorOverlay {
|
|
|
520
523
|
this.contentStripeIndices = items
|
|
521
524
|
.slice(start, start + viewportRows)
|
|
522
525
|
.map((_item, offset) => start + offset);
|
|
523
|
-
return items.slice(start, start + viewportRows).map((item, offset) => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
526
|
+
return [...banner, ...items.slice(start, start + viewportRows - banner.length).map((item, offset) => {
|
|
527
|
+
const index = start + offset;
|
|
528
|
+
const detail = item.detail && typeof item.detail === "object"
|
|
529
|
+
? item.detail
|
|
530
|
+
: {};
|
|
531
|
+
const marker = item.level === "error"
|
|
532
|
+
? this.theme.fg("error", "!")
|
|
533
|
+
: detail.attention
|
|
534
|
+
? this.theme.fg("warning", "A")
|
|
535
|
+
: " ";
|
|
536
|
+
const prefix = this.focus === "list" && index === this.rowIndex
|
|
537
|
+
? this.theme.fg("accent", " ▶ ")
|
|
538
|
+
: " ";
|
|
539
|
+
const row = `${prefix}${this.theme.fg("text", `#${items.length - index}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
|
|
540
|
+
return this.focus === "list" && index === this.rowIndex
|
|
541
|
+
? this.theme.fg("accent", row)
|
|
542
|
+
: row;
|
|
543
|
+
})];
|
|
541
544
|
}
|
|
542
545
|
renderTraceDetail(width) {
|
|
543
546
|
if (!this.traceDetail)
|
|
@@ -553,16 +556,29 @@ export class ActorInspectorOverlay {
|
|
|
553
556
|
? RunControlDelivery.readRunControlEndpoint(stateDir, run.runInstanceId)
|
|
554
557
|
: undefined;
|
|
555
558
|
const meta = this.readRunMeta(stateDir);
|
|
559
|
+
const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
|
|
560
|
+
const capacity = computeRunControlCapacity(journal.records);
|
|
561
|
+
const stalePending = journal.records.reduce((count, control) => count + Number(Boolean(RuntimeTriage.classifyRuntimeControl({ run: run.run,
|
|
562
|
+
runInstanceId: run.runInstanceId, status: run.status }, control, Date.now()).stale)), 0);
|
|
556
563
|
return {
|
|
557
564
|
status: run.status,
|
|
558
565
|
run_instance_id: run.runInstanceId,
|
|
566
|
+
pending: capacity.pending, pending_limit: capacity.limit,
|
|
567
|
+
available: capacity.available, backpressured: capacity.backpressured,
|
|
568
|
+
journal_bytes: (() => { try {
|
|
569
|
+
return statSync(RunsControls.runControlsFile(stateDir)).size;
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return 0;
|
|
573
|
+
} })(),
|
|
574
|
+
journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
|
|
575
|
+
stale_pending: stalePending,
|
|
576
|
+
diagnostics: journal.diagnostics.map((item) => item.line === undefined
|
|
577
|
+
? { reason: "unreadable_control_journal" } : { line: item.line, reason: "invalid_control_json" }),
|
|
559
578
|
actor_actions: Array.isArray(meta.control) ? meta.control : [],
|
|
560
579
|
runtime_actions: run.status === "running" ? ["kill"] : ["archive", "prune"],
|
|
561
580
|
endpoint,
|
|
562
|
-
recent_controls:
|
|
563
|
-
.slice(-20)
|
|
564
|
-
.reverse()
|
|
565
|
-
.map(ControlProjection.projectRunControl),
|
|
581
|
+
recent_controls: journal.records.slice(-20).reverse().map((control) => ControlProjection.projectRunControl(control)),
|
|
566
582
|
};
|
|
567
583
|
}
|
|
568
584
|
allTraceItems(run) {
|
|
@@ -572,10 +588,10 @@ export class ActorInspectorOverlay {
|
|
|
572
588
|
this.traceCache.runInstanceId === run.runInstanceId) {
|
|
573
589
|
return this.traceCache.items;
|
|
574
590
|
}
|
|
575
|
-
const items =
|
|
591
|
+
const items = this.readTrace(path.join(this.stateRoot, run.run), {
|
|
576
592
|
limit: 100,
|
|
577
593
|
source: "all",
|
|
578
|
-
})
|
|
594
|
+
});
|
|
579
595
|
this.traceCache = { run: run.run, runInstanceId: run.runInstanceId, items };
|
|
580
596
|
return items;
|
|
581
597
|
}
|
package/dist/lib/inspector.js
CHANGED
|
@@ -56,7 +56,12 @@ export function readActorInspectorRecipe(stateDir) {
|
|
|
56
56
|
!Array.isArray(primary.recipe)
|
|
57
57
|
? primary.recipe
|
|
58
58
|
: undefined;
|
|
59
|
-
const redactedRecord = (value) => SessionEvidence.redactSessionEvidenceValue(Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined
|
|
59
|
+
const redactedRecord = (value) => SessionEvidence.redactSessionEvidenceValue(Object.fromEntries(Object.entries(value).filter(([key, entry]) => entry !== undefined &&
|
|
60
|
+
key !== "recipe_dir" &&
|
|
61
|
+
key !== "skill_dir")));
|
|
62
|
+
const launchValues = meta.values && typeof meta.values === "object" && !Array.isArray(meta.values)
|
|
63
|
+
? Object.fromEntries(Object.entries(meta.values).filter(([key]) => key !== "recipe_dir" && key !== "skill_dir"))
|
|
64
|
+
: meta.values;
|
|
60
65
|
return {
|
|
61
66
|
composition: contexts.map((entry) => redactedRecord({
|
|
62
67
|
...(typeof entry.alias === "string" ? { alias: entry.alias } : {}),
|
|
@@ -64,6 +69,7 @@ export function readActorInspectorRecipe(stateDir) {
|
|
|
64
69
|
file: entry.file,
|
|
65
70
|
import_path: entry.import_path,
|
|
66
71
|
name: entry.name,
|
|
72
|
+
qualified_name: entry.qualified_name,
|
|
67
73
|
})),
|
|
68
74
|
...(definition ? { definition: redactedRecord(definition) } : {}),
|
|
69
75
|
diagnostics: result.diagnostics.map((diagnostic) => diagnostic.message),
|
|
@@ -76,7 +82,7 @@ export function readActorInspectorRecipe(stateDir) {
|
|
|
76
82
|
launch: redactedRecord({
|
|
77
83
|
cwd: meta.cwd,
|
|
78
84
|
template: meta.template,
|
|
79
|
-
values:
|
|
85
|
+
values: launchValues,
|
|
80
86
|
model_policy: meta.model_policy,
|
|
81
87
|
control: meta.control,
|
|
82
88
|
artifacts: meta.artifacts,
|
package/dist/lib/limits.d.ts
CHANGED
|
@@ -16,3 +16,13 @@ export declare const SESSION_EVIDENCE_TEXT_CHARS = 4000;
|
|
|
16
16
|
export declare const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
|
|
17
17
|
export declare const TRACE_EVENT_MAX_BYTES: number;
|
|
18
18
|
export declare const TRACE_EVENT_MAX_READ = 200;
|
|
19
|
+
export declare const TRACE_JOURNAL_MAX_EVENTS = 2048;
|
|
20
|
+
export declare const TRACE_JOURNAL_TARGET_EVENTS = 1536;
|
|
21
|
+
export declare const TRACE_JOURNAL_MAX_BYTES: number;
|
|
22
|
+
export declare const TRACE_JOURNAL_TARGET_BYTES: number;
|
|
23
|
+
export declare const RUN_CONTROL_PENDING_LIMIT = 64;
|
|
24
|
+
export declare const RUN_CONTROL_TERMINAL_LIMIT = 128;
|
|
25
|
+
export declare const RUN_CONTROL_ERROR_MAX_BYTES: number;
|
|
26
|
+
export declare const RUN_CONTROL_JOURNAL_MAX_BYTES: number;
|
|
27
|
+
export declare const RUN_RETENTION_MAX_RECORDS = 256;
|
|
28
|
+
export declare const RUN_RETENTION_MAX_BYTES: number;
|
package/dist/lib/limits.js
CHANGED
|
@@ -16,3 +16,13 @@ export const SESSION_EVIDENCE_TEXT_CHARS = 4_000;
|
|
|
16
16
|
export const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
|
|
17
17
|
export const TRACE_EVENT_MAX_BYTES = 64 * 1024;
|
|
18
18
|
export const TRACE_EVENT_MAX_READ = 200;
|
|
19
|
+
export const TRACE_JOURNAL_MAX_EVENTS = 2_048;
|
|
20
|
+
export const TRACE_JOURNAL_TARGET_EVENTS = 1_536;
|
|
21
|
+
export const TRACE_JOURNAL_MAX_BYTES = 4 * 1024 * 1024;
|
|
22
|
+
export const TRACE_JOURNAL_TARGET_BYTES = 3 * 1024 * 1024;
|
|
23
|
+
export const RUN_CONTROL_PENDING_LIMIT = 64;
|
|
24
|
+
export const RUN_CONTROL_TERMINAL_LIMIT = 128;
|
|
25
|
+
export const RUN_CONTROL_ERROR_MAX_BYTES = 4 * 1024;
|
|
26
|
+
export const RUN_CONTROL_JOURNAL_MAX_BYTES = 1024 * 1024;
|
|
27
|
+
export const RUN_RETENTION_MAX_RECORDS = 256;
|
|
28
|
+
export const RUN_RETENTION_MAX_BYTES = 1024 * 1024;
|
|
@@ -42,7 +42,7 @@ export interface RunSummary {
|
|
|
42
42
|
}
|
|
43
43
|
export interface RunUiObservationState {
|
|
44
44
|
attentionEventIds: Map<string, Set<string>>;
|
|
45
|
-
|
|
45
|
+
legacyEventLines: Map<string, number>;
|
|
46
46
|
frame: number;
|
|
47
47
|
observed: Map<string, RunObservedStatus>;
|
|
48
48
|
}
|
|
@@ -62,6 +62,7 @@ export interface RunUiNotificationSink {
|
|
|
62
62
|
}): void;
|
|
63
63
|
}
|
|
64
64
|
export declare function createRunUiObservationState(): RunUiObservationState;
|
|
65
|
+
export declare function primeRunAttentionState(state: RunUiObservationState, ownerId: string, stateRoot?: string): void;
|
|
65
66
|
export declare function readRunUiSnapshot(state: RunUiObservationState, ownerId: string, options?: {
|
|
66
67
|
includeAttention?: boolean;
|
|
67
68
|
stateRoot?: string;
|
|
@@ -74,6 +75,7 @@ export declare function reconcileRunTerminalNotifications(input: {
|
|
|
74
75
|
sink: RunUiNotificationSink;
|
|
75
76
|
state: RunUiObservationState;
|
|
76
77
|
stateRoot?: string;
|
|
78
|
+
includeAttention?: boolean;
|
|
77
79
|
}): RunUiSnapshot;
|
|
78
80
|
export declare function deliverRunAttentionNotifications(events: RunAttentionEvent[], sink: RunUiNotificationSink): void;
|
|
79
81
|
export interface RunRetirementCandidate {
|
|
@@ -173,7 +175,7 @@ export declare function findRunRetirementCandidates(summary: RunSummary): RunRet
|
|
|
173
175
|
export declare function executeRunRetirements(summary: RunSummary, options: RunRetirementExecutorOptions): Promise<RunRetirementExecution[]>;
|
|
174
176
|
export declare function detectRunTransitions(previous: Map<string, RunObservedStatus>, summary: RunSummary): RunTransition[];
|
|
175
177
|
export declare function pruneRunObservationState(previousStatuses: Map<string, RunObservedStatus>, previousLineCounts: Map<string, number>, summary: RunSummary, terminalRuns?: Iterable<string>, seenEventIds?: Map<string, Set<string>>): void;
|
|
176
|
-
export declare function detectRunAttentionEvents(
|
|
178
|
+
export declare function detectRunAttentionEvents(legacyLineCounts: Map<string, number>, summary: RunSummary, seenEventIds?: Map<string, Set<string>>, prime?: boolean): RunAttentionEvent[];
|
|
177
179
|
export declare function getRunAttentionNotificationType(event: RunAttentionEvent): RunTransitionNotificationType;
|
|
178
180
|
export declare function shouldNotifyRunAttentionEvent(event: RunAttentionEvent): boolean;
|
|
179
181
|
export declare function shouldSendRunAttentionFollowUp(event: RunAttentionEvent): boolean;
|