@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.74

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 (121) hide show
  1. package/CHANGELOG.md +396 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/help.js +81 -9
  5. package/dist/cli/run-cli.js +442 -46
  6. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  7. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  8. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  9. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  10. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  11. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  12. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  13. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  14. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  15. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
  16. package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
  17. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  18. package/dist/harness-runtime/lib/dd-droid.mjs +470 -0
  19. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  20. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  21. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  22. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  23. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  24. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  25. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  26. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  27. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  28. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  29. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  30. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  31. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  32. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  33. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  34. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  35. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  36. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  37. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  38. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  39. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  40. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  41. package/dist/runtime/context.js +3 -1
  42. package/dist/schemas/agent-profile.schema.json +1 -1
  43. package/dist/schemas/code-review-result.schema.json +2 -2
  44. package/dist/schemas/code-work-batch.schema.json +4 -3
  45. package/dist/schemas/code-work-result.schema.json +4 -4
  46. package/dist/schemas/harness-config.schema.json +23 -0
  47. package/dist/schemas/plan-review-result.schema.json +2 -2
  48. package/dist/schemas/run-control-receipt.schema.json +99 -0
  49. package/dist/schemas/run-control-request.schema.json +36 -0
  50. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  51. package/dist/services/cleanup.js +8 -5
  52. package/dist/services/cli-operation-classifier.js +11 -1
  53. package/dist/services/code-checks.js +543 -96
  54. package/dist/services/controller-fanout.js +120 -0
  55. package/dist/services/dashboard.js +9 -0
  56. package/dist/services/engines.js +50 -19
  57. package/dist/services/eval-snapshots.js +639 -60
  58. package/dist/services/execution-policy.js +177 -0
  59. package/dist/services/external-work-launch.js +110 -0
  60. package/dist/services/harness-adapter.js +147 -24
  61. package/dist/services/harness-config.js +68 -0
  62. package/dist/services/hooks.js +413 -168
  63. package/dist/services/lanes.js +1 -1
  64. package/dist/services/lifecycle-command.js +74 -6
  65. package/dist/services/lifecycle-invocations.js +637 -0
  66. package/dist/services/managed-daemon-binding.js +38 -0
  67. package/dist/services/managed-processes.js +138 -22
  68. package/dist/services/merge-queue.js +6 -6
  69. package/dist/services/merge-server.js +75 -29
  70. package/dist/services/migrations.js +1 -1
  71. package/dist/services/native-daemon-history.js +49 -0
  72. package/dist/services/native-session-control.js +39 -0
  73. package/dist/services/projects.js +7 -3
  74. package/dist/services/prompts.js +4 -2
  75. package/dist/services/protocols.js +11 -2
  76. package/dist/services/recovery-observation-budget.js +61 -0
  77. package/dist/services/recovery-snapshot-database.js +107 -0
  78. package/dist/services/run-control-receipt.js +56 -0
  79. package/dist/services/run-control-worker.js +363 -0
  80. package/dist/services/run-control.js +876 -0
  81. package/dist/services/run-controller-adapter.js +211 -0
  82. package/dist/services/run-controller-capture.js +134 -0
  83. package/dist/services/run-controller-process.js +189 -0
  84. package/dist/services/run-controller-recovery.js +222 -0
  85. package/dist/services/run-controller-state.js +31 -0
  86. package/dist/services/run-controller.js +733 -0
  87. package/dist/services/run-engine-bindings.js +20 -62
  88. package/dist/services/run-fork.js +124 -0
  89. package/dist/services/run-observations.js +112 -0
  90. package/dist/services/run-recovery-runtime.js +69 -0
  91. package/dist/services/run-recovery.js +327 -0
  92. package/dist/services/runs.js +280 -40
  93. package/dist/services/runtime-budget.js +362 -0
  94. package/dist/services/runtime-scope-capture.js +50 -0
  95. package/dist/services/runtime-scope-control.js +450 -0
  96. package/dist/services/runtime-scope-resume.js +543 -0
  97. package/dist/services/runtime-scope-stop.js +99 -0
  98. package/dist/services/runtime-scope-worker.js +210 -0
  99. package/dist/services/runtime-service.js +96 -0
  100. package/dist/services/schema-validation.js +12 -12
  101. package/dist/services/stage-context.js +2 -2
  102. package/dist/services/stage-lifecycle.js +32 -10
  103. package/dist/services/stage-pause.js +25 -13
  104. package/dist/services/usage.js +37 -93
  105. package/dist/services/vnext-code-review.js +149 -89
  106. package/dist/services/vnext-code.js +223 -215
  107. package/dist/services/vnext-execution-profile.js +5 -3
  108. package/dist/services/vnext-fanout.js +135 -21
  109. package/dist/services/vnext-merge.js +258 -89
  110. package/dist/services/vnext-plan-review.js +75 -64
  111. package/dist/services/vnext-plan.js +77 -29
  112. package/dist/services/vnext-protocolize.js +21 -14
  113. package/dist/services/vnext-specify.js +26 -16
  114. package/dist/services/work-registry.js +592 -154
  115. package/dist/services/workspace-bootstrap.js +76 -0
  116. package/dist/shared/errors.js +12 -0
  117. package/dist/storage/database.js +341 -31
  118. package/dist/storage/writer-contract.js +77 -0
  119. package/dist/storage/writer-migration.js +102 -0
  120. package/package.json +20 -13
  121. package/tools/repair-paused-run-status.mjs +59 -0
