@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.145

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 (197) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +9 -3
  5. package/dist/cli/command-metadata.js +33 -14
  6. package/dist/cli/help.js +32 -1
  7. package/dist/cli/hook-ingress.js +27 -3
  8. package/dist/cli/memory-flow-admission.js +7 -3
  9. package/dist/cli/memory-init-invocation.js +10 -3
  10. package/dist/cli/memory-init.js +74 -68
  11. package/dist/cli/memory-storage-admission.js +3 -2
  12. package/dist/cli/memory-storage-qualification.js +105 -0
  13. package/dist/cli/memory-storage-transition.js +123 -0
  14. package/dist/cli/memory-upgrade.js +189 -0
  15. package/dist/cli/native-hook-ingress.js +14 -4
  16. package/dist/cli/native-hook-preparation.js +55 -0
  17. package/dist/cli/response-file.js +2 -1
  18. package/dist/cli/run-cli.js +70 -32
  19. package/dist/cli/setup.js +2 -1
  20. package/dist/cli.js +12 -0
  21. package/dist/domain/stage-handler-identities.js +8 -0
  22. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  23. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  24. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  25. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -20
  27. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  28. package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
  29. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
  30. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  31. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  33. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  34. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
  36. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  37. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  38. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  39. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +4 -2
  40. package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
  41. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  42. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  43. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  44. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  45. package/dist/schemas/release-impact.schema.json +3 -3
  46. package/dist/schemas/stage-start-response.schema.json +4 -0
  47. package/dist/services/accepted-intent.js +13 -1
  48. package/dist/services/canon.js +43 -7
  49. package/dist/services/cli-operation-classifier.js +1 -1
  50. package/dist/services/cli-upgrade-allowlist.js +1 -1
  51. package/dist/services/code-checks.js +11 -4
  52. package/dist/services/compatibility-preflight.js +5 -2
  53. package/dist/services/controller-fanout.js +21 -4
  54. package/dist/services/controller-memory-task.js +45 -0
  55. package/dist/services/controller-memory-worker.js +48 -0
  56. package/dist/services/dashboard.js +2 -2
  57. package/dist/services/descriptor-run-start.js +26 -19
  58. package/dist/services/descriptor-stage-coordinator.js +30 -10
  59. package/dist/services/descriptor-stage-start.js +40 -4
  60. package/dist/services/engines.js +64 -14
  61. package/dist/services/eval-snapshots.js +104 -93
  62. package/dist/services/external-work-launch.js +5 -3
  63. package/dist/services/flow-catalog.js +8 -4
  64. package/dist/services/frozen-engine.js +113 -11
  65. package/dist/services/hook-memory-routing.js +188 -0
  66. package/dist/services/hook-recovery-guard.js +14 -0
  67. package/dist/services/hook-session-observation.js +24 -0
  68. package/dist/services/hooks.js +27 -8
  69. package/dist/services/hybrid-contracts.js +2 -13
  70. package/dist/services/hybrid-digest.js +14 -0
  71. package/dist/services/installed-flow-source.js +25 -4
  72. package/dist/services/lifecycle-invocations.js +33 -5
  73. package/dist/services/memory-accepted-composition.js +5 -4
  74. package/dist/services/memory-completed-correction-wave.js +23 -15
  75. package/dist/services/memory-compose-handler.js +163 -21
  76. package/dist/services/memory-composition-acceptance.js +186 -10
  77. package/dist/services/memory-composition-graph.js +42 -11
  78. package/dist/services/memory-composition-registration.js +23 -8
  79. package/dist/services/memory-composition-work-custody.js +14 -9
  80. package/dist/services/memory-composition-work-results.js +15 -3
  81. package/dist/services/memory-corrected-review-batch.js +10 -3
  82. package/dist/services/memory-corrected-review-findings.js +4 -2
  83. package/dist/services/memory-corrected-review-inputs.js +10 -5
  84. package/dist/services/memory-corrected-review-receipt.js +17 -9
  85. package/dist/services/memory-corrected-review-registration.js +3 -2
  86. package/dist/services/memory-correction-check-receipt.js +20 -4
  87. package/dist/services/memory-custody-snapshot.js +36 -0
  88. package/dist/services/memory-finish-evidence.js +44 -22
  89. package/dist/services/memory-finish-handler.js +37 -23
  90. package/dist/services/memory-finish-integration.js +218 -48
  91. package/dist/services/memory-finish-readiness.js +66 -19
  92. package/dist/services/memory-git-integration.js +87 -18
  93. package/dist/services/memory-historical-corrected-review.js +21 -15
  94. package/dist/services/memory-historical-original-review.js +22 -9
  95. package/dist/services/memory-historical-repair-registration.js +14 -8
  96. package/dist/services/memory-historical-second-review.js +30 -11
  97. package/dist/services/memory-init-contexts.js +28 -2
  98. package/dist/services/memory-init-controller-start.js +19 -8
  99. package/dist/services/memory-init-git-policy.js +50 -21
  100. package/dist/services/memory-init-lint-capability.js +21 -3
  101. package/dist/services/memory-init-native-capacity.js +13 -0
  102. package/dist/services/memory-init-preflight.js +11 -1
  103. package/dist/services/memory-init-request-candidate.js +73 -17
  104. package/dist/services/memory-init-request-input.js +23 -7
  105. package/dist/services/memory-init-run-bundle.js +49 -12
  106. package/dist/services/memory-init-storage.js +17 -7
  107. package/dist/services/memory-layer.js +68 -4
  108. package/dist/services/memory-lint-config.js +111 -2
  109. package/dist/services/memory-native-review-copy.js +5 -67
  110. package/dist/services/memory-normal-flow-entry.js +12 -0
  111. package/dist/services/memory-ordinary-defaults.js +22 -5
  112. package/dist/services/memory-preflight-handler.js +15 -0
  113. package/dist/services/memory-repair-current-view.js +1 -1
  114. package/dist/services/memory-repair-document-delta.js +70 -33
  115. package/dist/services/memory-repair-history.js +12 -7
  116. package/dist/services/memory-repair-physical-binding.js +1 -119
  117. package/dist/services/memory-repair-publication-plan.js +5 -3
  118. package/dist/services/memory-repair-publication-source.js +1 -1
  119. package/dist/services/memory-repair-publication.js +3 -2
  120. package/dist/services/memory-repair-registration.js +2 -1
  121. package/dist/services/memory-repair-work-baseline.js +4 -2
  122. package/dist/services/memory-repair-work-completion.js +32 -12
  123. package/dist/services/memory-repair-work-results.js +21 -2
  124. package/dist/services/memory-repair-workspace.js +11 -2
  125. package/dist/services/memory-repair-writer-copy.js +2 -1
  126. package/dist/services/memory-result-binding.js +78 -12
  127. package/dist/services/memory-retained-review-authority.js +17 -8
  128. package/dist/services/memory-retained-second-baseline.js +1 -1
  129. package/dist/services/memory-retained-second-repairs.js +12 -6
  130. package/dist/services/memory-review-correction-plan.js +47 -12
  131. package/dist/services/memory-review-handler.js +17 -17
  132. package/dist/services/memory-review-inputs.js +55 -9
  133. package/dist/services/memory-review-limits.js +10 -0
  134. package/dist/services/memory-review-receipt.js +2 -2
  135. package/dist/services/memory-review-registration.js +13 -4
  136. package/dist/services/memory-review-work-custody.js +3 -1
  137. package/dist/services/memory-review-work-input.js +26 -7
  138. package/dist/services/memory-review-work-results.js +26 -4
  139. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  140. package/dist/services/memory-second-correction-plan.js +2 -1
  141. package/dist/services/memory-second-repair-launch.js +4 -4
  142. package/dist/services/memory-second-repair-source.js +1 -1
  143. package/dist/services/memory-stage-dispatch.js +38 -1
  144. package/dist/services/memory-stage-results.js +113 -22
  145. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  146. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  147. package/dist/services/memory-upgrade-evidence.js +100 -0
  148. package/dist/services/memory-upgrade-inventory.js +442 -0
  149. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  150. package/dist/services/memory-upgrade-publication.js +413 -0
  151. package/dist/services/memory-upgrade-selection.js +430 -0
  152. package/dist/services/memory-upgrade-start.js +60 -0
  153. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  154. package/dist/services/memory-verification-acceptance.js +38 -17
  155. package/dist/services/memory-verification-checks.js +77 -23
  156. package/dist/services/memory-verification-receipt.js +6 -5
  157. package/dist/services/memory-verify-handler.js +15 -13
  158. package/dist/services/memory-work-result-json.js +12 -0
  159. package/dist/services/memory-work-result-preparation.js +5 -4
  160. package/dist/services/merge-server.js +4 -2
  161. package/dist/services/migrations.js +127 -98
  162. package/dist/services/physical-startup.js +45 -0
  163. package/dist/services/project-registry.js +97 -0
  164. package/dist/services/project-summary.js +1 -1
  165. package/dist/services/projects.js +3 -93
  166. package/dist/services/prompt-delivery.js +71 -0
  167. package/dist/services/provider-capacity.js +69 -8
  168. package/dist/services/recovery-snapshot-database.js +32 -4
  169. package/dist/services/repair-intents.js +1 -12
  170. package/dist/services/repair-receipt.js +13 -0
  171. package/dist/services/run-control-worker.js +35 -18
  172. package/dist/services/run-controller-process.js +2 -6
  173. package/dist/services/run-controller-recovery.js +6 -1
  174. package/dist/services/run-controller.js +131 -80
  175. package/dist/services/run-recovery-guard.js +30 -0
  176. package/dist/services/run-recovery.js +11 -44
  177. package/dist/services/run-start-custody.js +17 -5
  178. package/dist/services/runtime-scope-worker.js +46 -19
  179. package/dist/services/sessions.js +3 -24
  180. package/dist/services/setup.js +63 -13
  181. package/dist/services/stage-handlers.js +9 -7
  182. package/dist/services/stage-pause.js +108 -44
  183. package/dist/services/version-status.js +1 -1
  184. package/dist/services/vnext-fanout.js +21 -9
  185. package/dist/services/vnext-plan-review.js +3 -5
  186. package/dist/services/work-registry.js +80 -33
  187. package/dist/services/workspace-files.js +1 -1
  188. package/dist/shared/entity-references.js +1 -8
  189. package/dist/storage/data-preservation.js +65 -0
  190. package/dist/storage/database.js +53 -19
  191. package/dist/storage/import-guard.js +84 -27
  192. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  193. package/dist/storage/paired-writer-migration.js +424 -0
  194. package/dist/storage/writer-contract.js +11 -4
  195. package/dist/storage/writer-migration.js +313 -52
  196. package/dist/storage/writer-restore.js +217 -0
  197. package/package.json +8 -4
