@llblab/pi-actors 0.43.0 → 0.43.1

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 (95) hide show
  1. package/AGENTS.md +12 -6
  2. package/BACKLOG.md +189 -1
  3. package/CHANGELOG.md +180 -301
  4. package/README.md +9 -7
  5. package/dist/index.js +1 -1
  6. package/dist/lib/async-runs.d.ts +2 -1
  7. package/dist/lib/async-runs.js +16 -4
  8. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  9. package/dist/lib/automatic-review-runtime.js +5 -5
  10. package/dist/lib/command-templates.d.ts +2 -0
  11. package/dist/lib/command-templates.js +38 -4
  12. package/dist/lib/control-projection.d.ts +20 -0
  13. package/dist/lib/control-projection.js +66 -0
  14. package/dist/lib/control.d.ts +3 -0
  15. package/dist/lib/control.js +27 -14
  16. package/dist/lib/draft-sleep.js +3 -3
  17. package/dist/lib/file-state.d.ts +1 -0
  18. package/dist/lib/file-state.js +98 -42
  19. package/dist/lib/inspector-overlay.d.ts +2 -0
  20. package/dist/lib/inspector-overlay.js +92 -53
  21. package/dist/lib/limits.d.ts +5 -3
  22. package/dist/lib/limits.js +5 -3
  23. package/dist/lib/prompts.d.ts +1 -1
  24. package/dist/lib/prompts.js +1 -1
  25. package/dist/lib/recipe-control.js +6 -2
  26. package/dist/lib/review-control.d.ts +1 -1
  27. package/dist/lib/review-control.js +4 -5
  28. package/dist/lib/runs-control-delivery.d.ts +8 -1
  29. package/dist/lib/runs-control-delivery.js +38 -15
  30. package/dist/lib/runs-controls.d.ts +2 -0
  31. package/dist/lib/runs-controls.js +5 -3
  32. package/dist/lib/runs-trace.d.ts +2 -2
  33. package/dist/lib/runs-trace.js +24 -20
  34. package/dist/lib/runtime-identity.d.ts +7 -0
  35. package/dist/lib/runtime-identity.js +35 -0
  36. package/dist/lib/runtime-triage.d.ts +29 -0
  37. package/dist/lib/runtime-triage.js +76 -0
  38. package/dist/lib/tool-review-scheduler.js +7 -7
  39. package/dist/lib/tools-inspect.js +53 -14
  40. package/dist/lib/tools-message.d.ts +1 -2
  41. package/dist/lib/tools-message.js +6 -6
  42. package/dist/lib/tools-response.d.ts +0 -1
  43. package/dist/lib/tools-response.js +0 -9
  44. package/dist/lib/tools.d.ts +1 -1
  45. package/dist/lib/tools.js +1 -1
  46. package/dist/lib/trace-projection.js +30 -10
  47. package/dist/scripts/locker.mjs +9 -16
  48. package/dist/scripts/music-player.mjs +7 -13
  49. package/dist/scripts/release-gates.mjs +33 -2
  50. package/dist/scripts/validate-recipe.mjs +5 -4
  51. package/dist/skills/actors/SKILL.md +12 -7
  52. package/dist/skills/swarm/SKILL.md +0 -2
  53. package/docs/0.43-baseline.md +18 -23
  54. package/docs/README.md +2 -4
  55. package/docs/actor-inspector.md +5 -5
  56. package/docs/async-runs.md +2 -2
  57. package/docs/command-templates.md +6 -116
  58. package/docs/recipe-library.md +2 -4
  59. package/docs/releasing.md +28 -0
  60. package/docs/template-recipes.md +1 -1
  61. package/docs/tool-registry.md +2 -2
  62. package/index.ts +1 -1
  63. package/lib/async-runs.ts +18 -5
  64. package/lib/automatic-review-runtime.ts +7 -7
  65. package/lib/command-templates.ts +44 -4
  66. package/lib/control-projection.ts +105 -0
  67. package/lib/control.ts +33 -18
  68. package/lib/draft-sleep.ts +3 -3
  69. package/lib/file-state.ts +68 -61
  70. package/lib/inspector-overlay.ts +93 -58
  71. package/lib/limits.ts +5 -3
  72. package/lib/prompts.ts +1 -1
  73. package/lib/recipe-control.ts +9 -2
  74. package/lib/review-control.ts +4 -5
  75. package/lib/runs-control-delivery.ts +45 -17
  76. package/lib/runs-controls.ts +12 -3
  77. package/lib/runs-trace.ts +23 -19
  78. package/lib/runtime-identity.ts +39 -0
  79. package/lib/runtime-triage.ts +120 -0
  80. package/lib/tool-review-scheduler.ts +7 -7
  81. package/lib/tools-inspect.ts +60 -16
  82. package/lib/tools-message.ts +7 -8
  83. package/lib/tools-response.ts +0 -12
  84. package/lib/tools.ts +4 -4
  85. package/lib/trace-projection.ts +33 -10
  86. package/package.json +1 -1
  87. package/scripts/locker.mjs +9 -16
  88. package/scripts/music-player.mjs +7 -13
  89. package/scripts/release-gates.mjs +33 -2
  90. package/scripts/validate-recipe.mjs +5 -4
  91. package/skills/actors/SKILL.md +12 -7
  92. package/skills/swarm/SKILL.md +0 -2
  93. package/docs/actors-deep-reference.md +0 -108
  94. package/docs/component-recipes.md +0 -45
  95. package/docs/task-first-recipes.md +0 -261
