@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
@@ -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
 
@@ -9,18 +9,32 @@
9
9
  */
10
10
 
11
11
  import { spawnSync } from "node:child_process";
12
- import { createHash } from "node:crypto";
12
+ import { createHash, randomUUID } from "node:crypto";
13
13
  import {
14
14
  appendFileSync,
15
15
  existsSync,
16
16
  mkdirSync,
17
17
  readFileSync,
18
18
  rmSync,
19
- writeFileSync,
20
19
  } from "node:fs";
21
20
  import { createServer } from "node:net";
22
21
  import { dirname, join, resolve } from "node:path";
23
22
  import readline from "node:readline";
23
+ import { fileURLToPath, pathToFileURL } from "node:url";
24
+
25
+ function packageRoot() {
26
+ return dirname(dirname(fileURLToPath(import.meta.url)));
27
+ }
28
+
29
+ async function importRuntimeModule(name) {
30
+ const root = packageRoot();
31
+ const compiled = join(root, "dist", "lib", `${name}.js`);
32
+ const source = join(root, "lib", `${name}.ts`);
33
+ return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
34
+ }
35
+
36
+ const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
37
+ await importRuntimeModule("file-state");
24
38
 
25
39
  function parseArgs(argv) {
26
40
  const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
@@ -45,8 +59,10 @@ 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 tracePath = join(stateDir, "trace.jsonl");
63
+ const controlsPath = join(stateDir, "controls.jsonl");
49
64
  const controlPath = join(stateDir, "control.fifo");
65
+ let runInstanceId;
50
66
  mkdirSync(stateDir, { recursive: true });
51
67
 
52
68
  function readJson(path, fallback) {
@@ -59,8 +75,7 @@ async function runLocker(argv = process.argv.slice(2)) {
59
75
  }
60
76
 
61
77
  function writeJson(path, value) {
62
- mkdirSync(dirname(path), { recursive: true });
63
- writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
78
+ writeJsonAtomic(path, value);
64
79
  }
65
80
 
66
81
  function getControlEndpoint() {
@@ -73,11 +88,12 @@ async function runLocker(argv = process.argv.slice(2)) {
73
88
  }
74
89
 
75
90
  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 });
91
+ if (!runInstanceId) throw new Error("Run generation unavailable for Control endpoint");
92
+ writeJson(join(stateDir, "control-endpoint.json"), {
93
+ ...endpoint,
94
+ ready_at: new Date().toISOString(),
95
+ run_instance_id: runInstanceId,
96
+ });
81
97
  }
82
98
 
83
99
  function journal(event, data = {}) {
@@ -87,10 +103,18 @@ async function runLocker(argv = process.argv.slice(2)) {
87
103
  );
88
104
  }
89
105
 
