@llblab/pi-actors 0.42.3 → 0.43.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 (252) hide show
  1. package/AGENTS.md +121 -175
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +113 -276
  4. package/dist/fixtures/protocol/control-endpoint.json +6 -0
  5. package/dist/fixtures/protocol/control-record.json +9 -0
  6. package/dist/fixtures/protocol/recipe-summary.json +4 -12
  7. package/dist/fixtures/protocol/trace-event.json +9 -0
  8. package/dist/lib/async-runs.d.ts +14 -38
  9. package/dist/lib/async-runs.js +158 -108
  10. package/dist/lib/control.d.ts +12 -0
  11. package/dist/lib/control.js +84 -0
  12. package/dist/lib/execution-sessions.d.ts +17 -0
  13. package/dist/lib/execution-sessions.js +85 -0
  14. package/dist/lib/file-state.d.ts +1 -0
  15. package/dist/lib/file-state.js +17 -5
  16. package/dist/lib/inspector-actions.d.ts +2 -2
  17. package/dist/lib/inspector-actions.js +2 -2
  18. package/dist/lib/inspector-command.js +3 -3
  19. package/dist/lib/inspector-overlay.d.ts +52 -70
  20. package/dist/lib/inspector-overlay.js +532 -905
  21. package/dist/lib/inspector.d.ts +3 -71
  22. package/dist/lib/inspector.js +19 -665
  23. package/dist/lib/limits.d.ts +4 -2
  24. package/dist/lib/limits.js +4 -2
  25. package/dist/lib/observability.d.ts +16 -16
  26. package/dist/lib/observability.js +43 -82
  27. package/dist/lib/prompts.d.ts +1 -1
  28. package/dist/lib/prompts.js +2 -2
  29. package/dist/lib/recipe-control.d.ts +7 -0
  30. package/dist/lib/recipe-control.js +39 -0
  31. package/dist/lib/recipes-discovery.js +2 -0
  32. package/dist/lib/recipes-references.d.ts +1 -14
  33. package/dist/lib/recipes-references.js +6 -21
  34. package/dist/lib/review-projection.js +1 -5
  35. package/dist/lib/run-ui-runtime.js +2 -2
  36. package/dist/lib/runs-control-delivery.d.ts +21 -0
  37. package/dist/lib/runs-control-delivery.js +127 -0
  38. package/dist/lib/runs-controls.d.ts +35 -0
  39. package/dist/lib/runs-controls.js +144 -0
  40. package/dist/lib/runs-retention.d.ts +7 -0
  41. package/dist/lib/runs-retention.js +27 -3
  42. package/dist/lib/runs-start.js +4 -2
  43. package/dist/lib/runs-status.js +11 -6
  44. package/dist/lib/runs-trace.d.ts +24 -0
  45. package/dist/lib/runs-trace.js +98 -0
  46. package/dist/lib/runtime-notifier.d.ts +1 -1
  47. package/dist/lib/runtime-notifier.js +1 -1
  48. package/dist/lib/tools-inspect.d.ts +3 -3
  49. package/dist/lib/tools-inspect.js +203 -708
  50. package/dist/lib/tools-local.js +2 -10
  51. package/dist/lib/tools-message.d.ts +7 -7
  52. package/dist/lib/tools-message.js +95 -396
  53. package/dist/lib/tools-response.d.ts +1 -4
  54. package/dist/lib/tools-response.js +5 -39
  55. package/dist/lib/tools-spawn.js +16 -28
  56. package/dist/lib/tools.js +1 -2
  57. package/dist/lib/trace-projection.d.ts +22 -0
  58. package/dist/lib/trace-projection.js +165 -0
  59. package/dist/recipes/draft-review.json +0 -10
  60. package/dist/recipes/lens-swarm.json +0 -14
  61. package/dist/recipes/music-player.json +10 -19
  62. package/dist/recipes/pipeline-architect-coordinator.json +0 -11
  63. package/dist/recipes/pipeline-artifact-bundle.json +1 -22
  64. package/dist/recipes/pipeline-artifact-report.json +1 -18
  65. package/dist/recipes/pipeline-artifact-write.json +1 -18
  66. package/dist/recipes/pipeline-async-run-ops.json +0 -12
  67. package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
  68. package/dist/recipes/pipeline-development-tasking.json +0 -12
  69. package/dist/recipes/pipeline-docs-maintenance.json +0 -12
  70. package/dist/recipes/pipeline-media-library.json +0 -12
  71. package/dist/recipes/pipeline-quorum-review.json +0 -12
  72. package/dist/recipes/pipeline-release-readiness.json +0 -12
  73. package/dist/recipes/pipeline-release-summary.json +0 -12
  74. package/dist/recipes/pipeline-repo-health.json +0 -12
  75. package/dist/recipes/pipeline-research-synthesis.json +0 -11
  76. package/dist/recipes/pipeline-review-readiness.json +0 -12
  77. package/dist/recipes/resource-locker.json +27 -0
  78. package/dist/recipes/subagent-artifact.json +0 -9
  79. package/dist/recipes/subagent-checkpoint.json +0 -10
  80. package/dist/recipes/subagent-conflict-report.json +0 -11
  81. package/dist/recipes/subagent-contradiction-map.json +0 -11
  82. package/dist/recipes/subagent-critic.json +0 -11
  83. package/dist/recipes/subagent-evidence-map.json +0 -11
  84. package/dist/recipes/subagent-followup.json +0 -10
  85. package/dist/recipes/subagent-judge.json +0 -11
  86. package/dist/recipes/subagent-merge.json +0 -11
  87. package/dist/recipes/subagent-normalize.json +0 -11
  88. package/dist/recipes/subagent-plan.json +0 -11
  89. package/dist/recipes/subagent-preflight.json +0 -11
  90. package/dist/recipes/subagent-prompt.json +0 -10
  91. package/dist/recipes/subagent-quorum.json +0 -10
  92. package/dist/recipes/subagent-review-coordinator.json +0 -14
  93. package/dist/recipes/subagent-review.json +0 -11
  94. package/dist/recipes/subagent-task-card.json +0 -11
  95. package/dist/recipes/subagent-tools.json +0 -10
  96. package/dist/recipes/subagent-verify.json +0 -11
  97. package/dist/recipes/subagents-prompts.json +0 -10
  98. package/dist/recipes/tool-review.json +0 -10
  99. package/dist/scripts/async-runner.mjs +25 -25
  100. package/dist/scripts/conformance.mjs +4 -2
  101. package/dist/scripts/locker.mjs +200 -66
  102. package/dist/scripts/music-player.mjs +159 -150
  103. package/dist/scripts/recipe-utils.mjs +6 -96
  104. package/dist/scripts/release-gates.mjs +60 -0
  105. package/dist/scripts/validate-recipe.mjs +3 -53
  106. package/dist/skills/actors/SKILL.md +53 -266
  107. package/dist/skills/swarm/SKILL.md +11 -33
  108. package/docs/0.43-baseline.md +44 -0
  109. package/docs/README.md +3 -3
  110. package/docs/actor-inspector.md +26 -64
  111. package/docs/actors-deep-reference.md +92 -50
  112. package/docs/async-runs.md +81 -328
  113. package/docs/command-templates.md +2 -2
  114. package/docs/component-recipes.md +30 -133
  115. package/docs/recipe-library.md +57 -182
  116. package/docs/task-first-recipes.md +10 -12
  117. package/docs/template-recipes.md +76 -289
  118. package/docs/tool-registry.md +41 -161
  119. package/fixtures/protocol/control-endpoint.json +6 -0
  120. package/fixtures/protocol/control-record.json +9 -0
  121. package/fixtures/protocol/recipe-summary.json +4 -12
  122. package/fixtures/protocol/trace-event.json +9 -0
  123. package/lib/async-runs.ts +202 -201
  124. package/lib/control.ts +102 -0
  125. package/lib/execution-sessions.ts +111 -0
  126. package/lib/file-state.ts +17 -4
  127. package/lib/inspector-actions.ts +2 -2
  128. package/lib/inspector-command.ts +3 -3
  129. package/lib/inspector-overlay.ts +577 -1121
  130. package/lib/inspector.ts +46 -979
  131. package/lib/limits.ts +4 -2
  132. package/lib/observability.ts +60 -101
  133. package/lib/prompts.ts +2 -2
  134. package/lib/recipe-control.ts +45 -0
  135. package/lib/recipes-discovery.ts +2 -0
  136. package/lib/recipes-references.ts +9 -45
  137. package/lib/review-projection.ts +1 -5
  138. package/lib/run-ui-runtime.ts +2 -2
  139. package/lib/runs-control-delivery.ts +181 -0
  140. package/lib/runs-controls.ts +204 -0
  141. package/lib/runs-retention.ts +38 -3
  142. package/lib/runs-start.ts +4 -2
  143. package/lib/runs-status.ts +11 -6
  144. package/lib/runs-trace.ts +132 -0
  145. package/lib/runtime-notifier.ts +1 -1
  146. package/lib/tools-inspect.ts +240 -901
  147. package/lib/tools-local.ts +2 -12
  148. package/lib/tools-message.ts +112 -519
  149. package/lib/tools-response.ts +5 -52
  150. package/lib/tools-spawn.ts +16 -32
  151. package/lib/tools.ts +1 -2
  152. package/lib/trace-projection.ts +221 -0
  153. package/package.json +2 -1
  154. package/recipes/draft-review.json +0 -10
  155. package/recipes/lens-swarm.json +0 -14
  156. package/recipes/music-player.json +10 -19
  157. package/recipes/pipeline-architect-coordinator.json +0 -11
  158. package/recipes/pipeline-artifact-bundle.json +1 -22
  159. package/recipes/pipeline-artifact-report.json +1 -18
  160. package/recipes/pipeline-artifact-write.json +1 -18
  161. package/recipes/pipeline-async-run-ops.json +0 -12
  162. package/recipes/pipeline-checkpoint-continuation.json +0 -14
  163. package/recipes/pipeline-development-tasking.json +0 -12
  164. package/recipes/pipeline-docs-maintenance.json +0 -12
  165. package/recipes/pipeline-media-library.json +0 -12
  166. package/recipes/pipeline-quorum-review.json +0 -12
  167. package/recipes/pipeline-release-readiness.json +0 -12
  168. package/recipes/pipeline-release-summary.json +0 -12
  169. package/recipes/pipeline-repo-health.json +0 -12
  170. package/recipes/pipeline-research-synthesis.json +0 -11
  171. package/recipes/pipeline-review-readiness.json +0 -12
  172. package/recipes/resource-locker.json +27 -0
  173. package/recipes/subagent-artifact.json +0 -9
  174. package/recipes/subagent-checkpoint.json +0 -10
  175. package/recipes/subagent-conflict-report.json +0 -11
  176. package/recipes/subagent-contradiction-map.json +0 -11
  177. package/recipes/subagent-critic.json +0 -11
  178. package/recipes/subagent-evidence-map.json +0 -11
  179. package/recipes/subagent-followup.json +0 -10
  180. package/recipes/subagent-judge.json +0 -11
  181. package/recipes/subagent-merge.json +0 -11
  182. package/recipes/subagent-normalize.json +0 -11
  183. package/recipes/subagent-plan.json +0 -11
  184. package/recipes/subagent-preflight.json +0 -11
  185. package/recipes/subagent-prompt.json +0 -10
  186. package/recipes/subagent-quorum.json +0 -10
  187. package/recipes/subagent-review-coordinator.json +0 -14
  188. package/recipes/subagent-review.json +0 -11
  189. package/recipes/subagent-task-card.json +0 -11
  190. package/recipes/subagent-tools.json +0 -10
  191. package/recipes/subagent-verify.json +0 -11
  192. package/recipes/subagents-prompts.json +0 -10
  193. package/recipes/tool-review.json +0 -10
  194. package/scripts/async-runner.mjs +25 -25
  195. package/scripts/conformance.mjs +4 -2
  196. package/scripts/locker.mjs +200 -66
  197. package/scripts/music-player.mjs +159 -150
  198. package/scripts/recipe-utils.mjs +6 -96
  199. package/scripts/release-gates.mjs +60 -0
  200. package/scripts/validate-recipe.mjs +3 -53
  201. package/skills/actors/SKILL.md +53 -266
  202. package/skills/swarm/SKILL.md +11 -33
  203. package/dist/fixtures/protocol/actor-message-branch.json +0 -13
  204. package/dist/fixtures/protocol/mailbox-contract.json +0 -15
  205. package/dist/fixtures/protocol/room-message.json +0 -11
  206. package/dist/fixtures/protocol/room-roster.json +0 -11
  207. package/dist/fixtures/protocol/run-inbox-message.json +0 -9
  208. package/dist/fixtures/protocol/run-outbox-event.json +0 -9
  209. package/dist/lib/mailbox-loop.d.ts +0 -41
  210. package/dist/lib/mailbox-loop.js +0 -60
  211. package/dist/lib/messages.d.ts +0 -25
  212. package/dist/lib/messages.js +0 -122
  213. package/dist/lib/rooms.d.ts +0 -104
  214. package/dist/lib/rooms.js +0 -647
  215. package/dist/lib/runs-mailbox.d.ts +0 -25
  216. package/dist/lib/runs-mailbox.js +0 -146
  217. package/dist/lib/runs-messages.d.ts +0 -15
  218. package/dist/lib/runs-messages.js +0 -179
  219. package/dist/lib/runs-outbox.d.ts +0 -41
  220. package/dist/lib/runs-outbox.js +0 -87
  221. package/dist/lib/tools-mailbox.d.ts +0 -8
  222. package/dist/lib/tools-mailbox.js +0 -48
  223. package/dist/recipes/actor-worker.json +0 -39
  224. package/dist/recipes/coordinator-locker.json +0 -45
  225. package/dist/recipes/locker.json +0 -45
  226. package/dist/recipes/pipeline-room-swarm.json +0 -50
  227. package/dist/recipes/subagent-message.json +0 -32
  228. package/dist/recipes/utility-actor-message.json +0 -23
  229. package/dist/scripts/actor-worker.mjs +0 -214
  230. package/dist/scripts/coordinator.mjs +0 -799
  231. package/docs/actor-messages.md +0 -225
  232. package/fixtures/protocol/actor-message-branch.json +0 -13
  233. package/fixtures/protocol/mailbox-contract.json +0 -15
  234. package/fixtures/protocol/room-message.json +0 -11
  235. package/fixtures/protocol/room-roster.json +0 -11
  236. package/fixtures/protocol/run-inbox-message.json +0 -9
  237. package/fixtures/protocol/run-outbox-event.json +0 -9
  238. package/lib/mailbox-loop.ts +0 -144
  239. package/lib/messages.ts +0 -151
  240. package/lib/rooms.ts +0 -939
  241. package/lib/runs-mailbox.ts +0 -208
  242. package/lib/runs-messages.ts +0 -252
  243. package/lib/runs-outbox.ts +0 -144
  244. package/lib/tools-mailbox.ts +0 -56
  245. package/recipes/actor-worker.json +0 -39
  246. package/recipes/coordinator-locker.json +0 -45
  247. package/recipes/locker.json +0 -45
  248. package/recipes/pipeline-room-swarm.json +0 -50
  249. package/recipes/subagent-message.json +0 -32
  250. package/recipes/utility-actor-message.json +0 -23
  251. package/scripts/actor-worker.mjs +0 -214
  252. package/scripts/coordinator.mjs +0 -799
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { spawn } from "node:child_process";
6
6
  import { randomUUID } from "node:crypto";
