@llblab/pi-actors 0.41.1 → 0.42.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 (157) hide show
  1. package/AGENTS.md +11 -6
  2. package/BACKLOG.md +1 -65
  3. package/CHANGELOG.md +25 -0
  4. package/README.md +24 -4
  5. package/dist/index.js +25 -121
  6. package/dist/lib/async-runs.d.ts +37 -5
  7. package/dist/lib/async-runs.js +181 -51
  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/command-templates.js +1 -1
  11. package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  12. package/dist/lib/draft-consolidation-transaction.js +610 -0
  13. package/dist/lib/draft-consolidation.d.ts +35 -0
  14. package/dist/lib/draft-consolidation.js +126 -0
  15. package/dist/lib/draft-review.d.ts +56 -0
  16. package/dist/lib/draft-review.js +254 -0
  17. package/dist/lib/draft-sleep.d.ts +65 -0
  18. package/dist/lib/draft-sleep.js +468 -0
  19. package/dist/lib/file-state.d.ts +8 -1
  20. package/dist/lib/file-state.js +115 -18
  21. package/dist/lib/inspector-actions.d.ts +16 -0
  22. package/dist/lib/inspector-actions.js +57 -0
  23. package/dist/lib/inspector-command.d.ts +7 -0
  24. package/dist/lib/inspector-command.js +37 -0
  25. package/dist/lib/inspector-overlay.d.ts +23 -3
  26. package/dist/lib/inspector-overlay.js +327 -70
  27. package/dist/lib/inspector.d.ts +9 -0
  28. package/dist/lib/inspector.js +49 -1
  29. package/dist/lib/observability.d.ts +13 -0
  30. package/dist/lib/observability.js +160 -6
  31. package/dist/lib/paths.d.ts +7 -0
  32. package/dist/lib/paths.js +28 -0
  33. package/dist/lib/recipes-discovery.js +32 -24
  34. package/dist/lib/recipes-usage.d.ts +18 -6
  35. package/dist/lib/recipes-usage.js +445 -34
  36. package/dist/lib/review-control.d.ts +14 -0
  37. package/dist/lib/review-control.js +111 -0
  38. package/dist/lib/review-diagnostics.d.ts +11 -0
  39. package/dist/lib/review-diagnostics.js +148 -0
  40. package/dist/lib/review-projection.d.ts +14 -0
  41. package/dist/lib/review-projection.js +170 -0
  42. package/dist/lib/run-ui-runtime.d.ts +18 -0
  43. package/dist/lib/run-ui-runtime.js +123 -0
  44. package/dist/lib/runs-artifacts.d.ts +1 -1
  45. package/dist/lib/runs-artifacts.js +1 -1
  46. package/dist/lib/runs-control.d.ts +10 -2
  47. package/dist/lib/runs-control.js +37 -7
  48. package/dist/lib/runs-identity.d.ts +1 -1
  49. package/dist/lib/runs-identity.js +1 -1
  50. package/dist/lib/runs-index.d.ts +11 -2
  51. package/dist/lib/runs-index.js +46 -23
  52. package/dist/lib/runs-mailbox.d.ts +1 -1
  53. package/dist/lib/runs-mailbox.js +1 -1
  54. package/dist/lib/runs-messages.d.ts +1 -1
  55. package/dist/lib/runs-messages.js +1 -1
  56. package/dist/lib/runs-outbox.d.ts +1 -1
  57. package/dist/lib/runs-outbox.js +1 -1
  58. package/dist/lib/runs-ownership.d.ts +1 -1
  59. package/dist/lib/runs-ownership.js +18 -4
  60. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  61. package/dist/lib/runs-parent-teardown.js +172 -0
  62. package/dist/lib/runs-process.d.ts +1 -1
  63. package/dist/lib/runs-process.js +5 -4
  64. package/dist/lib/runs-retention.d.ts +1 -1
  65. package/dist/lib/runs-retention.js +1 -1
  66. package/dist/lib/runs-start.d.ts +5 -3
  67. package/dist/lib/runs-start.js +7 -48
  68. package/dist/lib/runs-status.d.ts +5 -3
  69. package/dist/lib/runs-status.js +7 -6
  70. package/dist/lib/runtime.d.ts +7 -1
  71. package/dist/lib/runtime.js +32 -18
  72. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  73. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  74. package/dist/lib/tool-review-lineage.d.ts +24 -0
  75. package/dist/lib/tool-review-lineage.js +98 -0
  76. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  77. package/dist/lib/tool-review-scheduler.js +494 -0
  78. package/dist/lib/tool-review-transaction.d.ts +50 -0
  79. package/dist/lib/tool-review-transaction.js +362 -0
  80. package/dist/lib/tool-review.d.ts +56 -0
  81. package/dist/lib/tool-review.js +197 -0
  82. package/dist/lib/tools-inspect.js +28 -4
  83. package/dist/lib/tools-local.js +21 -4
  84. package/dist/lib/tools-message.d.ts +1 -0
  85. package/dist/lib/tools-message.js +29 -17
  86. package/dist/lib/tools-spawn.js +14 -2
  87. package/dist/lib/tools.d.ts +1 -0
  88. package/dist/lib/tools.js +1 -0
  89. package/dist/recipes/draft-review.json +24 -0
  90. package/dist/recipes/tool-review.json +24 -0
  91. package/dist/scripts/async-runner.mjs +9 -9
  92. package/dist/scripts/build-dist.mjs +6 -1
  93. package/dist/scripts/conformance.mjs +6 -1
  94. package/dist/scripts/recipe-utils.mjs +3 -3
  95. package/dist/scripts/release-gates.mjs +165 -0
  96. package/dist/skills/actors/SKILL.md +9 -8
  97. package/dist/skills/swarm/SKILL.md +1 -1
  98. package/docs/actor-inspector.md +20 -11
  99. package/docs/async-runs.md +16 -2
  100. package/docs/recipe-library.md +7 -3
  101. package/docs/template-recipes.md +4 -11
  102. package/docs/tool-registry.md +13 -4
  103. package/index.ts +27 -142
  104. package/lib/async-runs.ts +283 -67
  105. package/lib/automatic-review-runtime.ts +135 -0
  106. package/lib/command-templates.ts +1 -1
  107. package/lib/draft-consolidation-transaction.ts +821 -0
  108. package/lib/draft-consolidation.ts +181 -0
  109. package/lib/draft-review.ts +325 -0
  110. package/lib/draft-sleep.ts +576 -0
  111. package/lib/file-state.ts +143 -19
  112. package/lib/inspector-actions.ts +79 -0
  113. package/lib/inspector-command.ts +54 -0
  114. package/lib/inspector-overlay.ts +377 -91
  115. package/lib/inspector.ts +73 -1
  116. package/lib/observability.ts +194 -5
  117. package/lib/paths.ts +43 -0
  118. package/lib/recipes-discovery.ts +34 -26
  119. package/lib/recipes-usage.ts +569 -40
  120. package/lib/review-control.ts +137 -0
  121. package/lib/review-diagnostics.ts +164 -0
  122. package/lib/review-projection.ts +200 -0
  123. package/lib/run-ui-runtime.ts +153 -0
  124. package/lib/runs-artifacts.ts +1 -1
  125. package/lib/runs-control.ts +69 -6
  126. package/lib/runs-identity.ts +1 -1
  127. package/lib/runs-index.ts +57 -21
  128. package/lib/runs-mailbox.ts +1 -1
  129. package/lib/runs-messages.ts +1 -1
  130. package/lib/runs-outbox.ts +1 -1
  131. package/lib/runs-ownership.ts +23 -4
  132. package/lib/runs-parent-teardown.ts +257 -0
  133. package/lib/runs-process.ts +5 -4
  134. package/lib/runs-retention.ts +1 -1
  135. package/lib/runs-start.ts +13 -68
  136. package/lib/runs-status.ts +17 -8
  137. package/lib/runtime.ts +34 -17
  138. package/lib/tool-review-lineage-transaction.ts +881 -0
  139. package/lib/tool-review-lineage.ts +145 -0
  140. package/lib/tool-review-scheduler.ts +635 -0
  141. package/lib/tool-review-transaction.ts +563 -0
  142. package/lib/tool-review.ts +270 -0
  143. package/lib/tools-inspect.ts +35 -4
  144. package/lib/tools-local.ts +29 -4
  145. package/lib/tools-message.ts +45 -30
  146. package/lib/tools-spawn.ts +22 -2
  147. package/lib/tools.ts +5 -0
  148. package/package.json +5 -3
  149. package/recipes/draft-review.json +24 -0
  150. package/recipes/tool-review.json +24 -0
  151. package/scripts/async-runner.mjs +9 -9
  152. package/scripts/build-dist.mjs +6 -1
  153. package/scripts/conformance.mjs +6 -1
  154. package/scripts/recipe-utils.mjs +3 -3
  155. package/scripts/release-gates.mjs +165 -0
  156. package/skills/actors/SKILL.md +9 -8
  157. package/skills/swarm/SKILL.md +1 -1
