@deksden-com/dd-flow-cli 0.9.0-beta.143 → 0.9.0-beta.145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +9 -3
- package/dist/cli/command-metadata.js +33 -14
- package/dist/cli/help.js +32 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +74 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +70 -32
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +12 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -20
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +4 -2
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +11 -4
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-fanout.js +21 -4
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +48 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-start.js +26 -19
- package/dist/services/descriptor-stage-coordinator.js +30 -10
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +104 -93
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +8 -4
- package/dist/services/frozen-engine.js +113 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +2 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +25 -4
- package/dist/services/lifecycle-invocations.js +33 -5
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +28 -2
- package/dist/services/memory-init-controller-start.js +19 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +21 -3
- package/dist/services/memory-init-native-capacity.js +13 -0
- package/dist/services/memory-init-preflight.js +11 -1
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-layer.js +68 -4
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +77 -23
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +74 -9
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-controller-process.js +2 -6
- package/dist/services/run-controller-recovery.js +6 -1
- package/dist/services/run-controller.js +131 -80
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +9 -7
- package/dist/services/stage-pause.js +108 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-fanout.js +21 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +80 -33
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +8 -4
package/dist/storage/database.js
CHANGED
|
@@ -5,7 +5,7 @@ import { channel } from "node:diagnostics_channel";
|
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { ensureDir } from "./paths.js";
|
|
7
7
|
import { AppError } from "../shared/errors.js";
|
|
8
|
-
import { executeStoreSchema, readStoreWriterContract, registerStoreWriter, registerReconciliationWriter, STORE_WRITER_VERSION } from "./writer-contract.js";
|
|
8
|
+
import { executeStoreSchema, readStoreWriterContract, registerStoreWriter, registerReconciliationWriter, bindStoreWriterAdmission, STORE_WRITER_VERSION } from "./writer-contract.js";
|
|
9
9
|
import { assertImportAdmission } from "./import-guard.js";
|
|
10
10
|
import { withStoreCleanup, rollbackStoreTransaction } from "./native-cleanup.js";
|
|
11
11
|
const workContinuationStorageSql = `
|
|
@@ -25,7 +25,7 @@ function closeAfterOpenFailure(db, error) {
|
|
|
25
25
|
db.close(); });
|
|
26
26
|
}
|
|
27
27
|
const require = createRequire(import.meta.url);
|
|
28
|
-
const { DatabaseSync } = require("node:sqlite");
|
|
28
|
+
const { DatabaseSync, constants } = require("node:sqlite");
|
|
29
29
|
const resourceDatabases = new Map();
|
|
30
30
|
const resourceDatabaseIdentities = new WeakMap();
|
|
31
31
|
function assertResourceDatabaseIdentity(database) {
|
|
@@ -400,6 +400,8 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
400
400
|
if (!exists || mode === "migrate")
|
|
401
401
|
ensureDir(ddFlowHome);
|
|
402
402
|
const db = mode === "read_existing" ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
|
|
403
|
+
if (mode !== "read_existing")
|
|
404
|
+
installWriterAdmission(db, () => assertImportAdmission(ddFlowHome, importPermitId), () => db.isTransaction, true);
|
|
403
405
|
try {
|
|
404
406
|
db.exec(`PRAGMA busy_timeout = ${deadline?.("store_open") ?? 4000}`);
|
|
405
407
|
}
|
|
@@ -408,10 +410,12 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
408
410
|
}
|
|
409
411
|
if (mode !== "read_existing") {
|
|
410
412
|
try {
|
|
413
|
+
const admission = () => { if (!db.isTransaction)
|
|
414
|
+
assertImportAdmission(ddFlowHome, importPermitId); };
|
|
411
415
|
if (mode === "reconcile")
|
|
412
|
-
registerReconciliationWriter(db);
|
|
416
|
+
registerReconciliationWriter(db, admission);
|
|
413
417
|
else
|
|
414
|
-
registerStoreWriter(db);
|
|
418
|
+
registerStoreWriter(db, admission);
|
|
415
419
|
}
|
|
416
420
|
catch (error) {
|
|
417
421
|
return closeAfterOpenFailure(db, error);
|
|
@@ -464,7 +468,7 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
464
468
|
}
|
|
465
469
|
}
|
|
466
470
|
db.exec("PRAGMA foreign_keys = ON");
|
|
467
|
-
return databaseFacade(dbPath, mode !== "read_existing", db, undefined, deadline);
|
|
471
|
+
return databaseFacade(dbPath, mode !== "read_existing", db, undefined, deadline, mode === "read_existing" ? undefined : () => assertImportAdmission(ddFlowHome, importPermitId));
|
|
468
472
|
}
|
|
469
473
|
/**
|
|
470
474
|
* Hook ingress is a hot, synchronous path. It may write receipts, but it
|
|
@@ -526,13 +530,15 @@ export function withResourceReadSnapshot(resourceHome, read) {
|
|
|
526
530
|
return withStoreCleanup(operation, () => { if (native.isOpen)
|
|
527
531
|
native.close(); });
|
|
528
532
|
}
|
|
529
|
-
export function getResourceDatabase(resourceHome, mode = "initialize", deadlineAt) {
|
|
533
|
+
export function getResourceDatabase(resourceHome, mode = "initialize", deadlineAt, importPermitId) {
|
|
530
534
|
channel("dd-flow.sqlite.ingress").publish({ phase: "store_open", store_kind: "resource" });
|
|
531
535
|
const deadline = requestDeadline(deadlineAt);
|
|
532
536
|
deadline?.("resource_open");
|
|
533
537
|
const dbPath = path.join(resourceHome, "runtime.sqlite");
|
|
538
|
+
if (mode !== "read_existing" && mode !== "read_isolated")
|
|
539
|
+
assertImportAdmission(resourceHome, importPermitId);
|
|
534
540
|
if (mode === "retirement" && !fs.existsSync(dbPath))
|
|
535
|
-
return getResourceDatabase(resourceHome, "initialize", deadlineAt);
|
|
541
|
+
return getResourceDatabase(resourceHome, "initialize", deadlineAt, importPermitId);
|
|
536
542
|
const cached = resourceDatabases.get(dbPath);
|
|
537
543
|
if (mode === "create_new") {
|
|
538
544
|
if (cached)
|
|
@@ -564,30 +570,29 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
|
|
|
564
570
|
if (mode === "prepared_existing" && !fs.existsSync(dbPath))
|
|
565
571
|
throw new AppError("resource_storage_unprepared", "Prepare the resource registry before maintenance", 1);
|
|
566
572
|
if (cached) {
|
|
573
|
+
if (importPermitId && resourceDatabaseIdentities.get(cached)?.importPermitId !== importPermitId)
|
|
574
|
+
throw new AppError("resource_writer_reopen_required", "Close the owned cached resource writer before opening a privileged maintenance connection", 1);
|
|
567
575
|
if (mode === "prepared_existing" || mode === "retirement")
|
|
568
576
|
assertResourceDatabaseIdentity(cached);
|
|
569
577
|
const bounded = deadlineAt === undefined ? cached : cached.withDeadline(deadlineAt);
|
|
570
578
|
const reader = { prepare: (sql) => ({ get: (...params) => bounded.get(sql, params), all: (...params) => bounded.all(sql, params), run: (...params) => bounded.run(sql, params) }) };
|
|
571
|
-
|
|
572
|
-
assertResourceStoreReady(reader, mode === "retirement");
|
|
573
|
-
else {
|
|
574
|
-
const required = readStoreWriterContract(reader);
|
|
575
|
-
if (required !== null && required !== STORE_WRITER_VERSION)
|
|
576
|
-
throw new AppError("store_writer_incompatible", "Retirement writer cannot admit productive resource operations", 1, { required, supported: STORE_WRITER_VERSION });
|
|
577
|
-
}
|
|
579
|
+
assertResourceStoreReady(reader, mode === "retirement");
|
|
578
580
|
return bounded;
|
|
579
581
|
}
|
|
580
582
|
if (mode !== "prepared_existing" && mode !== "retirement")
|
|
581
583
|
ensureDir(resourceHome);
|
|
582
584
|
channel("dd-flow.sqlite.ingress").publish({ phase: "store_connection", store_kind: "resource" });
|
|
583
585
|
const db = new DatabaseSync(dbPath);
|
|
586
|
+
installWriterAdmission(db, () => assertImportAdmission(resourceHome, importPermitId), () => db.isTransaction, true);
|
|
584
587
|
try {
|
|
585
588
|
channel("dd-flow.sqlite.ingress").publish({ phase: "store_writer_registration", store_kind: "resource" });
|
|
586
589
|
db.exec(`PRAGMA busy_timeout = ${deadline?.("resource_open") ?? 4000}`);
|
|
590
|
+
const admission = () => { if (!db.isTransaction)
|
|
591
|
+
assertImportAdmission(resourceHome, importPermitId); };
|
|
587
592
|
if (mode === "retirement")
|
|
588
|
-
registerReconciliationWriter(db);
|
|
593
|
+
registerReconciliationWriter(db, admission);
|
|
589
594
|
else
|
|
590
|
-
registerStoreWriter(db);
|
|
595
|
+
registerStoreWriter(db, admission);
|
|
591
596
|
}
|
|
592
597
|
catch (error) {
|
|
593
598
|
return closeAfterOpenFailure(db, error);
|
|
@@ -659,12 +664,12 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
|
|
|
659
664
|
const database = databaseFacade(dbPath, true, db, () => {
|
|
660
665
|
resourceDatabases.delete(dbPath);
|
|
661
666
|
db.close?.();
|
|
662
|
-
});
|
|
667
|
+
}, undefined, () => assertImportAdmission(resourceHome, importPermitId));
|
|
663
668
|
try {
|
|
664
669
|
const identity = fs.lstatSync(dbPath);
|
|
665
670
|
if (!identity.isFile())
|
|
666
671
|
throw new AppError("resource_storage_changed", "Resource registry target is not a regular file", 1);
|
|
667
|
-
resourceDatabaseIdentities.set(database, { dev: identity.dev, ino: identity.ino });
|
|
672
|
+
resourceDatabaseIdentities.set(database, { dev: identity.dev, ino: identity.ino, ...(importPermitId ? { importPermitId } : {}) });
|
|
668
673
|
}
|
|
669
674
|
catch (error) {
|
|
670
675
|
return closeAfterOpenFailure(db, error);
|
|
@@ -713,7 +718,25 @@ function emptyReadOnlyDatabase(dbPath) {
|
|
|
713
718
|
all: () => []
|
|
714
719
|
};
|
|
715
720
|
}
|
|
716
|
-
function
|
|
721
|
+
function installWriterAdmission(db, assertAdmission, reserved, initializing = false) {
|
|
722
|
+
const writes = new Set([constants.SQLITE_INSERT, constants.SQLITE_UPDATE, constants.SQLITE_DELETE,
|
|
723
|
+
constants.SQLITE_CREATE_INDEX, constants.SQLITE_CREATE_TABLE, constants.SQLITE_CREATE_TEMP_INDEX, constants.SQLITE_CREATE_TEMP_TABLE,
|
|
724
|
+
constants.SQLITE_CREATE_TEMP_TRIGGER, constants.SQLITE_CREATE_TEMP_VIEW, constants.SQLITE_CREATE_TRIGGER, constants.SQLITE_CREATE_VIEW,
|
|
725
|
+
constants.SQLITE_DROP_INDEX, constants.SQLITE_DROP_TABLE, constants.SQLITE_DROP_TEMP_INDEX, constants.SQLITE_DROP_TEMP_TABLE,
|
|
726
|
+
constants.SQLITE_DROP_TEMP_TRIGGER, constants.SQLITE_DROP_TEMP_VIEW, constants.SQLITE_DROP_TRIGGER, constants.SQLITE_DROP_VIEW,
|
|
727
|
+
constants.SQLITE_ATTACH, constants.SQLITE_DETACH, constants.SQLITE_ALTER_TABLE, constants.SQLITE_REINDEX, constants.SQLITE_ANALYZE,
|
|
728
|
+
constants.SQLITE_CREATE_VTABLE, constants.SQLITE_DROP_VTABLE]);
|
|
729
|
+
const filePragmas = new Set(["journal_mode", "wal_checkpoint", "optimize", "incremental_vacuum", "user_version", "application_id", "writable_schema", "auto_vacuum", "page_size", "max_page_count", "encoding", "locking_mode"]);
|
|
730
|
+
db.setAuthorizer((action, first, second) => {
|
|
731
|
+
if (initializing && action === constants.SQLITE_TRANSACTION && first === "BEGIN")
|
|
732
|
+
assertAdmission();
|
|
733
|
+
if (!reserved() && (writes.has(action) || action === constants.SQLITE_PRAGMA
|
|
734
|
+
&& filePragmas.has(first?.toLowerCase() ?? "") && (second !== null || ["wal_checkpoint", "optimize", "incremental_vacuum"].includes(first?.toLowerCase() ?? ""))))
|
|
735
|
+
assertAdmission();
|
|
736
|
+
return constants.SQLITE_OK;
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function databaseFacade(dbPath, writable, db, close = () => db.close?.(), deadline, assertAdmission) {
|
|
717
740
|
let scopedDeadline = deadline;
|
|
718
741
|
let reserved = false;
|
|
719
742
|
let poisoned = false;
|
|
@@ -727,6 +750,16 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.(), deadli
|
|
|
727
750
|
let manualFrames = [];
|
|
728
751
|
let readDepth = 0;
|
|
729
752
|
let ownedWriterToken = null;
|
|
753
|
+
if (writable && assertAdmission) {
|
|
754
|
+
// SQLite identifies DML/DDL even behind comments, CTEs or RETURNING.
|
|
755
|
+
// An already reserved writer may finish; maintenance must acquire its
|
|
756
|
+
// own barriers and recheck drain before mutating either store.
|
|
757
|
+
installWriterAdmission(db, assertAdmission, () => reserved && db.isTransaction);
|
|
758
|
+
// Existing writer triggers also check at execution time. This closes the
|
|
759
|
+
// prepare-before-fence / step-after-fence window for protected DML.
|
|
760
|
+
bindStoreWriterAdmission(db, () => { if (!(reserved && db.isTransaction))
|
|
761
|
+
assertAdmission(); });
|
|
762
|
+
}
|
|
730
763
|
const writerTiming = channel("dd-flow.sqlite.writer");
|
|
731
764
|
const ingressPhase = channel("dd-flow.sqlite.ingress");
|
|
732
765
|
const storeKind = path.basename(dbPath) === "runtime.sqlite" ? "resource" : "run";
|
|
@@ -754,6 +787,7 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.(), deadli
|
|
|
754
787
|
throw new AppError("read_snapshot_write", "Cannot reserve a writer inside a read snapshot", 1);
|
|
755
788
|
if (!writable)
|
|
756
789
|
throw new AppError("read_only_storage", "Read-only access cannot start a write transaction", 1, { path: dbPath });
|
|
790
|
+
assertAdmission?.();
|
|
757
791
|
bound("writer_reservation");
|
|
758
792
|
db.exec("BEGIN IMMEDIATE");
|
|
759
793
|
reserved = true;
|
|
@@ -2,6 +2,8 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { acquireInstallationLock } from "../services/installation-lock.js";
|
|
6
|
+
import { withStoreCleanup } from "./native-cleanup.js";
|
|
5
7
|
/** Outside the replaced runtime tree, so a copied source receipt cannot make
|
|
6
8
|
* an incomplete target look ready. A failed target is diagnostic evidence. */
|
|
7
9
|
export function importGuardPath(home) {
|
|
@@ -31,7 +33,7 @@ function readImportGuard(home) {
|
|
|
31
33
|
if (guard.schema_id !== "dd-flow/import-guard@1" || guard.runtime_home !== canonicalHome(home)
|
|
32
34
|
|| !/^[0-9a-f-]{36}$/.test(guard.import_id) || !/^[a-f0-9]{64}$/.test(guard.source_manifest_sha256)
|
|
33
35
|
|| !path.isAbsolute(guard.project_root) || !path.isAbsolute(guard.workspace_root)
|
|
34
|
-
|| !["in_progress", "completed"].includes(guard.status)) {
|
|
36
|
+
|| !["in_progress", "completed"].includes(guard.status) || guard.operation !== undefined && guard.operation !== "storage-migration") {
|
|
35
37
|
throw new AppError("snapshot_import_incomplete", "Snapshot import guard does not identify this target", 1, { guard_path: file });
|
|
36
38
|
}
|
|
37
39
|
return guard;
|
|
@@ -39,41 +41,96 @@ function readImportGuard(home) {
|
|
|
39
41
|
export function assertImportAdmission(home, permit) {
|
|
40
42
|
const guard = readImportGuard(home);
|
|
41
43
|
if (guard?.status === "in_progress" && guard.import_id !== permit)
|
|
42
|
-
throw new AppError("snapshot_import_incomplete", "Snapshot import has not completed; preserve this target for diagnosis and restore into fresh targets", 1, { guard_path: importGuardPath(home), import_id: guard.import_id });
|
|
44
|
+
throw new AppError(guard.operation === "storage-migration" ? "storage_migration_incomplete" : "snapshot_import_incomplete", guard.operation === "storage-migration" ? "Storage maintenance requires reconciliation of its original request" : "Snapshot import has not completed; preserve this target for diagnosis and restore into fresh targets", 1, { guard_path: importGuardPath(home), import_id: guard.import_id });
|
|
43
45
|
}
|
|
44
|
-
function durableWrite(file, bytes, create) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
function durableWrite(file, bytes, create, expected) {
|
|
47
|
+
const lease = acquireInstallationLock(file + ".lock", crypto.createHash("sha256").update(file).digest("hex"));
|
|
48
|
+
return withStoreCleanup(() => {
|
|
49
|
+
lease.assertOwnership();
|
|
50
|
+
// A different request may have selected maintenance since the caller read
|
|
51
|
+
// a completed guard. Never rename over its now-live admission fence.
|
|
52
|
+
if (!create && JSON.stringify(JSON.parse(fs.readFileSync(file, "utf8"))) !== JSON.stringify(expected))
|
|
53
|
+
throw new AppError("storage_guard_changed", "Re-read the current maintenance owner before continuing", 1);
|
|
54
|
+
const target = create ? file : `${file}.tmp-${crypto.randomUUID()}`;
|
|
49
55
|
try {
|
|
50
|
-
fs.
|
|
56
|
+
fs.writeFileSync(target, bytes, { flag: "wx", mode: 0o600 });
|
|
57
|
+
const descriptor = fs.openSync(target, "r");
|
|
58
|
+
try {
|
|
59
|
+
fs.fsyncSync(descriptor);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
fs.closeSync(descriptor);
|
|
63
|
+
}
|
|
64
|
+
if (!create)
|
|
65
|
+
fs.renameSync(target, file);
|
|
66
|
+
const directory = fs.openSync(path.dirname(file), "r");
|
|
67
|
+
try {
|
|
68
|
+
fs.fsyncSync(directory);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
fs.closeSync(directory);
|
|
72
|
+
}
|
|
51
73
|
}
|
|
52
74
|
finally {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!create)
|
|
56
|
-
fs.renameSync(target, file);
|
|
57
|
-
const directory = fs.openSync(path.dirname(file), "r");
|
|
58
|
-
try {
|
|
59
|
-
fs.fsyncSync(directory);
|
|
75
|
+
if (!create)
|
|
76
|
+
fs.rmSync(target, { force: true });
|
|
60
77
|
}
|
|
61
|
-
|
|
62
|
-
fs.closeSync(directory);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
finally {
|
|
66
|
-
if (!create)
|
|
67
|
-
fs.rmSync(target, { force: true });
|
|
68
|
-
}
|
|
78
|
+
}, () => lease.release());
|
|
69
79
|
}
|
|
70
80
|
export function beginSnapshotImport(input) {
|
|
71
81
|
const importId = crypto.randomUUID();
|
|
72
82
|
const guard = { schema_id: "dd-flow/import-guard@1", import_id: importId, status: "in_progress", source_manifest_sha256: input.sourceManifestSha256,
|
|
73
|
-
runtime_home: canonicalHome(input.home), project_root: path.resolve(input.projectRoot), workspace_root: path.resolve(input.workspaceRoot)
|
|
74
|
-
|
|
83
|
+
runtime_home: canonicalHome(input.home), project_root: path.resolve(input.projectRoot), workspace_root: path.resolve(input.workspaceRoot),
|
|
84
|
+
...(input.operation ? { operation: input.operation } : {}) };
|
|
85
|
+
durableWrite(importGuardPath(input.home), `${JSON.stringify(guard)}\n`, true, null);
|
|
75
86
|
return importId;
|
|
76
87
|
}
|
|
88
|
+
/** Reuse the import admission fence for one exact paired storage cutover.
|
|
89
|
+
* A completed older operation may be superseded; a live one never is. */
|
|
90
|
+
export function beginStorageMaintenance(input) {
|
|
91
|
+
if (!/^[0-9a-f-]{36}$/.test(input.importId) || !/^[a-f0-9]{64}$/.test(input.sourceManifestSha256))
|
|
92
|
+
throw new AppError("storage_migration_request_invalid", "Invalid retained maintenance identity", 2);
|
|
93
|
+
const guard = readImportGuard(input.home);
|
|
94
|
+
const next = { schema_id: "dd-flow/import-guard@1", import_id: input.importId, status: "in_progress",
|
|
95
|
+
source_manifest_sha256: input.sourceManifestSha256, runtime_home: canonicalHome(input.home),
|
|
96
|
+
project_root: path.resolve(input.projectRoot), workspace_root: path.resolve(input.workspaceRoot), operation: "storage-migration" };
|
|
97
|
+
if (guard?.import_id === input.importId && guard.source_manifest_sha256 === input.sourceManifestSha256
|
|
98
|
+
&& guard.project_root === next.project_root && guard.workspace_root === next.workspace_root && guard.operation === next.operation)
|
|
99
|
+
return;
|
|
100
|
+
if (guard?.import_id === input.importId)
|
|
101
|
+
throw new AppError("storage_migration_request_conflict", "A maintenance identity cannot select another request", 1);
|
|
102
|
+
if (guard?.status === "in_progress")
|
|
103
|
+
assertImportAdmission(input.home);
|
|
104
|
+
durableWrite(importGuardPath(input.home), `${JSON.stringify(next)}\n`, !guard, guard);
|
|
105
|
+
}
|
|
106
|
+
/** Explicit recovery may take over only its original paired maintenance scope.
|
|
107
|
+
* Keeping the permit identity makes an old apply unable to reopen recovery. */
|
|
108
|
+
export function beginStorageRecovery(input) {
|
|
109
|
+
if (!/^[a-f0-9]{64}$/.test(input.originalSha256) || !/^[a-f0-9]{64}$/.test(input.recoverySha256))
|
|
110
|
+
throw new AppError("storage_recovery_scope_invalid", "Invalid retained recovery identity", 2);
|
|
111
|
+
const guard = readImportGuard(input.home);
|
|
112
|
+
if (!guard) {
|
|
113
|
+
beginStorageMaintenance({ ...input, sourceManifestSha256: input.originalSha256 });
|
|
114
|
+
return beginStorageRecovery(input);
|
|
115
|
+
}
|
|
116
|
+
if (guard.operation !== "storage-migration" || guard.import_id !== input.importId
|
|
117
|
+
|| guard.project_root !== path.resolve(input.projectRoot) || guard.workspace_root !== path.resolve(input.workspaceRoot)
|
|
118
|
+
|| ![input.originalSha256, input.recoverySha256].includes(guard.source_manifest_sha256))
|
|
119
|
+
throw new AppError("storage_recovery_scope_invalid", "Recovery cannot replace another maintenance scope", 1);
|
|
120
|
+
if (guard.source_manifest_sha256 === input.recoverySha256)
|
|
121
|
+
return;
|
|
122
|
+
durableWrite(importGuardPath(input.home), `${JSON.stringify({ ...guard, status: "in_progress", source_manifest_sha256: input.recoverySha256 })}\n`, false, guard);
|
|
123
|
+
}
|
|
124
|
+
export function completeStorageMaintenance(input) {
|
|
125
|
+
const guard = readImportGuard(input.home);
|
|
126
|
+
if (guard?.operation !== "storage-migration")
|
|
127
|
+
throw new AppError("storage_migration_incomplete", "Completion requires the original storage maintenance fence", 1);
|
|
128
|
+
if (guard?.status === "completed" && guard.operation === "storage-migration" && guard.import_id === input.importId
|
|
129
|
+
&& guard.source_manifest_sha256 === input.sourceManifestSha256 && guard.project_root === path.resolve(input.projectRoot)
|
|
130
|
+
&& guard.workspace_root === path.resolve(input.workspaceRoot))
|
|
131
|
+
return;
|
|
132
|
+
completeSnapshotImport(input.home, input.importId, input);
|
|
133
|
+
}
|
|
77
134
|
/** A fork with its own matching intent may rebuild a completed import when
|
|
78
135
|
* its final fork receipt was lost. Fence the existing home before removing any
|
|
79
136
|
* copied files; an interrupted rebuild remains closed to productive opens. */
|
|
@@ -87,7 +144,7 @@ export function rearmCompletedSnapshotImport(input) {
|
|
|
87
144
|
throw new AppError("snapshot_import_incomplete", "Existing import does not match this completed fork; preserve it for diagnosis", 1, { guard_path: importGuardPath(input.home) });
|
|
88
145
|
}
|
|
89
146
|
const importId = crypto.randomUUID();
|
|
90
|
-
durableWrite(importGuardPath(input.home), `${JSON.stringify({ ...guard, import_id: importId, status: "in_progress" })}\n`, false);
|
|
147
|
+
durableWrite(importGuardPath(input.home), `${JSON.stringify({ ...guard, import_id: importId, status: "in_progress" })}\n`, false, guard);
|
|
91
148
|
return importId;
|
|
92
149
|
}
|
|
93
150
|
export function completeSnapshotImport(home, importId, completed) {
|
|
@@ -98,5 +155,5 @@ export function completeSnapshotImport(home, importId, completed) {
|
|
|
98
155
|
|| guard.source_manifest_sha256 !== completed.sourceManifestSha256) {
|
|
99
156
|
throw new AppError("snapshot_import_incomplete", "Import completion does not match its source and target paths", 1, { guard_path: importGuardPath(home) });
|
|
100
157
|
}
|
|
101
|
-
durableWrite(importGuardPath(home), `${JSON.stringify({ ...guard, status: "completed" })}\n`, false);
|
|
158
|
+
durableWrite(importGuardPath(home), `${JSON.stringify({ ...guard, status: "completed" })}\n`, false, guard);
|
|
102
159
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getDatabase, getResourceDatabase } from "./database.js";
|
|
2
|
+
import { withStoreCleanup } from "./native-cleanup.js";
|
|
3
|
+
import { PreparedMemoryInitRuntimeStorage } from "../services/memory-init-storage.js";
|
|
4
|
+
import { PreparedMemoryInitResourceStorage } from "../services/memory-init-resource-storage.js";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
/** Fixed executing-engine recipe, never an operator command or a normal
|
|
9
|
+
* admission side effect. The paired migration owns backup, drain, permit,
|
|
10
|
+
* preservation verification and recovery for both stores around this step. */
|
|
11
|
+
export function bootstrapMemoryUpgradeSchema(runHome, resourceHome, permitId) {
|
|
12
|
+
try {
|
|
13
|
+
for (const file of [path.join(runHome, "db.sqlite"), path.join(resourceHome, "runtime.sqlite")]) {
|
|
14
|
+
const owner = fs.lstatSync(file);
|
|
15
|
+
if (!owner.isFile() || owner.nlink !== 1)
|
|
16
|
+
throw new AppError("memory_schema_bootstrap_unavailable", "Schema bootstrap requires the existing paired stores", 1);
|
|
17
|
+
}
|
|
18
|
+
const db = getDatabase(runHome, "initialize", permitId);
|
|
19
|
+
withStoreCleanup(() => {
|
|
20
|
+
const resources = getResourceDatabase(resourceHome, "initialize", undefined, permitId);
|
|
21
|
+
withStoreCleanup(() => {
|
|
22
|
+
const context = { ddFlowHome: runHome, db,
|
|
23
|
+
env: { DD_FLOW_HOME: runHome, DD_FLOW_RESOURCE_HOME: resourceHome }, now: () => new Date().toISOString() };
|
|
24
|
+
PreparedMemoryInitRuntimeStorage.prepareUpgrade(context);
|
|
25
|
+
PreparedMemoryInitResourceStorage.prepare(context);
|
|
26
|
+
}, () => resources.close?.());
|
|
27
|
+
}, () => db.close?.());
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// The writer cutover already happened. An admission failure does not mean
|
|
31
|
+
// no effect: retain both maintenance fences for exact replay or recovery.
|
|
32
|
+
if (error instanceof AppError) {
|
|
33
|
+
Object.assign(error.details, { effect: "unknown", next_action: "reconcile_exact_request" });
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
throw new AppError("memory_schema_bootstrap_failed", "Reconcile the retained schema bootstrap before runtime admission", 1, { effect: "unknown", next_action: "reconcile_exact_request" });
|
|
37
|
+
}
|
|
38
|
+
}
|