7
- import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, writeFileSync, } from "node:fs";
7
+ import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, } from "node:fs";
8
8
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { writeJsonAtomic } from "./file-state.js";
@@ -15,16 +15,16 @@ import * as RecipesUsage from "./recipes-usage.js";
15
15
  import { resolveArtifactPaths, } from "./runs-artifacts.js";
16
16
  import { safeRunId } from "./runs-identity.js";
17
17
  import { buildTerminalProgress, markTerminalHandled, signalOwnedRunProcess, } from "./runs-control.js";
18
- import { buildRunOutboxEventPayload, parseRunOutboxEventLine, } from "./runs-outbox.js";
19
18
  import { claimRunStateDirectory } from "./runs-ownership.js";
20
- import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.js";
19
+ import { appendRunRetentionEvidence, archiveTerminalRun, pruneTerminalRun, } from "./runs-retention.js";
21
20
  import { captureRunProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
22
21
  import * as RunsStart from "./runs-start.js";
22
+ import { appendRunTraceEvent } from "./runs-trace.js";
23
23
  import * as RunsIndex from "./runs-index.js";
24
24
  import * as RunsParentTeardown from "./runs-parent-teardown.js";
25
- import { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
26
- import { deliverRunMessage, } from "./runs-messages.js";
27
- import { buildRunStatus, tailFile, tailLines, } from "./runs-status.js";
25
+ import { appendRunControlInStateDir, updateRunControlStatusInStateDir, } from "./runs-controls.js";
26
+ import { deliverRunControl, } from "./runs-control-delivery.js";
27
+ import { buildRunStatus, tailFile, } from "./runs-status.js";
28
28
  import { readJsonFileResilient } from "./state-readers.js";
29
29
  const RUNNER_IDENTITY_GRACE_MS = 5000;
30
30
  export function normalizeRunTransportContext(value) {
@@ -238,11 +238,9 @@ export function startRun(params, cwd) {
238
238
  const stateDir = resolveStateDir(startParams, run);
239
239
  const values = {
240
240
  ...(startParams.values || {}),
241
- actor_address: `run:${run}`,
242
- communication_file: join(stateDir, "communication.json"),
243
- default_room: `room:${run}`,
244
241
  run_id: run,
245
242
  state_dir: stateDir,
243
+ trace_file: join(stateDir, "trace.jsonl"),
246
244
  };
247
245
  const modelPolicy = describeCurrentPolicyProvenance({
248
246
  defaults: startParams.defaults,
@@ -300,6 +298,7 @@ export function startRun(params, cwd) {
300
298
  run,
301
299
  run_instance_id: randomUUID(),
302
300
  state_dir: stateDir,
301
+ state_schema: "run-kernel-v1",
303
302
  status: "running",
304
303
  ...(startParams.tool ? { tool: startParams.tool } : {}),
305
304
  template: resolved.template,
@@ -307,7 +306,9 @@ export function startRun(params, cwd) {
307
306
  model_policy: modelPolicy,
308
307
  ...(artifacts ? { artifacts } : {}),
309
308
  ...(startParams.control ? { control: startParams.control } : {}),
310
- ...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
309
+ ...(startParams.control_endpoint
310
+ ? { control_endpoint: startParams.control_endpoint }
311
+ : {}),
311
312
  ...(startParams.notification_policy === "silent"
312
313
  ? { notification_policy: "silent" }
313
314
  : {}),
@@ -340,7 +341,14 @@ export function startRun(params, cwd) {
340
341
  if (processIdentity)
341
342
  meta.process_identity = processIdentity;
342
343
  writeJsonAtomic(join(stateDir, "run.json"), meta);
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" });
344
+ appendRunTraceEvent(stateDir, {
345
+ kind: "run.start",
346
+ summary: `Run ${run} started`,
347
+ data: {
348
+ pid: meta.pid,
349
+ run_instance_id: meta.run_instance_id,
350
+ },
351
+ });
344
352
  child.unref();
345
353
  return meta;
346
354
  }
@@ -348,7 +356,6 @@ export function startRun(params, cwd) {
348
356
  releaseStartLock();
349
357
  }
350
358
  }
351
- export { parseRunOutboxEventLine } from "./runs-outbox.js";
352
359
  function resolveRunStateDir(runOrDir) {
353
360
  return resolve(/[\\/]/u.test(runOrDir)
354
361
  ? runOrDir
@@ -403,17 +410,19 @@ export function teardownRunsOwnedByParent(ownerId, stateRoot = DEFAULT_STATE_ROO
403
410
  current.run_instance_id !== attempt.runInstanceId) {
404
411
  throw new Error("run generation changed before teardown evidence");
405
412
  }
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" });
413
+ appendRunTraceEvent(attempt.stateDir, {
414
+ kind: "run.parent_teardown",
415
+ summary: `Parent teardown ${attempt.outcome}`,
416
+ data: {
417
+ outcome: attempt.outcome,
418
+ owner_id: attempt.ownerId,
419
+ reason: attempt.reason,
420
+ run: attempt.run,
421
+ run_instance_id: attempt.runInstanceId,
422
+ trigger: options.trigger ?? "parent_shutdown",
423
+ },
424
+ ...(attempt.outcome === "failed" ? { level: "error" } : {}),
425
+ });
417
426
  },
418
427
  });
419
428
  const summaryPath = join(stateRoot, "teardown", `${Date.now()}-${randomUUID()}.json`);
@@ -437,73 +446,43 @@ export function teardownRunsOwnedByParent(ownerId, stateRoot = DEFAULT_STATE_ROO
437
446
  export function tailRun(runOrDir, lines = 40) {
438
447
  const status = getRunStatus(runOrDir);
439
448
  const stateDir = String(status.state_dir);
440
- const events = tailFile(join(stateDir, "events.jsonl"), lines);
441
- if (events)
442
- return events;
449
+ const trace = tailFile(join(stateDir, "trace.jsonl"), lines);
450
+ if (trace)
451
+ return trace;
443
452
  return (tailFile(join(stateDir, "stdout.log"), lines) ||
444
453
  tailFile(join(stateDir, "stderr.log"), lines));
445
454
  }
446
- export function readRunEvents(runOrDir, lines = 40) {
447
- const status = getRunStatus(runOrDir);
448
- const stateDir = String(status.state_dir);
449
- const run = String(status.run ?? runOrDir);
450
- return tailLines(join(stateDir, "outbox.jsonl"), lines)
451
- .map((line, index) => parseRunOutboxEventLine(line, run, stateDir, index))
452
- .filter((event) => Boolean(event));
453
- }
454
- export function readRunInboxMessages(runOrDir, lines = 40) {
455
- const status = getRunStatus(runOrDir);
456
- const stateDir = String(status.state_dir);
457
- return tailLines(runInboxFile(stateDir), lines)
458
- .map(parseRunInboxLine)
459
- .filter((message) => Boolean(message));
460
- }
461
- export function updateRunInboxMessageStatus(runOrDir, id, nextStatus, metadata = {}) {
462
- const status = getRunStatus(runOrDir);
463
- const stateDir = String(status.state_dir);
464
- return updateRunInboxMessageStatusInStateDir(stateDir, id, nextStatus, metadata);
465
- }
466
- export function claimRunInboxMessage(runOrDir, owner = "runtime", statuses = ["queued"]) {
467
- const status = getRunStatus(runOrDir);
468
- const stateDir = String(status.state_dir);
469
- return claimRunInboxMessageInStateDir(stateDir, owner, statuses);
470
- }
471
- export async function processRunInboxMessages(runOrDir, handler, options = {}) {
472
- const status = getRunStatus(runOrDir);
473
- return processRunInboxMessagesInStateDir(String(status.state_dir), handler, options);
474
- }
475
- export function appendRunOutboxEvent(runOrDir, event) {
476
- const status = getRunStatus(runOrDir);
477
- const stateDir = String(status.state_dir);
478
- const run = String(status.run ?? runOrDir);
479
- const payload = buildRunOutboxEventPayload(run, event);
480
- const line = `${JSON.stringify(payload)}\n`;
481
- writeFileSync(join(stateDir, "outbox.jsonl"), line, { flag: "a" });
482
- return {
483
- bytes: Buffer.byteLength(line),
484
- outbox: "outbox.jsonl",
485
- run,
486
- sent: true,
487
- state_dir: stateDir,
488
- };
489
- }
490
- export async function sendRunMessage(runOrDir, message, options = {}) {
491
- const status = getRunStatus(runOrDir);
492
- const stateDir = String(status.state_dir);
493
- const run = String(status.run ?? runOrDir);
494
- const pid = Number(status.pid || 0);
495
- const identity = verifyRunProcessIdentity(pid, status.process_identity);
496
- if (status.status !== "running") {
497
- if (identity.status === "owner_mismatch" ||
498
- identity.status === "unsupported_proof") {
499
- throw new Error(`Run process identity ${identity.status}: ${run}`);
455
+ export async function sendRunControl(runOrDir, request, options = {}) {
456
+ const stateDir = resolveRunStateDir(runOrDir);
457
+ const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
458
+ try {
459
+ const status = getRunStatus(stateDir);
460
+ const run = String(status.run ?? runOrDir);
461
+ if (options.ownerId !== undefined && status.ownerId !== options.ownerId) {
462
+ throw Object.assign(new Error(`Run ownership changed: ${run}`), {
463
+ reason: "owner_mismatch",
464
+ });
465
+ }
466
+ if (status.run_instance_id !== request.run_instance_id) {
467
+ throw Object.assign(new Error(`Run generation changed: ${run}`), {
468
+ reason: "generation_mismatch",
469
+ });
500
470
  }
501
- throw new Error(`Run is not running: ${run}`);
471
+ if (status.status !== "running") {
472
+ throw Object.assign(new Error(`Run is not running: ${run}`), {
473
+ reason: "terminal_state",
474
+ });
475
+ }
476
+ const pid = Number(status.pid || 0);
477
+ const identity = verifyRunProcessIdentity(pid, status.process_identity);
478
+ if (!identity.valid) {
479
+ throw Object.assign(new Error(`Run process identity ${identity.status}: ${run}`), { reason: `process_identity_${identity.status}` });
480
+ }
481
+ return await deliverRunControl(run, stateDir, request, options);
502
482
  }
503
- if (!identity.valid) {
504
- throw new Error(`Run process identity ${identity.status}: ${run}`);
483
+ finally {
484
+ releaseControlLock();
505
485
  }
506
- return deliverRunMessage(status, run, stateDir, message, options);
507
486
  }
508
487
  export { getRunProcessSignalPlan } from "./runs-control.js";
509
488
  function markTerminalProgress(stateDir, phase) {
@@ -513,9 +492,9 @@ function markTerminalProgress(stateDir, phase) {
513
492
  : undefined;
514
493
  writeJsonAtomic(join(stateDir, "progress.json"), buildTerminalProgress(progress, phase));
515
494
  }
516
- function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
517
- const evidencePath = join(stateDir, "review-evidence.json");
518
- const manifest = readJson(evidencePath);
495
+ function finalizeInterruptedExecution(stateDir, phase, signal) {
496
+ const executionPath = join(stateDir, "execution.json");
497
+ const manifest = readJson(executionPath);
519
498
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
520
499
  return;
521
500
  const record = manifest;
@@ -564,7 +543,7 @@ function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
564
543
  : {}),
565
544
  };
566
545
  });
567
- writeJsonAtomic(evidencePath, {
546
+ writeJsonAtomic(executionPath, {
568
547
  ...record,
569
548
  status: phase,
570
549
  commands,
@@ -585,43 +564,80 @@ function stopRun(runOrDir, signal, event, expected = {}) {
585
564
  status.run_instance_id !== expected.runInstanceId) {
586
565
  return { stopped: false, reason: "run generation changed", status };
587
566
  }
567
+ const control = event === "run.kill" && typeof status.run_instance_id === "string"
568
+ ? appendRunControlInStateDir(stateDir, {
569
+ action: "kill",
570
+ run_instance_id: status.run_instance_id,
571
+ })
572
+ : undefined;
573
+ if (control) {
574
+ updateRunControlStatusInStateDir(stateDir, control.id, "claimed", {}, ["queued"]);
575
+ }
576
+ const finish = (result) => {
577
+ if (!control)
578
+ return result;
579
+ const handled = result.stopped === true;
580
+ updateRunControlStatusInStateDir(stateDir, control.id, handled ? "handled" : "failed", handled ? {} : { error: String(result.reason ?? "kill rejected") }, ["claimed"]);
581
+ return { ...result, control_id: control.id };
582
+ };
588
583
  const pid = Number(status.pid || 0);
589
584
  if (status.status !== "running" && status.status !== "exited") {
590
- return { stopped: false, reason: "not running", status };
585
+ return finish({ stopped: false, reason: "not running", status });
591
586
  }
592
587
  const identity = verifyRunProcessIdentity(pid, status.process_identity);
593
588
  if (status.status === "exited") {
594
589
  if (identity.status === "owner_mismatch" ||
595
590
  identity.status === "unsupported_proof") {
596
- return {
591
+ return finish({
597
592
  stopped: false,
598
593
  reason: identity.status.replaceAll("_", " "),
599
594
  process_identity_status: identity.status,
600
595
  status,
601
- };
596
+ });
602
597
  }
603
- return { stopped: false, reason: "not running", status };
598
+ return finish({ stopped: false, reason: "not running", status });
604
599
  }
605
600
  if (!identity.valid) {
606
- return {
601
+ return finish({
607
602
  stopped: false,
608
603
  reason: identity.status.replaceAll("_", " "),
609
604
  process_identity_status: identity.status,
610
605
  status,
611
- };
606
+ });
607
+ }
608
+ let signalResult;
609
+ try {
610
+ signalResult = signalOwnedRunProcess(pid, signal, status.process_identity);
611
+ }
612
+ catch (error) {
613
+ if (control) {
614
+ updateRunControlStatusInStateDir(stateDir, control.id, "failed", {
615
+ error: error instanceof Error ? error.message : String(error),
616
+ });
617
+ }
618
+ throw error;
612
619
  }
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" });
620
+ appendRunTraceEvent(stateDir, {
621
+ kind: event,
622
+ summary: `${event === "run.kill" ? "Killed" : "Cancelled"} Run process`,
623
+ data: { pid, signal, ...signalResult },
624
+ });
615
625
  markTerminalHandled(stateDir, { event, signal });
616
626
  if (event === "run.kill") {
617
- finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
627
+ finalizeInterruptedExecution(stateDir, "killed", signal);
618
628
  markTerminalProgress(stateDir, "killed");
619
629
  }
620
630
  if (event === "run.cancel") {
621
- finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
631
+ finalizeInterruptedExecution(stateDir, "cancelled", signal);
622
632
  markTerminalProgress(stateDir, "cancelled");
623
633
  }
624
- return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
634
+ return finish({
635
+ stopped: true,
636
+ pid,
637
+ signal,
638
+ ...signalResult,
639
+ state_dir: stateDir,
640
+ });
625
641
  }
626
642
  finally {
627
643
  releaseControlLock();
@@ -653,18 +669,52 @@ export function cancelRun(runOrDir, expected = {}) {
653
669
  ? { cancelled: result.stopped, ...result }
654
670
  : result;
655
671
  }
656
- function assertTerminalRun(runOrDir) {
657
- const status = getRunStatus(runOrDir);
658
- if (status.status === "running") {
659
- throw new Error("Only terminal runs can be archived or pruned.");
672
+ function retainRun(runOrDir, action, expected, options = {}) {
673
+ const stateDir = resolveRunStateDir(runOrDir);
674
+ const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
675
+ let status;
676
+ let evidenceId;
677
+ try {
678
+ expected.onLocked?.();
679
+ status = getRunStatus(stateDir);
680
+ if (expected.ownerId !== undefined && status.ownerId !== expected.ownerId) {
681
+ return { [`${action}d`]: false, reason: "ownership changed", status };
682
+ }
683
+ if (expected.runInstanceId !== undefined &&
684
+ status.run_instance_id !== expected.runInstanceId) {
685
+ return { [`${action}d`]: false, reason: "run generation changed", status };
686
+ }
687
+ if (status.status === "running") {
688
+ throw new Error("Only terminal runs can be archived or pruned.");
689
+ }
690
+ evidenceId = appendRunRetentionEvidence(status, action, "queued");
691
+ const result = action === "archive"
692
+ ? archiveTerminalRun(status)
693
+ : pruneTerminalRun(status, options);
694
+ appendRunRetentionEvidence(status, action, "handled", {
695
+ id: evidenceId,
696
+ result,
697
+ });
698
+ return { ...result, retention_id: evidenceId };
699
+ }
700
+ catch (error) {
701
+ if (status && evidenceId) {
702
+ appendRunRetentionEvidence(status, action, "failed", {
703
+ error: error instanceof Error ? error.message : String(error),
704
+ id: evidenceId,
705
+ });
706
+ }
707
+ throw error;
708
+ }
709
+ finally {
710
+ releaseControlLock();
660
711
  }
661
- return status;
662
712
  }
663
- export function archiveRun(runOrDir) {
664
- return archiveTerminalRun(assertTerminalRun(runOrDir));
713
+ export function archiveRun(runOrDir, expected = {}) {
714
+ return retainRun(runOrDir, "archive", expected);
665
715
  }
666
- export function pruneRun(runOrDir, options = {}) {
667
- return pruneTerminalRun(assertTerminalRun(runOrDir), options);
716
+ export function pruneRun(runOrDir, options = {}, expected = {}) {
717
+ return retainRun(runOrDir, "prune", expected, options);
668
718
  }
669
719
  export function killRun(runOrDir, expected = {}) {
670
720
  const result = stopRun(runOrDir, "SIGKILL", "run.kill", expected);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Run Control request contract.
3
+ * Zones: public request validation, Run/runtime target normalization, input bounds
4
+ * Owns pure Control validation; journaling, delivery, and lifecycle mutation stay in adapters.
5
+ */
6
+ export interface ControlRequest {
7
+ target: `run:${string}` | "runtime";
8
+ action: string;
9
+ input?: unknown;
10
+ verbose?: boolean;
11
+ }
12
+ export declare function normalizeControlRequest(input: unknown): ControlRequest;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Run Control request contract.
3
+ * Zones: public request validation, Run/runtime target normalization, input bounds
4
+ * Owns pure Control validation; journaling, delivery, and lifecycle mutation stay in adapters.
5
+ */
6
+ import * as Limits from "./limits.js";
7
+ const RUN_ID_PATTERN = /^[A-Za-z0-9_.-]+$/;
8
+ const ACTION_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)*$/;
9
+ const CONTROL_FIELDS = new Set(["action", "input", "target", "verbose"]);
10
+ const REMOVED_FIELDS = new Set([
11
+ "body",
12
+ "correlation_id",
13
+ "from",
14
+ "metadata",
15
+ "reply_to",
16
+ "summary",
17
+ "to",
18
+ "type",
19
+ ]);
20
+ function serializedInputBytes(input) {
21
+ let serialized;
22
+ try {
23
+ serialized = JSON.stringify(input);
24
+ }
25
+ catch {
26
+ throw new Error("control.input must be JSON-serializable");
27
+ }
28
+ if (serialized === undefined) {
29
+ throw new Error("control.input must be JSON-serializable");
30
+ }
31
+ return Buffer.byteLength(serialized);
32
+ }
33
+ function normalizeTarget(value) {
34
+ if (typeof value !== "string" || !value.trim()) {
35
+ throw new Error("control.target is required");
36
+ }
37
+ const target = value.trim();
38
+ if (target === "runtime")
39
+ return target;
40
+ if (!target.startsWith("run:")) {
41
+ throw new Error(`unsupported control target: ${target}; use run:<id> or runtime`);
42
+ }
43
+ const run = target.slice(4);
44
+ if (!RUN_ID_PATTERN.test(run)) {
45
+ throw new Error(`invalid control Run target: ${target}`);
46
+ }
47
+ return `run:${run}`;
48
+ }
49
+ export function normalizeControlRequest(input) {
50
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
51
+ throw new Error("control request must be an object");
52
+ }
53
+ const record = input;
54
+ const fields = Object.keys(record);
55
+ const removed = fields.filter((field) => REMOVED_FIELDS.has(field));
56
+ if (removed.length > 0) {
57
+ throw new Error(`actor-message fields are removed: ${removed.sort().join(", ")}; use target, action, input, verbose`);
58
+ }
59
+ const unknown = fields.filter((field) => !CONTROL_FIELDS.has(field));
60
+ if (unknown.length > 0) {
61
+ throw new Error(`unsupported control fields: ${unknown.sort().join(", ")}`);
62
+ }
63
+ const target = normalizeTarget(record.target);
64
+ if (typeof record.action !== "string" || !record.action.trim()) {
65
+ throw new Error("control.action is required");
66
+ }
67
+ const action = record.action.trim();
68
+ if (!ACTION_PATTERN.test(action)) {
69
+ throw new Error(`invalid control action: ${action}`);
70
+ }
71
+ if (record.verbose !== undefined && typeof record.verbose !== "boolean") {
72
+ throw new Error("control.verbose must be a boolean");
73
+ }
74
+ if (record.input !== undefined &&
75
+ serializedInputBytes(record.input) > Limits.CONTROL_INPUT_MAX_BYTES) {
76
+ throw new Error(`control.input exceeds ${Limits.CONTROL_INPUT_MAX_BYTES} bytes`);
77
+ }
78
+ return {
79
+ target,
80
+ action,
81
+ ...(record.input !== undefined ? { input: record.input } : {}),
82
+ ...(record.verbose !== undefined ? { verbose: record.verbose } : {}),
83
+ };
84
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Run-owned execution session evidence.
3
+ * Zones: execution manifest session discovery, canonical containment, rich turn correlation
4
+ * Owns reusable agent-turn loading; Trace and TUI projection stay in their adapters.
5
+ */
6
+ import * as SessionEvidence from "./session-evidence.ts";
7
+ export interface ExecutionTurnItem extends SessionEvidence.SessionEvidenceTurn {
8
+ commandId: string;
9
+ diagnostics: string[];
10
+ promptBytes?: number;
11
+ promptFile?: string;
12
+ recipeContext?: unknown;
13
+ sessionFile: string;
14
+ sessionTruncated: boolean;
15
+ stage?: string;
16
+ }
17
+ export declare function readExecutionTurns(stateDir: string): ExecutionTurnItem[];
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Run-owned execution session evidence.
3
+ * Zones: execution manifest session discovery, canonical containment, rich turn correlation
4
+ * Owns reusable agent-turn loading; Trace and TUI projection stay in their adapters.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import * as SessionEvidence from "./session-evidence.js";
9
+ import { readJsonFileResilient } from "./state-readers.js";
10
+ function ownedSessionPath(stateDir, sessionFile) {
11
+ if (path.isAbsolute(sessionFile))
12
+ return undefined;
13
+ const sessionsRoot = path.resolve(stateDir, "sessions");
14
+ const resolved = path.resolve(stateDir, sessionFile);
15
+ if (!resolved.startsWith(`${sessionsRoot}${path.sep}`))
16
+ return undefined;
17
+ try {
18
+ const canonicalRoot = fs.realpathSync(sessionsRoot);
19
+ const canonicalFile = fs.realpathSync(resolved);
20
+ return canonicalFile.startsWith(`${canonicalRoot}${path.sep}`)
21
+ ? canonicalFile
22
+ : undefined;
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ export function readExecutionTurns(stateDir) {
29
+ const execution = readJsonFileResilient(path.join(stateDir, "execution.json"), {}).value;
30
+ const commands = Array.isArray(execution.commands)
31
+ ? execution.commands
32
+ : [];
33
+ const recordedFiles = new Set(commands.flatMap((command) => (Array.isArray(command.session_files) ? command.session_files : []).filter((file) => typeof file === "string")));
34
+ let discoveredCommands = [];
35
+ try {
36
+ const sessionsDir = path.join(stateDir, "sessions");
37
+ discoveredCommands = fs
38
+ .readdirSync(sessionsDir, { withFileTypes: true })
39
+ .filter((entry) => entry.isDirectory())
40
+ .flatMap((entry) => {
41
+ const files = fs
42
+ .readdirSync(path.join(sessionsDir, entry.name), {
43
+ withFileTypes: true,
44
+ })
45
+ .filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
46
+ .map((file) => path.posix.join("sessions", entry.name, file.name))
47
+ .filter((file) => !recordedFiles.has(file));
48
+ return files.length > 0
49
+ ? [{ id: entry.name, session_files: files, stage: "subagent" }]
50
+ : [];
51
+ });
52
+ }
53
+ catch {
54
+ discoveredCommands = [];
55
+ }
56
+ return [...commands, ...discoveredCommands]
57
+ .flatMap((command) => (Array.isArray(command.session_files) ? command.session_files : [])
58
+ .filter((file) => typeof file === "string")
59
+ .flatMap((file) => {
60
+ const sessionPath = ownedSessionPath(stateDir, file);
61
+ if (!sessionPath)
62
+ return [];
63
+ const session = SessionEvidence.readSessionEvidence(sessionPath);
64
+ return session.turns.map((turn) => ({
65
+ ...turn,
66
+ commandId: String(command.id ?? "unknown"),
67
+ diagnostics: session.diagnostics.map((item) => `${item.line === undefined ? "" : `line ${item.line}: `}${item.message}`),
68
+ ...(typeof command.prompt_bytes === "number"
69
+ ? { promptBytes: command.prompt_bytes }
70
+ : {}),
71
+ ...(typeof command.prompt_file === "string"
72
+ ? { promptFile: command.prompt_file }
73
+ : {}),
74
+ ...(command.recipe_context !== undefined
75
+ ? { recipeContext: command.recipe_context }
76
+ : {}),
77
+ sessionFile: file,
78
+ sessionTruncated: session.truncated,
79
+ ...(typeof command.stage === "string"
80
+ ? { stage: command.stage }
81
+ : {}),
82
+ }));
83
+ }))
84
+ .map((turn, index) => ({ ...turn, index: index + 1 }));
85
+ }
@@ -11,4 +11,5 @@ export interface FileMutationLockOptions {
11
11
  }
12
12
  export declare function acquireFileMutationLock(path: string, options?: FileMutationLockOptions): () => void;
13
13
  export declare function withFileMutationLock<T>(path: string, mutate: () => T, options?: FileMutationLockOptions): T;
14
+ export declare function writeTextAtomic(path: string, content: string): void;
14
15
  export declare function writeJsonAtomic(path: string, value: unknown): void;
@@ -4,7 +4,7 @@
4
4
  * Owns generic durable JSON file writes shared by registry config and async run state.
5
5
  */
6
6
  import { createHash, randomUUID } from "node:crypto";
7
- import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
7
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
8
8
  import { tmpdir } from "node:os";
9
9
  import { basename, dirname, join, parse, resolve } from "node:path";
10
10
  const FILE_MUTATION_LOCK_TIMEOUT_MS = 5000;
@@ -12,8 +12,17 @@ const FILE_MUTATION_LOCK_STALE_MS = 30000;
12
12
  const FILE_MUTATION_LOCK_ROOT = join(tmpdir(), "pi-actors-file-locks");
13
13
  function canonicalMutationPath(path) {
14
14
  const absolute = resolve(path);
15
- const suffix = [];
16
- let existing = absolute;
15
+ try {
16
+ if (lstatSync(absolute).isSymbolicLink()) {
17
+ const target = realpathSync.native(absolute);
18
+ return process.platform === "win32" ? target.toLowerCase() : target;
19
+ }
20
+ }
21
+ catch {
22
+ /* Materialization may race lock-key derivation; canonicalize through the parent. */
23
+ }
24
+ const suffix = [basename(absolute)];
25
+ let existing = dirname(absolute);
17
26
  while (!existsSync(existing)) {
18
27
  const parent = dirname(existing);
19
28
  if (parent === existing || existing === parse(existing).root)
@@ -191,11 +200,11 @@ export function withFileMutationLock(path, mutate, options = {}) {
191
200
  release();
192
201
  }
193
202
  }
194
- export function writeJsonAtomic(path, value) {
203
+ export function writeTextAtomic(path, content) {
195
204
  mkdirSync(dirname(path), { recursive: true });
196
205
  const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
197
206
  try {
198
- writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
207
+ writeFileSync(tempPath, content, "utf8");
199
208
  renameSync(tempPath, path);
200
209
  }
201
210
  catch (error) {
@@ -208,3 +217,6 @@ export function writeJsonAtomic(path, value) {
208
217
  throw error;
209
218
  }
210
219
  }
220
+ export function writeJsonAtomic(path, value) {
221
+ writeTextAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
222
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Actor Inspector run actions.
2
+ * Actor Inspector actions for concrete Run instances.
3
3
  * Owns: exact-session run-action authorization, canonical kill routing, and bounded operator feedback.
4
4
  */
5
5
  export interface ActorInspectorKillResult {
@@ -13,4 +13,4 @@ export interface ActorInspectorKillDeps {
13
13
  runInstanceId: string;
14
14
  }) => Record<string, unknown>;
15
15
  }
16
- export declare function killOwnedInspectorRun(ownerId: string, run: string, stateRoot: string, expectedRunInstanceId: string, deps: ActorInspectorKillDeps): ActorInspectorKillResult;
16
+ export declare function killOwnedRunFromInspector(ownerId: string, run: string, stateRoot: string, expectedRunInstanceId: string, deps: ActorInspectorKillDeps): ActorInspectorKillResult;