@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
package/lib/async-runs.ts CHANGED
@@ -47,40 +47,34 @@ import {
47
47
  signalOwnedRunProcess,
48
48
  type RunProcessSignalPlan,
49
49
  } from "./runs-control.ts";
50
- import {
51
- buildRunOutboxEventPayload,
52
- parseRunOutboxEventLine,
53
- type RunOutboxEvent,
54
- } from "./runs-outbox.ts";
55
50
  import { claimRunStateDirectory } from "./runs-ownership.ts";
56
- import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.ts";
51
+ import {
52
+ appendRunRetentionEvidence,
53
+ archiveTerminalRun,
54
+ pruneTerminalRun,
55
+ type RunRetentionAction,
56
+ } from "./runs-retention.ts";
57
57
  import {
58
58
  captureRunProcessIdentity,
59
59
  verifyRunProcessIdentity,
60
60
  type RunProcessIdentity,
61
61
  } from "./runs-process.ts";
62
62
  import * as RunsStart from "./runs-start.ts";
63
+ import { appendRunTraceEvent } from "./runs-trace.ts";
63
64
  import * as RunsIndex from "./runs-index.ts";
64
65
  import * as RunsParentTeardown from "./runs-parent-teardown.ts";
65
66
  import {
66
- claimRunInboxMessageInStateDir,
67
- parseRunInboxLine,
68
- processRunInboxMessagesInStateDir,
69
- readRunInboxMessagesFromStateDir,
70
- runInboxFile,
71
- type ProcessRunInboxResult,
72
- type RunInboxMessage,
73
- type RunInboxStatus,
74
- updateRunInboxMessageStatusInStateDir,
75
- } from "./runs-mailbox.ts";
67
+ appendRunControlInStateDir,
68
+ updateRunControlStatusInStateDir,
69
+ } from "./runs-controls.ts";
76
70
  import {
77
- deliverRunMessage,
78
- type SendRunMessageOptions,
79
- } from "./runs-messages.ts";
71
+ deliverRunControl,
72
+ type DeliverRunControlOptions,
73
+ type DeliverRunControlRequest,
74
+ } from "./runs-control-delivery.ts";
80
75
  import {
81
76
  buildRunStatus,
82
77
  tailFile,
83
- tailLines,
84
78
  type AsyncRunStatus,
85
79
  } from "./runs-status.ts";
86
80
  import { readJsonFileResilient } from "./state-readers.ts";
@@ -91,7 +85,7 @@ export type AsyncRunLaunchSource = "spawn" | "tool";
91
85
 
92
86
  export interface AsyncRunControlEndpoint {
93
87
  path: string;
94
- type: "fifo" | "mailbox" | "named-pipe";
88
+ type: "fifo" | "named-pipe";
95
89
  }
96
90
 
