@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.81

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 (138) hide show
  1. package/CHANGELOG.md +402 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +300 -0
  5. package/dist/cli/help.js +81 -9
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +115 -0
  8. package/dist/cli/run-cli.js +1344 -376
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  11. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  12. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  13. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  14. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  15. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  16. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  17. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  18. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  19. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  21. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  22. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  23. package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
  24. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  25. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  26. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  27. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  28. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  29. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  30. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  32. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  33. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  34. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  36. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  37. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  38. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  39. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  40. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  41. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  42. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  43. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  44. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  45. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  46. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  47. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  48. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  49. package/dist/schemas/agent-profile.schema.json +1 -1
  50. package/dist/schemas/code-review-result.schema.json +2 -2
  51. package/dist/schemas/code-work-batch.schema.json +4 -3
  52. package/dist/schemas/code-work-result.schema.json +4 -4
  53. package/dist/schemas/plan-review-result.schema.json +2 -2
  54. package/dist/schemas/run-control-receipt.schema.json +99 -0
  55. package/dist/schemas/run-control-request.schema.json +36 -0
  56. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  57. package/dist/services/canon.js +11 -8
  58. package/dist/services/cleanup.js +57 -13
  59. package/dist/services/cli-operation-classifier.js +22 -24
  60. package/dist/services/code-checks.js +543 -96
  61. package/dist/services/codex-hook-delivery.js +28 -0
  62. package/dist/services/config.js +11 -8
  63. package/dist/services/controller-fanout.js +116 -0
  64. package/dist/services/dashboard.js +103 -31
  65. package/dist/services/engines.js +121 -76
  66. package/dist/services/eval-snapshots.js +750 -73
  67. package/dist/services/execution-policy.js +170 -0
  68. package/dist/services/external-work-launch.js +110 -0
  69. package/dist/services/harness-adapter.js +147 -24
  70. package/dist/services/harness-config.js +2 -0
  71. package/dist/services/hooks.js +575 -293
  72. package/dist/services/lanes.js +61 -53
  73. package/dist/services/lifecycle-command.js +87 -7
  74. package/dist/services/lifecycle-invocations.js +835 -0
  75. package/dist/services/managed-daemon-binding.js +38 -0
  76. package/dist/services/managed-processes.js +283 -27
  77. package/dist/services/merge-queue.js +179 -104
  78. package/dist/services/merge-server.js +73 -28
  79. package/dist/services/migrations.js +14 -9
  80. package/dist/services/native-daemon-history.js +49 -0
  81. package/dist/services/native-session-control.js +39 -0
  82. package/dist/services/plan-runtime.js +19 -8
  83. package/dist/services/plans.js +28 -25
  84. package/dist/services/projects.js +18 -5
  85. package/dist/services/prompts.js +23 -16
  86. package/dist/services/protocols.js +64 -20
  87. package/dist/services/recovery-observation-budget.js +61 -0
  88. package/dist/services/recovery-snapshot-database.js +107 -0
  89. package/dist/services/run-control-receipt.js +56 -0
  90. package/dist/services/run-control-worker.js +363 -0
  91. package/dist/services/run-control.js +876 -0
  92. package/dist/services/run-controller-adapter.js +222 -0
  93. package/dist/services/run-controller-capture.js +134 -0
  94. package/dist/services/run-controller-process.js +189 -0
  95. package/dist/services/run-controller-recovery.js +222 -0
  96. package/dist/services/run-controller-state.js +36 -0
  97. package/dist/services/run-controller.js +779 -0
  98. package/dist/services/run-engine-bindings.js +1 -1
  99. package/dist/services/run-fork.js +138 -0
  100. package/dist/services/run-observations.js +112 -0
  101. package/dist/services/run-recovery-runtime.js +69 -0
  102. package/dist/services/run-recovery.js +343 -0
  103. package/dist/services/runs.js +368 -74
  104. package/dist/services/runtime-budget.js +434 -0
  105. package/dist/services/runtime-scope-capture.js +50 -0
  106. package/dist/services/runtime-scope-control.js +450 -0
  107. package/dist/services/runtime-scope-resume.js +565 -0
  108. package/dist/services/runtime-scope-stop.js +99 -0
  109. package/dist/services/runtime-scope-worker.js +243 -0
  110. package/dist/services/runtime-service.js +97 -0
  111. package/dist/services/schema-validation.js +10 -8
  112. package/dist/services/sessions.js +35 -69
  113. package/dist/services/stage-blocker.js +17 -7
  114. package/dist/services/stage-context.js +44 -18
  115. package/dist/services/stage-lifecycle.js +99 -102
  116. package/dist/services/stage-pause.js +69 -28
  117. package/dist/services/usage.js +37 -93
  118. package/dist/services/vnext-code-review.js +189 -85
  119. package/dist/services/vnext-code.js +257 -243
  120. package/dist/services/vnext-execution-profile.js +5 -3
  121. package/dist/services/vnext-fanout.js +134 -19
  122. package/dist/services/vnext-merge.js +285 -102
  123. package/dist/services/vnext-plan-review.js +148 -94
  124. package/dist/services/vnext-plan.js +141 -72
  125. package/dist/services/vnext-protocolize.js +129 -56
  126. package/dist/services/vnext-specify.js +87 -64
  127. package/dist/services/work-registry.js +701 -164
  128. package/dist/services/workspace-bootstrap.js +76 -0
  129. package/dist/services/worktrees.js +85 -35
  130. package/dist/shared/entity-references.js +8 -0
  131. package/dist/shared/errors.js +12 -0
  132. package/dist/storage/database.js +444 -32
  133. package/dist/storage/paths.js +17 -4
  134. package/dist/storage/work-references.js +23 -0
  135. package/dist/storage/writer-contract.js +77 -0
  136. package/dist/storage/writer-migration.js +102 -0
  137. package/package.json +11 -4
  138. package/tools/repair-paused-run-status.mjs +59 -0
