@llblab/pi-actors 0.33.0 → 0.34.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 (155) hide show
  1. package/AGENTS.md +22 -11
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +22 -1
  4. package/dist/index.js +10 -10
  5. package/dist/lib/async-runs.d.ts +23 -75
  6. package/dist/lib/async-runs.js +49 -801
  7. package/dist/lib/command-templates.d.ts +3 -3
  8. package/dist/lib/command-templates.js +3 -3
  9. package/dist/lib/config.d.ts +2 -2
  10. package/dist/lib/config.js +5 -5
  11. package/dist/lib/{output.d.ts → execution-output.d.ts} +3 -3
  12. package/dist/lib/{output.js → execution-output.js} +3 -3
  13. package/dist/lib/execution.js +1 -1
  14. package/dist/lib/{actor-inspector-tui.d.ts → inspector.d.ts} +3 -2
  15. package/dist/lib/{actor-inspector-tui.js → inspector.js} +3 -2
  16. package/dist/lib/mailbox-loop.d.ts +3 -3
  17. package/dist/lib/mailbox-loop.js +3 -3
  18. package/dist/lib/{actor-messages.d.ts → messages.d.ts} +3 -3
  19. package/dist/lib/{actor-messages.js → messages.js} +3 -3
  20. package/dist/lib/{actor-recipe-context.d.ts → recipes-context.d.ts} +3 -2
  21. package/dist/lib/{actor-recipe-context.js → recipes-context.js} +2 -1
  22. package/dist/lib/{recipe-discovery.d.ts → recipes-discovery.d.ts} +8 -8
  23. package/dist/lib/{recipe-discovery.js → recipes-discovery.js} +6 -6
  24. package/dist/lib/registry.d.ts +1 -1
  25. package/dist/lib/registry.js +22 -22
  26. package/dist/lib/{actor-rooms.d.ts → rooms.d.ts} +4 -4
  27. package/dist/lib/{actor-rooms.js → rooms.js} +3 -3
  28. package/dist/lib/runs-artifacts.d.ts +21 -0
  29. package/dist/lib/runs-artifacts.js +68 -0
  30. package/dist/lib/runs-control.d.ts +13 -0
  31. package/dist/lib/runs-control.js +59 -0
  32. package/dist/lib/runs-identity.d.ts +5 -0
  33. package/dist/lib/runs-identity.js +11 -0
  34. package/dist/lib/runs-index.d.ts +18 -0
  35. package/dist/lib/runs-index.js +119 -0
  36. package/dist/lib/runs-mailbox.d.ts +25 -0
  37. package/dist/lib/runs-mailbox.js +146 -0
  38. package/dist/lib/runs-messages.d.ts +15 -0
  39. package/dist/lib/runs-messages.js +179 -0
  40. package/dist/lib/runs-outbox.d.ts +41 -0
  41. package/dist/lib/runs-outbox.js +87 -0
  42. package/dist/lib/runs-process.d.ts +7 -0
  43. package/dist/lib/runs-process.js +36 -0
  44. package/dist/lib/runs-retention.d.ts +8 -0
  45. package/dist/lib/runs-retention.js +56 -0
  46. package/dist/lib/runs-start.d.ts +9 -0
  47. package/dist/lib/runs-start.js +67 -0
  48. package/dist/lib/runs-status.d.ts +8 -0
  49. package/dist/lib/runs-status.js +60 -0
  50. package/dist/lib/runtime.d.ts +2 -2
  51. package/dist/lib/runtime.js +5 -5
  52. package/dist/lib/schema.d.ts +9 -0
  53. package/dist/lib/schema.js +40 -0
  54. package/dist/lib/tools-access.d.ts +19 -0
  55. package/dist/lib/tools-access.js +44 -0
  56. package/dist/lib/tools-inspect.d.ts +11 -0
  57. package/dist/lib/tools-inspect.js +534 -0
  58. package/dist/lib/tools-local.d.ts +14 -0
  59. package/dist/lib/tools-local.js +156 -0
  60. package/dist/lib/tools-mailbox.d.ts +8 -0
  61. package/dist/lib/tools-mailbox.js +48 -0
  62. package/dist/lib/tools-message.d.ts +9 -0
  63. package/dist/lib/tools-message.js +346 -0
  64. package/dist/lib/tools-register.d.ts +18 -0
  65. package/dist/lib/tools-register.js +40 -0
  66. package/dist/lib/tools-response.d.ts +21 -0
  67. package/dist/lib/tools-response.js +219 -0
  68. package/dist/lib/tools-spawn.d.ts +12 -0
  69. package/dist/lib/tools-spawn.js +159 -0
  70. package/dist/lib/tools.d.ts +8 -39
  71. package/dist/lib/tools.js +11 -1490
  72. package/dist/recipes/utility-coordinator-lock-snapshot.json +1 -1
  73. package/dist/scripts/actor-worker.mjs +186 -7
  74. package/dist/scripts/async-runner.mjs +171 -8
  75. package/dist/scripts/build-dist.mjs +1 -1
  76. package/dist/scripts/conformance.mjs +3 -3
  77. package/dist/scripts/coordinator.mjs +733 -11
  78. package/dist/scripts/locker.mjs +345 -16
  79. package/dist/scripts/recipe-utils.mjs +478 -14
  80. package/dist/scripts/validate-recipe.mjs +110 -10
  81. package/dist/skills/actors/SKILL.md +1 -1
  82. package/dist/skills/swarm/SKILL.md +1 -1
  83. package/docs/recipe-library.md +1 -1
  84. package/docs/template-recipes.md +1 -1
  85. package/index.ts +11 -18
  86. package/lib/async-runs.ts +124 -1016
  87. package/lib/command-templates.ts +3 -3
  88. package/lib/config.ts +7 -7
  89. package/lib/{output.ts → execution-output.ts} +3 -3
  90. package/lib/execution.ts +5 -1
  91. package/lib/{actor-inspector-tui.ts → inspector.ts} +4 -3
  92. package/lib/mailbox-loop.ts +3 -3
  93. package/lib/{actor-messages.ts → messages.ts} +3 -3
  94. package/lib/{actor-recipe-context.ts → recipes-context.ts} +3 -2
  95. package/lib/{recipe-discovery.ts → recipes-discovery.ts} +19 -19
  96. package/lib/{recipe-usage.ts → recipes-usage.ts} +2 -2
  97. package/lib/registry.ts +37 -24
  98. package/lib/{actor-rooms.ts → rooms.ts} +4 -4
  99. package/lib/runs-artifacts.ts +96 -0
  100. package/lib/runs-control.ts +85 -0
  101. package/lib/runs-identity.ts +14 -0
  102. package/lib/runs-index.ts +156 -0
  103. package/lib/runs-mailbox.ts +208 -0
  104. package/lib/runs-messages.ts +252 -0
  105. package/lib/runs-outbox.ts +144 -0
  106. package/lib/runs-process.ts +51 -0
  107. package/lib/runs-retention.ts +77 -0
  108. package/lib/runs-start.ts +98 -0
  109. package/lib/runs-status.ts +78 -0
  110. package/lib/runtime.ts +6 -6
  111. package/lib/schema.ts +55 -0
  112. package/lib/tools-access.ts +68 -0
  113. package/lib/tools-inspect.ts +735 -0
  114. package/lib/tools-local.ts +234 -0
  115. package/lib/tools-mailbox.ts +56 -0
  116. package/lib/tools-message.ts +483 -0
  117. package/lib/tools-register.ts +64 -0
  118. package/lib/tools-response.ts +267 -0
  119. package/lib/tools-spawn.ts +240 -0
  120. package/lib/tools.ts +20 -2040
  121. package/package.json +1 -1
  122. package/recipes/utility-coordinator-lock-snapshot.json +1 -1
  123. package/scripts/actor-worker.mjs +186 -7
  124. package/scripts/async-runner.mjs +171 -8
  125. package/scripts/build-dist.mjs +1 -1
  126. package/scripts/conformance.mjs +3 -3
  127. package/scripts/coordinator.mjs +733 -11
  128. package/scripts/locker.mjs +345 -16
  129. package/scripts/recipe-utils.mjs +478 -14
  130. package/scripts/validate-recipe.mjs +110 -10
  131. package/skills/actors/SKILL.md +1 -1
  132. package/skills/swarm/SKILL.md +1 -1
  133. package/dist/lib/actor-worker.d.ts +0 -16
  134. package/dist/lib/actor-worker.js +0 -134
  135. package/dist/lib/async-runner.d.ts +0 -5
  136. package/dist/lib/async-runner.js +0 -134
  137. package/dist/lib/coordinator.d.ts +0 -5
  138. package/dist/lib/coordinator.js +0 -660
  139. package/dist/lib/locker.d.ts +0 -5
  140. package/dist/lib/locker.js +0 -310
  141. package/dist/lib/recipe-utils.d.ts +0 -5
  142. package/dist/lib/recipe-utils.js +0 -385
  143. package/dist/lib/validate-recipe.d.ts +0 -6
  144. package/dist/lib/validate-recipe.js +0 -104
  145. package/lib/actor-worker.ts +0 -174
  146. package/lib/async-runner.ts +0 -173
  147. package/lib/coordinator.ts +0 -742
  148. package/lib/locker.ts +0 -340
  149. package/lib/recipe-utils.ts +0 -486
  150. package/lib/validate-recipe.ts +0 -110
  151. /package/dist/lib/{recipe-references.d.ts → recipes-references.d.ts} +0 -0
  152. /package/dist/lib/{recipe-references.js → recipes-references.js} +0 -0
  153. /package/dist/lib/{recipe-usage.d.ts → recipes-usage.d.ts} +0 -0
  154. /package/dist/lib/{recipe-usage.js → recipes-usage.js} +0 -0
  155. /package/lib/{recipe-references.ts → recipes-references.ts} +0 -0
