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