@@ -0,0 +1,434 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { getResourceDatabase, withResourceReadSnapshot } from "../storage/database.js";
7
+ import { AppError } from "../shared/errors.js";
8
+ import { harnessKey } from "./harness-config.js";
9
+ import { managedProcessBinding, resourceHome, processIsAlive } from "./managed-processes.js";
10
+ import { recoveryRuntimeAdmission } from "./run-recovery-runtime.js";
11
+ import { prepareTextFile, prepareJsonFile, prepareError } from "../cli/input-preparation.js";
12
+ const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
13
+ export function validateRuntimeScopeManifest(value, scopeId) {
14
+ const item = value;
15
+ if (!item || typeof item !== "object" || Array.isArray(item) || Object.keys(item).some(key => !["schema_id", "scope_id", "manifest_path", "manifest_sha256", "execution_ids"].includes(key)) || item.schema_id !== "dd-flow/runtime-scope-manifest@1" || item.scope_id !== scopeId || typeof item.manifest_path !== "string" || !path.isAbsolute(item.manifest_path) || typeof item.manifest_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(item.manifest_sha256) || !Array.isArray(item.execution_ids) || item.execution_ids.some(id => typeof id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id)) || new Set(item.execution_ids).size !== item.execution_ids.length)
16
+ throw new AppError("runtime_scope_manifest_invalid", "Scope manifest requires frozen source identity and unique execution IDs", 2);
17
+ const bytes = prepareTextFile(item.manifest_path, "scope manifest").text;
18
+ if (digest(bytes) !== item.manifest_sha256)
19
+ throw new AppError("runtime_scope_manifest_changed", "EVAL manifest no longer matches its frozen identity", 1);
20
+ let source;
21
+ try {
22
+ source = JSON.parse(bytes);
23
+ }
24
+ catch {
25
+ throw new AppError("runtime_scope_manifest_corrupt", "Retained EVAL manifest contains invalid JSON", 1);
26
+ }
27
+ if (!source || typeof source !== "object" || Array.isArray(source))
28
+ throw new AppError("runtime_scope_manifest_corrupt", "Retained EVAL manifest must be an object", 1);
29
+ if (source.run_id !== scopeId || !Array.isArray(source.executions) || JSON.stringify(source.executions.map(execution => execution?.id)) !== JSON.stringify(item.execution_ids))
30
+ throw new AppError("runtime_scope_manifest_invalid", "Scope execution inventory does not match the retained EVAL manifest", 2);
31
+ return { schema_id: item.schema_id, scope_id: scopeId, manifest_path: item.manifest_path, manifest_sha256: item.manifest_sha256, execution_ids: [...item.execution_ids] };
32
+ }
33
+ export function publicRuntimeScopeWorker(worker) {
34
+ return { status: worker.status, generation: worker.generation, process_id: worker.process_id ?? null, owner_pid: worker.owner?.pid ?? null, error: worker.error ?? null, snapshot: worker.snapshot ?? null };
35
+ }
36
+ /** Captured manifests are runtime authority, not a fresh CLI argument. */
37
+ export function validateRetainedRuntimeScopeManifest(value, scopeId) {
38
+ try {
39
+ return validateRuntimeScopeManifest(value, scopeId);
40
+ }
41
+ catch (error) {
42
+ if (error instanceof AppError && error.exitCode === 2)
43
+ throw new AppError("runtime_scope_manifest_corrupt", "Retained scope manifest is invalid or unavailable", 1, { scope_id: scopeId, cause_code: error.code });
44
+ throw error;
45
+ }
46
+ }
47
+ /** Prepared native owners cannot send the first recovery Turn until the scope
48
+ * arbiter publishes their exact release. Fence disappearance alone is not ACK. */
49
+ export async function waitForRuntimeScopeRecovery(context, input, assertOwner) {
50
+ const db = getResourceDatabase(resourceHome(context));
51
+ for (;;) {
52
+ assertOwner();
53
+ const snapshot = (() => {
54
+ db.exec("SAVEPOINT scope_recovery_release");
55
+ try {
56
+ return {
57
+ fence: db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [input.scopeId]),
58
+ latest: db.get("SELECT MAX(json_extract(metadata_json, '$.generation')) AS generation FROM managed_resources WHERE resource_kind = 'scope-control-request' AND owner_id = ?", [input.scopeId]),
59
+ control: db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [input.scopeId]),
60
+ release: db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-release' AND resource_key = ?", [digest(JSON.stringify([input.scopeId, input.generation]))])
61
+ };
62
+ }
63
+ finally {
64
+ db.exec("RELEASE scope_recovery_release");
65
+ }
66
+ })();
67
+ if (snapshot.fence)
68
+ throw new AppError("runtime_scope_stopped", "Terminal cancellation superseded scope recovery", 1);
69
+ const { latest, control: row, release } = snapshot;
70
+ if (latest?.generation !== input.generation)
71
+ throw new AppError("runtime_scope_control_superseded", "Prepared recovery belongs to a retired EVAL generation", 1);
72
+ if (row) {
73
+ if (JSON.parse(row.metadata_json).generation !== input.generation)
74
+ throw new AppError("runtime_scope_control_superseded", "A newer EVAL control superseded prepared recovery", 1);
75
+ await delay(100);
76
+ continue;
77
+ }
78
+ const receipt = release ? JSON.parse(release.metadata_json) : null;
79
+ if (receipt?.scope_id !== input.scopeId || receipt.generation !== input.generation || !Array.isArray(receipt.run_operations) || !receipt.run_operations.some(item => item.runtime_home === context.ddFlowHome && item.controller_id === input.controllerId && item.operation_id === input.operationId))
80
+ throw new AppError("runtime_scope_release_unproven", "Prepared recovery has no matching atomic EVAL release", 1);
81
+ assertOwner();
82
+ return;
83
+ }
84
+ }
85
+ /** Admission intent only. A persisted request is not physical settlement.
86
+ * The resource registry arbitrates all runtime homes in the selected EVAL. */
87
+ export function prepareRuntimeScopeControl(scopeId, requestId, mode, manifest, context) {
88
+ prepareRuntimeScopeFence(scopeId, requestId);
89
+ if (!["pause", "stop"].includes(mode))
90
+ throw new AppError("runtime_budget_invalid", "Scope control requires pause or stop", 2);
91
+ const file = context && path.join(resourceHome(context), "runtime.sqlite");
92
+ if (file && fs.existsSync(file)) {
93
+ const db = new DatabaseSync(file, { readOnly: true });
94
+ try {
95
+ db.exec("PRAGMA busy_timeout = 4000");
96
+ // Another process may have created the file but not its first schema yet.
97
+ // No table means no retained receipt; execute rechecks after initialization.
98
+ const ready = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'managed_resources'").get();
99
+ const prior = ready ? db.prepare("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-request' AND resource_key = ?").get(digest(JSON.stringify([scopeId, requestId]))) : undefined;
100
+ if (prior)
101
+ return { receipt: scopeControlReceipt(prior.metadata_json, mode) };
102
+ }
103
+ finally {
104
+ db.close();
105
+ }
106
+ }
107
+ return { manifest: manifest === undefined ? undefined : validateRuntimeScopeManifest(manifest, scopeId) };
108
+ }
109
+ function scopeControlReceipt(json, mode) {
110
+ const receipt = JSON.parse(json);
111
+ if (receipt.requested_mode !== mode)
112
+ throw new AppError("runtime_scope_control_conflict", "Scope request identity was already used for another mode", 1);
113
+ return receipt;
114
+ }
115
+ export function requestRuntimeScopeControl(context, scopeId, requestId, mode, manifest, prepared = prepareRuntimeScopeControl(scopeId, requestId, mode, manifest, context)) {
116
+ if (prepared.receipt)
117
+ return prepared.receipt;
118
+ const declaration = prepared.manifest;
119
+ const db = getResourceDatabase(resourceHome(context));
120
+ const requestKey = digest(JSON.stringify([scopeId, requestId]));
121
+ db.beginWriteTransaction();
122
+ try {
123
+ const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-request' AND resource_key = ?", [requestKey]);
124
+ if (prior) {
125
+ const receipt = scopeControlReceipt(prior.metadata_json, mode);
126
+ db.exec("COMMIT");
127
+ return receipt;
128
+ }
129
+ if (declaration !== undefined) {
130
+ const payload = JSON.stringify(declaration);
131
+ const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-manifest' AND resource_key = ?", [scopeId]);
132
+ if (saved && saved.metadata_json !== payload)
133
+ throw new AppError("runtime_scope_manifest_changed", "Scope already belongs to another frozen EVAL manifest", 1);
134
+ if (!saved)
135
+ db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-manifest',?,?,?,?,NULL,?,?,?)", [scopeId, scopeId, declaration.manifest_sha256, context.now(), payload, context.now(), context.now()]);
136
+ }
137
+ if (db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]))
138
+ throw new AppError("runtime_scope_stopped", "Terminal EVAL cancellation cannot become recoverable control", 1);
139
+ const current = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
140
+ const head = current ? JSON.parse(current.metadata_json) : null;
141
+ if (head?.requested_mode === "stop" && mode === "pause")
142
+ throw new AppError("runtime_scope_control_conflict", "Pause cannot weaken an existing stop", 1);
143
+ const retained = db.get("SELECT MAX(json_extract(metadata_json, '$.generation')) AS generation FROM managed_resources WHERE resource_kind = 'scope-control-request' AND owner_id = ?", [scopeId]);
144
+ const generation = Math.max(head?.generation ?? 0, retained?.generation ?? 0) + 1;
145
+ if (!Number.isSafeInteger(generation))
146
+ throw new AppError("runtime_scope_control_conflict", "Scope generation is exhausted", 1);
147
+ const now = context.now();
148
+ const receipt = { scope_id: scopeId, request_id: requestId, generation, requested_mode: mode, status: "requested", dispatch_blocked: true, requested_at: now };
149
+ const payload = JSON.stringify(receipt);
150
+ db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-request',?,?,?,?,NULL,?,?,?)", [requestKey, scopeId, requestKey, now, payload, now, now]);
151
+ db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control',?,?,?,?,NULL,?,?,?) ON CONFLICT(resource_kind,resource_key) DO UPDATE SET lease_token = excluded.lease_token, metadata_json = excluded.metadata_json, updated_at = excluded.updated_at", [scopeId, scopeId, requestKey, now, payload, now, now]);
152
+ db.exec("COMMIT");
153
+ return receipt;
154
+ }
155
+ catch (error) {
156
+ db.exec("ROLLBACK");
157
+ throw error;
158
+ }
159
+ }
160
+ function assertScopeAdmission(context, scopeId) {
161
+ const db = getResourceDatabase(resourceHome(context));
162
+ if (db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]))
163
+ throw new AppError("runtime_scope_stopped", "This runtime scope no longer admits productive dispatch", 1, { scope_id: scopeId });
164
+ const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
165
+ if (control)
166
+ throw new AppError("runtime_scope_controlled", "This runtime scope is under operator control", 1, { control: JSON.parse(control.metadata_json) });
167
+ }
168
+ /** Local observers never inherit a Subject RUN, Session or provider turn slot. */
169
+ export function scopeLocalProcessRole(record) {
170
+ const role = record.kind === "eval-baseline" ? "probe" : record.kind === "eval-observer" ? "observer" : null;
171
+ const metadata = JSON.parse(record.metadata_json);
172
+ if (!role || metadata.role !== role || metadata.state_dir || !record.operation_id)
173
+ return null;
174
+ if (role === "observer" && (record.owner_id !== metadata.budget?.scope_id || !record.operation_id.startsWith(`${record.owner_id}:observer:`) || !/^[a-f0-9]{64}$/.test(record.operation_id.slice(`${record.owner_id}:observer:`.length)) || record.project_id || record.run_id || record.work_id || record.check_id))
175
+ return null;
176
+ return role;
177
+ }
178
+ /** Local checks and EVAL observers share the scope fence, not provider capacity. */
179
+ export function admitRuntimeCheck(context, id, leaseToken) {
180
+ if (typeof id !== "string" || !id.trim() || typeof leaseToken !== "string" || !leaseToken.trim())
181
+ throw new AppError("validation", "Check admission requires a process ID and lease token", 2);
182
+ const db = getResourceDatabase(resourceHome(context), "read_existing");
183
+ try {
184
+ // One statement observes the owner and both admission fences together,
185
+ // without taking a writer lock or initializing an absent registry.
186
+ const record = db.get(`SELECT p.*,
187
+ EXISTS(SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = json_extract(p.metadata_json, '$.budget.scope_id')) AS scope_fenced,
188
+ (SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = json_extract(p.metadata_json, '$.budget.scope_id')) AS scope_control
189
+ FROM managed_processes p WHERE p.id = ?`, [id]);
190
+ if (!record || record.lease_token !== leaseToken || !["eval-baseline", "eval-observer", "check"].includes(record.kind) || record.state !== "running" || !processIsAlive(record))
191
+ throw new AppError("process_ownership_unknown", "Local process requires its confirmed live owner", 1);
192
+ let budget;
193
+ try {
194
+ const metadata = JSON.parse(record.metadata_json);
195
+ const retained = parseRuntimeBudget(metadata.budget ? JSON.stringify(metadata.budget) : undefined);
196
+ if (!retained || (record.kind !== "check" ? scopeLocalProcessRole(record) === null : metadata.role !== "check" || !record.check_id))
197
+ throw new Error("Missing retained EVAL scope or operation");
198
+ budget = retained;
199
+ }
200
+ catch (error) {
201
+ throw new AppError("runtime_artifact_invalid", "Local process has invalid retained scope or operation metadata", 1, { process_id: record.id, cause: String(error) });
202
+ }
203
+ if (record.scope_fenced)
204
+ throw new AppError("runtime_scope_stopped", "This runtime scope no longer admits productive dispatch", 1, { scope_id: budget.scope_id });
205
+ if (record.scope_control)
206
+ throw new AppError("runtime_scope_controlled", "This runtime scope is under operator control", 1, { control: JSON.parse(record.scope_control) });
207
+ return { admitted: true, process_id: record.id, scope_id: budget.scope_id };
208
+ }
209
+ finally {
210
+ db.close?.();
211
+ }
212
+ }
213
+ /** Scope selection uses retained registration, never a current profile or PID search. */
214
+ export function runtimeBudgetStatus(context, scopeId) {
215
+ parseRuntimeBudget(JSON.stringify({ schema_id: "dd-flow/runtime-budget@1", scope_id: scopeId, per_harness: {} }));
216
+ return withResourceReadSnapshot(resourceHome(context), db => {
217
+ const fence = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
218
+ const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
219
+ const processes = db.all("SELECT * FROM managed_processes WHERE json_extract(metadata_json, '$.budget.scope_id') = ? ORDER BY id", [scopeId]);
220
+ const manifest = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-manifest' AND resource_key = ?", [scopeId]);
221
+ const resumeRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [scopeId]);
222
+ const resume = resumeRow ? JSON.parse(resumeRow.metadata_json) : null;
223
+ const releaseRow = resume?.status === "released" ? db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-release' AND resource_key = ?", [digest(JSON.stringify([scopeId, resume.generation]))]) : null;
224
+ const release = releaseRow ? JSON.parse(releaseRow.metadata_json) : null;
225
+ const lastRequest = release ? db.get("SELECT MAX(json_extract(metadata_json, '$.generation')) AS generation FROM managed_resources WHERE resource_kind = 'scope-control-request' AND owner_id = ?", [scopeId]) : null;
226
+ const releaseCurrent = Boolean(release && !fence && !control && release.scope_id === scopeId && release.generation === resume.generation && release.request_id === resume.request_id && lastRequest?.generation === resume.generation);
227
+ // Retained turns remain obligations even after their process exits or its
228
+ // lease expires. Only explicit reconciliation releases them.
229
+ const providerTurns = db.all("SELECT resource_key, process_id, metadata_json FROM managed_resources WHERE resource_kind = 'provider-turn' AND json_extract(metadata_json, '$.scope_id') = ? ORDER BY resource_key", [scopeId]).map(row => ({ ...JSON.parse(row.metadata_json), turn_id: row.resource_key, process_id: row.process_id }));
230
+ const intent = control ? JSON.parse(control.metadata_json) : null;
231
+ const workerGeneration = intent?.generation ?? (releaseCurrent ? release.generation : null);
232
+ const worker = workerGeneration ? db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ?", [digest(JSON.stringify([scopeId, workerGeneration]))]) : null;
233
+ const drain = intent ? db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-drain' AND resource_key = ?", [digest(JSON.stringify([scopeId, intent.generation]))]) : null;
234
+ const result = { scope_id: scopeId, dispatch_blocked: Boolean(fence || control), fence: fence ? JSON.parse(fence.metadata_json) : null, control: intent, worker: worker ? publicRuntimeScopeWorker(JSON.parse(worker.metadata_json)) : null, drain: drain ? JSON.parse(drain.metadata_json) : null, manifest: manifest ? JSON.parse(manifest.metadata_json) : null, resume: resume ? { ...resume, dispatch_blocked: Boolean(fence || control), current: resume.status === "released" ? releaseCurrent : !fence && intent?.generation === resume.generation } : null, release: release ? { ...release, current: releaseCurrent } : null, processes, provider_turns: providerTurns };
235
+ return result;
236
+ });
237
+ }
238
+ /** Persist admission shutdown independently of client/process lease expiry.
239
+ * This is a dispatch fence, not evidence that the existing native tree stopped. */
240
+ export function prepareRuntimeScopeFence(scopeId, requestId) {
241
+ parseRuntimeBudget(JSON.stringify({ schema_id: "dd-flow/runtime-budget@1", scope_id: scopeId, per_harness: {} }));
242
+ if (typeof requestId !== "string" || !requestId.trim() || requestId.length > 256)
243
+ throw new AppError("runtime_budget_invalid", "Scope control requires a nonempty request identity of at most 256 characters", 2);
244
+ }
245
+ export function fenceRuntimeBudget(context, scopeId, requestId) {
246
+ prepareRuntimeScopeFence(scopeId, requestId);
247
+ const db = getResourceDatabase(resourceHome(context));
248
+ db.beginWriteTransaction();
249
+ try {
250
+ const existing = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
251
+ const now = context.now();
252
+ const receipt = existing ? JSON.parse(existing.metadata_json)
253
+ : { scope_id: scopeId, request_id: requestId, dispatch_blocked: true, fenced_at: now };
254
+ if (!existing)
255
+ db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('provider-fence',?,?,?,?,NULL,?,?,?)", [scopeId, scopeId, digest(requestId), now, JSON.stringify(receipt), now, now]);
256
+ db.exec("COMMIT");
257
+ return receipt;
258
+ }
259
+ catch (error) {
260
+ db.exec("ROLLBACK");
261
+ throw error;
262
+ }
263
+ }
264
+ export function runtimeBudgetForRun(context, projectId, runId) {
265
+ const retained = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_controllers'")
266
+ ? context.db.get("SELECT manifest_json FROM run_controllers WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC, controller_id DESC LIMIT 1", [projectId, runId]) : undefined;
267
+ if (!retained)
268
+ return parseRuntimeBudget(context.env.DD_FLOW_RUNTIME_BUDGET);
269
+ const manifest = JSON.parse(retained.manifest_json);
270
+ return parseRuntimeBudget(manifest.budget ? JSON.stringify(manifest.budget) : undefined);
271
+ }
272
+ export function parseRuntimeBudget(raw) {
273
+ if (raw === undefined)
274
+ return undefined;
275
+ let value;
276
+ try {
277
+ value = JSON.parse(raw);
278
+ }
279
+ catch {
280
+ throw new AppError("runtime_budget_invalid", "Runtime budget must be one JSON object", 2);
281
+ }
282
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some(key => !["schema_id", "scope_id", "per_harness"].includes(key)) || value.schema_id !== "dd-flow/runtime-budget@1"
283
+ || typeof value.scope_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value.scope_id) || !value.per_harness || typeof value.per_harness !== "object" || Array.isArray(value.per_harness))
284
+ throw new AppError("runtime_budget_invalid", "Runtime budget requires a stable scope and per-harness limits", 2);
285
+ const limits = {};
286
+ for (const [name, limit] of Object.entries(value.per_harness).sort(([left], [right]) => left.localeCompare(right))) {
287
+ const key = harnessKey(name);
288
+ if (name !== key || !Number.isSafeInteger(limit) || limit < 1 || limit > 1024)
289
+ throw new AppError("runtime_budget_invalid", "Budget keys must be canonical harness names and limits positive integers up to 1024", 2);
290
+ limits[key] = limit;
291
+ }
292
+ return { schema_id: "dd-flow/runtime-budget@1", scope_id: value.scope_id, per_harness: limits };
293
+ }
294
+ /** A host-registry transaction arbitrates all roots in this experiment, not
295
+ * just the CLI clients currently observing them. Expiry never frees a slot. */
296
+ export function reserveProviderTurn(context, record, input) {
297
+ const metadata = JSON.parse(record.metadata_json);
298
+ const budget = parseRuntimeBudget(metadata.budget === undefined ? undefined : JSON.stringify(metadata.budget));
299
+ if (!budget)
300
+ return { admitted: true };
301
+ if (!metadata.harness || !metadata.state_dir || !input.operationId || !["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(input.operation))
302
+ throw new AppError("runtime_budget_invalid", "Budget admission requires a frozen harness and productive operation", 2);
303
+ const harness = harnessKey(metadata.harness), limit = budget.per_harness[harness] ?? 1;
304
+ const db = getResourceDatabase(resourceHome(context));
305
+ const key = digest(JSON.stringify([record.id, input.operationId]));
306
+ const policy = JSON.stringify(budget), now = context.now();
307
+ db.beginWriteTransaction();
308
+ try {
309
+ const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [record.id]);
310
+ if (current?.lease_token !== record.lease_token || current.state !== "running")
311
+ throw new AppError("process_ownership_unknown", "Budget owner changed before admission", 1);
312
+ try {
313
+ assertScopeAdmission(context, budget.scope_id);
314
+ }
315
+ catch (error) {
316
+ if (!(error instanceof AppError) || error.code !== "runtime_scope_controlled" || !scopeRecoveryLoadAllowed(context, budget.scope_id, record, input))
317
+ throw error;
318
+ }
319
+ const frozen = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-budget' AND resource_key = ?", [budget.scope_id]);
320
+ if (frozen && frozen.metadata_json !== policy)
321
+ throw new AppError("runtime_budget_conflict", "This experiment already froze different concurrency limits", 1);
322
+ if (!frozen)
323
+ db.run("INSERT INTO managed_resources (resource_kind, resource_key, owner_id, lease_token, lease_expires_at, process_id, metadata_json, created_at, updated_at) VALUES ('provider-budget', ?, ?, ?, ?, NULL, ?, ?, ?)", [budget.scope_id, budget.scope_id, digest(policy), now, policy, now, now]);
324
+ const prior = db.get("SELECT resource_key, metadata_json, lease_token FROM managed_resources WHERE resource_kind = 'provider-turn' AND resource_key = ?", [key]);
325
+ if (prior) {
326
+ if (prior.lease_token !== record.lease_token)
327
+ throw new AppError("process_ownership_unknown", "Retained budget claim belongs to another physical owner", 1);
328
+ db.exec("COMMIT");
329
+ return { admitted: true, budget_scope: budget.scope_id, reused: true };
330
+ }
331
+ const active = db.get("SELECT count(*) AS count FROM managed_resources WHERE resource_kind = 'provider-turn' AND json_extract(metadata_json, '$.scope_id') = ? AND json_extract(metadata_json, '$.harness') = ?", [budget.scope_id, harness]).count;
332
+ if (active >= limit) {
333
+ db.exec("COMMIT");
334
+ return { admitted: false, reason: "provider_capacity", budget_scope: budget.scope_id, harness, limit, active };
335
+ }
336
+ db.run("INSERT INTO managed_resources (resource_kind, resource_key, owner_id, lease_token, lease_expires_at, process_id, metadata_json, created_at, updated_at) VALUES ('provider-turn', ?, ?, ?, ?, ?, ?, ?, ?)", [key, record.owner_id, record.lease_token, record.lease_expires_at, record.id, JSON.stringify({ scope_id: budget.scope_id, harness, operation_id: input.operationId, operation: input.operation, session_id: input.sessionId ?? null, state_dir: metadata.state_dir }), now, now]);
337
+ db.exec("COMMIT");
338
+ return { admitted: true, budget_scope: budget.scope_id, harness, limit, active: active + 1 };
339
+ }
340
+ catch (error) {
341
+ db.exec("ROLLBACK");
342
+ throw error;
343
+ }
344
+ }
345
+ function scopeRecoveryLoadAllowed(context, scopeId, record, input) {
346
+ const metadata = JSON.parse(record.metadata_json);
347
+ if (input.operation !== "session.resume" || !input.daemonId || !metadata.state_dir || metadata.dd_flow_home !== context.ddFlowHome || !processIsAlive(record))
348
+ return false;
349
+ const db = getResourceDatabase(resourceHome(context));
350
+ const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
351
+ const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [scopeId]);
352
+ const intent = row ? JSON.parse(row.metadata_json) : null;
353
+ if (!control || intent?.scope_id !== scopeId || intent.generation !== JSON.parse(control.metadata_json).generation || intent.status !== "preparing" || !Array.isArray(intent.runs))
354
+ return false;
355
+ if (!record.run_id) {
356
+ const loadRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native-load' AND resource_key = ?", [input.operationId]);
357
+ const load = loadRow ? JSON.parse(loadRow.metadata_json) : null;
358
+ if (!load || load.scope_id !== scopeId || load.generation !== intent.generation || load.request_id !== intent.request_id || load.status !== "dispatching" || load.process_id !== record.id || load.daemon_id !== input.daemonId || load.session_id !== input.sessionId)
359
+ return false;
360
+ const source = intent.native?.find((item) => item.process_id === load.source_process_id);
361
+ if (!source || source.state_dir !== metadata.state_dir || source.runtime_owner?.owner_id !== record.owner_id || !source.sessions?.some((item) => item.session_id === input.sessionId))
362
+ return false;
363
+ const parentRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [digest(JSON.stringify([scopeId, intent.generation, source.process_id]))]);
364
+ const parent = parentRow ? JSON.parse(parentRow.metadata_json) : null;
365
+ return parent?.status === "restarted" && parent.restart_receipt?.daemon_id === input.daemonId && JSON.stringify(parent.replacement_binding) === JSON.stringify(managedProcessBinding(record, resourceHome(context)));
366
+ }
367
+ if (!record.project_id)
368
+ return false;
369
+ const source = intent.runs.find((run) => run.runtime_home === context.ddFlowHome && run.project_id === record.project_id && run.run_id === record.run_id);
370
+ if (!source || !context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'run_controller_operations'"))
371
+ return false;
372
+ const prepared = context.db.get(`SELECT 1 FROM run_controller_operations o
373
+ JOIN run_controllers c ON c.controller_id = o.controller_id
374
+ JOIN run_controls ctl ON ctl.control_id = ? AND ctl.project_id = c.project_id AND ctl.run_id = c.run_id
375
+ JOIN run_recovery_guards g ON g.recovery_id = ctl.recovery_id
376
+ WHERE c.controller_id = ? AND c.project_id = ? AND c.run_id = ? AND c.generation = g.generation
377
+ AND o.generation = g.generation AND o.status = 'accepted' AND ctl.status = 'resuming' AND g.status = 'resuming'
378
+ AND ctl.recovery_id = ? AND json_extract(o.input_json, '$.action') = 'control_resume'
379
+ AND json_extract(o.input_json, '$.request_id') = ? AND json_extract(o.input_json, '$.control_id') = ctl.control_id`, [source.control_id, record.owner_id, record.project_id, record.run_id, source.recovery_id, source.resume_request_id]);
380
+ return Boolean(prepared) && recoveryRuntimeAdmission(context, record, { stateDir: metadata.state_dir, daemonId: input.daemonId, operationId: input.operationId, operation: input.operation, nativeSessionId: input.sessionId });
381
+ }
382
+ export function prepareProviderTurnRelease(input) {
383
+ if (Boolean(input.operationId) === Boolean(input.observationFile))
384
+ return prepareError(new AppError("usage", "Provide exactly one of --operation-id or --observation-file", 2));
385
+ if (!input.observationFile)
386
+ return { observation: null };
387
+ // This file is a retained daemon receipt, not model-authored input. Broken
388
+ // receipt syntax/schema is infrastructure failure, never a retryable typo.
389
+ let observed;
390
+ try {
391
+ observed = prepareJsonFile(input.observationFile, "budget observation").value;
392
+ }
393
+ catch {
394
+ throw new AppError("runtime_budget_evidence_invalid", "Cannot read valid JSON from the daemon budget observation", 1);
395
+ }
396
+ if (!observed || typeof observed !== "object" || Array.isArray(observed) || typeof observed.daemon_id !== "string" || typeof observed.session_id !== "string" || !observed.session_id || typeof observed.settled !== "boolean" || !Array.isArray(observed.operation_ids) || observed.operation_ids.some(id => typeof id !== "string" || !id))
397
+ throw new AppError("runtime_budget_evidence_invalid", "Budget observation has an invalid receipt shape", 1);
398
+ return { observation: observed };
399
+ }
400
+ export function releaseProviderTurns(context, record, input, prepared = prepareProviderTurnRelease(input)) {
401
+ const metadata = JSON.parse(record.metadata_json);
402
+ if (!metadata.state_dir)
403
+ throw new AppError("process_ownership_unknown", "Budget release has no retained state directory", 1);
404
+ const state = JSON.parse(fs.readFileSync(path.join(metadata.state_dir, "daemon.json"), "utf8"));
405
+ if ((state.daemon_id ?? state.config?.daemonId) !== input.daemonId || state.resource_process?.id !== record.id || state.resource_process.lease_token !== record.lease_token)
406
+ throw new AppError("process_ownership_unknown", "Budget release belongs to another daemon generation", 1);
407
+ let sessionId = null;
408
+ let operationIds;
409
+ if (input.observationFile) {
410
+ const parent = path.resolve(metadata.state_dir, "budget-observations");
411
+ if (path.dirname(path.resolve(input.observationFile)) !== parent)
412
+ throw new AppError("runtime_budget_evidence_invalid", "Budget observation is outside its daemon directory", 2);
413
+ const observed = prepared.observation;
414
+ if (!observed || observed.daemon_id !== input.daemonId || observed.settled !== true)
415
+ throw new AppError("runtime_budget_evidence_invalid", "Budget release requires fresh settled Session evidence", 1);
416
+ sessionId = observed.session_id;
417
+ operationIds = observed.operation_ids;
418
+ }
419
+ else {
420
+ if (!input.operationId)
421
+ throw new AppError("runtime_budget_evidence_invalid", "Budget release requires an operation or observation", 2);
422
+ const directory = path.join(metadata.state_dir, "operations", digest(input.operationId));
423
+ const requested = JSON.parse(fs.readFileSync(path.join(directory, "requested.json"), "utf8"));
424
+ const result = JSON.parse(fs.readFileSync(path.join(directory, "result.json"), "utf8"));
425
+ if (requested.daemon_id !== input.daemonId || requested.operation_id !== input.operationId || !["session.create", "session.fork"].includes(requested.operation ?? "") || result.state !== "completed")
426
+ throw new AppError("runtime_budget_evidence_invalid", "Only a completed Session creation can release without a tree observation", 1);
427
+ operationIds = [input.operationId];
428
+ }
429
+ const db = getResourceDatabase(resourceHome(context));
430
+ let released = 0;
431
+ for (const operationId of operationIds)
432
+ released += db.run("DELETE FROM managed_resources WHERE resource_kind = 'provider-turn' AND process_id = ? AND lease_token = ? AND json_extract(metadata_json, '$.operation_id') = ? AND (? IS NULL OR json_extract(metadata_json, '$.session_id') = ?)", [record.id, record.lease_token, operationId, sessionId, sessionId]).changes;
433
+ return { ok: true, released };
434
+ }
@@ -0,0 +1,50 @@
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 { getResourceDatabase } from "../storage/database.js";
6
+ import { resourceHome } from "./managed-processes.js";
7
+ import { validateRetainedRuntimeScopeManifest } from "./runtime-budget.js";
8
+ /** Archive a complete append-only prefix, not a claim that no later evidence
9
+ * can arrive. The caller holds the registry transaction; never take the EVAL
10
+ * journal lock here (its writer may itself be waiting for the registry).
11
+ */
12
+ export function captureScopeJournal(context, control, manifest) {
13
+ const source = path.join(path.dirname(manifest.manifest_path), "events.jsonl");
14
+ const bytes = fs.readFileSync(source), prefix = bytes.subarray(0, bytes.lastIndexOf(10) + 1), text = prefix.toString("utf8");
15
+ if (!prefix.length || !Buffer.from(text).equals(prefix))
16
+ throw new AppError("runtime_scope_journal_invalid", "EVAL journal has no complete valid UTF-8 prefix", 1);
17
+ const events = text.slice(0, -1).split("\n").map(line => {
18
+ try {
19
+ return JSON.parse(line);
20
+ }
21
+ catch {
22
+ throw new AppError("runtime_scope_journal_invalid", "EVAL journal contains a malformed complete record", 1);
23
+ }
24
+ });
25
+ let request;
26
+ for (const [index, event] of events.entries()) {
27
+ if (!event || event.specversion !== "1.0" || typeof event.id !== "string" || !event.id || event.runid !== control.scope_id || typeof event.type !== "string" || event.data?.sequence !== index + 1)
28
+ throw new AppError("runtime_scope_journal_invalid", "EVAL journal scope or sequence is inconsistent", 1);
29
+ if (event.type === "dev.dd.eval.cancel_requested")
30
+ throw new AppError("runtime_scope_control_superseded", "Terminal cancellation supersedes recoverable capture", 1);
31
+ if (event.type === "dev.dd.eval.control.requested")
32
+ request = event;
33
+ }
34
+ if (request?.data?.request_id !== control.request_id || request.data.mode !== control.requested_mode)
35
+ throw new AppError("runtime_scope_control_superseded", "EVAL journal belongs to another control request", 1);
36
+ const manifestBytes = fs.readFileSync(manifest.manifest_path, "utf8");
37
+ validateRetainedRuntimeScopeManifest(manifest, control.scope_id);
38
+ const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
39
+ if (hash(manifestBytes) !== manifest.manifest_sha256 || !fs.readFileSync(source).subarray(0, prefix.length).equals(prefix))
40
+ throw new AppError("runtime_scope_journal_changed", "EVAL source changed during capture", 1);
41
+ const sha256 = hash(prefix), key = hash(JSON.stringify([control.scope_id, control.generation, manifest.manifest_sha256, sha256]));
42
+ const payload = JSON.stringify({ scope_id: control.scope_id, generation: control.generation, manifest, manifest_bytes: manifestBytes, journal_source: source, journal_bytes: text });
43
+ const registry = getResourceDatabase(resourceHome(context));
44
+ const prior = registry.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [key]);
45
+ if (prior && prior.metadata_json !== payload)
46
+ throw new AppError("runtime_scope_capture_changed", "Retained EVAL capture no longer matches its content identity", 1);
47
+ if (!prior)
48
+ registry.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-capture',?,?,?,?,NULL,?,?,?)", [key, control.scope_id, key, context.now(), payload, context.now(), context.now()]);
49
+ return { artifact_key: key, source, sha256, byte_length: prefix.length, event_count: events.length, request_sequence: request.data.sequence, consistency: "append_only_prefix", trailing_bytes: bytes.length - prefix.length };
50
+ }