@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
@@ -15,6 +15,11 @@ const conformanceSuites = [
15
15
  "tests/control.test.ts",
16
16
  "tests/runs-controls.test.ts",
17
17
  "tests/runs-trace.test.ts",
18
+ "tests/observability.test.ts",
19
+ "tests/async-run-control.test.ts",
20
+ "tests/run-kernel-dogfood.test.ts",
21
+ "tests/installed.scripts.test.ts",
22
+ "tests/inspector-overlay.test.ts",
18
23
  "tests/tools-inspect-kernel.test.ts",
19
24
  "tests/recipes-discovery.test.ts",
20
25
  "tests/review-swarm-dogfood.test.ts",
@@ -11,7 +11,6 @@
11
11
  import { spawnSync } from "node:child_process";
12
12
  import { createHash } from "node:crypto";
13
13
  import {
14
- appendFileSync,
15
14
  existsSync,
16
15
  mkdirSync,
17
16
  readFileSync,
@@ -32,8 +31,12 @@ async function importRuntimeModule(name) {
32
31
  return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
33
32
  }
34
33
 
35
- const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
36
- await importRuntimeModule("file-state");
34
+ const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } = await importRuntimeModule("file-state");
35
+ const { readJsonlFileResilient } = await importRuntimeModule("state-readers");
36
+ const LOCKER_JOURNAL_MAX_RECORDS = 512;
37
+ const LOCKER_JOURNAL_MAX_BYTES = 1024 * 1024;
38
+ const { claimRunControlByIdInStateDir, updateRunControlStatusInStateDir } =
39
+ await importRuntimeModule("runs-controls");
37
40
  const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
38
41
 
39
42
  function parseArgs(argv) {
@@ -59,7 +62,6 @@ async function runLocker(argv = process.argv.slice(2)) {
59
62
  const queuePath = join(stateDir, "queue.json");
60
63
  const locksPath = join(stateDir, "locks.json");
61
64
  const journalPath = join(stateDir, "journal.jsonl");
62
- const controlsPath = join(stateDir, "controls.jsonl");
63
65
  const controlPath = join(stateDir, "control.fifo");
64
66
  let runInstanceId;
65
67
  mkdirSync(stateDir, { recursive: true });
@@ -96,10 +98,22 @@ async function runLocker(argv = process.argv.slice(2)) {
96
98
  }
97
99
 
98
100
  function journal(event, data = {}) {
99
- appendFileSync(
100
- journalPath,
101
- `${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`,
102
- );
101
+ const release = acquireFileMutationLock(journalPath);
102
+ try {
103
+ const records = [...readJsonlFileResilient(journalPath).records,
104
+ { event, ts: new Date().toISOString(), ...data }]
105
+ .slice(-LOCKER_JOURNAL_MAX_RECORDS);
106
+ let content = encodeJournal(records);
107
+ while (records.length && Buffer.byteLength(content) > LOCKER_JOURNAL_MAX_BYTES) {
108
+ records.shift();
109
+ content = encodeJournal(records);
110
+ }
111
+ writeTextAtomic(journalPath, content);
112
+ } finally { release(); }
113
+ }
114
+
115
+ function encodeJournal(records) {
116
+ return records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "";
103
117
  }
104
118
 
105
119
  function emitTrace(kind, summary, data = {}, level = "info") {
@@ -147,69 +161,6 @@ async function runLocker(argv = process.argv.slice(2)) {
147
161
  }
148
162
  }
149
163
 
150
- function acquireControlsLock() {
151
- return acquireFileMutationLock(controlsPath);
152
- }
153
-
154
- function readControls() {
155
- if (!existsSync(controlsPath)) return [];
156
- return readFileSync(controlsPath, "utf8")
157
- .split("\n")
158
- .filter((line) => line.trim())
159
- .map((line) => {
160
- try {
161
- return JSON.parse(line);
162
- } catch {
163
- return undefined;
164
- }
165
- })
166
- .filter(Boolean);
167
- }
168
-
169
- function writeControls(controls) {
170
- writeTextAtomic(
171
- controlsPath,
172
- controls.length
173
- ? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
174
- : "",
175
- );
176
- }
177
-
178
- function claimControl(id) {
179
- const release = acquireControlsLock();
180
- try {
181
- const controls = readControls();
182
- const control = controls.find((item) => item.id === id);
183
- if (
184
- !control ||
185
- control.run_instance_id !== runInstanceId ||
186
- (control.status !== "queued" && control.status !== "delivered")
187
- ) {
188
- return undefined;
189
- }
190
- control.claimed_at = new Date().toISOString();
191
- control.status = "claimed";
192
- writeControls(controls);
193
- return { ...control };
194
- } finally {
195
- release();
196
- }
197
- }
198
-
199
- function finalizeControl(id, status, error) {
200
- const release = acquireControlsLock();
201
- try {
202
- const controls = readControls();
203
- const control = controls.find((item) => item.id === id);
204
- if (!control || control.status !== "claimed") return;
205
- control.status = status;
206
- control[`${status}_at`] = new Date().toISOString();
207
- if (error) control.error = error;
208
- writeControls(controls);
209
- } finally {
210
- release();
211
- }
212
- }
213
164
 
214
165
  function tailJournal(count) {
215
166
  if (!existsSync(journalPath)) return [];
@@ -397,7 +348,9 @@ async function runLocker(argv = process.argv.slice(2)) {
397
348
  function handleLine(line) {
398
349
  const control = normalizeControl(line);
399
350
  if (!control) return false;
400
- const claimed = claimControl(control.id);
351
+ const claimed = claimRunControlByIdInStateDir(
352
+ stateDir, runInstanceId, control.id,
353
+ );
401
354
  if (!claimed) {
402
355
  emitTrace(
403
356
  "lock.control_rejected",
@@ -409,11 +362,15 @@ async function runLocker(argv = process.argv.slice(2)) {
409
362
  }
410
363
  try {
411
364
  const stopping = handle(claimed) === true;
412
- finalizeControl(claimed.id, "handled");
365
+ updateRunControlStatusInStateDir(
366
+ stateDir, claimed.id, "handled", {}, ["claimed"],
367
+ );
413
368
  return stopping;
414
369
  } catch (error) {
415
370
  const text = error instanceof Error ? error.message : String(error);
416
- finalizeControl(claimed.id, "failed", text);
371
+ updateRunControlStatusInStateDir(
372
+ stateDir, claimed.id, "failed", { error: text }, ["claimed"],
373
+ );
417
374
  journal("lock.error", { error: text });
418
375
  emitTrace("lock.error", text, { error: text }, "error");
419
376
  return false;
@@ -13,7 +13,6 @@
13
13
  */
14
14
 
15
15
  import { spawn, spawnSync } from "node:child_process";
16
- import { randomUUID } from "node:crypto";
17
16
  import {
18
17
  accessSync,
19
18
  constants,
@@ -50,8 +49,8 @@ async function importRuntimeModule(name) {
50
49
  return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
51
50
  }
52
51
 
53
- const { acquireFileMutationLock, writeTextAtomic } =
54
- await importRuntimeModule("file-state");
52
+ const { appendRunControlInStateDir, claimRunControlByIdInStateDir,
53
+ updateRunControlStatusInStateDir } = await importRuntimeModule("runs-controls");
55
54
  const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
56
55
 
57
56
  const AUDIO_EXTENSIONS = new Set([
@@ -408,8 +407,8 @@ function playerCommand(ctx, player, volume, track) {
408
407
  }
409
408
  }
410
409
 
411
- function writeText(path, value, flag = "w") {
412
- writeFileSync(path, value, { encoding: "utf8", flag });
410
+ function writeText(path, value) {
411
+ writeFileSync(path, value, "utf8");
413
412
  }
414
413
 
415
414
  function readText(path) {
@@ -584,7 +583,24 @@ async function startControlServer(ctx, wakeControlLoop) {
584
583
  : join(ctx.stateDir, "control.sock");
585
584
  if (process.platform !== "win32") rmSync(path, { force: true });
586
585
  const server = createServer((socket) => {
587
- socket.on("data", wakeControlLoop);
586
+ let content = "";
587
+ socket.setEncoding("utf8");
588
+ socket.on("data", (chunk) => { content += chunk; });
589
+ socket.on("end", () => {
590
+ for (const line of content.split("\n")) {
591
+ if (!line.trim()) continue;
592
+ try {
593
+ const claimed = claimControl(ctx, JSON.parse(line));
594
+ if (!claimed) continue;
595
+ handleControl(ctx, claimed.command);
596
+ finalizeControl(ctx, claimed.id, "handled");
597
+ } catch (error) {
598
+ const id = (() => { try { return JSON.parse(line).id; } catch { return undefined; } })();
599
+ finalizeControl(ctx, id, "failed", error instanceof Error ? error.message : String(error));
600
+ }
601
+ }
602
+ wakeControlLoop();
603
+ });
588
604
  socket.resume();
589
605
  });
590
606
  await new Promise((resolveReady, rejectReady) => {
@@ -606,133 +622,38 @@ async function startControlServer(ctx, wakeControlLoop) {
606
622
  };
607
623
  }
608
624
 
609
- function acquireControlsLock(ctx) {
610
- return acquireFileMutationLock(ctx.controlsFile);
611
- }
612
-
613
- function readControls(ctx) {
614
- if (!exists(ctx.controlsFile)) return [];
615
- return readFileSync(ctx.controlsFile, "utf8")
616
- .split("\n")
617
- .filter((line) => line.trim())
618
- .map((line) => {
619
- try {
620
- return JSON.parse(line);
621
- } catch {
622
- return undefined;
623
- }
624
- })
625
- .filter(Boolean);
626
- }
627
-
628
- function writeControls(ctx, controls) {
629
- writeTextAtomic(
630
- ctx.controlsFile,
631
- controls.length
632
- ? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
633
- : "",
634
- );
635
- }
636
-
637
625
  function commandFromControl(control) {
638
626
  if (typeof control.action !== "string") return undefined;
639
627
  const action = control.action.trim();
640
628
  return CONTROL_COMMANDS.has(action) ? action : undefined;
641
629
  }
642
630
 
643
- function runtimeWakeFile(ctx) {
644
- return join(ctx.stateDir, "wake.jsonl");
645
- }
646
-
647
- function notifyControlWake(ctx, reason = "control.queued") {
648
- try {
649
- writeText(
650
- runtimeWakeFile(ctx),
651
- `${JSON.stringify({
652
- actor: `run:${basename(ctx.stateDir)}`,
653
- id: randomUUID(),
654
- metadata: { command: "music-player" },
655
- reason,
656
- state_dir: ctx.stateDir,
657
- ts: new Date().toISOString(),
658
- })}\n`,
659
- "a",
660
- );
661
- } catch {
662
- // Wake records are advisory; the Control journal remains authoritative.
663
- }
664
- }
665
-
666
631
  function appendControl(ctx, action) {
667
- const release = acquireControlsLock(ctx);
668
- try {
669
- const run = readJsonFile(runJsonFile(ctx), {});
670
- const controls = readControls(ctx);
671
- controls.push({
672
- action,
673
- id: randomUUID(),
674
- queued_at: new Date().toISOString(),
675
- run_instance_id: run.run_instance_id,
676
- status: "queued",
677
- });
678
- writeControls(ctx, controls);
679
- } finally {
680
- release();
681
- }
682
- notifyControlWake(ctx);
632
+ const run = readJsonFile(runJsonFile(ctx), {});
633
+ appendRunControlInStateDir(ctx.stateDir, {
634
+ action,
635
+ run_instance_id: run.run_instance_id,
636
+ });
683
637
  }
684
638
 
685
- function claimControls(ctx) {
686
- const release = acquireControlsLock(ctx);
687
- try {
688
- const controls = readControls(ctx);
689
- const commands = [];
690
- let changed = false;
691
- const claimedAt = new Date().toISOString();
692
- for (const control of controls) {
693
- if (control.status !== "queued" && control.status !== "delivered") continue;
694
- const command =
695
- control.run_instance_id === ctx.runInstanceId
696
- ? commandFromControl(control)
697
- : undefined;
698
- if (!command) {
699
- control.failed_at = claimedAt;
700
- control.status = "failed";
701
- control.error = "Unsupported or stale music-player Control";
702
- changed = true;
703
- continue;
704
- }
705
- control.claimed_at = claimedAt;
706
- control.status = "claimed";
707
- commands.push({ command, id: control.id });
708
- changed = true;
709
- }
710
- if (changed) writeControls(ctx, controls);
711
- return commands;
712
- } finally {
713
- release();
639
+ function claimControl(ctx, wire) {
640
+ const control = claimRunControlByIdInStateDir(
641
+ ctx.stateDir, ctx.runInstanceId, wire.id,
642
+ );
643
+ const command = control ? commandFromControl(control) : undefined;
644
+ if (!command && control) {
645
+ updateRunControlStatusInStateDir(ctx.stateDir, control.id, "failed", {
646
+ error: "Unsupported music-player Control",
647
+ }, ["claimed"]);
714
648
  }
649
+ return command && control ? { command, id: control.id } : undefined;
715
650
  }
716
651
 
717
652
  function finalizeControl(ctx, id, status, error) {
718
653
  if (!id) return;
719
- const release = acquireControlsLock(ctx);
720
- try {
721
- const controls = readControls(ctx);
722
- const timestamp = new Date().toISOString();
723
- let changed = false;
724
- for (const control of controls) {
725
- if (control.id !== id || control.status !== "claimed") continue;
726
- control.status = status;
727
- if (status === "handled") control.handled_at = timestamp;
728
- else control.failed_at = timestamp;
729
- if (error) control.error = error;
730
- changed = true;
731
- }
732
- if (changed) writeControls(ctx, controls);
733
- } finally {
734
- release();
735
- }
654
+ updateRunControlStatusInStateDir(
655
+ ctx.stateDir, id, status, error ? { error } : {}, ["claimed"],
656
+ );
736
657
  }
737
658
 
738
659
  function controlsSignature(ctx) {
@@ -754,8 +675,7 @@ function startControlLoop(ctx) {
754
675
  const name = file ? String(file) : "";
755
676
  if (
756
677
  !name ||
757
- name === basename(ctx.controlsFile) ||
758
- name === basename(runtimeWakeFile(ctx))
678
+ name === basename(ctx.controlsFile)
759
679
  ) {
760
680
  dirty = true;
761
681
  }
@@ -774,14 +694,6 @@ function startControlLoop(ctx) {
774
694
  const signature = controlsSignature(ctx);
775
695
  if (dirty || signature !== lastSignature) {
776
696
  dirty = false;
777
- for (const { command, id } of claimControls(ctx)) {
778
- try {
779
- handleControl(ctx, command);
780
- finalizeControl(ctx, id, "handled");
781
- } catch (error) {
782
- finalizeControl(ctx, id, "failed", error.message);
783
- }
784
- }
785
697
  lastSignature = controlsSignature(ctx);
786
698
  continue;
787
699
  }
@@ -165,8 +165,30 @@ try {
165
165
  text.includes('importRuntimeModule("runs-trace")') && text.includes("appendRunTraceEvent"),
166
166
  `first-party Trace writer bypasses canonical runtime module: ${path}`,
167
167
  );
168
+ if (path !== "scripts/async-runner.mjs") check(
169
+ text.includes('importRuntimeModule("runs-controls")') &&
170
+ !/function (?:read|write|claimControls|finalizeControls)\b/u.test(text),
171
+ `first-party Control writer bypasses canonical runtime module: ${path}`,
172
+ );
168
173
  }
169
- console.log("[release] canonical Trace writer residue checked");
174
+ console.log("[release] canonical Trace and Control writer residue checked");
175
+
176
+ // owner | class | lock | record bound | byte bound | recovery
177
+ const appendOnlyOwners = new Set([
178
+ "lib/runs-trace.ts", // canonical Run evidence | token lock | 2048 | 4 MiB | compact valid suffix
179
+ "lib/runs-retention.ts", // shared kernel evidence | token lock | 256 | 1 MiB | skip malformed
180
+ "scripts/locker.mjs", // first-party actor state | token lock | 512 | 1 MiB | skip malformed
181
+ "lib/command-templates.ts", // complete execution capture | command owner | n/a | artifact policy | preserve
182
+ "scripts/async-runner.mjs", // captures/prompts | Run owner | n/a | artifact policy | preserve
183
+ "scripts/recipe-utils.mjs", // user-declared artifact | caller owner | n/a | caller policy | preserve
184
+ "scripts/release-gates.mjs", // build/test-only source inventory | n/a | n/a | n/a | self-match
185
+ ]);
186
+ const appendOnlyPattern = /\bappendFileSync\b|\b(?:writeFileSync|writeTextAtomic)\s*\([^)]*\{\s*flag:\s*["']a["']/su;
187
+ for (const path of files.filter((candidate) => /^(?:lib\/.*\.ts|scripts\/.*\.mjs)$/u.test(candidate)))
188
+ if (appendOnlyPattern.test(stagedText(path) ?? ""))
189
+ check(appendOnlyOwners.has(path), `unregistered shipped append-only writer: ${path}`);
190
+ for (const path of appendOnlyOwners) check(fileSet.has(path), `stale append-only owner inventory: ${path}`);
191
+ console.log(`[release] append-only owner inventory checked: ${appendOnlyOwners.size} source owners`);
170
192
 
171
193
  check(
172
194
  (stagedText("scripts/validate-recipe.mjs") ?? "").includes(
@@ -176,13 +198,13 @@ try {
176
198
  );
177
199
  console.log("[release] zero-warning Recipe QA gate checked");
178
200
 
179
- const baselineShippedLines = 28_853;
201
+ const maximumShippedLines = 29_767;
180
202
  const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
181
203
  const shippedLines = files
182
204
  .filter((path) => shippedPath.test(path))
183
205
  .reduce((total, path) => total + (((stagedText(path) ?? "").match(/\n/gu) ?? []).length + 1), 0);
184
- check(shippedLines < baselineShippedLines, `shipped lines ${shippedLines} are not below baseline ${baselineShippedLines}`);
185
- console.log(`[release] shipped lines ${shippedLines} < released baseline ${baselineShippedLines}`);
206
+ check(shippedLines <= maximumShippedLines, `shipped lines ${shippedLines} exceed release maximum ${maximumShippedLines}`);
207
+ console.log(`[release] shipped lines ${shippedLines} <= release maximum ${maximumShippedLines}`);
186
208
 
187
209
  const sources = files.filter((path) => path === "index.ts" || (path.startsWith("lib/") && path.endsWith(".ts")));
188
210
  const sourceSet = new Set(sources);
@@ -25,11 +25,11 @@ A Run target exposes exactly three inspect views: `recipe`, `trace`, and `contro
25
25
 
26
26
  ## Recipe
27
27
 
28
- A Recipe defines execution. It may declare args, defaults, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
28
+ A Recipe defines execution. It may declare named typed args, inline fallbacks, configuration `defaults`, composition `values`, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
29
29
 
30
30
  Do not declare Control for ordinary one-shot work. Runtime lifecycle actions such as `kill` stay runtime-owned and must not appear in Recipe Control declarations. Imported Recipes act as local definitions inside one Run; they do not create nested Runs unless execution explicitly spawns them.
31
31
 
32
- Prefer maintained packaged Recipes over ad hoc wrappers. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
32
+ Prefer maintained packaged Recipes over ad hoc wrappers. Use `std:<recipe>` for exact packaged lookup and `skill:<skill>/<recipe-path>` for a component bundled with a Pi-active Skill. File-backed Recipes own `{recipe_dir}` and Skill Recipes own `{skill_dir}`; callers never pass or override these origins. Skill Recipes are components, not automatic tools. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
33
33
 
34
34
  ## Trace
35
35
 
@@ -47,7 +47,7 @@ Trace records bounded structured observations in `trace.jsonl`:
47
47
  }
48
48
  ```
49
49
 
50
- Trace never carries sender, recipient, route, reply, or message-envelope fields. First-party writers use the canonical append authority, which validates and size-checks under a token-owned cross-process lock before one append-only JSONL write. Use `attention: "notify"` for visible notification and `attention: "followup"` only when the coordinator must receive semantic follow-up context. Prefer artifacts or complete execution captures for large evidence.
50
+ Trace never carries sender, recipient, route, reply, or message-envelope fields. It is a bounded retained suffix: the canonical lock 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; terminal/result/execution/artifact evidence stays independently authoritative. `inspect view=trace` reports completeness. Equal timestamps use same-source physical order, fixed source rank, then stable id without exposing an ordinal or claiming cross-source causality. Attention is a wake hint, not a queue: persist durable state or an artifact first, use `notify` for visible status, and reserve `followup` for needed coordinator context. Compaction may discard old hints.
51
51
 
52
52
  ## Control
53
53
 
@@ -57,9 +57,9 @@ The public Control request is exact:
57
57
  { "target": "run:<id>", "action": "pause", "input": {}, "verbose": false }
58
58
  ```
59
59
 
60
- Valid Controls persist in `controls.jsonl` before delivery; invalid envelopes remain outside the journal. Token-owned locks serialize atomic journal replacements. Service endpoints publish readiness in `control-endpoint.json` with the immutable startup `run_instance_id`; only FIFO and named-pipe endpoints transport Controls. Both transports share one portable envelope: action is at most 64 lowercase ASCII characters, serialized JSON input is at most 380 bytes, and the newline-terminated wire record is at most 512 bytes. Partial writes fail, and controlled FIFO readers remain gap-free across writers. Put larger data in a declared artifact/path and send only its bounded reference or instruction through Control. Delivery revalidates owner, generation, running state, and process identity under the lifecycle lock.
60
+ Valid Controls persist in `controls.jsonl` before delivery; invalid envelopes remain outside it. 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 queued record. Exact-id claims/finalization preserve a 128-terminal tail, expected-state fencing, and 4 KiB errors. Admitted nonterminal Controls never expire automatically. `inspect view=control` reports capacity, saturation, stale work, bytes, and diagnostics. Endpoints carry immutable startup `run_instance_id`; FIFO and named pipe share limits of 64 action characters, 380 serialized input bytes, and 512 newline-terminated wire bytes. Partial writes fail. Put larger data in an artifact and send only its reference. Delivery revalidates owner, generation, state, and process identity.
61
61
 
62
- `kill` remains a runtime lifecycle action. Use an actor-local action such as `stop` only when the Recipe declares and implements it.
62
+ `kill` remains the runtime recovery path for a stuck saturated Run: it bypasses actor-local Control capacity and adds no synthetic Control. Use actor-local `stop` only when declared and implemented. Restart clears generation-local evidence; archive preserves the bounded terminal tree, while prune preserves only requested artifacts.
63
63
 
64
64
  ## Run State and Safety
65
65
 
@@ -72,6 +72,8 @@ Run state lives under `~/.pi/agent/tmp/pi-actors/runs/<run>/`. Important evidenc
72
72
  - `execution.json`: command/session provenance and bounded complete-capture references.
73
73
  - `result.json`, logs, and declared artifacts.
74
74
 
75
+ Trace/Control quotas do not constrain user-declared artifacts, repositories, media sources, complete captures, or actor-owned workload state. No public noun, tool, target, or view is added by bounded retention.
76
+
75
77
  Never bypass owner filtering, immutable generation fencing, process-identity verification, path containment, redaction, terminal reconciliation, or shutdown kill behavior. Do not edit active Run state to force a result.
76
78
 
77
79
  ## Operating Pattern
@@ -79,8 +81,8 @@ Never bypass owner filtering, immutable generation fencing, process-identity ver
79
81
  1. Inspect the Recipe before launch when its contract or policy matters.
80
82
  2. Spawn with explicit values and retain the returned `run:<id>`.
81
83
  3. Let short Runs finish; avoid polling.
82
- 4. Inspect Trace when evidence or attention requires it.
83
- 5. Send only declared actor-local Controls.
84
+ 4. Inspect Trace when evidence or attention requires it; its summary states whether retained history is complete.
85
+ 5. Inspect Control capacity before diagnosing stale work or saturation, then send only declared actor-local Controls.
84
86
  6. Use runtime kill/cancel behavior for lifecycle termination.
85
87
  7. Inspect artifacts and execution evidence for final validation.
86
88
 
@@ -98,6 +100,5 @@ If work may outlive the current turn, needs steering, produces artifacts, fans o
98
100
 
99
101
  - [Recipe library](../../docs/recipe-library.md)
100
102
  - [Async Runs](../../docs/async-runs.md)
101
- - [Baseline and preservation gates](../../docs/0.43-baseline.md)
102
103
 
103
104
  Read repository source and tests for exact contracts when changing pi-actors itself. Update this skill whenever durable Run mechanics change.
@@ -265,9 +265,9 @@ Report white spots, contradictions, evidence, and risks.
265
265
 
266
266
  Detached execution is an adapter concern, not a portable Swarm-script requirement. When the host offers Runs, launch the composed Recipe, return its id, and rely on terminal follow-up rather than blocking or polling.
267
267
 
268
- `Progress contract`: expose bounded structured Trace, logs, artifacts, status, timestamps, and final result evidence. Read these through Run inspection rather than scraping process output.
268
+ `Progress contract`: expose bounded structured Trace, logs, artifacts, status, timestamps, and final result evidence. Treat Trace as a retained suffix and attention as a wake hint; write durable task cards, checkpoints, and large evidence to artifacts before signaling attention. Read completeness and Control capacity through existing Run inspection rather than scraping output. Artifact size/lifecycle remains separate from Trace/Control quotas.
269
269
 
270
- `Resumable checkpoint goal`: a controlled agent-backed Run may preserve context and accept a declared Control. When the host cannot preserve context, write a handoff artifact and launch a clean-context Run while marking the context loss explicitly.
270
+ `Resumable checkpoint goal`: a controlled agent-backed Run may preserve context and accept a declared Control. Control saturation rejects before admission and admitted work does not expire; lifecycle recovery remains host-owned. When the host cannot preserve context, write a handoff artifact and launch a clean-context Run while marking the context loss explicitly.
271
271
 
272
272
  `Cancellation boundary`: terminate only an owned active generation whose process identity the runtime can prove. Stale pid reuse must fail closed.
273
273
 
@@ -1,48 +0,0 @@
1
- /**
2
- * Runtime wake notifications for actor state.
3
- * Zones: advisory wake layer, file-backed runtime state, cross-platform notification boundary
4
- * Owns best-effort live wake signals while durable Run state remains canonical.
5
- */
6
- export interface RuntimeWakeEvent {
7
- actor: string;
8
- id: string;
9
- metadata?: Record<string, unknown>;
10
- reason: string;
11
- state_dir: string;
12
- ts: string;
13
- }
14
- export interface RuntimeNotifierSubscription {
15
- close(): void;
16
- }
17
- export type RuntimeReconcileReason = "initial" | "poll" | "wake";
18
- export interface RuntimeReconcileEvent {
19
- actor: string;
20
- reason: RuntimeReconcileReason;
21
- state_dir: string;
22
- ts: string;
23
- }
24
- export interface RuntimeNotifierSubscribeOptions {
25
- onReconcile?: (event: RuntimeReconcileEvent) => void;
26
- }
27
- export interface FileRuntimeNotifierOptions {
28
- pollIntervalMs?: number;
29
- replay?: boolean;
30
- watch?: boolean;
31
- }
32
- export interface RuntimeNotifier {
33
- notify(event: {
34
- actor: string;
35
- metadata?: Record<string, unknown>;
36
- reason: string;
37
- }): RuntimeWakeEvent;
38
- subscribe(actor: string, onWake: (event: RuntimeWakeEvent) => void, options?: RuntimeNotifierSubscribeOptions): RuntimeNotifierSubscription;
39
- }
40
- export declare function runtimeWakeFile(stateDir: string): string;
41
- export declare function notifyRuntimeWake(stateDir: string, event: {
42
- actor: string;
43
- metadata?: Record<string, unknown>;
44
- reason: string;
45
- }): RuntimeWakeEvent;
46
- export declare function parseRuntimeWakeEventLine(line: string): RuntimeWakeEvent | undefined;
47
- export declare function readRuntimeWakeEvents(stateDir: string): RuntimeWakeEvent[];
48
- export declare function createFileRuntimeNotifier(stateDir: string, options?: FileRuntimeNotifierOptions): RuntimeNotifier;