@@ -1,24 +1,27 @@
1
1
  /**
2
- * Command-template async run primitives
3
- * Zones: async runtime, lifecycle, state files
4
- * Owns detached run state, observation, log tailing, listing, and cancellation safety
2
+ * Command-template async run lifecycle facade.
3
+ * Owns launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
5
4
  */
6
- import { spawn, spawnSync } from "node:child_process";
7
- import { createHash, randomUUID } from "node:crypto";
8
- import { closeSync, constants, existsSync, mkdirSync, openSync, readdirSync, readFileSync, cpSync, readlinkSync, renameSync, rmSync, statSync, writeFileSync, writeSync, } from "node:fs";
9
- import { createConnection } from "node:net";
10
- import { platform } from "node:os";
5
+ import { spawn } from "node:child_process";
6
+ import { closeSync, existsSync, mkdirSync, openSync, writeFileSync, } from "node:fs";
11
7
  import { basename, dirname, extname, join, resolve } from "node:path";
12
8
  import { fileURLToPath } from "node:url";
13
- import { substituteCommandTemplateToken } from "./command-templates.js";
14
9
  import { writeJsonAtomic } from "./file-state.js";
15
- import { readJsonFileResilient, readJsonlFileResilient } from "./state-readers.js";
16
10
  import * as Paths from "./paths.js";
