@llblab/pi-actors 0.41.1 → 0.42.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 (147) hide show
  1. package/AGENTS.md +11 -6
  2. package/BACKLOG.md +2 -62
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +22 -4
  5. package/dist/index.js +25 -121
  6. package/dist/lib/async-runs.d.ts +25 -5
  7. package/dist/lib/async-runs.js +136 -47
  8. package/dist/lib/automatic-review-runtime.d.ts +18 -0
  9. package/dist/lib/automatic-review-runtime.js +96 -0
  10. package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  11. package/dist/lib/draft-consolidation-transaction.js +610 -0
  12. package/dist/lib/draft-consolidation.d.ts +35 -0
  13. package/dist/lib/draft-consolidation.js +126 -0
  14. package/dist/lib/draft-review.d.ts +56 -0
  15. package/dist/lib/draft-review.js +254 -0
  16. package/dist/lib/draft-sleep.d.ts +65 -0
  17. package/dist/lib/draft-sleep.js +468 -0
  18. package/dist/lib/file-state.d.ts +8 -1
  19. package/dist/lib/file-state.js +115 -18
  20. package/dist/lib/inspector-actions.d.ts +16 -0
  21. package/dist/lib/inspector-actions.js +57 -0
  22. package/dist/lib/inspector-command.d.ts +7 -0
  23. package/dist/lib/inspector-command.js +37 -0
  24. package/dist/lib/inspector-overlay.d.ts +20 -3
  25. package/dist/lib/inspector-overlay.js +275 -74
  26. package/dist/lib/inspector.d.ts +9 -0
  27. package/dist/lib/inspector.js +48 -0
  28. package/dist/lib/observability.d.ts +1 -0
  29. package/dist/lib/observability.js +11 -1
  30. package/dist/lib/paths.d.ts +7 -0
  31. package/dist/lib/paths.js +28 -0
  32. package/dist/lib/recipes-discovery.js +32 -24
  33. package/dist/lib/recipes-usage.d.ts +18 -6
  34. package/dist/lib/recipes-usage.js +445 -34
  35. package/dist/lib/review-control.d.ts +14 -0
  36. package/dist/lib/review-control.js +111 -0
  37. package/dist/lib/review-diagnostics.d.ts +11 -0
  38. package/dist/lib/review-diagnostics.js +148 -0
  39. package/dist/lib/review-projection.d.ts +14 -0
  40. package/dist/lib/review-projection.js +170 -0
  41. package/dist/lib/run-ui-runtime.d.ts +18 -0
  42. package/dist/lib/run-ui-runtime.js +123 -0
  43. package/dist/lib/runs-artifacts.d.ts +1 -1
  44. package/dist/lib/runs-artifacts.js +1 -1
  45. package/dist/lib/runs-control.d.ts +8 -2
  46. package/dist/lib/runs-control.js +23 -6
  47. package/dist/lib/runs-identity.d.ts +1 -1
  48. package/dist/lib/runs-identity.js +1 -1
  49. package/dist/lib/runs-index.d.ts +11 -2
  50. package/dist/lib/runs-index.js +46 -23
  51. package/dist/lib/runs-mailbox.d.ts +1 -1
  52. package/dist/lib/runs-mailbox.js +1 -1
  53. package/dist/lib/runs-messages.d.ts +1 -1
  54. package/dist/lib/runs-messages.js +1 -1
  55. package/dist/lib/runs-outbox.d.ts +1 -1
  56. package/dist/lib/runs-outbox.js +1 -1
  57. package/dist/lib/runs-ownership.d.ts +1 -1
  58. package/dist/lib/runs-ownership.js +1 -1
  59. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  60. package/dist/lib/runs-parent-teardown.js +172 -0
  61. package/dist/lib/runs-process.d.ts +1 -1
  62. package/dist/lib/runs-process.js +1 -1
  63. package/dist/lib/runs-retention.d.ts +1 -1
  64. package/dist/lib/runs-retention.js +1 -1
  65. package/dist/lib/runs-start.d.ts +5 -3
  66. package/dist/lib/runs-start.js +6 -48
  67. package/dist/lib/runs-status.d.ts +5 -3
  68. package/dist/lib/runs-status.js +4 -6
  69. package/dist/lib/runtime.d.ts +7 -1
  70. package/dist/lib/runtime.js +32 -18
  71. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  72. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  73. package/dist/lib/tool-review-lineage.d.ts +24 -0
  74. package/dist/lib/tool-review-lineage.js +98 -0
  75. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  76. package/dist/lib/tool-review-scheduler.js +494 -0
  77. package/dist/lib/tool-review-transaction.d.ts +50 -0
  78. package/dist/lib/tool-review-transaction.js +362 -0
  79. package/dist/lib/tool-review.d.ts +56 -0
  80. package/dist/lib/tool-review.js +197 -0
  81. package/dist/lib/tools-inspect.js +26 -3
  82. package/dist/lib/tools-local.js +4 -2
  83. package/dist/lib/tools-message.d.ts +1 -0
  84. package/dist/lib/tools-message.js +29 -17
  85. package/dist/lib/tools-spawn.js +4 -1
  86. package/dist/lib/tools.d.ts +1 -0
  87. package/dist/lib/tools.js +1 -0
  88. package/dist/recipes/draft-review.json +24 -0
  89. package/dist/recipes/tool-review.json +24 -0
  90. package/dist/scripts/release-gates.mjs +165 -0
  91. package/dist/skills/actors/SKILL.md +9 -8
  92. package/dist/skills/swarm/SKILL.md +1 -1
  93. package/docs/actor-inspector.md +20 -11
  94. package/docs/async-runs.md +9 -1
  95. package/docs/recipe-library.md +7 -3
  96. package/docs/template-recipes.md +4 -11
  97. package/docs/tool-registry.md +11 -4
  98. package/index.ts +27 -142
  99. package/lib/async-runs.ts +218 -62
  100. package/lib/automatic-review-runtime.ts +135 -0
  101. package/lib/draft-consolidation-transaction.ts +821 -0
  102. package/lib/draft-consolidation.ts +181 -0
  103. package/lib/draft-review.ts +325 -0
  104. package/lib/draft-sleep.ts +576 -0
  105. package/lib/file-state.ts +143 -19
  106. package/lib/inspector-actions.ts +79 -0
  107. package/lib/inspector-command.ts +54 -0
  108. package/lib/inspector-overlay.ts +325 -91
  109. package/lib/inspector.ts +72 -0
  110. package/lib/observability.ts +14 -1
  111. package/lib/paths.ts +43 -0
  112. package/lib/recipes-discovery.ts +34 -26
  113. package/lib/recipes-usage.ts +569 -40
  114. package/lib/review-control.ts +137 -0
  115. package/lib/review-diagnostics.ts +164 -0
  116. package/lib/review-projection.ts +200 -0
  117. package/lib/run-ui-runtime.ts +153 -0
  118. package/lib/runs-artifacts.ts +1 -1
  119. package/lib/runs-control.ts +49 -5
  120. package/lib/runs-identity.ts +1 -1
  121. package/lib/runs-index.ts +57 -21
  122. package/lib/runs-mailbox.ts +1 -1
  123. package/lib/runs-messages.ts +1 -1
  124. package/lib/runs-outbox.ts +1 -1
  125. package/lib/runs-ownership.ts +1 -1
  126. package/lib/runs-parent-teardown.ts +257 -0
  127. package/lib/runs-process.ts +1 -1
  128. package/lib/runs-retention.ts +1 -1
  129. package/lib/runs-start.ts +12 -68
  130. package/lib/runs-status.ts +12 -8
  131. package/lib/runtime.ts +34 -17
  132. package/lib/tool-review-lineage-transaction.ts +881 -0
  133. package/lib/tool-review-lineage.ts +145 -0
  134. package/lib/tool-review-scheduler.ts +635 -0
  135. package/lib/tool-review-transaction.ts +563 -0
  136. package/lib/tool-review.ts +270 -0
  137. package/lib/tools-inspect.ts +33 -3
  138. package/lib/tools-local.ts +8 -2
  139. package/lib/tools-message.ts +45 -30
  140. package/lib/tools-spawn.ts +8 -1
  141. package/lib/tools.ts +5 -0
  142. package/package.json +3 -2
  143. package/recipes/draft-review.json +24 -0
  144. package/recipes/tool-review.json +24 -0
  145. package/scripts/release-gates.mjs +165 -0
  146. package/skills/actors/SKILL.md +9 -8
  147. package/skills/swarm/SKILL.md +1 -1