@@ -16,6 +16,7 @@ import {
16
16
  } from "@earendil-works/pi-tui";
17
17
 
18
18
  import * as ActorInspector from "./inspector.ts";
19
+ import * as ControlProjection from "./control-projection.ts";
19
20
  import * as RunsControls from "./runs-controls.ts";
20
21
  import * as RunControlDelivery from "./runs-control-delivery.ts";
21
22
  import * as TraceProjection from "./trace-projection.ts";
@@ -58,6 +59,12 @@ const TRACE_SOURCES: TraceProjection.TraceSourceFilter[] = [
58
59
  "runtime",
59
60
  ];
60
61
 
62
+ function newestTraceItems(items: readonly TraceProjection.TraceItem[]): TraceProjection.TraceItem[] {
63
+ return [...items].sort((left, right) =>
64
+ right.ts.localeCompare(left.ts) || right.id.localeCompare(left.id),
65
+ );
66
+ }
67
+
61
68
  export class ActorInspectorOverlay {
62
69
  private readonly done: () => void;
63
70
  private readonly killRun?: (run: string, runInstanceId: string) => ActorInspectorActionResult;
@@ -77,6 +84,7 @@ export class ActorInspectorOverlay {
77
84
  private killDialogChoice: "cancel" | "kill" = "cancel";
78
85
  private rowIndex = 0;
79
86
  private runCache?: ActorInspector.ActorInspectorRunItem[];
87
+ private selectedTraceId?: string;
80
88
  private runIndex = 0;
81
89
  private selectorIndex = 0;
82
90
  private selectorMode?: SelectorMode;
@@ -99,8 +107,7 @@ export class ActorInspectorOverlay {
99
107
  this.theme = options.theme;
100
108
  this.tui = options.tui;
101
109
  this.refreshTimer = setInterval(() => {
102
- this.runCache = undefined;
103
- if (this.focus !== "list" && this.focus !== "detail") this.traceCache = undefined;
110
+ this.invalidate();
104
111
  this.tui.requestRender();
105
112
  }, 1_000);
106
113
  this.refreshTimer.unref?.();
@@ -110,7 +117,10 @@ export class ActorInspectorOverlay {
110
117
  clearInterval(this.refreshTimer);
111
118
  }
112
119
 
113
- invalidate(): void {}
120
+ invalidate(): void {
121
+ this.runCache = undefined;
122
+ this.traceCache = undefined;
123
+ }
114
124
 
115
125
  handleInput(data: string): void {
116
126
  const runs = this.runs();
@@ -134,10 +144,12 @@ export class ActorInspectorOverlay {
134
144
  this.selectorMode = undefined;
135
145
  this.focus = this.tab === "trace" ? "tabs" : "runs";
136
146
  } else if (this.focus === "detail") {
147
+ this.selectedTraceId = this.traceDetail?.id;
137
148
  this.traceDetail = undefined;
138
149
  this.detailScroll = 0;
139
150
  this.focus = "list";
140
151
  } else if (this.focus === "document" || this.focus === "list") {
152
+ if (this.focus === "list") this.selectedTraceId = undefined;
141
153
  this.focus = "tabs";
142
154
  } else {
143
155
  this.done();
@@ -179,19 +191,27 @@ export class ActorInspectorOverlay {
179
191
  } else if (matchesKey(data, "up")) {
180
192
  this.focus = "runs";
181
193
  } else if (matchesKey(data, "down")) {
182
- this.focus = this.tab === "trace" && this.traceItems(runs[this.runIndex]).length > 0
183
- ? "list"
184
- : "document";
194
+ const items = this.tab === "trace" ? this.traceItems(runs[this.runIndex]) : [];
195
+ this.focus = items.length > 0 ? "list" : "document";
196
+ if (this.focus === "list") {
197
+ this.rowIndex = 0;
198
+ this.selectedTraceId = items[0]?.id;
199
+ }
185
200
  } else if (matchesKey(data, "return")) {
186
201
  if (this.tab === "trace") this.openSourceSelector(runs[this.runIndex]);
187
202
  else this.focus = "document";
188
203
  }
189
204
  } else if (this.focus === "list") {
190
- const count = this.traceItems(runs[this.runIndex]).length;
191
- if (matchesKey(data, "left")) this.focus = "tabs";
192
- else if (matchesKey(data, "up")) {
193
- if (this.rowIndex === 0) this.focus = "tabs";
194
- else this.rowIndex -= 1;
205
+ const items = this.traceItems(runs[this.runIndex]);
206
+ const count = items.length;
207
+ if (matchesKey(data, "left")) {
208
+ this.selectedTraceId = undefined;
209
+ this.focus = "tabs";
210
+ } else if (matchesKey(data, "up")) {
211
+ if (this.rowIndex === 0) {
212
+ this.selectedTraceId = undefined;
213
+ this.focus = "tabs";
214
+ } else this.rowIndex -= 1;
195
215
  } else if (matchesKey(data, "down")) {
196
216
  this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + 1);
197
217
  } else if (matchesKey(data, "pageUp")) {
@@ -202,13 +222,14 @@ export class ActorInspectorOverlay {
202
222
  this.rowIndex + this.contentViewportRows(),
203
223
  );
204
224
  } else if (matchesKey(data, "return") || matchesKey(data, "right")) {
205
- const item = this.traceItems(runs[this.runIndex])[this.rowIndex];
225
+ const item = items[this.rowIndex];
206
226
  if (item) {
207
227
  this.traceDetail = item;
208
228
  this.detailScroll = 0;
209
229
  this.focus = "detail";
210
230
  }
211
231
  }
232
+ if (this.focus === "list") this.selectedTraceId = items[this.rowIndex]?.id;
212
233
  }
213
234
  if (data.toLowerCase() === "f" && this.tab === "trace") {
214
235
  this.cycleTraceSource(runs[this.runIndex]);
@@ -273,6 +294,7 @@ export class ActorInspectorOverlay {
273
294
  this.documentScroll = 0;
274
295
  this.detailScroll = 0;
275
296
  this.rowIndex = 0;
297
+ this.selectedTraceId = undefined;
276
298
  this.traceDetail = undefined;
277
299
  }
278
300
 
@@ -291,6 +313,7 @@ export class ActorInspectorOverlay {
291
313
  } else if (matchesKey(data, "pageDown")) {
292
314
  this[key] += this.contentViewportRows();
293
315
  } else if (matchesKey(data, "left") && target === "detail") {
316
+ this.selectedTraceId = this.traceDetail?.id;
294
317
  this.traceDetail = undefined;
295
318
  this.detailScroll = 0;
296
319
  this.focus = "list";
@@ -392,7 +415,9 @@ export class ActorInspectorOverlay {
392
415
  this.killDialogChoice = "cancel";
393
416
  if (!confirmation || !this.killRun) return;
394
417
  try {
395
- this.feedback = this.killRun(confirmation.run, confirmation.runInstanceId);
418
+ const result = this.killRun(confirmation.run, confirmation.runInstanceId);
419
+ this.feedback = result.ok ? undefined : result;
420
+ if (result.ok) this.invalidate();
396
421
  } catch (error) {
397
422
  const message = error instanceof Error ? error.message : String(error);
398
423
  this.feedback = {
@@ -418,7 +443,6 @@ export class ActorInspectorOverlay {
418
443
  `${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
419
444
  "",
420
445
  this.theme.fg("text", "This sends canonical kill Control."),
421
- this.theme.fg("error", "The action is destructive and cannot be undone."),
422
446
  "",
423
447
  `${cancel} ${kill}`,
424
448
  "",
@@ -521,7 +545,15 @@ export class ActorInspectorOverlay {
521
545
  private renderTraceList(run: ActorInspector.ActorInspectorRunItem): string[] {
522
546
  const items = this.traceItems(run);
523
547
  if (items.length === 0) return [this.theme.fg("muted", " No Trace evidence")];
548
+ if (this.selectedTraceId) {
549
+ const selectedIndex = items.findIndex((item) => item.id === this.selectedTraceId);
550
+ if (selectedIndex >= 0) this.rowIndex = selectedIndex;
551
+ else this.selectedTraceId = undefined;
552
+ }
524
553
  this.rowIndex = Math.min(this.rowIndex, items.length - 1);
554
+ if (this.focus === "list" && !this.selectedTraceId) {
555
+ this.selectedTraceId = items[this.rowIndex]?.id;
556
+ }
525
557
  const viewportRows = this.contentViewportRows();
526
558
  const maxStart = Math.max(0, items.length - viewportRows);
527
559
  const start = Math.max(0, Math.min(this.rowIndex - Math.floor(viewportRows / 2), maxStart));
@@ -536,12 +568,12 @@ export class ActorInspectorOverlay {
536
568
  const marker = item.level === "error"
537
569
  ? this.theme.fg("error", "!")
538
570
  : detail.attention
539
- ? this.theme.fg("warning", "•")
540
- : this.theme.fg("muted", "·");
571
+ ? this.theme.fg("warning", "A")
572
+ : " ";
541
573
  const prefix = this.focus === "list" && index === this.rowIndex
542
574
  ? this.theme.fg("accent", " ▶ ")
543
575
  : " ";
544
- const row = `${prefix}${this.theme.fg("text", `#${index + 1}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
576
+ 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)}`;
545
577
  return this.focus === "list" && index === this.rowIndex
546
578
  ? this.theme.fg("accent", row)
547
579
  : row;
@@ -577,7 +609,10 @@ export class ActorInspectorOverlay {
577
609
  actor_actions: Array.isArray(meta.control) ? meta.control : [],
578
610
  runtime_actions: run.status === "running" ? ["kill"] : ["archive", "prune"],
579
611
  endpoint,
580
- recent_controls: RunsControls.readRunControlsFromStateDir(stateDir).slice(-20).reverse(),
612
+ recent_controls: RunsControls.readRunControlsFromStateDir(stateDir)
613
+ .slice(-20)
614
+ .reverse()
615
+ .map(ControlProjection.projectRunControl),
581
616
  };
582
617
  }
583
618
 
@@ -589,10 +624,10 @@ export class ActorInspectorOverlay {
589
624
  ) {
590
625
  return this.traceCache.items;
591
626
  }
592
- const items = this.readTrace(path.join(this.stateRoot, run.run), {
627
+ const items = newestTraceItems(this.readTrace(path.join(this.stateRoot, run.run), {
593
628
  limit: 100,
594
629
  source: "all",
595
- });
630
+ }));
596
631
  this.traceCache = { run: run.run, runInstanceId: run.runInstanceId, items };
597
632
  return items;
598
633
  }
@@ -627,14 +662,9 @@ export class ActorInspectorOverlay {
627
662
  width: number,
628
663
  ): { lines: string[]; stripes: number[] } {
629
664
  const sections = value && typeof value === "object" && !Array.isArray(value)
630
- ? Object.entries(value as Record<string, unknown>).map(([key, item]) => {
631
- const inline = this.inlineDocumentValue(item);
632
- return inline !== undefined
633
- ? [`${key}: ${inline}`]
634
- : item && typeof item === "object"
635
- ? [`${key}:`, ...this.document(item, 1)]
636
- : this.labeledScalarLines(key, item);
637
- })
665
+ ? Object.entries(value as Record<string, unknown>).map(([key, item]) =>
666
+ this.labeledDocumentLines(key, item)
667
+ )
638
668
  : [this.document(value)];
639
669
  const lines: string[] = [];
640
670
  const stripes: number[] = [];
@@ -658,23 +688,22 @@ export class ActorInspectorOverlay {
658
688
  return undefined;
659
689
  }
660
690
  if (value && typeof value === "object") {
661
- const entries = Object.entries(value as Record<string, unknown>);
662
- if (entries.length === 0) return "{}";
663
- if (
664
- entries.length <= 3 &&
665
- entries.every(([, item]) =>
666
- (item === null || typeof item !== "object") &&
667
- !(typeof item === "string" && /[\r\n]/u.test(item))
668
- )
669
- ) {
670
- return `{ ${entries
671
- .map(([key, item]) => `${key}: ${String(item ?? "none")}`)
672
- .join(", ")} }`;
673
- }
691
+ if (Object.keys(value as Record<string, unknown>).length === 0) return "{}";
674
692
  }
675
693
  return undefined;
676
694
  }
677
695
 
696
+ private labeledDocumentLines(key: string, value: unknown, depth = 0): string[] {
697
+ const inline = this.inlineDocumentValue(value);
698
+ if (inline !== undefined) return [`${" ".repeat(depth)}${key}: ${inline}`];
699
+ if (value && typeof value === "object") {
700
+ const indent = " ".repeat(depth);
701
+ const nested = this.document(value, depth);
702
+ return [`${indent}${key}: ${nested[0]!.slice(indent.length)}`, ...nested.slice(1)];
703
+ }
704
+ return this.labeledScalarLines(key, value, depth);
705
+ }
706
+
678
707
  private labeledScalarLines(key: string, value: unknown, depth = 0): string[] {
679
708
  const indent = " ".repeat(depth);
680
709
  const [first = "", ...rest] = String(value ?? "none").split(/\r?\n/u);
@@ -689,25 +718,31 @@ export class ActorInspectorOverlay {
689
718
  const inline = this.inlineDocumentValue(value);
690
719
  if (inline !== undefined) return [`${" ".repeat(depth)}${inline}`];
691
720
  if (Array.isArray(value)) {
692
- return value.flatMap((item, index) => {
693
- const marker = `${" ".repeat(depth)}- #${index + 1}`;
694
- const itemInline = this.inlineDocumentValue(item);
695
- return itemInline !== undefined
696
- ? [`${marker}: ${itemInline}`]
697
- : [marker, ...this.document(item, depth + 1)];
698
- });
721
+ const indent = " ".repeat(depth);
722
+ return [
723
+ `${indent}[`,
724
+ ...value.flatMap((item, index) => {
725
+ const marker = `${indent} - #${index + 1}`;
726
+ const itemInline = this.inlineDocumentValue(item);
727
+ return itemInline !== undefined
728
+ ? [`${marker}: ${itemInline}`]
729
+ : [marker, ...this.document(item, depth + 2)];
730
+ }),
731
+ `${indent}]`,
732
+ ];
699
733
  }
700
734
  if (typeof value === "object") {
701
- return Object.entries(value as Record<string, unknown>).flatMap(([key, item]) => {
702
- const itemInline = this.inlineDocumentValue(item);
703
- if (itemInline !== undefined) {
704
- return [`${" ".repeat(depth)}${key}: ${itemInline}`];
705
- }
706
- if (item && typeof item === "object") {
707
- return [`${" ".repeat(depth)}${key}:`, ...this.document(item, depth + 1)];
708
- }
709
- return this.labeledScalarLines(key, item, depth);
710
- });
735
+ const indent = " ".repeat(depth);
736
+ const entries = Object.entries(value as Record<string, unknown>);
737
+ return [
738
+ `${indent}{`,
739
+ ...entries.flatMap(([key, item], index) => {
740
+ const lines = this.labeledDocumentLines(key, item, depth + 1);
741
+ if (index < entries.length - 1) lines[lines.length - 1] += ",";
742
+ return lines;
743
+ }),
744
+ `${indent}}`,
745
+ ];
711
746
  }
712
747
  return String(value)
713
748
  .split(/\r?\n/u)
package/lib/limits.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  /**
2
- * Shared output and preview size limits.
3
- * Zones: output governance, Trace previews, inspect defaults
2
+ * Shared output, preview, and Control envelope limits.
3
+ * Zones: output governance, Trace previews, Inspect defaults, Control portability
4
4
  */
5
5
 
6
6
  export const DEFAULT_INSPECT_LINES = 40;
7
7
  export const TOOL_OUTPUT_MAX_BYTES = 50 * 1024;
8
8
  export const TOOL_OUTPUT_MAX_LINES = 2_000;
9
9
  export const COMPACT_PREVIEW_CHARS = 160;
10
- export const CONTROL_INPUT_MAX_BYTES = 64 * 1024;
10
+ export const CONTROL_ACTION_MAX_LENGTH = 64;
11
+ export const CONTROL_INPUT_MAX_BYTES = 380;
12
+ export const CONTROL_WIRE_MAX_BYTES = 512;
11
13
  export const INSPECTOR_BODY_PREVIEW_CHARS = 320;
12
14
  export const DOCTOR_ACTION_PREVIEW_CHARS = 72;
13
15
  export const SESSION_EVIDENCE_MAX_TURNS = 100;
package/lib/prompts.ts CHANGED
@@ -22,7 +22,7 @@ export const REGISTER_TOOL_GUIDELINES = [
22
22
  export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
23
23
  - Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
24
24
  - Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
25
- - Command templates stay sync and shell-free: string leaves split into executable + argv, so operators such as && are literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.
25
+ - Command templates stay sync and shell-free: string leaves split into executable + argv, infer .js/.mjs through node→bun→deno run and .sh through bash, and treat operators such as && as literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.
26
26
  - Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
27
27
  - ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.
28
28
  - Recipes own template directly and may declare metadata/defaults/imports/control/artifacts; files >1 MiB or import depth >32 fail closed.
@@ -4,7 +4,9 @@
4
4
  * Owns pure Recipe control validation; Recipe loading and Run capture stay in recipe/run domains.
5
5
  */
6
6
 
7
- const ACTION_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)*$/;
7
+ import * as Control from "./control.ts";
8
+ import * as Limits from "./limits.ts";
9
+
8
10
  const RUNTIME_RUN_ACTIONS = new Set(["archive", "kill", "prune"]);
9
11
 
10
12
  export function normalizeRecipeControl(value: unknown): string[] | undefined {
@@ -19,9 +21,14 @@ export function normalizeRecipeControl(value: unknown): string[] | undefined {
19
21
  throw new Error("recipe.control actions must be non-empty strings");
20
22
  }
21
23
  const action = raw.trim();
22
- if (!ACTION_PATTERN.test(action)) {
24
+ if (!Control.isControlAction(action)) {
23
25
  throw new Error(`invalid recipe.control action: ${action}`);
24
26
  }
27
+ if (action.length > Limits.CONTROL_ACTION_MAX_LENGTH) {
28
+ throw new Error(
29
+ `recipe.control action exceeds ${Limits.CONTROL_ACTION_MAX_LENGTH} ASCII characters: ${action}`,
30
+ );
31
+ }
25
32
  if (RUNTIME_RUN_ACTIONS.has(action)) {
26
33
  throw new Error(
27
34
  `recipe.control action is runtime-reserved and must not be declared: ${action}`,
@@ -124,14 +124,13 @@ export function controlAutomaticReview(
124
124
  return {
125
125
  ...result,
126
126
  next_actions: nextActions,
127
- sent: result.changed === true,
128
127
  };
129
128
  }
130
129
 
131
- export function parseAutomaticReviewScope(body: unknown): AutomaticReviewScope {
132
- const record = body && typeof body === "object" && !Array.isArray(body)
133
- ? body as Record<string, unknown>
130
+ export function parseAutomaticReviewScope(input: unknown): AutomaticReviewScope {
131
+ const record = input && typeof input === "object" && !Array.isArray(input)
132
+ ? input as Record<string, unknown>
134
133
  : {};
135
134
  if (record.scope === "draft" || record.scope === "tool") return record.scope;
136
- throw new Error("review control requires body.scope=draft or body.scope=tool.");
135
+ throw new Error("review Control requires input.scope=draft or input.scope=tool.");
137
136
  }
@@ -15,6 +15,8 @@ import {
15
15
  import { createConnection } from "node:net";
16
16
  import { join } from "node:path";
17
17
 
18
+ import * as Control from "./control.ts";
19
+ import * as Limits from "./limits.ts";
18
20
  import {
19
21
  appendRunControlInStateDir,
20
22
  updateRunControlStatusInStateDir,
@@ -37,7 +39,26 @@ export interface DeliverRunControlRequest {
37
39
  run_instance_id: string;
38
40
  }
39
41
 
40
- export const FIFO_ATOMIC_CONTROL_MAX_BYTES = 512;
42
+ const PREFLIGHT_CONTROL_ID = "00000000-0000-4000-8000-000000000000";
43
+
44
+ export function encodeRunControlWire(wire: {
45
+ action: string;
46
+ id: string;
47
+ input?: unknown;
48
+ }): { bytes: number; payload: string } {
49
+ const payload = `${JSON.stringify({
50
+ id: wire.id,
51
+ action: wire.action,
52
+ ...(wire.input !== undefined ? { input: wire.input } : {}),
53
+ })}\n`;
54
+ const bytes = Buffer.byteLength(payload);
55
+ if (bytes > Limits.CONTROL_WIRE_MAX_BYTES) {
56
+ throw new Error(
57
+ `Run Control wire record exceeds the ${Limits.CONTROL_WIRE_MAX_BYTES}-byte portable bound`,
58
+ );
59
+ }
60
+ return { bytes, payload };
61
+ }
41
62
 
42
63
  export function readRunControlEndpoint(
43
64
  stateDir: string,
@@ -107,7 +128,21 @@ export async function deliverRunControl(
107
128
  request: DeliverRunControlRequest,
108
129
  options: DeliverRunControlOptions = {},
109
130
  ): Promise<Record<string, unknown>> {
110
- const control = appendRunControlInStateDir(stateDir, request);
131
+ const action = Control.normalizeControlAction(request.action);
132
+ const input = request.input === undefined
133
+ ? undefined
134
+ : Control.normalizeControlInput(request.input);
135
+ const normalizedRequest = {
136
+ action,
137
+ ...(input !== undefined ? { input } : {}),
138
+ run_instance_id: request.run_instance_id,
139
+ };
140
+ encodeRunControlWire({
141
+ id: PREFLIGHT_CONTROL_ID,
142
+ action,
143
+ ...(input !== undefined ? { input } : {}),
144
+ });
145
+ const control = appendRunControlInStateDir(stateDir, normalizedRequest);
111
146
  const endpoint = readRunControlEndpoint(stateDir, request.run_instance_id);
112
147
  if (!endpoint) {
113
148
  updateRunControlStatusInStateDir(
@@ -118,31 +153,24 @@ export async function deliverRunControl(
118
153
  ["queued", "delivered"],
119
154
  );
120
155
  throw Object.assign(new Error("Run Control endpoint is not ready."), {
121
- action: request.action,
156
+ action,
122
157
  control_id: control.id,
123
158
  reason: "endpoint_not_ready",
124
159
  run,
125
160
  run_instance_id: request.run_instance_id,
126
161
  });
127
162
  }
128
- const wire = {
129
- id: control.id,
130
- action: request.action,
131
- ...(request.input !== undefined ? { input: request.input } : {}),
132
- };
133
- const payload = `${JSON.stringify(wire)}\n`;
134
- const bytes = Buffer.byteLength(payload);
135
163
  try {
164
+ const { bytes, payload } = encodeRunControlWire({
165
+ id: control.id,
166
+ action,
167
+ ...(input !== undefined ? { input } : {}),
168
+ });
136
169
  let written: number;
137
170
  if (endpoint.type === "fifo") {
138
171
  if ((options.platform ?? process.platform) === "win32") {
139
172
  throw new Error("FIFO Control delivery is unsupported on native Windows");
140
173
  }
141
- if (bytes > FIFO_ATOMIC_CONTROL_MAX_BYTES) {
142
- throw new Error(
143
- `FIFO Control payload exceeds the ${FIFO_ATOMIC_CONTROL_MAX_BYTES}-byte portable atomic-write bound`,
144
- );
145
- }
146
174
  written = sendToFifo(endpoint, payload);
147
175
  } else {
148
176
  written = await sendToNamedPipe(endpoint, payload, options.namedPipeSend);
@@ -152,7 +180,7 @@ export async function deliverRunControl(
152
180
  }
153
181
  updateRunControlStatusInStateDir(stateDir, control.id, "delivered");
154
182
  return {
155
- action: request.action,
183
+ action,
156
184
  bytes,
157
185
  control_id: control.id,
158
186
  delivery: "delivered",
@@ -170,7 +198,7 @@ export async function deliverRunControl(
170
198
  ["queued", "delivered"],
171
199
  );
172
200
  throw Object.assign(new Error(`Run Control delivery failed: ${reason}`), {
173
- action: request.action,
201
+ action,
174
202
  control_id: control.id,
175
203
  endpoint_type: endpoint.type,
176
204
  reason: "delivery_failed",
@@ -9,7 +9,11 @@ import { writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
 
11
11
  import { acquireFileMutationLock, writeTextAtomic } from "./file-state.ts";
12
- import { readJsonlFileResilient } from "./state-readers.ts";
12
+ import {
13
+ readJsonlFileResilient,
14
+ type JsonlReadResult,
15
+ } from "./state-readers.ts";
16
+
13
17
  export const RUN_CONTROL_TERMINAL_LIMIT = 128;
14
18
 
15
19
  export type RunControlStatus =
@@ -45,9 +49,14 @@ export function runControlsFile(stateDir: string): string {
45
49
  return join(stateDir, "controls.jsonl");
46
50
  }
47
51
 
52
+ export function readRunControlJournalFromStateDir(
53
+ stateDir: string,
54
+ ): JsonlReadResult<unknown> {
55
+ return readJsonlFileResilient<unknown>(runControlsFile(stateDir));
56
+ }
57
+
48
58
  export function readRunControlsFromStateDir(stateDir: string): RunControlRecord[] {
49
- return readJsonlFileResilient<RunControlRecord>(runControlsFile(stateDir))
50
- .records;
59
+ return readRunControlJournalFromStateDir(stateDir).records as RunControlRecord[];
51
60
  }
52
61
 
53
62
  function acquireRunControlsLock(stateDir: string): () => void {
package/lib/runs-trace.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Run Trace event journal.
3
- * Zones: structured event validation, bounded append, resilient bounded reads
4
- * Owns Run-local semantic Trace events; lifecycle projection and owner attention delivery stay in adapters.
3
+ * Zones: structured event validation, token-locked append-only writes, resilient bounded reads
4
+ * Owns canonical Run-local Trace persistence; lifecycle projection and owner attention delivery stay in adapters.
5
5
  */
6
6
 
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
 
11
+ import { withFileMutationLock } from "./file-state.ts";
11
12
  import * as Limits from "./limits.ts";
12
13
  import { readJsonlFileResilient } from "./state-readers.ts";
13
14
 
@@ -104,23 +105,26 @@ export function appendRunTraceEvent(
104
105
  stateDir: string,
105
106
  input: AppendTraceEventInput,
106
107
  ): TraceEvent {
107
- const normalized = normalizeTraceEventInput(input);
108
- const event: TraceEvent = {
109
- id: randomUUID(),
110
- ts: new Date().toISOString(),
111
- ...normalized,
112
- };
113
- let encoded: string;
114
- try {
115
- encoded = JSON.stringify(event);
116
- } catch {
117
- throw new Error("Trace event data must be JSON-serializable");
118
- }
119
- if (Buffer.byteLength(encoded) > Limits.TRACE_EVENT_MAX_BYTES) {
120
- throw new Error(`Trace event exceeds ${Limits.TRACE_EVENT_MAX_BYTES} bytes`);
121
- }
122
- writeFileSync(runTraceFile(stateDir), `${encoded}\n`, { flag: "a" });
123
- return event;
108
+ const path = runTraceFile(stateDir);
109
+ return withFileMutationLock(path, () => {
110
+ const normalized = normalizeTraceEventInput(input);
111
+ const event: TraceEvent = {
112
+ id: randomUUID(),
113
+ ts: new Date().toISOString(),
114
+ ...normalized,
115
+ };
116
+ let encoded: string;
117
+ try {
118
+ encoded = JSON.stringify(event);
119
+ } catch {
120
+ throw new Error("Trace event data must be JSON-serializable");
121
+ }
122
+ if (Buffer.byteLength(encoded) > Limits.TRACE_EVENT_MAX_BYTES) {
123
+ throw new Error(`Trace event exceeds ${Limits.TRACE_EVENT_MAX_BYTES} bytes`);
124
+ }
125
+ writeFileSync(path, `${encoded}\n`, { flag: "a" });
126
+ return event;
127
+ });
124
128
  }
125
129
 
126
130
  export function readRunTraceEvents(
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Immutable runtime package identity.
3
+ * Zones: source/installed package version resolution and Run-state schema marker
4
+ * Owns package-local identity reads; runtime status and Run persistence remain in their domains.
5
+ */
6
+
7
+ import { readFileSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ export const RUN_STATE_SCHEMA = "run-kernel-v1" as const;
12
+
13
+ let cachedVersion: string | undefined;
14
+
15
+ export function getPackageVersion(): string {
16
+ if (cachedVersion) return cachedVersion;
17
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
18
+ for (const path of [
19
+ join(moduleDir, "..", "package.json"),
20
+ join(moduleDir, "..", "..", "package.json"),
21
+ ]) {
22
+ try {
23
+ const pkg = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
24
+ if (
25
+ pkg.name === "@llblab/pi-actors" &&
26
+ typeof pkg.version === "string" &&
27
+ pkg.version.length > 0 &&
28
+ pkg.version.length <= 128 &&
29
+ pkg.version.trim() === pkg.version
30
+ ) {
31
+ cachedVersion = pkg.version;
32
+ return cachedVersion;
33
+ }
34
+ } catch {
35
+ // Source and installed-dist modes resolve through different package ancestors.
36
+ }
37
+ }
38
+ throw new Error("pi-actors package identity is unavailable");
39
+ }