@llblab/pi-actors 0.42.3 → 0.43.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 (279) hide show
  1. package/AGENTS.md +127 -175
  2. package/BACKLOG.md +189 -1
  3. package/CHANGELOG.md +183 -292
  4. package/README.md +115 -276
  5. package/dist/fixtures/protocol/control-endpoint.json +6 -0
  6. package/dist/fixtures/protocol/control-record.json +9 -0
  7. package/dist/fixtures/protocol/recipe-summary.json +4 -12
  8. package/dist/fixtures/protocol/trace-event.json +9 -0
  9. package/dist/index.js +1 -1
  10. package/dist/lib/async-runs.d.ts +15 -38
  11. package/dist/lib/async-runs.js +173 -111
  12. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  13. package/dist/lib/automatic-review-runtime.js +5 -5
  14. package/dist/lib/command-templates.d.ts +2 -0
  15. package/dist/lib/command-templates.js +38 -4
  16. package/dist/lib/control-projection.d.ts +20 -0
  17. package/dist/lib/control-projection.js +66 -0
  18. package/dist/lib/control.d.ts +15 -0
  19. package/dist/lib/control.js +97 -0
  20. package/dist/lib/draft-sleep.js +3 -3
  21. package/dist/lib/execution-sessions.d.ts +17 -0
  22. package/dist/lib/execution-sessions.js +85 -0
  23. package/dist/lib/file-state.d.ts +2 -0
  24. package/dist/lib/file-state.js +114 -46
  25. package/dist/lib/inspector-actions.d.ts +2 -2
  26. package/dist/lib/inspector-actions.js +2 -2
  27. package/dist/lib/inspector-command.js +3 -3
  28. package/dist/lib/inspector-overlay.d.ts +54 -70
  29. package/dist/lib/inspector-overlay.js +576 -910
  30. package/dist/lib/inspector.d.ts +3 -71
  31. package/dist/lib/inspector.js +19 -665
  32. package/dist/lib/limits.d.ts +7 -3
  33. package/dist/lib/limits.js +7 -3
  34. package/dist/lib/observability.d.ts +16 -16
  35. package/dist/lib/observability.js +43 -82
  36. package/dist/lib/prompts.d.ts +1 -1
  37. package/dist/lib/prompts.js +3 -3
  38. package/dist/lib/recipe-control.d.ts +7 -0
  39. package/dist/lib/recipe-control.js +43 -0
  40. package/dist/lib/recipes-discovery.js +2 -0
  41. package/dist/lib/recipes-references.d.ts +1 -14
  42. package/dist/lib/recipes-references.js +6 -21
  43. package/dist/lib/review-control.d.ts +1 -1
  44. package/dist/lib/review-control.js +4 -5
  45. package/dist/lib/review-projection.js +1 -5
  46. package/dist/lib/run-ui-runtime.js +2 -2
  47. package/dist/lib/runs-control-delivery.d.ts +28 -0
  48. package/dist/lib/runs-control-delivery.js +150 -0
  49. package/dist/lib/runs-controls.d.ts +37 -0
  50. package/dist/lib/runs-controls.js +146 -0
  51. package/dist/lib/runs-retention.d.ts +7 -0
  52. package/dist/lib/runs-retention.js +27 -3
  53. package/dist/lib/runs-start.js +4 -2
  54. package/dist/lib/runs-status.js +11 -6
  55. package/dist/lib/runs-trace.d.ts +24 -0
  56. package/dist/lib/runs-trace.js +102 -0
  57. package/dist/lib/runtime-identity.d.ts +7 -0
  58. package/dist/lib/runtime-identity.js +35 -0
  59. package/dist/lib/runtime-notifier.d.ts +1 -1
  60. package/dist/lib/runtime-notifier.js +1 -1
  61. package/dist/lib/runtime-triage.d.ts +29 -0
  62. package/dist/lib/runtime-triage.js +76 -0
  63. package/dist/lib/tool-review-scheduler.js +7 -7
  64. package/dist/lib/tools-inspect.d.ts +3 -3
  65. package/dist/lib/tools-inspect.js +241 -707
  66. package/dist/lib/tools-local.js +2 -10
  67. package/dist/lib/tools-message.d.ts +6 -7
  68. package/dist/lib/tools-message.js +95 -396
  69. package/dist/lib/tools-response.d.ts +1 -5
  70. package/dist/lib/tools-response.js +5 -48
  71. package/dist/lib/tools-spawn.js +16 -28
  72. package/dist/lib/tools.d.ts +1 -1
  73. package/dist/lib/tools.js +2 -3
  74. package/dist/lib/trace-projection.d.ts +22 -0
  75. package/dist/lib/trace-projection.js +185 -0
  76. package/dist/recipes/draft-review.json +0 -10
  77. package/dist/recipes/lens-swarm.json +0 -14
  78. package/dist/recipes/music-player.json +10 -19
  79. package/dist/recipes/pipeline-architect-coordinator.json +0 -11
  80. package/dist/recipes/pipeline-artifact-bundle.json +1 -22
  81. package/dist/recipes/pipeline-artifact-report.json +1 -18
  82. package/dist/recipes/pipeline-artifact-write.json +1 -18
  83. package/dist/recipes/pipeline-async-run-ops.json +0 -12
  84. package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
  85. package/dist/recipes/pipeline-development-tasking.json +0 -12
  86. package/dist/recipes/pipeline-docs-maintenance.json +0 -12
  87. package/dist/recipes/pipeline-media-library.json +0 -12
  88. package/dist/recipes/pipeline-quorum-review.json +0 -12
  89. package/dist/recipes/pipeline-release-readiness.json +0 -12
  90. package/dist/recipes/pipeline-release-summary.json +0 -12
  91. package/dist/recipes/pipeline-repo-health.json +0 -12
  92. package/dist/recipes/pipeline-research-synthesis.json +0 -11
  93. package/dist/recipes/pipeline-review-readiness.json +0 -12
  94. package/dist/recipes/resource-locker.json +27 -0
  95. package/dist/recipes/subagent-artifact.json +0 -9
  96. package/dist/recipes/subagent-checkpoint.json +0 -10
  97. package/dist/recipes/subagent-conflict-report.json +0 -11
  98. package/dist/recipes/subagent-contradiction-map.json +0 -11
  99. package/dist/recipes/subagent-critic.json +0 -11
  100. package/dist/recipes/subagent-evidence-map.json +0 -11
  101. package/dist/recipes/subagent-followup.json +0 -10
  102. package/dist/recipes/subagent-judge.json +0 -11
  103. package/dist/recipes/subagent-merge.json +0 -11
  104. package/dist/recipes/subagent-normalize.json +0 -11
  105. package/dist/recipes/subagent-plan.json +0 -11
  106. package/dist/recipes/subagent-preflight.json +0 -11
  107. package/dist/recipes/subagent-prompt.json +0 -10
  108. package/dist/recipes/subagent-quorum.json +0 -10
  109. package/dist/recipes/subagent-review-coordinator.json +0 -14
  110. package/dist/recipes/subagent-review.json +0 -11
  111. package/dist/recipes/subagent-task-card.json +0 -11
  112. package/dist/recipes/subagent-tools.json +0 -10
  113. package/dist/recipes/subagent-verify.json +0 -11
  114. package/dist/recipes/subagents-prompts.json +0 -10
  115. package/dist/recipes/tool-review.json +0 -10
  116. package/dist/scripts/async-runner.mjs +25 -25
  117. package/dist/scripts/conformance.mjs +4 -2
  118. package/dist/scripts/locker.mjs +196 -69
  119. package/dist/scripts/music-player.mjs +162 -159
  120. package/dist/scripts/recipe-utils.mjs +6 -96
  121. package/dist/scripts/release-gates.mjs +91 -0
  122. package/dist/scripts/validate-recipe.mjs +8 -57
  123. package/dist/skills/actors/SKILL.md +57 -265
  124. package/dist/skills/swarm/SKILL.md +10 -34
  125. package/docs/0.43-baseline.md +39 -0
  126. package/docs/README.md +4 -6
  127. package/docs/actor-inspector.md +26 -64
  128. package/docs/async-runs.md +81 -328
  129. package/docs/command-templates.md +8 -118
  130. package/docs/recipe-library.md +55 -182
  131. package/docs/releasing.md +28 -0
  132. package/docs/template-recipes.md +76 -289
  133. package/docs/tool-registry.md +41 -161
  134. package/fixtures/protocol/control-endpoint.json +6 -0
  135. package/fixtures/protocol/control-record.json +9 -0
  136. package/fixtures/protocol/recipe-summary.json +4 -12
  137. package/fixtures/protocol/trace-event.json +9 -0
  138. package/index.ts +1 -1
  139. package/lib/async-runs.ts +218 -204
  140. package/lib/automatic-review-runtime.ts +7 -7
  141. package/lib/command-templates.ts +44 -4
  142. package/lib/control-projection.ts +105 -0
  143. package/lib/control.ts +117 -0
  144. package/lib/draft-sleep.ts +3 -3
  145. package/lib/execution-sessions.ts +111 -0
  146. package/lib/file-state.ts +84 -64
  147. package/lib/inspector-actions.ts +2 -2
  148. package/lib/inspector-command.ts +3 -3
  149. package/lib/inspector-overlay.ts +617 -1126
  150. package/lib/inspector.ts +46 -979
  151. package/lib/limits.ts +7 -3
  152. package/lib/observability.ts +60 -101
  153. package/lib/prompts.ts +3 -3
  154. package/lib/recipe-control.ts +52 -0
  155. package/lib/recipes-discovery.ts +2 -0
  156. package/lib/recipes-references.ts +9 -45
  157. package/lib/review-control.ts +4 -5
  158. package/lib/review-projection.ts +1 -5
  159. package/lib/run-ui-runtime.ts +2 -2
  160. package/lib/runs-control-delivery.ts +209 -0
  161. package/lib/runs-controls.ts +213 -0
  162. package/lib/runs-retention.ts +38 -3
  163. package/lib/runs-start.ts +4 -2
  164. package/lib/runs-status.ts +11 -6
  165. package/lib/runs-trace.ts +136 -0
  166. package/lib/runtime-identity.ts +39 -0
  167. package/lib/runtime-notifier.ts +1 -1
  168. package/lib/runtime-triage.ts +120 -0
  169. package/lib/tool-review-scheduler.ts +7 -7
  170. package/lib/tools-inspect.ts +283 -900
  171. package/lib/tools-local.ts +2 -12
  172. package/lib/tools-message.ts +112 -520
  173. package/lib/tools-response.ts +5 -64
  174. package/lib/tools-spawn.ts +16 -32
  175. package/lib/tools.ts +5 -6
  176. package/lib/trace-projection.ts +244 -0
  177. package/package.json +2 -1
  178. package/recipes/draft-review.json +0 -10
  179. package/recipes/lens-swarm.json +0 -14
  180. package/recipes/music-player.json +10 -19
  181. package/recipes/pipeline-architect-coordinator.json +0 -11
  182. package/recipes/pipeline-artifact-bundle.json +1 -22
  183. package/recipes/pipeline-artifact-report.json +1 -18
  184. package/recipes/pipeline-artifact-write.json +1 -18
  185. package/recipes/pipeline-async-run-ops.json +0 -12
  186. package/recipes/pipeline-checkpoint-continuation.json +0 -14
  187. package/recipes/pipeline-development-tasking.json +0 -12
  188. package/recipes/pipeline-docs-maintenance.json +0 -12
  189. package/recipes/pipeline-media-library.json +0 -12
  190. package/recipes/pipeline-quorum-review.json +0 -12
  191. package/recipes/pipeline-release-readiness.json +0 -12
  192. package/recipes/pipeline-release-summary.json +0 -12
  193. package/recipes/pipeline-repo-health.json +0 -12
  194. package/recipes/pipeline-research-synthesis.json +0 -11
  195. package/recipes/pipeline-review-readiness.json +0 -12
  196. package/recipes/resource-locker.json +27 -0
  197. package/recipes/subagent-artifact.json +0 -9
  198. package/recipes/subagent-checkpoint.json +0 -10
  199. package/recipes/subagent-conflict-report.json +0 -11
  200. package/recipes/subagent-contradiction-map.json +0 -11
  201. package/recipes/subagent-critic.json +0 -11
  202. package/recipes/subagent-evidence-map.json +0 -11
  203. package/recipes/subagent-followup.json +0 -10
  204. package/recipes/subagent-judge.json +0 -11
  205. package/recipes/subagent-merge.json +0 -11
  206. package/recipes/subagent-normalize.json +0 -11
  207. package/recipes/subagent-plan.json +0 -11
  208. package/recipes/subagent-preflight.json +0 -11
  209. package/recipes/subagent-prompt.json +0 -10
  210. package/recipes/subagent-quorum.json +0 -10
  211. package/recipes/subagent-review-coordinator.json +0 -14
  212. package/recipes/subagent-review.json +0 -11
  213. package/recipes/subagent-task-card.json +0 -11
  214. package/recipes/subagent-tools.json +0 -10
  215. package/recipes/subagent-verify.json +0 -11
  216. package/recipes/subagents-prompts.json +0 -10
  217. package/recipes/tool-review.json +0 -10
  218. package/scripts/async-runner.mjs +25 -25
  219. package/scripts/conformance.mjs +4 -2
  220. package/scripts/locker.mjs +196 -69
  221. package/scripts/music-player.mjs +162 -159
  222. package/scripts/recipe-utils.mjs +6 -96
  223. package/scripts/release-gates.mjs +91 -0
  224. package/scripts/validate-recipe.mjs +8 -57
  225. package/skills/actors/SKILL.md +57 -265
  226. package/skills/swarm/SKILL.md +10 -34
  227. package/dist/fixtures/protocol/actor-message-branch.json +0 -13
  228. package/dist/fixtures/protocol/mailbox-contract.json +0 -15
  229. package/dist/fixtures/protocol/room-message.json +0 -11
  230. package/dist/fixtures/protocol/room-roster.json +0 -11
  231. package/dist/fixtures/protocol/run-inbox-message.json +0 -9
  232. package/dist/fixtures/protocol/run-outbox-event.json +0 -9
  233. package/dist/lib/mailbox-loop.d.ts +0 -41
  234. package/dist/lib/mailbox-loop.js +0 -60
  235. package/dist/lib/messages.d.ts +0 -25
  236. package/dist/lib/messages.js +0 -122
  237. package/dist/lib/rooms.d.ts +0 -104
  238. package/dist/lib/rooms.js +0 -647
  239. package/dist/lib/runs-mailbox.d.ts +0 -25
  240. package/dist/lib/runs-mailbox.js +0 -146
  241. package/dist/lib/runs-messages.d.ts +0 -15
  242. package/dist/lib/runs-messages.js +0 -179
  243. package/dist/lib/runs-outbox.d.ts +0 -41
  244. package/dist/lib/runs-outbox.js +0 -87
  245. package/dist/lib/tools-mailbox.d.ts +0 -8
  246. package/dist/lib/tools-mailbox.js +0 -48
  247. package/dist/recipes/actor-worker.json +0 -39
  248. package/dist/recipes/coordinator-locker.json +0 -45
  249. package/dist/recipes/locker.json +0 -45
  250. package/dist/recipes/pipeline-room-swarm.json +0 -50
  251. package/dist/recipes/subagent-message.json +0 -32
  252. package/dist/recipes/utility-actor-message.json +0 -23
  253. package/dist/scripts/actor-worker.mjs +0 -214
  254. package/dist/scripts/coordinator.mjs +0 -799
  255. package/docs/actor-messages.md +0 -225
  256. package/docs/actors-deep-reference.md +0 -66
  257. package/docs/component-recipes.md +0 -148
  258. package/docs/task-first-recipes.md +0 -263
  259. package/fixtures/protocol/actor-message-branch.json +0 -13
  260. package/fixtures/protocol/mailbox-contract.json +0 -15
  261. package/fixtures/protocol/room-message.json +0 -11
  262. package/fixtures/protocol/room-roster.json +0 -11
  263. package/fixtures/protocol/run-inbox-message.json +0 -9
  264. package/fixtures/protocol/run-outbox-event.json +0 -9
  265. package/lib/mailbox-loop.ts +0 -144
  266. package/lib/messages.ts +0 -151
  267. package/lib/rooms.ts +0 -939
  268. package/lib/runs-mailbox.ts +0 -208
  269. package/lib/runs-messages.ts +0 -252
  270. package/lib/runs-outbox.ts +0 -144
  271. package/lib/tools-mailbox.ts +0 -56
  272. package/recipes/actor-worker.json +0 -39
  273. package/recipes/coordinator-locker.json +0 -45
  274. package/recipes/locker.json +0 -45
  275. package/recipes/pipeline-room-swarm.json +0 -50
  276. package/recipes/subagent-message.json +0 -32
  277. package/recipes/utility-actor-message.json +0 -23
  278. package/scripts/actor-worker.mjs +0 -214
  279. package/scripts/coordinator.mjs +0 -799
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Run Control delivery.
3
+ * Zones: generation-fenced endpoint resolution, FIFO/named-pipe writes, durable outcomes
4
+ * Owns delivery of persisted actor-local Controls to ready service endpoints.
5
+ */
6
+
7
+ import {
8
+ closeSync,
9
+ constants,
10
+ existsSync,
11
+ openSync,
12
+ statSync,
13
+ writeSync,
14
+ } from "node:fs";
15
+ import { createConnection } from "node:net";
16
+ import { join } from "node:path";
17
+
18
+ import * as Control from "./control.ts";
19
+ import * as Limits from "./limits.ts";
20
+ import {
21
+ appendRunControlInStateDir,
22
+ updateRunControlStatusInStateDir,
23
+ } from "./runs-controls.ts";
24
+ import { readJsonFileResilient } from "./state-readers.ts";
25
+
26
+ export interface RunControlEndpoint {
27
+ path: string;
28
+ type: "fifo" | "named-pipe";
29
+ }
30
+
31
+ export interface DeliverRunControlOptions {
32
+ namedPipeSend?: (path: string, payload: string) => Promise<number>;
33
+ platform?: NodeJS.Platform;
34
+ }
35
+
36
+ export interface DeliverRunControlRequest {
37
+ action: string;
38
+ input?: unknown;
39
+ run_instance_id: string;
40
+ }
41
+
42
+ const PREFLIGHT_CONTROL_ID = "00000000-0000-4000-8000-000000000000";
43
+
44
+ export function encodeRunControlWire(wire: {
45
+ action: string;
46
+ id: string;
47
+ input?: unknown;
48
+ }): { bytes: number; payload: string } {
49
+ const payload = `${JSON.stringify({
50
+ id: wire.id,
51
+ action: wire.action,
52
+ ...(wire.input !== undefined ? { input: wire.input } : {}),
53
+ })}\n`;
54
+ const bytes = Buffer.byteLength(payload);
55
+ if (bytes > Limits.CONTROL_WIRE_MAX_BYTES) {
56
+ throw new Error(
57
+ `Run Control wire record exceeds the ${Limits.CONTROL_WIRE_MAX_BYTES}-byte portable bound`,
58
+ );
59
+ }
60
+ return { bytes, payload };
61
+ }
62
+
63
+ export function readRunControlEndpoint(
64
+ stateDir: string,
65
+ runInstanceId: string,
66
+ ): RunControlEndpoint | undefined {
67
+ const endpoint = readJsonFileResilient<Record<string, unknown>>(
68
+ join(stateDir, "control-endpoint.json"),
69
+ {},
70
+ ).value;
71
+ if (endpoint.run_instance_id !== runInstanceId) return undefined;
72
+ if (
73
+ (endpoint.type === "fifo" || endpoint.type === "named-pipe") &&
74
+ typeof endpoint.path === "string" &&
75
+ endpoint.path.trim()
76
+ ) {
77
+ return { path: endpoint.path, type: endpoint.type };
78
+ }
79
+ return undefined;
80
+ }
81
+
82
+ function sendToFifo(endpoint: RunControlEndpoint, payload: string): number {
83
+ if (!existsSync(endpoint.path))
84
+ throw new Error(`Run Control FIFO not found: ${endpoint.path}`);
85
+ const stat = statSync(endpoint.path);
86
+ if ((stat.mode & constants.S_IFMT) !== constants.S_IFIFO) {
87
+ throw new Error(`Run Control endpoint is not a FIFO: ${endpoint.path}`);
88
+ }
89
+ let fd: number | undefined;
90
+ try {
91
+ fd = openSync(endpoint.path, constants.O_WRONLY | constants.O_NONBLOCK);
92
+ return writeSync(fd, payload);
93
+ } finally {
94
+ if (fd !== undefined) closeSync(fd);
95
+ }
96
+ }
97
+
98
+ function sendToNamedPipe(
99
+ endpoint: RunControlEndpoint,
100
+ payload: string,
101
+ send?: (path: string, payload: string) => Promise<number>,
102
+ ): Promise<number> {
103
+ if (send) return send(endpoint.path, payload);
104
+ return new Promise((resolve, reject) => {
105
+ const socket = createConnection(endpoint.path);
106
+ let settled = false;
107
+ const timeout = setTimeout(() => {
108
+ if (settled) return;
109
+ settled = true;
110
+ socket.destroy();
111
+ reject(new Error("named pipe connection timed out"));
112
+ }, 5000);
113
+ const finish = (error?: Error): void => {
114
+ if (settled) return;
115
+ settled = true;
116
+ clearTimeout(timeout);
117
+ if (error) reject(error);
118
+ else resolve(Buffer.byteLength(payload));
119
+ };
120
+ socket.on("error", finish);
121
+ socket.on("connect", () => socket.end(payload, () => finish()));
122
+ });
123
+ }
124
+
125
+ export async function deliverRunControl(
126
+ run: string,
127
+ stateDir: string,
128
+ request: DeliverRunControlRequest,
129
+ options: DeliverRunControlOptions = {},
130
+ ): Promise<Record<string, unknown>> {
131
+ const action = Control.normalizeControlAction(request.action);
132
+ const input = request.input === undefined
133
+ ? undefined
134
+ : Control.normalizeControlInput(request.input);
135
+ const normalizedRequest = {
136
+ action,
137
+ ...(input !== undefined ? { input } : {}),
138
+ run_instance_id: request.run_instance_id,
139
+ };
140
+ encodeRunControlWire({
141
+ id: PREFLIGHT_CONTROL_ID,
142
+ action,
143
+ ...(input !== undefined ? { input } : {}),
144
+ });
145
+ const control = appendRunControlInStateDir(stateDir, normalizedRequest);
146
+ const endpoint = readRunControlEndpoint(stateDir, request.run_instance_id);
147
+ if (!endpoint) {
148
+ updateRunControlStatusInStateDir(
149
+ stateDir,
150
+ control.id,
151
+ "failed",
152
+ { error: "control endpoint is not ready for this Run generation" },
153
+ ["queued", "delivered"],
154
+ );
155
+ throw Object.assign(new Error("Run Control endpoint is not ready."), {
156
+ action,
157
+ control_id: control.id,
158
+ reason: "endpoint_not_ready",
159
+ run,
160
+ run_instance_id: request.run_instance_id,
161
+ });
162
+ }
163
+ try {
164
+ const { bytes, payload } = encodeRunControlWire({
165
+ id: control.id,
166
+ action,
167
+ ...(input !== undefined ? { input } : {}),
168
+ });
169
+ let written: number;
170
+ if (endpoint.type === "fifo") {
171
+ if ((options.platform ?? process.platform) === "win32") {
172
+ throw new Error("FIFO Control delivery is unsupported on native Windows");
173
+ }
174
+ written = sendToFifo(endpoint, payload);
175
+ } else {
176
+ written = await sendToNamedPipe(endpoint, payload, options.namedPipeSend);
177
+ }
178
+ if (written !== bytes) {
179
+ throw new Error(`Run Control endpoint wrote ${written} of ${bytes} bytes`);
180
+ }
181
+ updateRunControlStatusInStateDir(stateDir, control.id, "delivered");
182
+ return {
183
+ action,
184
+ bytes,
185
+ control_id: control.id,
186
+ delivery: "delivered",
187
+ endpoint_type: endpoint.type,
188
+ run,
189
+ run_instance_id: request.run_instance_id,
190
+ };
191
+ } catch (error) {
192
+ const reason = error instanceof Error ? error.message : String(error);
193
+ updateRunControlStatusInStateDir(
194
+ stateDir,
195
+ control.id,
196
+ "failed",
197
+ { error: reason },
198
+ ["queued", "delivered"],
199
+ );
200
+ throw Object.assign(new Error(`Run Control delivery failed: ${reason}`), {
201
+ action,
202
+ control_id: control.id,
203
+ endpoint_type: endpoint.type,
204
+ reason: "delivery_failed",
205
+ run,
206
+ run_instance_id: request.run_instance_id,
207
+ });
208
+ }
209
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Run Control journal.
3
+ * Zones: durable Control records, claim locks, status transitions, bounded compaction
4
+ * Owns Run-local Control persistence; owner/generation authorization and transport delivery stay in lifecycle adapters.
5
+ */
6
+
7
+ import { randomUUID } from "node:crypto";
8
+ import { writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ import { acquireFileMutationLock, writeTextAtomic } from "./file-state.ts";
12
+ import {
13
+ readJsonlFileResilient,
14
+ type JsonlReadResult,
15
+ } from "./state-readers.ts";
16
+
17
+ export const RUN_CONTROL_TERMINAL_LIMIT = 128;
18
+
19
+ export type RunControlStatus =
20
+ | "queued"
21
+ | "delivered"
22
+ | "claimed"
23
+ | "handled"
24
+ | "failed";
25
+
26
+ export interface RunControlRecord {
27
+ id: string;
28
+ run_instance_id: string;
29
+ action: string;
30
+ input?: unknown;
31
+ status: RunControlStatus;
32
+ queued_at: string;
33
+ delivered_at?: string;
34
+ claimed_at?: string;
35
+ handled_at?: string;
36
+ failed_at?: string;
37
+ error?: string;
38
+ }
39
+
40
+ export interface ProcessRunControlsResult {
41
+ claimed: number;
42
+ failed: number;
43
+ handled: number;
44
+ }
45
+
46
+ const TERMINAL_STATUSES = new Set<RunControlStatus>(["handled", "failed"]);
47
+
48
+ export function runControlsFile(stateDir: string): string {
49
+ return join(stateDir, "controls.jsonl");
50
+ }
51
+
52
+ export function readRunControlJournalFromStateDir(
53
+ stateDir: string,
54
+ ): JsonlReadResult<unknown> {
55
+ return readJsonlFileResilient<unknown>(runControlsFile(stateDir));
56
+ }
57
+
58
+ export function readRunControlsFromStateDir(stateDir: string): RunControlRecord[] {
59
+ return readRunControlJournalFromStateDir(stateDir).records as RunControlRecord[];
60
+ }
61
+
62
+ function acquireRunControlsLock(stateDir: string): () => void {
63
+ return acquireFileMutationLock(runControlsFile(stateDir));
64
+ }
65
+
66
+ function compactRunControls(records: RunControlRecord[]): RunControlRecord[] {
67
+ const active = records.filter((record) => !TERMINAL_STATUSES.has(record.status));
68
+ const terminal = records
69
+ .filter((record) => TERMINAL_STATUSES.has(record.status))
70
+ .slice(-RUN_CONTROL_TERMINAL_LIMIT);
71
+ const retained = new Set([...active, ...terminal]);
72
+ return records.filter((record) => retained.has(record));
73
+ }
74
+
75
+ function writeRunControls(stateDir: string, records: RunControlRecord[]): void {
76
+ const compacted = compactRunControls(records);
77
+ writeTextAtomic(
78
+ runControlsFile(stateDir),
79
+ compacted.length
80
+ ? `${compacted.map((record) => JSON.stringify(record)).join("\n")}\n`
81
+ : "",
82
+ );
83
+ }
84
+
85
+ export function appendRunControlInStateDir(
86
+ stateDir: string,
87
+ request: { run_instance_id: string; action: string; input?: unknown },
88
+ ): RunControlRecord {
89
+ const record: RunControlRecord = {
90
+ id: randomUUID(),
91
+ run_instance_id: request.run_instance_id,
92
+ action: request.action,
93
+ ...(request.input !== undefined ? { input: request.input } : {}),
94
+ status: "queued",
95
+ queued_at: new Date().toISOString(),
96
+ };
97
+ const releaseLock = acquireRunControlsLock(stateDir);
98
+ try {
99
+ writeFileSync(runControlsFile(stateDir), `${JSON.stringify(record)}\n`, {
100
+ flag: "a",
101
+ });
102
+ } finally {
103
+ releaseLock();
104
+ }
105
+ return record;
106
+ }
107
+
108
+ export function updateRunControlStatusInStateDir(
109
+ stateDir: string,
110
+ id: string,
111
+ nextStatus: RunControlStatus,
112
+ metadata: Pick<RunControlRecord, "error"> = {},
113
+ expectedStatuses?: readonly RunControlStatus[],
114
+ ): boolean {
115
+ const releaseLock = acquireRunControlsLock(stateDir);
116
+ try {
117
+ const records = readRunControlsFromStateDir(stateDir);
118
+ const index = records.findIndex((record) => record.id === id);
119
+ if (index < 0) return false;
120
+ const record = records[index]!;
121
+ if (expectedStatuses && !expectedStatuses.includes(record.status)) return false;
122
+ if (record.status === nextStatus) return false;
123
+ const timestamp = new Date().toISOString();
124
+ if (nextStatus === "delivered") {
125
+ if (record.delivered_at) return false;
126
+ records[index] = {
127
+ ...record,
128
+ delivered_at: timestamp,
129
+ ...(record.status === "queued" ? { status: "delivered" as const } : {}),
130
+ };
131
+ } else {
132
+ if (TERMINAL_STATUSES.has(record.status) || nextStatus === "queued") return false;
133
+ if (
134
+ (nextStatus === "claimed" &&
135
+ record.status !== "queued" &&
136
+ record.status !== "delivered") ||
137
+ (nextStatus === "handled" && record.status !== "claimed")
138
+ ) {
139
+ return false;
140
+ }
141
+ const timestampKey = `${nextStatus}_at` as const;
142
+ records[index] = {
143
+ ...record,
144
+ ...metadata,
145
+ [timestampKey]: timestamp,
146
+ status: nextStatus,
147
+ };
148
+ }
149
+ writeRunControls(stateDir, records);
150
+ return true;
151
+ } finally {
152
+ releaseLock();
153
+ }
154
+ }
155
+
156
+ export function claimRunControlInStateDir(
157
+ stateDir: string,
158
+ runInstanceId: string,
159
+ ): RunControlRecord | undefined {
160
+ const releaseLock = acquireRunControlsLock(stateDir);
161
+ try {
162
+ const records = readRunControlsFromStateDir(stateDir);
163
+ const index = records.findIndex(
164
+ (record) =>
165
+ record.run_instance_id === runInstanceId &&
166
+ (record.status === "queued" || record.status === "delivered"),
167
+ );
168
+ if (index < 0) return undefined;
169
+ const claimed: RunControlRecord = {
170
+ ...records[index]!,
171
+ claimed_at: new Date().toISOString(),
172
+ status: "claimed",
173
+ };
174
+ records[index] = claimed;
175
+ writeRunControls(stateDir, records);
176
+ return claimed;
177
+ } finally {
178
+ releaseLock();
179
+ }
180
+ }
181
+
182
+ export async function processRunControlsInStateDir(
183
+ stateDir: string,
184
+ runInstanceId: string,
185
+ handler: (control: RunControlRecord) => Promise<void> | void,
186
+ limit = 1,
187
+ ): Promise<ProcessRunControlsResult> {
188
+ const result: ProcessRunControlsResult = { claimed: 0, failed: 0, handled: 0 };
189
+ for (let index = 0; index < Math.max(1, limit); index += 1) {
190
+ const control = claimRunControlInStateDir(stateDir, runInstanceId);
191
+ if (!control) break;
192
+ result.claimed += 1;
193
+ try {
194
+ await handler(control);
195
+ if (updateRunControlStatusInStateDir(stateDir, control.id, "handled", {}, ["claimed"])) {
196
+ result.handled += 1;
197
+ }
198
+ } catch (error) {
199
+ if (
200
+ updateRunControlStatusInStateDir(
201
+ stateDir,
202
+ control.id,
203
+ "failed",
204
+ { error: error instanceof Error ? error.message : String(error) },
205
+ ["claimed"],
206
+ )
207
+ ) {
208
+ result.failed += 1;
209
+ }
210
+ }
211
+ }
212
+ return result;
213
+ }
@@ -3,11 +3,11 @@
3
3
  * Owns: terminal-run archive and prune filesystem behavior.
4
4
  */
5
5
 
6
- import { createHash } from "node:crypto";
7
- import { cpSync, mkdirSync, renameSync, rmSync } from "node:fs";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { appendFileSync, cpSync, mkdirSync, renameSync, rmSync } from "node:fs";
8
8
  import { basename, dirname, join } from "node:path";
9
9
 
10
- import { writeJsonAtomic } from "./file-state.ts";
10
+ import { acquireFileMutationLock, writeJsonAtomic } from "./file-state.ts";
11
11
  import {
12
12
  resolveArtifactManifest,
13
13
  type RunArtifactDeclaration,
@@ -15,6 +15,41 @@ import {
15
15
  import { safeRunId } from "./runs-identity.ts";
16
16
  import { assertOwnedRunStateDirectory } from "./runs-ownership.ts";
17
17
 
18
+ export type RunRetentionAction = "archive" | "prune";
19
+ export type RunRetentionOutcome = "queued" | "handled" | "failed";
20
+
21
+ export function appendRunRetentionEvidence(
22
+ status: Record<string, unknown>,
23
+ action: RunRetentionAction,
24
+ outcome: RunRetentionOutcome,
25
+ options: { error?: string; id?: string; result?: Record<string, unknown> } = {},
26
+ ): string {
27
+ const stateDir = String(status.state_dir);
28
+ const path = join(dirname(stateDir), "retention.jsonl");
29
+ const id = options.id ?? randomUUID();
30
+ const release = acquireFileMutationLock(path);
31
+ try {
32
+ appendFileSync(
33
+ path,
34
+ `${JSON.stringify({
35
+ action,
36
+ ...(options.error ? { error: options.error } : {}),
37
+ id,
38
+ outcome,
39
+ ...(options.result ? { result: options.result } : {}),
40
+ run: String(status.run ?? basename(stateDir)),
41
+ ...(typeof status.run_instance_id === "string"
42
+ ? { run_instance_id: status.run_instance_id }
43
+ : {}),
44
+ ts: new Date().toISOString(),
45
+ })}\n`,
46
+ );
47
+ } finally {
48
+ release();
49
+ }
50
+ return id;
51
+ }
52
+
18
53
  function retainedArtifactFilename(name: string, path: string): string {
19
54
  const readableName = name.replace(/[^A-Za-z0-9_.-]+/g, "_") || "artifact";
20
55
  const identity = createHash("sha256")
package/lib/runs-start.ts CHANGED
@@ -86,15 +86,17 @@ export function prepareStateDirForStart(
86
86
  }
87
87
  }
88
88
  for (const file of [
89
+ "control-endpoint.json",
90
+ "controls.jsonl",
89
91
  "events.jsonl",
90
- "inbox.jsonl",
91
- "outbox.jsonl",
92
+ "execution.json",
92
93
  "progress.json",
93
94
  "result.json",
94
95
  "stderr.log",
95
96
  "stdout.log",
96
97
  "terminal-delivery-failure.json",
97
98
  "terminal-handled.json",
99
+ "trace.jsonl",
98
100
  ]) {
99
101
  rmSync(join(stateDir, file), { force: true });
100
102
  }
@@ -25,10 +25,17 @@ type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
25
25
  function getInterruptedRunStatus(
26
26
  stateDir: string,
27
27
  ): "cancelled" | "killed" | undefined {
28
- const events = readJsonlFileResilient<Record<string, unknown>>(
28
+ const trace = readJsonlFileResilient<Record<string, unknown>>(
29
+ join(stateDir, "trace.jsonl"),
30
+ ).records.slice(-200);
31
+ for (const event of trace.reverse()) {
32
+ if (event.kind === "run.kill") return "killed";
33
+ if (event.kind === "run.cancel") return "cancelled";
34
+ }
35
+ const legacyEvents = readJsonlFileResilient<Record<string, unknown>>(
29
36
  join(stateDir, "events.jsonl"),
30
37
  ).records.slice(-200);
31
- for (const event of events.reverse()) {
38
+ for (const event of legacyEvents.reverse()) {
32
39
  if (event.event === "run.kill") return "killed";
33
40
  if (event.event === "run.cancel") return "cancelled";
34
41
  }
@@ -66,10 +73,8 @@ export function buildRunStatus(
66
73
  );
67
74
  return {
68
75
  ...meta,
69
- eventsFile: join(stateDir, "events.jsonl"),
70
- evidenceFile: join(stateDir, "review-evidence.json"),
71
- inboxFile: join(stateDir, "inbox.jsonl"),
72
- outboxFile: join(stateDir, "outbox.jsonl"),
76
+ traceFile: join(stateDir, "trace.jsonl"),
77
+ executionFile: join(stateDir, "execution.json"),
73
78
  process_identity_status: processIdentity.status,
74
79
  progress: readJson(join(stateDir, "progress.json")) || null,
75
80
  result: result || null,
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Run Trace event journal.
3
+ * Zones: structured event validation, token-locked append-only writes, resilient bounded reads
4
+ * Owns canonical Run-local Trace persistence; lifecycle projection and owner attention delivery stay in adapters.
5
+ */
6
+
7
+ import { randomUUID } from "node:crypto";
8
+ import { writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ import { withFileMutationLock } from "./file-state.ts";
12
+ import * as Limits from "./limits.ts";
13
+ import { readJsonlFileResilient } from "./state-readers.ts";
14
+
15
+ export interface TraceEvent {
16
+ id: string;
17
+ ts: string;
18
+ kind: string;
19
+ summary?: string;
20
+ data?: unknown;
21
+ level?: "info" | "warning" | "error";
22
+ attention?: "notify" | "followup";
23
+ }
24
+
25
+ export interface AppendTraceEventInput {
26
+ kind: string;
27
+ summary?: string;
28
+ data?: unknown;
29
+ level?: TraceEvent["level"];
30
+ attention?: TraceEvent["attention"];
31
+ }
32
+
33
+ const KIND_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)*$/;
34
+ const TRACE_INPUT_FIELDS = new Set([
35
+ "attention",
36
+ "data",
37
+ "kind",
38
+ "level",
39
+ "summary",
40
+ ]);
41
+ const FORBIDDEN_FIELDS = new Set([
42
+ "body",
43
+ "correlation_id",
44
+ "from",
45
+ "metadata",
46
+ "reply_to",
47
+ "to",
48
+ "type",
49
+ ]);
50
+
51
+ export function runTraceFile(stateDir: string): string {
52
+ return join(stateDir, "trace.jsonl");
53
+ }
54
+
55
+ function normalizeTraceEventInput(input: unknown): AppendTraceEventInput {
56
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
57
+ throw new Error("Trace event must be an object");
58
+ }
59
+ const record = input as Record<string, unknown>;
60
+ const forbidden = Object.keys(record).filter((key) => FORBIDDEN_FIELDS.has(key));
61
+ if (forbidden.length > 0) {
62
+ throw new Error(`Trace event fields are removed: ${forbidden.sort().join(", ")}`);
63
+ }
64
+ const unknown = Object.keys(record).filter((key) => !TRACE_INPUT_FIELDS.has(key));
65
+ if (unknown.length > 0) {
66
+ throw new Error(`Unsupported Trace event fields: ${unknown.sort().join(", ")}`);
67
+ }
68
+ if (typeof record.kind !== "string" || !KIND_PATTERN.test(record.kind)) {
69
+ throw new Error("Trace event kind must be a lowercase semantic token");
70
+ }
71
+ if (record.summary !== undefined && typeof record.summary !== "string") {
72
+ throw new Error("Trace event summary must be a string");
73
+ }
74
+ if (
75
+ record.level !== undefined &&
76
+ record.level !== "info" &&
77
+ record.level !== "warning" &&
78
+ record.level !== "error"
79
+ ) {
80
+ throw new Error("Trace event level must be info, warning, or error");
81
+ }
82
+ if (
83
+ record.attention !== undefined &&
84
+ record.attention !== "notify" &&
85
+ record.attention !== "followup"
86
+ ) {
87
+ throw new Error("Trace event attention must be notify or followup");
88
+ }
89
+ return {
90
+ kind: record.kind,
91
+ ...(record.summary !== undefined
92
+ ? { summary: record.summary.slice(0, 1_000) }
93
+ : {}),
94
+ ...(record.data !== undefined ? { data: record.data } : {}),
95
+ ...(record.level !== undefined
96
+ ? { level: record.level as TraceEvent["level"] }
97
+ : {}),
98
+ ...(record.attention !== undefined
99
+ ? { attention: record.attention as TraceEvent["attention"] }
100
+ : {}),
101
+ };
102
+ }
103
+
104
+ export function appendRunTraceEvent(
105
+ stateDir: string,
106
+ input: AppendTraceEventInput,
107
+ ): TraceEvent {
108
+ const path = runTraceFile(stateDir);
109
+ return withFileMutationLock(path, () => {
110
+ const normalized = normalizeTraceEventInput(input);
111
+ const event: TraceEvent = {
112
+ id: randomUUID(),
113
+ ts: new Date().toISOString(),
114
+ ...normalized,
115
+ };
116
+ let encoded: string;
117
+ try {
118
+ encoded = JSON.stringify(event);
119
+ } catch {
120
+ throw new Error("Trace event data must be JSON-serializable");
121
+ }
122
+ if (Buffer.byteLength(encoded) > Limits.TRACE_EVENT_MAX_BYTES) {
123
+ throw new Error(`Trace event exceeds ${Limits.TRACE_EVENT_MAX_BYTES} bytes`);
124
+ }
125
+ writeFileSync(path, `${encoded}\n`, { flag: "a" });
126
+ return event;
127
+ });
128
+ }
129
+
130
+ export function readRunTraceEvents(
131
+ stateDir: string,
132
+ limit = Limits.TRACE_EVENT_MAX_READ,
133
+ ): TraceEvent[] {
134
+ const records = readJsonlFileResilient<TraceEvent>(runTraceFile(stateDir)).records;
135
+ return records.slice(-Math.max(1, Math.min(limit, Limits.TRACE_EVENT_MAX_READ)));
136
+ }