@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.
Files changed (79) hide show
  1. package/AGENTS.md +6 -6
  2. package/BACKLOG.md +1 -189
  3. package/CHANGELOG.md +419 -402
  4. package/README.md +8 -6
  5. package/dist/index.js +7 -3
  6. package/dist/lib/async-runs.d.ts +2 -0
  7. package/dist/lib/async-runs.js +41 -34
  8. package/dist/lib/config.js +1 -0
  9. package/dist/lib/file-state.d.ts +3 -1
  10. package/dist/lib/file-state.js +25 -7
  11. package/dist/lib/inspector-overlay.js +46 -30
  12. package/dist/lib/inspector.js +8 -2
  13. package/dist/lib/limits.d.ts +10 -0
  14. package/dist/lib/limits.js +10 -0
  15. package/dist/lib/observability.d.ts +4 -2
  16. package/dist/lib/observability.js +43 -36
  17. package/dist/lib/recipes-discovery.js +20 -3
  18. package/dist/lib/recipes-references.d.ts +10 -0
  19. package/dist/lib/recipes-references.js +168 -5
  20. package/dist/lib/run-evidence-policy.d.ts +95 -0
  21. package/dist/lib/run-evidence-policy.js +177 -0
  22. package/dist/lib/run-ui-runtime.js +2 -0
  23. package/dist/lib/runs-controls.d.ts +7 -5
  24. package/dist/lib/runs-controls.js +186 -49
  25. package/dist/lib/runs-retention.js +27 -14
  26. package/dist/lib/runs-trace.d.ts +25 -1
  27. package/dist/lib/runs-trace.js +410 -21
  28. package/dist/lib/runtime-triage.js +6 -22
  29. package/dist/lib/schema.d.ts +3 -0
  30. package/dist/lib/schema.js +34 -1
  31. package/dist/lib/tools-inspect.js +58 -14
  32. package/dist/lib/tools-local.js +27 -10
  33. package/dist/lib/tools-spawn.js +2 -2
  34. package/dist/lib/trace-projection.js +90 -44
  35. package/dist/scripts/conformance.mjs +5 -0
  36. package/dist/scripts/locker.mjs +31 -74
  37. package/dist/scripts/music-player.mjs +41 -129
  38. package/dist/scripts/release-gates.mjs +26 -4
  39. package/dist/skills/actors/SKILL.md +9 -8
  40. package/dist/skills/swarm/SKILL.md +2 -2
  41. package/docs/README.md +0 -1
  42. package/docs/actor-inspector.md +3 -2
  43. package/docs/async-runs.md +10 -8
  44. package/docs/command-templates.md +2 -2
  45. package/docs/recipe-library.md +4 -2
  46. package/docs/template-recipes.md +10 -4
  47. package/docs/tool-registry.md +1 -1
  48. package/index.ts +9 -3
  49. package/lib/async-runs.ts +51 -54
  50. package/lib/config.ts +4 -0
  51. package/lib/file-state.ts +28 -7
  52. package/lib/inspector-overlay.ts +29 -17
  53. package/lib/inspector.ts +16 -2
  54. package/lib/limits.ts +10 -0
  55. package/lib/observability.ts +55 -57
  56. package/lib/recipes-discovery.ts +23 -3
  57. package/lib/recipes-references.ts +227 -7
  58. package/lib/run-evidence-policy.ts +242 -0
  59. package/lib/run-ui-runtime.ts +2 -0
  60. package/lib/runs-controls.ts +177 -108
  61. package/lib/runs-retention.ts +28 -20
  62. package/lib/runs-trace.ts +496 -21
  63. package/lib/runtime-triage.ts +11 -25
  64. package/lib/schema.ts +53 -1
  65. package/lib/tools-inspect.ts +56 -15
  66. package/lib/tools-local.ts +49 -19
  67. package/lib/tools-spawn.ts +2 -2
  68. package/lib/trace-projection.ts +137 -75
  69. package/package.json +1 -1
  70. package/scripts/conformance.mjs +5 -0
  71. package/scripts/locker.mjs +31 -74
  72. package/scripts/music-player.mjs +41 -129
  73. package/scripts/release-gates.mjs +26 -4
  74. package/skills/actors/SKILL.md +9 -8
  75. package/skills/swarm/SKILL.md +2 -2
  76. package/dist/lib/runtime-notifier.d.ts +0 -48
  77. package/dist/lib/runtime-notifier.js +0 -138
  78. package/docs/0.43-baseline.md +0 -39
  79. 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 typed defaults;
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. The canonical append authority validates and size-checks under a cross-process mutation lock before one append-only JSONL write; first-party scripts never append this file directly.
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. Token-owned dead-process-reclaiming locks serialize atomic journal replacements. Every record carries the immutable `run_instance_id`; expected-status fencing advances outcomes monotonically through queued/delivered/claimed/handled/failed evidence, while a fast consumer may claim or handle before the sender adds independent delivery-time evidence.
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), and [0.43 baseline](./docs/0.43-baseline.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
- systemPrompt: `${event.systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
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(),
@@ -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;
@@ -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: { ...(fileParams.values ?? {}), ...(params.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 values = {
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: resolved.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 finish({ stopped: false, reason: "not running", status });
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 finish({
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 finish({ stopped: false, reason: "not running", status });
622
+ return { stopped: false, reason: "not running", status };
611
623
  }
612
624
  if (!identity.valid) {
613
- return finish({
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 finish({
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();
@@ -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,
@@ -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): void;
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;
@@ -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" ? 15000 : 5000;
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 > 50 && isZombieProcess(pid))
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
- tryReclaimMutationLock(lockPath, options);
215
- if (Date.now() >= deadline) {
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
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
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 sends canonical kill Control."),
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
- const index = start + offset;
525
- const detail = item.detail && typeof item.detail === "object"
526
- ? item.detail
527
- : {};
528
- const marker = item.level === "error"
529
- ? this.theme.fg("error", "!")
530
- : detail.attention
531
- ? this.theme.fg("warning", "A")
532
- : " ";
533
- const prefix = this.focus === "list" && index === this.rowIndex
534
- ? this.theme.fg("accent", " ▶ ")
535
- : " ";
536
- 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)}`;
537
- return this.focus === "list" && index === this.rowIndex
538
- ? this.theme.fg("accent", row)
539
- : row;
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: RunsControls.readRunControlsFromStateDir(stateDir)
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 = newestTraceItems(this.readTrace(path.join(this.stateRoot, run.run), {
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
  }
@@ -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: meta.values,
85
+ values: launchValues,
80
86
  model_policy: meta.model_policy,
81
87
  control: meta.control,
82
88
  artifacts: meta.artifacts,
@@ -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;
@@ -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
- eventLines: Map<string, number>;
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(previousLineCounts: Map<string, number>, summary: RunSummary, seenEventIds?: Map<string, Set<string>>): RunAttentionEvent[];
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;