@llblab/pi-actors 0.54.0 → 0.56.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 (188) hide show
  1. package/AGENTS.md +110 -20
  2. package/BACKLOG.md +207 -4
  3. package/CHANGELOG.md +17 -0
  4. package/README.md +33 -3
  5. package/dist/index.js +2 -1
  6. package/dist/lib/async-runs.d.ts +37 -0
  7. package/dist/lib/async-runs.js +131 -62
  8. package/dist/lib/automatic-review-runtime.d.ts +2 -0
  9. package/dist/lib/automatic-review-runtime.js +3 -2
  10. package/dist/lib/command-templates.d.ts +37 -0
  11. package/dist/lib/command-templates.js +263 -129
  12. package/dist/lib/execution-files.d.ts +32 -0
  13. package/dist/lib/execution-files.js +128 -0
  14. package/dist/lib/execution-output.js +5 -4
  15. package/dist/lib/execution-sessions.d.ts +19 -1
  16. package/dist/lib/execution-sessions.js +62 -26
  17. package/dist/lib/execution.d.ts +4 -1
  18. package/dist/lib/execution.js +6 -1
  19. package/dist/lib/extension-runtime.d.ts +10 -2
  20. package/dist/lib/extension-runtime.js +95 -14
  21. package/dist/lib/file-state.js +21 -2
  22. package/dist/lib/inspector-command.js +2 -2
  23. package/dist/lib/inspector-overlay.d.ts +25 -7
  24. package/dist/lib/inspector-overlay.js +562 -262
  25. package/dist/lib/inspector.d.ts +20 -10
  26. package/dist/lib/inspector.js +80 -91
  27. package/dist/lib/limits.d.ts +1 -0
  28. package/dist/lib/limits.js +1 -0
  29. package/dist/lib/observability.d.ts +15 -1
  30. package/dist/lib/observability.js +216 -99
  31. package/dist/lib/pi.d.ts +9 -3
  32. package/dist/lib/pi.js +80 -12
  33. package/dist/lib/recipe-projection.d.ts +18 -0
  34. package/dist/lib/recipe-projection.js +220 -0
  35. package/dist/lib/recipes-context.d.ts +2 -0
  36. package/dist/lib/recipes-context.js +30 -6
  37. package/dist/lib/registry.js +270 -36
  38. package/dist/lib/run-delivery.d.ts +213 -5
  39. package/dist/lib/run-delivery.js +1909 -40
  40. package/dist/lib/run-supervision.d.ts +107 -0
  41. package/dist/lib/run-supervision.js +412 -0
  42. package/dist/lib/run-ui-runtime.d.ts +10 -2
  43. package/dist/lib/run-ui-runtime.js +347 -124
  44. package/dist/lib/runs-artifacts.d.ts +26 -1
  45. package/dist/lib/runs-artifacts.js +68 -2
  46. package/dist/lib/runs-context-exports.d.ts +451 -0
  47. package/dist/lib/runs-context-exports.js +2315 -0
  48. package/dist/lib/runs-context-index.d.ts +97 -0
  49. package/dist/lib/runs-context-index.js +368 -0
  50. package/dist/lib/runs-control-delivery.js +46 -7
  51. package/dist/lib/runs-control.d.ts +78 -2
  52. package/dist/lib/runs-control.js +334 -2
  53. package/dist/lib/runs-controls.js +4 -1
  54. package/dist/lib/runs-descendant-launches.d.ts +80 -0
  55. package/dist/lib/runs-descendant-launches.js +287 -0
  56. package/dist/lib/runs-execution-evidence.d.ts +110 -0
  57. package/dist/lib/runs-execution-evidence.js +282 -0
  58. package/dist/lib/runs-index.d.ts +66 -1
  59. package/dist/lib/runs-index.js +349 -13
  60. package/dist/lib/runs-native-launches.d.ts +73 -0
  61. package/dist/lib/runs-native-launches.js +227 -0
  62. package/dist/lib/runs-outcomes.d.ts +141 -0
  63. package/dist/lib/runs-outcomes.js +292 -0
  64. package/dist/lib/runs-ownership.d.ts +10 -1
  65. package/dist/lib/runs-ownership.js +36 -4
  66. package/dist/lib/runs-process-windows-mi.d.ts +46 -0
  67. package/dist/lib/runs-process-windows-mi.js +114 -0
  68. package/dist/lib/runs-process.d.ts +34 -1
  69. package/dist/lib/runs-process.js +158 -26
  70. package/dist/lib/runs-results.d.ts +116 -0
  71. package/dist/lib/runs-results.js +518 -0
  72. package/dist/lib/runs-retention.d.ts +15 -2
  73. package/dist/lib/runs-retention.js +148 -9
  74. package/dist/lib/runs-session-exports.d.ts +95 -0
  75. package/dist/lib/runs-session-exports.js +470 -0
  76. package/dist/lib/runs-snapshot.d.ts +58 -0
  77. package/dist/lib/runs-snapshot.js +92 -0
  78. package/dist/lib/runs-start.d.ts +2 -0
  79. package/dist/lib/runs-start.js +44 -3
  80. package/dist/lib/runs-status.d.ts +15 -1
  81. package/dist/lib/runs-status.js +337 -13
  82. package/dist/lib/runs-supervision.d.ts +105 -0
  83. package/dist/lib/runs-supervision.js +501 -0
  84. package/dist/lib/runs-trace.d.ts +1 -0
  85. package/dist/lib/runs-trace.js +14 -3
  86. package/dist/lib/runtime.js +22 -3
  87. package/dist/lib/session-evidence.d.ts +37 -0
  88. package/dist/lib/session-evidence.js +59 -3
  89. package/dist/lib/state-readers.d.ts +4 -1
  90. package/dist/lib/state-readers.js +14 -8
  91. package/dist/lib/temp.d.ts +2 -4
  92. package/dist/lib/temp.js +4 -56
  93. package/dist/lib/tools-access.d.ts +3 -0
  94. package/dist/lib/tools-access.js +13 -0
  95. package/dist/lib/tools-inspect.d.ts +1 -0
  96. package/dist/lib/tools-inspect.js +110 -43
  97. package/dist/lib/tools-local.d.ts +3 -0
  98. package/dist/lib/tools-local.js +18 -6
  99. package/dist/lib/tools-message.d.ts +4 -1
  100. package/dist/lib/tools-message.js +126 -31
  101. package/dist/lib/tools-response.d.ts +31 -3
  102. package/dist/lib/tools-response.js +311 -47
  103. package/dist/lib/tools-spawn.d.ts +3 -0
  104. package/dist/lib/tools-spawn.js +29 -7
  105. package/dist/lib/trace-projection.d.ts +20 -0
  106. package/dist/lib/trace-projection.js +192 -43
  107. package/dist/scripts/async-runner.mjs +272 -194
  108. package/dist/scripts/conformance.mjs +14 -2
  109. package/dist/skills/actors/SKILL.md +47 -3
  110. package/dist/skills/actors/references/diagnostics.md +40 -0
  111. package/dist/skills/actors/references/runs.md +56 -2
  112. package/docs/README.md +9 -1
  113. package/docs/actor-inspector.md +84 -15
  114. package/docs/agent-experience.md +216 -0
  115. package/docs/async-runs.md +21 -3
  116. package/docs/composition-transport.md +103 -0
  117. package/docs/coordinator-delivery.md +226 -24
  118. package/docs/inspection.md +38 -2
  119. package/docs/output-projection-map.md +130 -0
  120. package/docs/releasing.md +19 -2
  121. package/docs/run-supervision.md +1622 -0
  122. package/docs/template-recipes.md +4 -0
  123. package/docs/windows-process-observation.md +248 -0
  124. package/index.ts +2 -1
  125. package/lib/async-runs.ts +164 -73
  126. package/lib/automatic-review-runtime.ts +4 -2
  127. package/lib/command-templates.ts +287 -138
  128. package/lib/execution-files.ts +118 -0
  129. package/lib/execution-output.ts +4 -4
  130. package/lib/execution-sessions.ts +57 -27
  131. package/lib/execution.ts +8 -0
  132. package/lib/extension-runtime.ts +106 -12
  133. package/lib/file-state.ts +16 -2
  134. package/lib/inspector-command.ts +2 -2
  135. package/lib/inspector-overlay.ts +524 -261
  136. package/lib/inspector.ts +101 -130
  137. package/lib/limits.ts +1 -0
  138. package/lib/observability.ts +235 -116
  139. package/lib/pi.ts +89 -17
  140. package/lib/recipe-projection.ts +232 -0
  141. package/lib/recipes-context.ts +27 -6
  142. package/lib/registry.ts +266 -36
  143. package/lib/run-delivery.ts +1737 -49
  144. package/lib/run-supervision.ts +411 -0
  145. package/lib/run-ui-runtime.ts +293 -126
  146. package/lib/runs-artifacts.ts +73 -1
  147. package/lib/runs-context-exports.ts +1871 -0
  148. package/lib/runs-context-index.ts +372 -0
  149. package/lib/runs-control-delivery.ts +36 -18
  150. package/lib/runs-control.ts +297 -1
  151. package/lib/runs-controls.ts +3 -1
  152. package/lib/runs-descendant-launches.ts +338 -0
  153. package/lib/runs-execution-evidence.ts +302 -0
  154. package/lib/runs-index.ts +345 -17
  155. package/lib/runs-native-launches.ts +270 -0
  156. package/lib/runs-outcomes.ts +377 -0
  157. package/lib/runs-ownership.ts +37 -4
  158. package/lib/runs-process-windows-mi.ts +113 -0
  159. package/lib/runs-process.ts +210 -34
  160. package/lib/runs-results.ts +601 -0
  161. package/lib/runs-retention.ts +134 -10
  162. package/lib/runs-session-exports.ts +406 -0
  163. package/lib/runs-snapshot.ts +143 -0
  164. package/lib/runs-start.ts +44 -2
  165. package/lib/runs-status.ts +252 -10
  166. package/lib/runs-supervision.ts +394 -0
  167. package/lib/runs-trace.ts +14 -3
  168. package/lib/runtime.ts +22 -3
  169. package/lib/session-evidence.ts +76 -4
  170. package/lib/state-readers.ts +16 -7
  171. package/lib/temp.ts +3 -56
  172. package/lib/tools-access.ts +13 -0
  173. package/lib/tools-inspect.ts +110 -51
  174. package/lib/tools-local.ts +37 -24
  175. package/lib/tools-message.ts +120 -37
  176. package/lib/tools-response.ts +273 -50
  177. package/lib/tools-spawn.ts +31 -10
  178. package/lib/trace-projection.ts +165 -42
  179. package/package.json +10 -6
  180. package/scripts/async-runner.mjs +272 -194
  181. package/scripts/conformance.mjs +14 -2
  182. package/skills/actors/SKILL.md +47 -3
  183. package/skills/actors/references/diagnostics.md +40 -0
  184. package/skills/actors/references/runs.md +56 -2
  185. package/banner.jpg +0 -0
  186. package/dist/lib/inspector-actions.d.ts +0 -16
  187. package/dist/lib/inspector-actions.js +0 -57
  188. package/lib/inspector-actions.ts +0 -79