@@ -1,10 +1,11 @@
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
  import { spawn } from "node:child_process";
6
+ import { randomUUID } from "node:crypto";
6
7
  import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, writeFileSync, } from "node:fs";
7
- import { basename, dirname, extname, join, relative, resolve } from "node:path";
8
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
8
9
  import { fileURLToPath } from "node:url";
9
10
  import { writeJsonAtomic } from "./file-state.js";
10
11
  import { CURRENT_MODEL_VALUE_KEY, CURRENT_THINKING_VALUE_KEY, describeCurrentPolicyProvenance, } from "./model-context.js";
@@ -20,11 +21,33 @@ import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.js";
20
21
  import { captureRunProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
21
22
  import * as RunsStart from "./runs-start.js";
22
23
  import * as RunsIndex from "./runs-index.js";
24
+ import * as RunsParentTeardown from "./runs-parent-teardown.js";
23
25
  import { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
24
26
  import { deliverRunMessage, } from "./runs-messages.js";
25
- import { buildRunStatus, tailFile, tailLines } from "./runs-status.js";
27
+ import { buildRunStatus, tailFile, tailLines, } from "./runs-status.js";
26
28
  import { readJsonFileResilient } from "./state-readers.js";
27
29
  const RUNNER_IDENTITY_GRACE_MS = 5000;
30
+ export function normalizeRunTransportContext(value) {
31
+ if (!value || typeof value !== "object" || Array.isArray(value))
32
+ return undefined;
33
+ const normalized = {};
34
+ for (const [key, item] of Object.entries(value).slice(0, 16)) {
35
+ const safeKey = key.trim().slice(0, 64);
36
+ if (!safeKey)
37
+ continue;
38
+ if (typeof item === "string") {
39
+ normalized[safeKey] = item.trim().slice(0, 256);
40
+ continue;
41
+ }
42
+ if (typeof item === "number" && Number.isFinite(item)) {
43
+ normalized[safeKey] = item;
44
+ continue;
45
+ }
46
+ if (typeof item === "boolean")
47
+ normalized[safeKey] = item;
48
+ }
49
+ return Object.keys(normalized).length ? normalized : undefined;
50
+ }
28
51
  const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
29
52
  const DEFAULT_RECIPE_ROOT = Paths.getRecipeRoot();
30
53
  function packageRoot() {
@@ -89,7 +112,8 @@ function resolveRecipeFile(file) {
89
112
  function isMutableUsageRecipeFile(file) {
90
113
  const userRoot = resolve(DEFAULT_RECIPE_ROOT);
91
114
  const resolved = resolve(file);
92
- return resolved.startsWith(`${userRoot}/`);
115
+ const relation = relative(userRoot, resolved);
116
+ return relation !== "" && !relation.startsWith("..") && !isAbsolute(relation);
93
117
  }
94
118
  function readRecipeFile(file) {
95
119
  const path = resolveRecipeFile(file);
@@ -201,8 +225,8 @@ function assertCurrentPlaceholderReferencesResolved(template, defaults, values,
201
225
  throw Object.assign(new Error(`Template recipe requires the current Pi ${check.label} for inheritance, but no current ${check.label} was available. Pass explicit values or launch from a Pi session with a selected ${check.label}. unresolved=${refs.slice(0, 4).join(",")}`), { model_policy: modelPolicy });
202
226
  }
203
227
  }
204
- function acquireStateStartLock(stateDir) {
205
- return RunsStart.acquireStateStartLock(stateDir);
228
+ function acquireStateStartLock(stateDir, options = {}) {
229
+ return RunsStart.acquireStateStartLock(stateDir, options);
206
230
  }
207
231
  function prepareStateDirForStart(stateDir) {
208
232
  RunsStart.prepareStateDirForStart(stateDir, readJson, RUNNER_PATH);
@@ -228,7 +252,9 @@ export function startRun(params, cwd) {
228
252
  assertCurrentPlaceholderReferencesResolved(resolved.template, startParams.defaults, values, modelPolicy);
229
253
  assertNoActiveRunState(stateDir);
230
254
  mkdirSync(stateDir, { recursive: true });
231
- const releaseStartLock = acquireStateStartLock(stateDir);
255
+ const releaseStartLock = acquireStateStartLock(stateDir, {
256
+ onContention: startParams.lifecycleHooks?.onLockContention,
257
+ });
232
258
  try {
233
259
  claimRunStateDirectory(stateDir, run);
234
260
  assertNoActiveRunState(stateDir);
@@ -244,8 +270,10 @@ export function startRun(params, cwd) {
244
270
  const recipeContextRecords = recipeFile && includeActorRecipeContext
245
271
  ? RecipesReferences.buildRecipeContextRecords(recipeFile)
246
272
  : undefined;
247
- if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
248
- RecipesUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn");
273
+ if (recipeFile &&
274
+ isMutableUsageRecipeFile(recipeFile) &&
275
+ !RecipesUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn")) {
276
+ throw new Error(`Recipe launch rejected because its source changed during activation: ${recipeFile}. Reload recipe tools and retry.`);
249
277
  }
250
278
  const outFd = openSync(stdout, "a");
251
279
  const errFd = openSync(stderr, "a");
@@ -254,6 +282,7 @@ export function startRun(params, cwd) {
254
282
  ...(startParams.defaults || {}),
255
283
  ...values,
256
284
  };
285
+ const transportContext = normalizeRunTransportContext(startParams.transport_context);
257
286
  const artifacts = resolveArtifactPaths(startParams.artifacts, outputValues);
258
287
  const meta = {
259
288
  argv: [process.execPath, ...argv],
@@ -262,11 +291,14 @@ export function startRun(params, cwd) {
262
291
  ...(startParams.launch_source
263
292
  ? { launch_source: startParams.launch_source }
264
293
  : {}),
294
+ ...(startParams.launch_correlation
295
+ ? { launch_correlation: startParams.launch_correlation } : {}),
265
296
  ...(startParams.ownerId ? { ownerId: startParams.ownerId } : {}),
266
297
  pid: 0,
267
298
  ...(recipe ? { recipe } : {}),
268
299
  ...(recipeFile ? { recipe_file: recipeFile } : {}),
269
300
  run,
301
+ run_instance_id: randomUUID(),
270
302
  state_dir: stateDir,
271
303
  status: "running",
272
304
  ...(startParams.tool ? { tool: startParams.tool } : {}),
@@ -276,12 +308,17 @@ export function startRun(params, cwd) {
276
308
  ...(artifacts ? { artifacts } : {}),
277
309
  ...(startParams.control ? { control: startParams.control } : {}),
278
310
  ...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
311
+ ...(startParams.notification_policy === "silent"
312
+ ? { notification_policy: "silent" }
313
+ : {}),
279
314
  ...(recipeContextRecords && recipeContextRecords.length > 0
280
315
  ? { recipe_context_records: recipeContextRecords }
281
316
  : {}),
282
317
  ...(startParams.retire_when === "children_terminal"
283
318
  ? { retire_when: "children_terminal" }
284
319
  : {}),
320
+ ...(transportContext
321
+ ? { transport_context: transportContext } : {}),
285
322
  };
286
323
  writeJsonAtomic(join(stateDir, "run.json"), meta);
287
324
  const child = spawn(process.execPath, argv, {
@@ -303,7 +340,7 @@ export function startRun(params, cwd) {
303
340
  phase: "starting",
304
341
  updatedAt: new Date().toISOString(),
305
342
  });
306
- writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event: "run.start", run, pid: meta.pid, ts: new Date().toISOString() })}\n`, { flag: "a" });
343
+ writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event: "run.start", run, run_instance_id: meta.run_instance_id, pid: meta.pid, ts: new Date().toISOString() })}\n`, { flag: "a" });
307
344
  child.unref();
308
345
  return meta;
309
346
  }
@@ -312,17 +349,20 @@ export function startRun(params, cwd) {
312
349
  }
313
350
  }
314
351
  export { parseRunOutboxEventLine } from "./runs-outbox.js";
315
- export function getRunStatus(runOrDir) {
316
- const stateDir = resolve(runOrDir.includes("/")
352
+ function resolveRunStateDir(runOrDir) {
353
+ return resolve(/[\\/]/u.test(runOrDir)
317
354
  ? runOrDir
318
355
  : join(DEFAULT_STATE_ROOT, safeRunId(runOrDir)));
356
+ }
357
+ export function getRunStatus(runOrDir) {
358
+ const stateDir = resolveRunStateDir(runOrDir);
319
359
  const meta = readJson(join(stateDir, "run.json"));
320
360
  if (!meta)
321
361
  throw new Error(`Run not found: ${runOrDir}`);
322
362
  return buildRunStatus(stateDir, runOrDir, meta, readJson, RUNNER_PATH, RUNNER_IDENTITY_GRACE_MS);
323
363
  }
324
- export function listRunStateDirs(stateRoot = DEFAULT_STATE_ROOT, depth = 0, seen = new Set()) {
325
- return RunsIndex.listRunStateDirs(stateRoot, depth, seen);
364
+ export function listRunStateDirs(stateRoot = DEFAULT_STATE_ROOT) {
365
+ return RunsIndex.listRunStateDirs(stateRoot);
326
366
  }
327
367
  export function rebuildRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
328
368
  return RunsIndex.rebuildRunStateIndex(stateRoot, getRunStatus);
@@ -333,6 +373,67 @@ export function readRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
333
373
  export function listRuns(stateRoot = DEFAULT_STATE_ROOT, statusFilter) {
334
374
  return RunsIndex.listRuns(stateRoot, getRunStatus, readJson, statusFilter);
335
375
  }
376
+ export function teardownRunsOwnedByParent(ownerId, stateRoot = DEFAULT_STATE_ROOT, options = {}) {
377
+ const result = RunsParentTeardown.teardownParentRuns(ownerId, {
378
+ getRunStatus,
379
+ killRun,
380
+ listRunStatuses: () => {
381
+ const discovered = RunsIndex.discoverRunStateDirs(stateRoot, Number.POSITIVE_INFINITY);
382
+ const failures = discovered.issues.map((issue) => ({
383
+ path: issue.path,
384
+ reason: issue.reason.replaceAll("_", " "),
385
+ }));
386
+ const statuses = discovered.stateDirs.flatMap((stateDir) => {
387
+ try {
388
+ return [getRunStatus(stateDir)];
389
+ }
390
+ catch (error) {
391
+ failures.push({
392
+ path: stateDir,
393
+ reason: error instanceof Error ? error.message : String(error),
394
+ });
395
+ return [];
396
+ }
397
+ });
398
+ return { failures, statuses };
399
+ },
400
+ recordAttempt: (attempt) => {
401
+ const current = getRunStatus(attempt.stateDir);
402
+ if (!attempt.runInstanceId ||
403
+ current.run_instance_id !== attempt.runInstanceId) {
404
+ throw new Error("run generation changed before teardown evidence");
405
+ }
406
+ writeFileSync(join(attempt.stateDir, "events.jsonl"), `${JSON.stringify({
407
+ event: "run.parent_teardown",
408
+ ownerId: attempt.ownerId,
409
+ outcome: attempt.outcome,
410
+ reason: attempt.reason,
411
+ run: attempt.run,
412
+ run_instance_id: attempt.runInstanceId,
413
+ type: "control.kill",
414
+ trigger: options.trigger ?? "parent_shutdown",
415
+ ts: new Date().toISOString(),
416
+ })}\n`, { flag: "a" });
417
+ },
418
+ });
419
+ const summaryPath = join(stateRoot, "teardown", `${Date.now()}-${randomUUID()}.json`);
420
+ try {
421
+ writeJsonAtomic(summaryPath, RunsParentTeardown.buildBoundedParentTeardownSummary(result, ownerId ?? "unknown", options.trigger ?? "parent_shutdown", new Date().toISOString()));
422
+ return { ...result, summaryPath };
423
+ }
424
+ catch (error) {
425
+ const failure = {
426
+ path: summaryPath,
427
+ reason: `summary: ${error instanceof Error ? error.message : String(error)}`,
428
+ };
429
+ return {
430
+ ...result,
431
+ discoveryFailed: result.discoveryFailed + 1,
432
+ discoveryFailures: [...result.discoveryFailures, failure],
433
+ failed: result.failed + 1,
434
+ };
435
+ }
436
+ }
336
437
  export function tailRun(runOrDir, lines = 40) {
337
438
  const status = getRunStatus(runOrDir);
338
439
  const stateDir = String(status.state_dir);
@@ -441,11 +542,11 @@ function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
441
542
  return {
442
543
  attempt: index + 1,
443
544
  stdout: {
444
- path: relative(stateDir, stdoutFile),
545
+ path: relative(stateDir, stdoutFile).replaceAll("\\", "/"),
445
546
  bytes: existsSync(stdoutFile) ? statSync(stdoutFile).size : 0,
446
547
  },
447
548
  stderr: {
448
- path: relative(stateDir, stderrFile),
549
+ path: relative(stateDir, stderrFile).replaceAll("\\", "/"),
449
550
  bytes: existsSync(stderrFile) ? statSync(stderrFile).size : 0,
450
551
  },
451
552
  };
@@ -470,17 +571,38 @@ function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
470
571
  updated_at: completedAt,
471
572
  });
472
573
  }
473
- function stopRun(runOrDir, signal, event) {
474
- const status = getRunStatus(runOrDir);
475
- const pid = Number(status.pid || 0);
476
- const stateDir = String(status.state_dir);
477
- if (status.status !== "running" && status.status !== "exited") {
478
- return { stopped: false, reason: "not running", status };
479
- }
480
- const identity = verifyRunProcessIdentity(pid, status.process_identity);
481
- if (status.status === "exited") {
482
- if (identity.status === "owner_mismatch" ||
483
- identity.status === "unsupported_proof") {
574
+ function stopRun(runOrDir, signal, event, expected = {}) {
575
+ const stateDir = resolveRunStateDir(runOrDir);
576
+ const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
577
+ try {
578
+ expected.onLocked?.();
579
+ const status = getRunStatus(stateDir);
580
+ if (expected.ownerId !== undefined &&
581
+ status.ownerId !== expected.ownerId) {
582
+ return { stopped: false, reason: "ownership changed", status };
583
+ }
584
+ if (expected.runInstanceId !== undefined &&
585
+ status.run_instance_id !== expected.runInstanceId) {
586
+ return { stopped: false, reason: "run generation changed", status };
587
+ }
588
+ const pid = Number(status.pid || 0);
589
+ if (status.status !== "running" && status.status !== "exited") {
590
+ return { stopped: false, reason: "not running", status };
591
+ }
592
+ const identity = verifyRunProcessIdentity(pid, status.process_identity);
593
+ if (status.status === "exited") {
594
+ if (identity.status === "owner_mismatch" ||
595
+ identity.status === "unsupported_proof") {
596
+ return {
597
+ stopped: false,
598
+ reason: identity.status.replaceAll("_", " "),
599
+ process_identity_status: identity.status,
600
+ status,
601
+ };
602
+ }
603
+ return { stopped: false, reason: "not running", status };
604
+ }
605
+ if (!identity.valid) {
484
606
  return {
485
607
  stopped: false,
486
608
  reason: identity.status.replaceAll("_", " "),
@@ -488,28 +610,22 @@ function stopRun(runOrDir, signal, event) {
488
610
  status,
489
611
  };
490
612
  }
491
- return { stopped: false, reason: "not running", status };
492
- }
493
- if (!identity.valid) {
494
- return {
495
- stopped: false,
496
- reason: identity.status.replaceAll("_", " "),
497
- process_identity_status: identity.status,
498
- status,
499
- };
500
- }
501
- const signalResult = signalOwnedRunProcess(pid, signal);
502
- writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`, { flag: "a" });
503
- markTerminalHandled(stateDir, { event, signal });
504
- if (event === "run.kill") {
505
- finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
506
- markTerminalProgress(stateDir, "killed");
613
+ const signalResult = signalOwnedRunProcess(pid, signal, status.process_identity);
614
+ writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`, { flag: "a" });
615
+ markTerminalHandled(stateDir, { event, signal });
616
+ if (event === "run.kill") {
617
+ finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
618
+ markTerminalProgress(stateDir, "killed");
619
+ }
620
+ if (event === "run.cancel") {
621
+ finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
622
+ markTerminalProgress(stateDir, "cancelled");
623
+ }
624
+ return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
507
625
  }
508
- if (event === "run.cancel") {
509
- finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
510
- markTerminalProgress(stateDir, "cancelled");
626
+ finally {
627
+ releaseControlLock();
511
628
  }
512
- return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
513
629
  }
514
630
  export function markRunTerminalNotificationHandled(stateDir, status) {
515
631
  markTerminalHandled(stateDir, {
@@ -517,8 +633,22 @@ export function markRunTerminalNotificationHandled(stateDir, status) {
517
633
  status,
518
634
  });
519
635
  }
520
- export function cancelRun(runOrDir) {
521
- const result = stopRun(runOrDir, "SIGTERM", "run.cancel");
636
+ export function recordRunTerminalDeliveryFailure(stateDir, status, error) {
637
+ const path = join(stateDir, "terminal-delivery-failure.json");
638
+ const previous = readJson(path);
639
+ const message = (error instanceof Error ? error.message : String(error))
640
+ .replaceAll(/\s+/g, " ")
641
+ .trim()
642
+ .slice(0, 500);
643
+ writeJsonAtomic(path, {
644
+ attempts: Math.max(0, Number(previous?.attempts ?? 0)) + 1,
645
+ error: message || "unknown delivery failure",
646
+ status,
647
+ ts: new Date().toISOString(),
648
+ });
649
+ }
650
+ export function cancelRun(runOrDir, expected = {}) {
651
+ const result = stopRun(runOrDir, "SIGTERM", "run.cancel", expected);
522
652
  return Object.hasOwn(result, "stopped")
523
653
  ? { cancelled: result.stopped, ...result }
524
654
  : result;
@@ -536,8 +666,8 @@ export function archiveRun(runOrDir) {
536
666
  export function pruneRun(runOrDir, options = {}) {
537
667
  return pruneTerminalRun(assertTerminalRun(runOrDir), options);
538
668
  }
539
- export function killRun(runOrDir) {
540
- const result = stopRun(runOrDir, "SIGKILL", "run.kill");
669
+ export function killRun(runOrDir, expected = {}) {
670
+ const result = stopRun(runOrDir, "SIGKILL", "run.kill", expected);
541
671
  return Object.hasOwn(result, "stopped")
542
672
  ? { killed: result.stopped, ...result }
543
673
  : result;
@@ -0,0 +1,18 @@
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
+ import type * as Pi from "./pi.ts";
7
+ export interface AutomaticReviewRuntime {
8
+ close(): void;
9
+ handleMessage(type: string, body: unknown): Record<string, unknown>;
10
+ schedule(): void;
11
+ start(ctx: Pi.ExtensionContext): void;
12
+ }
13
+ export interface AutomaticReviewRuntimeDeps {
14
+ getActiveContext(): Pi.ExtensionContext | undefined;
15
+ getRunOwnerId(ctx: Pi.ExtensionContext): string;
16
+ getThinkingLevel(): unknown;
17
+ }
18
+ export declare function createAutomaticReviewRuntime(deps: AutomaticReviewRuntimeDeps): AutomaticReviewRuntime;
@@ -0,0 +1,96 @@
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
+ import { join } from "node:path";
7
+ import * as AsyncRuns from "./async-runs.js";
8
+ import * as DraftSleep from "./draft-sleep.js";
9
+ import * as ModelContext from "./model-context.js";
10
+ import * as Paths from "./paths.js";
11
+ import * as ReviewControl from "./review-control.js";
12
+ import * as ToolReviewScheduler from "./tool-review-scheduler.js";
13
+ export function createAutomaticReviewRuntime(deps) {
14
+ let draftScheduler;
15
+ let toolScheduler;
16
+ const close = () => {
17
+ draftScheduler?.close();
18
+ draftScheduler = undefined;
19
+ toolScheduler?.close();
20
+ toolScheduler = undefined;
21
+ };
22
+ const hasActiveActors = () => AsyncRuns.listRuns(Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, "running").length > 0;
23
+ const policyValues = (ctx) => ModelContext.withCurrentModelValues({}, {
24
+ ...ctx,
25
+ getThinkingLevel: deps.getThinkingLevel,
26
+ });
27
+ return {
28
+ close,
29
+ handleMessage(type, body) {
30
+ if (type !== "review.retry" && type !== "review.reset") {
31
+ throw new Error("tool:pi-actors accepts review.retry or review.reset messages.");
32
+ }
33
+ if (type === "review.retry" && !Paths.isAutomaticRecipeReviewEnabled()) {
34
+ throw new Error("Automatic recipe review is disabled by PI_ACTORS_AUTOMATIC_REVIEW.");
35
+ }
36
+ return ReviewControl.controlAutomaticReview(type, ReviewControl.parseAutomaticReviewScope(body), {
37
+ scheduleDraft: () => draftScheduler?.schedule(),
38
+ scheduleTool: () => toolScheduler?.schedule(),
39
+ });
40
+ },
41
+ schedule() {
42
+ draftScheduler?.schedule();
43
+ toolScheduler?.schedule();
44
+ },
45
+ start(ctx) {
46
+ close();
47
+ if (!Paths.isAutomaticRecipeReviewEnabled())
48
+ return;
49
+ ToolReviewScheduler.applyApprovedToolReviewAtSessionBoundary({
50
+ recipeRoot: Paths.getRecipeRoot(),
51
+ });
52
+ draftScheduler = DraftSleep.createDraftSleepScheduler({
53
+ hasActiveActors,
54
+ launch: (batch) => {
55
+ if (deps.getActiveContext() !== ctx) {
56
+ throw new Error("Draft review session changed before launch.");
57
+ }
58
+ return AsyncRuns.startRun({
59
+ file: join(Paths.getPackagedRecipeRoot(), "draft-review.json"),
60
+ launch_source: "tool",
61
+ notification_policy: "silent",
62
+ ownerId: deps.getRunOwnerId(ctx),
63
+ policy_values: policyValues(ctx),
64
+ run_id: DraftSleep.draftSleepRunId(batch.batchId),
65
+ values: { input_path: batch.reviewerInputPath },
66
+ }, ctx.cwd);
67
+ },
68
+ process: (state) => DraftSleep.processDraftSleepReview(state, {
69
+ getRunStatus: AsyncRuns.getRunStatus,
70
+ recipeRoot: Paths.getRecipeRoot(),
71
+ }),
72
+ });
73
+ toolScheduler = ToolReviewScheduler.createToolReviewScheduler({
74
+ hasActiveActors,
75
+ launch: (batch) => {
76
+ if (deps.getActiveContext() !== ctx) {
77
+ throw new Error("Tool review session changed before launch.");
78
+ }
79
+ return AsyncRuns.startRun({
80
+ file: join(Paths.getPackagedRecipeRoot(), "tool-review.json"),
81
+ launch_source: "tool",
82
+ notification_policy: "silent",
83
+ ownerId: deps.getRunOwnerId(ctx),
84
+ policy_values: policyValues(ctx),
85
+ run_id: ToolReviewScheduler.toolReviewRunId(batch.reviewId),
86
+ values: { input_path: batch.reviewerInputPath },
87
+ }, ctx.cwd);
88
+ },
89
+ process: (state) => ToolReviewScheduler.processToolReviewResult(state, {
90
+ getRunStatus: AsyncRuns.getRunStatus,
91
+ recipeRoot: Paths.getRecipeRoot(),
92
+ }),
93
+ });
94
+ },
95
+ };
96
+ }
@@ -344,7 +344,7 @@ export function splitCommandTemplate(input) {
344
344
  let active = false;
345
345
  for (const char of input) {
346
346
  if (escaped) {
347
- current += char;
347
+ current += /[\s'"\\]/u.test(char) ? char : `\\${char}`;
348
348
  escaped = false;
349
349
  active = true;
350
350
  continue;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Deterministic draft consolidation transaction and crash recovery.
3
+ * Owns: cycle claim, source/target compare-and-swap, complete recipe persistence, source quarantine, journal transitions, rollback, and runtime evidence.
4
+ */
5
+ import type { DraftConsolidationPlan } from "./draft-consolidation.ts";
6
+ import { type DraftConsolidationInventoryItem } from "./draft-consolidation.ts";
7
+ export type DraftConsolidationTransactionPhase = "prepared" | "targets_validated" | "sources_quarantined" | "committed_evidence_pending" | "committed" | "rolled_back" | "rollback_required";
8
+ interface TargetOperation {
9
+ backupPath?: string;
10
+ expectedSha256: string | null;
11
+ intendedSha256: string;
12
+ path: string;
13
+ recipe: Record<string, unknown>;
14
+ target: string;
15
+ }
16
+ interface SourceOperation {
17
+ path: string;
18
+ quarantinePath: string;
19
+ sha256: string;
20
+ }
21
+ interface ConsolidationRootIdentity {
22
+ dev: string;
23
+ ino: string;
24
+ path: string;
25
+ realpath: string;
26
+ }
27
+ interface ConsolidationRootIdentities {
28
+ cycleDir: ConsolidationRootIdentity;
29
+ draftRoot: ConsolidationRootIdentity;
30
+ recipeRoot: ConsolidationRootIdentity;
31
+ }
32
+ export interface DraftConsolidationJournal {
33
+ cycleId: string;
34
+ error?: string;
35
+ operationsSha256: string;
36
+ phase: DraftConsolidationTransactionPhase;
37
+ plan: DraftConsolidationPlan;
38
+ planSha256: string;
39
+ roots: ConsolidationRootIdentities;
40
+ sourceScope: "batch" | "complete";
41
+ sources: SourceOperation[];
42
+ targets: TargetOperation[];
43
+ updatedAt: string;
44
+ }
45
+ export type DraftConsolidationCheckpoint = "prepared" | "target_written" | "targets_validated" | "source_quarantined" | "sources_quarantined" | "committed_evidence_pending" | "evidence_written";
46
+ export interface DraftConsolidationTransactionOptions {
47
+ checkpoint?(point: DraftConsolidationCheckpoint): void;
48
+ cycleDir: string;
49
+ draftRoot: string;
50
+ inventory: DraftConsolidationInventoryItem[];
51
+ onLockContention?(): void;
52
+ sourceScope?: "batch" | "complete";
53
+ recipeRoot: string;
54
+ }
55
+ export interface DraftConsolidationTransactionResult {
56
+ cycleId: string;
57
+ evidencePath: string;
58
+ phase: "committed" | "rolled_back";
59
+ plan: DraftConsolidationPlan;
60
+ sources: number;
61
+ targets: number;
62
+ }
63
+ export declare function applyDraftConsolidationPlan(plan: DraftConsolidationPlan, options: DraftConsolidationTransactionOptions): DraftConsolidationTransactionResult;
64
+ export declare function recoverDraftConsolidationCycle(options: Pick<DraftConsolidationTransactionOptions, "cycleDir" | "draftRoot" | "recipeRoot">): DraftConsolidationTransactionResult;
65
+ export {};