@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.
- package/CHANGELOG.md +59 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +9 -3
- package/dist/cli/command-metadata.js +33 -14
- package/dist/cli/help.js +32 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +74 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +70 -32
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +12 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -20
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +4 -2
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +11 -4
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-fanout.js +21 -4
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +48 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-start.js +26 -19
- package/dist/services/descriptor-stage-coordinator.js +30 -10
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +104 -93
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +8 -4
- package/dist/services/frozen-engine.js +113 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +2 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +25 -4
- package/dist/services/lifecycle-invocations.js +33 -5
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +28 -2
- package/dist/services/memory-init-controller-start.js +19 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +21 -3
- package/dist/services/memory-init-native-capacity.js +13 -0
- package/dist/services/memory-init-preflight.js +11 -1
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-layer.js +68 -4
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +77 -23
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +69 -8
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-controller-process.js +2 -6
- package/dist/services/run-controller-recovery.js +6 -1
- package/dist/services/run-controller.js +131 -80
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +9 -7
- package/dist/services/stage-pause.js +108 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-fanout.js +21 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +80 -33
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- 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 "./
|
|
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 {
|
|
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
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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')
|
|
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')
|
|
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')
|
|
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')='
|
|
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
|
|
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 =
|
|
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
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 } });
|