@@ -1,14 +1,33 @@
1
1
  /**
2
2
  * Async run process control primitives.
3
- * Owns: platform signal planning, owned-process signalling, and terminal control markers.
3
+ * Owns: platform signalling, stop-request admission, evidence-only member/preparation recovery, and terminal control markers.
4
+ * Excludes: deadline eligibility and proof of complete owned-scope termination.
4
5
  */
5
6
 
6
7
  import { spawnSync } from "node:child_process";
8
+ import { lstatSync } from "node:fs";
7
9
  import { join } from "node:path";
8
10
 
9
11
  import { writeJsonAtomic } from "./file-state.ts";
12
+ import { readRunSupervisionClock } from "./run-supervision.ts";
13
+ import { assertRunDescendantPreparedMetadata, readRunDescendantLaunchLedger, readRunDescendantPreparation, recordRunDescendantLaunchIntentUnderLock, resolveRunDescendantLaunchTarget, type RunDescendantLaunchIntent, type RunDescendantLaunchOwner, type RunDescendantLaunchRequest } from "./runs-descendant-launches.ts";
14
+ import { readRunNativeLaunchLedger, recordRunNativeLaunchSettlementUnderLock, type RunNativeLaunchAbsenceSettlement, type RunNativeLaunchOwner, type RunNativeLaunchSettlement } from "./runs-native-launches.ts";
10
15
  import {
16
+ readRunNaturalOutcome,
17
+ readRunStopClaim,
18
+ RUN_OUTCOME_PROTOCOL,
19
+ writeRunStopClaimUnderLock,
20
+ type RunStopClaim,
21
+ type RunStopReason,
22
+ } from "./runs-outcomes.ts";
23
+ import { assertDistinctRunStateDirectories, readOwnedRunStateDirectory } from "./runs-ownership.ts";
24
+ import { acquireStateStartLock } from "./runs-start.ts";
25
+ import { readJsonFileResilient } from "./state-readers.ts";
26
+ import {
27
+ observeRunProcessExit,
28
+ runProcessIdentityDigest,
11
29
  verifyRunProcessIdentity,
30
+ type RunProcessExitDeps,
12
31
  type RunProcessIdentity,
13
32
  type RunProcessIdentityResult,
14
33
  } from "./runs-process.ts";