90
- function outbox(type, summary, body = {}, level = "info") {
106
+ function emitTrace(kind, summary, data = {}, level = "info") {
91
107
  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`,
108
+ tracePath,
109
+ `${JSON.stringify({
110
+ attention: "followup",
111
+ data,
112
+ id: randomUUID(),
113
+ kind,
114
+ level,
115
+ summary,
116
+ ts: new Date().toISOString(),
117
+ })}\n`,
94
118
  );
95
119
  }
96
120
 
@@ -108,16 +132,88 @@ async function runLocker(argv = process.argv.slice(2)) {
108
132
  return kept;
109
133
  }
110
134
 
111
- function normalizeMessage(line) {
135
+ function normalizeControl(line) {
112
136
  const trimmed = line.trim();
113
137
  if (!trimmed) return undefined;
114
- if (["stop", "quit", "exit", "cancel"].includes(trimmed.toLowerCase())) {
115
- return { type: "control.stop", body: {} };
138
+ try {
139
+ const control = JSON.parse(trimmed);
140
+ if (
141
+ !control ||
142
+ typeof control !== "object" ||
143
+ typeof control.id !== "string" ||
144
+ typeof control.action !== "string"
145
+ ) {
146
+ throw new Error("invalid Control envelope");
147
+ }
148
+ return control;
149
+ } catch (error) {
150
+ throw new Error(
151
+ `Invalid Control envelope: ${error instanceof Error ? error.message : String(error)}`,
152
+ );
116
153
  }
154
+ }
155
+
156
+ function acquireControlsLock() {
157
+ return acquireFileMutationLock(controlsPath);
158
+ }
159
+
160
+ function readControls() {
161
+ if (!existsSync(controlsPath)) return [];
162
+ return readFileSync(controlsPath, "utf8")
163
+ .split("\n")
164
+ .filter((line) => line.trim())
165
+ .map((line) => {
166
+ try {
167
+ return JSON.parse(line);
168
+ } catch {
169
+ return undefined;
170
+ }
171
+ })
172
+ .filter(Boolean);
173
+ }
174
+
175
+ function writeControls(controls) {
176
+ writeTextAtomic(
177
+ controlsPath,
178
+ controls.length
179
+ ? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
180
+ : "",
181
+ );
182
+ }
183
+
184
+ function claimControl(id) {
185
+ const release = acquireControlsLock();
117
186
  try {
118
- return JSON.parse(trimmed);
119
- } catch {
120
- return { type: "lock.enqueue", body: { task: trimmed } };
187
+ const controls = readControls();
188
+ const control = controls.find((item) => item.id === id);
189
+ if (
190
+ !control ||
191
+ control.run_instance_id !== runInstanceId ||
192
+ (control.status !== "queued" && control.status !== "delivered")
193
+ ) {
194
+ return undefined;
195
+ }
196
+ control.claimed_at = new Date().toISOString();
197
+ control.status = "claimed";
198
+ writeControls(controls);
199
+ return { ...control };
200
+ } finally {
201
+ release();
202
+ }
203
+ }
204
+
205
+ function finalizeControl(id, status, error) {
206
+ const release = acquireControlsLock();
207
+ try {
208
+ const controls = readControls();
209
+ const control = controls.find((item) => item.id === id);
210
+ if (!control || control.status !== "claimed") return;
211
+ control.status = status;
212
+ control[`${status}_at`] = new Date().toISOString();
213
+ if (error) control.error = error;
214
+ writeControls(controls);
215
+ } finally {
216
+ release();
121
217
  }
122
218
  }
123
219
 
@@ -165,21 +261,21 @@ async function runLocker(argv = process.argv.slice(2)) {
165
261
  return items.splice(index, 1)[0];
166
262
  }
167
263
 
168
- function handle(message) {
169
- const type = message.type || message.event || "lock.enqueue";
264
+ function handle(control) {
265
+ const action = control.action;
170
266
  const body =
171
- message.body && typeof message.body === "object" ? message.body : message;
267
+ control.input && typeof control.input === "object" ? control.input : {};
172
268
  let queue = readJson(queuePath, { items: [] });
173
269
  let locks = cleanExpiredLocks(readJson(locksPath, {}));
174
- if (type === "control.stop" || type === "control.cancel") {
270
+ if (action === "stop") {
175
271
  writeJson(locksPath, locks);
176
- journal("control.stop", {});
177
- outbox("lock.stopped", "Locker stopped", {
272
+ journal("lock.stopped", {});
273
+ emitTrace("lock.stopped", "Locker stopped", {
178
274
  queueDepth: queue.items?.length ?? 0,
179
275
  });
180
- process.exit(0);
276
+ return true;
181
277
  }
182
- if (type === "lock.enqueue" || type === "coord.enqueue") {
278
+ if (action === "enqueue") {
183
279
  const item = {
184
280
  id: body.id || `task-${Date.now()}`,
185
281
  task: body.task ?? body,
@@ -190,19 +286,20 @@ async function runLocker(argv = process.argv.slice(2)) {
190
286
  writeJson(queuePath, queue);
191
287
  writeJson(locksPath, locks);
192
288
  journal("lock.enqueued", { id: item.id, resources: item.resources });
193
- outbox("lock.enqueued", `Queued task ${item.id}`, {
289
+ emitTrace("lock.enqueued", `Queued task ${item.id}`, {
194
290
  id: item.id,
195
291
  queueDepth: queue.items.length,
196
292
  });
197
293
  return;
198
294
  }
199
- if (type === "lock.claim" || type === "coord.claim") {
200
- const owner = body.owner || message.from || "worker";
295
+ if (action === "claim") {
296
+ const owner = body.owner;
297
+ if (!owner) throw new Error("claim input.owner is required");
201
298
  const item = nextTask(queue, locks);
202
299
  if (!item) {
203
300
  writeJson(queuePath, queue);
204
301
  writeJson(locksPath, locks);
205
- outbox("lock.empty", "No claimable task", {
302
+ emitTrace("lock.empty", "No claimable task", {
206
303
  owner,
207
304
  queueDepth: queue.items?.length ?? 0,
208
305
  });
@@ -217,15 +314,16 @@ async function runLocker(argv = process.argv.slice(2)) {
217
314
  owner,
218
315
  resources: item.resources,
219
316
  });
220
- outbox("lock.assigned", `Assigned task ${item.id}`, { owner, task: item });
317
+ emitTrace("lock.assigned", `Assigned task ${item.id}`, { owner, task: item });
221
318
  return;
222
319
  }
223
- if (type === "lock.acquire") {
320
+ if (action === "acquire") {
224
321
  const resource = body.resource;
225
- const owner = body.owner || message.from || "worker";
226
- if (!resource) throw new Error("lock.acquire body.resource is required");
322
+ const owner = body.owner;
323
+ if (!resource || !owner)
324
+ throw new Error("acquire input.resource and input.owner are required");
227
325
  if (locks[resource])
228
- outbox(
326
+ emitTrace(
229
327
  "lock.denied",
230
328
  `Lock denied ${resource}`,
231
329
  { resource, owner, current: locks[resource] },
@@ -233,25 +331,26 @@ async function runLocker(argv = process.argv.slice(2)) {
233
331
  );
234
332
  else {
235
333
  locks[resource] = { owner, expiresAt: now() + leaseMs };
236
- outbox("lock.granted", `Lock granted ${resource}`, { resource, owner });
334
+ emitTrace("lock.granted", `Lock granted ${resource}`, { resource, owner });
237
335
  }
238
336
  writeJson(locksPath, locks);
239
337
  return;
240
338
  }
241
- if (type === "lock.renew") {
339
+ if (action === "renew") {
242
340
  const resource = body.resource;
243
- const owner = body.owner || message.from || "worker";
244
- if (!resource) throw new Error("lock.renew body.resource is required");
341
+ const owner = body.owner;
342
+ if (!resource || !owner)
343
+ throw new Error("renew input.resource and input.owner are required");
245
344
  const current = locks[resource];
246
345
  if (!current) {
247
- outbox(
346
+ emitTrace(
248
347
  "lock.denied",
249
348
  `Lock renew denied ${resource}`,
250
349
  { resource, owner, reason: "missing" },
251
350
  "warning",
252
351
  );
253
352
  } else if (current.owner !== owner) {
254
- outbox(
353
+ emitTrace(
255
354
  "lock.denied",
256
355
  `Lock renew denied ${resource}`,
257
356
  { resource, owner, current },
@@ -259,29 +358,24 @@ async function runLocker(argv = process.argv.slice(2)) {
259
358
  );
260
359
  } else {
261
360
  locks[resource] = { ...current, expiresAt: now() + leaseMs };
262
- outbox("lock.renewed", `Lock renewed ${resource}`, { resource, owner });
361
+ emitTrace("lock.renewed", `Lock renewed ${resource}`, { resource, owner });
263
362
  }
264
363
  writeJson(locksPath, locks);
265
364
  return;
266
365
  }
267
- if (type === "lock.release") {
366
+ if (action === "release") {
268
367
  const resource = body.resource;
269
368
  if (resource) delete locks[resource];
270
369
  writeJson(locksPath, locks);
271
- outbox("lock.released", `Lock released ${resource}`, { resource });
370
+ emitTrace("lock.released", `Lock released ${resource}`, { resource });
272
371
  return;
273
372
  }
274
373
  if (
275
- type === "lock.complete" ||
276
- type === "lock.fail" ||
277
- type === "coord.complete" ||
278
- type === "coord.fail"
374
+ action === "complete" || action === "fail"
279
375
  ) {
280
- const eventType = type.startsWith("coord.")
281
- ? type.replace("coord.", "lock.")
282
- : type;
376
+ const eventType = `lock.${action}`;
283
377
  journal(eventType, body);
284
- outbox(
378
+ emitTrace(
285
379
  eventType,
286
380
  `${eventType} ${body.id ?? ""}`.trim(),
287
381
  body,
@@ -291,8 +385,8 @@ async function runLocker(argv = process.argv.slice(2)) {
291
385
  writeJson(queuePath, queue);
292
386
  return;
293
387
  }
294
- journal("lock.unknown", { type, body });
295
- outbox("lock.unknown", `Unknown message ${type}`, { type, body }, "warning");
388
+ journal("lock.unknown", { action, input: body });
389
+ emitTrace("lock.unknown", `Unknown Control ${action}`, { action, input: body }, "warning");
296
390
  }
297
391
 
298
392
  if (mode === "snapshot") {
@@ -300,15 +394,35 @@ async function runLocker(argv = process.argv.slice(2)) {
300
394
  process.exit(0);
301
395
  }
302
396
 
397
+ const startupRun = readJson(join(stateDir, "run.json"), undefined);
398
+ if (!startupRun || typeof startupRun.run_instance_id !== "string") {
399
+ throw new Error("Run generation unavailable for Control service");
400
+ }
401
+ runInstanceId = startupRun.run_instance_id;
402
+
303
403
  function handleLine(line) {
304
- const message = normalizeMessage(line);
305
- if (!message) return;
404
+ const control = normalizeControl(line);
405
+ if (!control) return false;
406
+ const claimed = claimControl(control.id);
407
+ if (!claimed) {
408
+ emitTrace(
409
+ "lock.control_rejected",
410
+ "Rejected stale or unjournaled Control",
411
+ { id: control.id },
412
+ "warning",
413
+ );
414
+ return false;
415
+ }
306
416
  try {
307
- handle(message);
417
+ const stopping = handle(claimed) === true;
418
+ finalizeControl(claimed.id, "handled");
419
+ return stopping;
308
420
  } catch (error) {
309
421
  const text = error instanceof Error ? error.message : String(error);
422
+ finalizeControl(claimed.id, "failed", text);
310
423
  journal("lock.error", { error: text });
311
- outbox("lock.error", text, { error: text }, "error");
424
+ emitTrace("lock.error", text, { error: text }, "error");
425
+ return false;
312
426
  }
313
427
  }
314
428
 
@@ -320,19 +434,39 @@ async function runLocker(argv = process.argv.slice(2)) {
320
434
  }
321
435
  writeControlEndpoint(endpoint);
322
436
  while (true) {
323
- const stream = await import("node:fs").then((fs) =>
324
- fs.createReadStream(endpoint.path, { encoding: "utf8" }),
325
- );
437
+ const fs = await import("node:fs");
438
+ const fd = fs.openSync(endpoint.path, fs.constants.O_RDWR);
439
+ const stream = fs.createReadStream(undefined, {
440
+ autoClose: false,
441
+ encoding: "utf8",
442
+ fd,
443
+ });
326
444
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
327
- for await (const line of rl) handleLine(line);
445
+ for await (const line of rl) {
446
+ if (handleLine(line)) {
447
+ fs.writeSync(fd, "\n");
448
+ stream.destroy();
449
+ return;
450
+ }
451
+ }
452
+ fs.closeSync(fd);
328
453
  }
329
454
  }
330
455
 
331
456
  async function serveNamedPipe(endpoint) {
332
457
  rmSync(endpoint.path, { force: true });
458
+ let resolveStopped;
459
+ const stopped = new Promise((resolve) => {
460
+ resolveStopped = resolve;
461
+ });
333
462
  const server = createServer((socket) => {
334
463
  const rl = readline.createInterface({ input: socket, crlfDelay: Infinity });
335
- rl.on("line", handleLine);
464
+ rl.on("line", (line) => {
465
+ if (!handleLine(line)) return;
466
+ rl.close();
467
+ socket.destroy();
468
+ server.close(() => resolveStopped());
469
+ });
336
470
  });
337
471
  await new Promise((resolveReady, rejectReady) => {
338
472
  server.once("error", rejectReady);
@@ -342,14 +476,14 @@ async function runLocker(argv = process.argv.slice(2)) {
342
476
  resolveReady();
343
477
  });
344
478
  });
345
- await new Promise(() => {});
479
+ await stopped;
346
480
  }
347
481
 
348
482
  const endpoint = getControlEndpoint();
349
483
  writeJson(queuePath, readJson(queuePath, { items: [] }));
350
484
  writeJson(locksPath, cleanExpiredLocks(readJson(locksPath, {})));
351
485
  journal("lock.started", { leaseMs, control: endpoint.type });
352
- outbox("lock.started", "Locker ready", { leaseMs, control: endpoint.type });
486
+ emitTrace("lock.started", "Locker ready", { leaseMs, control: endpoint.type });
353
487
  if (endpoint.type === "named-pipe") await serveNamedPipe(endpoint);
354
488
  else await serveFifo(endpoint);
355
489
  }