@llblab/pi-actors 0.43.0 → 0.44.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 (109) hide show
  1. package/AGENTS.md +16 -10
  2. package/CHANGELOG.md +425 -536
  3. package/README.md +13 -11
  4. package/dist/index.js +1 -1
  5. package/dist/lib/async-runs.d.ts +2 -1
  6. package/dist/lib/async-runs.js +24 -34
  7. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  8. package/dist/lib/automatic-review-runtime.js +5 -5
  9. package/dist/lib/command-templates.d.ts +2 -0
  10. package/dist/lib/command-templates.js +38 -4
  11. package/dist/lib/control-projection.d.ts +20 -0
  12. package/dist/lib/control-projection.js +66 -0
  13. package/dist/lib/control.d.ts +3 -0
  14. package/dist/lib/control.js +27 -14
  15. package/dist/lib/draft-sleep.js +3 -3
  16. package/dist/lib/file-state.d.ts +4 -1
  17. package/dist/lib/file-state.js +118 -44
  18. package/dist/lib/inspector-overlay.d.ts +2 -0
  19. package/dist/lib/inspector-overlay.js +124 -69
  20. package/dist/lib/limits.d.ts +15 -3
  21. package/dist/lib/limits.js +15 -3
  22. package/dist/lib/observability.d.ts +4 -2
  23. package/dist/lib/observability.js +43 -36
  24. package/dist/lib/prompts.d.ts +1 -1
  25. package/dist/lib/prompts.js +1 -1
  26. package/dist/lib/recipe-control.js +6 -2
  27. package/dist/lib/review-control.d.ts +1 -1
  28. package/dist/lib/review-control.js +4 -5
  29. package/dist/lib/run-evidence-policy.d.ts +95 -0
  30. package/dist/lib/run-evidence-policy.js +177 -0
  31. package/dist/lib/run-ui-runtime.js +2 -0
  32. package/dist/lib/runs-control-delivery.d.ts +8 -1
  33. package/dist/lib/runs-control-delivery.js +38 -15
  34. package/dist/lib/runs-controls.d.ts +8 -4
  35. package/dist/lib/runs-controls.js +189 -50
  36. package/dist/lib/runs-retention.js +27 -14
  37. package/dist/lib/runs-trace.d.ts +26 -2
  38. package/dist/lib/runs-trace.js +411 -18
  39. package/dist/lib/runtime-identity.d.ts +7 -0
  40. package/dist/lib/runtime-identity.js +35 -0
  41. package/dist/lib/runtime-triage.d.ts +29 -0
  42. package/dist/lib/runtime-triage.js +60 -0
  43. package/dist/lib/tool-review-scheduler.js +7 -7
  44. package/dist/lib/tools-inspect.js +91 -18
  45. package/dist/lib/tools-message.d.ts +1 -2
  46. package/dist/lib/tools-message.js +6 -6
  47. package/dist/lib/tools-response.d.ts +0 -1
  48. package/dist/lib/tools-response.js +0 -9
  49. package/dist/lib/tools.d.ts +1 -1
  50. package/dist/lib/tools.js +1 -1
  51. package/dist/lib/trace-projection.js +107 -41
  52. package/dist/scripts/conformance.mjs +5 -0
  53. package/dist/scripts/locker.mjs +40 -90
  54. package/dist/scripts/music-player.mjs +48 -142
  55. package/dist/scripts/release-gates.mjs +56 -3
  56. package/dist/scripts/validate-recipe.mjs +5 -4
  57. package/dist/skills/actors/SKILL.md +17 -11
  58. package/dist/skills/swarm/SKILL.md +2 -4
  59. package/docs/README.md +1 -4
  60. package/docs/actor-inspector.md +6 -5
  61. package/docs/async-runs.md +11 -9
  62. package/docs/command-templates.md +6 -116
  63. package/docs/recipe-library.md +4 -6
  64. package/docs/releasing.md +28 -0
  65. package/docs/template-recipes.md +1 -1
  66. package/docs/tool-registry.md +2 -2
  67. package/index.ts +1 -1
  68. package/lib/async-runs.ts +26 -50
  69. package/lib/automatic-review-runtime.ts +7 -7
  70. package/lib/command-templates.ts +44 -4
  71. package/lib/control-projection.ts +105 -0
  72. package/lib/control.ts +33 -18
  73. package/lib/draft-sleep.ts +3 -3
  74. package/lib/file-state.ts +91 -63
  75. package/lib/inspector-overlay.ts +108 -61
  76. package/lib/limits.ts +15 -3
  77. package/lib/observability.ts +55 -57
  78. package/lib/prompts.ts +1 -1
  79. package/lib/recipe-control.ts +9 -2
  80. package/lib/review-control.ts +4 -5
  81. package/lib/run-evidence-policy.ts +242 -0
  82. package/lib/run-ui-runtime.ts +2 -0
  83. package/lib/runs-control-delivery.ts +45 -17
  84. package/lib/runs-controls.ts +180 -102
  85. package/lib/runs-retention.ts +28 -20
  86. package/lib/runs-trace.ts +499 -20
  87. package/lib/runtime-identity.ts +39 -0
  88. package/lib/runtime-triage.ts +106 -0
  89. package/lib/tool-review-scheduler.ts +7 -7
  90. package/lib/tools-inspect.ts +94 -20
  91. package/lib/tools-message.ts +7 -8
  92. package/lib/tools-response.ts +0 -12
  93. package/lib/tools.ts +4 -4
  94. package/lib/trace-projection.ts +156 -71
  95. package/package.json +1 -1
  96. package/scripts/conformance.mjs +5 -0
  97. package/scripts/locker.mjs +40 -90
  98. package/scripts/music-player.mjs +48 -142
  99. package/scripts/release-gates.mjs +56 -3
  100. package/scripts/validate-recipe.mjs +5 -4
  101. package/skills/actors/SKILL.md +17 -11
  102. package/skills/swarm/SKILL.md +2 -4
  103. package/dist/lib/runtime-notifier.d.ts +0 -48
  104. package/dist/lib/runtime-notifier.js +0 -138
  105. package/docs/0.43-baseline.md +0 -44
  106. package/docs/actors-deep-reference.md +0 -108
  107. package/docs/component-recipes.md +0 -45
  108. package/docs/task-first-recipes.md +0 -261
  109. package/lib/runtime-notifier.ts +0 -211
