@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149
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 +130 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +13 -3
- package/dist/cli/command-metadata.js +51 -16
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +67 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +9 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +77 -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 +79 -40
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +18 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -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/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- 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/driver-recovery.mjs +9 -8
- 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.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -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 +5 -3
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- 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/project-flow-pack-manifest.schema.json +1 -1
- 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/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +34 -19
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +34 -10
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +46 -20
- package/dist/services/descriptor-stage-coordinator.js +66 -13
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +186 -119
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +13 -4
- package/dist/services/frozen-engine.js +148 -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 +4 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +47 -4
- package/dist/services/lifecycle-invocations.js +179 -38
- 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 +44 -2
- package/dist/services/memory-init-controller-start.js +27 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +24 -6
- package/dist/services/memory-init-native-capacity.js +17 -4
- package/dist/services/memory-init-preflight.js +21 -17
- 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-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +71 -6
- 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-tool-probe.js +25 -0
- 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 +92 -27
- 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/native-daemon-history.js +2 -1
- 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 +112 -46
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/release-cache.js +8 -0
- 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-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +4 -7
- package/dist/services/run-controller-recovery.js +8 -2
- package/dist/services/run-controller.js +215 -94
- package/dist/services/run-fork.js +1 -1
- 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/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +22 -7
- package/dist/services/stage-pause.js +111 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +51 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +117 -39
- package/dist/services/workspace-bootstrap.js +2 -1
- 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 +13 -6
|
@@ -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,52 @@
|
|
|
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
|
+
import { nativeTreeReadiness } from "../harness-runtime/lib/native-children.mjs";
|
|
8
|
+
import { ObservationClock } from "../harness-runtime/lib/observation-clock.mjs";
|
|
9
|
+
/** Native output is evidence, not authority: match the committed current scope. */
|
|
10
|
+
export function ownedProviderBoundary(context, input, boundary) {
|
|
11
|
+
if (typeof boundary.native_hook_event_key !== "string" || typeof boundary.daemon_instance !== "string"
|
|
12
|
+
|| !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]))
|
|
13
|
+
return false;
|
|
14
|
+
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]);
|
|
15
|
+
if (!invocation)
|
|
16
|
+
return false;
|
|
17
|
+
const scope = JSON.parse(invocation.scope_json);
|
|
18
|
+
const outcome = JSON.parse(invocation.outcome_json);
|
|
19
|
+
if (scope.runId !== input.runId || scope.daemonId !== boundary.daemon_instance || scope.rootSessionId !== input.sessionId || scope.generation !== input.generation
|
|
20
|
+
|| outcome.error || outcome.effect !== undefined && outcome.effect !== "committed" || outcome.result?.ok !== true
|
|
21
|
+
|| outcome.result.effect !== undefined && outcome.result.effect !== "committed")
|
|
22
|
+
return false;
|
|
23
|
+
const parsed = parseLifecycleCommand(invocation.command);
|
|
24
|
+
if (parsed.kind === "none" || parsed.kind === "compound" && !(parsed.supported === true && parsed.invocations?.length === 1) || !["stage_pause", "stage_finish", "work_finish"].includes(parsed.invocation.operation)
|
|
25
|
+
|| boundary.outcome === "completed" && parsed.invocation.operation === "stage_pause" || boundary.outcome === "paused" && parsed.invocation.operation !== "stage_pause"
|
|
26
|
+
|| boundary.lifecycle_operation !== undefined && boundary.lifecycle_operation !== parsed.invocation.operation
|
|
27
|
+
|| parsed.invocation.operation !== "work_finish" && commandOption(parsed.invocation, "stage") !== input.stage)
|
|
28
|
+
return false;
|
|
29
|
+
const result = outcome.result;
|
|
30
|
+
if (result.run_id !== undefined && result.run_id !== input.runId || result.stage !== undefined && result.stage !== input.stage
|
|
31
|
+
|| boundary.outcome === "paused" && (result.pause?.id !== boundary.pause?.id || result.outcome !== "paused")
|
|
32
|
+
|| boundary.work_id !== undefined && result.work_id !== undefined && boundary.work_id !== result.work_id)
|
|
33
|
+
return false;
|
|
34
|
+
if (boundary.run_id !== undefined && boundary.run_id !== input.runId || boundary.stage !== undefined && boundary.stage !== input.stage
|
|
35
|
+
|| boundary.work_id !== undefined && input.workId && boundary.work_id !== input.workId)
|
|
36
|
+
return false;
|
|
37
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
|
|
38
|
+
const index = JSON.parse(run?.index_json ?? "null");
|
|
39
|
+
const stage = index?.stage_runs?.find(item => item.stage === input.stage);
|
|
40
|
+
if (!stage?.attempt)
|
|
41
|
+
return false;
|
|
42
|
+
const workId = input.workId ?? (typeof boundary.work_id === "string" ? boundary.work_id : null);
|
|
43
|
+
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]);
|
|
44
|
+
if (rows.length !== 1 || boundary.work_id !== undefined && boundary.work_id !== rows[0].work_id)
|
|
45
|
+
return false;
|
|
46
|
+
if (boundary.outcome === "paused")
|
|
47
|
+
return rows[0].status === "paused" && stage.pause?.work_id === rows[0].work_id && stage.pause.id === boundary.pause?.id;
|
|
48
|
+
return boundary.outcome === "completed" && (input.scope === "work" ? rows[0].status === "completed" : stage.status === "done");
|
|
49
|
+
}
|
|
6
50
|
export function providerChildFailure(child) {
|
|
7
51
|
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
52
|
}
|
|
@@ -12,26 +56,9 @@ export function eligibleProviderChildOverload(child, parentSessionId) {
|
|
|
12
56
|
}
|
|
13
57
|
/** A terminal root is not a settled tree. Only the daemon's exact owned tree
|
|
14
58
|
* with a settled root and positively live descendants permits waiting. */
|
|
15
|
-
export function capacityReadiness(seen, sessionId, operationId, turnId, items, allowDescendants = false) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
|
|
19
|
-
if (!ids.length || ids[0] !== sessionId || ids.some(id => typeof id !== "string" || !id) || new Set(ids).size !== ids.length)
|
|
20
|
-
return "unproven";
|
|
21
|
-
const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
|
|
22
|
-
const exactTree = observations.length === ids.length && ids.every(id => observations.filter(item => item.provider_session_id === id).length === 1);
|
|
23
|
-
const settlement = seen.settlement;
|
|
24
|
-
const budgetSettled = !settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true;
|
|
25
|
-
const treeSettled = allowDescendants ? exactTree && observations.every(item => item.settled === true) && Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0 : ids.length === 1;
|
|
26
|
-
if (seen.settled === true && treeSettled && budgetSettled)
|
|
27
|
-
return "ready";
|
|
28
|
-
if (!allowDescendants || seen.settled !== false || !exactTree || ids.length < 2 || observations[0]?.provider_session_id !== sessionId || observations[0].settled !== true
|
|
29
|
-
|| settlement && settlement.state !== "not_required" && !(settlement.state === "pending" && settlement.reason === "tree_unsettled"))
|
|
30
|
-
return "unproven";
|
|
31
|
-
const live = observations.filter(item => item.settled === false);
|
|
32
|
-
const unsettled = seen.unsettled_sessions;
|
|
33
|
-
const knownLive = live.length > 0 && live.every(item => item.provider_session_id !== sessionId && ["active", "running", "inProgress"].includes(String(typeof item.status === "object" && item.status ? item.status.type : item.status)));
|
|
34
|
-
return knownLive && observations.every(item => typeof item.settled === "boolean") && Array.isArray(unsettled) && unsettled.length === live.length && live.every(item => unsettled.includes(item.provider_session_id)) ? "owned_children_live" : "unproven";
|
|
59
|
+
export function capacityReadiness(seen, sessionId, operationId, turnId, items, allowDescendants = false, requestProof) {
|
|
60
|
+
const readiness = nativeTreeReadiness(seen, { sessionId, operationId, turnId, allowDescendants, requestProof });
|
|
61
|
+
return readiness === "inspection_pending" ? readiness : nativeItemsSettled(items) ? readiness : "unproven";
|
|
35
62
|
}
|
|
36
63
|
/** Permit acquisition must still belong to the original task, not merely an
|
|
37
64
|
* open RUN. Controls/inspection and ordinary successor prompts are unaffected. */
|
|
@@ -237,9 +264,13 @@ export async function continueProviderCapacity(input) {
|
|
|
237
264
|
const decide = async (observedOperationId = operationId) => {
|
|
238
265
|
input.signal?.throwIfAborted();
|
|
239
266
|
input.assertCurrent();
|
|
240
|
-
const
|
|
267
|
+
const normalized = normalizeProviderFailure(error, input.sessionId);
|
|
268
|
+
const candidate = normalized?.request_proof.kind === "stream_request" ? normalized.primary_error.details?.recovered_error_boundary : undefined;
|
|
269
|
+
const decision = await observe(async () => await input.decision?.(candidate?.receipt) ?? "continue", observedOperationId);
|
|
241
270
|
input.signal?.throwIfAborted();
|
|
242
271
|
input.assertCurrent();
|
|
272
|
+
if (normalized?.request_proof.kind === "stream_request" && ["completed", "paused"].includes(decision) && !candidate?.receipt)
|
|
273
|
+
throw blocked("Stream refusal has no ordered evidence of recovery before the committed boundary");
|
|
243
274
|
if (decision === "reconciliation_blocked")
|
|
244
275
|
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
276
|
if (decision === "continue" && input.deadline !== undefined && Date.now() >= input.deadline)
|
|
@@ -254,9 +285,11 @@ export async function continueProviderCapacity(input) {
|
|
|
254
285
|
return null;
|
|
255
286
|
throw error;
|
|
256
287
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
288
|
+
if (normalizeProviderFailure(error, input.sessionId)?.request_proof.kind !== "stream_request") {
|
|
289
|
+
const completed = await decide();
|
|
290
|
+
if (completed)
|
|
291
|
+
return completed;
|
|
292
|
+
}
|
|
260
293
|
const prior = await observe(() => input.inspectOperation(operationId));
|
|
261
294
|
const failure = prior.error;
|
|
262
295
|
const confirmed = normalizeProviderFailure(failure, input.sessionId);
|
|
@@ -266,25 +299,71 @@ export async function continueProviderCapacity(input) {
|
|
|
266
299
|
if (prior.primary_error)
|
|
267
300
|
throw prior.primary_error;
|
|
268
301
|
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)
|
|
302
|
+
|| prior.state !== "failed" || prior.primary_error || !turnId || !terminalProviderOverload(failure, input.sessionId, turnId)
|
|
303
|
+
|| confirmed?.request_proof.kind === "stream_request" && confirmed.request_proof.operation_id !== operationId) {
|
|
270
304
|
throw blocked("Retained predecessor does not prove the exact terminal overloaded Turn");
|
|
271
305
|
}
|
|
306
|
+
let pendingClock;
|
|
272
307
|
const readiness = async () => {
|
|
273
308
|
input.signal?.throwIfAborted();
|
|
274
309
|
input.assertCurrent();
|
|
275
|
-
const seen = await observe(() => input.inspectSession(turnId));
|
|
310
|
+
const seen = await observe(() => input.inspectSession(confirmed?.request_proof.kind === "stream_request" ? undefined : turnId));
|
|
276
311
|
input.signal?.throwIfAborted();
|
|
277
312
|
input.assertCurrent();
|
|
278
313
|
// An unloaded exact-Turn snapshot is not conflicting tool evidence.
|
|
279
314
|
// Preserve the bound failure receipt, but never hide a pending item.
|
|
280
315
|
const freshItems = seen.native_turn_items;
|
|
281
|
-
const
|
|
316
|
+
const stream = confirmed?.request_proof.kind === "stream_request";
|
|
317
|
+
const streamMetadata = seen.terminal_metadata;
|
|
318
|
+
const exactFresh = stream ? streamRequestKey(streamMetadata?.request_proof) === turnId : seen.terminal_turn_id === turnId && seen.terminal_status === "failed";
|
|
319
|
+
const items = (exactFresh
|
|
282
320
|
&& !(freshItems?.observed === false && freshItems.pending !== true) ? freshItems : confirmed?.native_turn_items);
|
|
283
|
-
|
|
321
|
+
if (confirmed?.request_proof.kind === "stream_request") {
|
|
322
|
+
const metadata = seen.terminal_metadata;
|
|
323
|
+
if (streamRequestKey(metadata?.request_proof) !== turnId)
|
|
324
|
+
return "unproven";
|
|
325
|
+
}
|
|
326
|
+
const current = capacityReadiness(seen, input.sessionId, operationId, turnId, items, input.allowSettledDescendants, confirmed?.request_proof);
|
|
327
|
+
if (current === "inspection_pending") {
|
|
328
|
+
const saved = seen.native_settlement.observation_clock;
|
|
329
|
+
pendingClock ??= new ObservationClock({ timeoutMs: saved.timeout_ms, saved, snapshotIsLive: true });
|
|
330
|
+
if (pendingClock.sample(saved.cursor, saved.observed_at ?? undefined))
|
|
331
|
+
return "unproven";
|
|
332
|
+
}
|
|
333
|
+
return current;
|
|
284
334
|
};
|
|
285
|
-
|
|
335
|
+
// Pending is a bound observer, not a refusal or an intent to write. Resolve
|
|
336
|
+
// it before retaining a productive retry. Its producer owns the old clock.
|
|
337
|
+
const waitForReadiness = async (initial) => {
|
|
338
|
+
let current = initial;
|
|
339
|
+
while (current === "owned_children_live" || current === "inspection_pending") {
|
|
340
|
+
await delay(250, undefined, { signal: input.signal });
|
|
341
|
+
const boundary = await decide();
|
|
342
|
+
if (boundary)
|
|
343
|
+
return boundary;
|
|
344
|
+
current = await readiness();
|
|
345
|
+
}
|
|
346
|
+
if (current !== "ready")
|
|
347
|
+
throw blocked("Native predecessor readiness became unproven");
|
|
348
|
+
return null;
|
|
349
|
+
};
|
|
350
|
+
let ready = await readiness();
|
|
351
|
+
while (ready === "inspection_pending") {
|
|
352
|
+
await delay(250, undefined, { signal: input.signal });
|
|
353
|
+
const boundary = await decide();
|
|
354
|
+
if (boundary)
|
|
355
|
+
return boundary;
|
|
356
|
+
ready = await readiness();
|
|
357
|
+
}
|
|
286
358
|
if (ready === "unproven")
|
|
287
359
|
throw blocked("Native predecessor readiness is unproven; do not cancel or replay to manufacture proof");
|
|
360
|
+
if (ready === "ready") {
|
|
361
|
+
const completed = await decide();
|
|
362
|
+
if (completed)
|
|
363
|
+
return completed;
|
|
364
|
+
if (confirmed?.request_proof.kind === "stream_request" && confirmed.primary_error.details?.recovered_error_boundary)
|
|
365
|
+
throw blocked("Recovered native error does not match its current durable owner boundary");
|
|
366
|
+
}
|
|
288
367
|
const native = normalizeProviderFailure(failure, input.sessionId, turnId);
|
|
289
368
|
let refusal = refusals.find(item => item.ordinal === ordinal - 1);
|
|
290
369
|
if (refusal && (refusal.turn_id !== turnId || refusal.session_id !== input.sessionId || refusal.policy !== CAPACITY_POLICY))
|
|
@@ -332,26 +411,13 @@ export async function continueProviderCapacity(input) {
|
|
|
332
411
|
throw new AppError("capacity_owner_deadline_exceeded", "Continuation wait exceeds its original owner deadline", 1, { primary_error: failure, deadline: input.deadline ?? null });
|
|
333
412
|
}
|
|
334
413
|
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 };
|
|
414
|
+
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
415
|
input.retainIntent(intent);
|
|
337
416
|
history.push(intent);
|
|
338
417
|
}
|
|
339
418
|
// Intent/refusal are durable before the first wait; no permit or writer is
|
|
340
419
|
// held here, and live siblings are never cancelled as a settlement proof.
|
|
341
|
-
const
|
|
342
|
-
let current = initial;
|
|
343
|
-
while (current === "owned_children_live") {
|
|
344
|
-
await delay(250, undefined, { signal: input.signal });
|
|
345
|
-
const boundary = await decide();
|
|
346
|
-
if (boundary)
|
|
347
|
-
return boundary;
|
|
348
|
-
current = await readiness();
|
|
349
|
-
}
|
|
350
|
-
if (current !== "ready")
|
|
351
|
-
throw blocked("Owned native child readiness became unproven");
|
|
352
|
-
return null;
|
|
353
|
-
};
|
|
354
|
-
const childBoundary = await waitForOwnedChildren(ready);
|
|
420
|
+
const childBoundary = await waitForReadiness(ready);
|
|
355
421
|
if (childBoundary)
|
|
356
422
|
return childBoundary;
|
|
357
423
|
// Node timers truncate large delays. Bounded slices preserve cancellation
|
|
@@ -366,7 +432,7 @@ export async function continueProviderCapacity(input) {
|
|
|
366
432
|
const afterWait = await decide();
|
|
367
433
|
if (afterWait)
|
|
368
434
|
return afterWait;
|
|
369
|
-
const refreshedBoundary = await
|
|
435
|
+
const refreshedBoundary = await waitForReadiness(await readiness());
|
|
370
436
|
if (refreshedBoundary)
|
|
371
437
|
return refreshedBoundary;
|
|
372
438
|
const beforeDispatch = await decide();
|
|
@@ -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'
|
|
@@ -6,7 +6,9 @@ import { inspectReleaseArchive } from "./release-archive.js";
|
|
|
6
6
|
import { verifyReleaseArchive } from "./release-manifest.js";
|
|
7
7
|
import { acquireInstallationLock } from "./installation-lock.js";
|
|
8
8
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
9
|
+
import { publishSetup } from "../harness-runtime/lib/setup-progress.mjs";
|
|
9
10
|
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
11
|
+
let cacheVerificationPass = 0;
|
|
10
12
|
function refuse(reason) {
|
|
11
13
|
throw new AppError("cache_integrity", "Release cache cannot be published", 1, { reason, phase: "cache_publication", effect: "no_effect", help: "setup" });
|
|
12
14
|
}
|
|
@@ -33,6 +35,8 @@ function write(target, bytes, mode) {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
function retained(target, archive, entries) {
|
|
38
|
+
const observation = `release-cache:${++cacheVerificationPass}:${target}`;
|
|
39
|
+
publishSetup("probe_started", observation);
|
|
36
40
|
if (!fs.lstatSync(target).isDirectory())
|
|
37
41
|
refuse("unsafe_retained_cache");
|
|
38
42
|
const archiveFile = path.join(target, "archive.tgz");
|
|
@@ -53,6 +57,7 @@ function retained(target, archive, entries) {
|
|
|
53
57
|
directories.add(parts.slice(0, index).join("/"));
|
|
54
58
|
}
|
|
55
59
|
const seen = new Set();
|
|
60
|
+
let checked = 0;
|
|
56
61
|
const visit = (root, relative) => {
|
|
57
62
|
if (!fs.lstatSync(root).isDirectory())
|
|
58
63
|
refuse("unsafe_retained_cache");
|
|
@@ -70,12 +75,15 @@ function retained(target, archive, entries) {
|
|
|
70
75
|
if (!stat.isFile() || entry?.kind !== "file" || stat.size !== entry.bytes.length || digest(fs.readFileSync(full)) !== digest(entry.bytes) || (stat.mode & 0o111) !== (entry.mode & 0o111))
|
|
71
76
|
refuse("retained_cache_changed");
|
|
72
77
|
seen.add(logical);
|
|
78
|
+
if (++checked % 64 === 0)
|
|
79
|
+
publishSetup("identity_digest_progress", `${observation}:${checked}`);
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
};
|
|
76
83
|
visit(payload, "");
|
|
77
84
|
if (seen.size !== entries.length)
|
|
78
85
|
refuse("retained_cache_changed");
|
|
86
|
+
publishSetup("identity_digest_completed", observation);
|
|
79
87
|
}
|
|
80
88
|
/** Verify retained compressed and expanded bytes without executing or activating them. */
|
|
81
89
|
export function verifyPublishedReleaseCache(target, archive) {
|
|
@@ -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) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setFlowRunRecoveryPaused } from "./runs.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import { ensureRunControlStorage } from "../storage/database.js";
|
|
5
6
|
import { canonicalPath, resolveProjectRoot } from "../storage/paths.js";
|
|
@@ -414,7 +415,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
414
415
|
const unfinished = receipts.filter(receipt => !["completed", "failed", "interrupted"].includes(String(receipt.state)));
|
|
415
416
|
if (unfinished.some(receipt => receipt.daemon_id !== daemonId || String(receipt.operation) !== "session.create" || receipt.session_id !== null))
|
|
416
417
|
throw new AppError("shutdown_operation_unsettled", "Closed daemon retains an unfinished non-create operation", 1);
|
|
417
|
-
const stopped = receipts.find(receipt => receipt.daemon_id === daemonId && receipt.operation === "daemon.stop" && receipt.state === "completed" && receipt.result?.stopped === true && receipt.result
|
|
418
|
+
const stopped = receipts.find(receipt => receipt.daemon_id === daemonId && receipt.operation === "daemon.stop" && receipt.state === "completed" && receipt.result?.stopped === true && nativeShutdownConfirmed(receipt.result, { daemonId }));
|
|
418
419
|
if (!stopped)
|
|
419
420
|
throw new AppError("shutdown_receipt_required", "Closed daemon has no clean native shutdown receipt for its current identity", 1);
|
|
420
421
|
closedDaemons.set(directory, { source: "retained_clean_shutdown", daemon_id: daemonId, operation_id: stopped.operation_id, session_ids: state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item)), stopped: true, settled: true, processes: records.map(record => ({ process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at })), result: stopped.result });
|
|
@@ -12,6 +12,7 @@ import { canonicalNativeOperation } from "../harness-runtime/lib/adapter-timeout
|
|
|
12
12
|
import { retainedDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
|
|
13
13
|
import { readOperationRecord } from "../harness-runtime/lib/operation-progress.mjs";
|
|
14
14
|
import { readNativeDaemonState } from "./managed-daemon-binding.js";
|
|
15
|
+
import { validateCheckEnvironment } from "./check-environment.js";
|
|
15
16
|
/** A paused RUN admits only its controller's exact accepted answer Turn. */
|
|
16
17
|
export function controllerHitlAdmission(context, record, input) {
|
|
17
18
|
if (input.operation !== "session.prompt" || !input.operationId || !input.promptSha256)
|
|
@@ -76,7 +77,7 @@ export function sessionArguments(context, manifest, session) {
|
|
|
76
77
|
return ["--dd-flow-bin", manifest.flow_executable, "--dd-flow-home", context.ddFlowHome, "--project-root", manifest.request.project_root, "--state-dir", session.state_dir, "--journal", session.journal, "--cwd", session.cwd, "--provider", profile.provider, "--model", profile.model, "--reasoning", profile.reasoning, "--mode", profile.mode, "--permission", profile.permission, ...manifest.adapters[runtimeHarness(profile)].runtime_args, "--json"];
|
|
77
78
|
}
|
|
78
79
|
export function controllerEnvironment(context, manifest, session) {
|
|
79
|
-
const env = { ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_RUNTIME_BUDGET: manifest.budget ? JSON.stringify(manifest.budget) : undefined };
|
|
80
|
+
const env = { ...context.env, ...validateCheckEnvironment(manifest.check_environment), DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_RUNTIME_BUDGET: manifest.budget ? JSON.stringify(manifest.budget) : undefined };
|
|
80
81
|
for (const key of ["DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_DAEMON_ID", "DD_FLOW_RUNTIME_OWNER", "DD_FLOW_WORK_ID", "DD_FLOW_SESSION_ID", "CODEX_THREAD_ID"])
|
|
81
82
|
delete env[key];
|
|
82
83
|
if (session?.codex_home)
|