@@ -0,0 +1,362 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { getResourceDatabase } from "../storage/database.js";
6
+ import { AppError } from "../shared/errors.js";
7
+ import { harnessKey } from "./harness-config.js";
8
+ import { managedProcessBinding, resourceHome, processIsAlive } from "./managed-processes.js";
9
+ import { recoveryRuntimeAdmission } from "./run-recovery-runtime.js";
10
+ const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
11
+ export function validateRuntimeScopeManifest(value, scopeId) {
12
+ const item = value;
13
+ 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)
14
+ throw new AppError("runtime_scope_manifest_invalid", "Scope manifest requires frozen source identity and unique execution IDs", 2);
15
+ const bytes = fs.readFileSync(item.manifest_path, "utf8");
16
+ if (digest(bytes) !== item.manifest_sha256)
17
+ throw new AppError("runtime_scope_manifest_changed", "EVAL manifest no longer matches its frozen identity", 1);
18
+ const source = JSON.parse(bytes);
19
+ if (source.run_id !== scopeId || !Array.isArray(source.executions) || JSON.stringify(source.executions.map(execution => execution?.id)) !== JSON.stringify(item.execution_ids))
20
+ throw new AppError("runtime_scope_manifest_invalid", "Scope execution inventory does not match the retained EVAL manifest", 2);
21
+ return { schema_id: item.schema_id, scope_id: scopeId, manifest_path: item.manifest_path, manifest_sha256: item.manifest_sha256, execution_ids: item.execution_ids };
22
+ }
23
+ export function publicRuntimeScopeWorker(worker) {
24
+ 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 };
25
+ }
26
+ /** Prepared native owners cannot send the first recovery Turn until the scope
27
+ * arbiter publishes their exact release. Fence disappearance alone is not ACK. */
28
+ export async function waitForRuntimeScopeRecovery(context, input, assertOwner) {
29
+ const db = getResourceDatabase(resourceHome(context));
30
+ for (;;) {
31
+ assertOwner();
32
+ const snapshot = (() => {
33
+ db.exec("SAVEPOINT scope_recovery_release");
34
+ try {
35
+ return {
36
+ fence: db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [input.scopeId]),
37
+ 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]),
38
+ control: db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [input.scopeId]),
39
+ 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]))])
40
+ };
41
+ }
42
+ finally {
43
+ db.exec("RELEASE scope_recovery_release");
44
+ }
45
+ })();
46
+ if (snapshot.fence)
47
+ throw new AppError("runtime_scope_stopped", "Terminal cancellation superseded scope recovery", 1);
48
+ const { latest, control: row, release } = snapshot;
49
+ if (latest?.generation !== input.generation)
50
+ throw new AppError("runtime_scope_control_superseded", "Prepared recovery belongs to a retired EVAL generation", 1);
51
+ if (row) {
52
+ if (JSON.parse(row.metadata_json).generation !== input.generation)
53
+ throw new AppError("runtime_scope_control_superseded", "A newer EVAL control superseded prepared recovery", 1);
54
+ await delay(100);
55
+ continue;
56
+ }
57
+ const receipt = release ? JSON.parse(release.metadata_json) : null;
58
+ 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))
59
+ throw new AppError("runtime_scope_release_unproven", "Prepared recovery has no matching atomic EVAL release", 1);
60
+ assertOwner();
61
+ return;
62
+ }
63
+ }
64
+ /** Admission intent only. A persisted request is not physical settlement.
65
+ * The resource registry arbitrates all runtime homes in the selected EVAL. */
66
+ export function requestRuntimeScopeControl(context, scopeId, requestId, mode, manifest) {
67
+ parseRuntimeBudget(JSON.stringify({ schema_id: "dd-flow/runtime-budget@1", scope_id: scopeId, per_harness: {} }));
68
+ if (!requestId || requestId.length > 256 || !["pause", "stop"].includes(mode))
69
+ throw new AppError("runtime_budget_invalid", "Scope control requires a bounded request identity and pause or stop", 2);
70
+ const db = getResourceDatabase(resourceHome(context));
71
+ const requestKey = digest(JSON.stringify([scopeId, requestId]));
72
+ db.beginWriteTransaction();
73
+ try {
74
+ if (manifest !== undefined) {
75
+ const declaration = validateRuntimeScopeManifest(manifest, scopeId), payload = JSON.stringify(declaration);
76
+ const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-manifest' AND resource_key = ?", [scopeId]);
77
+ if (saved && saved.metadata_json !== payload)
78
+ throw new AppError("runtime_scope_manifest_changed", "Scope already belongs to another frozen EVAL manifest", 1);
79
+ if (!saved)
80
+ 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()]);
81
+ }
82
+ const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-request' AND resource_key = ?", [requestKey]);
83
+ if (prior) {
84
+ const receipt = JSON.parse(prior.metadata_json);
85
+ if (receipt.requested_mode !== mode)
86
+ throw new AppError("runtime_scope_control_conflict", "Scope request identity was already used for another mode", 1);
87
+ db.exec("COMMIT");
88
+ return receipt;
89
+ }
90
+ if (db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]))
91
+ throw new AppError("runtime_scope_stopped", "Terminal EVAL cancellation cannot become recoverable control", 1);
92
+ const current = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
93
+ const head = current ? JSON.parse(current.metadata_json) : null;
94
+ if (head?.requested_mode === "stop" && mode === "pause")
95
+ throw new AppError("runtime_scope_control_conflict", "Pause cannot weaken an existing stop", 1);
96
+ 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]);
97
+ const generation = Math.max(head?.generation ?? 0, retained?.generation ?? 0) + 1;
98
+ if (!Number.isSafeInteger(generation))
99
+ throw new AppError("runtime_scope_control_conflict", "Scope generation is exhausted", 1);
100
+ const now = context.now();
101
+ const receipt = { scope_id: scopeId, request_id: requestId, generation, requested_mode: mode, status: "requested", dispatch_blocked: true, requested_at: now };
102
+ const payload = JSON.stringify(receipt);
103
+ 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]);
104
+ 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]);
105
+ db.exec("COMMIT");
106
+ return receipt;
107
+ }
108
+ catch (error) {
109
+ db.exec("ROLLBACK");
110
+ throw error;
111
+ }
112
+ }
113
+ function assertScopeAdmission(context, scopeId) {
114
+ const db = getResourceDatabase(resourceHome(context));
115
+ if (db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]))
116
+ throw new AppError("runtime_scope_stopped", "This runtime scope no longer admits productive dispatch", 1, { scope_id: scopeId });
117
+ const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
118
+ if (control)
119
+ throw new AppError("runtime_scope_controlled", "This runtime scope is under operator control", 1, { control: JSON.parse(control.metadata_json) });
120
+ }
121
+ /** Local observers never inherit a Subject RUN, Session or provider turn slot. */
122
+ export function scopeLocalProcessRole(record) {
123
+ const role = record.kind === "eval-baseline" ? "probe" : record.kind === "eval-observer" ? "observer" : null;
124
+ const metadata = JSON.parse(record.metadata_json);
125
+ if (!role || metadata.role !== role || metadata.state_dir || !record.operation_id)
126
+ return null;
127
+ 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))
128
+ return null;
129
+ return role;
130
+ }
131
+ /** Local checks and EVAL observers share the scope fence, not provider capacity. */
132
+ export function admitRuntimeCheck(context, id, leaseToken) {
133
+ const db = getResourceDatabase(resourceHome(context));
134
+ db.beginWriteTransaction();
135
+ try {
136
+ const record = db.get("SELECT * FROM managed_processes WHERE id = ?", [id]);
137
+ if (!record || record.lease_token !== leaseToken || !["eval-baseline", "eval-observer", "check"].includes(record.kind) || record.state !== "running" || !processIsAlive(record))
138
+ throw new AppError("process_ownership_unknown", "Local process requires its confirmed live owner", 1);
139
+ const metadata = JSON.parse(record.metadata_json);
140
+ const budget = parseRuntimeBudget(metadata.budget ? JSON.stringify(metadata.budget) : undefined);
141
+ if (!budget || (record.kind !== "check" ? scopeLocalProcessRole(record) === null : metadata.role !== "check" || !record.check_id))
142
+ throw new AppError("runtime_budget_invalid", "Local process requires a retained EVAL scope and operation", 2);
143
+ assertScopeAdmission(context, budget.scope_id);
144
+ db.exec("COMMIT");
145
+ return { admitted: true, process_id: record.id, scope_id: budget.scope_id };
146
+ }
147
+ catch (error) {
148
+ db.exec("ROLLBACK");
149
+ throw error;
150
+ }
151
+ }
152
+ /** Scope selection uses retained registration, never a current profile or PID search. */
153
+ export function runtimeBudgetStatus(context, scopeId) {
154
+ parseRuntimeBudget(JSON.stringify({ schema_id: "dd-flow/runtime-budget@1", scope_id: scopeId, per_harness: {} }));
155
+ const db = getResourceDatabase(resourceHome(context));
156
+ db.exec("SAVEPOINT runtime_scope_status");
157
+ try {
158
+ const fence = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
159
+ const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
160
+ const processes = db.all("SELECT * FROM managed_processes WHERE json_extract(metadata_json, '$.budget.scope_id') = ? ORDER BY id", [scopeId]);
161
+ const manifest = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-manifest' AND resource_key = ?", [scopeId]);
162
+ const resumeRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [scopeId]);
163
+ const resume = resumeRow ? JSON.parse(resumeRow.metadata_json) : null;
164
+ 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;
165
+ const release = releaseRow ? JSON.parse(releaseRow.metadata_json) : null;
166
+ 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;
167
+ const releaseCurrent = Boolean(release && !fence && !control && release.scope_id === scopeId && release.generation === resume.generation && release.request_id === resume.request_id && lastRequest?.generation === resume.generation);
168
+ // Retained turns remain obligations even after their process exits or its
169
+ // lease expires. Only explicit reconciliation releases them.
170
+ 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 }));
171
+ const intent = control ? JSON.parse(control.metadata_json) : null;
172
+ const workerGeneration = intent?.generation ?? (releaseCurrent ? release.generation : null);
173
+ 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;
174
+ 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;
175
+ 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 };
176
+ db.exec("RELEASE runtime_scope_status");
177
+ return result;
178
+ }
179
+ catch (error) {
180
+ db.exec("ROLLBACK TO runtime_scope_status");
181
+ db.exec("RELEASE runtime_scope_status");
182
+ throw error;
183
+ }
184
+ }
185
+ /** Persist admission shutdown independently of client/process lease expiry.
186
+ * This is a dispatch fence, not evidence that the existing native tree stopped. */
187
+ export function fenceRuntimeBudget(context, scopeId, requestId) {
188
+ parseRuntimeBudget(JSON.stringify({ schema_id: "dd-flow/runtime-budget@1", scope_id: scopeId, per_harness: {} }));
189
+ if (!requestId || requestId.length > 256)
190
+ throw new AppError("runtime_budget_invalid", "Scope fence requires a bounded request identity", 2);
191
+ const db = getResourceDatabase(resourceHome(context));
192
+ db.beginWriteTransaction();
193
+ try {
194
+ const existing = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
195
+ const now = context.now();
196
+ const receipt = existing ? JSON.parse(existing.metadata_json)
197
+ : { scope_id: scopeId, request_id: requestId, dispatch_blocked: true, fenced_at: now };
198
+ if (!existing)
199
+ 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]);
200
+ db.exec("COMMIT");
201
+ return receipt;
202
+ }
203
+ catch (error) {
204
+ db.exec("ROLLBACK");
205
+ throw error;
206
+ }
207
+ }
208
+ export function runtimeBudgetForRun(context, projectId, runId) {
209
+ const retained = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_controllers'")
210
+ ? 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;
211
+ if (!retained)
212
+ return parseRuntimeBudget(context.env.DD_FLOW_RUNTIME_BUDGET);
213
+ const manifest = JSON.parse(retained.manifest_json);
214
+ return parseRuntimeBudget(manifest.budget ? JSON.stringify(manifest.budget) : undefined);
215
+ }
216
+ export function parseRuntimeBudget(raw) {
217
+ if (raw === undefined)
218
+ return undefined;
219
+ let value;
220
+ try {
221
+ value = JSON.parse(raw);
222
+ }
223
+ catch {
224
+ throw new AppError("runtime_budget_invalid", "Runtime budget must be one JSON object", 2);
225
+ }
226
+ 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"
227
+ || 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))
228
+ throw new AppError("runtime_budget_invalid", "Runtime budget requires a stable scope and per-harness limits", 2);
229
+ const limits = {};
230
+ for (const [name, limit] of Object.entries(value.per_harness).sort(([left], [right]) => left.localeCompare(right))) {
231
+ const key = harnessKey(name);
232
+ if (name !== key || !Number.isSafeInteger(limit) || limit < 1 || limit > 1024)
233
+ throw new AppError("runtime_budget_invalid", "Budget keys must be canonical harness names and limits positive integers up to 1024", 2);
234
+ limits[key] = limit;
235
+ }
236
+ return { schema_id: "dd-flow/runtime-budget@1", scope_id: value.scope_id, per_harness: limits };
237
+ }
238
+ /** A host-registry transaction arbitrates all roots in this experiment, not
239
+ * just the CLI clients currently observing them. Expiry never frees a slot. */
240
+ export function reserveProviderTurn(context, record, input) {
241
+ const metadata = JSON.parse(record.metadata_json);
242
+ const budget = parseRuntimeBudget(metadata.budget === undefined ? undefined : JSON.stringify(metadata.budget));
243
+ if (!budget)
244
+ return { admitted: true };
245
+ if (!metadata.harness || !metadata.state_dir || !input.operationId || !["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(input.operation))
246
+ throw new AppError("runtime_budget_invalid", "Budget admission requires a frozen harness and productive operation", 2);
247
+ const harness = harnessKey(metadata.harness), limit = budget.per_harness[harness] ?? 1;
248
+ const db = getResourceDatabase(resourceHome(context));
249
+ const key = digest(JSON.stringify([record.id, input.operationId]));
250
+ const policy = JSON.stringify(budget), now = context.now();
251
+ db.beginWriteTransaction();
252
+ try {
253
+ const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [record.id]);
254
+ if (current?.lease_token !== record.lease_token || current.state !== "running")
255
+ throw new AppError("process_ownership_unknown", "Budget owner changed before admission", 1);
256
+ try {
257
+ assertScopeAdmission(context, budget.scope_id);
258
+ }
259
+ catch (error) {
260
+ if (!(error instanceof AppError) || error.code !== "runtime_scope_controlled" || !scopeRecoveryLoadAllowed(context, budget.scope_id, record, input))
261
+ throw error;
262
+ }
263
+ const frozen = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-budget' AND resource_key = ?", [budget.scope_id]);
264
+ if (frozen && frozen.metadata_json !== policy)
265
+ throw new AppError("runtime_budget_conflict", "This experiment already froze different concurrency limits", 1);
266
+ if (!frozen)
267
+ 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]);
268
+ const prior = db.get("SELECT resource_key, metadata_json, lease_token FROM managed_resources WHERE resource_kind = 'provider-turn' AND resource_key = ?", [key]);
269
+ if (prior) {
270
+ if (prior.lease_token !== record.lease_token)
271
+ throw new AppError("process_ownership_unknown", "Retained budget claim belongs to another physical owner", 1);
272
+ db.exec("COMMIT");
273
+ return { admitted: true, budget_scope: budget.scope_id, reused: true };
274
+ }
275
+ 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;
276
+ if (active >= limit) {
277
+ db.exec("COMMIT");
278
+ return { admitted: false, reason: "provider_capacity", budget_scope: budget.scope_id, harness, limit, active };
279
+ }
280
+ 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]);
281
+ db.exec("COMMIT");
282
+ return { admitted: true, budget_scope: budget.scope_id, harness, limit, active: active + 1 };
283
+ }
284
+ catch (error) {
285
+ db.exec("ROLLBACK");
286
+ throw error;
287
+ }
288
+ }
289
+ function scopeRecoveryLoadAllowed(context, scopeId, record, input) {
290
+ const metadata = JSON.parse(record.metadata_json);
291
+ if (input.operation !== "session.resume" || !input.daemonId || !metadata.state_dir || metadata.dd_flow_home !== context.ddFlowHome || !processIsAlive(record))
292
+ return false;
293
+ const db = getResourceDatabase(resourceHome(context));
294
+ const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
295
+ const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [scopeId]);
296
+ const intent = row ? JSON.parse(row.metadata_json) : null;
297
+ if (!control || intent?.scope_id !== scopeId || intent.generation !== JSON.parse(control.metadata_json).generation || intent.status !== "preparing" || !Array.isArray(intent.runs))
298
+ return false;
299
+ if (!record.run_id) {
300
+ const loadRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native-load' AND resource_key = ?", [input.operationId]);
301
+ const load = loadRow ? JSON.parse(loadRow.metadata_json) : null;
302
+ 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)
303
+ return false;
304
+ const source = intent.native?.find((item) => item.process_id === load.source_process_id);
305
+ 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))
306
+ return false;
307
+ 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]))]);
308
+ const parent = parentRow ? JSON.parse(parentRow.metadata_json) : null;
309
+ return parent?.status === "restarted" && parent.restart_receipt?.daemon_id === input.daemonId && JSON.stringify(parent.replacement_binding) === JSON.stringify(managedProcessBinding(record, resourceHome(context)));
310
+ }
311
+ if (!record.project_id)
312
+ return false;
313
+ const source = intent.runs.find((run) => run.runtime_home === context.ddFlowHome && run.project_id === record.project_id && run.run_id === record.run_id);
314
+ if (!source || !context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'run_controller_operations'"))
315
+ return false;
316
+ const prepared = context.db.get(`SELECT 1 FROM run_controller_operations o
317
+ JOIN run_controllers c ON c.controller_id = o.controller_id
318
+ JOIN run_controls ctl ON ctl.control_id = ? AND ctl.project_id = c.project_id AND ctl.run_id = c.run_id
319
+ JOIN run_recovery_guards g ON g.recovery_id = ctl.recovery_id
320
+ WHERE c.controller_id = ? AND c.project_id = ? AND c.run_id = ? AND c.generation = g.generation
321
+ AND o.generation = g.generation AND o.status = 'accepted' AND ctl.status = 'resuming' AND g.status = 'resuming'
322
+ AND ctl.recovery_id = ? AND json_extract(o.input_json, '$.action') = 'control_resume'
323
+ 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]);
324
+ return Boolean(prepared) && recoveryRuntimeAdmission(context, record, { stateDir: metadata.state_dir, daemonId: input.daemonId, operationId: input.operationId, operation: input.operation, nativeSessionId: input.sessionId });
325
+ }
326
+ /** Release uses the daemon's durable reply or fresh whole-tree observation.
327
+ * Unknown/error replies and client timeouts are deliberately not release proof. */
328
+ export function releaseProviderTurns(context, record, input) {
329
+ const metadata = JSON.parse(record.metadata_json);
330
+ if (!metadata.state_dir)
331
+ throw new AppError("process_ownership_unknown", "Budget release has no retained state directory", 1);
332
+ const state = JSON.parse(fs.readFileSync(path.join(metadata.state_dir, "daemon.json"), "utf8"));
333
+ if ((state.daemon_id ?? state.config?.daemonId) !== input.daemonId || state.resource_process?.id !== record.id || state.resource_process.lease_token !== record.lease_token)
334
+ throw new AppError("process_ownership_unknown", "Budget release belongs to another daemon generation", 1);
335
+ let sessionId = null;
336
+ let operationIds;
337
+ if (input.observationFile) {
338
+ const parent = path.resolve(metadata.state_dir, "budget-observations");
339
+ if (path.dirname(path.resolve(input.observationFile)) !== parent)
340
+ throw new AppError("runtime_budget_evidence_invalid", "Budget observation is outside its daemon directory", 2);
341
+ const observed = JSON.parse(fs.readFileSync(input.observationFile, "utf8"));
342
+ if (observed.daemon_id !== input.daemonId || observed.settled !== true || typeof observed.session_id !== "string" || !observed.session_id || !Array.isArray(observed.operation_ids) || observed.operation_ids.some(id => typeof id !== "string" || !id))
343
+ throw new AppError("runtime_budget_evidence_invalid", "Budget release requires fresh settled Session evidence", 1);
344
+ sessionId = observed.session_id;
345
+ operationIds = observed.operation_ids;
346
+ }
347
+ else {
348
+ if (!input.operationId)
349
+ throw new AppError("runtime_budget_evidence_invalid", "Budget release requires an operation or observation", 2);
350
+ const directory = path.join(metadata.state_dir, "operations", digest(input.operationId));
351
+ const requested = JSON.parse(fs.readFileSync(path.join(directory, "requested.json"), "utf8"));
352
+ const result = JSON.parse(fs.readFileSync(path.join(directory, "result.json"), "utf8"));
353
+ if (requested.daemon_id !== input.daemonId || requested.operation_id !== input.operationId || !["session.create", "session.fork"].includes(requested.operation ?? "") || result.state !== "completed")
354
+ throw new AppError("runtime_budget_evidence_invalid", "Only a completed Session creation can release without a tree observation", 1);
355
+ operationIds = [input.operationId];
356
+ }
357
+ const db = getResourceDatabase(resourceHome(context));
358
+ let released = 0;
359
+ for (const operationId of operationIds)
360
+ 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;
361
+ return { ok: true, released };
362
+ }
@@ -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 { validateRuntimeScopeManifest } 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
+ validateRuntimeScopeManifest(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
+ }