@@ -4,7 +4,7 @@
4
4
  * Owns manual kernel navigation; evidence parsing and lifecycle mutation stay in ports.
5
5
  */
6
6
 
7
- import { readFileSync } from "node:fs";
7
+ import { readFileSync, statSync } from "node:fs";
8
8
  import * as path from "node:path";
9
9
 
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
@@ -16,8 +16,13 @@ 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";
22
+ import * as RunsTrace from "./runs-trace.ts";
23
+ import * as Limits from "./limits.ts";
24
+ import { computeRunControlCapacity } from "./run-evidence-policy.ts";
25
+ import * as RuntimeTriage from "./runtime-triage.ts";
21
26
  import * as TraceProjection from "./trace-projection.ts";
22
27
 
23
28
  export type ActorInspectorTab = "recipe" | "trace" | "control";
@@ -77,6 +82,7 @@ export class ActorInspectorOverlay {
77
82
  private killDialogChoice: "cancel" | "kill" = "cancel";
78
83
  private rowIndex = 0;
79
84
  private runCache?: ActorInspector.ActorInspectorRunItem[];
85
+ private selectedTraceId?: string;
80
86
  private runIndex = 0;
81
87
  private selectorIndex = 0;
82
88
  private selectorMode?: SelectorMode;
@@ -99,8 +105,7 @@ export class ActorInspectorOverlay {
99
105
  this.theme = options.theme;
100
106
  this.tui = options.tui;
101
107
  this.refreshTimer = setInterval(() => {
102
- this.runCache = undefined;
103
- if (this.focus !== "list" && this.focus !== "detail") this.traceCache = undefined;
108
+ this.invalidate();
104
109
  this.tui.requestRender();
105
110
  }, 1_000);
106
111
  this.refreshTimer.unref?.();
@@ -110,7 +115,10 @@ export class ActorInspectorOverlay {
110
115
  clearInterval(this.refreshTimer);
111
116
  }
112
117
 
113
- invalidate(): void {}
118
+ invalidate(): void {
119
+ this.runCache = undefined;
120
+ this.traceCache = undefined;
121
+ }
114
122
 
115
123
  handleInput(data: string): void {
116
124
  const runs = this.runs();
@@ -134,10 +142,12 @@ export class ActorInspectorOverlay {
134
142
  this.selectorMode = undefined;
135
143
  this.focus = this.tab === "trace" ? "tabs" : "runs";
136
144
  } else if (this.focus === "detail") {
145
+ this.selectedTraceId = this.traceDetail?.id;
137
146
  this.traceDetail = undefined;
138
147
  this.detailScroll = 0;
139
148
  this.focus = "list";
140
149
  } else if (this.focus === "document" || this.focus === "list") {
150
+ if (this.focus === "list") this.selectedTraceId = undefined;
141
151
  this.focus = "tabs";
142
152
  } else {
143
153
  this.done();
@@ -179,19 +189,27 @@ export class ActorInspectorOverlay {
179
189
  } else if (matchesKey(data, "up")) {
180
190
  this.focus = "runs";
181
191
  } else if (matchesKey(data, "down")) {
182
- this.focus = this.tab === "trace" && this.traceItems(runs[this.runIndex]).length > 0
183
- ? "list"
184
- : "document";
192
+ const items = this.tab === "trace" ? this.traceItems(runs[this.runIndex]) : [];
193
+ this.focus = items.length > 0 ? "list" : "document";
194
+ if (this.focus === "list") {
195
+ this.rowIndex = 0;
196
+ this.selectedTraceId = items[0]?.id;
197
+ }
185
198
  } else if (matchesKey(data, "return")) {
186
199
  if (this.tab === "trace") this.openSourceSelector(runs[this.runIndex]);
187
200
  else this.focus = "document";
188
201
  }
189
202
  } 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;
203
+ const items = this.traceItems(runs[this.runIndex]);
204
+ const count = items.length;
205
+ if (matchesKey(data, "left")) {
206
+ this.selectedTraceId = undefined;
207
+ this.focus = "tabs";
208
+ } else if (matchesKey(data, "up")) {
209
+ if (this.rowIndex === 0) {
210
+ this.selectedTraceId = undefined;
211
+ this.focus = "tabs";
212
+ } else this.rowIndex -= 1;
195
213
  } else if (matchesKey(data, "down")) {
196
214
  this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + 1);
197
215
  } else if (matchesKey(data, "pageUp")) {
@@ -202,13 +220,14 @@ export class ActorInspectorOverlay {
202
220
  this.rowIndex + this.contentViewportRows(),
203
221
  );
204
222
  } else if (matchesKey(data, "return") || matchesKey(data, "right")) {
205
- const item = this.traceItems(runs[this.runIndex])[this.rowIndex];
223
+ const item = items[this.rowIndex];
206
224
  if (item) {
207
225
  this.traceDetail = item;
208
226
  this.detailScroll = 0;
209
227
  this.focus = "detail";
210
228
  }
211
229
  }
230
+ if (this.focus === "list") this.selectedTraceId = items[this.rowIndex]?.id;
212
231
  }
213
232
  if (data.toLowerCase() === "f" && this.tab === "trace") {
214
233
  this.cycleTraceSource(runs[this.runIndex]);
@@ -273,6 +292,7 @@ export class ActorInspectorOverlay {
273
292
  this.documentScroll = 0;
274
293
  this.detailScroll = 0;
275
294
  this.rowIndex = 0;
295
+ this.selectedTraceId = undefined;
276
296
  this.traceDetail = undefined;
277
297
  }
278
298
 
@@ -291,6 +311,7 @@ export class ActorInspectorOverlay {
291
311
  } else if (matchesKey(data, "pageDown")) {
292
312
  this[key] += this.contentViewportRows();
293
313
  } else if (matchesKey(data, "left") && target === "detail") {
314
+ this.selectedTraceId = this.traceDetail?.id;
294
315
  this.traceDetail = undefined;
295
316
  this.detailScroll = 0;
296
317
  this.focus = "list";
@@ -392,7 +413,9 @@ export class ActorInspectorOverlay {
392
413
  this.killDialogChoice = "cancel";
393
414
  if (!confirmation || !this.killRun) return;
394
415
  try {
395
- this.feedback = this.killRun(confirmation.run, confirmation.runInstanceId);
416
+ const result = this.killRun(confirmation.run, confirmation.runInstanceId);
417
+ this.feedback = result.ok ? undefined : result;
418
+ if (result.ok) this.invalidate();
396
419
  } catch (error) {
397
420
  const message = error instanceof Error ? error.message : String(error);
398
421
  this.feedback = {
@@ -417,8 +440,7 @@ export class ActorInspectorOverlay {
417
440
  `${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
418
441
  `${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
419
442
  "",
420
- this.theme.fg("text", "This sends canonical kill Control."),
421
- this.theme.fg("error", "The action is destructive and cannot be undone."),
443
+ this.theme.fg("text", "This invokes generation-fenced runtime kill."),
422
444
  "",
423
445
  `${cancel} ${kill}`,
424
446
  "",
@@ -520,15 +542,27 @@ export class ActorInspectorOverlay {
520
542
 
521
543
  private renderTraceList(run: ActorInspector.ActorInspectorRunItem): string[] {
522
544
  const items = this.traceItems(run);
523
- if (items.length === 0) return [this.theme.fg("muted", " No Trace evidence")];
545
+ const summary = RunsTrace.summarizeRunTraceJournal(
546
+ RunsTrace.readRunTraceJournal(path.join(this.stateRoot, run.run)));
547
+ const banner = summary.history_complete ? [] : [this.theme.fg("warning",
548
+ ` ! Trace history incomplete: ${summary.compacted ? `${summary.compactions_total} compaction(s), ` : ""}${summary.dropped_event_count_exact ? summary.dropped_events : "unknown"} dropped event(s), ${summary.dropped_bytes} bytes`)];
549
+ if (items.length === 0) return [...banner, this.theme.fg("muted", " No Trace evidence")];
550
+ if (this.selectedTraceId) {
551
+ const selectedIndex = items.findIndex((item) => item.id === this.selectedTraceId);
552
+ if (selectedIndex >= 0) this.rowIndex = selectedIndex;
553
+ else this.selectedTraceId = undefined;
554
+ }
524
555
  this.rowIndex = Math.min(this.rowIndex, items.length - 1);
556
+ if (this.focus === "list" && !this.selectedTraceId) {
557
+ this.selectedTraceId = items[this.rowIndex]?.id;
558
+ }
525
559
  const viewportRows = this.contentViewportRows();
526
560
  const maxStart = Math.max(0, items.length - viewportRows);
527
561
  const start = Math.max(0, Math.min(this.rowIndex - Math.floor(viewportRows / 2), maxStart));
528
562
  this.contentStripeIndices = items
529
563
  .slice(start, start + viewportRows)
530
564
  .map((_item, offset) => start + offset);
531
- return items.slice(start, start + viewportRows).map((item, offset) => {
565
+ return [...banner, ...items.slice(start, start + viewportRows - banner.length).map((item, offset) => {
532
566
  const index = start + offset;
533
567
  const detail = item.detail && typeof item.detail === "object"
534
568
  ? item.detail as Record<string, unknown>
@@ -536,16 +570,16 @@ export class ActorInspectorOverlay {
536
570
  const marker = item.level === "error"
537
571
  ? this.theme.fg("error", "!")
538
572
  : detail.attention
539
- ? this.theme.fg("warning", "•")
540
- : this.theme.fg("muted", "·");
573
+ ? this.theme.fg("warning", "A")
574
+ : " ";
541
575
  const prefix = this.focus === "list" && index === this.rowIndex
542
576
  ? this.theme.fg("accent", " ▶ ")
543
577
  : " ";
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)}`;
578
+ 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
579
  return this.focus === "list" && index === this.rowIndex
546
580
  ? this.theme.fg("accent", row)
547
581
  : row;
548
- });
582
+ })];
549
583
  }
550
584
 
551
585
  private renderTraceDetail(width: number): string[] {
@@ -571,13 +605,26 @@ export class ActorInspectorOverlay {
571
605
  ? RunControlDelivery.readRunControlEndpoint(stateDir, run.runInstanceId)
572
606
  : undefined;
573
607
  const meta = this.readRunMeta(stateDir);
608
+ const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
609
+ const capacity = computeRunControlCapacity(journal.records);
610
+ const stalePending = journal.records.reduce<number>((count, control) => count + Number(Boolean(
611
+ RuntimeTriage.classifyRuntimeControl({ run: run.run,
612
+ runInstanceId: run.runInstanceId, status: run.status }, control, Date.now()).stale)), 0);
574
613
  return {
575
614
  status: run.status,
576
615
  run_instance_id: run.runInstanceId,
616
+ pending: capacity.pending, pending_limit: capacity.limit,
617
+ available: capacity.available, backpressured: capacity.backpressured,
618
+ journal_bytes: (() => { try { return statSync(RunsControls.runControlsFile(stateDir)).size; } catch { return 0; } })(),
619
+ journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
620
+ stale_pending: stalePending,
621
+ diagnostics: journal.diagnostics.map((item) => item.line === undefined
622
+ ? { reason: "unreadable_control_journal" } : { line: item.line, reason: "invalid_control_json" }),
577
623
  actor_actions: Array.isArray(meta.control) ? meta.control : [],
578
624
  runtime_actions: run.status === "running" ? ["kill"] : ["archive", "prune"],
579
625
  endpoint,
580
- recent_controls: RunsControls.readRunControlsFromStateDir(stateDir).slice(-20).reverse(),
626
+ recent_controls: journal.records.slice(-20).reverse().map((control) =>
627
+ ControlProjection.projectRunControl(control as RunsControls.RunControlRecord)),
581
628
  };
582
629
  }
583
630
 
@@ -627,14 +674,9 @@ export class ActorInspectorOverlay {
627
674
  width: number,
628
675
  ): { lines: string[]; stripes: number[] } {
629
676
  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
- })
677
+ ? Object.entries(value as Record<string, unknown>).map(([key, item]) =>
678
+ this.labeledDocumentLines(key, item)
679
+ )
638
680
  : [this.document(value)];
639
681
  const lines: string[] = [];
640
682
  const stripes: number[] = [];
@@ -658,23 +700,22 @@ export class ActorInspectorOverlay {
658
700
  return undefined;
659
701
  }
660
702
  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
- }
703
+ if (Object.keys(value as Record<string, unknown>).length === 0) return "{}";
674
704
  }
675
705
  return undefined;
676
706
  }
677
707
 
708
+ private labeledDocumentLines(key: string, value: unknown, depth = 0): string[] {
709
+ const inline = this.inlineDocumentValue(value);
710
+ if (inline !== undefined) return [`${" ".repeat(depth)}${key}: ${inline}`];
711
+ if (value && typeof value === "object") {
712
+ const indent = " ".repeat(depth);
713
+ const nested = this.document(value, depth);
714
+ return [`${indent}${key}: ${nested[0]!.slice(indent.length)}`, ...nested.slice(1)];
715
+ }
716
+ return this.labeledScalarLines(key, value, depth);
717
+ }
718
+
678
719
  private labeledScalarLines(key: string, value: unknown, depth = 0): string[] {
679
720
  const indent = " ".repeat(depth);
680
721
  const [first = "", ...rest] = String(value ?? "none").split(/\r?\n/u);
@@ -689,25 +730,31 @@ export class ActorInspectorOverlay {
689
730
  const inline = this.inlineDocumentValue(value);
690
731
  if (inline !== undefined) return [`${" ".repeat(depth)}${inline}`];
691
732
  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
- });
733
+ const indent = " ".repeat(depth);
734
+ return [
735
+ `${indent}[`,
736
+ ...value.flatMap((item, index) => {
737
+ const marker = `${indent} - #${index + 1}`;
738
+ const itemInline = this.inlineDocumentValue(item);
739
+ return itemInline !== undefined
740
+ ? [`${marker}: ${itemInline}`]
741
+ : [marker, ...this.document(item, depth + 2)];
742
+ }),
743
+ `${indent}]`,
744
+ ];
699
745
  }
700
746
  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
- });
747
+ const indent = " ".repeat(depth);
748
+ const entries = Object.entries(value as Record<string, unknown>);
749
+ return [
750
+ `${indent}{`,
751
+ ...entries.flatMap(([key, item], index) => {
752
+ const lines = this.labeledDocumentLines(key, item, depth + 1);
753
+ if (index < entries.length - 1) lines[lines.length - 1] += ",";
754
+ return lines;
755
+ }),
756
+ `${indent}}`,
757
+ ];
711
758
  }
712
759
  return String(value)
713
760
  .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;
@@ -15,3 +17,13 @@ export const SESSION_EVIDENCE_TEXT_CHARS = 4_000;
15
17
  export const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
16
18
  export const TRACE_EVENT_MAX_BYTES = 64 * 1024;
17
19
  export const TRACE_EVENT_MAX_READ = 200;
20
+ export const TRACE_JOURNAL_MAX_EVENTS = 2_048;
21
+ export const TRACE_JOURNAL_TARGET_EVENTS = 1_536;
22
+ export const TRACE_JOURNAL_MAX_BYTES = 4 * 1024 * 1024;
23
+ export const TRACE_JOURNAL_TARGET_BYTES = 3 * 1024 * 1024;
24
+ export const RUN_CONTROL_PENDING_LIMIT = 64;
25
+ export const RUN_CONTROL_TERMINAL_LIMIT = 128;
26
+ export const RUN_CONTROL_ERROR_MAX_BYTES = 4 * 1024;
27
+ export const RUN_CONTROL_JOURNAL_MAX_BYTES = 1024 * 1024;
28
+ export const RUN_RETENTION_MAX_RECORDS = 256;
29
+ export const RUN_RETENTION_MAX_BYTES = 1024 * 1024;
@@ -25,6 +25,7 @@ import {
25
25
 
26
26
  import * as AsyncRuns from "./async-runs.ts";
27
27
  import * as Paths from "./paths.ts";
28
+ import * as RunsTrace from "./runs-trace.ts";
28
29
  import { readJsonlFileResilient } from "./state-readers.ts";
29
30
 
30
31
  export type RunObservedStatus =
@@ -73,7 +74,7 @@ export interface RunSummary {
73
74
 
74
75
  export interface RunUiObservationState {
75
76
  attentionEventIds: Map<string, Set<string>>;
76
- eventLines: Map<string, number>;
77
+ legacyEventLines: Map<string, number>;
77
78
  frame: number;
78
79
  observed: Map<string, RunObservedStatus>;
79
80
  }
@@ -98,12 +99,19 @@ export interface RunUiNotificationSink {
98
99
  export function createRunUiObservationState(): RunUiObservationState {
99
100
  return {
100
101
  attentionEventIds: new Map<string, Set<string>>(),
101
- eventLines: new Map<string, number>(),
102
+ legacyEventLines: new Map<string, number>(),
102
103
  frame: 0,
103
104
  observed: new Map<string, RunObservedStatus>(),
104
105
  };
105
106
  }
106
107
 
108
+ export function primeRunAttentionState(
109
+ state: RunUiObservationState, ownerId: string, stateRoot?: string,
110
+ ): void {
111
+ detectRunAttentionEvents(state.legacyEventLines, summarizeRuns(stateRoot, ownerId),
112
+ state.attentionEventIds, true);
113
+ }
114
+
107
115
  export function readRunUiSnapshot(
108
116
  state: RunUiObservationState,
109
117
  ownerId: string,
@@ -116,7 +124,7 @@ export function readRunUiSnapshot(
116
124
  options.includeAttention === false
117
125
  ? []
118
126
  : detectRunAttentionEvents(
119
- state.eventLines,
127
+ state.legacyEventLines,
120
128
  summary,
121
129
  state.attentionEventIds,
122
130
  ),
@@ -132,7 +140,7 @@ export function pruneRunUiObservationState(
132
140
  ): void {
133
141
  pruneRunObservationState(
134
142
  state.observed,
135
- state.eventLines,
143
+ state.legacyEventLines,
136
144
  snapshot.summary,
137
145
  snapshot.transitions.map(
138
146
  (transition) => transition.stateDir ?? transition.run,
@@ -192,16 +200,15 @@ export function reconcileRunTerminalNotifications(input: {
192
200
  sink: RunUiNotificationSink;
193
201
  state: RunUiObservationState;
194
202
  stateRoot?: string;
203
+ includeAttention?: boolean;
195
204
  }): RunUiSnapshot {
196
205
  const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
197
- includeAttention: false,
206
+ includeAttention: input.includeAttention,
198
207
  stateRoot: input.stateRoot,
199
208
  });
200
- deliverRunTransitionNotifications(
201
- snapshot.transitions,
202
- input.sink,
203
- input.inFlight,
204
- );
209
+ if (input.includeAttention)
210
+ deliverRunAttentionNotifications(snapshot.attentionEvents, input.sink);
211
+ deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
205
212
  pruneRunUiObservationState(input.state, snapshot);
206
213
  return snapshot;
207
214
  }
@@ -1114,15 +1121,17 @@ function parseAttentionRecord(
1114
1121
  };
1115
1122
  }
1116
1123
 
1117
- function readTraceAttentionRecords(run: RunObservation): Record<string, unknown>[] {
1118
- if (!run.stateDir) return [];
1119
- const tracePath = join(run.stateDir, "trace.jsonl");
1120
- if (existsSync(tracePath)) {
1121
- return readJsonlFileResilient<Record<string, unknown>>(tracePath).records;
1122
- }
1123
- return readJsonlFileResilient<Record<string, unknown>>(
1124
- join(run.stateDir, "outbox.jsonl"),
1125
- ).records;
1124
+ function readTraceAttentionRecords(run: RunObservation): {
1125
+ canonical: boolean; records: Record<string, unknown>[];
1126
+ } {
1127
+ if (!run.stateDir) return { canonical: true, records: [] };
1128
+ if (existsSync(join(run.stateDir, "trace.jsonl"))) return {
1129
+ canonical: true,
1130
+ records: RunsTrace.readRunTraceJournal(run.stateDir).events.map(
1131
+ ({ event }) => event as unknown as Record<string, unknown>),
1132
+ };
1133
+ return { canonical: false, records: readJsonlFileResilient<Record<string, unknown>>(
1134
+ join(run.stateDir, "outbox.jsonl")).records };
1126
1135
  }
1127
1136
 
1128
1137
  export function pruneRunObservationState(
@@ -1134,55 +1143,44 @@ export function pruneRunObservationState(
1134
1143
  ): void {
1135
1144
  const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
1136
1145
  const terminalRunSet = new Set(terminalRuns);
1137
- const terminalLineKeys = new Set(
1138
- summary.runs
1139
- .filter((run) => terminalRunSet.has(runObservationKey(run)))
1140
- .map((run) => runObservationKey(run)),
1141
- );
1142
- const activeLineKeys = new Set(
1143
- summary.runs.map((run) => run.stateDir ?? run.run),
1144
- );
1146
+ const terminalLineKeys = new Set(summary.runs
1147
+ .filter((run) => terminalRunSet.has(runObservationKey(run)))
1148
+ .map((run) => runObservationKey(run)));
1149
+ const activeLineKeys = new Set(summary.runs.map((run) => run.stateDir ?? run.run));
1145
1150
  for (const run of terminalRunSet) previousStatuses.delete(run);
1146
- for (const run of previousStatuses.keys()) {
1151
+ for (const run of previousStatuses.keys())
1147
1152
  if (!activeRuns.has(run)) previousStatuses.delete(run);
1148
- }
1149
- for (const key of previousLineCounts.keys()) {
1150
- if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) {
1151
- previousLineCounts.delete(key);
1152
- }
1153
- }
1154
- for (const key of seenEventIds.keys()) {
1155
- if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) {
1156
- seenEventIds.delete(key);
1157
- }
1158
- }
1153
+ for (const key of previousLineCounts.keys())
1154
+ if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) previousLineCounts.delete(key);
1155
+ for (const key of seenEventIds.keys())
1156
+ if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) seenEventIds.delete(key);
1159
1157
  }
1160
1158
 
1161
1159
  export function detectRunAttentionEvents(
1162
- previousLineCounts: Map<string, number>,
1163
- summary: RunSummary,
1164
- seenEventIds: Map<string, Set<string>> = new Map(),
1160
+ legacyLineCounts: Map<string, number>, summary: RunSummary,
1161
+ seenEventIds: Map<string, Set<string>> = new Map(), prime = false,
1165
1162
  ): RunAttentionEvent[] {
1166
1163
  const events: RunAttentionEvent[] = [];
1167
1164
  for (const run of summary.runs) {
1168
1165
  const key = run.stateDir ?? run.run;
1169
- const records = readTraceAttentionRecords(run);
1170
- const previousCount = previousLineCounts.get(key) ?? 0;
1171
- if (run.notificationPolicy === "silent") {
1172
- previousLineCounts.set(key, records.length);
1173
- seenEventIds.set(key, seenEventIds.get(key) ?? new Set<string>());
1174
- continue;
1175
- }
1176
- const start = Math.min(previousCount, records.length);
1166
+ const read = readTraceAttentionRecords(run);
1167
+ const retained = new Set<string>();
1177
1168
  const seen = seenEventIds.get(key) ?? new Set<string>();
1178
- for (let index = start; index < records.length; index += 1) {
1179
- const event = parseAttentionRecord(records[index], run, index);
1180
- if (!event || seen.has(event.id)) continue;
1181
- events.push(event);
1182
- seen.add(event.id);
1169
+ const start = read.canonical ? 0
1170
+ : Math.min(legacyLineCounts.get(key) ?? 0, read.records.length);
1171
+ for (const [index, record] of read.records.entries()) {
1172
+ const event = parseAttentionRecord(record, run, index);
1173
+ if (!event || !shouldNotifyRunAttentionEvent(event) ||
1174
+ event.kind === "runtime.trace_compacted") continue;
1175
+ retained.add(event.id);
1176
+ if (prime || run.notificationPolicy === "silent") seen.add(event.id);
1177
+ else if (index >= start && !seen.has(event.id)) {
1178
+ events.push(event); seen.add(event.id);
1179
+ }
1183
1180
  }
1184
- previousLineCounts.set(key, records.length);
1185
- seenEventIds.set(key, seen);
1181
+ seenEventIds.set(key, new Set([...retained].filter((id) => seen.has(id))));
1182
+ if (read.canonical) legacyLineCounts.delete(key);
1183
+ else legacyLineCounts.set(key, read.records.length);
1186
1184
  }
1187
1185
  return events;
1188
1186
  }
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
  }