97
91
  export function normalizeRunTransportContext(
@@ -119,7 +113,7 @@ export function normalizeRunTransportContext(
119
113
 
120
114
  export interface AsyncRunStartParams {
121
115
  async?: boolean;
122
- control?: AsyncRunControlEndpoint;
116
+ control_endpoint?: AsyncRunControlEndpoint;
123
117
  file?: string;
124
118
  launch_source?: AsyncRunLaunchSource;
125
119
  lifecycleHooks?: {
@@ -147,7 +141,7 @@ export interface AsyncRunStartParams {
147
141
  accept_output?: "review_evidence";
148
142
  output?: string;
149
143
  artifacts?: Record<string, RunArtifactDeclaration>;
150
- mailbox?: RecipesReferences.TemplateRecipeMailbox;
144
+ control?: string[];
151
145
  notification_policy?: "normal" | "silent";
152
146
  retire_when?: "children_terminal";
153
147
  retry?: number | string;
@@ -179,13 +173,14 @@ export interface AsyncRunMeta {
179
173
  run: string;
180
174
  run_instance_id: string;
181
175
  state_dir: string;
176
+ state_schema: "run-kernel-v1";
182
177
  status: AsyncRunStatus;
183
178
  tool?: string;
184
179
  template: CommandTemplateValue;
185
180
  values: Record<string, unknown>;
186
181
  artifacts?: Record<string, RunArtifactDeclaration>;
187
- control?: AsyncRunControlEndpoint;
188
- mailbox?: RecipesReferences.TemplateRecipeMailbox;
182
+ control?: string[];
183
+ control_endpoint?: AsyncRunControlEndpoint;
189
184
  model_policy?: CurrentPolicyProvenance;
190
185
  notification_policy?: "normal" | "silent";
191
186
  process_identity?: RunProcessIdentity;
@@ -473,11 +468,9 @@ export function startRun(
473
468
  const stateDir = resolveStateDir(startParams, run);
474
469
  const values = {
475
470
  ...(startParams.values || {}),
476
- actor_address: `run:${run}`,
477
- communication_file: join(stateDir, "communication.json"),
478
- default_room: `room:${run}`,
479
471
  run_id: run,
480
472
  state_dir: stateDir,
473
+ trace_file: join(stateDir, "trace.jsonl"),
481
474
  };
482
475
  const modelPolicy = describeCurrentPolicyProvenance({
483
476
  defaults: startParams.defaults,
@@ -552,6 +545,7 @@ export function startRun(
552
545
  run,
553
546
  run_instance_id: randomUUID(),
554
547
  state_dir: stateDir,
548
+ state_schema: "run-kernel-v1",
555
549
  status: "running",
556
550
  ...(startParams.tool ? { tool: startParams.tool } : {}),
557
551
  template: resolved.template,
@@ -559,7 +553,9 @@ export function startRun(
559
553
  model_policy: modelPolicy,
560
554
  ...(artifacts ? { artifacts } : {}),
561
555
  ...(startParams.control ? { control: startParams.control } : {}),
562
- ...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
556
+ ...(startParams.control_endpoint
557
+ ? { control_endpoint: startParams.control_endpoint }
558
+ : {}),
563
559
  ...(startParams.notification_policy === "silent"
564
560
  ? { notification_policy: "silent" as const }
565
561
  : {}),
@@ -596,11 +592,14 @@ export function startRun(
596
592
  );
597
593
  if (processIdentity) meta.process_identity = processIdentity;
598
594
  writeJsonAtomic(join(stateDir, "run.json"), meta);
599
- writeFileSync(
600
- join(stateDir, "events.jsonl"),
601
- `${JSON.stringify({ event: "run.start", run, run_instance_id: meta.run_instance_id, pid: meta.pid, ts: new Date().toISOString() })}\n`,
602
- { flag: "a" },
603
- );
595
+ appendRunTraceEvent(stateDir, {
596
+ kind: "run.start",
597
+ summary: `Run ${run} started`,
598
+ data: {
599
+ pid: meta.pid,
600
+ run_instance_id: meta.run_instance_id,
601
+ },
602
+ });
604
603
  child.unref();
605
604
  return meta;
606
605
  } finally {
@@ -608,13 +607,6 @@ export function startRun(
608
607
  }
609
608
  }
610
609
 
611
- export { parseRunOutboxEventLine } from "./runs-outbox.ts";
612
- export type {
613
- RunOutboxDelivery,
614
- RunOutboxEvent,
615
- RunOutboxLevel,
616
- } from "./runs-outbox.ts";
617
-
618
610
  function resolveRunStateDir(runOrDir: string): string {
619
611
  return resolve(
620
612
  /[\\/]/u.test(runOrDir)
@@ -714,21 +706,19 @@ export function teardownRunsOwnedByParent(
714
706
  ) {
715
707
  throw new Error("run generation changed before teardown evidence");
716
708
  }
717
- writeFileSync(
718
- join(attempt.stateDir, "events.jsonl"),
719
- `${JSON.stringify({
720
- event: "run.parent_teardown",
721
- ownerId: attempt.ownerId,
709
+ appendRunTraceEvent(attempt.stateDir, {
710
+ kind: "run.parent_teardown",
711
+ summary: `Parent teardown ${attempt.outcome}`,
712
+ data: {
722
713
  outcome: attempt.outcome,
714
+ owner_id: attempt.ownerId,
723
715
  reason: attempt.reason,
724
716
  run: attempt.run,
725
717
  run_instance_id: attempt.runInstanceId,
726
- type: "control.kill",
727
718
  trigger: options.trigger ?? "parent_shutdown",
728
- ts: new Date().toISOString(),
729
- })}\n`,
730
- { flag: "a" },
731
- );
719
+ },
720
+ ...(attempt.outcome === "failed" ? { level: "error" as const } : {}),
721
+ });
732
722
  },
733
723
  });
734
724
  const summaryPath = join(
@@ -764,139 +754,63 @@ export function teardownRunsOwnedByParent(
764
754
  export function tailRun(runOrDir: string, lines = 40): string {
765
755
  const status = getRunStatus(runOrDir);
766
756
  const stateDir = String(status.state_dir);
767
- const events = tailFile(join(stateDir, "events.jsonl"), lines);
768
- if (events) return events;
757
+ const trace = tailFile(join(stateDir, "trace.jsonl"), lines);
758
+ if (trace) return trace;
769
759
  return (
770
760
  tailFile(join(stateDir, "stdout.log"), lines) ||
771
761
  tailFile(join(stateDir, "stderr.log"), lines)
772
762
  );
773
763
  }
774
764
 
775
- export function readRunEvents(runOrDir: string, lines = 40): RunOutboxEvent[] {
776
- const status = getRunStatus(runOrDir);
777
- const stateDir = String(status.state_dir);
778
- const run = String(status.run ?? runOrDir);
779
- return tailLines(join(stateDir, "outbox.jsonl"), lines)
780
- .map((line, index) => parseRunOutboxEventLine(line, run, stateDir, index))
781
- .filter((event): event is RunOutboxEvent => Boolean(event));
782
- }
783
-
784
765
  export type {
785
- ProcessRunInboxResult,
786
- RunInboxMessage,
787
- RunInboxStatus,
788
- } from "./runs-mailbox.ts";
789
-
790
- export function readRunInboxMessages(
791
- runOrDir: string,
792
- lines = 40,
793
- ): RunInboxMessage[] {
794
- const status = getRunStatus(runOrDir);
795
- const stateDir = String(status.state_dir);
796
- return tailLines(runInboxFile(stateDir), lines)
797
- .map(parseRunInboxLine)
798
- .filter((message): message is RunInboxMessage => Boolean(message));
799
- }
800
-
801
- export function updateRunInboxMessageStatus(
802
- runOrDir: string,
803
- id: string,
804
- nextStatus: RunInboxStatus,
805
- metadata: Record<string, unknown> = {},
806
- ): boolean {
807
- const status = getRunStatus(runOrDir);
808
- const stateDir = String(status.state_dir);
809
- return updateRunInboxMessageStatusInStateDir(
810
- stateDir,
811
- id,
812
- nextStatus,
813
- metadata,
814
- );
815
- }
816
-
817
- export function claimRunInboxMessage(
818
- runOrDir: string,
819
- owner = "runtime",
820
- statuses: string[] = ["queued"],
821
- ): RunInboxMessage | undefined {
822
- const status = getRunStatus(runOrDir);
823
- const stateDir = String(status.state_dir);
824
- return claimRunInboxMessageInStateDir(stateDir, owner, statuses);
825
- }
826
-
827
- export async function processRunInboxMessages(
828
- runOrDir: string,
829
- handler: (message: RunInboxMessage) => Promise<void> | void,
830
- options: { limit?: number; owner?: string; statuses?: string[] } = {},
831
- ): Promise<ProcessRunInboxResult> {
832
- const status = getRunStatus(runOrDir);
833
- return processRunInboxMessagesInStateDir(
834
- String(status.state_dir),
835
- handler,
836
- options,
837
- );
838
- }
766
+ DeliverRunControlOptions,
767
+ DeliverRunControlRequest,
768
+ } from "./runs-control-delivery.ts";
839
769
 
840
- export function appendRunOutboxEvent(
841
- runOrDir: string,
842
- event: {
843
- body?: unknown;
844
- correlation_id?: string;
845
- data?: unknown;
846
- delivery?: string;
847
- event?: string;
848
- from?: string;
849
- level?: string;
850
- metadata?: Record<string, unknown>;
851
- reply_to?: string;
852
- summary?: string;
853
- to?: string;
854
- type?: string;
855
- },
856
- ): Record<string, unknown> {
857
- const status = getRunStatus(runOrDir);
858
- const stateDir = String(status.state_dir);
859
- const run = String(status.run ?? runOrDir);
860
- const payload = buildRunOutboxEventPayload(run, event);
861
- const line = `${JSON.stringify(payload)}\n`;
862
- writeFileSync(join(stateDir, "outbox.jsonl"), line, { flag: "a" });
863
- return {
864
- bytes: Buffer.byteLength(line),
865
- outbox: "outbox.jsonl",
866
- run,
867
- sent: true,
868
- state_dir: stateDir,
869
- };
770
+ export interface SendRunControlOptions extends DeliverRunControlOptions {
771
+ ownerId?: string;
870
772
  }
871
773
 
872
- export type { SendRunMessageOptions } from "./runs-messages.ts";
873
-
874
- export async function sendRunMessage(
774
+ export async function sendRunControl(
875
775
  runOrDir: string,
876
- message: string,
877
- options: SendRunMessageOptions = {},
776
+ request: DeliverRunControlRequest,
777
+ options: SendRunControlOptions = {},
878
778
  ): Promise<Record<string, unknown>> {
879
- const status = getRunStatus(runOrDir);
880
- const stateDir = String(status.state_dir);
881
- const run = String(status.run ?? runOrDir);
882
- const pid = Number(status.pid || 0);
883
- const identity = verifyRunProcessIdentity(
884
- pid,
885
- status.process_identity as RunProcessIdentity | undefined,
886
- );
887
- if (status.status !== "running") {
888
- if (
889
- identity.status === "owner_mismatch" ||
890
- identity.status === "unsupported_proof"
891
- ) {
892
- throw new Error(`Run process identity ${identity.status}: ${run}`);
779
+ const stateDir = resolveRunStateDir(runOrDir);
780
+ const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
781
+ try {
782
+ const status = getRunStatus(stateDir);
783
+ const run = String(status.run ?? runOrDir);
784
+ if (options.ownerId !== undefined && status.ownerId !== options.ownerId) {
785
+ throw Object.assign(new Error(`Run ownership changed: ${run}`), {
786
+ reason: "owner_mismatch",
787
+ });
893
788
  }
894
- throw new Error(`Run is not running: ${run}`);
895
- }
896
- if (!identity.valid) {
897
- throw new Error(`Run process identity ${identity.status}: ${run}`);
789
+ if (status.run_instance_id !== request.run_instance_id) {
790
+ throw Object.assign(new Error(`Run generation changed: ${run}`), {
791
+ reason: "generation_mismatch",
792
+ });
793
+ }
794
+ if (status.status !== "running") {
795
+ throw Object.assign(new Error(`Run is not running: ${run}`), {
796
+ reason: "terminal_state",
797
+ });
798
+ }
799
+ const pid = Number(status.pid || 0);
800
+ const identity = verifyRunProcessIdentity(
801
+ pid,
802
+ status.process_identity as RunProcessIdentity | undefined,
803
+ );
804
+ if (!identity.valid) {
805
+ throw Object.assign(
806
+ new Error(`Run process identity ${identity.status}: ${run}`),
807
+ { reason: `process_identity_${identity.status}` },
808
+ );
809
+ }
810
+ return await deliverRunControl(run, stateDir, request, options);
811
+ } finally {
812
+ releaseControlLock();
898
813
  }
899
- return deliverRunMessage(status, run, stateDir, message, options);
900
814
  }
901
815
 
902
816
  export { getRunProcessSignalPlan } from "./runs-control.ts";
@@ -917,13 +831,13 @@ function markTerminalProgress(
917
831
  );
918
832
  }
919
833
 
920
- function finalizeInterruptedReviewEvidence(
834
+ function finalizeInterruptedExecution(
921
835
  stateDir: string,
922
836
  phase: "cancelled" | "killed",
923
837
  signal: NodeJS.Signals,
924
838
  ): void {
925
- const evidencePath = join(stateDir, "review-evidence.json");
926
- const manifest = readJson(evidencePath);
839
+ const executionPath = join(stateDir, "execution.json");
840
+ const manifest = readJson(executionPath);
927
841
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return;
928
842
  const record = manifest as Record<string, unknown>;
929
843
  if (!Array.isArray(record.commands)) return;
@@ -969,7 +883,7 @@ function finalizeInterruptedReviewEvidence(
969
883
  : {}),
970
884
  };
971
885
  });
972
- writeJsonAtomic(evidencePath, {
886
+ writeJsonAtomic(executionPath, {
973
887
  ...record,
974
888
  status: phase,
975
889
  commands,
@@ -1006,9 +920,37 @@ function stopRun(
1006
920
  ) {
1007
921
  return { stopped: false, reason: "run generation changed", status };
1008
922
  }
923
+ const control =
924
+ event === "run.kill" && typeof status.run_instance_id === "string"
925
+ ? appendRunControlInStateDir(stateDir, {
926
+ action: "kill",
927
+ run_instance_id: status.run_instance_id,
928
+ })
929
+ : undefined;
930
+ if (control) {
931
+ updateRunControlStatusInStateDir(
932
+ stateDir,
933
+ control.id,
934
+ "claimed",
935
+ {},
936
+ ["queued"],
937
+ );
938
+ }
939
+ const finish = (result: Record<string, unknown>): Record<string, unknown> => {
940
+ if (!control) return result;
941
+ const handled = result.stopped === true;
942
+ updateRunControlStatusInStateDir(
943
+ stateDir,
944
+ control.id,
945
+ handled ? "handled" : "failed",
946
+ handled ? {} : { error: String(result.reason ?? "kill rejected") },
947
+ ["claimed"],
948
+ );
949
+ return { ...result, control_id: control.id };
950
+ };
1009
951
  const pid = Number(status.pid || 0);
1010
952
  if (status.status !== "running" && status.status !== "exited") {
1011
- return { stopped: false, reason: "not running", status };
953
+ return finish({ stopped: false, reason: "not running", status });
1012
954
  }
1013
955
  const identity = verifyRunProcessIdentity(
1014
956
  pid,
@@ -1019,43 +961,59 @@ function stopRun(
1019
961
  identity.status === "owner_mismatch" ||
1020
962
  identity.status === "unsupported_proof"
1021
963
  ) {
1022
- return {
964
+ return finish({
1023
965
  stopped: false,
1024
966
  reason: identity.status.replaceAll("_", " "),
1025
967
  process_identity_status: identity.status,
1026
968
  status,
1027
- };
969
+ });
1028
970
  }
1029
- return { stopped: false, reason: "not running", status };
971
+ return finish({ stopped: false, reason: "not running", status });
1030
972
  }
1031
973
  if (!identity.valid) {
1032
- return {
974
+ return finish({
1033
975
  stopped: false,
1034
976
  reason: identity.status.replaceAll("_", " "),
1035
977
  process_identity_status: identity.status,
1036
978
  status,
1037
- };
979
+ });
1038
980
  }
1039
- const signalResult = signalOwnedRunProcess(
1040
- pid,
1041
- signal,
1042
- status.process_identity as RunProcessIdentity,
1043
- );
1044
- writeFileSync(
1045
- join(stateDir, "events.jsonl"),
1046
- `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`,
1047
- { flag: "a" },
1048
- );
981
+ let signalResult: RunProcessSignalPlan;
982
+ try {
983
+ signalResult = signalOwnedRunProcess(
984
+ pid,
985
+ signal,
986
+ status.process_identity as RunProcessIdentity,
987
+ );
988
+ } catch (error) {
989
+ if (control) {
990
+ updateRunControlStatusInStateDir(stateDir, control.id, "failed", {
991
+ error: error instanceof Error ? error.message : String(error),
992
+ });
993
+ }
994
+ throw error;
995
+ }
996
+ appendRunTraceEvent(stateDir, {
997
+ kind: event,
998
+ summary: `${event === "run.kill" ? "Killed" : "Cancelled"} Run process`,
999
+ data: { pid, signal, ...signalResult },
1000
+ });
1049
1001
  markTerminalHandled(stateDir, { event, signal });
1050
1002
  if (event === "run.kill") {
1051
- finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
1003
+ finalizeInterruptedExecution(stateDir, "killed", signal);
1052
1004
  markTerminalProgress(stateDir, "killed");
1053
1005
  }
1054
1006
  if (event === "run.cancel") {
1055
- finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
1007
+ finalizeInterruptedExecution(stateDir, "cancelled", signal);
1056
1008
  markTerminalProgress(stateDir, "cancelled");
1057
1009
  }
1058
- return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
1010
+ return finish({
1011
+ stopped: true,
1012
+ pid,
1013
+ signal,
1014
+ ...signalResult,
1015
+ state_dir: stateDir,
1016
+ });
1059
1017
  } finally {
1060
1018
  releaseControlLock();
1061
1019
  }
@@ -1100,23 +1058,66 @@ export function cancelRun(
1100
1058
  : result;
1101
1059
  }
1102
1060
 
1103
- function assertTerminalRun(runOrDir: string): Record<string, unknown> {
1104
- const status = getRunStatus(runOrDir);
1105
- if (status.status === "running") {
1106
- throw new Error("Only terminal runs can be archived or pruned.");
1061
+ function retainRun(
1062
+ runOrDir: string,
1063
+ action: RunRetentionAction,
1064
+ expected: RunControlExpectation,
1065
+ options: { preserveArtifacts?: boolean } = {},
1066
+ ): Record<string, unknown> {
1067
+ const stateDir = resolveRunStateDir(runOrDir);
1068
+ const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
1069
+ let status: Record<string, unknown> | undefined;
1070
+ let evidenceId: string | undefined;
1071
+ try {
1072
+ expected.onLocked?.();
1073
+ status = getRunStatus(stateDir);
1074
+ if (expected.ownerId !== undefined && status.ownerId !== expected.ownerId) {
1075
+ return { [`${action}d`]: false, reason: "ownership changed", status };
1076
+ }
1077
+ if (
1078
+ expected.runInstanceId !== undefined &&
1079
+ status.run_instance_id !== expected.runInstanceId
1080
+ ) {
1081
+ return { [`${action}d`]: false, reason: "run generation changed", status };
1082
+ }
1083
+ if (status.status === "running") {
1084
+ throw new Error("Only terminal runs can be archived or pruned.");
1085
+ }
1086
+ evidenceId = appendRunRetentionEvidence(status, action, "queued");
1087
+ const result = action === "archive"
1088
+ ? archiveTerminalRun(status)
1089
+ : pruneTerminalRun(status, options);
1090
+ appendRunRetentionEvidence(status, action, "handled", {
1091
+ id: evidenceId,
1092
+ result,
1093
+ });
1094
+ return { ...result, retention_id: evidenceId };
1095
+ } catch (error) {
1096
+ if (status && evidenceId) {
1097
+ appendRunRetentionEvidence(status, action, "failed", {
1098
+ error: error instanceof Error ? error.message : String(error),
1099
+ id: evidenceId,
1100
+ });
1101
+ }
1102
+ throw error;
1103
+ } finally {
1104
+ releaseControlLock();
1107
1105
  }
1108
- return status;
1109
1106
  }
1110
1107
 
1111
- export function archiveRun(runOrDir: string): Record<string, unknown> {
1112
- return archiveTerminalRun(assertTerminalRun(runOrDir));
1108
+ export function archiveRun(
1109
+ runOrDir: string,
1110
+ expected: RunControlExpectation = {},
1111
+ ): Record<string, unknown> {
1112
+ return retainRun(runOrDir, "archive", expected);
1113
1113
  }
1114
1114
 
1115
1115
  export function pruneRun(
1116
1116
  runOrDir: string,
1117
1117
  options: { preserveArtifacts?: boolean } = {},
1118
+ expected: RunControlExpectation = {},
1118
1119
  ): Record<string, unknown> {
1119
- return pruneTerminalRun(assertTerminalRun(runOrDir), options);
1120
+ return retainRun(runOrDir, "prune", expected, options);
1120
1121
  }
1121
1122
 
1122
1123
  export function killRun(
package/lib/control.ts ADDED
@@ -0,0 +1,102 @@
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
+
7
+ import * as Limits from "./limits.ts";
8
+
9
+ export interface ControlRequest {
10
+ target: `run:${string}` | "runtime";
11
+ action: string;
12
+ input?: unknown;
13
+ verbose?: boolean;
14
+ }
15
+
16
+ const RUN_ID_PATTERN = /^[A-Za-z0-9_.-]+$/;
17
+ const ACTION_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)*$/;
18
+ const CONTROL_FIELDS = new Set(["action", "input", "target", "verbose"]);
19
+ const REMOVED_FIELDS = new Set([
20
+ "body",
21
+ "correlation_id",
22
+ "from",
23
+ "metadata",
24
+ "reply_to",
25
+ "summary",
26
+ "to",
27
+ "type",
28
+ ]);
29
+
30
+ function serializedInputBytes(input: unknown): number {
31
+ let serialized: string | undefined;
32
+ try {
33
+ serialized = JSON.stringify(input);
34
+ } catch {
35
+ throw new Error("control.input must be JSON-serializable");
36
+ }
37
+ if (serialized === undefined) {
38
+ throw new Error("control.input must be JSON-serializable");
39
+ }
40
+ return Buffer.byteLength(serialized);
41
+ }
42
+
43
+ function normalizeTarget(value: unknown): ControlRequest["target"] {
44
+ if (typeof value !== "string" || !value.trim()) {
45
+ throw new Error("control.target is required");
46
+ }
47
+ const target = value.trim();
48
+ if (target === "runtime") return target;
49
+ if (!target.startsWith("run:")) {
50
+ throw new Error(
51
+ `unsupported control target: ${target}; use run:<id> or runtime`,
52
+ );
53
+ }
54
+ const run = target.slice(4);
55
+ if (!RUN_ID_PATTERN.test(run)) {
56
+ throw new Error(`invalid control Run target: ${target}`);
57
+ }
58
+ return `run:${run}`;
59
+ }
60
+
61
+ export function normalizeControlRequest(input: unknown): ControlRequest {
62
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
63
+ throw new Error("control request must be an object");
64
+ }
65
+ const record = input as Record<string, unknown>;
66
+ const fields = Object.keys(record);
67
+ const removed = fields.filter((field) => REMOVED_FIELDS.has(field));
68
+ if (removed.length > 0) {
69
+ throw new Error(
70
+ `actor-message fields are removed: ${removed.sort().join(", ")}; use target, action, input, verbose`,
71
+ );
72
+ }
73
+ const unknown = fields.filter((field) => !CONTROL_FIELDS.has(field));
74
+ if (unknown.length > 0) {
75
+ throw new Error(`unsupported control fields: ${unknown.sort().join(", ")}`);
76
+ }
77
+ const target = normalizeTarget(record.target);
78
+ if (typeof record.action !== "string" || !record.action.trim()) {
79
+ throw new Error("control.action is required");
80
+ }
81
+ const action = record.action.trim();
82
+ if (!ACTION_PATTERN.test(action)) {
83
+ throw new Error(`invalid control action: ${action}`);
84
+ }
85
+ if (record.verbose !== undefined && typeof record.verbose !== "boolean") {
86
+ throw new Error("control.verbose must be a boolean");
87
+ }
88
+ if (
89
+ record.input !== undefined &&
90
+ serializedInputBytes(record.input) > Limits.CONTROL_INPUT_MAX_BYTES
91
+ ) {
92
+ throw new Error(
93
+ `control.input exceeds ${Limits.CONTROL_INPUT_MAX_BYTES} bytes`,
94
+ );
95
+ }
96
+ return {
97
+ target,
98
+ action,
99
+ ...(record.input !== undefined ? { input: record.input } : {}),
100
+ ...(record.verbose !== undefined ? { verbose: record.verbose } : {}),
101
+ };
102
+ }