@@ -5,7 +5,7 @@ import { ensureDir } from "../storage/paths.js";
5
5
  import { getCliVersionReport } from "./build-info.js";
6
6
  import { projectDashboardHtmlPath, projectSummaryJsonPath } from "./config.js";
7
7
  import { queueForProject } from "./merge-queue.js";
8
- import { activeFlowSessionsForProject } from "./sessions.js";
8
+ import { activeFlowSessionsForProject } from "./hook-session-observation.js";
9
9
  import { getProjectVersionStatus } from "./version-status.js";
10
10
  export function projectSummaryPath(context, projectId) {
11
11
  return projectSummaryJsonPath(context, projectId);
@@ -1,58 +1,17 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
2
  import { foreignKeyTransaction } from "../storage/foreign-key-transaction.js";
4
- import { formatFullId, isFullEntityId, isShortEntityId, parseFullEntityId, slugFromRoot } from "../domain/entity-ids.js";
3
+ import { isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
5
4
  import { AppError } from "../shared/errors.js";
6
5
  import { projectRuntimeRoot, resolveProjectRoot } from "../storage/paths.js";
7
6
  import { appendAudit } from "./audit.js";
7
+ import { registerProject, registerPreparedProject, PreparedProjectRegistration, normalizeStoredRoot, findProjectByRoot, requireProjectByRoot, newProjectRecord } from "./project-registry.js";
8
+ export { registerProject, registerPreparedProject, PreparedProjectRegistration, findProjectByRoot, requireProjectByRoot };
8
9
  import { activeCodexSessionBindingsForProject, activeFlowSessionBindingsForProject, codexHomeProfilesForProject, codexHookEventsForProject, hookStatusForProject } from "./hooks.js";
9
10
  import { dashboardMarkdownPath, globalDashboardMarkdownPath, readProjectConfig } from "./config.js";
10
11
  import { loadProjectFlowContract } from "../domain/flow-contract.js";
11
12
  import { buildStaticFlowGuidance } from "./flow-guidance.js";
12
13
  import { normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
13
14
  import { buildProjectSummary, projectRegistrySummary, readPublishedProjectSummary } from "./project-summary.js";
14
- /** Resolve filesystem identity before entering a composite SQL writer. */
15
- export class PreparedProjectRegistration {
16
- #root;
17
- constructor(root) { this.#root = root; Object.freeze(this); }
18
- static prepare(input) {
19
- return new PreparedProjectRegistration(resolveProjectRoot(input.root));
20
- }
21
- root() { return this.#root; }
22
- }
23
- export function registerProject(context, input) {
24
- return registerPreparedProject(context, PreparedProjectRegistration.prepare(input));
25
- }
26
- /** Metadata-only registration; the composite caller owns its SQL transaction. */
27
- export function registerPreparedProject(context, prepared) {
28
- if (!(prepared instanceof PreparedProjectRegistration))
29
- throw new AppError("validation", "Project registration must be prepared", 2);
30
- const root = prepared.root();
31
- const existing = context.db.get("SELECT * FROM projects WHERE root = ?", [root]);
32
- const now = context.now();
33
- const project = existing ?? newProjectRecord(context, root, now);
34
- if (existing) {
35
- context.db.run("UPDATE projects SET status = 'active', updated_at = ? WHERE id = ?", [now, existing.id]);
36
- }
37
- else {
38
- context.db.run("INSERT INTO projects (id, short_id, slug, root, status, state_root, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
39
- project.id,
40
- project.short_id,
41
- project.slug,
42
- project.root,
43
- project.status,
44
- project.state_root,
45
- project.created_at,
46
- project.updated_at
47
- ]);
48
- appendAudit(context, {
49
- projectId: project.id,
50
- eventType: "project.registered",
51
- payload: { project_id: project.id, root }
52
- });
53
- }
54
- return { ok: true, project: { ...project, status: "active", updated_at: now }, db_path: context.db.path };
55
- }
56
15
  export function prepareProjectArchive(context, input) {
57
16
  if (Boolean(input.idOrAlias) === Boolean(input.root))
58
17
  throw new AppError("usage", "Project archive requires exactly one project ID/alias or --root", 2);
@@ -92,17 +51,6 @@ export function archiveProject(context, input) {
92
51
  ...(result.changes === 1 ? {} : { skipped: true, reason: "already_archived" })
93
52
  };
94
53
  }
95
- function normalizeStoredRoot(root) {
96
- const absolute = path.resolve(root);
97
- if (fs.existsSync(absolute)) {
98
- return fs.realpathSync(absolute);
99
- }
100
- const parent = path.dirname(absolute);
101
- if (fs.existsSync(parent)) {
102
- return path.join(fs.realpathSync(parent), path.basename(absolute));
103
- }
104
- return absolute;
105
- }
106
54
  export function resolveProject(context, input) {
107
55
  const candidates = resolveProjectCandidates(context, input.idOrAlias);
108
56
  if (candidates.length === 1) {
@@ -244,35 +192,6 @@ export function getProjectStatus(context, input) {
244
192
  }
245
193
  };
246
194
  }
247
- export function findProjectByRoot(context, root) {
248
- return context.db.get("SELECT * FROM projects WHERE root = ?", [root]);
249
- }
250
- export function requireProjectByRoot(context, root) {
251
- // Callers may receive a path through a native adapter or shell. Resolve
252
- // aliases such as macOS `/var` -> `/private/var` before comparing with the
253
- // canonical root persisted in the project registry.
254
- const canonical = normalizeStoredRoot(root);
255
- const project = findProjectByRoot(context, canonical);
256
- if (!project) {
257
- throw new AppError("not_found", `Project is not registered: ${canonical}`, 1);
258
- }
259
- return project;
260
- }
261
- function newProjectRecord(context, root, now) {
262
- const slug = slugFromRoot(root);
263
- const id = nextFullProjectId(context, slug);
264
- const { shortId } = parseFullEntityId(id);
265
- return {
266
- id,
267
- short_id: shortId,
268
- slug,
269
- root,
270
- status: "active",
271
- state_root: projectRuntimeRoot(context.ddFlowHome, id),
272
- created_at: now,
273
- updated_at: now
274
- };
275
- }
276
195
  function backfilledTypedProjectRecord(context, project, typed, now) {
277
196
  return {
278
197
  ...project,
@@ -282,15 +201,6 @@ function backfilledTypedProjectRecord(context, project, typed, now) {
282
201
  updated_at: now
283
202
  };
284
203
  }
285
- function nextFullProjectId(context, slug) {
286
- const rows = context.db.all("SELECT id FROM projects WHERE id LIKE 'PRJ-%'");
287
- const used = rows
288
- .map((row) => /^PRJ-(\d{3})-/.exec(row.id)?.[1])
289
- .filter((value) => Boolean(value))
290
- .map((value) => Number(value));
291
- const next = Math.max(0, ...used) + 1;
292
- return formatFullId("PRJ", next, slug);
293
- }
294
204
  function typedProjectMetadata(project) {
295
205
  if (!isFullEntityId(project.id)) {
296
206
  return null;
@@ -0,0 +1,71 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { publishImmutableInput } from "../storage/immutable-input.js";
6
+ const digest = (text) => crypto.createHash("sha256").update(text).digest("hex");
7
+ /** Describe the exact delivery before the existing operation receipt is settled.
8
+ * This does not issue commands or replace the original semantic prompt. */
9
+ export function preparePromptDelivery(value) {
10
+ if (!value || typeof value !== "object" || Array.isArray(value))
11
+ return value;
12
+ const packet = value, text = packet.worker_prompt_markdown;
13
+ if (typeof text !== "string" || typeof packet.prompt_path !== "string")
14
+ return value;
15
+ if (!path.isAbsolute(packet.prompt_path))
16
+ throw new AppError("prompt_delivery_invalid", "Prompt delivery requires an absolute original prompt path", 1, { effect: "committed" });
17
+ const sha256 = digest(text);
18
+ let file = path.join(path.dirname(packet.prompt_path), "prompt-deliveries", `${sha256}.md`);
19
+ try {
20
+ if (fs.lstatSync(packet.prompt_path).isFile() && !fs.lstatSync(packet.prompt_path).isSymbolicLink()
21
+ && fs.readFileSync(packet.prompt_path, "utf8") === text)
22
+ file = packet.prompt_path;
23
+ }
24
+ catch { /* A retained delivery can republish an absent original without replay. */ }
25
+ const retained = packet.prompt_delivery;
26
+ if (retained && (retained.sha256 !== sha256 || retained.path !== file)) {
27
+ // An original file may disappear after commit. Keep the exact retained locator.
28
+ if (retained.sha256 !== sha256 || !path.isAbsolute(retained.path)
29
+ || retained.path !== packet.prompt_path && retained.path !== path.join(path.dirname(packet.prompt_path), "prompt-deliveries", `${sha256}.md`)) {
30
+ throw new AppError("prompt_delivery_invalid", "Retained delivery differs from its committed bytes", 1, { effect: "committed" });
31
+ }
32
+ file = retained.path;
33
+ }
34
+ const delivery = { path: file, sha256 };
35
+ // Keep the locator visible even when native tools clip verbose JSON metadata.
36
+ return Object.assign({ prompt_delivery: delivery }, packet, { prompt_delivery: delivery });
37
+ }
38
+ /** Publish only retained bytes, after commit and before advertising the locator. */
39
+ export function publishPromptDelivery(value) {
40
+ if (!value || typeof value !== "object")
41
+ return;
42
+ const packet = value, delivery = packet.prompt_delivery;
43
+ if (!delivery)
44
+ return;
45
+ const text = packet.worker_prompt_markdown;
46
+ try {
47
+ if (typeof text !== "string" || digest(text) !== delivery.sha256 || !path.isAbsolute(delivery.path))
48
+ throw new Error("Delivery checksum mismatch");
49
+ if (typeof packet.prompt_path !== "string" || !path.isAbsolute(packet.prompt_path))
50
+ throw new Error("Delivery has no original prompt directory");
51
+ // Revalidate retained routing before touching the filesystem. System path
52
+ // aliases (/var on macOS) are allowed; a delivery-directory symlink is not.
53
+ preparePromptDelivery(packet);
54
+ const directory = path.dirname(delivery.path);
55
+ if (delivery.path !== packet.prompt_path) {
56
+ const stat = fs.lstatSync(directory, { throwIfNoEntry: false });
57
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink()))
58
+ throw new Error("Delivery directory is not an owned regular directory");
59
+ }
60
+ fs.mkdirSync(path.dirname(delivery.path), { recursive: true, mode: 0o700 });
61
+ if (delivery.path !== packet.prompt_path
62
+ && fs.realpathSync(directory) !== path.join(fs.realpathSync(path.dirname(packet.prompt_path)), "prompt-deliveries"))
63
+ throw new Error("Delivery directory escaped its original prompt directory");
64
+ publishImmutableInput(delivery.path, Buffer.from(text), "prompt_delivery_conflict");
65
+ fs.accessSync(delivery.path, fs.constants.R_OK);
66
+ }
67
+ catch (cause) {
68
+ throw new AppError("prompt_delivery_pending", "Operation committed; reconcile its retained delivery before continuing. Do not repeat the lifecycle operation or execute the old prompt", 1, { effect: "committed", publication_pending: true, path: delivery.path, sha256: delivery.sha256, cause: cause instanceof Error ? cause.message : String(cause) });
69
+ }
70
+ }
71
+ export const promptDeliveryInstruction = "Read the complete plain Markdown file at prompt_delivery.path; it is the exact current instruction packet (SHA-256 in prompt_delivery.sha256). Read all chunks if needed. The JSON response is routing metadata, not the primary prompt. If prompt_delivery is absent on a legacy response, read the complete worker_prompt_markdown from response_file. If publication is pending, stop and reconcile the existing operation; never replay Stage/Work start or use an old prompt.";
@@ -1,8 +1,50 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
2
  import crypto from "node:crypto";
3
- import { CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, capacityBackoff, nativeItemsSettled, normalizeProviderFailure, overloadExhausted, parseCapacityOperation, refusalObservation, terminalProviderOverload } from "../harness-runtime/lib/provider-overload-policy.mjs";
3
+ import { CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, capacityBackoff, nativeItemsSettled, normalizeProviderFailure, overloadExhausted, parseCapacityOperation, refusalObservation, terminalProviderOverload, streamRequestKey } from "../harness-runtime/lib/provider-overload-policy.mjs";
4
4
  import { AppError } from "../shared/errors.js";
5
5
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
6
+ import { parseLifecycleCommand, commandOption } from "./lifecycle-command.js";
7
+ /** Native output is evidence, not authority: match the committed current scope. */
8
+ export function ownedProviderBoundary(context, input, boundary) {
9
+ if (typeof boundary.native_hook_event_key !== "string" || typeof boundary.daemon_instance !== "string"
10
+ || !context.db.get("SELECT 1 FROM hook_events WHERE project_id = ? AND event_key = ? AND daemon_id = ? AND provider_session_id = ? AND status = 'claimed'", [input.projectId, boundary.native_hook_event_key, boundary.daemon_instance, input.sessionId]))
11
+ return false;
12
+ const invocation = context.db.get("SELECT scope_json, outcome_json, command FROM lifecycle_invocations WHERE event_key = ? AND status = 'settled'", [boundary.native_hook_event_key]);
13
+ if (!invocation)
14
+ return false;
15
+ const scope = JSON.parse(invocation.scope_json);
16
+ const outcome = JSON.parse(invocation.outcome_json);
17
+ if (scope.runId !== input.runId || scope.daemonId !== boundary.daemon_instance || scope.rootSessionId !== input.sessionId || scope.generation !== input.generation
18
+ || outcome.error || outcome.effect !== undefined && outcome.effect !== "committed" || outcome.result?.ok !== true
19
+ || outcome.result.effect !== undefined && outcome.result.effect !== "committed")
20
+ return false;
21
+ const parsed = parseLifecycleCommand(invocation.command);
22
+ if (parsed.kind === "none" || parsed.kind === "compound" && !(parsed.supported === true && parsed.invocations?.length === 1) || !["stage_pause", "stage_finish", "work_finish"].includes(parsed.invocation.operation)
23
+ || boundary.outcome === "completed" && parsed.invocation.operation === "stage_pause" || boundary.outcome === "paused" && parsed.invocation.operation !== "stage_pause"
24
+ || boundary.lifecycle_operation !== undefined && boundary.lifecycle_operation !== parsed.invocation.operation
25
+ || parsed.invocation.operation !== "work_finish" && commandOption(parsed.invocation, "stage") !== input.stage)
26
+ return false;
27
+ const result = outcome.result;
28
+ if (result.run_id !== undefined && result.run_id !== input.runId || result.stage !== undefined && result.stage !== input.stage
29
+ || boundary.outcome === "paused" && (result.pause?.id !== boundary.pause?.id || result.outcome !== "paused")
30
+ || boundary.work_id !== undefined && result.work_id !== undefined && boundary.work_id !== result.work_id)
31
+ return false;
32
+ if (boundary.run_id !== undefined && boundary.run_id !== input.runId || boundary.stage !== undefined && boundary.stage !== input.stage
33
+ || boundary.work_id !== undefined && input.workId && boundary.work_id !== input.workId)
34
+ return false;
35
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
36
+ const index = JSON.parse(run?.index_json ?? "null");
37
+ const stage = index?.stage_runs?.find(item => item.stage === input.stage);
38
+ if (!stage?.attempt)
39
+ return false;
40
+ const workId = input.workId ?? (typeof boundary.work_id === "string" ? boundary.work_id : null);
41
+ const rows = context.db.all(`SELECT DISTINCT w.work_id, w.status FROM works w JOIN work_sessions ws ON ws.work_id = w.work_id JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id JOIN stage_work_memberships m ON m.project_id = w.project_id AND m.run_id = w.run_id AND m.work_id = w.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.status IN ('running', 'completed') AND s.provider_session_id = ? AND m.stage = ? AND m.attempt = ? AND (? IS NULL OR w.work_id = ?) ${!workId && input.scope === "stage" ? "AND w.parent_work_id IS NULL" : ""}`, [input.projectId, input.runId, input.sessionId, input.stage, stage.attempt, workId, workId]);
42
+ if (rows.length !== 1 || boundary.work_id !== undefined && boundary.work_id !== rows[0].work_id)
43
+ return false;
44
+ if (boundary.outcome === "paused")
45
+ return rows[0].status === "paused" && stage.pause?.work_id === rows[0].work_id && stage.pause.id === boundary.pause?.id;
46
+ return boundary.outcome === "completed" && (input.scope === "work" ? rows[0].status === "completed" : stage.status === "done");
47
+ }
6
48
  export function providerChildFailure(child) {
7
49
  return child.provider_failure ?? { code: child.native_source ? "provider_capacity_transient" : "turn_interrupted", details: { native_source: child.native_source, provider_session_id: child.session_id, turn_id: child.terminal_turn_id, native_turn_id: child.terminal_turn_id, terminal_status: child.status, observed_at: child.terminal_observed_at ?? null, provider_error: child.provider_error, native_turn_items: child.native_turn_items } };
8
50
  }
@@ -237,9 +279,13 @@ export async function continueProviderCapacity(input) {
237
279
  const decide = async (observedOperationId = operationId) => {
238
280
  input.signal?.throwIfAborted();
239
281
  input.assertCurrent();
240
- const decision = await observe(async () => await input.decision?.() ?? "continue", observedOperationId);
282
+ const normalized = normalizeProviderFailure(error, input.sessionId);
283
+ const candidate = normalized?.request_proof.kind === "stream_request" ? normalized.primary_error.details?.recovered_error_boundary : undefined;
284
+ const decision = await observe(async () => await input.decision?.(candidate?.receipt) ?? "continue", observedOperationId);
241
285
  input.signal?.throwIfAborted();
242
286
  input.assertCurrent();
287
+ if (normalized?.request_proof.kind === "stream_request" && ["completed", "paused"].includes(decision) && !candidate?.receipt)
288
+ throw blocked("Stream refusal has no ordered evidence of recovery before the committed boundary");
243
289
  if (decision === "reconciliation_blocked")
244
290
  throw new AppError("capacity_continuation_blocked", "Native refusal cannot be reconciled with its durable owner boundary", 1, { primary_error: error, root_operation_id: rootId, provider_session_id: input.sessionId });
245
291
  if (decision === "continue" && input.deadline !== undefined && Date.now() >= input.deadline)
@@ -254,9 +300,11 @@ export async function continueProviderCapacity(input) {
254
300
  return null;
255
301
  throw error;
256
302
  }
257
- const completed = await decide();
258
- if (completed)
259
- return completed;
303
+ if (normalizeProviderFailure(error, input.sessionId)?.request_proof.kind !== "stream_request") {
304
+ const completed = await decide();
305
+ if (completed)
306
+ return completed;
307
+ }
260
308
  const prior = await observe(() => input.inspectOperation(operationId));
261
309
  const failure = prior.error;
262
310
  const confirmed = normalizeProviderFailure(failure, input.sessionId);
@@ -266,13 +314,14 @@ export async function continueProviderCapacity(input) {
266
314
  if (prior.primary_error)
267
315
  throw prior.primary_error;
268
316
  if (prior.operation_id !== operationId || prior.operation !== "session.prompt" || prior.session_id !== input.sessionId
269
- || prior.state !== "failed" || prior.primary_error || !turnId || !terminalProviderOverload(failure, input.sessionId, turnId)) {
317
+ || prior.state !== "failed" || prior.primary_error || !turnId || !terminalProviderOverload(failure, input.sessionId, turnId)
318
+ || confirmed?.request_proof.kind === "stream_request" && confirmed.request_proof.operation_id !== operationId) {
270
319
  throw blocked("Retained predecessor does not prove the exact terminal overloaded Turn");
271
320
  }
272
321
  const readiness = async () => {
273
322
  input.signal?.throwIfAborted();
274
323
  input.assertCurrent();
275
- const seen = await observe(() => input.inspectSession(turnId));
324
+ const seen = await observe(() => input.inspectSession(confirmed?.request_proof.kind === "stream_request" ? undefined : turnId));
276
325
  input.signal?.throwIfAborted();
277
326
  input.assertCurrent();
278
327
  // An unloaded exact-Turn snapshot is not conflicting tool evidence.
@@ -280,11 +329,23 @@ export async function continueProviderCapacity(input) {
280
329
  const freshItems = seen.native_turn_items;
281
330
  const items = (seen.terminal_turn_id === turnId && seen.terminal_status === "failed"
282
331
  && !(freshItems?.observed === false && freshItems.pending !== true) ? freshItems : confirmed?.native_turn_items);
332
+ if (confirmed?.request_proof.kind === "stream_request") {
333
+ const metadata = seen.terminal_metadata;
334
+ if (streamRequestKey(metadata?.request_proof) !== turnId)
335
+ return "unproven";
336
+ }
283
337
  return capacityReadiness(seen, input.sessionId, operationId, turnId, items, input.allowSettledDescendants);
284
338
  };
285
339
  const ready = await readiness();
286
340
  if (ready === "unproven")
287
341
  throw blocked("Native predecessor readiness is unproven; do not cancel or replay to manufacture proof");
342
+ if (ready === "ready") {
343
+ const completed = await decide();
344
+ if (completed)
345
+ return completed;
346
+ if (confirmed?.request_proof.kind === "stream_request" && confirmed.primary_error.details?.recovered_error_boundary)
347
+ throw blocked("Recovered native error does not match its current durable owner boundary");
348
+ }
288
349
  const native = normalizeProviderFailure(failure, input.sessionId, turnId);
289
350
  let refusal = refusals.find(item => item.ordinal === ordinal - 1);
290
351
  if (refusal && (refusal.turn_id !== turnId || refusal.session_id !== input.sessionId || refusal.policy !== CAPACITY_POLICY))
@@ -332,7 +393,7 @@ export async function continueProviderCapacity(input) {
332
393
  throw new AppError("capacity_owner_deadline_exceeded", "Continuation wait exceeds its original owner deadline", 1, { primary_error: failure, deadline: input.deadline ?? null });
333
394
  }
334
395
  if (!existing) {
335
- const intent = { predecessor: operationId, operation_id: nextId, turn_id: turnId, ordinal, prompt_sha256: promptSha, not_before: new Date(Date.now() + wait).toISOString(), policy: CAPACITY_POLICY, root_operation_id: rootId, session_id: input.sessionId, deadline: input.deadline ?? null, refusal };
396
+ const intent = { predecessor: operationId, operation_id: nextId, turn_id: turnId, request_key: native.request_key, request_proof: native.request_proof, ordinal, prompt_sha256: promptSha, not_before: new Date(Date.now() + wait).toISOString(), policy: CAPACITY_POLICY, root_operation_id: rootId, session_id: input.sessionId, deadline: input.deadline ?? null, refusal };
336
397
  input.retainIntent(intent);
337
398
  history.push(intent);
338
399
  }
@@ -74,7 +74,7 @@ export function captureRecoveryDatabase(source, output, projectId, runId, protoc
74
74
  const bootstrapReceipts = `EXISTS (SELECT 1 FROM run_bundle_authorities b JOIN runs r
75
75
  ON r.project_id=b.project_id AND r.id=b.run_id
76
76
  WHERE (b.project_id,b.run_id) IN (SELECT * FROM capture_scope)
77
- AND json_extract(r.index_json,'$.execution_bundle.flow_id')='mb-init'
77
+ AND json_extract(r.index_json,'$.execution_bundle.flow_id') IN ('mb-init','mb-upgrade')
78
78
  AND (audit_events.project_id IS NULL OR audit_events.project_id=b.project_id)
79
79
  AND (
80
80
  (event_type IN ('memory.init_git_policy','memory.init_release_selection','memory.init_startup_input','memory.init_startup_input_intent','memory.init_project_bootstrap_intent',
@@ -109,6 +109,33 @@ export function captureRecoveryDatabase(source, output, projectId, runId, protoc
109
109
  AND ((event_type='memory.bootstrap_ready' AND json_extract(payload_json,'$.schema_id')='dd-flow/memory-bootstrap-authority@1')
110
110
  OR (event_type='memory.bootstrap_readiness_intent' AND json_extract(payload_json,'$.schema_id')='dd-flow/memory-bootstrap-readiness-intent@1')))))`;
111
111
  rules.audit_events = `(${rules.audit_events}) OR (${bootstrapReceipts})`;
112
+ // Upgrade selection/storage bytes are request-owned historical evidence,
113
+ // not ambient Home configuration. Keep only the bundle's original scope.
114
+ const upgradeScope = `SELECT 1 FROM run_bundle_authorities b JOIN runs r
115
+ ON r.project_id=b.project_id AND r.id=b.run_id
116
+ WHERE (b.project_id,b.run_id) IN (SELECT * FROM capture_scope)
117
+ AND json_extract(r.index_json,'$.execution_bundle.flow_id')='mb-upgrade'`;
118
+ rules.runtime_config = `CASE WHEN key GLOB 'mb-upgrade.storage.*' THEN EXISTS (${upgradeScope}
119
+ AND key='mb-upgrade.storage.' || b.project_id || '.' || json_extract(b.admission_json,'$.executionScope.requestId')
120
+ AND json_extract(value_json,'$.project_id')=b.project_id
121
+ AND json_extract(value_json,'$.scope.requestId')=json_extract(b.admission_json,'$.executionScope.requestId')
122
+ AND json_extract(value_json,'$.scope.projectRoot')=json_extract(b.admission_json,'$.executionScope.projectRoot')
123
+ AND json_extract(value_json,'$.scope.workspaceRoot')=json_extract(b.admission_json,'$.executionScope.workspace')) ELSE 0 END`;
124
+ rules.audit_events = `(${rules.audit_events}) OR (event_type='memory.upgrade_storage_ready'
125
+ AND EXISTS (${upgradeScope} AND audit_events.project_id=b.project_id
126
+ AND json_extract(payload_json,'$.request_id')=json_extract(b.admission_json,'$.executionScope.requestId')
127
+ AND json_extract(payload_json,'$.project_root')=json_extract(b.admission_json,'$.executionScope.projectRoot')
128
+ AND json_extract(payload_json,'$.workspace_root')=json_extract(b.admission_json,'$.executionScope.workspace')))`;
129
+ const publication = `CASE WHEN key GLOB 'memory_upgrade_publication:*' THEN EXISTS (${upgradeScope}
130
+ AND json_extract(value_json,'$.binding.scope.project_id')=b.project_id
131
+ AND json_extract(value_json,'$.binding.scope.run_id')=b.run_id
132
+ AND json_extract(value_json,'$.binding.scope.stage')='upgrade') ELSE 0 END`;
133
+ rules.runtime_config = `(${rules.runtime_config}) OR (${publication})`;
134
+ rules.audit_events = `(${rules.audit_events}) OR (event_type IN ('memory.upgrade_publication_intent','memory.upgrade_publication_complete')
135
+ AND EXISTS (SELECT 1 FROM runtime_config WHERE ${publication}
136
+ AND key='memory_upgrade_publication:' || json_extract(audit_events.payload_json,'$.scope_sha256')
137
+ AND json_extract(value_json,'$.binding.scope.project_id')=audit_events.project_id
138
+ AND json_extract(value_json,'$.binding.scope.run_id')=json_extract(audit_events.payload_json,'$.run_id')))`;
112
139
  }
113
140
  // Batch and composition registration are portable RUN evidence, not a host lease. Keep their
114
141
  // independent receipt alongside the selected Works, never a neighboring
@@ -361,7 +388,7 @@ export function captureRecoveryDatabase(source, output, projectId, runId, protoc
361
388
  AND json_extract(payload_json,'$.readiness_sha256') NOT GLOB '*[^0-9a-f]*'
362
389
  AND EXISTS (SELECT 1 FROM runs r,json_each(r.index_json,'$.stage_runs') f,json_each(r.index_json,'$.stage_runs') v
363
390
  WHERE r.project_id=audit_events.project_id AND r.id=json_extract(audit_events.payload_json,'$.run_id')
364
- AND json_extract(r.index_json,'$.execution_bundle.flow_id')='mb-init'
391
+ AND json_extract(r.index_json,'$.execution_bundle.flow_id') IN ('mb-init','mb-upgrade')
365
392
  AND json_extract(r.index_json,'$.execution_bundle.manifest_sha256')=json_extract(audit_events.payload_json,'$.bundle_sha256')
366
393
  AND json_extract(f.value,'$.stage')='finish'
367
394
  AND json_extract(f.value,'$.attempt')=json_extract(audit_events.payload_json,'$.attempt')
@@ -379,12 +406,13 @@ export function captureRecoveryDatabase(source, output, projectId, runId, protoc
379
406
  AND json_extract(payload_json,'$.evidence_sha256') NOT GLOB '*[^0-9a-f]*'
380
407
  AND EXISTS (SELECT 1 FROM runs r,json_each(r.index_json,'$.stage_runs') f
381
408
  WHERE r.project_id=audit_events.project_id AND r.id=json_extract(audit_events.payload_json,'$.run_id')
382
- AND json_extract(r.index_json,'$.execution_bundle.flow_id')='mb-init'
409
+ AND json_extract(r.index_json,'$.execution_bundle.flow_id') IN ('mb-init','mb-upgrade')
383
410
  AND json_extract(r.index_json,'$.execution_bundle.manifest_sha256')=json_extract(audit_events.payload_json,'$.bundle_sha256')
384
411
  AND json_extract(f.value,'$.stage')='finish' AND json_extract(f.value,'$.status')='done'
385
412
  AND json_extract(f.value,'$.attempt')=json_extract(audit_events.payload_json,'$.attempt')
386
413
  AND json_extract(f.value,'$.input_sha256')=json_extract(audit_events.payload_json,'$.input_sha256')
387
- AND json_extract(f.value,'$.data_schema_id')='dd-flow/mb-init-finish-result@1'
414
+ AND json_extract(f.value,'$.data_schema_id')=CASE json_extract(r.index_json,'$.execution_bundle.flow_id')
415
+ WHEN 'mb-upgrade' THEN 'dd-flow/mb-upgrade-stage-result@1' ELSE 'dd-flow/mb-init-finish-result@1' END
388
416
  AND json_extract(f.value,'$.data_sha256')=json_extract(audit_events.payload_json,'$.result_sha256'))`;
389
417
  rules.audit_events = `(${rules.audit_events}) OR (${ownProject} AND (${memoryFinishAcceptance}))`;
390
418
  const descriptorRunSettlementReceipt = `event_type = 'flow_run.descriptor_run_settlement'
@@ -28,15 +28,4 @@ export function ensureRepairIntent(context, input, create) {
28
28
  return { schema_id: "dd-flow/continuation@1", kind: "repair_required", project_id: input.projectId, run_id: input.runId, stage: input.stage, attempt, identity, work_id: workId };
29
29
  });
30
30
  }
31
- export function isRegisteredRepair(context, value, scope) {
32
- if (!value || typeof value !== "object")
33
- return false;
34
- const receipt = value;
35
- if (receipt.schema_id !== "dd-flow/continuation@1" || receipt.kind !== "repair_required")
36
- return false;
37
- if (scope && (receipt.project_id !== scope.projectId || receipt.run_id !== scope.runId))
38
- return false;
39
- if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'repair_intents'"))
40
- return false;
41
- return Boolean(context.db.get("SELECT 1 FROM repair_intents i JOIN works w ON w.work_id = i.work_id AND w.project_id = i.project_id AND w.run_id = i.run_id WHERE i.identity = ? AND i.project_id = ? AND i.run_id = ? AND i.stage = ? AND i.attempt = ? AND i.work_id = ?", [receipt.identity, receipt.project_id, receipt.run_id, receipt.stage, receipt.attempt, receipt.work_id]));
42
- }
31
+ export { isRegisteredRepair } from "./repair-receipt.js";
@@ -0,0 +1,13 @@
1
+ /** Read-only receipt validation must not load repair execution on hook admission. */
2
+ export function isRegisteredRepair(context, value, scope) {
3
+ if (!value || typeof value !== "object")
4
+ return false;
5
+ const receipt = value;
6
+ if (receipt.schema_id !== "dd-flow/continuation@1" || receipt.kind !== "repair_required")
7
+ return false;
8
+ if (scope && (receipt.project_id !== scope.projectId || receipt.run_id !== scope.runId))
9
+ return false;
10
+ if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'repair_intents'"))
11
+ return false;
12
+ return Boolean(context.db.get("SELECT 1 FROM repair_intents i JOIN works w ON w.work_id = i.work_id AND w.project_id = i.project_id AND w.run_id = i.run_id WHERE i.identity = ? AND i.project_id = ? AND i.run_id = ? AND i.stage = ? AND i.attempt = ? AND i.work_id = ?", [receipt.identity, receipt.project_id, receipt.run_id, receipt.stage, receipt.attempt, receipt.work_id]));
13
+ }
@@ -3,14 +3,14 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
5
  import { spawn } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
7
6
  import { setTimeout as delay } from "node:timers/promises";
8
7
  import { AppError, errorRecord } from "../shared/errors.js";
9
8
  import { resolveProjectRoot } from "../storage/paths.js";
10
9
  import { requireProjectByRoot } from "./projects.js";
11
10
  import { getResourceDatabase } from "../storage/database.js";
12
- import { assertManagedProcessLease, confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessRuntimeHome, processIsAlive, processStartedAt, processTreeIsAlive, registerManagedProcess, resourceHome } from "./managed-processes.js";
11
+ import { assertManagedProcessLease, confirmManagedProcess, prepareManagedProcessConfirmation, finishManagedProcess, renewManagedProcess, managedProcessRuntimeHome, processIsAlive, processStartedAt, processTreeIsAlive, registerManagedProcess, resourceHome } from "./managed-processes.js";
13
12
  import { managedLeaseMonitor } from "./managed-lease-monitor.js";
13
+ import { physicalStartup } from "./physical-startup.js";
14
14
  import { interruptControlledSessions, observeControlledSessions, recordRunControlIntent, runControlStatus } from "./run-control.js";
15
15
  import { recoveryGuard, recordRecoveryCapture, sealRunRecovery } from "./run-recovery.js";
16
16
  import { createEvalRunSnapshot, verifyRecoverySnapshot } from "./eval-snapshots.js";
@@ -70,9 +70,7 @@ async function launchControlWorker(context, input) {
70
70
  if (!claimed && !reclaimControlWorker(context, input, project.id, token, home))
71
71
  return;
72
72
  try {
73
- const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
74
- if (!fs.existsSync(entry))
75
- throw new AppError("harness_runtime_unbuilt", "Build the CLI before launching its detached control owner", 1);
73
+ const entry = (await import("./engines.js")).boundRunEngineEntrypoint(context, { projectRoot: project.root, runId: input.runId });
76
74
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
77
75
  const log = path.join(directory, "control.log");
78
76
  const output = fs.openSync(log, "a", 0o600);
@@ -228,18 +226,19 @@ export async function serveRunControl(context, input) {
228
226
  throw new AppError("run_control_owner_mismatch", "Control registration is not authorized", 1);
229
227
  // Physical identity is committed with the sole RUN claim, before the worker
230
228
  // registers itself in the separate resource store or performs any dispatch.
231
- const workerBirth = processStartedAt(process.pid);
232
- context.db.writeTransaction(() => {
233
- const claimed = context.db.run("UPDATE run_control_workers SET status = 'running', owner_pid = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [process.pid, context.now(), input.controlId, worker.owner_token]);
234
- if (claimed.changes !== 1)
235
- throw new AppError("run_control_already_owned", "A control owner has already claimed this request", 1);
236
- const now = context.now();
237
- context.db.run("INSERT INTO run_control_operations (operation_id, control_id, target_json, status, receipt_json, created_at, updated_at) VALUES (?, ?, ?, 'completed', ?, ?, ?)", [`${launchOperationId(input.controlId, worker.owner_token)}:worker-claim`, input.controlId, JSON.stringify({ kind: "control_worker_self_claim" }), JSON.stringify({ pid: process.pid, pid_started_at: workerBirth }), now, now]);
238
- });
229
+ const startup = physicalStartup(context.databaseDeadlineAt);
230
+ const assertStartupCurrent = () => {
231
+ const current = context.db.get("SELECT * FROM run_control_workers WHERE control_id = ?", [input.controlId]);
232
+ const currentIntent = context.db.get("SELECT * FROM run_controls WHERE control_id = ?", [input.controlId]);
233
+ const guard = recoveryGuard(context, project.id, input.runId);
234
+ if (!current || current.owner_token !== worker.owner_token || current.owner_pid !== worker.owner_pid || current.status !== worker.status || current.process_id !== worker.process_id || current.process_lease_token !== worker.process_lease_token || !["requested", "settled"].includes(currentIntent?.status ?? "") || guard?.recovery_id !== intent.recovery_id || !["draining", "sealed"].includes(guard.status))
235
+ throw new AppError("run_control_owner_mismatch", "Control startup no longer owns this request", 1);
236
+ };
239
237
  const resourceContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: worker.resource_home } };
240
238
  const lease = managedLeaseMonitor(attemptMs => renewManagedProcess({ ...resourceContext, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: worker.process_id, leaseToken: worker.process_lease_token }));
241
239
  let timerFailure;
242
240
  let primaryFailure;
241
+ let claimedByThisServe = false;
243
242
  const assertControlCurrent = () => {
244
243
  const latest = recoveryGuard(context, project.id, input.runId);
245
244
  if (latest?.recovery_id !== intent.recovery_id || !["draining", "sealed"].includes(latest.status))
@@ -250,8 +249,9 @@ export async function serveRunControl(context, input) {
250
249
  assertManagedProcessLease(resourceContext, { id: worker.process_id, leaseToken: worker.process_lease_token, pid: process.pid });
251
250
  assertControlCurrent();
252
251
  } };
252
+ let admitted = false;
253
253
  const heartbeat = setInterval(() => {
254
- if (!worker.process_id)
254
+ if (!admitted)
255
255
  return;
256
256
  lease.background(assertControlCurrent, error => { timerFailure = error; });
257
257
  }, 10_000);
@@ -259,17 +259,33 @@ export async function serveRunControl(context, input) {
259
259
  let cleanupFailure;
260
260
  const result = await (async () => {
261
261
  try {
262
+ const workerBirth = (await startup.birth(assertStartupCurrent)).pid_started_at;
263
+ context.db.writeTransaction(() => {
264
+ assertStartupCurrent();
265
+ const claimed = context.db.run("UPDATE run_control_workers SET status = 'running', owner_pid = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [process.pid, context.now(), input.controlId, worker.owner_token]);
266
+ if (claimed.changes !== 1)
267
+ throw new AppError("run_control_already_owned", "A control owner has already claimed this request", 1);
268
+ const now = context.now();
269
+ context.db.run("INSERT INTO run_control_operations (operation_id, control_id, target_json, status, receipt_json, created_at, updated_at) VALUES (?, ?, ?, 'completed', ?, ?, ?)", [`${launchOperationId(input.controlId, worker.owner_token)}:worker-claim`, input.controlId, JSON.stringify({ kind: "control_worker_self_claim" }), JSON.stringify({ pid: process.pid, pid_started_at: workerBirth }), now, now]);
270
+ });
271
+ worker.owner_pid = process.pid;
272
+ worker.status = "running";
273
+ claimedByThisServe = true;
262
274
  if (!worker.process_id) {
263
275
  const log = path.join(worker.state_dir, "control.log");
264
276
  const record = registerManagedProcess(resourceContext, { kind: "run-control", ownerId: input.controlId, projectId: project.id, runId: input.runId, operationId: `control:${input.controlId}`, uniqueActiveOperation: true, ownerPid: process.pid, stdoutPath: log, stderrPath: log, metadata: { control_id: input.controlId } });
265
277
  worker.process_id = record.id;
266
278
  worker.process_lease_token = record.lease_token;
267
- context.db.run("UPDATE run_control_workers SET process_id = ?, process_lease_token = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND owner_pid = ? AND status = 'running'", [record.id, record.lease_token, context.now(), input.controlId, worker.owner_token, process.pid]);
279
+ if (context.db.run("UPDATE run_control_workers SET process_id = ?, process_lease_token = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND owner_pid = ? AND status = 'running'", [record.id, record.lease_token, context.now(), input.controlId, worker.owner_token, process.pid]).changes !== 1)
280
+ throw new AppError("run_control_owner_mismatch", "Control owner changed during registration", 1);
268
281
  }
269
282
  if (!worker.process_lease_token)
270
283
  throw new AppError("run_control_owner_mismatch", "Control owner has no resource lease", 1);
271
- const confirmed = confirmManagedProcess(resourceContext, { id: worker.process_id, leaseToken: worker.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
284
+ const confirmation = { id: worker.process_id, leaseToken: worker.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid };
285
+ const originalLease = prepareManagedProcessConfirmation(resourceContext, confirmation).lease_expires_at;
286
+ const confirmed = await startup.observe(() => confirmManagedProcess(resourceContext, confirmation), assertStartupCurrent, originalLease);
272
287
  lease.retainAuthority(confirmed.lease_expires_at);
288
+ admitted = true;
273
289
  let priorObservation = null;
274
290
  try {
275
291
  priorObservation = worker.snapshot_json ? JSON.parse(worker.snapshot_json) : null;
@@ -380,7 +396,7 @@ export async function serveRunControl(context, input) {
380
396
  catch (error) {
381
397
  primaryFailure = error;
382
398
  try {
383
- context.db.run("UPDATE run_control_workers SET status = 'failed', error_json = ?, updated_at = ? WHERE control_id = ? AND owner_token = ?", [JSON.stringify(failure(error)), context.now(), input.controlId, worker.owner_token]);
399
+ context.db.run("UPDATE run_control_workers SET status = 'failed', error_json = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND owner_pid IS ? AND process_id IS ? AND process_lease_token IS ? AND status = ?", [JSON.stringify(failure(error)), context.now(), input.controlId, worker.owner_token, claimedByThisServe ? process.pid : null, worker.process_id, worker.process_lease_token, claimedByThisServe ? "running" : "starting"]);
384
400
  }
385
401
  catch (secondary) {
386
402
  process.stderr.write(`${JSON.stringify({ primary: failure(error), phase: "persist_control_failure", secondary: failure(secondary) })}\n`);
@@ -391,7 +407,8 @@ export async function serveRunControl(context, input) {
391
407
  lease.close();
392
408
  clearInterval(heartbeat);
393
409
  try {
394
- if (worker.process_id && worker.process_lease_token)
410
+ const current = claimedByThisServe && context.db.get("SELECT * FROM run_control_workers WHERE control_id = ?", [input.controlId]);
411
+ if (current && current.owner_token === worker.owner_token && current.owner_pid === process.pid && current.process_id === worker.process_id && current.process_lease_token === worker.process_lease_token && worker.process_id && worker.process_lease_token)
395
412
  finishManagedProcess(resourceContext, { id: worker.process_id, leaseToken: worker.process_lease_token, state: "stopped", reason: "control_owner_exited" });
396
413
  }
397
414
  catch (secondary) {
@@ -2,7 +2,6 @@ import fs from "node:fs";
2
2
  import crypto from "node:crypto";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
5
  import { AppError } from "../shared/errors.js";
7
6
  import { getResourceDatabase } from "../storage/database.js";
8
7
  import { finishManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, processStartedAt, processTreeIsAlive, registerManagedProcess } from "./managed-processes.js";
@@ -133,11 +132,10 @@ function prepareSpawn(context, row) {
133
132
  });
134
133
  }
135
134
  /** The physical child records its own birth before resource registration. */
136
- export function claimControllerProcess(context, row) {
135
+ export function claimControllerProcess(context, row, owner = birth()) {
137
136
  const launch = launchRecord(context, row);
138
137
  if (!launch && (!row.process_id || !row.process_lease_token))
139
138
  throw new AppError("controller_owner_mismatch", "Managed registration is not authorized", 1);
140
- const owner = birth();
141
139
  context.db.writeTransaction(() => {
142
140
  // Recovery fences are reconciled by serve's retained failure boundary;
143
141
  // only terminal admission must reject before claiming a physical owner.
@@ -173,10 +171,8 @@ export async function spawnController(context, row) {
173
171
  const manifest = JSON.parse(row.manifest_json);
174
172
  const run = requireRun(context, row.project_id, row.run_id);
175
173
  const controllerId = row.controller_id, root = row.state_dir, token = row.owner_token;
176
- const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
177
174
  try {
178
- if (!fs.existsSync(entry))
179
- throw new AppError("harness_runtime_unbuilt", "Build the CLI before launching its detached controller", 1);
175
+ const entry = (await import("./engines.js")).boundRunEngineEntrypoint(context, { projectRoot: run.project_root, runId: run.id });
180
176
  const output = fs.openSync(path.join(root, "controller.log"), "a", 0o600);
181
177
  try {
182
178
  const child = spawn(process.execPath, [entry, "run", "drive", "serve", "--run", run.id, "--project-root", run.project_root, "--controller-id", controllerId, "--json"], { cwd: run.workspace_root, detached: true, stdio: ["ignore", output, output], env: { ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_CONTROLLER_TOKEN: token } });