17
- import * as RecipeReferences from "./recipe-references.js";
18
- import * as RecipeUsage from "./recipe-usage.js";
19
- import { notifyRuntimeWake } from "./runtime-notifier.js";
20
- const START_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
21
- const RUN_INBOX_LOCK_TIMEOUT_MS = 5000;
11
+ import * as RecipesReferences from "./recipes-references.js";
12
+ import * as RecipesUsage from "./recipes-usage.js";
13
+ import { resolveArtifactPaths, } from "./runs-artifacts.js";
14
+ import { safeRunId } from "./runs-identity.js";
15
+ import { buildTerminalProgress, markTerminalHandled, signalOwnedRunProcess, } from "./runs-control.js";
16
+ import { buildRunOutboxEventPayload, parseRunOutboxEventLine, } from "./runs-outbox.js";
17
+ import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.js";
18
+ import { isAlive, isWithinRunnerIdentityGrace, pidMatchesRun, } from "./runs-process.js";
19
+ import * as RunsStart from "./runs-start.js";
20
+ import * as RunsIndex from "./runs-index.js";
21
+ import { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
22
+ import { deliverRunMessage, } from "./runs-messages.js";
23
+ import { buildRunStatus, tailFile, tailLines, } from "./runs-status.js";
24
+ import { readJsonFileResilient } from "./state-readers.js";
22
25
  const RUNNER_IDENTITY_GRACE_MS = 5000;
23
26
  const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
24
27
  const DEFAULT_RECIPE_ROOT = Paths.getRecipeRoot();
@@ -37,73 +40,8 @@ function asyncRunnerArgv(stateDir) {
37
40
  ? [RUNNER_PATH, stateDir]
38
41
  : ["--experimental-strip-types", RUNNER_PATH, stateDir];
39
42
  }
40
- function safeRunId(value) {
41
- const run = (value || `run-${Date.now()}`).trim();
42
- if (!/^[A-Za-z0-9_.-]+$/.test(run))
43
- throw new Error("Run id may contain only letters, numbers, dot, underscore, and dash.");
44
- return run;
45
- }
46
- function resolveArtifactPaths(artifacts, values) {
47
- if (!artifacts)
48
- return undefined;
49
- const resolved = {};
50
- for (const [key, value] of Object.entries(artifacts)) {
51
- if (!key.trim())
52
- continue;
53
- if (typeof value === "string") {
54
- resolved[key] = substituteCommandTemplateToken(value, values, `recipe artifacts.${key}`);
55
- }
56
- else if (value &&
57
- typeof value === "object" &&
58
- typeof value.path === "string") {
59
- resolved[key] = {
60
- ...value,
61
- path: substituteCommandTemplateToken(value.path, values, `recipe artifacts.${key}.path`),
62
- };
63
- }
64
- }
65
- return Object.keys(resolved).length > 0 ? resolved : undefined;
66
- }
67
- export function resolveArtifactManifest(artifacts) {
68
- if (!artifacts)
69
- return undefined;
70
- const manifest = {};
71
- for (const [name, artifact] of Object.entries(artifacts)) {
72
- const declaration = typeof artifact === "string" ? { path: artifact } : artifact;
73
- if (!declaration?.path)
74
- continue;
75
- try {
76
- const content = readFileSync(declaration.path);
77
- manifest[name] = {
78
- exists: true,
79
- ...(declaration.kind ? { kind: declaration.kind } : {}),
80
- ...(declaration.media_type
81
- ? { media_type: declaration.media_type }
82
- : {}),
83
- path: declaration.path,
84
- ...(declaration.required !== undefined
85
- ? { required: declaration.required }
86
- : {}),
87
- sha256: createHash("sha256").update(content).digest("hex"),
88
- size: content.byteLength,
89
- };
90
- }
91
- catch {
92
- manifest[name] = {
93
- exists: false,
94
- ...(declaration.kind ? { kind: declaration.kind } : {}),
95
- ...(declaration.media_type
96
- ? { media_type: declaration.media_type }
97
- : {}),
98
- path: declaration.path,
99
- ...(declaration.required !== undefined
100
- ? { required: declaration.required }
101
- : {}),
102
- };
103
- }
104
- }
105
- return Object.keys(manifest).length > 0 ? manifest : undefined;
106
- }
43
+ export { safeRunId } from "./runs-identity.js";
44
+ export { resolveArtifactManifest } from "./runs-artifacts.js";
107
45
  function resolveRunTemplate(params) {
108
46
  if (!params.template)
109
47
  throw new Error("spawn requires file or template.");
@@ -136,18 +74,11 @@ function resolveStateDir(params, run) {
136
74
  return resolve(params.state_dir || join(DEFAULT_STATE_ROOT, run));
137
75
  }
138
76
  function assertNoActiveRunState(stateDir) {
139
- const meta = readJson(join(stateDir, "run.json"));
140
- if (!meta)
141
- return;
142
- const pid = Number(meta.pid || 0);
143
- const cwd = String(meta.cwd ?? "");
144
- if (!pid || !isAlive(pid) || !pidMatchesRun(pid, cwd, stateDir))
145
- return;
146
- throw new Error(`Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`);
77
+ RunsStart.assertNoActiveRunState(stateDir, readJson, RUNNER_PATH);
147
78
  }
148
79
  function resolveRecipeFile(file) {
149
- return (RecipeReferences.getRecipePath(file, DEFAULT_RECIPE_ROOT) ??
150
- RecipeReferences.resolveRecipePath(file, DEFAULT_RECIPE_ROOT));
80
+ return (RecipesReferences.getRecipePath(file, DEFAULT_RECIPE_ROOT) ??
81
+ RecipesReferences.resolveRecipePath(file, DEFAULT_RECIPE_ROOT));
151
82
  }
152
83
  function isMutableUsageRecipeFile(file) {
153
84
  const userRoot = resolve(DEFAULT_RECIPE_ROOT);
@@ -156,9 +87,9 @@ function isMutableUsageRecipeFile(file) {
156
87
  }
157
88
  function readRecipeFile(file) {
158
89
  const path = resolveRecipeFile(file);
159
- const raw = RecipeReferences.readRawRecipeConfig(path);
90
+ const raw = RecipesReferences.readRawRecipeConfig(path);
160
91
  const includeActorRecipeContext = raw?.actor_context !== false && raw?.actor_context !== "off";
161
- const config = RecipeReferences.readResolvedRecipeConfig(path, [], {
92
+ const config = RecipesReferences.readResolvedRecipeConfig(path, [], {
162
93
  includeActorRecipeContext,
163
94
  });
164
95
  if (!config) {
@@ -196,104 +127,11 @@ function resolveStartParams(params) {
196
127
  function readJson(path) {
197
128
  return readJsonFileResilient(path, undefined).value;
198
129
  }
199
- function isAlive(pid) {
200
- try {
201
- process.kill(pid, 0);
202
- return true;
203
- }
204
- catch {
205
- return false;
206
- }
207
- }
208
- function pidMatchesRun(pid, cwd, stateDir) {
209
- if (platform() !== "linux" || !existsSync(`/proc/${pid}`))
210
- return isAlive(pid);
211
- try {
212
- const procCwd = readlinkSync(`/proc/${pid}/cwd`);
213
- const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
214
- return (procCwd === resolve(cwd) &&
215
- cmdline.includes(RUNNER_PATH) &&
216
- cmdline.includes(stateDir));
217
- }
218
- catch {
219
- return false;
220
- }
221
- }
222
- function isWithinRunnerIdentityGrace(meta) {
223
- const createdAt = typeof meta.createdAt === "string" ? Date.parse(meta.createdAt) : NaN;
224
- return (Number.isFinite(createdAt) &&
225
- Date.now() - createdAt >= 0 &&
226
- Date.now() - createdAt <= RUNNER_IDENTITY_GRACE_MS);
227
- }
228
- function tailFile(path, lines) {
229
- if (!existsSync(path))
230
- return "";
231
- const content = readFileSync(path, "utf8").trimEnd();
232
- if (!content)
233
- return "";
234
- return content.split("\n").slice(-lines).join("\n");
235
- }
236
- function tailLines(path, lines) {
237
- const content = tailFile(path, lines);
238
- return content ? content.split("\n") : [];
239
- }
240
- function getInterruptedRunStatus(stateDir) {
241
- const events = readJsonlFileResilient(join(stateDir, "events.jsonl")).records.slice(-200);
242
- for (const event of events.reverse()) {
243
- if (event.event === "run.kill")
244
- return "killed";
245
- if (event.event === "run.cancel")
246
- return "cancelled";
247
- }
248
- return undefined;
249
- }
250
130
  function acquireStateStartLock(stateDir) {
251
- const lockDir = join(stateDir, ".start.lock");
252
- try {
253
- mkdirSync(lockDir);
254
- writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`, "utf8");
255
- }
256
- catch (error) {
257
- try {
258
- const stat = statSync(lockDir);
259
- if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS) {
260
- rmSync(lockDir, { recursive: true, force: true });
261
- mkdirSync(lockDir);
262
- writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), recovered: true })}\n`, "utf8");
263
- return () => rmSync(lockDir, { recursive: true, force: true });
264
- }
265
- }
266
- catch {
267
- // Keep the original lock acquisition error below.
268
- }
269
- throw new Error(`Run state is already being started: ${stateDir}. Retry after the current start finishes.`, { cause: error });
270
- }
271
- return () => rmSync(lockDir, { recursive: true, force: true });
131
+ return RunsStart.acquireStateStartLock(stateDir);
272
132
  }
273
133
  function prepareStateDirForStart(stateDir) {
274
- const existing = readJson(join(stateDir, "run.json"));
275
- const existingPid = Number(existing?.pid || 0);
276
- const existingCwd = typeof existing?.cwd === "string" ? existing.cwd : undefined;
277
- const existingResult = readJson(join(stateDir, "result.json"));
278
- if (!existingResult &&
279
- existingPid &&
280
- existingCwd &&
281
- isAlive(existingPid) &&
282
- pidMatchesRun(existingPid, existingCwd, stateDir)) {
283
- throw new Error(`Run is already running: ${String(existing?.run ?? stateDir)}`);
284
- }
285
- for (const file of [
286
- "events.jsonl",
287
- "inbox.jsonl",
288
- "outbox.jsonl",
289
- "progress.json",
290
- "result.json",
291
- "stderr.log",
292
- "stdout.log",
293
- "terminal-handled.json",
294
- ]) {
295
- rmSync(join(stateDir, file), { force: true });
296
- }
134
+ RunsStart.prepareStateDirForStart(stateDir, readJson, RUNNER_PATH);
297
135
  }
298
136
  export function startRun(params, cwd) {
299
137
  const startParams = resolveStartParams(params);
@@ -315,10 +153,10 @@ export function startRun(params, cwd) {
315
153
  const includeActorRecipeContext = startParams.actor_context !== false &&
316
154
  startParams.actor_context !== "off";
317
155
  const recipeContextRecords = recipeFile && includeActorRecipeContext
318
- ? RecipeReferences.buildRecipeContextRecords(recipeFile)
156
+ ? RecipesReferences.buildRecipeContextRecords(recipeFile)
319
157
  : undefined;
320
158
  if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
321
- RecipeUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn");
159
+ RecipesUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn");
322
160
  }
323
161
  const outFd = openSync(stdout, "a");
324
162
  const errFd = openSync(stderr, "a");
@@ -387,63 +225,7 @@ export function startRun(params, cwd) {
387
225
  releaseStartLock();
388
226
  }
389
227
  }
390
- function normalizeRunOutboxDelivery(value) {
391
- return value === "notify" || value === "followup" ? value : "log";
392
- }
393
- function normalizeRunOutboxLevel(value) {
394
- return value === "warning" || value === "error" ? value : "info";
395
- }
396
- function normalizeRunOutboxEvent(raw, run, stateDir, index) {
397
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
398
- return undefined;
399
- const record = raw;
400
- const event = typeof record.event === "string" && record.event.trim()
401
- ? record.event.trim()
402
- : "run.event";
403
- const summary = typeof record.summary === "string" && record.summary.trim()
404
- ? record.summary.trim()
405
- : event;
406
- const ts = typeof record.ts === "string" && record.ts.trim()
407
- ? record.ts.trim()
408
- : new Date(0).toISOString();
409
- const id = typeof record.id === "string" && record.id.trim()
410
- ? record.id.trim()
411
- : `${run}:${index}`;
412
- return {
413
- ...(record.body !== undefined ? { body: record.body } : {}),
414
- ...(typeof record.correlation_id === "string"
415
- ? { correlation_id: record.correlation_id }
416
- : {}),
417
- ...(record.data !== undefined ? { data: record.data } : {}),
418
- delivery: normalizeRunOutboxDelivery(record.delivery),
419
- ...(record.metadata &&
420
- typeof record.metadata === "object" &&
421
- !Array.isArray(record.metadata)
422
- ? { metadata: record.metadata }
423
- : {}),
424
- event,
425
- ...(typeof record.from === "string" ? { from: record.from } : {}),
426
- id,
427
- level: normalizeRunOutboxLevel(record.level),
428
- ...(typeof record.reply_to === "string"
429
- ? { reply_to: record.reply_to }
430
- : {}),
431
- run,
432
- state_dir: stateDir,
433
- summary,
434
- ...(typeof record.to === "string" ? { to: record.to } : {}),
435
- ts,
436
- ...(typeof record.type === "string" ? { type: record.type } : {}),
437
- };
438
- }
439
- export function parseRunOutboxEventLine(line, run, stateDir, index) {
440
- try {
441
- return normalizeRunOutboxEvent(JSON.parse(line), run, stateDir, index);
442
- }
443
- catch {
444
- return undefined;
445
- }
446
- }
228
+ export { parseRunOutboxEventLine } from "./runs-outbox.js";
447
229
  export function getRunStatus(runOrDir) {
448
230
  const stateDir = resolve(runOrDir.includes("/")
449
231
  ? runOrDir
@@ -451,153 +233,19 @@ export function getRunStatus(runOrDir) {
451
233
  const meta = readJson(join(stateDir, "run.json"));
452
234
  if (!meta)
453
235
  throw new Error(`Run not found: ${runOrDir}`);
454
- const result = readJson(join(stateDir, "result.json"));
455
- const pid = Number(meta.pid || 0);
456
- const aliveOwnedRunner = Boolean(pid &&
457
- isAlive(pid) &&
458
- (!Array.isArray(meta.argv) ||
459
- pidMatchesRun(pid, String(meta.cwd ?? ""), stateDir) ||
460
- isWithinRunnerIdentityGrace(meta)));
461
- const status = result
462
- ? Number(result.code ?? 0) === 0
463
- ? "done"
464
- : "failed"
465
- : aliveOwnedRunner
466
- ? "running"
467
- : (getInterruptedRunStatus(stateDir) ?? "exited");
468
- const terminalHandled = readJson(join(stateDir, "terminal-handled.json"));
469
- return {
470
- ...meta,
471
- eventsFile: join(stateDir, "events.jsonl"),
472
- inboxFile: join(stateDir, "inbox.jsonl"),
473
- outboxFile: join(stateDir, "outbox.jsonl"),
474
- progress: readJson(join(stateDir, "progress.json")) || null,
475
- result: result || null,
476
- ...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
477
- state_dir: String(meta.state_dir ?? stateDir),
478
- stderrLog: join(stateDir, "stderr.log"),
479
- stdoutLog: join(stateDir, "stdout.log"),
480
- status,
481
- };
482
- }
483
- function matchesStatusFilter(status, filter) {
484
- if (!filter || filter === "all")
485
- return true;
486
- if (filter === "active")
487
- return status === "running";
488
- if (filter === "terminal")
489
- return status !== "running";
490
- return status === filter;
236
+ return buildRunStatus(stateDir, runOrDir, meta, readJson, RUNNER_PATH, RUNNER_IDENTITY_GRACE_MS);
491
237
  }
492
238
  export function listRunStateDirs(stateRoot = DEFAULT_STATE_ROOT, depth = 0, seen = new Set()) {
493
- if (!existsSync(stateRoot) || seen.has(resolve(stateRoot)))
494
- return [];
495
- seen.add(resolve(stateRoot));
496
- let entries;
497
- try {
498
- entries = readdirSync(stateRoot, { withFileTypes: true });
499
- }
500
- catch {
501
- return [];
502
- }
503
- const result = [];
504
- for (const entry of entries) {
505
- if (!entry.isDirectory())
506
- continue;
507
- const child = join(stateRoot, entry.name);
508
- if (existsSync(join(child, "run.json")))
509
- result.push(child);
510
- if (depth + 1 < 8)
511
- result.push(...listRunStateDirs(child, depth + 1, seen));
512
- }
513
- return result;
514
- }
515
- function runIndexPath(stateRoot) {
516
- return join(stateRoot, "index.json");
517
- }
518
- function indexEntryFromStatus(status) {
519
- const progress = status.progress && typeof status.progress === "object"
520
- ? status.progress
521
- : {};
522
- return {
523
- ...(typeof status.ownerId === "string" ? { ownerId: status.ownerId } : {}),
524
- ...(typeof status.recipe === "string" ? { recipe: status.recipe } : {}),
525
- run: String(status.run),
526
- state_dir: String(status.state_dir),
527
- status: status.status,
528
- ...(typeof status.tool === "string" ? { tool: status.tool } : {}),
529
- updated_at: typeof progress.updatedAt === "string"
530
- ? progress.updatedAt
531
- : typeof status.createdAt === "string"
532
- ? status.createdAt
533
- : new Date(0).toISOString(),
534
- };
239
+ return RunsIndex.listRunStateDirs(stateRoot, depth, seen);
535
240
  }
536
241
  export function rebuildRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
537
- mkdirSync(stateRoot, { recursive: true });
538
- const entries = listRunStateDirs(stateRoot)
539
- .flatMap((stateDir) => {
540
- try {
541
- return [indexEntryFromStatus(getRunStatus(stateDir))];
542
- }
543
- catch {
544
- return [];
545
- }
546
- })
547
- .sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""));
548
- writeJsonAtomic(runIndexPath(stateRoot), {
549
- entries,
550
- rebuilt_at: new Date().toISOString(),
551
- });
552
- return entries;
242
+ return RunsIndex.rebuildRunStateIndex(stateRoot, getRunStatus);
553
243
  }
554
244
  export function readRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
555
- const index = readJson(runIndexPath(stateRoot));
556
- if (!index || typeof index !== "object")
557
- return undefined;
558
- const entries = index.entries;
559
- if (!Array.isArray(entries))
560
- return undefined;
561
- const valid = entries.filter((entry) => {
562
- const record = entry && typeof entry === "object"
563
- ? entry
564
- : {};
565
- return (typeof record.run === "string" &&
566
- typeof record.state_dir === "string" &&
567
- typeof record.status === "string");
568
- });
569
- if (valid.some((entry) => !existsSync(join(entry.state_dir, "run.json"))))
570
- return undefined;
571
- const indexedDirs = new Set(valid.map((entry) => resolve(entry.state_dir)));
572
- const stateDirs = listRunStateDirs(stateRoot).map((stateDir) => resolve(stateDir));
573
- if (stateDirs.some((stateDir) => !indexedDirs.has(stateDir)))
574
- return undefined;
575
- return valid;
245
+ return RunsIndex.readRunStateIndex(stateRoot, readJson);
576
246
  }
577
247
  export function listRuns(stateRoot = DEFAULT_STATE_ROOT, statusFilter) {
578
- if (!existsSync(stateRoot))
579
- return [];
580
- const indexed = readRunStateIndex(stateRoot);
581
- if (indexed) {
582
- return indexed
583
- .filter((entry) => matchesStatusFilter(entry.status, statusFilter))
584
- .map((entry) => ({
585
- run: entry.run,
586
- state_dir: entry.state_dir,
587
- status: entry.status,
588
- ...(entry.tool ? { tool: entry.tool } : {}),
589
- ...(entry.recipe ? { recipe: entry.recipe } : {}),
590
- }));
591
- }
592
- return rebuildRunStateIndex(stateRoot)
593
- .filter((entry) => matchesStatusFilter(entry.status, statusFilter))
594
- .map((entry) => ({
595
- run: entry.run,
596
- state_dir: entry.state_dir,
597
- status: entry.status,
598
- ...(entry.tool ? { tool: entry.tool } : {}),
599
- ...(entry.recipe ? { recipe: entry.recipe } : {}),
600
- }));
248
+ return RunsIndex.listRuns(stateRoot, getRunStatus, readJson, statusFilter);
601
249
  }
602
250
  export function tailRun(runOrDir, lines = 40) {
603
251
  const status = getRunStatus(runOrDir);
@@ -616,55 +264,6 @@ export function readRunEvents(runOrDir, lines = 40) {
616
264
  .map((line, index) => parseRunOutboxEventLine(line, run, stateDir, index))
617
265
  .filter((event) => Boolean(event));
618
266
  }
619
- function runInboxFile(stateDir) {
620
- return join(stateDir, "inbox.jsonl");
621
- }
622
- function sleepSync(ms) {
623
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
624
- }
625
- function acquireRunInboxLock(stateDir) {
626
- const lockDir = join(stateDir, ".inbox.lock");
627
- const started = Date.now();
628
- while (true) {
629
- try {
630
- mkdirSync(lockDir, { recursive: false });
631
- writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() })}\n`, "utf8");
632
- return () => rmSync(lockDir, { recursive: true, force: true });
633
- }
634
- catch (error) {
635
- try {
636
- const stat = statSync(lockDir);
637
- if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS) {
638
- rmSync(lockDir, { recursive: true, force: true });
639
- continue;
640
- }
641
- }
642
- catch {
643
- continue;
644
- }
645
- if (Date.now() - started > RUN_INBOX_LOCK_TIMEOUT_MS) {
646
- throw new Error("Run inbox lock timed out.", { cause: error });
647
- }
648
- sleepSync(10);
649
- }
650
- }
651
- }
652
- function parseRunInboxLine(line) {
653
- try {
654
- return JSON.parse(line);
655
- }
656
- catch {
657
- return undefined;
658
- }
659
- }
660
- function readRunInboxMessagesFromStateDir(stateDir) {
661
- return readJsonlFileResilient(runInboxFile(stateDir)).records;
662
- }
663
- function writeRunInboxMessages(stateDir, messages) {
664
- writeFileSync(runInboxFile(stateDir), messages.length
665
- ? `${messages.map((message) => JSON.stringify(message)).join("\n")}\n`
666
- : "", "utf8");
667
- }
668
267
  export function readRunInboxMessages(runOrDir, lines = 40) {
669
268
  const status = getRunStatus(runOrDir);
670
269
  const stateDir = String(status.state_dir);
@@ -675,109 +274,22 @@ export function readRunInboxMessages(runOrDir, lines = 40) {
675
274
  export function updateRunInboxMessageStatus(runOrDir, id, nextStatus, metadata = {}) {
676
275
  const status = getRunStatus(runOrDir);
677
276
  const stateDir = String(status.state_dir);
678
- const releaseLock = acquireRunInboxLock(stateDir);
679
- try {
680
- const messages = readRunInboxMessagesFromStateDir(stateDir);
681
- const timestampKey = `${nextStatus}_at`;
682
- let changed = false;
683
- const updated = messages.map((message) => {
684
- if (message.id !== id)
685
- return message;
686
- changed = true;
687
- return {
688
- ...message,
689
- ...metadata,
690
- [timestampKey]: new Date().toISOString(),
691
- status: nextStatus,
692
- };
693
- });
694
- if (changed)
695
- writeRunInboxMessages(stateDir, updated);
696
- return changed;
697
- }
698
- finally {
699
- releaseLock();
700
- }
277
+ return updateRunInboxMessageStatusInStateDir(stateDir, id, nextStatus, metadata);
701
278
  }
702
279
  export function claimRunInboxMessage(runOrDir, owner = "runtime", statuses = ["queued"]) {
703
280
  const status = getRunStatus(runOrDir);
704
281
  const stateDir = String(status.state_dir);
705
- const releaseLock = acquireRunInboxLock(stateDir);
706
- try {
707
- const messages = readRunInboxMessagesFromStateDir(stateDir);
708
- const index = messages.findIndex((message) => statuses.includes(String(message.status ?? "queued")));
709
- if (index < 0)
710
- return undefined;
711
- const claimed = {
712
- ...messages[index],
713
- claimed_at: new Date().toISOString(),
714
- claimed_by: owner,
715
- id: typeof messages[index].id === "string"
716
- ? messages[index].id
717
- : randomUUID(),
718
- status: "claimed",
719
- };
720
- messages[index] = claimed;
721
- writeRunInboxMessages(stateDir, messages);
722
- return claimed;
723
- }
724
- finally {
725
- releaseLock();
726
- }
282
+ return claimRunInboxMessageInStateDir(stateDir, owner, statuses);
727
283
  }
728
284
  export async function processRunInboxMessages(runOrDir, handler, options = {}) {
729
- const result = { claimed: 0, failed: 0, handled: 0 };
730
- const limit = Math.max(1, Number(options.limit ?? 1));
731
- const owner = options.owner ?? "runtime";
732
- for (let index = 0; index < limit; index += 1) {
733
- const message = claimRunInboxMessage(runOrDir, owner, options.statuses);
734
- if (!message?.id)
735
- break;
736
- result.claimed += 1;
737
- try {
738
- await handler(message);
739
- if (updateRunInboxMessageStatus(runOrDir, message.id, "handled")) {
740
- result.handled += 1;
741
- }
742
- }
743
- catch (error) {
744
- if (updateRunInboxMessageStatus(runOrDir, message.id, "failed", {
745
- error: error instanceof Error ? error.message : String(error),
746
- })) {
747
- result.failed += 1;
748
- }
749
- }
750
- }
751
- return result;
285
+ const status = getRunStatus(runOrDir);
286
+ return processRunInboxMessagesInStateDir(String(status.state_dir), handler, options);
752
287
  }
753
288
  export function appendRunOutboxEvent(runOrDir, event) {
754
289
  const status = getRunStatus(runOrDir);
755
290
  const stateDir = String(status.state_dir);
756
291
  const run = String(status.run ?? runOrDir);
757
- const type = event.type || event.event || "run.message";
758
- const to = event.to || "coordinator";
759
- const metadata = event.metadata ?? {};
760
- const requiresResponse = metadata.requires_response === true;
761
- const payload = {
762
- ...(event.body !== undefined ? { body: event.body } : {}),
763
- ...(event.correlation_id ? { correlation_id: event.correlation_id } : {}),
764
- ...(event.data !== undefined ? { data: event.data } : {}),
765
- delivery: normalizeRunOutboxDelivery(event.delivery ??
766
- (requiresResponse
767
- ? "followup"
768
- : to === "coordinator"
769
- ? "notify"
770
- : "log")),
771
- event: type,
772
- from: event.from || `run:${run}`,
773
- level: normalizeRunOutboxLevel(event.level),
774
- ...(event.metadata ? { metadata: event.metadata } : {}),
775
- ...(event.reply_to ? { reply_to: event.reply_to } : {}),
776
- summary: event.summary || type,
777
- to,
778
- ts: new Date().toISOString(),
779
- type,
780
- };
292
+ const payload = buildRunOutboxEventPayload(run, event);
781
293
  const line = `${JSON.stringify(payload)}\n`;
782
294
  writeFileSync(join(stateDir, "outbox.jsonl"), line, { flag: "a" });
783
295
  return {
@@ -788,115 +300,6 @@ export function appendRunOutboxEvent(runOrDir, event) {
788
300
  state_dir: stateDir,
789
301
  };
790
302
  }
791
- function getRunControlEndpoint(status, stateDir) {
792
- const control = status.control;
793
- if (control && typeof control === "object" && !Array.isArray(control)) {
794
- const record = control;
795
- if ((record.type === "fifo" ||
796
- record.type === "mailbox" ||
797
- record.type === "named-pipe") &&
798
- typeof record.path === "string" &&
799
- record.path.trim()) {
800
- return { path: record.path, type: record.type };
801
- }
802
- }
803
- return { path: join(stateDir, "control.fifo"), type: "fifo" };
804
- }
805
- function appendRunInboxMessage(stateDir, message) {
806
- const id = randomUUID();
807
- const ts = new Date().toISOString();
808
- let record;
809
- try {
810
- const parsed = JSON.parse(message);
811
- record =
812
- parsed && typeof parsed === "object" && !Array.isArray(parsed)
813
- ? parsed
814
- : { body: parsed, type: "run.message" };
815
- }
816
- catch {
817
- record = { body: message, type: "run.message" };
818
- }
819
- const releaseLock = acquireRunInboxLock(stateDir);
820
- try {
821
- writeFileSync(runInboxFile(stateDir), `${JSON.stringify({ ...record, id, queued_at: ts, received_at: ts, status: "queued" })}\n`, { flag: "a" });
822
- }
823
- finally {
824
- releaseLock();
825
- }
826
- return id;
827
- }
828
- function writeRunMessageReceipt(stateDir, message, bytes, inboxId) {
829
- const trimmedMessage = message.trim().toLowerCase();
830
- const terminalMessage = ["stop", "cancel", "quit", "exit"].includes(trimmedMessage);
831
- const ts = new Date().toISOString();
832
- writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ bytes, event: "run.message", inbox_id: inboxId, terminal: terminalMessage || undefined, ts })}\n`, { flag: "a" });
833
- updateRunInboxMessageStatus(stateDir, inboxId, "sent", { bytes });
834
- if (terminalMessage) {
835
- markTerminalHandled(stateDir, {
836
- event: "run.message",
837
- message: trimmedMessage,
838
- });
839
- }
840
- }
841
- function sendRunMessageToFifo(endpoint, payload) {
842
- if (!existsSync(endpoint.path))
843
- throw new Error(`Run control FIFO not found: ${endpoint.path}`);
844
- const stat = statSync(endpoint.path);
845
- if ((stat.mode & constants.S_IFMT) !== constants.S_IFIFO) {
846
- throw new Error(`Run control endpoint is not a FIFO: ${endpoint.path}`);
847
- }
848
- let fd;
849
- try {
850
- fd = openSync(endpoint.path, constants.O_WRONLY | constants.O_NONBLOCK);
851
- return writeSync(fd, payload);
852
- }
853
- finally {
854
- if (fd !== undefined)
855
- closeSync(fd);
856
- }
857
- }
858
- function notifyRunMessageWake(stateDir, run, bytes, endpoint, inboxId) {
859
- try {
860
- const event = notifyRuntimeWake(stateDir, {
861
- actor: `run:${run}`,
862
- metadata: { bytes, control_type: endpoint.type, inbox_id: inboxId },
863
- reason: "run.message",
864
- });
865
- return { wake: "wake.jsonl", wake_id: event.id };
866
- }
867
- catch {
868
- return undefined;
869
- }
870
- }
871
- function sendRunMessageToNamedPipe(endpoint, payload, send) {
872
- if (send)
873
- return send(endpoint.path, payload);
874
- return new Promise((resolve, reject) => {
875
- const socket = createConnection(endpoint.path);
876
- let settled = false;
877
- const timeout = setTimeout(() => {
878
- if (settled)
879
- return;
880
- settled = true;
881
- socket.destroy();
882
- reject(new Error("named pipe connection timed out"));
883
- }, 5000);
884
- const finish = (error) => {
885
- if (settled)
886
- return;
887
- settled = true;
888
- clearTimeout(timeout);
889
- if (error)
890
- reject(error);
891
- else
892
- resolve(Buffer.byteLength(payload));
893
- };
894
- socket.on("error", finish);
895
- socket.on("connect", () => {
896
- socket.end(payload, () => finish());
897
- });
898
- });
899
- }
900
303
  export async function sendRunMessage(runOrDir, message, options = {}) {
901
304
  const status = getRunStatus(runOrDir);
902
305
  const stateDir = String(status.state_dir);
@@ -906,130 +309,18 @@ export async function sendRunMessage(runOrDir, message, options = {}) {
906
309
  const pid = Number(status.pid || 0);
907
310
  if (!pid || !isAlive(pid))
908
311
  throw new Error(`Run pid is not alive: ${run}`);
909
- if (!pidMatchesRun(pid, String(status.cwd), stateDir) &&
910
- !isWithinRunnerIdentityGrace(status))
312
+ if (!pidMatchesRun(pid, String(status.cwd), stateDir, RUNNER_PATH) &&
313
+ !isWithinRunnerIdentityGrace(status, RUNNER_IDENTITY_GRACE_MS))
911
314
  throw new Error(`Run pid owner mismatch: ${run}`);
912
- const endpoint = getRunControlEndpoint(status, stateDir);
913
- const payload = message.endsWith("\n") ? message : `${message}\n`;
914
- const payloadBytes = Buffer.byteLength(payload);
915
- const inboxId = appendRunInboxMessage(stateDir, message);
916
- const wake = notifyRunMessageWake(stateDir, run, payloadBytes, endpoint, inboxId);
917
- const runtimePlatform = options.platform ?? process.platform;
918
- if (endpoint.type === "mailbox") {
919
- return {
920
- ...(wake ?? {}),
921
- bytes: payloadBytes,
922
- control: "inbox.jsonl",
923
- control_path: endpoint.path,
924
- control_type: endpoint.type,
925
- inbox_id: inboxId,
926
- queued: true,
927
- run,
928
- sent: true,
929
- state_dir: stateDir,
930
- };
931
- }
932
- try {
933
- if (endpoint.type === "fifo") {
934
- if (runtimePlatform === "win32") {
935
- throw new Error("run actor messages on native Windows require a named-pipe control endpoint; this recipe still exposes Unix FIFO control.");
936
- }
937
- const bytes = sendRunMessageToFifo(endpoint, payload);
938
- writeRunMessageReceipt(stateDir, message, bytes, inboxId);
939
- return {
940
- ...(wake ?? {}),
941
- bytes,
942
- control: "control.fifo",
943
- control_path: endpoint.path,
944
- control_type: endpoint.type,
945
- inbox_id: inboxId,
946
- run,
947
- sent: true,
948
- state_dir: stateDir,
949
- };
950
- }
951
- const bytes = await sendRunMessageToNamedPipe(endpoint, payload, options.namedPipeSend);
952
- writeRunMessageReceipt(stateDir, message, bytes, inboxId);
953
- return {
954
- ...(wake ?? {}),
955
- bytes,
956
- control: endpoint.path,
957
- control_path: endpoint.path,
958
- control_type: endpoint.type,
959
- inbox_id: inboxId,
960
- run,
961
- sent: true,
962
- state_dir: stateDir,
963
- };
964
- }
965
- catch (error) {
966
- const deliveryError = error instanceof Error ? error.message : String(error);
967
- throw Object.assign(new Error(`Run control endpoint is not ready: ${endpoint.path}: ${deliveryError}`), {
968
- control_path: endpoint.path,
969
- control_type: endpoint.type,
970
- delivery_error: deliveryError,
971
- inbox_id: inboxId,
972
- queued: true,
973
- run,
974
- sent: false,
975
- state_dir: stateDir,
976
- });
977
- }
978
- }
979
- export function getRunProcessSignalPlan(pid, signal, runtimePlatform = process.platform) {
980
- if (runtimePlatform === "win32") {
981
- return {
982
- args: [
983
- "/PID",
984
- String(pid),
985
- "/T",
986
- ...(signal === "SIGKILL" ? ["/F"] : []),
987
- ],
988
- command: "taskkill",
989
- signalTarget: "processTree",
990
- };
991
- }
992
- return { signalTarget: "processGroup" };
993
- }
994
- function signalOwnedRunProcess(pid, signal) {
995
- const plan = getRunProcessSignalPlan(pid, signal);
996
- if (plan.command && plan.args) {
997
- const result = spawnSync(plan.command, plan.args, { encoding: "utf8" });
998
- if (result.status !== 0) {
999
- throw new Error(result.stderr?.trim() ||
1000
- result.stdout?.trim() ||
1001
- `${plan.command} failed`);
1002
- }
1003
- return plan;
1004
- }
1005
- try {
1006
- process.kill(-pid, signal);
1007
- return { signalTarget: "processGroup" };
1008
- }
1009
- catch {
1010
- process.kill(pid, signal);
1011
- return { signalTarget: "process" };
1012
- }
1013
- }
1014
- function markTerminalHandled(stateDir, details) {
1015
- writeJsonAtomic(join(stateDir, "terminal-handled.json"), {
1016
- ...details,
1017
- ts: new Date().toISOString(),
1018
- });
315
+ return deliverRunMessage(status, run, stateDir, message, options);
1019
316
  }
317
+ export { getRunProcessSignalPlan } from "./runs-control.js";
1020
318
  function markTerminalProgress(stateDir, phase) {
1021
319
  const existing = readJson(join(stateDir, "progress.json"));
1022
320
  const progress = existing && typeof existing === "object" && !Array.isArray(existing)
1023
321
  ? existing
1024
- : {};
1025
- const { activeSubagents: _activeSubagents, ...rest } = progress;
1026
- writeJsonAtomic(join(stateDir, "progress.json"), {
1027
- ...rest,
1028
- completed: typeof progress.completed === "number" ? progress.completed : 0,
1029
- failures: Array.isArray(progress.failures) ? progress.failures : [],
1030
- phase,
1031
- updatedAt: new Date().toISOString(),
1032
- });
322
+ : undefined;
323
+ writeJsonAtomic(join(stateDir, "progress.json"), buildTerminalProgress(progress, phase));
1033
324
  }
1034
325
  function stopRun(runOrDir, signal, event) {
1035
326
  const status = getRunStatus(runOrDir);
@@ -1039,7 +330,7 @@ function stopRun(runOrDir, signal, event) {
1039
330
  return { stopped: false, reason: "not running", status };
1040
331
  if (!pid || !isAlive(pid))
1041
332
  return { stopped: false, reason: "pid not alive", status };
1042
- if (!pidMatchesRun(pid, String(status.cwd), stateDir)) {
333
+ if (!pidMatchesRun(pid, String(status.cwd), stateDir, RUNNER_PATH)) {
1043
334
  return { stopped: false, reason: "pid owner mismatch", status };
1044
335
  }
1045
336
  const signalResult = signalOwnedRunProcess(pid, signal);
@@ -1064,54 +355,11 @@ function assertTerminalRun(runOrDir) {
1064
355
  }
1065
356
  return status;
1066
357
  }
1067
- function archivePathFor(run, stateDir) {
1068
- const archiveRoot = join(dirname(stateDir), "archived");
1069
- mkdirSync(archiveRoot, { recursive: true });
1070
- return join(archiveRoot, `${safeRunId(run)}-${new Date().toISOString().replace(/[:.]/g, "-")}`);
1071
- }
1072
358
  export function archiveRun(runOrDir) {
1073
- const status = assertTerminalRun(runOrDir);
1074
- const stateDir = String(status.state_dir);
1075
- const run = String(status.run ?? basename(stateDir));
1076
- const archiveDir = archivePathFor(run, stateDir);
1077
- renameSync(stateDir, archiveDir);
1078
- mkdirSync(stateDir, { recursive: true });
1079
- const tombstone = {
1080
- archived: true,
1081
- archive_dir: archiveDir,
1082
- original_state_dir: stateDir,
1083
- run,
1084
- status: status.status,
1085
- ts: new Date().toISOString(),
1086
- };
1087
- writeJsonAtomic(join(stateDir, "archive-tombstone.json"), tombstone);
1088
- return tombstone;
359
+ return archiveTerminalRun(assertTerminalRun(runOrDir));
1089
360
  }
1090
361
  export function pruneRun(runOrDir, options = {}) {
1091
- const status = assertTerminalRun(runOrDir);
1092
- const stateDir = String(status.state_dir);
1093
- const run = String(status.run ?? basename(stateDir));
1094
- const manifest = resolveArtifactManifest(status.artifacts);
1095
- const preserved = {};
1096
- if (options.preserveArtifacts && manifest) {
1097
- const preserveRoot = join(dirname(stateDir), "preserved-artifacts", safeRunId(run));
1098
- mkdirSync(preserveRoot, { recursive: true });
1099
- for (const [name, artifact] of Object.entries(manifest)) {
1100
- if (!artifact.exists)
1101
- continue;
1102
- const target = join(preserveRoot, basename(artifact.path));
1103
- cpSync(artifact.path, target, { force: true });
1104
- preserved[name] = target;
1105
- }
1106
- }
1107
- rmSync(stateDir, { recursive: true, force: true });
1108
- return {
1109
- pruned: true,
1110
- preserved_artifacts: preserved,
1111
- run,
1112
- state_dir: stateDir,
1113
- ts: new Date().toISOString(),
1114
- };
362
+ return pruneTerminalRun(assertTerminalRun(runOrDir), options);
1115
363
  }
1116
364
  export function killRun(runOrDir) {
1117
365
  const result = stopRun(runOrDir, "SIGKILL", "run.kill");