@cleocode/core 2026.10.8 → 2026.10.9
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/dist/build-provenance.json +2 -2
- package/dist/cloud/auto-sync-command-entry.d.ts +13 -0
- package/dist/cloud/auto-sync-command-entry.d.ts.map +1 -0
- package/dist/cloud/auto-sync-command-entry.js +18 -0
- package/dist/cloud/auto-sync-command-entry.js.map +1 -0
- package/dist/cloud/auto-sync-command.d.ts +64 -0
- package/dist/cloud/auto-sync-command.d.ts.map +1 -0
- package/dist/cloud/auto-sync-command.js +109 -0
- package/dist/cloud/auto-sync-command.js.map +1 -0
- package/dist/cloud/auto-sync.d.ts +111 -0
- package/dist/cloud/auto-sync.d.ts.map +1 -0
- package/dist/cloud/auto-sync.js +275 -0
- package/dist/cloud/auto-sync.js.map +1 -0
- package/dist/cloud/nexus-cloud-device-overview.d.ts +52 -0
- package/dist/cloud/nexus-cloud-device-overview.d.ts.map +1 -0
- package/dist/cloud/nexus-cloud-device-overview.js +105 -0
- package/dist/cloud/nexus-cloud-device-overview.js.map +1 -0
- package/dist/cloud/nexus-cloud-journal-activity.d.ts +56 -0
- package/dist/cloud/nexus-cloud-journal-activity.d.ts.map +1 -0
- package/dist/cloud/nexus-cloud-journal-activity.js +98 -0
- package/dist/cloud/nexus-cloud-journal-activity.js.map +1 -0
- package/dist/cloud/nexus-cloud-status.d.ts.map +1 -1
- package/dist/cloud/nexus-cloud-status.js +88 -5
- package/dist/cloud/nexus-cloud-status.js.map +1 -1
- package/dist/cloud/nexus-vault.d.ts +19 -5
- package/dist/cloud/nexus-vault.d.ts.map +1 -1
- package/dist/cloud/nexus-vault.js +376 -71
- package/dist/cloud/nexus-vault.js.map +1 -1
- package/dist/cloud/replica-rebind.d.ts +14 -0
- package/dist/cloud/replica-rebind.d.ts.map +1 -1
- package/dist/cloud/replica-rebind.js +20 -1
- package/dist/cloud/replica-rebind.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -1
- package/dist/docs/docs-ops.d.ts +4 -2
- package/dist/docs/docs-ops.d.ts.map +1 -1
- package/dist/docs/docs-ops.js +107 -15
- package/dist/docs/docs-ops.js.map +1 -1
- package/dist/doctor/system-health.d.ts +76 -2
- package/dist/doctor/system-health.d.ts.map +1 -1
- package/dist/doctor/system-health.js +264 -41
- package/dist/doctor/system-health.js.map +1 -1
- package/dist/hooks/project-runner.d.ts +10 -0
- package/dist/hooks/project-runner.d.ts.map +1 -0
- package/dist/hooks/project-runner.js +328 -0
- package/dist/hooks/project-runner.js.map +1 -0
- package/dist/hooks/project-state.d.ts +41 -0
- package/dist/hooks/project-state.d.ts.map +1 -0
- package/dist/hooks/project-state.js +331 -0
- package/dist/hooks/project-state.js.map +1 -0
- package/dist/hooks/release-gate.d.ts +61 -0
- package/dist/hooks/release-gate.d.ts.map +1 -0
- package/dist/hooks/release-gate.js +70 -0
- package/dist/hooks/release-gate.js.map +1 -0
- package/dist/nexus/analyze-orchestrator.d.ts +2 -0
- package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
- package/dist/nexus/analyze-orchestrator.js +1 -0
- package/dist/nexus/analyze-orchestrator.js.map +1 -1
- package/dist/nexus/freshness.d.ts +67 -3
- package/dist/nexus/freshness.d.ts.map +1 -1
- package/dist/nexus/freshness.js +130 -9
- package/dist/nexus/freshness.js.map +1 -1
- package/dist/nexus/refresh-child.d.ts +14 -0
- package/dist/nexus/refresh-child.d.ts.map +1 -0
- package/dist/nexus/refresh-child.js +24 -0
- package/dist/nexus/refresh-child.js.map +1 -0
- package/dist/orchestrate/spawn-ops.d.ts +8 -1
- package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
- package/dist/orchestrate/spawn-ops.js +42 -12
- package/dist/orchestrate/spawn-ops.js.map +1 -1
- package/dist/orchestration/spawn.d.ts +5 -0
- package/dist/orchestration/spawn.d.ts.map +1 -1
- package/dist/orchestration/spawn.js +4 -1
- package/dist/orchestration/spawn.js.map +1 -1
- package/dist/release/channel.d.ts.map +1 -1
- package/dist/release/channel.js +10 -0
- package/dist/release/channel.js.map +1 -1
- package/dist/release/github-pr.d.ts.map +1 -1
- package/dist/release/github-pr.js +1 -0
- package/dist/release/github-pr.js.map +1 -1
- package/dist/release/plan.d.ts +8 -3
- package/dist/release/plan.d.ts.map +1 -1
- package/dist/release/plan.js +72 -15
- package/dist/release/plan.js.map +1 -1
- package/dist/release/reconcile.d.ts.map +1 -1
- package/dist/release/reconcile.js +6 -15
- package/dist/release/reconcile.js.map +1 -1
- package/dist/sentient/daemon-entry.d.ts +6 -0
- package/dist/sentient/daemon-entry.d.ts.map +1 -1
- package/dist/sentient/daemon-entry.js +12 -0
- package/dist/sentient/daemon-entry.js.map +1 -1
- package/dist/sentient/worktree-dispatch.d.ts +7 -0
- package/dist/sentient/worktree-dispatch.d.ts.map +1 -1
- package/dist/sentient/worktree-dispatch.js +37 -1
- package/dist/sentient/worktree-dispatch.js.map +1 -1
- package/dist/sessions/session-end-snapshot-entry.d.ts +4 -1
- package/dist/sessions/session-end-snapshot-entry.d.ts.map +1 -1
- package/dist/sessions/session-end-snapshot-entry.js +9 -1
- package/dist/sessions/session-end-snapshot-entry.js.map +1 -1
- package/dist/spawn/agent-identity.d.ts.map +1 -1
- package/dist/spawn/agent-identity.js +96 -1
- package/dist/spawn/agent-identity.js.map +1 -1
- package/dist/spawn/branch-lock.d.ts +27 -8
- package/dist/spawn/branch-lock.d.ts.map +1 -1
- package/dist/spawn/branch-lock.js +57 -73
- package/dist/spawn/branch-lock.js.map +1 -1
- package/dist/store/agent-resolver.d.ts +6 -0
- package/dist/store/agent-resolver.d.ts.map +1 -1
- package/dist/store/agent-resolver.js +2 -0
- package/dist/store/agent-resolver.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts +20 -0
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +81 -8
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/exodus/bare-family.d.ts.map +1 -1
- package/dist/store/exodus/bare-family.js +4 -1
- package/dist/store/exodus/bare-family.js.map +1 -1
- package/dist/store/exodus/migrate.d.ts.map +1 -1
- package/dist/store/exodus/migrate.js +2 -0
- package/dist/store/exodus/migrate.js.map +1 -1
- package/dist/store/exodus/on-open.d.ts.map +1 -1
- package/dist/store/exodus/on-open.js +16 -4
- package/dist/store/exodus/on-open.js.map +1 -1
- package/dist/store/exodus/reconcile.d.ts.map +1 -1
- package/dist/store/exodus/reconcile.js +2 -0
- package/dist/store/exodus/reconcile.js.map +1 -1
- package/dist/store/portable-bundle-import.d.ts +17 -0
- package/dist/store/portable-bundle-import.d.ts.map +1 -1
- package/dist/store/portable-bundle-import.js +46 -31
- package/dist/store/portable-bundle-import.js.map +1 -1
- package/dist/store/portable-bundle-scan.d.ts.map +1 -1
- package/dist/store/portable-bundle-scan.js +5 -0
- package/dist/store/portable-bundle-scan.js.map +1 -1
- package/dist/store/row-identity-registry.d.ts +4 -3
- package/dist/store/row-identity-registry.d.ts.map +1 -1
- package/dist/store/row-identity-registry.js +37 -35
- package/dist/store/row-identity-registry.js.map +1 -1
- package/dist/store/row-identity.d.ts.map +1 -1
- package/dist/store/row-identity.js +6 -1
- package/dist/store/row-identity.js.map +1 -1
- package/dist/store/schema/cleo-global/agent-registry.d.ts +16 -0
- package/dist/store/schema/cleo-global/agent-registry.d.ts.map +1 -1
- package/dist/store/schema/cleo-global/agent-registry.js +4 -1
- package/dist/store/schema/cleo-global/agent-registry.js.map +1 -1
- package/dist/store/schema/cleo-project/provenance-orphans.d.ts +126 -1
- package/dist/store/schema/cleo-project/provenance-orphans.d.ts.map +1 -1
- package/dist/store/schema/cleo-project/provenance-orphans.js +41 -1
- package/dist/store/schema/cleo-project/provenance-orphans.js.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts +40 -0
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.js +122 -0
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/sync/activity.d.ts +64 -0
- package/dist/store/sync/activity.d.ts.map +1 -0
- package/dist/store/sync/activity.js +220 -0
- package/dist/store/sync/activity.js.map +1 -0
- package/dist/store/sync/apply/applier.d.ts.map +1 -1
- package/dist/store/sync/apply/applier.js +58 -2
- package/dist/store/sync/apply/applier.js.map +1 -1
- package/dist/store/sync/apply/write-api.d.ts.map +1 -1
- package/dist/store/sync/apply/write-api.js +45 -3
- package/dist/store/sync/apply/write-api.js.map +1 -1
- package/dist/store/sync/capture.d.ts +6 -4
- package/dist/store/sync/capture.d.ts.map +1 -1
- package/dist/store/sync/capture.js +16 -4
- package/dist/store/sync/capture.js.map +1 -1
- package/dist/store/sync/collision-settle.d.ts +120 -0
- package/dist/store/sync/collision-settle.d.ts.map +1 -1
- package/dist/store/sync/collision-settle.js +342 -5
- package/dist/store/sync/collision-settle.js.map +1 -1
- package/dist/store/sync/flags.d.ts +34 -0
- package/dist/store/sync/flags.d.ts.map +1 -1
- package/dist/store/sync/flags.js +46 -0
- package/dist/store/sync/flags.js.map +1 -1
- package/dist/store/sync/genesis.d.ts +8 -0
- package/dist/store/sync/genesis.d.ts.map +1 -1
- package/dist/store/sync/genesis.js +18 -5
- package/dist/store/sync/genesis.js.map +1 -1
- package/dist/store/sync/push.d.ts +4 -2
- package/dist/store/sync/push.d.ts.map +1 -1
- package/dist/store/sync/push.js +10 -1
- package/dist/store/sync/push.js.map +1 -1
- package/dist/store/sync/rebind.d.ts +20 -0
- package/dist/store/sync/rebind.d.ts.map +1 -1
- package/dist/store/sync/rebind.js +86 -4
- package/dist/store/sync/rebind.js.map +1 -1
- package/dist/store/sync/reconcile-copy.d.ts +152 -0
- package/dist/store/sync/reconcile-copy.d.ts.map +1 -0
- package/dist/store/sync/reconcile-copy.js +428 -0
- package/dist/store/sync/reconcile-copy.js.map +1 -0
- package/dist/store/sync/reconcile.d.ts +80 -0
- package/dist/store/sync/reconcile.d.ts.map +1 -1
- package/dist/store/sync/reconcile.js +97 -5
- package/dist/store/sync/reconcile.js.map +1 -1
- package/dist/store/sync/repair.d.ts +11 -0
- package/dist/store/sync/repair.d.ts.map +1 -1
- package/dist/store/sync/repair.js +14 -3
- package/dist/store/sync/repair.js.map +1 -1
- package/dist/store/sync/replica.d.ts +74 -1
- package/dist/store/sync/replica.d.ts.map +1 -1
- package/dist/store/sync/replica.js +104 -6
- package/dist/store/sync/replica.js.map +1 -1
- package/dist/store/sync/row-meta.d.ts +4 -2
- package/dist/store/sync/row-meta.d.ts.map +1 -1
- package/dist/store/sync/row-meta.js +6 -4
- package/dist/store/sync/row-meta.js.map +1 -1
- package/dist/store/sync/sealer.d.ts +2 -0
- package/dist/store/sync/sealer.d.ts.map +1 -1
- package/dist/store/sync/sealer.js +33 -4
- package/dist/store/sync/sealer.js.map +1 -1
- package/dist/store/sync/structural.d.ts.map +1 -1
- package/dist/store/sync/structural.js +3 -1
- package/dist/store/sync/structural.js.map +1 -1
- package/dist/store/sync/timestamp-canon.d.ts.map +1 -1
- package/dist/store/sync/timestamp-canon.js +2 -0
- package/dist/store/sync/timestamp-canon.js.map +1 -1
- package/dist/store/table-classification.d.ts +29 -0
- package/dist/store/table-classification.d.ts.map +1 -1
- package/dist/store/table-classification.js +85 -19
- package/dist/store/table-classification.js.map +1 -1
- package/dist/store/twin-collapse.d.ts.map +1 -1
- package/dist/store/twin-collapse.js +28 -10
- package/dist/store/twin-collapse.js.map +1 -1
- package/dist/store/vault-manifest.d.ts +5 -0
- package/dist/store/vault-manifest.d.ts.map +1 -1
- package/dist/store/vault-manifest.js +14 -1
- package/dist/store/vault-manifest.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts +15 -5
- package/dist/tasks/affected-packages.d.ts.map +1 -1
- package/dist/tasks/affected-packages.js +20 -8
- package/dist/tasks/affected-packages.js.map +1 -1
- package/dist/tasks/done-record.d.ts +14 -4
- package/dist/tasks/done-record.d.ts.map +1 -1
- package/dist/tasks/done-record.js +21 -9
- package/dist/tasks/done-record.js.map +1 -1
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +45 -4
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/gate-runner.d.ts +5 -4
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +18 -11
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/tasks/tool-cache.d.ts +27 -1
- package/dist/tasks/tool-cache.d.ts.map +1 -1
- package/dist/tasks/tool-cache.js +110 -6
- package/dist/tasks/tool-cache.js.map +1 -1
- package/dist/tools/fs.d.ts +3 -1
- package/dist/tools/fs.d.ts.map +1 -1
- package/dist/tools/fs.js +35 -2
- package/dist/tools/fs.js.map +1 -1
- package/dist/validation/engine-ops.d.ts +10 -3
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +17 -8
- package/dist/validation/engine-ops.js.map +1 -1
- package/migrations/drizzle-cleo-global/20261010180000_t12915-agent-registry-uids/migration.sql +18 -0
- package/migrations/drizzle-cleo-project/20261010170000_t12896b-brain-task-observations/migration.sql +37 -0
- package/migrations/sync-journal/20261009150000_t13335-capture-pin/migration.sql +13 -0
- package/package.json +18 -13
- package/schemas/config.schema.json +12 -0
- package/schemas/project-context.schema.json +5 -0
- package/templates/CLEO-INJECTION.md +2 -2
- package/templates/CLEO-REFERENCE.md +6 -4
- package/templates/git-hooks/pre-push +29 -1
- package/templates/workflows/release-prepare.yml.tmpl +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nexus-vault.d.ts","sourceRoot":"","sources":["../../src/cloud/nexus-vault.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;
|
|
1
|
+
{"version":3,"file":"nexus-vault.d.ts","sourceRoot":"","sources":["../../src/cloud/nexus-vault.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAQH,OAAO,KAAK,EACV,uBAAuB,EACvB,eAAe,EACf,kBAAkB,EAClB,yBAAyB,EACzB,eAAe,EAGf,eAAe,EAEf,sBAAsB,EAGtB,iBAAiB,EAGlB,MAAM,qBAAqB,CAAC;AAkD7B,OAAO,EACL,KAAK,gBAAgB,EAMtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAoB,KAAK,gBAAgB,EAAc,MAAM,uBAAuB,CAAC;AAgB5F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAsChE,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EAIvB,MAAM,uBAAuB,CAAC;AAK/B,gGAAgG;AAChG,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAMjD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;CA6B1B,CAAC;AAkCX,4CAA4C;AAC5C,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,0FAA0F;IAC1F,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB;AAw8DD;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,IAAI,CAAC,oBAAoB,EAAE,MAAM,CAAC,EACxC,KAAK,EAAE,eAAe,EACtB,SAAS,GAAE,MAAM,GAAG,IAAW,GAC9B,OAAO,CAAC,gBAAgB,EAAE,GAAG,SAAS,CAAC,CAmCzC;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,IAAI,CAAC,oBAAoB,EAAE,MAAM,GAAG,UAAU,CAAC,EACrD,KAAK,EAAE,eAAe,EACtB,SAAS,GAAE,MAAM,GAAG,IAAW,GAC9B,OAAO,CAAC,gBAAgB,EAAE,GAAG,SAAS,CAAC,CAUzC;AA2ID,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,gGAAgG;IAChG,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAC3B,2BAA2B;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAC3B,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,aAAa,CAAC,EACvD,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,aAAa,CAAC,GACtD,cAAc,CAkBhB;AAgID,4CAA4C;AAC5C,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACxE,uHAAuH;IACvH,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4FAA4F;IAC5F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACrD;AAyDD,4FAA4F;AAC5F,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAq3BlD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,IAAI,GAAE,wBAAwB,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GACxE,OAAO,CAAC,eAAe,CAAC,CAE1B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,IAAI,GAAE,wBAAwB,GAAG;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GAClE,OAAO,CAAC,yBAAyB,CAAC,CAEpC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,IAAI,GAAE,wBAAwB,GAAG;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GAClE,OAAO,CAAC,gBAAgB,CAAC,CAE3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,GAAE,wBAA6B,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAE7F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CACvB,IAAI,GAAE,wBAAwB,GAAG;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GAClE,OAAO,CAAC,eAAe,CAAC,CAE1B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAE7F;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,GAAE,wBAA6B,GAClC,OAAO,CAAC,sBAAsB,CAAC,CAEjC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,GAAE,wBAAwB,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GACvD,OAAO,CAAC,iBAAiB,CAAC,CAE5B;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,GAAE,wBAA6B,GAClC,OAAO,CAAC,uBAAuB,CAAC,CAElC"}
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
* @epic T12322
|
|
53
53
|
*/
|
|
54
54
|
import { execFileSync } from 'node:child_process';
|
|
55
|
-
import { createHash } from 'node:crypto';
|
|
55
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
56
56
|
import fs from 'node:fs';
|
|
57
57
|
import os from 'node:os';
|
|
58
58
|
import path from 'node:path';
|
|
59
59
|
import { ListCheckpointsResult, ListHomeReplicasResult, ListLeasesResult, ReplayPin, } from '@cleocode/contracts/cloud';
|
|
60
|
-
import { nexusCloudDevicePageSchema, nexusCloudStatusSchema, } from '@cleocode/contracts/nexus-cloud.js';
|
|
60
|
+
import { NEXUS_CLOUD_MAX_PAGES, NEXUS_PAGE_LIMIT_MAX, nexusCloudDevicePageSchema, nexusCloudReplicaPageSchema, nexusCloudStatusSchema, } from '@cleocode/contracts/nexus-cloud.js';
|
|
61
61
|
import { NEXUS_VAULT_LEASE_ROLE, nexusLeaseAcquireSchema, nexusStreamHeadSchema, } from '@cleocode/contracts/nexus-vault.js';
|
|
62
62
|
import { SYNC_SCHEMA_VERSION } from '@cleocode/contracts/sync-schema.js';
|
|
63
63
|
import { readDeclaredProjectIdentity } from '@cleocode/paths';
|
|
@@ -66,16 +66,17 @@ import { z } from 'zod';
|
|
|
66
66
|
import { getCleoHome, resolveOrCwd } from '../paths.js';
|
|
67
67
|
import { withLock } from '../store/lock.js';
|
|
68
68
|
import { exportPortableBundle, globalHomeRules } from '../store/portable-bundle.js';
|
|
69
|
-
import { importPortableBundle } from '../store/portable-bundle-import.js';
|
|
69
|
+
import { extractPortableBundle, importPortableBundle } from '../store/portable-bundle-import.js';
|
|
70
70
|
import { integrityCheck, PROJECT_SECTION_RULES, scanSection, sha256File, } from '../store/portable-bundle-scan.js';
|
|
71
71
|
import { writeRestoreMarker } from '../store/restore-marker.js';
|
|
72
72
|
import { FIRST_OPEN_LOCK_SUFFIX } from '../store/sqlite.js';
|
|
73
|
-
import { isSyncFlagOn, UNRELEASED_FLAGS } from '../store/sync/flags.js';
|
|
73
|
+
import { isSyncFlagOn, setSyncFlag, UNRELEASED_FLAGS } from '../store/sync/flags.js';
|
|
74
74
|
import { completeGenesis, cutGenesisWithSnapshot, GenesisRacedError, GenesisTakenOverError, genesisCutOf, genesisPending, joinStream, readGenesisCut, } from '../store/sync/genesis.js';
|
|
75
|
-
import { pullStream, readStreamCursor } from '../store/sync/pull.js';
|
|
75
|
+
import { pullStream, readStreamCursor, } from '../store/sync/pull.js';
|
|
76
76
|
import { pushStream } from '../store/sync/push.js';
|
|
77
|
+
import { MergeKeyCollisionError, mergeKeyCollisions, reconcileCopy, } from '../store/sync/reconcile-copy.js';
|
|
77
78
|
import { replayPinOf } from '../store/sync/replay-pin.js';
|
|
78
|
-
import { activeReplica, fileIdentity, readActiveReplicaId, rebindAfterVaultRestore, } from '../store/sync/replica.js';
|
|
79
|
+
import { activeReplica, fileIdentity, readActiveReplicaId, rebindAfterVaultRestore, reconcileDue, } from '../store/sync/replica.js';
|
|
79
80
|
import { ReplicaRegistry } from '../store/sync/replica-registry.js';
|
|
80
81
|
import { sealPending } from '../store/sync/sealer.js';
|
|
81
82
|
import { firstBadTxnSignature, signTxn } from '../store/sync/txn-signing.js';
|
|
@@ -92,7 +93,7 @@ import { nexusQueryPath } from './nexus-cloud.js';
|
|
|
92
93
|
import { nexusApiErrorToAccountError } from './nexus-enrol.js';
|
|
93
94
|
import { readNexusProjectLink } from './nexus-link.js';
|
|
94
95
|
import { connectNexusVault, nexusHomeDataKey, nexusProjectDataKey, unlockNexusAccountKey, } from './nexus-vault-keys.js';
|
|
95
|
-
import {
|
|
96
|
+
import { completeOwedRebinds } from './replica-rebind.js';
|
|
96
97
|
import { homeStream, projectStream } from './streams.js';
|
|
97
98
|
/** Default lease length of a push: long enough for a large upload, short enough to hand off. */
|
|
98
99
|
export const NEXUS_VAULT_LEASE_TTL_SECONDS = 900;
|
|
@@ -363,7 +364,7 @@ async function replaySegments(journal, cursor, signers) {
|
|
|
363
364
|
* What works on a stream the change journal writes (its head snapshot is
|
|
364
365
|
* checkpoint/v3), for every message that would otherwise say "push" (T13034).
|
|
365
366
|
*/
|
|
366
|
-
const JOURNAL_STREAM_REMEDY = 'on this stream local changes travel through the change journal (`sync.push`), not vault snapshots (vault snapshots of journal streams are tracked in T12999); `cleo cloud pull`, `cleo cloud restore` and `cleo cloud verify` still work';
|
|
367
|
+
const JOURNAL_STREAM_REMEDY = 'on this stream local changes travel through the change journal (`sync.push`), not vault snapshots (vault snapshots of journal streams are tracked in T12999); a store that has not joined it joins with `cleo sync enable push`, which keeps its own rows and merges them into the stream (T13466); `cleo cloud pull`, `cleo cloud restore` and `cleo cloud verify` still work';
|
|
367
368
|
/** Whether a snapshot is checkpoint/v3: the change journal writes its stream (T13034). */
|
|
368
369
|
function isJournalSnapshot(cp) {
|
|
369
370
|
return cp !== null && cp !== undefined && manifestVersion(cp.manifest) === 3;
|
|
@@ -1192,6 +1193,109 @@ async function enableSyncPushImpl(opts = {}) {
|
|
|
1192
1193
|
// genesis. A store that holds exactly that journal checkpoint JOINS it
|
|
1193
1194
|
// (T13312); any other store restores it first.
|
|
1194
1195
|
if (parent !== null && isJournalSnapshot(parent)) {
|
|
1196
|
+
// A store that never synced with this stream keeps its own rows (T13466):
|
|
1197
|
+
// it joins, then reconciles against the checkpoint pulled to head, which
|
|
1198
|
+
// adopts the stream's rows and emits its own as its first transactions.
|
|
1199
|
+
if (synced === null) {
|
|
1200
|
+
let safetyBackup = '';
|
|
1201
|
+
let cut = 0;
|
|
1202
|
+
// Assigned inside the callback: typed so the checks below are not narrowed to null.
|
|
1203
|
+
let merged = null;
|
|
1204
|
+
// `warnings` copied conn.warnings at the start; the scratch pull adds more (T13551).
|
|
1205
|
+
const connWarned = conn.warnings.length;
|
|
1206
|
+
await withMergedScratch({ conn, key, t }, async (scratch, mergedCursor, baseline) => {
|
|
1207
|
+
// A stream row whose key this store's own row holds cannot be
|
|
1208
|
+
// settled yet (T13504): refuse before anything changes.
|
|
1209
|
+
const collisions = mergeKeyCollisions(db, scratch, tableScopeOf(t));
|
|
1210
|
+
if (collisions.length > 0) {
|
|
1211
|
+
throw vaultError('E_NEXUS_SYNC_REFUSED', new MergeKeyCollisionError(collisions).message, "nothing was joined or changed; give this store's row a key the stream does not hold, then run `cleo sync enable push` again");
|
|
1212
|
+
}
|
|
1213
|
+
// The merge decides field by field which side's value stays: keep the
|
|
1214
|
+
// store as it is now first, so a value it replaces can be recovered.
|
|
1215
|
+
safetyBackup = await exportSafetyBundle(t, 'pre-merge', 'cloud-sync-pre-merge');
|
|
1216
|
+
const joined = joinStream(db, {
|
|
1217
|
+
scope: tableScopeOf(t),
|
|
1218
|
+
stream: t.streamId,
|
|
1219
|
+
cursor: cursorFromCheckpoint(parent),
|
|
1220
|
+
merge: true,
|
|
1221
|
+
...(opts.allowUnreleased ? { allowUnreleased: true } : {}),
|
|
1222
|
+
});
|
|
1223
|
+
if (joined.refused !== null) {
|
|
1224
|
+
throw vaultError('E_NEXUS_SYNC_REFUSED', `the join was refused: ${joined.refused}`);
|
|
1225
|
+
}
|
|
1226
|
+
cut = joined.cut ?? 0;
|
|
1227
|
+
// A failure leaves the reconcile due and push paused; `cleo cloud sync` finishes it.
|
|
1228
|
+
try {
|
|
1229
|
+
merged = reconcileCopy(db, scratch, {
|
|
1230
|
+
scope: tableScopeOf(t),
|
|
1231
|
+
stream: t.streamId,
|
|
1232
|
+
mergedCursor,
|
|
1233
|
+
baseline,
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
warnings.push({
|
|
1238
|
+
code: 'W_SYNC_MERGE_PENDING',
|
|
1239
|
+
message: `joined ${t.streamId}, but merging this store's rows did not finish (${err instanceof Error ? err.message : String(err)}); push stays paused until \`cleo cloud sync\` finishes it`,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
warnings.push(...conn.warnings.slice(connWarned));
|
|
1244
|
+
if (merged !== null && merged.overwritten.length > 0) {
|
|
1245
|
+
warnings.push({
|
|
1246
|
+
code: 'W_SYNC_MERGE_OVERWROTE',
|
|
1247
|
+
message: `${merged.overwritten.length} field(s) of this store's own rows lost to the stream's value (older here, or written on the stream after its checkpoint): ${merged.overwritten
|
|
1248
|
+
.map((f) => `${f.table} ${f.uid} ${f.column}`)
|
|
1249
|
+
.join(', ')}; the store as it was is kept at ${safetyBackup}`,
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
if (merged !== null && merged.replaced.length > 0) {
|
|
1253
|
+
warnings.push({
|
|
1254
|
+
code: 'W_SYNC_MERGE_REPLACED',
|
|
1255
|
+
message: `${merged.replaced.length} field(s) of the stream took this store's newer value on every replica: ${merged.replaced
|
|
1256
|
+
.map((f) => `${f.table} ${f.uid} ${f.column}`)
|
|
1257
|
+
.join(', ')}`,
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
if (merged !== null && merged.deleted.length > 0) {
|
|
1261
|
+
warnings.push({
|
|
1262
|
+
code: 'W_SYNC_MERGE_DELETED',
|
|
1263
|
+
message: `${merged.deleted.length} of this store's own row(s) were removed by a newer delete on the stream: ${merged.deleted
|
|
1264
|
+
.map((r) => `${r.table} ${r.uid}`)
|
|
1265
|
+
.join(', ')}; the store as it was is kept at ${safetyBackup}`,
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
if (merged !== null && merged.unresolved > 0) {
|
|
1269
|
+
warnings.push({
|
|
1270
|
+
code: 'W_SYNC_MERGE_UNRESOLVED',
|
|
1271
|
+
message: `${merged.unresolved} stream row(s) were not placed here (a key this store's own row already holds, or a reference that does not resolve); this store kept its own`,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
warnings.push(...conn.state.drainWarnings());
|
|
1275
|
+
return {
|
|
1276
|
+
...base,
|
|
1277
|
+
status: 'joined',
|
|
1278
|
+
cut,
|
|
1279
|
+
sealed: 0,
|
|
1280
|
+
folded: 0,
|
|
1281
|
+
baselined: {},
|
|
1282
|
+
snapshot: snapshotOf(parent, await deviceNames(conn)),
|
|
1283
|
+
deltaSegmentSeq: null,
|
|
1284
|
+
replicaSeqFloor: null,
|
|
1285
|
+
merged: merged === null
|
|
1286
|
+
? null
|
|
1287
|
+
: {
|
|
1288
|
+
adopted: merged.adoptedInserts,
|
|
1289
|
+
emitted: merged.frame.inserts,
|
|
1290
|
+
unresolved: merged.unresolved,
|
|
1291
|
+
overwritten: [...merged.overwritten],
|
|
1292
|
+
replaced: [...merged.replaced],
|
|
1293
|
+
deleted: [...merged.deleted],
|
|
1294
|
+
safetyBackup,
|
|
1295
|
+
},
|
|
1296
|
+
warnings,
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1195
1299
|
if (synced?.lastCheckpointId !== parent.checkpointId) {
|
|
1196
1300
|
throw vaultError('E_NEXUS_SYNC_STREAM_JOURNALED', `${t.streamId} already carries the change journal: its head snapshot ${parent.checkpointId} is a journal checkpoint this store has not restored`, 'nothing was cut; restore the journal checkpoint (`cleo cloud restore`), then join the stream with `cleo sync enable push`');
|
|
1197
1301
|
}
|
|
@@ -1400,7 +1504,8 @@ async function openStreamSession(opts) {
|
|
|
1400
1504
|
}
|
|
1401
1505
|
/**
|
|
1402
1506
|
* Complete a rebind's server half the store still owes (T13278): a crash, or
|
|
1403
|
-
* a refused call, after the store rebound at a pull's head
|
|
1507
|
+
* a refused call, after the store rebound at a pull's head, or the retire a
|
|
1508
|
+
* rollback or move rebind at open recorded (T13337). Until it is done
|
|
1404
1509
|
* the stream does not know the store's replica, so nothing is pushed or
|
|
1405
1510
|
* pulled first. Returns the session with the replica the store is bound to.
|
|
1406
1511
|
*/
|
|
@@ -1412,9 +1517,180 @@ async function withCompletedRebind(session) {
|
|
|
1412
1517
|
const db = getDualScopeNativeDb(t.scope === 'global'
|
|
1413
1518
|
? await openDualScopeDbAtPath('global', t.dbPath)
|
|
1414
1519
|
: await openDualScopeDbAtPath('project', t.dbPath));
|
|
1415
|
-
const done = await
|
|
1520
|
+
const done = await completeOwedRebinds(conn, t, db);
|
|
1521
|
+
// §1.5 N7 (T13335): a store the open pass rebound reconciles before it
|
|
1522
|
+
// pushes or pulls; push stays refused until this commits.
|
|
1523
|
+
await reconcileOwedCopy(session, db);
|
|
1416
1524
|
return done === null ? session : { ...session, t: { ...t, replicaId: done.to } };
|
|
1417
1525
|
}
|
|
1526
|
+
/** Pages the stream's verified segments for {@link pullStream}. */
|
|
1527
|
+
function journalPuller(journal, key) {
|
|
1528
|
+
return async (cursor) => {
|
|
1529
|
+
const page = await journal.pull({
|
|
1530
|
+
after: cursor.after,
|
|
1531
|
+
knowsAllReplicas: cursor.knowsAllReplicas,
|
|
1532
|
+
replicas: { ...cursor.replicas },
|
|
1533
|
+
}, key.signers);
|
|
1534
|
+
return {
|
|
1535
|
+
segments: page.segments.map((s) => ({
|
|
1536
|
+
seq: s.seq,
|
|
1537
|
+
replicaId: s.replicaId,
|
|
1538
|
+
replicaSeq: s.replicaSeq,
|
|
1539
|
+
deviceId: s.deviceId,
|
|
1540
|
+
plaintext: s.plaintext,
|
|
1541
|
+
schemaVersion: s.meta.schemaVersion,
|
|
1542
|
+
})),
|
|
1543
|
+
cursor: page.cursor,
|
|
1544
|
+
head: page.head,
|
|
1545
|
+
};
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
/** Checks each pulled transaction's device signature for {@link pullStream}. */
|
|
1549
|
+
function txnVerifier(key, streamId) {
|
|
1550
|
+
return (deviceId, txns) => {
|
|
1551
|
+
const keys = signerKeys(key.signers, deviceId);
|
|
1552
|
+
const first = keys[0];
|
|
1553
|
+
if (first === undefined)
|
|
1554
|
+
return 0;
|
|
1555
|
+
for (const k of keys) {
|
|
1556
|
+
if (firstBadTxnSignature(k.publicKey, streamId, txns) === null)
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
return firstBadTxnSignature(first.publicKey, streamId, txns);
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* The copy reconcile a store the open pass rebound owes (§1.5 N7, T13335):
|
|
1564
|
+
* restore the stream's latest verified journal checkpoint into a scratch
|
|
1565
|
+
* store (`sync: 'off'`, a dedicated handle), pull the stream to head there
|
|
1566
|
+
* with the merge engine, and reconcile the store against that merged state
|
|
1567
|
+
* ({@link reconcileCopy}). The scratch is removed whatever happens. Nothing
|
|
1568
|
+
* is done when no reconcile is due.
|
|
1569
|
+
*
|
|
1570
|
+
* @throws {NexusAccountError} `E_NEXUS_SYNC_REFUSED` when the stream has no
|
|
1571
|
+
* verified journal checkpoint, or the scratch pull stopped short; the
|
|
1572
|
+
* reconcile stays due and push stays refused.
|
|
1573
|
+
*/
|
|
1574
|
+
async function reconcileOwedCopy(session, db) {
|
|
1575
|
+
if (reconcileDue(db) === null)
|
|
1576
|
+
return null;
|
|
1577
|
+
const { t } = session;
|
|
1578
|
+
return withMergedScratch(session, (scratch, mergedCursor, baseline) => {
|
|
1579
|
+
try {
|
|
1580
|
+
return reconcileCopy(db, scratch, {
|
|
1581
|
+
scope: tableScopeOf(t),
|
|
1582
|
+
stream: t.streamId,
|
|
1583
|
+
mergedCursor,
|
|
1584
|
+
baseline,
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
catch (err) {
|
|
1588
|
+
if (!(err instanceof MergeKeyCollisionError))
|
|
1589
|
+
throw err;
|
|
1590
|
+
throw vaultError('E_NEXUS_SYNC_REFUSED', err.message, "nothing was merged or pushed, and push stays paused; give this store's row a key the stream does not hold, then run `cleo cloud sync`");
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Run `fn` against the stream's merged state: its latest verified journal
|
|
1596
|
+
* checkpoint restored into a scratch store (`sync: 'off'`, a dedicated
|
|
1597
|
+
* handle) and pulled to head there with the merge engine (§1.5 N7). The
|
|
1598
|
+
* scratch is removed whatever happens.
|
|
1599
|
+
*
|
|
1600
|
+
* @param session - The stream session.
|
|
1601
|
+
* @param fn - Gets the scratch (read it only) and its pull position at head.
|
|
1602
|
+
* @returns What `fn` returns.
|
|
1603
|
+
* @throws {NexusAccountError} `E_NEXUS_SYNC_REFUSED` when the stream has no
|
|
1604
|
+
* verified journal checkpoint, or the scratch pull stopped short.
|
|
1605
|
+
*/
|
|
1606
|
+
async function withMergedScratch(session, fn) {
|
|
1607
|
+
const { conn, key, t } = session;
|
|
1608
|
+
const journal = journalFor(conn, t);
|
|
1609
|
+
const journalCheckpoints = trustedCheckpoints(journal, await listCheckpoints(conn, t.streamId), key.signers, null)
|
|
1610
|
+
.filter(isJournalSnapshot)
|
|
1611
|
+
.sort((a, b) => b.coversSeq - a.coversSeq);
|
|
1612
|
+
const latest = journalCheckpoints[0];
|
|
1613
|
+
// The merge join's baseline is the cut (the oldest journal checkpoint, T13508 LOW): a
|
|
1614
|
+
// later checkpoint would hide stream edits made between the cut and it.
|
|
1615
|
+
const cutCheckpoint = journalCheckpoints[journalCheckpoints.length - 1];
|
|
1616
|
+
if (latest === undefined) {
|
|
1617
|
+
throw vaultError('E_NEXUS_SYNC_REFUSED', `${t.streamId} has no verified journal checkpoint to reconcile this store against`, 'nothing was pushed or pulled; restore the stream (`cleo cloud restore`) instead');
|
|
1618
|
+
}
|
|
1619
|
+
const restored = await journal.restoreCheckpoint(latest.checkpointId, key.signers);
|
|
1620
|
+
const work = tempDir('cleo-reconcile-');
|
|
1621
|
+
try {
|
|
1622
|
+
const bundlePath = path.join(work, 'checkpoint.cleobundle.tar.gz');
|
|
1623
|
+
fs.writeFileSync(bundlePath, restored.bundle);
|
|
1624
|
+
const staging = path.join(work, 'staging');
|
|
1625
|
+
fs.mkdirSync(staging);
|
|
1626
|
+
const { extractDir, manifest } = await extractPortableBundle(bundlePath, staging);
|
|
1627
|
+
const section = t.scope === 'global' ? manifest.global?.home : manifest.projects[0];
|
|
1628
|
+
const entry = section?.databases.find((d) => d.role === 'primary' && d.relPath === 'cleo.db');
|
|
1629
|
+
if (!entry) {
|
|
1630
|
+
throw vaultError('E_NEXUS_VAULT_VERIFY_FAILED', 'the checkpoint holds no primary cleo.db');
|
|
1631
|
+
}
|
|
1632
|
+
const scratchPath = path.join(work, 'scratch', 'cleo.db');
|
|
1633
|
+
fs.mkdirSync(path.dirname(scratchPath));
|
|
1634
|
+
fs.copyFileSync(path.join(extractDir, entry.bundlePath), scratchPath);
|
|
1635
|
+
// The checkpoint as restored, never pulled: a merge join's baseline (T13507).
|
|
1636
|
+
const baselinePath = path.join(work, 'baseline', 'cleo.db');
|
|
1637
|
+
fs.mkdirSync(path.dirname(baselinePath));
|
|
1638
|
+
if (cutCheckpoint === undefined || cutCheckpoint.checkpointId === latest.checkpointId) {
|
|
1639
|
+
fs.copyFileSync(path.join(extractDir, entry.bundlePath), baselinePath);
|
|
1640
|
+
}
|
|
1641
|
+
else {
|
|
1642
|
+
const cutRestored = await journal.restoreCheckpoint(cutCheckpoint.checkpointId, key.signers);
|
|
1643
|
+
const cutBundle = path.join(work, 'cut.cleobundle.tar.gz');
|
|
1644
|
+
fs.writeFileSync(cutBundle, cutRestored.bundle);
|
|
1645
|
+
const cutStaging = path.join(work, 'cut-staging');
|
|
1646
|
+
fs.mkdirSync(cutStaging);
|
|
1647
|
+
const cut = await extractPortableBundle(cutBundle, cutStaging);
|
|
1648
|
+
const cutSection = t.scope === 'global' ? cut.manifest.global?.home : cut.manifest.projects[0];
|
|
1649
|
+
const cutEntry = cutSection?.databases.find((d) => d.role === 'primary' && d.relPath === 'cleo.db');
|
|
1650
|
+
if (!cutEntry) {
|
|
1651
|
+
throw vaultError('E_NEXUS_VAULT_VERIFY_FAILED', 'the cut checkpoint holds no primary cleo.db');
|
|
1652
|
+
}
|
|
1653
|
+
fs.copyFileSync(path.join(cut.extractDir, cutEntry.bundlePath), baselinePath);
|
|
1654
|
+
}
|
|
1655
|
+
const { openDualScopeDbAtPath, getDualScopeNativeDb } = await import('../store/dual-scope-db.js');
|
|
1656
|
+
// The reconcile scratch is never bound and runs no open pass (§1.5).
|
|
1657
|
+
const openScratch = (p) => t.scope === 'global'
|
|
1658
|
+
? openDualScopeDbAtPath('global', p, undefined, { dedicated: true, syncMode: 'off' })
|
|
1659
|
+
: openDualScopeDbAtPath('project', p, undefined, { dedicated: true, syncMode: 'off' });
|
|
1660
|
+
const handle = await openScratch(scratchPath);
|
|
1661
|
+
const baselineHandle = await openScratch(baselinePath);
|
|
1662
|
+
try {
|
|
1663
|
+
const scratch = getDualScopeNativeDb(handle);
|
|
1664
|
+
// The scratch replays the stream; it never seals or pushes. The store
|
|
1665
|
+
// it reconciles already has its sync flags, so the release gate holds.
|
|
1666
|
+
setSyncFlag(scratch, 'sync.pull', true, { allowUnreleased: true });
|
|
1667
|
+
// Soft and project-aware like the live pull (T13551): a failed listing confirms none, never aborts.
|
|
1668
|
+
const serverRetirements = await confirmableRetirements(conn, t.scope, t.projectId);
|
|
1669
|
+
const merged = await pullStream(scratch, {
|
|
1670
|
+
scope: tableScopeOf(t),
|
|
1671
|
+
stream: t.streamId,
|
|
1672
|
+
// No own echoes: the scratch is no replica.
|
|
1673
|
+
replica: randomUUID(),
|
|
1674
|
+
initialCursor: cursorFromCheckpoint(latest),
|
|
1675
|
+
pull: journalPuller(journal, key),
|
|
1676
|
+
verify: txnVerifier(key, t.streamId),
|
|
1677
|
+
seal: () => { },
|
|
1678
|
+
...(serverRetirements !== undefined ? { serverRetirements } : {}),
|
|
1679
|
+
});
|
|
1680
|
+
if (merged.refused !== null) {
|
|
1681
|
+
throw vaultError('E_NEXUS_SYNC_REFUSED', `the merged state this store reconciles against stopped short of ${t.streamId}'s head: ${merged.refused}`, 'nothing was pushed or pulled; run `cleo cloud sync` again');
|
|
1682
|
+
}
|
|
1683
|
+
return await fn(scratch, readStreamCursor(scratch, t.streamId) ?? cursorFromCheckpoint(latest), getDualScopeNativeDb(baselineHandle));
|
|
1684
|
+
}
|
|
1685
|
+
finally {
|
|
1686
|
+
handle.close();
|
|
1687
|
+
baselineHandle.close();
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
finally {
|
|
1691
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1418
1694
|
async function pushSyncStreamImpl(opts = {}) {
|
|
1419
1695
|
return pushWithSession(await openStreamSession(opts), opts);
|
|
1420
1696
|
}
|
|
@@ -1473,23 +1749,64 @@ async function pullSyncStreamImpl(opts = {}) {
|
|
|
1473
1749
|
/**
|
|
1474
1750
|
* The server's retirements a receiver of `scope`'s stream confirms a `retire`
|
|
1475
1751
|
* by (T13366). The home stream: `retiredAt` and `successor` on
|
|
1476
|
-
* `GET /v1/account/home/replicas
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
1752
|
+
* `GET /v1/account/home/replicas`. A project stream: the same fields on E15
|
|
1753
|
+
* `GET /v1/projects/:projectId/replicas`, every page (T13391). A server
|
|
1754
|
+
* without the listing (404), or one older than retirement, confirms none.
|
|
1479
1755
|
*
|
|
1480
1756
|
* @param conn - The vault connection.
|
|
1481
1757
|
* @param scope - The store's scope.
|
|
1482
|
-
* @
|
|
1758
|
+
* @param projectId - The server project of a project stream.
|
|
1759
|
+
* @returns The retirements, or undefined for a project stream with no server project.
|
|
1483
1760
|
*/
|
|
1484
|
-
export async function serverRetirementsFor(conn, scope) {
|
|
1485
|
-
|
|
1486
|
-
return undefined;
|
|
1487
|
-
const list = await conn.find('/v1/account/home/replicas', ListHomeReplicasResult);
|
|
1488
|
-
if (list === null)
|
|
1489
|
-
return [];
|
|
1490
|
-
return list.replicas.flatMap((r) => r.retiredAt
|
|
1761
|
+
export async function serverRetirementsFor(conn, scope, projectId = null) {
|
|
1762
|
+
const retired = (rows) => rows.flatMap((r) => r.retiredAt
|
|
1491
1763
|
? [{ replicaId: r.replicaId, successor: r.successor ?? null, retiredAt: r.retiredAt }]
|
|
1492
1764
|
: []);
|
|
1765
|
+
if (scope === 'global') {
|
|
1766
|
+
const list = await conn.find('/v1/account/home/replicas', ListHomeReplicasResult);
|
|
1767
|
+
return list === null ? [] : retired(list.replicas);
|
|
1768
|
+
}
|
|
1769
|
+
if (projectId === null)
|
|
1770
|
+
return undefined;
|
|
1771
|
+
const out = [];
|
|
1772
|
+
let cursor = null;
|
|
1773
|
+
for (let page = 0; page < NEXUS_CLOUD_MAX_PAGES; page++) {
|
|
1774
|
+
const res = await conn.find(nexusQueryPath(`/v1/projects/${encodeURIComponent(projectId)}/replicas`, {
|
|
1775
|
+
limit: NEXUS_PAGE_LIMIT_MAX,
|
|
1776
|
+
cursor: cursor ?? undefined,
|
|
1777
|
+
}), nexusCloudReplicaPageSchema);
|
|
1778
|
+
if (res === null)
|
|
1779
|
+
return out;
|
|
1780
|
+
out.push(...retired(res.replicas));
|
|
1781
|
+
cursor = res.nextCursor;
|
|
1782
|
+
if (cursor === null)
|
|
1783
|
+
return out;
|
|
1784
|
+
}
|
|
1785
|
+
return out;
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* {@link serverRetirementsFor}, soft (T13395): a listing that fails (network,
|
|
1789
|
+
* 5xx, a 404) confirms nothing this round instead of failing the pull. The
|
|
1790
|
+
* pull still stages and applies; retires it records stay unconfirmed until a
|
|
1791
|
+
* later pull reads the listing. The failure is a `W_NEXUS_RETIREMENTS_UNAVAILABLE`
|
|
1792
|
+
* warning on the connection.
|
|
1793
|
+
*
|
|
1794
|
+
* @param conn - The vault connection (its warnings collect the failure).
|
|
1795
|
+
* @param scope - The store's scope.
|
|
1796
|
+
* @param projectId - The server project of a project stream.
|
|
1797
|
+
* @returns The retirements, `[]` when the listing failed, or undefined for a project stream with no server project.
|
|
1798
|
+
*/
|
|
1799
|
+
export async function confirmableRetirements(conn, scope, projectId = null) {
|
|
1800
|
+
try {
|
|
1801
|
+
return await serverRetirementsFor(conn, scope, projectId);
|
|
1802
|
+
}
|
|
1803
|
+
catch (err) {
|
|
1804
|
+
conn.warnings.push({
|
|
1805
|
+
code: 'W_NEXUS_RETIREMENTS_UNAVAILABLE',
|
|
1806
|
+
message: `the replica listing could not be read (${err instanceof Error ? err.message : String(err)}); no replica retire is confirmed this round`,
|
|
1807
|
+
});
|
|
1808
|
+
return [];
|
|
1809
|
+
}
|
|
1493
1810
|
}
|
|
1494
1811
|
async function pullWithSession(opened) {
|
|
1495
1812
|
const session = await withCompletedRebind(opened);
|
|
@@ -1553,43 +1870,15 @@ async function pullWithSession(opened) {
|
|
|
1553
1870
|
// server-held old segments pushed and inherits the rest (never uploaded under
|
|
1554
1871
|
// the new id; unpushedSegments filters by replica), its re-emits are new ids,
|
|
1555
1872
|
// and a retired replica's late segment carries ids never stored before.
|
|
1556
|
-
const serverRetirements = await
|
|
1873
|
+
const serverRetirements = await confirmableRetirements(conn, t.scope, t.projectId);
|
|
1557
1874
|
const report = await pullStream(db, {
|
|
1558
1875
|
scope: tableScopeOf(t),
|
|
1559
1876
|
stream: t.streamId,
|
|
1560
1877
|
replica: replicaId,
|
|
1561
1878
|
initialCursor,
|
|
1562
1879
|
pruneSeen: true,
|
|
1563
|
-
pull:
|
|
1564
|
-
|
|
1565
|
-
after: cursor.after,
|
|
1566
|
-
knowsAllReplicas: cursor.knowsAllReplicas,
|
|
1567
|
-
replicas: { ...cursor.replicas },
|
|
1568
|
-
}, key.signers);
|
|
1569
|
-
return {
|
|
1570
|
-
segments: page.segments.map((s) => ({
|
|
1571
|
-
seq: s.seq,
|
|
1572
|
-
replicaId: s.replicaId,
|
|
1573
|
-
replicaSeq: s.replicaSeq,
|
|
1574
|
-
deviceId: s.deviceId,
|
|
1575
|
-
plaintext: s.plaintext,
|
|
1576
|
-
schemaVersion: s.meta.schemaVersion,
|
|
1577
|
-
})),
|
|
1578
|
-
cursor: page.cursor,
|
|
1579
|
-
head: page.head,
|
|
1580
|
-
};
|
|
1581
|
-
},
|
|
1582
|
-
verify: (deviceId, txns) => {
|
|
1583
|
-
const keys = signerKeys(key.signers, deviceId);
|
|
1584
|
-
const first = keys[0];
|
|
1585
|
-
if (first === undefined)
|
|
1586
|
-
return 0;
|
|
1587
|
-
for (const k of keys) {
|
|
1588
|
-
if (firstBadTxnSignature(k.publicKey, t.streamId, txns) === null)
|
|
1589
|
-
return null;
|
|
1590
|
-
}
|
|
1591
|
-
return firstBadTxnSignature(first.publicKey, t.streamId, txns);
|
|
1592
|
-
},
|
|
1880
|
+
pull: journalPuller(journal, key),
|
|
1881
|
+
verify: txnVerifier(key, t.streamId),
|
|
1593
1882
|
seal: () => {
|
|
1594
1883
|
sealPending(db, { scope: tableScopeOf(t), replica: replicaId });
|
|
1595
1884
|
},
|
|
@@ -1601,7 +1890,7 @@ async function pullWithSession(opened) {
|
|
|
1601
1890
|
});
|
|
1602
1891
|
// Its server half at once, so this run's next push announces the retire.
|
|
1603
1892
|
if (report.rebind !== null)
|
|
1604
|
-
await
|
|
1893
|
+
await completeOwedRebinds(conn, t, db);
|
|
1605
1894
|
return report;
|
|
1606
1895
|
}
|
|
1607
1896
|
/** A `cleo cloud sync` stream result with nothing done yet. */
|
|
@@ -1821,12 +2110,41 @@ async function assertStoreQuiescent(t) {
|
|
|
1821
2110
|
}
|
|
1822
2111
|
/** Safety bundles kept per store under `backups/vault`; older ones are removed (T13007). */
|
|
1823
2112
|
export const NEXUS_VAULT_SAFETY_BUNDLES_KEPT = 10;
|
|
1824
|
-
/**
|
|
1825
|
-
|
|
2113
|
+
/** A safety bundle's file name: its kind, then its timestamp. */
|
|
2114
|
+
const SAFETY_BUNDLE = /^pre-(restore|merge)-(.+)\.cleobundle\.tar\.gz$/;
|
|
2115
|
+
/**
|
|
2116
|
+
* Export the store as it is now to a safety bundle under `backups/vault`,
|
|
2117
|
+
* keeping the newest {@link NEXUS_VAULT_SAFETY_BUNDLES_KEPT}.
|
|
2118
|
+
*
|
|
2119
|
+
* @returns The bundle's path.
|
|
2120
|
+
*/
|
|
2121
|
+
async function exportSafetyBundle(t, kind, label) {
|
|
2122
|
+
const dir = t.scope === 'global'
|
|
2123
|
+
? path.join(t.storeRoot, 'backups', 'vault')
|
|
2124
|
+
: path.join(t.storeRoot, '.cleo', 'backups', 'vault');
|
|
2125
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2126
|
+
const out = path.join(dir, `${kind}-${new Date().toISOString().replace(/[:.]/g, '-')}.cleobundle.tar.gz`);
|
|
2127
|
+
await exportPortableBundle({
|
|
2128
|
+
scope: t.scope === 'global' ? 'global' : 'project',
|
|
2129
|
+
...(t.scope === 'project' ? { projectRoot: t.storeRoot } : {}),
|
|
2130
|
+
outputPath: out,
|
|
2131
|
+
label,
|
|
2132
|
+
// A local safety copy: it shares nothing (T13250).
|
|
2133
|
+
sharesIdentity: false,
|
|
2134
|
+
});
|
|
2135
|
+
rotateSafetyBundles(dir, kind);
|
|
2136
|
+
return out;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Keep the newest {@link NEXUS_VAULT_SAFETY_BUNDLES_KEPT} safety bundles of `kind` in `dir`: each kind
|
|
2140
|
+
* rotates on its own, so repeated merges never evict the last pre-restore bundle.
|
|
2141
|
+
*/
|
|
2142
|
+
function rotateSafetyBundles(dir, kind) {
|
|
1826
2143
|
const bundles = fs
|
|
1827
2144
|
.readdirSync(dir)
|
|
1828
|
-
.filter((name) =>
|
|
1829
|
-
.
|
|
2145
|
+
.filter((name) => SAFETY_BUNDLE.test(name) && name.startsWith(`${kind}-`))
|
|
2146
|
+
// Oldest first by the timestamp.
|
|
2147
|
+
.sort((a, b) => a.replace(SAFETY_BUNDLE, '$2').localeCompare(b.replace(SAFETY_BUNDLE, '$2')));
|
|
1830
2148
|
for (const name of bundles.slice(0, Math.max(0, bundles.length - NEXUS_VAULT_SAFETY_BUNDLES_KEPT))) {
|
|
1831
2149
|
fs.rmSync(path.join(dir, name), { force: true });
|
|
1832
2150
|
}
|
|
@@ -2074,21 +2392,8 @@ async function restoreNexusVaultImpl(opts) {
|
|
|
2074
2392
|
let safetyBackup = null;
|
|
2075
2393
|
let safety = new Map();
|
|
2076
2394
|
if (hasLocal) {
|
|
2077
|
-
|
|
2078
|
-
? path.join(t.storeRoot, 'backups', 'vault')
|
|
2079
|
-
: path.join(t.storeRoot, '.cleo', 'backups', 'vault');
|
|
2080
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
2081
|
-
safetyBackup = path.join(dir, `pre-restore-${new Date().toISOString().replace(/[:.]/g, '-')}.cleobundle.tar.gz`);
|
|
2082
|
-
await exportPortableBundle({
|
|
2083
|
-
scope: t.scope === 'global' ? 'global' : 'project',
|
|
2084
|
-
...(t.scope === 'project' ? { projectRoot: t.storeRoot } : {}),
|
|
2085
|
-
outputPath: safetyBackup,
|
|
2086
|
-
label: 'cloud-vault-pre-restore',
|
|
2087
|
-
// A local safety copy: it shares nothing (T13250).
|
|
2088
|
-
sharesIdentity: false,
|
|
2089
|
-
});
|
|
2395
|
+
safetyBackup = await exportSafetyBundle(t, 'pre-restore', 'cloud-vault-pre-restore');
|
|
2090
2396
|
safety = await safetyInventory(safetyBackup, t);
|
|
2091
|
-
rotateSafetyBundles(dir);
|
|
2092
2397
|
}
|
|
2093
2398
|
let tables = 0;
|
|
2094
2399
|
// Every path the snapshot lists in its section; the rest of the inventory is removed.
|