@@ -122,6 +141,283 @@ export function signalOwnedRunProcess(
122
141
  }
123
142
  }
124
143
 
144
+ function readOwnedRunControlMeta(stateDir: string): { stateDir: string; meta: Record<string, unknown> } | undefined {
145
+ const owned = readOwnedRunStateDirectory(stateDir);
146
+ const path = join(owned.stateDir, "run.json");
147
+ if (!lstatSync(path).isFile()) return undefined;
148
+ const read = readJsonFileResilient<unknown>(path, undefined);
149
+ if (read.diagnostics.length || !read.value || typeof read.value !== "object" || Array.isArray(read.value)) return undefined;
150
+ const meta = read.value as Record<string, unknown>;
151
+ if (meta.run !== owned.run || meta.state_dir !== owned.stateDir) return undefined;
152
+ return { stateDir: owned.stateDir, meta };
153
+ }
154
+
155
+ export interface RunStopClaimRequest {
156
+ runInstanceId: string;
157
+ ownerId?: string;
158
+ reason: RunStopReason;
159
+ }
160
+
161
+ export type RunStopClaimResult =
162
+ | { status: "stop_requested"; stopped: false; reused: boolean; claim: Readonly<RunStopClaim> }
163
+ | { status: "already_terminal"; terminal_status: "done" | "failed" }
164
+ | { status: "rejected"; code: string };
165
+
166
+ /**
167
+ * Requests only: no signal, handled marker, or claim of physical termination.
168
+ * The supervisor owns deadline eligibility; this owner revalidates configuration and identity.
169
+ * Legacy producers cannot participate until they bind the serialized outcome protocol.
170
+ */
171
+ export function claimRunStop(
172
+ stateDir: string,
173
+ request: RunStopClaimRequest,
174
+ deps: RunProcessSignalDeps & { onLockContention?: () => void } = {},
175
+ ): RunStopClaimResult {
176
+ const reject = (code: string): RunStopClaimResult => ({ status: "rejected", code });
177
+ if (!request || typeof request.runInstanceId !== "string" || !request.runInstanceId) {
178
+ return reject("run_generation_required");
179
+ }
180
+ if (request.reason !== "manual_kill" && request.reason !== "max_runtime") return reject("invalid_stop_reason");
181
+ const release = acquireStateStartLock(stateDir, { onContention: deps.onLockContention });
182
+ try {
183
+ const owned = readOwnedRunControlMeta(stateDir);
184
+ if (!owned) return reject("run_identity_unavailable");
185
+ const { meta } = owned;
186
+ if (meta.run_instance_id !== request.runInstanceId) return reject("run_generation_mismatch");
187
+ if (meta.ownerId !== request.ownerId) return reject("run_owner_mismatch");
188
+ if (meta.outcome_protocol !== RUN_OUTCOME_PROTOCOL) return reject("run_outcome_protocol_unavailable");
189
+ const natural = readRunNaturalOutcome(owned.stateDir, meta);
190
+ if (natural.state === "accepted") return { status: "already_terminal", terminal_status: natural.status };
191
+ const stop = readRunStopClaim(owned.stateDir, meta);
192
+ if (stop.state === "pending") return { status: "stop_requested", stopped: false, reused: true, claim: stop.claim };
193
+ if (stop.state !== "absent") return reject("run_stop_state_unavailable");
194
+ if (natural.state !== "absent") return reject("run_result_proof_unavailable");
195
+ if (!Number.isSafeInteger(meta.pid) || Number(meta.pid) <= 0 ||
196
+ (meta.ownerId !== undefined && (typeof meta.ownerId !== "string" || !meta.ownerId))) {
197
+ return reject("run_identity_unavailable");
198
+ }
199
+ if (request.reason === "max_runtime") {
200
+ try {
201
+ if (readRunSupervisionClock(meta)?.supervision.max_runtime_ms == null) return reject("run_deadline_unconfigured");
202
+ } catch {
203
+ return reject("run_deadline_unproven");
204
+ }
205
+ }
206
+ const proof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(
207
+ Number(meta.pid), meta.process_identity as RunProcessIdentity, deps.runtimePlatform ?? process.platform,
208
+ );
209
+ if (!proof.valid) return reject("run_process_unproven");
210
+ const claim = writeRunStopClaimUnderLock(owned.stateDir, meta, request.reason);
211
+ return { status: "stop_requested", stopped: false, reused: false, claim };
212
+ } finally {
213
+ release();
214
+ }
215
+ }
216
+
217
+ export interface RunNativeMemberRecoveryRequest {
218
+ runInstanceId: string;
219
+ ownerId?: string;
220
+ intentId: string;
221
+ }
222
+
223
+ export type RunNativeMemberRecoveryResult =
224
+ | { status: "recorded"; settlement: Readonly<RunNativeLaunchAbsenceSettlement> }
225
+ | { status: "already_recorded"; settlement: Readonly<RunNativeLaunchSettlement> }
226
+ | { status: "unproven"; reason: string }
227
+ | { status: "rejected"; code: string };
228
+
229
+ /** Evidence only after producer exit; never repair birth, dispatch signals, reopen work, or finalize a scope. */
230
+ export function recoverRunNativeMemberExit(
231
+ stateDir: string,
232
+ request: RunNativeMemberRecoveryRequest,
233
+ deps: RunProcessExitDeps & { onLockContention?: () => void } = {},
234
+ ): RunNativeMemberRecoveryResult {
235
+ const reject = (code: string): RunNativeMemberRecoveryResult => ({ status: "rejected", code });
236
+ if (!request || typeof request.runInstanceId !== "string" || !request.runInstanceId) return reject("run_generation_required");
237
+ if (typeof request.intentId !== "string" || !request.intentId) return reject("run_native_intent_required");
238
+ const release = acquireStateStartLock(stateDir, { onContention: deps.onLockContention });
239
+ try {
240
+ let owned: ReturnType<typeof readOwnedRunControlMeta>;
241
+ try { owned = readOwnedRunControlMeta(stateDir); } catch { return reject("run_identity_unavailable"); }
242
+ if (!owned) return reject("run_identity_unavailable");
243
+ const { meta } = owned;
244
+ if (meta.run_instance_id !== request.runInstanceId) return reject("run_generation_mismatch");
245
+ if (meta.ownerId !== request.ownerId) return reject("run_owner_mismatch");
246
+ if (meta.outcome_protocol !== RUN_OUTCOME_PROTOCOL) return reject("run_outcome_protocol_unavailable");
247
+ if (!Number.isSafeInteger(meta.pid) || Number(meta.pid) <= 0 ||
248
+ (meta.ownerId !== undefined && (typeof meta.ownerId !== "string" || !meta.ownerId))) return reject("run_identity_unavailable");
249
+ const owner: RunNativeLaunchOwner = {
250
+ run: String(meta.run), runInstanceId: request.runInstanceId,
251
+ ...(meta.ownerId === undefined ? {} : { ownerId: String(meta.ownerId) }),
252
+ pid: Number(meta.pid), processIdentityDigest: runProcessIdentityDigest(meta.process_identity),
253
+ };
254
+ const read = readRunNativeLaunchLedger(owned.stateDir, owner);
255
+ if (read.state !== "available") return reject("run_native_evidence_unavailable");
256
+ const intent = read.ledger.intents.find((item) => item.intent_id === request.intentId);
257
+ if (!intent) return reject("run_native_intent_unavailable");
258
+ if (intent.settlement) return { status: "already_recorded", settlement: intent.settlement };
259
+ if (!intent.birth) return { status: "unproven", reason: "birth_unavailable" };
260
+ if (!owner.processIdentityDigest) return { status: "unproven", reason: "producer_identity_unavailable" };
261
+ const producer = observeRunProcessExit(owner.pid, meta.process_identity as RunProcessIdentity, deps);
262
+ if (producer.status !== "exited") return { status: "unproven", reason: producer.status === "running" ? "producer_running" : "producer_exit_unproven" };
263
+ const member = observeRunProcessExit(intent.birth.pid, intent.birth.process_identity, deps);
264
+ if (member.status !== "exited") return { status: "unproven", reason: member.status === "running" ? "member_running" : "member_exit_unproven" };
265
+ let current: ReturnType<typeof readOwnedRunControlMeta>;
266
+ try { current = readOwnedRunControlMeta(owned.stateDir); } catch { return reject("run_identity_changed"); }
267
+ if (!current || current.stateDir !== owned.stateDir || current.meta.run !== owner.run ||
268
+ current.meta.run_instance_id !== owner.runInstanceId || current.meta.ownerId !== owner.ownerId ||
269
+ current.meta.pid !== owner.pid || current.meta.outcome_protocol !== RUN_OUTCOME_PROTOCOL ||
270
+ runProcessIdentityDigest(current.meta.process_identity) !== owner.processIdentityDigest) return reject("run_identity_changed");
271
+ const settlement = recordRunNativeLaunchSettlementUnderLock(owned.stateDir, owner, {
272
+ intent, birth: intent.birth, settlement: { basis: "verified_absence", observed_at: new Date().toISOString() },
273
+ });
274
+ return { status: "recorded", settlement };
275
+ } finally { release(); }
276
+ }
277
+
278
+ export interface RunDescendantPreparationRecoveryRequest {
279
+ runInstanceId: string;
280
+ ownerId?: string;
281
+ child: RunDescendantLaunchRequest;
282
+ }
283
+
284
+ export interface RunDescendantRecoveryCleanupFailure {
285
+ readonly status: "release_unproven";
286
+ readonly error_code: string;
287
+ }
288
+
289
+ export type RunDescendantPreparationRecoveryResult = (
290
+ | { status: "recorded" | "already_recorded"; intent: Readonly<RunDescendantLaunchIntent> }
291
+ | { status: "unproven"; reason: string }
292
+ | { status: "rejected"; code: string }
293
+ ) & { readonly cleanup?: Readonly<RunDescendantRecoveryCleanupFailure> };
294
+
295
+ const descendantRecoveryCleanupFailures = new Map<string, Readonly<RunDescendantRecoveryCleanupFailure>>();
296
+
297
+ function retainDescendantRecoveryCleanupFailure(key: string, error: unknown): Readonly<RunDescendantRecoveryCleanupFailure> {
298
+ const prior = descendantRecoveryCleanupFailures.get(key);
299
+ if (prior) return prior;
300
+ let code = "UNKNOWN";
301
+ try {
302
+ const value = (error as NodeJS.ErrnoException)?.code;
303
+ if (typeof value === "string" && /^[A-Z0-9_]{1,64}$/.exec(value)?.[0] === value) code = value;
304
+ } catch { /* A diagnostic getter cannot replace an action outcome. */ }
305
+ const failure = Object.freeze({ status: "release_unproven" as const, error_code: code });
306
+ descendantRecoveryCleanupFailures.set(key, failure);
307
+ try { console.error("Run descendant recovery lifecycle cleanup failed; release remains unproven and this runtime's recovery scope is fenced."); }
308
+ catch { /* A diagnostic sink cannot replace an action outcome. */ }
309
+ return failure;
310
+ }
311
+
312
+ /** Post-producer metadata evidence only; never recreate a producer, launch work, invent exits, or release scope guards. */
313
+ export function recoverRunDescendantPreparation(
314
+ stateDir: string,
315
+ request: RunDescendantPreparationRecoveryRequest,
316
+ deps: RunProcessExitDeps & { onLockContention?: () => void } = {},
317
+ ): Readonly<RunDescendantPreparationRecoveryResult> {
318
+ const reject = (code: string): RunDescendantPreparationRecoveryResult => ({ status: "rejected", code });
319
+ const generation = request?.runInstanceId;
320
+ if (typeof generation !== "string" || !generation) return Object.freeze(reject("run_generation_required"));
321
+ const ownerId = request.ownerId;
322
+ if (ownerId !== undefined && (typeof ownerId !== "string" || !ownerId)) return Object.freeze(reject("run_owner_mismatch"));
323
+ if (!request.child || typeof request.child !== "object" || Array.isArray(request.child)) return Object.freeze(reject("run_descendant_target_required"));
324
+ const childRequest = Object.freeze({ ...request.child });
325
+ let directory: ReturnType<typeof readOwnedRunStateDirectory>;
326
+ try { directory = readOwnedRunStateDirectory(stateDir); } catch { return Object.freeze(reject("run_identity_unavailable")); }
327
+ const key = JSON.stringify([directory.stateDir, generation, ownerId ?? null]);
328
+ const quarantined = descendantRecoveryCleanupFailures.get(key);
329
+ if (quarantined) return Object.freeze({ ...reject("run_descendant_recovery_cleanup_unproven"), cleanup: quarantined });
330
+ const releaseParent = acquireStateStartLock(directory.stateDir, { onContention: deps.onLockContention });
331
+ let releaseChild: (() => void) | undefined;
332
+ let result!: RunDescendantPreparationRecoveryResult;
333
+ let primary: unknown;
334
+ let failed = false;
335
+ let cleanup: Readonly<RunDescendantRecoveryCleanupFailure> | undefined;
336
+ try {
337
+ result = (() : RunDescendantPreparationRecoveryResult => {
338
+ let owned: ReturnType<typeof readOwnedRunControlMeta>;
339
+ try { owned = readOwnedRunControlMeta(directory.stateDir); } catch { return reject("run_identity_unavailable"); }
340
+ if (!owned) return reject("run_identity_unavailable");
341
+ const { meta } = owned;
342
+ if (meta.run_instance_id !== generation) return reject("run_generation_mismatch");
343
+ if (meta.ownerId !== ownerId) return reject("run_owner_mismatch");
344
+ if (meta.outcome_protocol !== RUN_OUTCOME_PROTOCOL) return reject("run_outcome_protocol_unavailable");
345
+ if (!Number.isSafeInteger(meta.pid) || Number(meta.pid) <= 0) return reject("run_identity_unavailable");
346
+ const owner: RunDescendantLaunchOwner = { run: String(meta.run), runInstanceId: generation,
347
+ ...(ownerId === undefined ? {} : { ownerId }), pid: Number(meta.pid), processIdentityDigest: runProcessIdentityDigest(meta.process_identity) };
348
+ const child = resolveRunDescendantLaunchTarget(childRequest, owner, owned.stateDir);
349
+ if (!child) return reject("run_descendant_target_invalid");
350
+ const readIntent = (): { intent?: Readonly<RunDescendantLaunchIntent>; error?: string } => {
351
+ const reading = readRunDescendantLaunchLedger(owned.stateDir, owner);
352
+ if (reading.state === "invalid") return { error: "run_descendant_evidence_unavailable" };
353
+ const intent = reading.state === "available" ? reading.ledger.intents.find((item) => item.child.run_instance_id === child.run_instance_id) : undefined;
354
+ if (intent && (intent.child.run !== child.run || intent.child.ownerId !== child.ownerId || intent.child.state_dir !== child.state_dir)) {
355
+ return { error: "run_descendant_target_mismatch" };
356
+ }
357
+ return { intent };
358
+ };
359
+ const sameParent = (): boolean => {
360
+ try {
361
+ const current = readOwnedRunControlMeta(owned.stateDir);
362
+ return Boolean(current && current.stateDir === owned.stateDir && current.meta.run === owner.run &&
363
+ current.meta.run_instance_id === generation && current.meta.ownerId === ownerId && current.meta.pid === owner.pid &&
364
+ current.meta.outcome_protocol === RUN_OUTCOME_PROTOCOL && runProcessIdentityDigest(current.meta.process_identity) === owner.processIdentityDigest);
365
+ } catch { return false; }
366
+ };
367
+ const initial = readIntent();
368
+ if (initial.error) return reject(initial.error);
369
+ if (initial.intent?.preparation) return { status: "already_recorded", intent: initial.intent };
370
+ try {
371
+ assertDistinctRunStateDirectories(owned.stateDir, child.state_dir);
372
+ assertRunDescendantPreparedMetadata(child.state_dir, child);
373
+ } catch { return reject("run_descendant_preparation_unavailable"); }
374
+ releaseChild = acquireStateStartLock(child.state_dir, { onContention: deps.onLockContention });
375
+ if (!sameParent()) return reject("run_identity_changed");
376
+ const admitted = readIntent();
377
+ if (admitted.error) return reject(admitted.error);
378
+ if (admitted.intent?.preparation) return { status: "already_recorded", intent: admitted.intent };
379
+ const parent = { run: owner.run, run_instance_id: generation, ...(ownerId === undefined ? {} : { ownerId }), state_dir: owned.stateDir };
380
+ try { assertRunDescendantPreparedMetadata(child.state_dir, child); }
381
+ catch { return reject("run_descendant_preparation_unavailable"); }
382
+ const preparation = readRunDescendantPreparation(child.state_dir, child, parent);
383
+ if (!preparation) return reject("run_descendant_preparation_unavailable");
384
+ if (!preparation.parent_producer) return { status: "unproven", reason: "producer_binding_unavailable" };
385
+ if (preparation.parent_producer.process_identity_digest === null || !owner.processIdentityDigest) {
386
+ return { status: "unproven", reason: "producer_identity_unavailable" };
387
+ }
388
+ if (preparation.parent_producer.pid !== owner.pid || preparation.parent_producer.process_identity_digest !== owner.processIdentityDigest) {
389
+ return reject("run_descendant_producer_mismatch");
390
+ }
391
+ const producer = observeRunProcessExit(owner.pid, meta.process_identity as RunProcessIdentity, deps);
392
+ if (producer.status !== "exited") return { status: "unproven", reason: producer.status === "running" ? "producer_running" : "producer_exit_unproven" };
393
+ if (!sameParent()) return reject("run_identity_changed");
394
+ const current = readIntent();
395
+ if (current.error) return reject(current.error);
396
+ if (current.intent?.preparation) {
397
+ if (current.intent.preparation.preparation_id !== preparation.preparation_id || current.intent.preparation.prepared_at !== preparation.prepared_at) {
398
+ return reject("run_descendant_preparation_changed");
399
+ }
400
+ return { status: "already_recorded", intent: current.intent };
401
+ }
402
+ const retained = readRunDescendantPreparation(child.state_dir, child, parent);
403
+ if (!retained || JSON.stringify(retained) !== JSON.stringify(preparation)) return reject("run_descendant_preparation_changed");
404
+ try { assertRunDescendantPreparedMetadata(child.state_dir, child); }
405
+ catch { return reject("run_descendant_preparation_changed"); }
406
+ const intent = recordRunDescendantLaunchIntentUnderLock(owned.stateDir, owner, {
407
+ run: child.run, runInstanceId: child.run_instance_id, ownerId: child.ownerId, stateDir: child.state_dir,
408
+ }, preparation);
409
+ return { status: "recorded", intent };
410
+ })();
411
+ } catch (error) { primary = error; failed = true; }
412
+ finally {
413
+ for (const release of [releaseChild, releaseParent]) if (release) {
414
+ try { release(); } catch (error) { cleanup = retainDescendantRecoveryCleanupFailure(key, error); }
415
+ }
416
+ }
417
+ if (failed) throw primary;
418
+ return Object.freeze({ ...result, ...(cleanup ? { cleanup } : {}) });
419
+ }
420
+
125
421
  export function markTerminalHandled(
126
422
  stateDir: string,
127
423
  details: Record<string, unknown>,
@@ -57,7 +57,9 @@ export function readRunControlJournalFromStateDir(stateDir: string): JsonlReadRe
57
57
  return { diagnostics: [{ message: String(error), path }], records: [] };
58
58
  }
59
59
  try {
60
- const bytes = fstatSync(fd).size;
60
+ const descriptor = fstatSync(fd);
61
+ if (!descriptor.isFile()) return { diagnostics: [{ message: "Run Control journal is not a regular file", path }], records: [] };
62
+ const bytes = descriptor.size;
61
63
  if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
62
64
  return { diagnostics: [{ message: "Run Control journal exceeds bounded read limit", path }], records: [] };
63
65
  }