package/lib/async-runs.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Command-template async run lifecycle facade.
3
- * Owns launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
3
+ * Owns: launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
4
4
  */
5
5
 
6
6
  import { spawn } from "node:child_process";
7
+ import { randomUUID } from "node:crypto";
7
8
  import {
8
9
  closeSync,
9
10
  existsSync,
@@ -60,6 +61,7 @@ import {
60
61
  } from "./runs-process.ts";
61
62
  import * as RunsStart from "./runs-start.ts";
62
63
  import * as RunsIndex from "./runs-index.ts";
64
+ import * as RunsParentTeardown from "./runs-parent-teardown.ts";
63
65
  import {
64
66
  claimRunInboxMessageInStateDir,
65
67
  parseRunInboxLine,
@@ -75,7 +77,12 @@ import {
75
77
  deliverRunMessage,
76
78
  type SendRunMessageOptions,
77
79
  } from "./runs-messages.ts";
78
- import { buildRunStatus, tailFile, tailLines } from "./runs-status.ts";
80
+ import {
81
+ buildRunStatus,
82
+ tailFile,
83
+ tailLines,
84
+ type AsyncRunStatus,
85
+ } from "./runs-status.ts";
79
86
  import { readJsonFileResilient } from "./state-readers.ts";
80
87
 
81
88
  const RUNNER_IDENTITY_GRACE_MS = 5000;
@@ -92,6 +99,9 @@ export interface AsyncRunStartParams {
92
99
  control?: AsyncRunControlEndpoint;
93
100
  file?: string;
94
101
  launch_source?: AsyncRunLaunchSource;
102
+ lifecycleHooks?: {
103
+ onLockContention?(): void;
104
+ };
95
105
  name?: string;
96
106
  ownerId?: string;
97
107
  run_id?: string;
@@ -111,6 +121,7 @@ export interface AsyncRunStartParams {
111
121
  output?: string;
112
122
  artifacts?: Record<string, RunArtifactDeclaration>;
113
123
  mailbox?: RecipesReferences.TemplateRecipeMailbox;
124
+ notification_policy?: "normal" | "silent";
114
125
  retire_when?: "children_terminal";
115
126
  retry?: number | string;
116
127
  failure?: CommandTemplateFailureScope;
@@ -122,13 +133,7 @@ export interface AsyncRunStartParams {
122
133
  cwd?: string;
123
134
  }
124
135
 
125
- export type AsyncRunStatus =
126
- | "running"
127
- | "done"
128
- | "failed"
129
- | "exited"
130
- | "cancelled"
131
- | "killed";
136
+ export type { AsyncRunStatus } from "./runs-status.ts";
132
137
 
133
138
  export interface AsyncRunMeta {
134
139
  argv: string[];
@@ -140,6 +145,7 @@ export interface AsyncRunMeta {
140
145
  recipe?: string;
141
146
  recipe_file?: string;
142
147
  run: string;
148
+ run_instance_id: string;
143
149
  state_dir: string;
144
150
  status: AsyncRunStatus;
145
151
  tool?: string;
@@ -149,6 +155,7 @@ export interface AsyncRunMeta {
149
155
  control?: AsyncRunControlEndpoint;
150
156
  mailbox?: RecipesReferences.TemplateRecipeMailbox;
151
157
  model_policy?: CurrentPolicyProvenance;
158
+ notification_policy?: "normal" | "silent";
152
159
  process_identity?: RunProcessIdentity;
153
160
  recipe_context_records?: RecipesReferences.TemplateRecipeContextRecord[];
154
161
  retire_when?: "children_terminal";
@@ -411,8 +418,11 @@ function assertCurrentPlaceholderReferencesResolved(
411
418
  }
412
419
  }
413
420
 
414
- function acquireStateStartLock(stateDir: string): () => void {
415
- return RunsStart.acquireStateStartLock(stateDir);
421
+ function acquireStateStartLock(
422
+ stateDir: string,
423
+ options: import("./file-state.ts").FileMutationLockOptions = {},
424
+ ): () => void {
425
+ return RunsStart.acquireStateStartLock(stateDir, options);
416
426
  }
417
427
 
418
428
  function prepareStateDirForStart(stateDir: string): void {
@@ -448,7 +458,9 @@ export function startRun(
448
458
  );
449
459
  assertNoActiveRunState(stateDir);
450
460
  mkdirSync(stateDir, { recursive: true });
451
- const releaseStartLock = acquireStateStartLock(stateDir);
461
+ const releaseStartLock = acquireStateStartLock(stateDir, {
462
+ onContention: startParams.lifecycleHooks?.onLockContention,
463
+ });
452
464
  try {
453
465
  claimRunStateDirectory(stateDir, run);
454
466
  assertNoActiveRunState(stateDir);
@@ -466,11 +478,17 @@ export function startRun(
466
478
  recipeFile && includeActorRecipeContext
467
479
  ? RecipesReferences.buildRecipeContextRecords(recipeFile)
468
480
  : undefined;
469
- if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
470
- RecipesUsage.recordRecipeLaunch(
481
+ if (
482
+ recipeFile &&
483
+ isMutableUsageRecipeFile(recipeFile) &&
484
+ !RecipesUsage.recordRecipeLaunch(
471
485
  recipeFile,
472
486
  new Date(),
473
487
  startParams.launch_source === "tool" ? "tool" : "spawn",
488
+ )
489
+ ) {
490
+ throw new Error(
491
+ `Recipe launch rejected because its source changed during activation: ${recipeFile}. Reload recipe tools and retry.`,
474
492
  );
475
493
  }
476
494
  const outFd = openSync(stdout, "a");
@@ -493,6 +511,7 @@ export function startRun(
493
511
  ...(recipe ? { recipe } : {}),
494
512
  ...(recipeFile ? { recipe_file: recipeFile } : {}),
495
513
  run,
514
+ run_instance_id: randomUUID(),
496
515
  state_dir: stateDir,
497
516
  status: "running",
498
517
  ...(startParams.tool ? { tool: startParams.tool } : {}),
@@ -502,6 +521,9 @@ export function startRun(
502
521
  ...(artifacts ? { artifacts } : {}),
503
522
  ...(startParams.control ? { control: startParams.control } : {}),
504
523
  ...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
524
+ ...(startParams.notification_policy === "silent"
525
+ ? { notification_policy: "silent" as const }
526
+ : {}),
505
527
  ...(recipeContextRecords && recipeContextRecords.length > 0
506
528
  ? { recipe_context_records: recipeContextRecords }
507
529
  : {}),
@@ -535,7 +557,7 @@ export function startRun(
535
557
  });
536
558
  writeFileSync(
537
559
  join(stateDir, "events.jsonl"),
538
- `${JSON.stringify({ event: "run.start", run, pid: meta.pid, ts: new Date().toISOString() })}\n`,
560
+ `${JSON.stringify({ event: "run.start", run, run_instance_id: meta.run_instance_id, pid: meta.pid, ts: new Date().toISOString() })}\n`,
539
561
  { flag: "a" },
540
562
  );
541
563
  child.unref();
@@ -552,12 +574,16 @@ export type {
552
574
  RunOutboxLevel,
553
575
  } from "./runs-outbox.ts";
554
576
 
555
- export function getRunStatus(runOrDir: string): Record<string, unknown> {
556
- const stateDir = resolve(
577
+ function resolveRunStateDir(runOrDir: string): string {
578
+ return resolve(
557
579
  runOrDir.includes("/")
558
580
  ? runOrDir
559
581
  : join(DEFAULT_STATE_ROOT, safeRunId(runOrDir)),
560
582
  );
583
+ }
584
+
585
+ export function getRunStatus(runOrDir: string): Record<string, unknown> {
586
+ const stateDir = resolveRunStateDir(runOrDir);
561
587
  const meta = readJson(join(stateDir, "run.json"));
562
588
  if (!meta) throw new Error(`Run not found: ${runOrDir}`);
563
589
  return buildRunStatus(
@@ -574,10 +600,8 @@ export type { RunStateIndexEntry } from "./runs-index.ts";
574
600
 
575
601
  export function listRunStateDirs(
576
602
  stateRoot = DEFAULT_STATE_ROOT,
577
- depth = 0,
578
- seen = new Set<string>(),
579
603
  ): string[] {
580
- return RunsIndex.listRunStateDirs(stateRoot, depth, seen);
604
+ return RunsIndex.listRunStateDirs(stateRoot);
581
605
  }
582
606
 
583
607
  export function rebuildRunStateIndex(
@@ -599,6 +623,103 @@ export function listRuns(
599
623
  return RunsIndex.listRuns(stateRoot, getRunStatus, readJson, statusFilter);
600
624
  }
601
625
 
626
+ export type {
627
+ ParentRunTeardownAttempt,
628
+ ParentRunTeardownDiscoveryFailure,
629
+ ParentRunTeardownResult,
630
+ } from "./runs-parent-teardown.ts";
631
+
632
+ export interface ParentRunsTeardownSummaryResult
633
+ extends RunsParentTeardown.ParentRunTeardownResult {
634
+ summaryPath?: string;
635
+ }
636
+
637
+ export function teardownRunsOwnedByParent(
638
+ ownerId: string | undefined,
639
+ stateRoot = DEFAULT_STATE_ROOT,
640
+ options: { trigger?: string } = {},
641
+ ): ParentRunsTeardownSummaryResult {
642
+ const result = RunsParentTeardown.teardownParentRuns(ownerId, {
643
+ getRunStatus,
644
+ killRun,
645
+ listRunStatuses: () => {
646
+ const discovered = RunsIndex.discoverRunStateDirs(
647
+ stateRoot,
648
+ Number.POSITIVE_INFINITY,
649
+ );
650
+ const failures: RunsParentTeardown.ParentRunTeardownDiscoveryFailure[] =
651
+ discovered.issues.map((issue) => ({
652
+ path: issue.path,
653
+ reason: issue.reason.replaceAll("_", " "),
654
+ }));
655
+ const statuses = discovered.stateDirs.flatMap((stateDir) => {
656
+ try {
657
+ return [getRunStatus(stateDir)];
658
+ } catch (error) {
659
+ failures.push({
660
+ path: stateDir,
661
+ reason: error instanceof Error ? error.message : String(error),
662
+ });
663
+ return [];
664
+ }
665
+ });
666
+ return { failures, statuses };
667
+ },
668
+ recordAttempt: (attempt) => {
669
+ const current = getRunStatus(attempt.stateDir);
670
+ if (
671
+ !attempt.runInstanceId ||
672
+ current.run_instance_id !== attempt.runInstanceId
673
+ ) {
674
+ throw new Error("run generation changed before teardown evidence");
675
+ }
676
+ writeFileSync(
677
+ join(attempt.stateDir, "events.jsonl"),
678
+ `${JSON.stringify({
679
+ event: "run.parent_teardown",
680
+ ownerId: attempt.ownerId,
681
+ outcome: attempt.outcome,
682
+ reason: attempt.reason,
683
+ run: attempt.run,
684
+ run_instance_id: attempt.runInstanceId,
685
+ type: "control.kill",
686
+ trigger: options.trigger ?? "parent_shutdown",
687
+ ts: new Date().toISOString(),
688
+ })}\n`,
689
+ { flag: "a" },
690
+ );
691
+ },
692
+ });
693
+ const summaryPath = join(
694
+ stateRoot,
695
+ "teardown",
696
+ `${Date.now()}-${randomUUID()}.json`,
697
+ );
698
+ try {
699
+ writeJsonAtomic(
700
+ summaryPath,
701
+ RunsParentTeardown.buildBoundedParentTeardownSummary(
702
+ result,
703
+ ownerId ?? "unknown",
704
+ options.trigger ?? "parent_shutdown",
705
+ new Date().toISOString(),
706
+ ),
707
+ );
708
+ return { ...result, summaryPath };
709
+ } catch (error) {
710
+ const failure = {
711
+ path: summaryPath,
712
+ reason: `summary: ${error instanceof Error ? error.message : String(error)}`,
713
+ };
714
+ return {
715
+ ...result,
716
+ discoveryFailed: result.discoveryFailed + 1,
717
+ discoveryFailures: [...result.discoveryFailures, failure],
718
+ failed: result.failed + 1,
719
+ };
720
+ }
721
+ }
722
+
602
723
  export function tailRun(runOrDir: string, lines = 40): string {
603
724
  const status = getRunStatus(runOrDir);
604
725
  const stateDir = String(status.state_dir);
@@ -815,26 +936,58 @@ function finalizeInterruptedReviewEvidence(
815
936
  });
816
937
  }
817
938
 
939
+ export interface RunControlExpectation {
940
+ onLocked?(): void;
941
+ ownerId?: string;
942
+ runInstanceId?: string;
943
+ }
944
+
818
945
  function stopRun(
819
946
  runOrDir: string,
820
947
  signal: NodeJS.Signals,
821
948
  event: string,
949
+ expected: RunControlExpectation = {},
822
950
  ): Record<string, unknown> {
823
- const status = getRunStatus(runOrDir);
824
- const pid = Number(status.pid || 0);
825
- const stateDir = String(status.state_dir);
826
- if (status.status !== "running" && status.status !== "exited") {
827
- return { stopped: false, reason: "not running", status };
828
- }
829
- const identity = verifyRunProcessIdentity(
830
- pid,
831
- status.process_identity as RunProcessIdentity | undefined,
832
- );
833
- if (status.status === "exited") {
951
+ const stateDir = resolveRunStateDir(runOrDir);
952
+ const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
953
+ try {
954
+ expected.onLocked?.();
955
+ const status = getRunStatus(stateDir);
834
956
  if (
835
- identity.status === "owner_mismatch" ||
836
- identity.status === "unsupported_proof"
957
+ expected.ownerId !== undefined &&
958
+ status.ownerId !== expected.ownerId
837
959
  ) {
960
+ return { stopped: false, reason: "ownership changed", status };
961
+ }
962
+ if (
963
+ expected.runInstanceId !== undefined &&
964
+ status.run_instance_id !== expected.runInstanceId
965
+ ) {
966
+ return { stopped: false, reason: "run generation changed", status };
967
+ }
968
+ const pid = Number(status.pid || 0);
969
+ if (status.status !== "running" && status.status !== "exited") {
970
+ return { stopped: false, reason: "not running", status };
971
+ }
972
+ const identity = verifyRunProcessIdentity(
973
+ pid,
974
+ status.process_identity as RunProcessIdentity | undefined,
975
+ );
976
+ if (status.status === "exited") {
977
+ if (
978
+ identity.status === "owner_mismatch" ||
979
+ identity.status === "unsupported_proof"
980
+ ) {
981
+ return {
982
+ stopped: false,
983
+ reason: identity.status.replaceAll("_", " "),
984
+ process_identity_status: identity.status,
985
+ status,
986
+ };
987
+ }
988
+ return { stopped: false, reason: "not running", status };
989
+ }
990
+ if (!identity.valid) {
838
991
  return {
839
992
  stopped: false,
840
993
  reason: identity.status.replaceAll("_", " "),
@@ -842,32 +995,29 @@ function stopRun(
842
995
  status,
843
996
  };
844
997
  }
845
- return { stopped: false, reason: "not running", status };
846
- }
847
- if (!identity.valid) {
848
- return {
849
- stopped: false,
850
- reason: identity.status.replaceAll("_", " "),
851
- process_identity_status: identity.status,
852
- status,
853
- };
854
- }
855
- const signalResult = signalOwnedRunProcess(pid, signal);
856
- writeFileSync(
857
- join(stateDir, "events.jsonl"),
858
- `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`,
859
- { flag: "a" },
860
- );
861
- markTerminalHandled(stateDir, { event, signal });
862
- if (event === "run.kill") {
863
- finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
864
- markTerminalProgress(stateDir, "killed");
865
- }
866
- if (event === "run.cancel") {
867
- finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
868
- markTerminalProgress(stateDir, "cancelled");
998
+ const signalResult = signalOwnedRunProcess(
999
+ pid,
1000
+ signal,
1001
+ status.process_identity as RunProcessIdentity,
1002
+ );
1003
+ writeFileSync(
1004
+ join(stateDir, "events.jsonl"),
1005
+ `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`,
1006
+ { flag: "a" },
1007
+ );
1008
+ markTerminalHandled(stateDir, { event, signal });
1009
+ if (event === "run.kill") {
1010
+ finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
1011
+ markTerminalProgress(stateDir, "killed");
1012
+ }
1013
+ if (event === "run.cancel") {
1014
+ finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
1015
+ markTerminalProgress(stateDir, "cancelled");
1016
+ }
1017
+ return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
1018
+ } finally {
1019
+ releaseControlLock();
869
1020
  }
870
- return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
871
1021
  }
872
1022
 
873
1023
  export function markRunTerminalNotificationHandled(
@@ -880,8 +1030,11 @@ export function markRunTerminalNotificationHandled(
880
1030
  });
881
1031
  }
882
1032
 
883
- export function cancelRun(runOrDir: string): Record<string, unknown> {
884
- const result = stopRun(runOrDir, "SIGTERM", "run.cancel");
1033
+ export function cancelRun(
1034
+ runOrDir: string,
1035
+ expected: RunControlExpectation = {},
1036
+ ): Record<string, unknown> {
1037
+ const result = stopRun(runOrDir, "SIGTERM", "run.cancel", expected);
885
1038
  return Object.hasOwn(result, "stopped")
886
1039
  ? { cancelled: result.stopped, ...result }
887
1040
  : result;
@@ -906,8 +1059,11 @@ export function pruneRun(
906
1059
  return pruneTerminalRun(assertTerminalRun(runOrDir), options);
907
1060
  }
908
1061
 
909
- export function killRun(runOrDir: string): Record<string, unknown> {
910
- const result = stopRun(runOrDir, "SIGKILL", "run.kill");
1062
+ export function killRun(
1063
+ runOrDir: string,
1064
+ expected: RunControlExpectation = {},
1065
+ ): Record<string, unknown> {
1066
+ const result = stopRun(runOrDir, "SIGKILL", "run.kill", expected);
911
1067
  return Object.hasOwn(result, "stopped")
912
1068
  ? { killed: result.stopped, ...result }
913
1069
  : result;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Automatic recipe-review runtime composition.
3
+ * Zones: session-bound review schedulers, silent reviewer launch adapters, review controls
4
+ * Owns automatic-review lifecycle wiring without owning review decisions or filesystem transactions.
5
+ */
6
+
7
+ import { join } from "node:path";
8
+
9
+ import * as AsyncRuns from "./async-runs.ts";
10
+ import * as DraftSleep from "./draft-sleep.ts";
11
+ import * as ModelContext from "./model-context.ts";
12
+ import * as Paths from "./paths.ts";
13
+ import type * as Pi from "./pi.ts";
14
+ import * as ReviewControl from "./review-control.ts";
15
+ import * as ToolReviewScheduler from "./tool-review-scheduler.ts";
16
+
17
+ export interface AutomaticReviewRuntime {
18
+ close(): void;
19
+ handleMessage(type: string, body: unknown): Record<string, unknown>;
20
+ schedule(): void;
21
+ start(ctx: Pi.ExtensionContext): void;
22
+ }
23
+
24
+ export interface AutomaticReviewRuntimeDeps {
25
+ getActiveContext(): Pi.ExtensionContext | undefined;
26
+ getRunOwnerId(ctx: Pi.ExtensionContext): string;
27
+ getThinkingLevel(): unknown;
28
+ }
29
+
30
+ export function createAutomaticReviewRuntime(
31
+ deps: AutomaticReviewRuntimeDeps,
32
+ ): AutomaticReviewRuntime {
33
+ let draftScheduler: DraftSleep.DraftSleepScheduler | undefined;
34
+ let toolScheduler: ToolReviewScheduler.ToolReviewScheduler | undefined;
35
+
36
+ const close = (): void => {
37
+ draftScheduler?.close();
38
+ draftScheduler = undefined;
39
+ toolScheduler?.close();
40
+ toolScheduler = undefined;
41
+ };
42
+ const hasActiveActors = (): boolean =>
43
+ AsyncRuns.listRuns(Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, "running").length > 0;
44
+ const policyValues = (ctx: Pi.ExtensionContext): Record<string, unknown> =>
45
+ ModelContext.withCurrentModelValues(
46
+ {},
47
+ {
48
+ ...(ctx as ModelContext.CurrentModelContext),
49
+ getThinkingLevel: deps.getThinkingLevel,
50
+ },
51
+ );
52
+
53
+ return {
54
+ close,
55
+ handleMessage(type, body) {
56
+ if (type !== "review.retry" && type !== "review.reset") {
57
+ throw new Error("tool:pi-actors accepts review.retry or review.reset messages.");
58
+ }
59
+ if (type === "review.retry" && !Paths.isAutomaticRecipeReviewEnabled()) {
60
+ throw new Error(
61
+ "Automatic recipe review is disabled by PI_ACTORS_AUTOMATIC_REVIEW.",
62
+ );
63
+ }
64
+ return ReviewControl.controlAutomaticReview(
65
+ type,
66
+ ReviewControl.parseAutomaticReviewScope(body),
67
+ {
68
+ scheduleDraft: () => draftScheduler?.schedule(),
69
+ scheduleTool: () => toolScheduler?.schedule(),
70
+ },
71
+ );
72
+ },
73
+ schedule() {
74
+ draftScheduler?.schedule();
75
+ toolScheduler?.schedule();
76
+ },
77
+ start(ctx) {
78
+ close();
79
+ if (!Paths.isAutomaticRecipeReviewEnabled()) return;
80
+ ToolReviewScheduler.applyApprovedToolReviewAtSessionBoundary({
81
+ recipeRoot: Paths.getRecipeRoot(),
82
+ });
83
+ draftScheduler = DraftSleep.createDraftSleepScheduler({
84
+ hasActiveActors,
85
+ launch: (batch) => {
86
+ if (deps.getActiveContext() !== ctx) {
87
+ throw new Error("Draft review session changed before launch.");
88
+ }
89
+ return AsyncRuns.startRun(
90
+ {
91
+ file: join(Paths.getPackagedRecipeRoot(), "draft-review.json"),
92
+ launch_source: "tool",
93
+ notification_policy: "silent",
94
+ ownerId: deps.getRunOwnerId(ctx),
95
+ policy_values: policyValues(ctx),
96
+ run_id: DraftSleep.draftSleepRunId(batch.batchId),
97
+ values: { input_path: batch.reviewerInputPath },
98
+ },
99
+ ctx.cwd,
100
+ );
101
+ },
102
+ process: (state) =>
103
+ DraftSleep.processDraftSleepReview(state, {
104
+ getRunStatus: AsyncRuns.getRunStatus,
105
+ recipeRoot: Paths.getRecipeRoot(),
106
+ }),
107
+ });
108
+ toolScheduler = ToolReviewScheduler.createToolReviewScheduler({
109
+ hasActiveActors,
110
+ launch: (batch) => {
111
+ if (deps.getActiveContext() !== ctx) {
112
+ throw new Error("Tool review session changed before launch.");
113
+ }
114
+ return AsyncRuns.startRun(
115
+ {
116
+ file: join(Paths.getPackagedRecipeRoot(), "tool-review.json"),
117
+ launch_source: "tool",
118
+ notification_policy: "silent",
119
+ ownerId: deps.getRunOwnerId(ctx),
120
+ policy_values: policyValues(ctx),
121
+ run_id: ToolReviewScheduler.toolReviewRunId(batch.reviewId),
122
+ values: { input_path: batch.reviewerInputPath },
123
+ },
124
+ ctx.cwd,
125
+ );
126
+ },
127
+ process: (state) =>
128
+ ToolReviewScheduler.processToolReviewResult(state, {
129
+ getRunStatus: AsyncRuns.getRunStatus,
130
+ recipeRoot: Paths.getRecipeRoot(),
131
+ }),
132
+ });
133
+ },
134
+ };
135
+ }