@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
@@ -43,6 +43,7 @@ const { execCommandTemplate } = await importRuntimeModule("command-templates");
43
43
  const { applyOutputAcceptancePolicy, executeRegisteredTool } =
44
44
  await importRuntimeModule("execution");
45
45
  const { writeJsonAtomic } = await importRuntimeModule("file-state");
46
+ const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
46
47
 
47
48
  function quoteCommandDetailPart(value) {
48
49
  if (value === "") return "''";
@@ -77,25 +78,24 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
77
78
  const runPath = join(stateDir, "run.json");
78
79
  const progressPath = join(stateDir, "progress.json");
79
80
  const resultPath = join(stateDir, "result.json");
80
- const eventsPath = join(stateDir, "events.jsonl");
81
- const outboxPath = join(stateDir, "outbox.jsonl");
81
+ const tracePath = join(stateDir, "trace.jsonl");
82
82
  const stdoutPath = join(stateDir, "stdout.log");
83
83
  const stderrPath = join(stateDir, "stderr.log");
84
- const evidencePath = join(stateDir, "review-evidence.json");
84
+ const executionPath = join(stateDir, "execution.json");
85
85
  const meta = JSON.parse(readFileSync(runPath, "utf8"));
86
86
 
87
- function event(name, data = {}) {
88
- appendFileSync(
89
- eventsPath,
90
- `${JSON.stringify({ event: name, ts: new Date().toISOString(), ...data })}\n`,
91
- );
87
+ function event(kind, data = {}) {
88
+ appendRunTraceEvent(stateDir, { kind, data });
92
89
  }
93
90
 
94
- function outbox(name, summary, data = {}, delivery = "log", level = "info") {
95
- appendFileSync(
96
- outboxPath,
97
- `${JSON.stringify({ body: data, data, delivery, event: name, from: `run:${meta.run}`, level, summary, to: "coordinator", ts: new Date().toISOString(), type: name })}\n`,
98
- );
91
+ function observation(kind, summary, data = {}, delivery = "log", level = "info") {
92
+ appendRunTraceEvent(stateDir, {
93
+ kind,
94
+ summary,
95
+ data,
96
+ ...(delivery === "followup" ? { attention: "followup" } : {}),
97
+ ...(level === "error" ? { level: "error" } : {}),
98
+ });
99
99
  }
100
100
 
101
101
  function progress(phase, extra = {}) {
@@ -116,8 +116,8 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
116
116
  const stageOccurrences = new Map();
117
117
  let reportEvidence;
118
118
 
119
- function writeEvidenceManifest(status) {
120
- writeJsonAtomic(evidencePath, {
119
+ function writeExecutionManifest(status) {
120
+ writeJsonAtomic(executionPath, {
121
121
  version: 1,
122
122
  run: meta.run,
123
123
  status,
@@ -279,10 +279,10 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
279
279
  .filter((record) =>
280
280
  ["reviewer", "verifier", "merger", "judge"].includes(record.stage)
281
281
  )
282
- .map((record) => `review-evidence.json#${record.id}`);
282
+ .map((record) => `execution.json#${record.id}`);
283
283
  if (required.length === 0) return undefined;
284
284
  const cited = [...new Set(
285
- text.match(/review-evidence\.json#command-\d{3}/g) || [],
285
+ text.match(/execution\.json#command-\d{3}/g) || [],
286
286
  )].sort();
287
287
  const missing = required.filter((reference) => !cited.includes(reference));
288
288
  const claimsComplete = /\bStatus\b[\s:*#_-]{0,40}\bcomplete\b/i.test(text);
@@ -358,7 +358,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
358
358
  .filter((record) =>
359
359
  ["reviewer", "verifier", "merger", "judge"].includes(record.stage)
360
360
  )
361
- .map((record) => `ACTOR_EVIDENCE_REF: review-evidence.json#${record.id}`);
361
+ .map((record) => `ACTOR_EVIDENCE_REF: execution.json#${record.id}`);
362
362
  if (references.length > 0) {
363
363
  appendFileSync(
364
364
  materialized.promptFile,
@@ -379,7 +379,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
379
379
  occurrence,
380
380
  });
381
381
  evidenceRecords.push(startedEvidence);
382
- writeEvidenceManifest("running");
382
+ writeExecutionManifest("running");
383
383
  event("command.start", {
384
384
  activeSubagents,
385
385
  command_id: commandId,
@@ -400,7 +400,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
400
400
  );
401
401
  result = {
402
402
  ...result,
403
- evidenceRef: `review-evidence.json#${commandId}`,
403
+ evidenceRef: `execution.json#${commandId}`,
404
404
  };
405
405
  const preflightDiagnostic = result.code !== 0
406
406
  ? buildReviewPreflightDiagnostic({
@@ -435,7 +435,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
435
435
  occurrence,
436
436
  startedAt: startedEvidence.started_at,
437
437
  });
438
- writeEvidenceManifest("running");
438
+ writeExecutionManifest("running");
439
439
  activeSubagents = Math.max(0, activeSubagents - 1);
440
440
  completedSubagents += 1;
441
441
  if (result.code !== 0) {
@@ -463,13 +463,13 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
463
463
  : {}),
464
464
  ...(preflightDiagnostic ? { preflight: preflightDiagnostic } : {}),
465
465
  });
466
- outbox(
466
+ observation(
467
467
  "command.done",
468
468
  `Command ${summarizeCommandDetail(commandDetail)} completed with code ${result.code}`,
469
469
  {
470
470
  activeSubagents,
471
471
  ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
472
- run_files: [stdoutPath, stderrPath, resultPath, eventsPath, outboxPath, evidencePath],
472
+ run_files: [stdoutPath, stderrPath, resultPath, tracePath, executionPath],
473
473
  command_id: commandId,
474
474
  code: result.code,
475
475
  command: commandDetail,
@@ -517,7 +517,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
517
517
  };
518
518
  throw error;
519
519
  }
520
- writeEvidenceManifest("done");
520
+ writeExecutionManifest("done");
521
521
  progress("done", {
522
522
  completed: 1,
523
523
  failures: result.details.nonCriticalFailures || [],
@@ -535,7 +535,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
535
535
  const message = error instanceof Error ? error.message : String(error);
536
536
  const details = error && typeof error === "object" ? error.details : undefined;
537
537
  appendFileSync(stderrPath, `${message}\n`);
538
- writeEvidenceManifest("failed");
538
+ writeExecutionManifest("failed");
539
539
  progress("failed", {
540
540
  completed: 0,
541
541
  failures: Array.isArray(details?.branches) && details.branches.length > 0
@@ -12,13 +12,15 @@ import { dirname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
14
  const conformanceSuites = [
15
- "tests/protocol-examples.test.ts",
15
+ "tests/control.test.ts",
16
+ "tests/runs-controls.test.ts",
17
+ "tests/runs-trace.test.ts",
18
+ "tests/tools-inspect-kernel.test.ts",
16
19
  "tests/recipes-discovery.test.ts",
17
20
  "tests/review-swarm-dogfood.test.ts",
18
21
  "tests/registry.test.ts",
19
22
  "tests/runtime.test.ts",
20
23
  "tests/async-runs.test.ts",
21
- "tests/rooms.test.ts",
22
24
  "tests/tools.test.ts",
23
25
  ];
24
26
 
@@ -15,12 +15,26 @@ import {
15
15
  existsSync,
16
16
  mkdirSync,
17
17
  readFileSync,
18
- rmSync,
19
- writeFileSync,
20
18
  } from "node:fs";
21
19
  import { createServer } from "node:net";
22
20
  import { dirname, join, resolve } from "node:path";
23
21
  import readline from "node:readline";
22
+ import { fileURLToPath, pathToFileURL } from "node:url";
23
+
24
+ function packageRoot() {
25
+ return dirname(dirname(fileURLToPath(import.meta.url)));
26
+ }
27
+
28
+ async function importRuntimeModule(name) {
29
+ const root = packageRoot();
30
+ const compiled = join(root, "dist", "lib", `${name}.js`);
31
+ const source = join(root, "lib", `${name}.ts`);
32
+ return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
33
+ }
34
+
35
+ const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
36
+ await importRuntimeModule("file-state");
37
+ const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
24
38
 
25
39
  function parseArgs(argv) {
26
40
  const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
@@ -45,8 +59,9 @@ async function runLocker(argv = process.argv.slice(2)) {
45
59
  const queuePath = join(stateDir, "queue.json");
46
60
  const locksPath = join(stateDir, "locks.json");
47
61
  const journalPath = join(stateDir, "journal.jsonl");
48
- const outboxPath = join(stateDir, "outbox.jsonl");
62
+ const controlsPath = join(stateDir, "controls.jsonl");
49
63
  const controlPath = join(stateDir, "control.fifo");
64
+ let runInstanceId;
50
65
  mkdirSync(stateDir, { recursive: true });
51
66
 
52
67
  function readJson(path, fallback) {
@@ -59,8 +74,7 @@ async function runLocker(argv = process.argv.slice(2)) {
59
74
  }
60
75
 
61
76
  function writeJson(path, value) {
62
- mkdirSync(dirname(path), { recursive: true });
63
- writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
77
+ writeJsonAtomic(path, value);
64
78
  }
65
79
 
66
80
  function getControlEndpoint() {
@@ -73,11 +87,12 @@ async function runLocker(argv = process.argv.slice(2)) {
73
87
  }
74
88
 
75
89
  function writeControlEndpoint(endpoint) {
76
- writeJson(join(stateDir, "control.json"), endpoint);
77
- const runPath = join(stateDir, "run.json");
78
- const run = readJson(runPath, undefined);
79
- if (run && typeof run === "object")
80
- writeJson(runPath, { ...run, control: endpoint });
90
+ if (!runInstanceId) throw new Error("Run generation unavailable for Control endpoint");
91
+ writeJson(join(stateDir, "control-endpoint.json"), {
92
+ ...endpoint,
93
+ ready_at: new Date().toISOString(),
94
+ run_instance_id: runInstanceId,
95
+ });
81
96
  }
82
97
 
83
98
  function journal(event, data = {}) {
@@ -87,11 +102,14 @@ async function runLocker(argv = process.argv.slice(2)) {
87
102
  );
88
103
  }
89
104
 
90
- function outbox(type, summary, body = {}, level = "info") {
91
- appendFileSync(
92
- outboxPath,
93
- `${JSON.stringify({ to: "coordinator", from: `run:${process.env.run_id ?? "locker"}`, type, event: type, summary, body, data: body, delivery: "followup", level, ts: new Date().toISOString() })}\n`,
94
- );
105
+ function emitTrace(kind, summary, data = {}, level = "info") {
106
+ appendRunTraceEvent(stateDir, {
107
+ attention: "followup",
108
+ data,
109
+ kind,
110
+ level,
111
+ summary,
112
+ });
95
113
  }
96
114
 
97
115
  function now() {
@@ -108,16 +126,88 @@ async function runLocker(argv = process.argv.slice(2)) {
108
126
  return kept;
109
127
  }
110
128
 
111
- function normalizeMessage(line) {
129
+ function normalizeControl(line) {
112
130
  const trimmed = line.trim();
113
131
  if (!trimmed) return undefined;
114
- if (["stop", "quit", "exit", "cancel"].includes(trimmed.toLowerCase())) {
115
- return { type: "control.stop", body: {} };
132
+ try {
133
+ const control = JSON.parse(trimmed);
134
+ if (
135
+ !control ||
136
+ typeof control !== "object" ||
137
+ typeof control.id !== "string" ||
138
+ typeof control.action !== "string"
139
+ ) {
140
+ throw new Error("invalid Control envelope");
141
+ }
142
+ return control;
143
+ } catch (error) {
144
+ throw new Error(
145
+ `Invalid Control envelope: ${error instanceof Error ? error.message : String(error)}`,
146
+ );
147
+ }
148
+ }
149
+
150
+ function acquireControlsLock() {
151
+ return acquireFileMutationLock(controlsPath);
152
+ }
153
+
154
+ function readControls() {
155
+ if (!existsSync(controlsPath)) return [];
156
+ return readFileSync(controlsPath, "utf8")
157
+ .split("\n")
158
+ .filter((line) => line.trim())
159
+ .map((line) => {
160
+ try {
161
+ return JSON.parse(line);
162
+ } catch {
163
+ return undefined;
164
+ }
165
+ })
166
+ .filter(Boolean);
167
+ }
168
+
169
+ function writeControls(controls) {
170
+ writeTextAtomic(
171
+ controlsPath,
172
+ controls.length
173
+ ? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
174
+ : "",
175
+ );
176
+ }
177
+
178
+ function claimControl(id) {
179
+ const release = acquireControlsLock();
180
+ try {
181
+ const controls = readControls();
182
+ const control = controls.find((item) => item.id === id);
183
+ if (
184
+ !control ||
185
+ control.run_instance_id !== runInstanceId ||
186
+ (control.status !== "queued" && control.status !== "delivered")
187
+ ) {
188
+ return undefined;
189
+ }
190
+ control.claimed_at = new Date().toISOString();
191
+ control.status = "claimed";
192
+ writeControls(controls);
193
+ return { ...control };
194
+ } finally {
195
+ release();
116
196
  }
197
+ }
198
+
199
+ function finalizeControl(id, status, error) {
200
+ const release = acquireControlsLock();
117
201
  try {
118
- return JSON.parse(trimmed);
119
- } catch {
120
- return { type: "lock.enqueue", body: { task: trimmed } };
202
+ const controls = readControls();
203
+ const control = controls.find((item) => item.id === id);
204
+ if (!control || control.status !== "claimed") return;
205
+ control.status = status;
206
+ control[`${status}_at`] = new Date().toISOString();
207
+ if (error) control.error = error;
208
+ writeControls(controls);
209
+ } finally {
210
+ release();
121
211
  }
122
212
  }
123
213
 
@@ -165,21 +255,21 @@ async function runLocker(argv = process.argv.slice(2)) {
165
255
  return items.splice(index, 1)[0];
166
256
  }
167
257
 
168
- function handle(message) {
169
- const type = message.type || message.event || "lock.enqueue";
258
+ function handle(control) {
259
+ const action = control.action;
170
260
  const body =
171
- message.body && typeof message.body === "object" ? message.body : message;
261
+ control.input && typeof control.input === "object" ? control.input : {};
172
262
  let queue = readJson(queuePath, { items: [] });
173
263
  let locks = cleanExpiredLocks(readJson(locksPath, {}));
174
- if (type === "control.stop" || type === "control.cancel") {
264
+ if (action === "stop") {
175
265
  writeJson(locksPath, locks);
176
- journal("control.stop", {});
177
- outbox("lock.stopped", "Locker stopped", {
266
+ journal("lock.stopped", {});
267
+ emitTrace("lock.stopped", "Locker stopped", {
178
268
  queueDepth: queue.items?.length ?? 0,
179
269
  });
180
- process.exit(0);
270
+ return true;
181
271
  }
182
- if (type === "lock.enqueue" || type === "coord.enqueue") {
272
+ if (action === "enqueue") {
183
273
  const item = {
184
274
  id: body.id || `task-${Date.now()}`,
185
275
  task: body.task ?? body,
@@ -190,19 +280,20 @@ async function runLocker(argv = process.argv.slice(2)) {
190
280
  writeJson(queuePath, queue);
191
281
  writeJson(locksPath, locks);
192
282
  journal("lock.enqueued", { id: item.id, resources: item.resources });
193
- outbox("lock.enqueued", `Queued task ${item.id}`, {
283
+ emitTrace("lock.enqueued", `Queued task ${item.id}`, {
194
284
  id: item.id,
195
285
  queueDepth: queue.items.length,
196
286
  });
197
287
  return;
198
288
  }
199
- if (type === "lock.claim" || type === "coord.claim") {
200
- const owner = body.owner || message.from || "worker";
289
+ if (action === "claim") {
290
+ const owner = body.owner;
291
+ if (!owner) throw new Error("claim input.owner is required");
201
292
  const item = nextTask(queue, locks);
202
293
  if (!item) {
203
294
  writeJson(queuePath, queue);
204
295
  writeJson(locksPath, locks);
205
- outbox("lock.empty", "No claimable task", {
296
+ emitTrace("lock.empty", "No claimable task", {
206
297
  owner,
207
298
  queueDepth: queue.items?.length ?? 0,
208
299
  });
@@ -217,15 +308,16 @@ async function runLocker(argv = process.argv.slice(2)) {
217
308
  owner,
218
309
  resources: item.resources,
219
310
  });
220
- outbox("lock.assigned", `Assigned task ${item.id}`, { owner, task: item });
311
+ emitTrace("lock.assigned", `Assigned task ${item.id}`, { owner, task: item });
221
312
  return;
222
313
  }
223
- if (type === "lock.acquire") {
314
+ if (action === "acquire") {
224
315
  const resource = body.resource;
225
- const owner = body.owner || message.from || "worker";
226
- if (!resource) throw new Error("lock.acquire body.resource is required");
316
+ const owner = body.owner;
317
+ if (!resource || !owner)
318
+ throw new Error("acquire input.resource and input.owner are required");
227
319
  if (locks[resource])
228
- outbox(
320
+ emitTrace(
229
321
  "lock.denied",
230
322
  `Lock denied ${resource}`,
231
323
  { resource, owner, current: locks[resource] },
@@ -233,25 +325,26 @@ async function runLocker(argv = process.argv.slice(2)) {
233
325
  );
234
326
  else {
235
327
  locks[resource] = { owner, expiresAt: now() + leaseMs };
236
- outbox("lock.granted", `Lock granted ${resource}`, { resource, owner });
328
+ emitTrace("lock.granted", `Lock granted ${resource}`, { resource, owner });
237
329
  }
238
330
  writeJson(locksPath, locks);
239
331
  return;
240
332
  }
241
- if (type === "lock.renew") {
333
+ if (action === "renew") {
242
334
  const resource = body.resource;
243
- const owner = body.owner || message.from || "worker";
244
- if (!resource) throw new Error("lock.renew body.resource is required");
335
+ const owner = body.owner;
336
+ if (!resource || !owner)
337
+ throw new Error("renew input.resource and input.owner are required");
245
338
  const current = locks[resource];
246
339
  if (!current) {
247
- outbox(
340
+ emitTrace(
248
341
  "lock.denied",
249
342
  `Lock renew denied ${resource}`,
250
343
  { resource, owner, reason: "missing" },
251
344
  "warning",
252
345
  );
253
346
  } else if (current.owner !== owner) {
254
- outbox(
347
+ emitTrace(
255
348
  "lock.denied",
256
349
  `Lock renew denied ${resource}`,
257
350
  { resource, owner, current },
@@ -259,29 +352,24 @@ async function runLocker(argv = process.argv.slice(2)) {
259
352
  );
260
353
  } else {
261
354
  locks[resource] = { ...current, expiresAt: now() + leaseMs };
262
- outbox("lock.renewed", `Lock renewed ${resource}`, { resource, owner });
355
+ emitTrace("lock.renewed", `Lock renewed ${resource}`, { resource, owner });
263
356
  }
264
357
  writeJson(locksPath, locks);
265
358
  return;
266
359
  }
267
- if (type === "lock.release") {
360
+ if (action === "release") {
268
361
  const resource = body.resource;
269
362
  if (resource) delete locks[resource];
270
363
  writeJson(locksPath, locks);
271
- outbox("lock.released", `Lock released ${resource}`, { resource });
364
+ emitTrace("lock.released", `Lock released ${resource}`, { resource });
272
365
  return;
273
366
  }
274
367
  if (
275
- type === "lock.complete" ||
276
- type === "lock.fail" ||
277
- type === "coord.complete" ||
278
- type === "coord.fail"
368
+ action === "complete" || action === "fail"
279
369
  ) {
280
- const eventType = type.startsWith("coord.")
281
- ? type.replace("coord.", "lock.")
282
- : type;
370
+ const eventType = `lock.${action}`;
283
371
  journal(eventType, body);
284
- outbox(
372
+ emitTrace(
285
373
  eventType,
286
374
  `${eventType} ${body.id ?? ""}`.trim(),
287
375
  body,
@@ -291,8 +379,8 @@ async function runLocker(argv = process.argv.slice(2)) {
291
379
  writeJson(queuePath, queue);
292
380
  return;
293
381
  }
294
- journal("lock.unknown", { type, body });
295
- outbox("lock.unknown", `Unknown message ${type}`, { type, body }, "warning");
382
+ journal("lock.unknown", { action, input: body });
383
+ emitTrace("lock.unknown", `Unknown Control ${action}`, { action, input: body }, "warning");
296
384
  }
297
385
 
298
386
  if (mode === "snapshot") {
@@ -300,15 +388,35 @@ async function runLocker(argv = process.argv.slice(2)) {
300
388
  process.exit(0);
301
389
  }
302
390
 
391
+ const startupRun = readJson(join(stateDir, "run.json"), undefined);
392
+ if (!startupRun || typeof startupRun.run_instance_id !== "string") {
393
+ throw new Error("Run generation unavailable for Control service");
394
+ }
395
+ runInstanceId = startupRun.run_instance_id;
396
+
303
397
  function handleLine(line) {
304
- const message = normalizeMessage(line);
305
- if (!message) return;
398
+ const control = normalizeControl(line);
399
+ if (!control) return false;
400
+ const claimed = claimControl(control.id);
401
+ if (!claimed) {
402
+ emitTrace(
403
+ "lock.control_rejected",
404
+ "Rejected stale or unjournaled Control",
405
+ { id: control.id },
406
+ "warning",
407
+ );
408
+ return false;
409
+ }
306
410
  try {
307
- handle(message);
411
+ const stopping = handle(claimed) === true;
412
+ finalizeControl(claimed.id, "handled");
413
+ return stopping;
308
414
  } catch (error) {
309
415
  const text = error instanceof Error ? error.message : String(error);
416
+ finalizeControl(claimed.id, "failed", text);
310
417
  journal("lock.error", { error: text });
311
- outbox("lock.error", text, { error: text }, "error");
418
+ emitTrace("lock.error", text, { error: text }, "error");
419
+ return false;
312
420
  }
313
421
  }
314
422
 
@@ -320,19 +428,38 @@ async function runLocker(argv = process.argv.slice(2)) {
320
428
  }
321
429
  writeControlEndpoint(endpoint);
322
430
  while (true) {
323
- const stream = await import("node:fs").then((fs) =>
324
- fs.createReadStream(endpoint.path, { encoding: "utf8" }),
325
- );
431
+ const fs = await import("node:fs");
432
+ const fd = fs.openSync(endpoint.path, fs.constants.O_RDWR);
433
+ const stream = fs.createReadStream(undefined, {
434
+ autoClose: false,
435
+ encoding: "utf8",
436
+ fd,
437
+ });
326
438
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
327
- for await (const line of rl) handleLine(line);
439
+ for await (const line of rl) {
440
+ if (handleLine(line)) {
441
+ fs.writeSync(fd, "\n");
442
+ stream.destroy();
443
+ return;
444
+ }
445
+ }
446
+ fs.closeSync(fd);
328
447
  }
329
448
  }
330
449
 
331
450
  async function serveNamedPipe(endpoint) {
332
- rmSync(endpoint.path, { force: true });
451
+ let resolveStopped;
452
+ const stopped = new Promise((resolve) => {
453
+ resolveStopped = resolve;
454
+ });
333
455
  const server = createServer((socket) => {
334
456
  const rl = readline.createInterface({ input: socket, crlfDelay: Infinity });
335
- rl.on("line", handleLine);
457
+ rl.on("line", (line) => {
458
+ if (!handleLine(line)) return;
459
+ rl.close();
460
+ socket.destroy();
461
+ server.close(() => resolveStopped());
462
+ });
336
463
  });
337
464
  await new Promise((resolveReady, rejectReady) => {
338
465
  server.once("error", rejectReady);
@@ -342,14 +469,14 @@ async function runLocker(argv = process.argv.slice(2)) {
342
469
  resolveReady();
343
470
  });
344
471
  });
345
- await new Promise(() => {});
472
+ await stopped;
346
473
  }
347
474
 
348
475
  const endpoint = getControlEndpoint();
349
476
  writeJson(queuePath, readJson(queuePath, { items: [] }));
350
477
  writeJson(locksPath, cleanExpiredLocks(readJson(locksPath, {})));
351
478
  journal("lock.started", { leaseMs, control: endpoint.type });
352
- outbox("lock.started", "Locker ready", { leaseMs, control: endpoint.type });
479
+ emitTrace("lock.started", "Locker ready", { leaseMs, control: endpoint.type });
353
480
  if (endpoint.type === "named-pipe") await serveNamedPipe(endpoint);
354
481
  else await serveFifo(endpoint);
355
482
  }