@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { hasLifecyclePacketAssignments, managedLifecycleCommand, recoverLifecycleCommands } from "./lifecycle-invocations.js";
|
|
2
|
-
import { recoveryGuard,
|
|
2
|
+
import { recoveryGuard, recoveryPacketReceipt } from "./run-recovery-guard.js";
|
|
3
|
+
export { assertStageSettlementAllowed, assertWorkSettlementAllowed, recoveryPacketReceipt } from "./run-recovery-guard.js";
|
|
3
4
|
export { recoveryGuard, assertRunMutationAllowed, assertRunRecoveryAllowed } from "./run-recovery-guard.js";
|
|
4
5
|
import crypto from "node:crypto";
|
|
5
6
|
import fs from "node:fs";
|
|
@@ -19,11 +20,6 @@ import { findRecentMatchingHookEvent, hookSessionIdentity, recoveryAcceptMatchKe
|
|
|
19
20
|
import { flowCommand } from "./stage-pause.js";
|
|
20
21
|
import { materializePendingWorkResults } from "./work-registry.js";
|
|
21
22
|
import { currentStagePrompt } from "./stage-context.js";
|
|
22
|
-
export function recoveryPacketReceipt(binding) {
|
|
23
|
-
if (!binding.packet_json || !binding.packet_sha256 || crypto.createHash("sha256").update(binding.packet_json).digest("hex") !== binding.packet_sha256)
|
|
24
|
-
throw new AppError("recovery_packet_missing", "Recovery requires its original verified Work packet", 1);
|
|
25
|
-
return { packet_json: binding.packet_json, packet_sha256: binding.packet_sha256, packet: JSON.parse(binding.packet_json) };
|
|
26
|
-
}
|
|
27
23
|
/** Read-only custody permission for one prepared recovery ACK, not lifecycle admission. */
|
|
28
24
|
export class PreparedRecoveryCoordinatorRead {
|
|
29
25
|
#scope;
|
|
@@ -90,12 +86,16 @@ export function recoveredCoordinatorBinding(original, input) {
|
|
|
90
86
|
export function capturedStageDelivery(capture, scope, originalPrompt) {
|
|
91
87
|
const db = new DatabaseSync(path.join(capture, "runtime", "db.sqlite"), { readOnly: true });
|
|
92
88
|
try {
|
|
89
|
+
const resumed = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='audit_events'").get()
|
|
90
|
+
? db.prepare("SELECT json_extract(payload_json, '$.pause_id') AS pause_id FROM audit_events WHERE event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.work_id') = ? AND json_extract(payload_json, '$.stage') = ? ORDER BY id DESC LIMIT 1").get(scope.runId, scope.workId, scope.stage) : undefined;
|
|
93
91
|
const rows = db.prepare("SELECT command, outcome_json FROM lifecycle_invocations WHERE status = 'settled' AND json_extract(scope_json, '$.projectRoot') = ? AND json_extract(scope_json, '$.runId') = ? AND json_extract(scope_json, '$.rootSessionId') = ? AND json_extract(scope_json, '$.generation') < ? ORDER BY rowid DESC").all(scope.projectRoot, scope.runId, scope.sessionId, scope.generation);
|
|
94
92
|
for (const row of rows) {
|
|
95
93
|
const result = row.outcome_json ? JSON.parse(row.outcome_json).result : null;
|
|
96
94
|
const parsed = parseLifecycleCommand(row.command);
|
|
97
|
-
if (parsed.kind === "none" || !parsed.supported || parsed.invocation.operation
|
|
98
|
-
|| result?.stage !== scope.stage || result.attempt !== scope.attempt || result.work_id !== scope.workId || typeof result.worker_prompt_markdown !== "string")
|
|
95
|
+
if (parsed.kind === "none" || !parsed.supported || !["stage_start", "stage_resume"].includes(parsed.invocation.operation) || parsed.invocation.args.options.get("stage")?.[0] !== scope.stage
|
|
96
|
+
|| result?.stage !== scope.stage || parsed.invocation.operation === "stage_start" && result.attempt !== scope.attempt || result.work_id !== scope.workId || typeof result.worker_prompt_markdown !== "string")
|
|
97
|
+
continue;
|
|
98
|
+
if (resumed && (parsed.invocation.operation !== "stage_resume" || result.pause_id !== resumed.pause_id))
|
|
99
99
|
continue;
|
|
100
100
|
const text = result.worker_prompt_markdown;
|
|
101
101
|
if (!text.startsWith(originalPrompt))
|
|
@@ -105,6 +105,8 @@ export function capturedStageDelivery(capture, scope, originalPrompt) {
|
|
|
105
105
|
throw new AppError("recovery_packet_missing", "Captured Stage delivery has no committed lifecycle assignment", 1);
|
|
106
106
|
return { text, sha256 };
|
|
107
107
|
}
|
|
108
|
+
if (resumed)
|
|
109
|
+
throw new AppError("recovery_packet_missing", "Accepted HITL answer has no captured settled current delivery; reconcile its retained resume receipt, never fall back to the pre-answer prompt", 1, { work_id: scope.workId, pause_id: resumed.pause_id });
|
|
108
110
|
return null;
|
|
109
111
|
}
|
|
110
112
|
finally {
|
|
@@ -174,42 +176,7 @@ export function prepareRecoveryPayload(value, label) {
|
|
|
174
176
|
throw new AppError("recovery_not_settled", "Recovery capture requires an explicit settled tree receipt", 1);
|
|
175
177
|
return payload;
|
|
176
178
|
}
|
|
177
|
-
|
|
178
|
-
export function assertWorkSettlementAllowed(context, projectId, runId, workId) {
|
|
179
|
-
const guard = recoveryGuard(context, projectId, runId);
|
|
180
|
-
if (guard?.status === "draining" && context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controls'")) {
|
|
181
|
-
const admission = context.db.get("SELECT a.work_session_id FROM run_control_admissions a JOIN run_controls c ON c.control_id = a.control_id JOIN work_sessions ws ON ws.id = a.work_session_id JOIN works w ON w.work_id = a.work_id WHERE c.project_id = ? AND c.run_id = ? AND c.recovery_id = ? AND c.action = 'pause' AND c.status = 'requested' AND a.work_id = ? AND ws.work_id = a.work_id AND ws.session_id = a.session_id AND ws.status = 'running' AND w.status = 'running' AND w.project_id = c.project_id AND w.run_id = c.run_id LIMIT 1", [projectId, runId, guard.recovery_id, workId]);
|
|
182
|
-
if (admission)
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
assertRunMutationAllowed(context, projectId, runId);
|
|
186
|
-
}
|
|
187
|
-
/** Completion rights are pinned to the Stage attempt present at the pause fence. */
|
|
188
|
-
export function assertStageSettlementAllowed(context, projectId, runId, stage, expectedGeneration) {
|
|
189
|
-
const guard = recoveryGuard(context, projectId, runId);
|
|
190
|
-
const generation = guard?.generation ?? 0;
|
|
191
|
-
if (expectedGeneration !== undefined && generation !== expectedGeneration)
|
|
192
|
-
throw new AppError("run_recovery_generation_stale", "RUN execution generation changed while the operation was in flight", 1, { run_id: runId, expected_generation: expectedGeneration, generation });
|
|
193
|
-
if (guard?.status === "draining" && context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_control_stage_admissions'")) {
|
|
194
|
-
const admitted = context.db.get(`SELECT a.work_session_id FROM run_control_stage_admissions a JOIN run_controls c ON c.control_id = a.control_id JOIN run_control_admissions w ON w.control_id = a.control_id AND w.work_session_id = a.work_session_id JOIN work_sessions ws ON ws.id = w.work_session_id JOIN runs r ON r.project_id = c.project_id AND r.id = c.run_id, json_each(r.index_json, '$.stage_runs') live
|
|
195
|
-
WHERE c.project_id = ? AND c.run_id = ? AND c.recovery_id = ? AND c.action = 'pause' AND c.status = 'requested' AND a.stage = ? AND json_extract(live.value, '$.stage') = a.stage AND COALESCE(json_extract(live.value, '$.attempt'), 'try-001') = a.attempt AND ws.work_id = w.work_id AND ws.session_id = w.session_id AND NOT EXISTS (SELECT 1 FROM work_sessions newer WHERE newer.work_id = w.work_id AND newer.status = 'running' AND newer.id <> ws.id) LIMIT 1`, [projectId, runId, guard.recovery_id, stage]);
|
|
196
|
-
if (admitted)
|
|
197
|
-
return generation;
|
|
198
|
-
}
|
|
199
|
-
return assertRunMutationAllowed(context, projectId, runId, expectedGeneration);
|
|
200
|
-
}
|
|
201
|
-
/** A recovered RUN never grants the retired process a new execution segment. */
|
|
202
|
-
export function assertRecoveryHookCurrent(context, projectId, runId, identity) {
|
|
203
|
-
const generation = assertRunMutationAllowed(context, projectId, runId);
|
|
204
|
-
if (!generation)
|
|
205
|
-
return;
|
|
206
|
-
const current = identity.daemonId && context.db.get("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.generation = ? AND g.status = 'recovered' AND b.status = 'accepted' AND b.daemon_id = ? LIMIT 1", [projectId, runId, generation, identity.daemonId]);
|
|
207
|
-
if (current)
|
|
208
|
-
return;
|
|
209
|
-
const retired = identity.daemonId && (context.db.get(`SELECT ws.id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN hook_events he ON he.project_id = w.project_id AND he.event_key = ws.hook_event_id WHERE w.project_id = ? AND w.run_id = ? AND he.daemon_id = ? LIMIT 1`, [projectId, runId, identity.daemonId]) || context.db.get("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND b.daemon_id = ? AND (g.generation <> ? OR b.status = 'abandoned') LIMIT 1", [projectId, runId, identity.daemonId, generation]));
|
|
210
|
-
if (!identity.daemonId || retired)
|
|
211
|
-
throw new AppError("recovery_daemon_stale", "Lifecycle receipt belongs to an unidentified or retired recovery daemon", 1, { run_id: runId, generation, daemon_id: identity.daemonId });
|
|
212
|
-
}
|
|
179
|
+
export { assertRecoveryHookCurrent } from "./hook-recovery-guard.js";
|
|
213
180
|
export function beginRunRecovery(context, input, interruption = prepareRecoveryPayload(input.interruption, "interruption")) {
|
|
214
181
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
215
182
|
const run = requireRecoveryRun(context, project.id, input.runId);
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { AppError } from "../shared/errors.js";
|
|
4
|
-
import { hybridDigest } from "./hybrid-
|
|
4
|
+
import { hybridDigest } from "./hybrid-digest.js";
|
|
5
|
+
import { runContextRelocations, projectPacketLocators } from "./stage-context.js";
|
|
5
6
|
export const runStartPayloadLimit = 64 * 1024 * 1024;
|
|
7
|
+
// One exact immutable payload per database, not cached custody or returned
|
|
8
|
+
// objects. Every read still checks current SQL hashes, markers and receipts.
|
|
9
|
+
const validatedPayloadBytes = new WeakMap();
|
|
6
10
|
export function runStartBytesSha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
|
|
7
11
|
export function refuseRunStart(reason) {
|
|
8
12
|
throw new AppError("run_start_custody_invalid", "RUN creation requires its exact retained request", 1, { reason, effect: "no_effect", next_action: "reconcile_exact_request" });
|
|
@@ -59,8 +63,11 @@ export function validateRunStartPayload(value) {
|
|
|
59
63
|
|| !entry.path || entry.path.includes("\\") || entry.path.includes("\0") || entry.path.split("/").some(part => !part || part === "." || part === "..")
|
|
60
64
|
|| seen.has(entry.path) || !canonicalBase64(entry.base64))
|
|
61
65
|
refuseRunStart("invalid_allocation_assets");
|
|
62
|
-
|
|
63
|
-
|| Buffer.from(entry.base64, "base64").
|
|
66
|
+
const upgradeEvidence = value.initial_index.flow_kind === "mb-upgrade" && (["upgrade-baseline.json", "upgrade-target.json"].includes(entry.path)
|
|
67
|
+
|| entry.path === `upgrade-target-${runStartBytesSha256(Buffer.from(entry.base64, "base64"))}.bin`
|
|
68
|
+
|| entry.path === `upgrade-baseline-${runStartBytesSha256(Buffer.from(entry.base64, "base64"))}.bin`);
|
|
69
|
+
if (key === "artifacts" && (!["engine-binding.json", "operational-policy.source.md", "operational-contract.json"].includes(entry.path) && !upgradeEvidence
|
|
70
|
+
|| Buffer.from(entry.base64, "base64").length > (upgradeEvidence ? 16 : 1) * 1024 * 1024))
|
|
64
71
|
refuseRunStart("invalid_allocation_artifact");
|
|
65
72
|
seen.add(entry.path);
|
|
66
73
|
}
|
|
@@ -96,7 +103,10 @@ function readRunStartRequestSnapshot(context, projectId, requestId) {
|
|
|
96
103
|
catch {
|
|
97
104
|
return refuseRunStart("invalid_allocation_json");
|
|
98
105
|
}
|
|
99
|
-
|
|
106
|
+
if (validatedPayloadBytes.get(context.db) !== row.payload_json) {
|
|
107
|
+
validateRunStartPayload(payload);
|
|
108
|
+
validatedPayloadBytes.set(context.db, row.payload_json);
|
|
109
|
+
}
|
|
100
110
|
if (!object(receipt))
|
|
101
111
|
refuseRunStart("invalid_allocation_receipt");
|
|
102
112
|
const run = context.db.get("SELECT project_root, workspace_root, run_root, flow_kind, subject_type, subject_id, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, row.run_id]);
|
|
@@ -114,6 +124,8 @@ function readRunStartRequestSnapshot(context, projectId, requestId) {
|
|
|
114
124
|
if (!object(index))
|
|
115
125
|
refuseRunStart("allocated_run_changed");
|
|
116
126
|
const marker = index.start_request, initial = payload.initial_index;
|
|
127
|
+
const relocations = runContextRelocations(context, projectId, row.run_id);
|
|
128
|
+
const currentPath = (value) => projectPacketLocators(value, relocations);
|
|
117
129
|
if (!payload || payload.schema_id !== "dd-flow/run-start-allocation@1" || payload.project_id !== projectId
|
|
118
130
|
|| payload.request_id !== requestId || payload.run_id !== row.run_id || !Array.isArray(payload.blobs) || !Array.isArray(payload.artifacts)
|
|
119
131
|
|| !initial || initial.run_id !== row.run_id || initial.project?.id !== projectId || initial.start_request !== undefined
|
|
@@ -121,7 +133,7 @@ function readRunStartRequestSnapshot(context, projectId, requestId) {
|
|
|
121
133
|
|| !marker || marker.schema_id !== "dd-flow/run-start-request@1" || marker.request_id !== requestId
|
|
122
134
|
|| marker.request_sha256 !== payload.request_sha256 || marker.allocation_sha256 !== row.payload_sha256
|
|
123
135
|
|| receipt.run_id !== row.run_id || receipt.request_id !== requestId || receipt.request_sha256 !== payload.request_sha256 || receipt.allocation_sha256 !== row.payload_sha256
|
|
124
|
-
|| run.project_root !== payload.project_root || run.workspace_root !== payload.workspace_root || run.run_root !== payload.run_root
|
|
136
|
+
|| run.project_root !== currentPath(payload.project_root) || run.workspace_root !== currentPath(payload.workspace_root) || run.run_root !== currentPath(payload.run_root)
|
|
125
137
|
|| run.flow_kind !== initial.flow_kind || run.subject_id !== payload.subject_id || run.subject_type !== payload.subject_type
|
|
126
138
|
|| !object(index.execution_bundle) || hybridDigest(index.execution_bundle) !== hybridDigest(initial.execution_bundle))
|
|
127
139
|
refuseRunStart("allocated_run_changed");
|
package/dist/services/runs.js
CHANGED
|
@@ -27,6 +27,7 @@ import { RetainedExecutionBundle } from "./execution-bundles.js";
|
|
|
27
27
|
import { admitBundledFlow, bundledHandlerContext } from "./stage-handlers.js";
|
|
28
28
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
29
29
|
import { projectPacketLocators, runContextRelocations } from "./stage-context.js";
|
|
30
|
+
import { readRunEngineBinding } from "./run-engine-bindings.js";
|
|
30
31
|
const runSchemaId = "dd-flow/flow-run@3";
|
|
31
32
|
const runtimeSchemaId = "dd-flow/flow-run@3";
|
|
32
33
|
const allowedRunFlowKinds = [
|
|
@@ -36,6 +37,7 @@ const allowedRunFlowKinds = [
|
|
|
36
37
|
"mb-init",
|
|
37
38
|
"mb-upgrade",
|
|
38
39
|
"mb-audit",
|
|
40
|
+
"project-review",
|
|
39
41
|
"mb-distill",
|
|
40
42
|
"mb-upgrade-review",
|
|
41
43
|
"mb-sdlc-review",
|
|
@@ -1843,7 +1845,10 @@ export function readFlowRunExecutionBundle(context, input) {
|
|
|
1843
1845
|
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed", 1);
|
|
1844
1846
|
}
|
|
1845
1847
|
const admission = { ...retained, receipts: new Map(retained.receipts), descriptorContext: bundledHandlerContext(new Map()) };
|
|
1846
|
-
const
|
|
1848
|
+
const relocations = runContextRelocations(context, run.project_id, run.id);
|
|
1849
|
+
const binding = relocations.length ? readRunEngineBinding(path.join(runArtifactRoot(run), "engine-binding.json")) : null;
|
|
1850
|
+
const relocation = binding?.source === "fork_upgrade" ? { binding, relocations } : undefined;
|
|
1851
|
+
const bundle = RetainedExecutionBundle.read(path.join(runArtifactRoot(run), "execution-bundle"), anchor.manifest_sha256, admission, relocation);
|
|
1847
1852
|
assertBundleRunIdentity(context, run, bundle);
|
|
1848
1853
|
if (bundle.manifest.descriptor.sha256 !== anchor.descriptor_sha256 || bundle.manifest.flow_id !== anchor.flow_id) {
|
|
1849
1854
|
throw new AppError("run_bundle_anchor_invalid", "RUN bundle descriptor identity changed", 1);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -16,15 +17,19 @@ import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
|
|
|
16
17
|
import { harnessKey } from "./harness-config.js";
|
|
17
18
|
import { canonicalNativeOperation } from "../harness-runtime/lib/adapter-timeouts.mjs";
|
|
18
19
|
import { retainedDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
|
|
20
|
+
import { validateNativeLaunchEnvironment } from "../harness-runtime/lib/daemon-operations.mjs";
|
|
19
21
|
const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
20
|
-
const nativeLaunchEnvironmentKeys = ["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"];
|
|
21
22
|
function nativeLaunchEnvironment(context, saved) {
|
|
22
23
|
const env = { ...context.env };
|
|
23
24
|
for (const name of ["DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN", "DD_FLOW_DAEMON_ID", "DD_FLOW_RUNTIME_OWNER"])
|
|
24
25
|
delete env[name];
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
try {
|
|
27
|
+
validateNativeLaunchEnvironment(saved);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new AppError("runtime_scope_native_launch_required", "Native launch has an unsupported or invalid configuration environment", 1);
|
|
31
|
+
}
|
|
32
|
+
for (const name of Object.keys(saved)) {
|
|
28
33
|
if (saved[name] === null)
|
|
29
34
|
delete env[name];
|
|
30
35
|
else if (typeof saved[name] === "string")
|
|
@@ -460,8 +465,9 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
|
|
|
460
465
|
}
|
|
461
466
|
if (!launch || launch.schema_id !== "dd-flow/native-launch@1" || launch.executable !== source.runtime_owner?.adapter_executable || !path.isAbsolute(launch.executable) || !path.isAbsolute(launch.cwd) || !Array.isArray(launch.args) || launch.args[0] !== "daemon" || launch.args[1] !== "start" || launch.args[launch.args.indexOf("--state-dir") + 1] !== source.state_dir || !isDeepStrictEqual(launchOwner, source.runtime_owner))
|
|
462
467
|
throw new AppError("runtime_scope_native_launch_required", "Native recovery requires the original owned launch contract", 1);
|
|
463
|
-
if (
|
|
468
|
+
if (launch.args.some(arg => typeof arg !== "string"))
|
|
464
469
|
throw new AppError("runtime_scope_native_launch_required", "Native recovery requires explicit configuration presence before retiring its owner", 1);
|
|
470
|
+
nativeLaunchEnvironment(context, launch.env);
|
|
465
471
|
let dispatch = false;
|
|
466
472
|
const preparedInventory = db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]) ? null : nativeInventoryVersion(source.state_dir);
|
|
467
473
|
const operation = db.writeTransaction(() => {
|
|
@@ -494,7 +500,7 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
|
|
|
494
500
|
assertOwner();
|
|
495
501
|
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [JSON.stringify(operation), context.now(), key]);
|
|
496
502
|
}
|
|
497
|
-
if (operation.receipt.stopped !== true || operation.receipt.
|
|
503
|
+
if (operation.receipt.stopped !== true || !nativeShutdownConfirmed(operation.receipt, { daemonId: source.daemon_id }) || state().shutdown_state !== "clean" || processTreeIsAlive(record))
|
|
498
504
|
return { ...source, ...operation, pending: true, reason: "native_retirement_settlement_required" };
|
|
499
505
|
assertOwner();
|
|
500
506
|
operation.status = "retired";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import os from "node:os";
|
|
@@ -30,7 +31,7 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
30
31
|
if (!state.daemon_id || !owner || owner.budget?.scope_id !== scopeId || owner.owner_id !== record.owner_id || owner.role !== metadata.role || owner.state_dir !== metadata.state_dir || owner.adapter_executable !== metadata.adapter_executable || !retainedResourceHomeMatches(context, owner.resource_home) || (owner.project_id ?? null) !== record.project_id || (owner.run_id ?? null) !== record.run_id || (owner.work_id ?? null) !== record.work_id || !record.operation_id?.startsWith(`${owner.operation_id}:`) || state.resource_process?.id !== record.id || state.resource_process.lease_token !== record.lease_token || state.pid !== record.pid)
|
|
31
32
|
throw new AppError("runtime_scope_owner_unknown", "Native owner does not match its registration", 1);
|
|
32
33
|
const closed = state.shutdown_state === "clean" && state.active_tree === false && !processTreeIsAlive(record)
|
|
33
|
-
? retainedDaemonOperations(metadata.state_dir).find(operation => operation.daemon_id === state.daemon_id && operation.operation === "daemon.stop" && operation.state === "completed" && operation.result?.stopped === true && operation.result
|
|
34
|
+
? retainedDaemonOperations(metadata.state_dir).find(operation => operation.daemon_id === state.daemon_id && operation.operation === "daemon.stop" && operation.state === "completed" && operation.result?.stopped === true && nativeShutdownConfirmed(operation.result, { daemonId: state.daemon_id ?? "" })) : undefined;
|
|
34
35
|
const operationId = closed ? String(closed.operation_id) : `scope-stop:${crypto.createHash("sha256").update(JSON.stringify([scopeId, record.id, state.daemon_id])).digest("hex")}`;
|
|
35
36
|
const requested = path.join(metadata.state_dir, "operations", crypto.createHash("sha256").update(operationId).digest("hex"), "requested.json");
|
|
36
37
|
const prior = fs.existsSync(requested);
|
|
@@ -56,7 +57,7 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
56
57
|
const finalOwner = managedProcessStatus(context).find(item => item.id === record.id);
|
|
57
58
|
if (!finalOwner || finalOwner.lease_token !== record.lease_token || finalOwner.pid !== record.pid || finalOwner.pid_started_at !== record.pid_started_at || after.resource_process?.id !== record.id || after.resource_process.lease_token !== record.lease_token || JSON.stringify(after.config?.runtime_owner) !== JSON.stringify(owner))
|
|
58
59
|
throw new AppError("runtime_scope_owner_changed", "Native ownership changed during control", 1);
|
|
59
|
-
if (result?.stopped !== true || result.
|
|
60
|
+
if (result?.stopped !== true || !nativeShutdownConfirmed(result, { daemonId: state.daemon_id }) || after.daemon_id !== state.daemon_id || after.shutdown_state !== "clean" || after.active_tree !== false || processTreeIsAlive(finalOwner))
|
|
60
61
|
throw new AppError("runtime_scope_not_settled", "Native close has not proved whole-tree settlement", 1);
|
|
61
62
|
const processes = managedProcessStatus(context).filter(item => managedProviderBelongsToDaemon(item, finalOwner, after) && item.operation_id?.startsWith(`${owner.operation_id}:`) && !processTreeIsAlive(item)).map(item => ({ process_id: item.id, native_settled: true, binding: managedProcessBinding(item, resourceHome(context)) }));
|
|
62
63
|
return { process_id: record.id, state_dir: metadata.state_dir, settled: true, native_settled: true, physical_stopped: true, operation_id: operationId, owner_operation_id: owner.operation_id, processes };
|
|
@@ -8,6 +8,7 @@ import { AppError, errorRecord } from "../shared/errors.js";
|
|
|
8
8
|
import { getResourceDatabase } from "../storage/database.js";
|
|
9
9
|
import { assertManagedProcessLease, confirmManagedProcess, finishManagedProcess, renewManagedProcess, processIsAlive, processStartedAt, registerManagedProcess, resourceHome } from "./managed-processes.js";
|
|
10
10
|
import { managedLeaseMonitor } from "./managed-lease-monitor.js";
|
|
11
|
+
import { physicalStartup } from "./physical-startup.js";
|
|
11
12
|
import { publicRuntimeScopeWorker, requestRuntimeScopeControl, runtimeBudgetStatus, prepareRuntimeScopeFence, parseRuntimeBudget } from "./runtime-budget.js";
|
|
12
13
|
import { controlRuntimeScopeRuns } from "./runtime-scope-control.js";
|
|
13
14
|
import { prepareRuntimeScopeResume, recordRuntimeScopeResume } from "./runtime-scope-resume.js";
|
|
@@ -138,8 +139,7 @@ export function prepareRuntimeScopeServing(context, scopeId, generation) {
|
|
|
138
139
|
}
|
|
139
140
|
return { key, token };
|
|
140
141
|
}
|
|
141
|
-
function requireStartingScopeWorker(db, key, token) {
|
|
142
|
-
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
|
|
142
|
+
function requireStartingScopeWorker(db, key, token, row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token])) {
|
|
143
143
|
if (!row)
|
|
144
144
|
throw new AppError("runtime_scope_owner_unknown", "Scope worker claim changed", 1);
|
|
145
145
|
const worker = JSON.parse(row.metadata_json);
|
|
@@ -150,29 +150,55 @@ function requireStartingScopeWorker(db, key, token) {
|
|
|
150
150
|
export async function serveRuntimeScopeControl(context, scopeId, generation) {
|
|
151
151
|
const { key, token } = prepareRuntimeScopeServing(context, scopeId, generation);
|
|
152
152
|
const db = getResourceDatabase(resourceHome(context));
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
db.
|
|
159
|
-
|
|
160
|
-
|
|
153
|
+
const startup = physicalStartup(context.databaseDeadlineAt);
|
|
154
|
+
let claimedOwner = undefined;
|
|
155
|
+
const assertStartupCurrent = () => {
|
|
156
|
+
const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
|
|
157
|
+
const fence = db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
|
|
158
|
+
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
|
|
159
|
+
const current = row ? JSON.parse(row.metadata_json) : null;
|
|
160
|
+
if (!control || JSON.parse(control.metadata_json).generation !== generation || fence || !current || current.token !== token || current.status !== (claimedOwner ? "running" : "starting") || current.owner?.pid !== claimedOwner?.pid || current.owner?.pid_started_at !== claimedOwner?.pid_started_at)
|
|
161
|
+
throw new AppError("runtime_scope_control_superseded", "Scope startup no longer owns this control", 1);
|
|
162
|
+
};
|
|
163
|
+
const starting = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
|
|
164
|
+
let worker = requireStartingScopeWorker(db, key, token, starting);
|
|
165
|
+
let retainedMetadata = starting.metadata_json;
|
|
166
|
+
let claimedByThisServe = false;
|
|
167
|
+
const persistWorker = () => {
|
|
168
|
+
const next = JSON.stringify(worker);
|
|
169
|
+
if (db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ? AND metadata_json = ?", [next, context.now(), key, token, retainedMetadata]).changes !== 1)
|
|
170
|
+
throw new AppError("runtime_scope_owner_unknown", "Scope owner changed before its retained result could be saved", 1);
|
|
171
|
+
retainedMetadata = next;
|
|
172
|
+
};
|
|
161
173
|
let heartbeat;
|
|
162
174
|
let lease;
|
|
163
175
|
// A fence must never be held indefinitely by an observer that cannot obtain
|
|
164
176
|
// fresh provider evidence. A later operator request continues the retained
|
|
165
177
|
// budget; this owner records why it stopped without claiming settlement.
|
|
166
|
-
|
|
167
|
-
worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation: observation.state() };
|
|
178
|
+
let observationCleanup;
|
|
168
179
|
let cleanupFailure;
|
|
169
180
|
const result = await (async () => {
|
|
170
181
|
try {
|
|
182
|
+
const owner = await startup.birth(assertStartupCurrent);
|
|
183
|
+
const claim = db.writeTransaction(() => {
|
|
184
|
+
assertStartupCurrent();
|
|
185
|
+
const current = requireStartingScopeWorker(db, key, token);
|
|
186
|
+
current.owner = owner;
|
|
187
|
+
current.status = "running";
|
|
188
|
+
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(current), context.now(), key, token]);
|
|
189
|
+
return { worker: current, metadata: JSON.stringify(current) };
|
|
190
|
+
});
|
|
191
|
+
worker = claim.worker;
|
|
192
|
+
retainedMetadata = claim.metadata;
|
|
193
|
+
claimedOwner = owner;
|
|
194
|
+
claimedByThisServe = true;
|
|
195
|
+
const observation = observationCleanup = recoveryObservationBudget(worker.snapshot);
|
|
196
|
+
worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation: observation.state() };
|
|
171
197
|
const record = registerManagedProcess(context, { kind: "scope-control", ownerId: key, operationId: `scope-control:${key}:${token}`, ownerPid: process.pid, metadata: { role: "control", scope_id: scopeId, generation } });
|
|
172
198
|
worker.process_id = record.id;
|
|
173
199
|
worker.process_lease_token = record.lease_token;
|
|
174
|
-
const confirmed = confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
|
|
175
|
-
|
|
200
|
+
const confirmed = await startup.observe(() => confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid }), assertStartupCurrent, record.lease_expires_at);
|
|
201
|
+
persistWorker();
|
|
176
202
|
lease = managedLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: record.id, leaseToken: record.lease_token }), confirmed.lease_expires_at);
|
|
177
203
|
let timerFailure;
|
|
178
204
|
const assertScopeCurrent = () => {
|
|
@@ -233,7 +259,7 @@ export async function serveRuntimeScopeControl(context, scopeId, generation) {
|
|
|
233
259
|
await lease.ensureConfirmed(assertScopeCurrent);
|
|
234
260
|
observation.observe(outcome);
|
|
235
261
|
worker.snapshot = { ...outcome, recovery_observation: observation.state() };
|
|
236
|
-
|
|
262
|
+
persistWorker();
|
|
237
263
|
if (worker.snapshot.drain?.physical_settled === true) {
|
|
238
264
|
worker.status = "completed";
|
|
239
265
|
break;
|
|
@@ -259,18 +285,19 @@ export async function serveRuntimeScopeControl(context, scopeId, generation) {
|
|
|
259
285
|
lease?.close();
|
|
260
286
|
if (heartbeat)
|
|
261
287
|
clearInterval(heartbeat);
|
|
262
|
-
|
|
263
|
-
worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation:
|
|
288
|
+
observationCleanup?.exhausted();
|
|
289
|
+
worker.snapshot = { ...(worker.snapshot ?? {}), ...(observationCleanup ? { recovery_observation: observationCleanup.state() } : {}) };
|
|
264
290
|
let persistenceFailure;
|
|
265
291
|
try {
|
|
266
|
-
|
|
292
|
+
persistWorker();
|
|
267
293
|
}
|
|
268
294
|
catch (error) {
|
|
269
295
|
persistenceFailure = error;
|
|
270
296
|
process.stderr.write(`${JSON.stringify({ primary: worker.error, primary_error: worker.snapshot.error, phase: "persist_scope_owner", secondary: errorRecord(error) })}\n`);
|
|
271
297
|
}
|
|
272
298
|
try {
|
|
273
|
-
|
|
299
|
+
const current = claimedByThisServe && db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
|
|
300
|
+
if (current && current.metadata_json === retainedMetadata && !persistenceFailure && worker.process_id && worker.process_lease_token)
|
|
274
301
|
finishManagedProcess(context, { id: worker.process_id, leaseToken: worker.process_lease_token, state: worker.status === "failed" ? "failed" : "stopped", reason: "scope_owner_exited" });
|
|
275
302
|
}
|
|
276
303
|
catch (error) {
|
|
@@ -7,6 +7,7 @@ import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
|
7
7
|
import { requireProjectByRoot } from "./projects.js";
|
|
8
8
|
import { resolveRun } from "./runs.js";
|
|
9
9
|
import { codeExecutionEnvironment, runCheck } from "./code-checks.js";
|
|
10
|
+
import { retainedBrowserEnvironment } from "./check-environment.js";
|
|
10
11
|
import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessStatus, processTreeIsAlive, registerManagedProcess, reservePorts, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
11
12
|
export function prepareRuntimeService(context, input) {
|
|
12
13
|
validateObservationDuration(input.timeoutMs);
|
|
@@ -33,7 +34,7 @@ export async function startRuntimeService(context, input) {
|
|
|
33
34
|
const save = () => { const temporary = `${receiptPath}.${process.pid}.tmp`; fs.writeFileSync(temporary, JSON.stringify(receipt, null, 2)); fs.renameSync(temporary, receiptPath); };
|
|
34
35
|
try {
|
|
35
36
|
const allocation = await reservePorts(context, { ownerId: record.owner_id, processId: record.id, names: input.ports });
|
|
36
|
-
const environment = { ...codeExecutionEnvironment(run.workspace_root), ...Object.fromEntries(Object.entries(allocation.ports).map(([name, port]) => [`DD_FLOW_PORT_${name.toUpperCase()}`, String(port)])), DD_FLOW_EVIDENCE_DIR: directory, DD_FLOW_CHECK_COMPLETION_FILE: path.join(directory, "completion.json") };
|
|
37
|
+
const environment = { ...codeExecutionEnvironment(run.workspace_root, retainedBrowserEnvironment(context.env)), ...Object.fromEntries(Object.entries(allocation.ports).map(([name, port]) => [`DD_FLOW_PORT_${name.toUpperCase()}`, String(port)])), DD_FLOW_EVIDENCE_DIR: directory, DD_FLOW_CHECK_COMPLETION_FILE: path.join(directory, "completion.json") };
|
|
37
38
|
const quoted = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
38
39
|
Object.assign(receipt, { ports: allocation.ports, environment: Object.fromEntries(Object.entries(environment).filter(([key]) => key.startsWith("DD_FLOW_PORT_"))), stop_command: `DD_FLOW_HOME=${quoted(context.ddFlowHome)} DD_FLOW_RESOURCE_HOME=${quoted(resourceHome(context))} dd-flow runtime process stop --id ${record.id} --lease-token ${record.lease_token} --json` });
|
|
39
40
|
save();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { recordFlowSessionObservation } from "./hook-session-observation.js";
|
|
2
3
|
import { continuationPolicies, flowKinds } from "../domain/contracts.js";
|
|
3
4
|
import { AppError } from "../shared/errors.js";
|
|
4
5
|
import { hookSessionIdentity } from "./hooks.js";
|
|
@@ -23,25 +24,7 @@ export function publicFlowSession(context, session) {
|
|
|
23
24
|
delete publicFields.parent_session_id;
|
|
24
25
|
return { ...publicFields, harness_id: identity.harness_id, session_id: identity.session_id, parent_session: parent ? publicSessionIdentity(parent) : null };
|
|
25
26
|
}
|
|
26
|
-
export
|
|
27
|
-
const duplicate = context.db.get("SELECT id FROM flow_session_segments WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
|
|
28
|
-
if (duplicate)
|
|
29
|
-
return { changed: false, segment_id: duplicate.id };
|
|
30
|
-
const open = context.db.get(`SELECT id, run_id, protocol_id FROM flow_session_segments
|
|
31
|
-
WHERE project_id = ? AND session_id = ? AND ended_at IS NULL ORDER BY started_at DESC, id DESC LIMIT 1`, [input.projectId, input.sessionId]);
|
|
32
|
-
const now = context.now();
|
|
33
|
-
const runId = input.runId === undefined ? open?.run_id ?? null : input.runId;
|
|
34
|
-
const protocolId = input.protocolId === undefined ? open?.protocol_id ?? null : input.protocolId;
|
|
35
|
-
if (open && open.run_id === runId && open.protocol_id === protocolId) {
|
|
36
|
-
return { changed: false, segment_id: open.id };
|
|
37
|
-
}
|
|
38
|
-
if (open)
|
|
39
|
-
context.db.run("UPDATE flow_session_segments SET ended_at = ? WHERE id = ? AND ended_at IS NULL", [now, open.id]);
|
|
40
|
-
const result = context.db.run(`INSERT INTO flow_session_segments
|
|
41
|
-
(project_id, session_id, run_id, protocol_id, started_at, ended_at, cwd, tool_name, event_key)
|
|
42
|
-
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)`, [input.projectId, input.sessionId, runId, protocolId, now, input.cwd ?? null, input.toolName ?? null, input.eventKey]);
|
|
43
|
-
return { changed: true, segment_id: Number(result.lastInsertRowid) };
|
|
44
|
-
}
|
|
27
|
+
export { recordFlowSessionObservation } from "./hook-session-observation.js";
|
|
45
28
|
export function registerFlowJob(context, input) {
|
|
46
29
|
const jobId = input.jobId ?? `JOB-${input.runId}-${input.planItemId}`.replace(/[^A-Za-z0-9_-]+/g, "-");
|
|
47
30
|
const now = context.now();
|
|
@@ -167,11 +150,7 @@ export function stoppedMergeWorkerState(context, projectId, workerId) {
|
|
|
167
150
|
status: latest?.status ?? null
|
|
168
151
|
};
|
|
169
152
|
}
|
|
170
|
-
export
|
|
171
|
-
return context.db.all(`SELECT * FROM sessions
|
|
172
|
-
WHERE project_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')
|
|
173
|
-
ORDER BY updated_at DESC`, [projectId]);
|
|
174
|
-
}
|
|
153
|
+
export { activeFlowSessionsForProject } from "./hook-session-observation.js";
|
|
175
154
|
export function flowSessionsForProject(context, projectId, filter = {}) {
|
|
176
155
|
if (filter.sessionId) {
|
|
177
156
|
return context.db.all("SELECT * FROM sessions WHERE project_id = ? AND (session_id = ? OR provider_session_id = ?) ORDER BY updated_at DESC", [projectId, filter.sessionId, filter.sessionId]);
|
package/dist/services/setup.js
CHANGED
|
@@ -60,7 +60,7 @@ function atomicJson(file, value) {
|
|
|
60
60
|
fs.unlinkSync(temporary);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
function readSetupRequest(file) {
|
|
63
|
+
function readSetupRequest(file, validateExecutor = true) {
|
|
64
64
|
const value = regularJson(file);
|
|
65
65
|
const required = ["schema_id", "request_id", "identity", "previous_selection", "phase", "installation"];
|
|
66
66
|
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
@@ -77,7 +77,7 @@ function readSetupRequest(file) {
|
|
|
77
77
|
|| !["prepared", "canon_cached", "engine_cached", "profiles_saved", "committed"].includes(request.phase)
|
|
78
78
|
|| !request.installation || typeof request.installation !== "object" || Array.isArray(request.installation))
|
|
79
79
|
refuse("invalid_request_record");
|
|
80
|
-
if (Object.hasOwn(request, "executor") && request.executor !== null) {
|
|
80
|
+
if (validateExecutor && Object.hasOwn(request, "executor") && request.executor !== null) {
|
|
81
81
|
try {
|
|
82
82
|
prepareOperatorProfile(request.executor);
|
|
83
83
|
}
|
|
@@ -85,7 +85,7 @@ function readSetupRequest(file) {
|
|
|
85
85
|
refuse("invalid_request_executor");
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
if (Object.hasOwn(request, "profileCreation")) {
|
|
88
|
+
if (validateExecutor && Object.hasOwn(request, "profileCreation")) {
|
|
89
89
|
try {
|
|
90
90
|
validateProfileCreationIntent(request.profileCreation);
|
|
91
91
|
}
|
|
@@ -225,22 +225,72 @@ export function commitSetupOnboarding(home, requestId, selection) {
|
|
|
225
225
|
return result;
|
|
226
226
|
}
|
|
227
227
|
/** Read-only global readiness. No SQLite connection, migration, project registration or probes. */
|
|
228
|
+
function validateInstallation(root, installation) {
|
|
229
|
+
if (!installation.manifest || typeof installation.manifest_bytes_base64 !== "string" || validateHybridShape("release-manifest", installation.manifest).length ||
|
|
230
|
+
installation.canon_root !== path.join(root, "release-cache", "canon", installation.manifest.canon.version, "payload") ||
|
|
231
|
+
installation.engine_root !== path.join(root, "release-cache", "engine", installation.manifest.engine.version, "payload"))
|
|
232
|
+
refuse("invalid_installation_selection");
|
|
233
|
+
const bytes = Buffer.from(installation.manifest_bytes_base64, "base64");
|
|
234
|
+
if (bytes.toString("base64") !== installation.manifest_bytes_base64 || bytesDigest(bytes) !== installation.manifest_sha256 ||
|
|
235
|
+
hybridDigest(manifestFromBytes(bytes)) !== hybridDigest(installation.manifest))
|
|
236
|
+
refuse("invalid_installation_selection");
|
|
237
|
+
}
|
|
238
|
+
/** Independently committed canonical data, never activation or old code
|
|
239
|
+
* admission. A missing old engine cache is irrelevant to comparing documents.
|
|
240
|
+
* Same canon commit/version with different archive bytes is not disambiguated
|
|
241
|
+
* by picking the newest CLI or whichever request happens to be listed first.
|
|
242
|
+
*/
|
|
243
|
+
export function readCommittedCanonRelease(home, version, commit, expectedInstallationSha256) {
|
|
244
|
+
const root = path.resolve(home), directory = path.join(root, "setup-requests");
|
|
245
|
+
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/.test(version) || !/^[a-f0-9]{40}$/.test(commit)
|
|
246
|
+
|| expectedInstallationSha256 !== undefined && !/^[a-f0-9]{64}$/.test(expectedInstallationSha256))
|
|
247
|
+
refuse("invalid_canonical_baseline_selection");
|
|
248
|
+
for (const target of [root, directory])
|
|
249
|
+
if (fs.existsSync(target) && !fs.lstatSync(target).isDirectory())
|
|
250
|
+
refuse("unsafe_setup_directory");
|
|
251
|
+
if (!fs.existsSync(directory))
|
|
252
|
+
return null;
|
|
253
|
+
const matches = [];
|
|
254
|
+
for (const name of fs.readdirSync(directory).filter(name => name.endsWith(".json")).sort()) {
|
|
255
|
+
const request = readSetupRequest(path.join(directory, name), false);
|
|
256
|
+
if (`${request.request_id}.json` !== name)
|
|
257
|
+
refuse("request_identity_conflict");
|
|
258
|
+
if (request.phase !== "committed" || request.installation.manifest?.canon?.version !== version || request.installation.manifest.canon.commit !== commit)
|
|
259
|
+
continue;
|
|
260
|
+
if (expectedInstallationSha256 !== undefined && request.installation.manifest_sha256 !== expectedInstallationSha256)
|
|
261
|
+
continue;
|
|
262
|
+
validateInstallation(root, request.installation);
|
|
263
|
+
const binding = { installation: request.installation,
|
|
264
|
+
...(Object.hasOwn(request, "executor") ? { executor: request.executor } : {}),
|
|
265
|
+
...(request.profileCreation ? { profileCreation: request.profileCreation } : {}),
|
|
266
|
+
...(request.deferredOnboarding ? { deferredOnboarding: true } : {}) };
|
|
267
|
+
if (!["install", "update"].some(action => hybridDigest({ action, ...binding }) === request.identity))
|
|
268
|
+
refuse("request_identity_conflict");
|
|
269
|
+
matches.push(request.installation);
|
|
270
|
+
}
|
|
271
|
+
if (!matches.length)
|
|
272
|
+
return null;
|
|
273
|
+
if (new Set(matches.map(installation => `${installation.manifest.canon.archive.sha256}:${installation.manifest.canon.archive.bytes}`)).size !== 1)
|
|
274
|
+
refuse("ambiguous_canonical_baseline");
|
|
275
|
+
const selected = matches.sort((left, right) => left.manifest_sha256.localeCompare(right.manifest_sha256, "en"))[0];
|
|
276
|
+
try {
|
|
277
|
+
verifyPublishedReleaseCache(path.dirname(selected.canon_root), selected.manifest.canon.archive);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (error.code === "ENOENT")
|
|
281
|
+
return null;
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
return structuredClone(selected);
|
|
285
|
+
}
|
|
228
286
|
export function readSetupStatus(home) {
|
|
229
287
|
const root = path.resolve(home);
|
|
230
288
|
if (fs.existsSync(root) && !fs.lstatSync(root).isDirectory())
|
|
231
289
|
refuse("unsafe_setup_home");
|
|
232
290
|
const file = path.join(root, "installation.json");
|
|
233
291
|
const installation = fs.existsSync(file) ? regularJson(file) : null;
|
|
234
|
-
if (installation)
|
|
235
|
-
|
|
236
|
-
installation.canon_root !== path.join(root, "release-cache", "canon", installation.manifest.canon.version, "payload") ||
|
|
237
|
-
installation.engine_root !== path.join(root, "release-cache", "engine", installation.manifest.engine.version, "payload"))
|
|
238
|
-
refuse("invalid_installation_selection");
|
|
239
|
-
const bytes = Buffer.from(installation.manifest_bytes_base64, "base64");
|
|
240
|
-
if (bytes.toString("base64") !== installation.manifest_bytes_base64 || bytesDigest(bytes) !== installation.manifest_sha256 ||
|
|
241
|
-
hybridDigest(manifestFromBytes(bytes)) !== hybridDigest(installation.manifest))
|
|
242
|
-
refuse("invalid_installation_selection");
|
|
243
|
-
}
|
|
292
|
+
if (installation)
|
|
293
|
+
validateInstallation(root, installation);
|
|
244
294
|
let readiness = installation ? "installed" : "not_installed";
|
|
245
295
|
if (installation) {
|
|
246
296
|
try {
|