@cleocode/core 2026.9.7 → 2026.9.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/audit/reconstruct.d.ts +13 -43
- package/dist/audit/reconstruct.d.ts.map +1 -1
- package/dist/audit/reconstruct.js +295 -290
- package/dist/audit/reconstruct.js.map +1 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +8 -2
- package/dist/bootstrap.js.map +1 -1
- package/dist/dispatch/mutate-projection.d.ts.map +1 -1
- package/dist/dispatch/mutate-projection.js +12 -0
- package/dist/dispatch/mutate-projection.js.map +1 -1
- package/dist/dispatch/mvi-projection.d.ts +29 -6
- package/dist/dispatch/mvi-projection.d.ts.map +1 -1
- package/dist/dispatch/mvi-projection.js +109 -139
- package/dist/dispatch/mvi-projection.js.map +1 -1
- package/dist/docs/docs-read-model.d.ts +3 -2
- package/dist/docs/docs-read-model.d.ts.map +1 -1
- package/dist/docs/docs-read-model.js +4 -3
- package/dist/docs/docs-read-model.js.map +1 -1
- package/dist/docs/export-document.js +5100 -2297
- package/dist/docs/export-document.js.map +3 -3
- package/dist/docs/projection.d.ts +67 -0
- package/dist/docs/projection.d.ts.map +1 -0
- package/dist/docs/projection.js +370 -0
- package/dist/docs/projection.js.map +1 -0
- package/dist/doctor/acceptance-drift.d.ts +6 -2
- package/dist/doctor/acceptance-drift.d.ts.map +1 -1
- package/dist/doctor/acceptance-drift.js +43 -5
- package/dist/doctor/acceptance-drift.js.map +1 -1
- package/dist/doctor/knowledge-summary.d.ts +41 -0
- package/dist/doctor/knowledge-summary.d.ts.map +1 -0
- package/dist/doctor/knowledge-summary.js +81 -0
- package/dist/doctor/knowledge-summary.js.map +1 -0
- package/dist/doctor/knowledge.d.ts +192 -0
- package/dist/doctor/knowledge.d.ts.map +1 -0
- package/dist/doctor/knowledge.js +1864 -0
- package/dist/doctor/knowledge.js.map +1 -0
- package/dist/git/work-tree.d.ts +64 -0
- package/dist/git/work-tree.d.ts.map +1 -0
- package/dist/git/work-tree.js +111 -0
- package/dist/git/work-tree.js.map +1 -0
- package/dist/hooks/handlers/task-hooks.d.ts +8 -5
- package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/task-hooks.js +15 -60
- package/dist/hooks/handlers/task-hooks.js.map +1 -1
- package/dist/identity/cleo-identity.d.ts +13 -0
- package/dist/identity/cleo-identity.d.ts.map +1 -1
- package/dist/identity/cleo-identity.js +20 -6
- package/dist/identity/cleo-identity.js.map +1 -1
- package/dist/injection.d.ts.map +1 -1
- package/dist/injection.js +29 -13
- package/dist/injection.js.map +1 -1
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -0
- package/dist/internal.js.map +1 -1
- package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
- package/dist/lifecycle/verification/satisfies-validator.js +124 -120
- package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
- package/dist/llm/plugin-facade.js +4018 -1726
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/memory/brain-backfill.d.ts +27 -12
- package/dist/memory/brain-backfill.d.ts.map +1 -1
- package/dist/memory/brain-backfill.js +325 -134
- package/dist/memory/brain-backfill.js.map +1 -1
- package/dist/memory/brain-doctor.d.ts +21 -0
- package/dist/memory/brain-doctor.d.ts.map +1 -1
- package/dist/memory/brain-doctor.js +111 -53
- package/dist/memory/brain-doctor.js.map +1 -1
- package/dist/memory/brain-links.d.ts.map +1 -1
- package/dist/memory/brain-links.js +31 -4
- package/dist/memory/brain-links.js.map +1 -1
- package/dist/memory/brain-reasoning.d.ts.map +1 -1
- package/dist/memory/brain-reasoning.js +28 -2
- package/dist/memory/brain-reasoning.js.map +1 -1
- package/dist/memory/brain-reconciler.d.ts +14 -19
- package/dist/memory/brain-reconciler.d.ts.map +1 -1
- package/dist/memory/brain-reconciler.js +70 -97
- package/dist/memory/brain-reconciler.js.map +1 -1
- package/dist/memory/brain-search.d.ts +2 -0
- package/dist/memory/brain-search.d.ts.map +1 -1
- package/dist/memory/brain-search.js +59 -29
- package/dist/memory/brain-search.js.map +1 -1
- package/dist/memory/brain-similarity.d.ts.map +1 -1
- package/dist/memory/brain-similarity.js +5 -4
- package/dist/memory/brain-similarity.js.map +1 -1
- package/dist/memory/brain-stub-prune.d.ts +31 -9
- package/dist/memory/brain-stub-prune.d.ts.map +1 -1
- package/dist/memory/brain-stub-prune.js +65 -18
- package/dist/memory/brain-stub-prune.js.map +1 -1
- package/dist/memory/brain-writer-handlers.d.ts +10 -1
- package/dist/memory/brain-writer-handlers.d.ts.map +1 -1
- package/dist/memory/brain-writer-handlers.js +23 -3
- package/dist/memory/brain-writer-handlers.js.map +1 -1
- package/dist/memory/brain-writer-thread.d.ts +31 -1
- package/dist/memory/brain-writer-thread.d.ts.map +1 -1
- package/dist/memory/brain-writer-thread.js +166 -24
- package/dist/memory/brain-writer-thread.js.map +1 -1
- package/dist/memory/brain-writer-worker.js +8 -1
- package/dist/memory/brain-writer-worker.js.map +1 -1
- package/dist/memory/decision-cross-link.d.ts +39 -0
- package/dist/memory/decision-cross-link.d.ts.map +1 -1
- package/dist/memory/decision-cross-link.js +316 -0
- package/dist/memory/decision-cross-link.js.map +1 -1
- package/dist/memory/decisions.d.ts +4 -1
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/memory/decisions.js +7 -24
- package/dist/memory/decisions.js.map +1 -1
- package/dist/memory/dispatch-trace.d.ts +12 -5
- package/dist/memory/dispatch-trace.d.ts.map +1 -1
- package/dist/memory/dispatch-trace.js +17 -7
- package/dist/memory/dispatch-trace.js.map +1 -1
- package/dist/memory/eligibility.d.ts +51 -0
- package/dist/memory/eligibility.d.ts.map +1 -0
- package/dist/memory/eligibility.js +77 -0
- package/dist/memory/eligibility.js.map +1 -0
- package/dist/memory/engine-compat.d.ts +2 -0
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/engine-compat.js +10 -0
- package/dist/memory/engine-compat.js.map +1 -1
- package/dist/memory/extraction-gate.d.ts +21 -7
- package/dist/memory/extraction-gate.d.ts.map +1 -1
- package/dist/memory/extraction-gate.js +178 -216
- package/dist/memory/extraction-gate.js.map +1 -1
- package/dist/memory/graph-auto-populate.d.ts +38 -5
- package/dist/memory/graph-auto-populate.d.ts.map +1 -1
- package/dist/memory/graph-auto-populate.js +182 -60
- package/dist/memory/graph-auto-populate.js.map +1 -1
- package/dist/memory/graph-memory-bridge.d.ts +14 -1
- package/dist/memory/graph-memory-bridge.d.ts.map +1 -1
- package/dist/memory/graph-memory-bridge.js +66 -28
- package/dist/memory/graph-memory-bridge.js.map +1 -1
- package/dist/memory/index.d.ts +1 -19
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +1 -50
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/learnings.d.ts.map +1 -1
- package/dist/memory/learnings.js +2 -23
- package/dist/memory/learnings.js.map +1 -1
- package/dist/memory/manifest-filter.d.ts +21 -0
- package/dist/memory/manifest-filter.d.ts.map +1 -0
- package/dist/memory/manifest-filter.js +51 -0
- package/dist/memory/manifest-filter.js.map +1 -0
- package/dist/memory/manifest-ingestion.d.ts +28 -30
- package/dist/memory/manifest-ingestion.d.ts.map +1 -1
- package/dist/memory/manifest-ingestion.js +104 -235
- package/dist/memory/manifest-ingestion.js.map +1 -1
- package/dist/memory/observer-reflector.d.ts +2 -0
- package/dist/memory/observer-reflector.d.ts.map +1 -1
- package/dist/memory/observer-reflector.js +6 -49
- package/dist/memory/observer-reflector.js.map +1 -1
- package/dist/memory/patterns.d.ts +10 -0
- package/dist/memory/patterns.d.ts.map +1 -1
- package/dist/memory/patterns.js +25 -33
- package/dist/memory/patterns.js.map +1 -1
- package/dist/memory/pipeline-manifest-sqlite.d.ts +72 -9
- package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
- package/dist/memory/pipeline-manifest-sqlite.js +508 -412
- package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
- package/dist/memory/retrieval/build-retrieval-bundle.d.ts.map +1 -1
- package/dist/memory/retrieval/build-retrieval-bundle.js +16 -15
- package/dist/memory/retrieval/build-retrieval-bundle.js.map +1 -1
- package/dist/memory/retrieval/fetch.d.ts +7 -0
- package/dist/memory/retrieval/fetch.d.ts.map +1 -1
- package/dist/memory/retrieval/fetch.js +14 -11
- package/dist/memory/retrieval/fetch.js.map +1 -1
- package/dist/memory/retrieval/observe.d.ts +7 -1
- package/dist/memory/retrieval/observe.d.ts.map +1 -1
- package/dist/memory/retrieval/observe.js +58 -4
- package/dist/memory/retrieval/observe.js.map +1 -1
- package/dist/memory/retrieval/search.d.ts +14 -1
- package/dist/memory/retrieval/search.d.ts.map +1 -1
- package/dist/memory/retrieval/search.js +133 -69
- package/dist/memory/retrieval/search.js.map +1 -1
- package/dist/memory/retrieval/telemetry.d.ts +31 -0
- package/dist/memory/retrieval/telemetry.d.ts.map +1 -0
- package/dist/memory/retrieval/telemetry.js +53 -0
- package/dist/memory/retrieval/telemetry.js.map +1 -0
- package/dist/nexus/analyze-orchestrator.d.ts +41 -9
- package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
- package/dist/nexus/analyze-orchestrator.js +257 -58
- package/dist/nexus/analyze-orchestrator.js.map +1 -1
- package/dist/nexus/api-contracts.d.ts +2 -6
- package/dist/nexus/api-contracts.d.ts.map +1 -1
- package/dist/nexus/api-contracts.js +11 -2
- package/dist/nexus/api-contracts.js.map +1 -1
- package/dist/nexus/context.d.ts.map +1 -1
- package/dist/nexus/context.js +14 -37
- package/dist/nexus/context.js.map +1 -1
- package/dist/nexus/diff.d.ts.map +1 -1
- package/dist/nexus/diff.js +2 -1
- package/dist/nexus/diff.js.map +1 -1
- package/dist/nexus/identity.d.ts +29 -3
- package/dist/nexus/identity.d.ts.map +1 -1
- package/dist/nexus/identity.js +63 -12
- package/dist/nexus/identity.js.map +1 -1
- package/dist/nexus/impact.d.ts +21 -19
- package/dist/nexus/impact.d.ts.map +1 -1
- package/dist/nexus/impact.js +96 -192
- package/dist/nexus/impact.js.map +1 -1
- package/dist/nexus/knowledge.d.ts +80 -0
- package/dist/nexus/knowledge.d.ts.map +1 -0
- package/dist/nexus/knowledge.js +786 -0
- package/dist/nexus/knowledge.js.map +1 -0
- package/dist/nexus/living-brain.d.ts +8 -5
- package/dist/nexus/living-brain.d.ts.map +1 -1
- package/dist/nexus/living-brain.js +252 -188
- package/dist/nexus/living-brain.js.map +1 -1
- package/dist/nexus/registry.d.ts +11 -2
- package/dist/nexus/registry.d.ts.map +1 -1
- package/dist/nexus/registry.js +144 -128
- package/dist/nexus/registry.js.map +1 -1
- package/dist/nexus/source-roots.d.ts +16 -0
- package/dist/nexus/source-roots.d.ts.map +1 -0
- package/dist/nexus/source-roots.js +174 -0
- package/dist/nexus/source-roots.js.map +1 -0
- package/dist/nexus/task-evidence.d.ts +22 -0
- package/dist/nexus/task-evidence.d.ts.map +1 -0
- package/dist/nexus/task-evidence.js +210 -0
- package/dist/nexus/task-evidence.js.map +1 -0
- package/dist/nexus/tasks-bridge.d.ts +14 -7
- package/dist/nexus/tasks-bridge.d.ts.map +1 -1
- package/dist/nexus/tasks-bridge.js +81 -40
- package/dist/nexus/tasks-bridge.js.map +1 -1
- package/dist/orchestrate/lifecycle-ops.d.ts.map +1 -1
- package/dist/orchestrate/lifecycle-ops.js +170 -120
- package/dist/orchestrate/lifecycle-ops.js.map +1 -1
- package/dist/orchestrate/query-ops.d.ts +6 -7
- package/dist/orchestrate/query-ops.d.ts.map +1 -1
- package/dist/orchestrate/query-ops.js +509 -532
- package/dist/orchestrate/query-ops.js.map +1 -1
- package/dist/orchestration/bootstrap.d.ts +15 -1
- package/dist/orchestration/bootstrap.d.ts.map +1 -1
- package/dist/orchestration/bootstrap.js +21 -10
- package/dist/orchestration/bootstrap.js.map +1 -1
- package/dist/orchestration/index.d.ts +35 -3
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +64 -35
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/lead-rollup.d.ts +24 -1
- package/dist/orchestration/lead-rollup.d.ts.map +1 -1
- package/dist/orchestration/lead-rollup.js +31 -11
- package/dist/orchestration/lead-rollup.js.map +1 -1
- package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
- package/dist/orchestration/spawn-prompt.js +16 -41
- package/dist/orchestration/spawn-prompt.js.map +1 -1
- package/dist/orchestration/unblock.d.ts +15 -1
- package/dist/orchestration/unblock.d.ts.map +1 -1
- package/dist/orchestration/unblock.js +23 -12
- package/dist/orchestration/unblock.js.map +1 -1
- package/dist/orchestration/validate-spawn.d.ts.map +1 -1
- package/dist/orchestration/validate-spawn.js +3 -3
- package/dist/orchestration/validate-spawn.js.map +1 -1
- package/dist/orchestration/waves.d.ts +29 -13
- package/dist/orchestration/waves.d.ts.map +1 -1
- package/dist/orchestration/waves.js +43 -55
- package/dist/orchestration/waves.js.map +1 -1
- package/dist/paths.d.ts +17 -163
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +145 -496
- package/dist/paths.js.map +1 -1
- package/dist/project-info.d.ts +2 -11
- package/dist/project-info.d.ts.map +1 -1
- package/dist/project-info.js +3 -31
- package/dist/project-info.js.map +1 -1
- package/dist/project-scope.d.ts +239 -0
- package/dist/project-scope.d.ts.map +1 -0
- package/dist/project-scope.js +473 -0
- package/dist/project-scope.js.map +1 -0
- package/dist/release/pr-evidence.d.ts +16 -3
- package/dist/release/pr-evidence.d.ts.map +1 -1
- package/dist/release/pr-evidence.js +53 -7
- package/dist/release/pr-evidence.js.map +1 -1
- package/dist/render/nexus/graph/index.d.ts.map +1 -1
- package/dist/render/nexus/graph/index.js +4 -2
- package/dist/render/nexus/graph/index.js.map +1 -1
- package/dist/render/orchestration/audit-reconstruct.d.ts +11 -0
- package/dist/render/orchestration/audit-reconstruct.d.ts.map +1 -1
- package/dist/render/orchestration/audit-reconstruct.js +21 -6
- package/dist/render/orchestration/audit-reconstruct.js.map +1 -1
- package/dist/render/tasks/show.js +2 -1
- package/dist/render/tasks/show.js.map +1 -1
- package/dist/resources/spawn-wrapper.d.ts +82 -14
- package/dist/resources/spawn-wrapper.d.ts.map +1 -1
- package/dist/resources/spawn-wrapper.js +585 -27
- package/dist/resources/spawn-wrapper.js.map +1 -1
- package/dist/selfimprove/fix-gen.js +4026 -1734
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/sequence/index.d.ts.map +1 -1
- package/dist/sequence/index.js +76 -68
- package/dist/sequence/index.js.map +1 -1
- package/dist/session/canon-lint.d.ts +24 -3
- package/dist/session/canon-lint.d.ts.map +1 -1
- package/dist/session/canon-lint.js +72 -31
- package/dist/session/canon-lint.js.map +1 -1
- package/dist/session/engine-ops.d.ts +1 -4
- package/dist/session/engine-ops.d.ts.map +1 -1
- package/dist/session/engine-ops.js +6 -2
- package/dist/session/engine-ops.js.map +1 -1
- package/dist/sessions/briefing.d.ts +11 -1
- package/dist/sessions/briefing.d.ts.map +1 -1
- package/dist/sessions/briefing.js +76 -8
- package/dist/sessions/briefing.js.map +1 -1
- package/dist/sessions/handoff.d.ts +3 -1
- package/dist/sessions/handoff.d.ts.map +1 -1
- package/dist/sessions/handoff.js +2 -0
- package/dist/sessions/handoff.js.map +1 -1
- package/dist/shutdown-deadline.d.ts +24 -33
- package/dist/shutdown-deadline.d.ts.map +1 -1
- package/dist/shutdown-deadline.js +71 -14
- package/dist/shutdown-deadline.js.map +1 -1
- package/dist/shutdown.d.ts +21 -10
- package/dist/shutdown.d.ts.map +1 -1
- package/dist/shutdown.js +91 -35
- package/dist/shutdown.js.map +1 -1
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/orchestrator/validator.d.ts +87 -20
- package/dist/skills/orchestrator/validator.d.ts.map +1 -1
- package/dist/skills/orchestrator/validator.js +330 -241
- package/dist/skills/orchestrator/validator.js.map +1 -1
- package/dist/store/agent-resolver.d.ts.map +1 -1
- package/dist/store/agent-resolver.js +16 -9
- package/dist/store/agent-resolver.js.map +1 -1
- package/dist/store/attachment-store.d.ts +4 -1
- package/dist/store/attachment-store.d.ts.map +1 -1
- package/dist/store/attachment-store.js +20 -3
- package/dist/store/attachment-store.js.map +1 -1
- package/dist/store/background-jobs.d.ts +426 -105
- package/dist/store/background-jobs.d.ts.map +1 -1
- package/dist/store/background-jobs.js +1208 -252
- package/dist/store/background-jobs.js.map +1 -1
- package/dist/store/background-ops.d.ts +145 -8
- package/dist/store/background-ops.d.ts.map +1 -1
- package/dist/store/background-ops.js +459 -15
- package/dist/store/background-ops.js.map +1 -1
- package/dist/store/backup-inspect.d.ts +48 -4
- package/dist/store/backup-inspect.d.ts.map +1 -1
- package/dist/store/backup-inspect.js +384 -4
- package/dist/store/backup-inspect.js.map +1 -1
- package/dist/store/converters.d.ts.map +1 -1
- package/dist/store/converters.js +19 -2
- package/dist/store/converters.js.map +1 -1
- package/dist/store/data-accessor.d.ts +9 -1
- package/dist/store/data-accessor.d.ts.map +1 -1
- package/dist/store/data-accessor.js +22 -5
- package/dist/store/data-accessor.js.map +1 -1
- package/dist/store/data-safety-central.d.ts.map +1 -1
- package/dist/store/data-safety-central.js +272 -232
- package/dist/store/data-safety-central.js.map +1 -1
- package/dist/store/db-helpers.d.ts +19 -3
- package/dist/store/db-helpers.d.ts.map +1 -1
- package/dist/store/db-helpers.js +42 -58
- package/dist/store/db-helpers.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts +63 -4
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +166 -19
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/exodus/archive.d.ts +21 -7
- package/dist/store/exodus/archive.d.ts.map +1 -1
- package/dist/store/exodus/archive.js +60 -18
- package/dist/store/exodus/archive.js.map +1 -1
- package/dist/store/exodus/migrate.d.ts.map +1 -1
- package/dist/store/exodus/migrate.js +27 -23
- package/dist/store/exodus/migrate.js.map +1 -1
- package/dist/store/exodus/on-open.d.ts +16 -27
- package/dist/store/exodus/on-open.d.ts.map +1 -1
- package/dist/store/exodus/on-open.js +121 -169
- package/dist/store/exodus/on-open.js.map +1 -1
- package/dist/store/exodus/recovery.d.ts +90 -0
- package/dist/store/exodus/recovery.d.ts.map +1 -0
- package/dist/store/exodus/recovery.js +434 -0
- package/dist/store/exodus/recovery.js.map +1 -0
- package/dist/store/exodus/types.d.ts +15 -0
- package/dist/store/exodus/types.d.ts.map +1 -1
- package/dist/store/memory-accessor.d.ts +52 -3
- package/dist/store/memory-accessor.d.ts.map +1 -1
- package/dist/store/memory-accessor.js +122 -7
- package/dist/store/memory-accessor.js.map +1 -1
- package/dist/store/nexus-sqlite.d.ts +24 -6
- package/dist/store/nexus-sqlite.d.ts.map +1 -1
- package/dist/store/nexus-sqlite.js +39 -20
- package/dist/store/nexus-sqlite.js.map +1 -1
- package/dist/store/ports/domain-binding.d.ts.map +1 -1
- package/dist/store/ports/domain-binding.js +27 -5
- package/dist/store/ports/domain-binding.js.map +1 -1
- package/dist/store/schema/background-jobs.d.ts +181 -2
- package/dist/store/schema/background-jobs.d.ts.map +1 -1
- package/dist/store/schema/background-jobs.js +27 -3
- package/dist/store/schema/background-jobs.js.map +1 -1
- package/dist/store/schema/cleo-global/skills.d.ts +2 -2
- package/dist/store/schema/cleo-project/lifecycle.d.ts +2 -2
- package/dist/store/schema/cleo-project/runtime.d.ts +1 -1
- package/dist/store/schema/cleo-project/tasks-core.d.ts +1 -1
- package/dist/store/schema/lifecycle.d.ts +2 -2
- package/dist/store/schema/skills-schema.d.ts +2 -2
- package/dist/store/schema/tasks.d.ts +1 -1
- package/dist/store/session-store.d.ts +3 -1
- package/dist/store/session-store.d.ts.map +1 -1
- package/dist/store/session-store.js +252 -207
- package/dist/store/session-store.js.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts +39 -4
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.js +689 -527
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/tasks-sqlite.d.ts +3 -1
- package/dist/store/tasks-sqlite.d.ts.map +1 -1
- package/dist/store/tasks-sqlite.js +309 -279
- package/dist/store/tasks-sqlite.js.map +1 -1
- package/dist/store/validation-schemas.d.ts +8 -8
- package/dist/store/writer-lease.d.ts +80 -8
- package/dist/store/writer-lease.d.ts.map +1 -1
- package/dist/store/writer-lease.js +503 -134
- package/dist/store/writer-lease.js.map +1 -1
- package/dist/system/audit.d.ts +2 -1
- package/dist/system/audit.d.ts.map +1 -1
- package/dist/system/audit.js +118 -71
- package/dist/system/audit.js.map +1 -1
- package/dist/system/backup.d.ts.map +1 -1
- package/dist/system/backup.js +5 -5
- package/dist/system/backup.js.map +1 -1
- package/dist/system/inject-generate.js +1 -1
- package/dist/task-work/index.d.ts +3 -0
- package/dist/task-work/index.d.ts.map +1 -1
- package/dist/task-work/index.js +34 -37
- package/dist/task-work/index.js.map +1 -1
- package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
- package/dist/tasks/ac-coverage-gate.js +4 -0
- package/dist/tasks/ac-coverage-gate.js.map +1 -1
- package/dist/tasks/ac-immutability.d.ts +7 -4
- package/dist/tasks/ac-immutability.d.ts.map +1 -1
- package/dist/tasks/ac-immutability.js +19 -17
- package/dist/tasks/ac-immutability.js.map +1 -1
- package/dist/tasks/ac-table.d.ts +5 -5
- package/dist/tasks/ac-table.d.ts.map +1 -1
- package/dist/tasks/ac-table.js +26 -12
- package/dist/tasks/ac-table.js.map +1 -1
- package/dist/tasks/acceptance-input.d.ts +52 -0
- package/dist/tasks/acceptance-input.d.ts.map +1 -0
- package/dist/tasks/acceptance-input.js +259 -0
- package/dist/tasks/acceptance-input.js.map +1 -0
- package/dist/tasks/add.d.ts +67 -29
- package/dist/tasks/add.d.ts.map +1 -1
- package/dist/tasks/add.js +255 -97
- package/dist/tasks/add.js.map +1 -1
- package/dist/tasks/complete.d.ts +16 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +152 -55
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/decompose.d.ts +103 -0
- package/dist/tasks/decompose.d.ts.map +1 -0
- package/dist/tasks/decompose.js +224 -0
- package/dist/tasks/decompose.js.map +1 -0
- package/dist/tasks/delete.d.ts +5 -8
- package/dist/tasks/delete.d.ts.map +1 -1
- package/dist/tasks/delete.js +4 -3
- package/dist/tasks/delete.js.map +1 -1
- package/dist/tasks/dependency-check.d.ts +59 -0
- package/dist/tasks/dependency-check.d.ts.map +1 -1
- package/dist/tasks/dependency-check.js +73 -0
- package/dist/tasks/dependency-check.js.map +1 -1
- package/dist/tasks/deps-ready.d.ts +13 -1
- package/dist/tasks/deps-ready.d.ts.map +1 -1
- package/dist/tasks/deps-ready.js +16 -5
- package/dist/tasks/deps-ready.js.map +1 -1
- package/dist/tasks/engine-converters.d.ts +7 -0
- package/dist/tasks/engine-converters.d.ts.map +1 -1
- package/dist/tasks/engine-converters.js +8 -1
- package/dist/tasks/engine-converters.js.map +1 -1
- package/dist/tasks/evidence.d.ts +54 -26
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +292 -40
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/find.d.ts +16 -12
- package/dist/tasks/find.d.ts.map +1 -1
- package/dist/tasks/find.js +107 -93
- package/dist/tasks/find.js.map +1 -1
- package/dist/tasks/gate-runner.d.ts +88 -14
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +596 -253
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/tasks/hierarchy-policy.d.ts +8 -0
- package/dist/tasks/hierarchy-policy.d.ts.map +1 -1
- package/dist/tasks/hierarchy-policy.js +9 -2
- package/dist/tasks/hierarchy-policy.js.map +1 -1
- package/dist/tasks/hierarchy.d.ts +67 -1
- package/dist/tasks/hierarchy.d.ts.map +1 -1
- package/dist/tasks/hierarchy.js +81 -1
- package/dist/tasks/hierarchy.js.map +1 -1
- package/dist/tasks/index.d.ts +3 -0
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +3 -0
- package/dist/tasks/index.js.map +1 -1
- package/dist/tasks/infer-add-params.d.ts +1 -20
- package/dist/tasks/infer-add-params.d.ts.map +1 -1
- package/dist/tasks/infer-add-params.js +2 -203
- package/dist/tasks/infer-add-params.js.map +1 -1
- package/dist/tasks/list.d.ts +7 -2
- package/dist/tasks/list.d.ts.map +1 -1
- package/dist/tasks/list.js +24 -36
- package/dist/tasks/list.js.map +1 -1
- package/dist/tasks/ops.d.ts +6 -61
- package/dist/tasks/ops.d.ts.map +1 -1
- package/dist/tasks/ops.js +5 -53
- package/dist/tasks/ops.js.map +1 -1
- package/dist/tasks/plan.d.ts +15 -1
- package/dist/tasks/plan.d.ts.map +1 -1
- package/dist/tasks/plan.js +23 -25
- package/dist/tasks/plan.js.map +1 -1
- package/dist/tasks/population.d.ts +46 -0
- package/dist/tasks/population.d.ts.map +1 -0
- package/dist/tasks/population.js +88 -0
- package/dist/tasks/population.js.map +1 -0
- package/dist/tasks/reconcile-scope.d.ts +157 -0
- package/dist/tasks/reconcile-scope.d.ts.map +1 -0
- package/dist/tasks/reconcile-scope.js +242 -0
- package/dist/tasks/reconcile-scope.js.map +1 -0
- package/dist/tasks/req.d.ts +51 -12
- package/dist/tasks/req.d.ts.map +1 -1
- package/dist/tasks/req.js +125 -69
- package/dist/tasks/req.js.map +1 -1
- package/dist/tasks/session-scope.d.ts +6 -31
- package/dist/tasks/session-scope.d.ts.map +1 -1
- package/dist/tasks/session-scope.js +3 -17
- package/dist/tasks/session-scope.js.map +1 -1
- package/dist/tasks/severity-attestation.d.ts +39 -10
- package/dist/tasks/severity-attestation.d.ts.map +1 -1
- package/dist/tasks/severity-attestation.js +75 -36
- package/dist/tasks/severity-attestation.js.map +1 -1
- package/dist/tasks/task-next.d.ts +5 -0
- package/dist/tasks/task-next.d.ts.map +1 -1
- package/dist/tasks/task-next.js +10 -9
- package/dist/tasks/task-next.js.map +1 -1
- package/dist/tasks/task-reparent.d.ts.map +1 -1
- package/dist/tasks/task-reparent.js +3 -13
- package/dist/tasks/task-reparent.js.map +1 -1
- package/dist/tasks/update.d.ts +20 -43
- package/dist/tasks/update.d.ts.map +1 -1
- package/dist/tasks/update.js +64 -49
- package/dist/tasks/update.js.map +1 -1
- package/dist/teardown-signal.d.ts +72 -0
- package/dist/teardown-signal.d.ts.map +1 -0
- package/dist/teardown-signal.js +108 -0
- package/dist/teardown-signal.js.map +1 -0
- package/dist/validation/engine-ops.d.ts +17 -1
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +149 -11
- package/dist/validation/engine-ops.js.map +1 -1
- package/dist/workgraph/reparent-preflight.d.ts +1 -1
- package/dist/workgraph/reparent-preflight.d.ts.map +1 -1
- package/dist/workgraph/reparent-preflight.js +6 -1
- package/dist/workgraph/reparent-preflight.js.map +1 -1
- package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
- package/dist/workgraph/scaffold-apply.js +81 -76
- package/dist/workgraph/scaffold-apply.js.map +1 -1
- package/dist/workgraph/structure-validation.d.ts +1 -1
- package/dist/workgraph/structure-validation.d.ts.map +1 -1
- package/dist/workgraph/structure-validation.js +6 -1
- package/dist/workgraph/structure-validation.js.map +1 -1
- package/migrations/drizzle-tasks/20260919180000_t12263-job-ownership-fences/migration.sql +34 -0
- package/migrations/drizzle-tasks/20260919184500_t12265-durable-pending-proposals/migration.sql +4 -0
- package/package.json +18 -13
- package/schemas/config.schema.json +2 -2
- package/templates/CLEO-INJECTION.md +103 -101
|
@@ -62,10 +62,14 @@
|
|
|
62
62
|
* @see ./writer-lease-schema.ts — the drizzle table decls + bootstrap index assertion
|
|
63
63
|
* @see ./dual-scope-db.ts — the open chokepoint this engine routes through (Seams 0 & 1)
|
|
64
64
|
*/
|
|
65
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
65
66
|
import { randomUUID } from 'node:crypto';
|
|
67
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
66
68
|
import { resolve as resolvePath } from 'node:path';
|
|
67
69
|
import { getLogger } from '../logger.js';
|
|
68
|
-
import {
|
|
70
|
+
import { observeOperation } from './background-ops.js';
|
|
71
|
+
import { getDualScopeNativeDb, openDualScopeDb, openDualScopeDbAtPath, resolveDualScopeDbPath, } from './dual-scope-db.js';
|
|
72
|
+
import { openNativeDatabase } from './sqlite-native.js';
|
|
69
73
|
import { assertWriterLeaseActiveIndexPresent, WRITER_LEASES_ACTIVE_INDEX, WRITER_LEASES_TABLE, WRITER_QUEUE_TABLE, } from './writer-lease-schema.js';
|
|
70
74
|
/** Default lease TTL — aligned to `busy_timeout=30000` (specs/sqlite-pragmas.json). */
|
|
71
75
|
export const DEFAULT_TTL_MS = 30_000;
|
|
@@ -167,25 +171,36 @@ export function _resetWriterLeaseStateForTest() {
|
|
|
167
171
|
* the cached path-aware opener is used so the lease row lands in THAT file (the
|
|
168
172
|
* multi-project-in-one-process case — Finding 1).
|
|
169
173
|
*/
|
|
170
|
-
const defaultNativeDbResolver = async (scope, dbPath) => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// `handle.dbPath` is the path this open resolved; fall back to the scope→path
|
|
185
|
-
// resolver defensively (the handle always carries it in practice).
|
|
186
|
-
const resolvedPath = handle.dbPath ?? dbPath ?? resolveDualScopeDbPath(scope);
|
|
187
|
-
return { native, dbPath: resolvedPath };
|
|
174
|
+
const defaultNativeDbResolver = async (scope, dbPath, execution) => {
|
|
175
|
+
execution?.assertActive();
|
|
176
|
+
const explicit = dbPath !== undefined && (execution !== undefined || dbPath !== resolveDualScopeDbPath(scope));
|
|
177
|
+
// Scoped leases already carry the exact database resource. Forward their lifetime
|
|
178
|
+
// directly through the existing opener API, avoiding the paths routing module's
|
|
179
|
+
// nexus/store dependency chain. Legacy canonical opens retain their exodus hook.
|
|
180
|
+
const handle = explicit
|
|
181
|
+
? scope === 'project'
|
|
182
|
+
? await openDualScopeDbAtPath('project', dbPath, undefined, { execution })
|
|
183
|
+
: await openDualScopeDbAtPath('global', dbPath, undefined, { execution })
|
|
184
|
+
: scope === 'project'
|
|
185
|
+
? await openDualScopeDb('project', undefined, { execution })
|
|
186
|
+
: await openDualScopeDb('global', undefined, { execution });
|
|
187
|
+
return { native: getDualScopeNativeDb(handle), dbPath: handle.dbPath };
|
|
188
188
|
};
|
|
189
|
+
/** Observe native resolution within the shared budget without pretending its opener was preempted. */
|
|
190
|
+
async function resolveLeaseTarget(scope, dbPath, execution) {
|
|
191
|
+
execution?.assertActive();
|
|
192
|
+
const pending = _nativeDbResolver(scope, dbPath, execution);
|
|
193
|
+
if (!execution)
|
|
194
|
+
return pending;
|
|
195
|
+
const observed = await observeOperation(execution, pending);
|
|
196
|
+
if (!observed.settled) {
|
|
197
|
+
throw new Error('Writer native resolution remains unresolved after operation cancellation; the opener may still finish', { cause: observed.reason });
|
|
198
|
+
}
|
|
199
|
+
if (!observed.success)
|
|
200
|
+
throw observed.error;
|
|
201
|
+
execution.assertActive();
|
|
202
|
+
return observed.value;
|
|
203
|
+
}
|
|
189
204
|
let _nativeDbResolver = defaultNativeDbResolver;
|
|
190
205
|
/**
|
|
191
206
|
* Override how the engine obtains a scope's native `cleo.db` handle + path. Tests
|
|
@@ -205,8 +220,8 @@ export function _setNativeDbResolverForTest(resolver) {
|
|
|
205
220
|
_nativeDbResolver = defaultNativeDbResolver;
|
|
206
221
|
return;
|
|
207
222
|
}
|
|
208
|
-
_nativeDbResolver = async (scope, dbPath) => {
|
|
209
|
-
const out = await resolver(scope, dbPath);
|
|
223
|
+
_nativeDbResolver = async (scope, dbPath, execution) => {
|
|
224
|
+
const out = await resolver(scope, dbPath, execution);
|
|
210
225
|
// Adapt a bare native handle to a LeaseTarget with a stable synthetic path.
|
|
211
226
|
return 'native' in out ? out : { native: out, dbPath: dbPath ?? scopePathToken(scope) };
|
|
212
227
|
};
|
|
@@ -295,12 +310,20 @@ function canonicalizeDbPath(dbPath) {
|
|
|
295
310
|
* @param scope - The cleo.db scope.
|
|
296
311
|
* @param dbPath - An absolute or relative path to the scope's cleo.db. Normalized
|
|
297
312
|
* via `path.resolve()` before freezing.
|
|
298
|
-
* @returns A frozen, normalized identity.
|
|
313
|
+
* @returns A frozen, normalized identity, with device/inode when the file already exists.
|
|
314
|
+
* @remarks Required schema ownership refuses identities without captured file provenance.
|
|
315
|
+
* Missing abstract paths remain valid for legacy unscoped consumers; other stat errors propagate.
|
|
299
316
|
*
|
|
300
317
|
* @task T12042 (E6-L12b)
|
|
301
318
|
*/
|
|
302
319
|
export function makeWriterLeaseIdentity(scope, dbPath) {
|
|
303
|
-
|
|
320
|
+
const path = resolvePath(dbPath);
|
|
321
|
+
const file = statSync(path, { bigint: true, throwIfNoEntry: false });
|
|
322
|
+
return Object.freeze({
|
|
323
|
+
scope,
|
|
324
|
+
dbPath: path,
|
|
325
|
+
...(file ? { fileDevice: String(file.dev), fileInode: String(file.ino) } : {}),
|
|
326
|
+
});
|
|
304
327
|
}
|
|
305
328
|
// ── DB-identity registry (typed WeakMap — immutable, concurrency-safe) ──────
|
|
306
329
|
/**
|
|
@@ -336,8 +359,13 @@ const _dbIdentityRegistry = new WeakMap();
|
|
|
336
359
|
export function registerDbIdentity(db, identity) {
|
|
337
360
|
const existing = _dbIdentityRegistry.get(db);
|
|
338
361
|
if (existing) {
|
|
339
|
-
if (existing.scope === identity.scope &&
|
|
340
|
-
|
|
362
|
+
if (existing.scope === identity.scope &&
|
|
363
|
+
existing.dbPath === identity.dbPath &&
|
|
364
|
+
(existing.fileDevice === undefined ||
|
|
365
|
+
identity.fileDevice === undefined ||
|
|
366
|
+
(existing.fileDevice === identity.fileDevice && existing.fileInode === identity.fileInode))) {
|
|
367
|
+
// Never upgrade a legacy path-only identity using a later filesystem observation.
|
|
368
|
+
return; // idempotent — same canonical identity; original provenance remains immutable
|
|
341
369
|
}
|
|
342
370
|
throw new Error('E_WRITER_LEASE_IDENTITY_MISMATCH: the same drizzle DB handle was ' +
|
|
343
371
|
`previously registered as ${existing.scope}::${existing.dbPath} ` +
|
|
@@ -437,6 +465,10 @@ class InternalLeaseHandle {
|
|
|
437
465
|
heartbeat() {
|
|
438
466
|
if (this.released)
|
|
439
467
|
return;
|
|
468
|
+
if (this.nativeDb.isOpen && this.nativeDb.isTransaction) {
|
|
469
|
+
log().debug({ scope: this.scope, lane: this.lane }, 'Lease heartbeat deferred while another caller owns the native transaction');
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
440
472
|
// The heartbeat fires from an unref'd timer; if the underlying native handle
|
|
441
473
|
// was closed out from under a still-held lease (cache eviction / shutdown /
|
|
442
474
|
// test teardown), `prepare()` throws `database is not open` INSIDE the timer
|
|
@@ -468,9 +500,10 @@ class InternalLeaseHandle {
|
|
|
468
500
|
* @param nativeDb - Handle to use for the release update. A caller may retry
|
|
469
501
|
* with a freshly resolved handle when the lease's original shared handle
|
|
470
502
|
* was closed mid-operation.
|
|
503
|
+
* @param guardTransaction - Refuse to borrow another caller's native transaction.
|
|
471
504
|
* @returns True when the release update executed successfully.
|
|
472
505
|
*/
|
|
473
|
-
releaseRow(nativeDb = this.nativeDb) {
|
|
506
|
+
releaseRow(nativeDb = this.nativeDb, guardTransaction = false) {
|
|
474
507
|
if (!this.released) {
|
|
475
508
|
this.released = true;
|
|
476
509
|
if (this.heartbeatTimer) {
|
|
@@ -478,18 +511,46 @@ class InternalLeaseHandle {
|
|
|
478
511
|
this.heartbeatTimer = null;
|
|
479
512
|
}
|
|
480
513
|
}
|
|
514
|
+
let began = false;
|
|
481
515
|
try {
|
|
516
|
+
if (guardTransaction) {
|
|
517
|
+
nativeDb.exec('BEGIN IMMEDIATE');
|
|
518
|
+
began = true;
|
|
519
|
+
}
|
|
482
520
|
nativeDb
|
|
483
521
|
.prepare(`UPDATE ${WRITER_LEASES_TABLE} SET active = 0, reentrancy_depth = 0 ` +
|
|
484
522
|
`WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
|
|
485
523
|
.run(this.scope, this.lane, this.holderId, this.epoch);
|
|
524
|
+
if (began)
|
|
525
|
+
nativeDb.exec('COMMIT');
|
|
486
526
|
return true;
|
|
487
527
|
}
|
|
488
528
|
catch (err) {
|
|
529
|
+
if (began)
|
|
530
|
+
nativeDb.exec('ROLLBACK');
|
|
489
531
|
log().debug({ scope: this.scope, lane: this.lane, err: err instanceof Error ? err.message : err }, 'writer-lease releaseRow failed (native handle likely closed); fresh-handle retry required');
|
|
490
532
|
return false;
|
|
491
533
|
}
|
|
492
534
|
}
|
|
535
|
+
/** Adjust a shared grant using its existing connection only; never reopen during cleanup. */
|
|
536
|
+
releaseScopedDepth(depth) {
|
|
537
|
+
let began = false;
|
|
538
|
+
try {
|
|
539
|
+
this.nativeDb.exec('BEGIN IMMEDIATE');
|
|
540
|
+
began = true;
|
|
541
|
+
this.nativeDb
|
|
542
|
+
.prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = ? WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
|
|
543
|
+
.run(depth, this.scope, this.lane, this.holderId, this.epoch);
|
|
544
|
+
this.nativeDb.exec('COMMIT');
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
if (began)
|
|
549
|
+
this.nativeDb.exec('ROLLBACK');
|
|
550
|
+
log().debug({ scope: this.scope, lane: this.lane, error }, 'Scoped lease depth cleanup deferred; original handle unavailable or owned');
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
493
554
|
async release() {
|
|
494
555
|
await releaseGrant(this.scope, this.dbPath, this.lane);
|
|
495
556
|
}
|
|
@@ -551,6 +612,30 @@ export function hasActiveGrant(scope, lane, dbPath) {
|
|
|
551
612
|
const entry = _grantMemo.get(memoKey(scope, normalized, lane));
|
|
552
613
|
return entry !== undefined && entry.refcount > 0;
|
|
553
614
|
}
|
|
615
|
+
/** Increment only a live captured grant, refusing borrowed native transactions for scoped callers. */
|
|
616
|
+
function incrementGrantDepth(native, scope, lane, entry, execution) {
|
|
617
|
+
execution?.assertActive();
|
|
618
|
+
if (execution)
|
|
619
|
+
native.exec('BEGIN IMMEDIATE');
|
|
620
|
+
try {
|
|
621
|
+
execution?.assertActive();
|
|
622
|
+
const result = native
|
|
623
|
+
.prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth + 1 ` +
|
|
624
|
+
`WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
|
|
625
|
+
.run(scope, lane, entry.handle.holderId, entry.handle.epoch);
|
|
626
|
+
if (execution && Number(result.changes) !== 1) {
|
|
627
|
+
throw new LeaseUnavailableError(scope, lane, 'captured grant is no longer active');
|
|
628
|
+
}
|
|
629
|
+
execution?.assertActive();
|
|
630
|
+
if (execution)
|
|
631
|
+
native.exec('COMMIT');
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
if (execution)
|
|
635
|
+
native.exec('ROLLBACK');
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
554
639
|
/**
|
|
555
640
|
* AC4 guard. Assert this process holds the `(scope, lane)` writer lease before it
|
|
556
641
|
* opens a dedicated WRITE handle. In `off` mode the assertion is a no-op (there is
|
|
@@ -577,10 +662,12 @@ export function assertWriterLeaseHeld(scope, lane, dbPath) {
|
|
|
577
662
|
* epoch on success, or `null` if a live holder owns the row (caller backs off and
|
|
578
663
|
* retries; the queue row was enqueued on the first miss).
|
|
579
664
|
*/
|
|
580
|
-
function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs) {
|
|
665
|
+
function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs, execution) {
|
|
666
|
+
execution?.assertActive();
|
|
581
667
|
const now = Date.now();
|
|
582
668
|
nativeDb.exec('BEGIN IMMEDIATE TRANSACTION');
|
|
583
669
|
try {
|
|
670
|
+
execution?.assertActive();
|
|
584
671
|
const active = nativeDb
|
|
585
672
|
.prepare(`SELECT id, holder_id, holder_pid, epoch, heartbeat_at, ttl_ms, reentrancy_depth ` +
|
|
586
673
|
`FROM ${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ? AND active = 1`)
|
|
@@ -590,6 +677,7 @@ function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs) {
|
|
|
590
677
|
const nextEpoch = nativeDb
|
|
591
678
|
.prepare(`SELECT COALESCE(MAX(epoch), 0) + 1 AS e FROM ${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ?`)
|
|
592
679
|
.get(scope, lane)?.e ?? 1;
|
|
680
|
+
execution?.assertActive();
|
|
593
681
|
nativeDb
|
|
594
682
|
.prepare(`INSERT INTO ${WRITER_LEASES_TABLE} ` +
|
|
595
683
|
`(scope, lane, holder_id, holder_pid, epoch, acquired_at, heartbeat_at, ttl_ms, reentrancy_depth, active) ` +
|
|
@@ -601,6 +689,7 @@ function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs) {
|
|
|
601
689
|
if (active.holder_id === holderId) {
|
|
602
690
|
// Same holder (defensive — same-process re-entrancy is handled by the memo
|
|
603
691
|
// before reaching here). Bump the durable depth and re-assert the epoch.
|
|
692
|
+
execution?.assertActive();
|
|
604
693
|
nativeDb
|
|
605
694
|
.prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth + 1, heartbeat_at = ? ` +
|
|
606
695
|
`WHERE id = ?`)
|
|
@@ -614,6 +703,7 @@ function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs) {
|
|
|
614
703
|
const stale = now - active.heartbeat_at > active.ttl_ms && !isPidAlive(active.holder_pid);
|
|
615
704
|
if (stale) {
|
|
616
705
|
const reclaimedEpoch = active.epoch + 1;
|
|
706
|
+
execution?.assertActive();
|
|
617
707
|
nativeDb
|
|
618
708
|
.prepare(`UPDATE ${WRITER_LEASES_TABLE} ` +
|
|
619
709
|
`SET holder_id = ?, holder_pid = ?, epoch = ?, acquired_at = ?, heartbeat_at = ?, ttl_ms = ?, reentrancy_depth = 1 ` +
|
|
@@ -650,10 +740,22 @@ function enqueueWaiter(nativeDb, scope, lane, holderId, priority, ttlMs) {
|
|
|
650
740
|
.run(scope, lane, holderId, priority, now, now + ttlMs);
|
|
651
741
|
}
|
|
652
742
|
/** Remove this holder's waiter row once granted (or on give-up). */
|
|
653
|
-
function dequeueWaiter(nativeDb, scope, lane, holderId) {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
.
|
|
743
|
+
function dequeueWaiter(nativeDb, scope, lane, holderId, guardTransaction = false) {
|
|
744
|
+
// Cleanup may run after cancellation, but it must never join another owner's transaction.
|
|
745
|
+
if (guardTransaction)
|
|
746
|
+
nativeDb.exec('BEGIN IMMEDIATE');
|
|
747
|
+
try {
|
|
748
|
+
nativeDb
|
|
749
|
+
.prepare(`DELETE FROM ${WRITER_QUEUE_TABLE} WHERE scope = ? AND lane = ? AND holder_id = ?`)
|
|
750
|
+
.run(scope, lane, holderId);
|
|
751
|
+
if (guardTransaction)
|
|
752
|
+
nativeDb.exec('COMMIT');
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
if (guardTransaction)
|
|
756
|
+
nativeDb.exec('ROLLBACK');
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
657
759
|
}
|
|
658
760
|
// ── Acquire / release surface ─────────────────────────────────────────────────
|
|
659
761
|
/**
|
|
@@ -668,6 +770,13 @@ function dequeueWaiter(nativeDb, scope, lane, holderId) {
|
|
|
668
770
|
* - `require` mode → throws {@link LeaseUnavailableError} if the row cannot be
|
|
669
771
|
* taken within the acquire window (`min(ACQUIRE_DEADLINE_MS, ttlMs)`).
|
|
670
772
|
*
|
|
773
|
+
* @remarks A supplied execution context preserves its original deadline and requires
|
|
774
|
+
* an explicit database path. Cancellation never grants degraded write permission.
|
|
775
|
+
* Unsettled native resolution is reported explicitly; its opener is not preempted.
|
|
776
|
+
* @example
|
|
777
|
+
* ```ts
|
|
778
|
+
* const lease = await acquireWriterLease('project', 'brain', options);
|
|
779
|
+
* ```
|
|
671
780
|
* @param scope - The cleo.db scope.
|
|
672
781
|
* @param lane - The write lane within the scope.
|
|
673
782
|
* @param opts - Priority / TTL / re-entrancy options.
|
|
@@ -676,6 +785,10 @@ function dequeueWaiter(nativeDb, scope, lane, holderId) {
|
|
|
676
785
|
* @task T11627
|
|
677
786
|
*/
|
|
678
787
|
export async function acquireWriterLease(scope, lane, opts) {
|
|
788
|
+
opts?.execution?.assertActive();
|
|
789
|
+
if (opts?.execution && !opts.dbPath) {
|
|
790
|
+
throw new TypeError('Scoped writer acquisition requires an explicit database path');
|
|
791
|
+
}
|
|
679
792
|
const mode = effectiveMode();
|
|
680
793
|
const reentrant = opts?.reentrant ?? true;
|
|
681
794
|
// `off` mode — pure pass-through. No row written, no memo entry; busy_timeout on
|
|
@@ -696,11 +809,10 @@ export async function acquireWriterLease(scope, lane, opts) {
|
|
|
696
809
|
if (existing) {
|
|
697
810
|
existing.refcount += 1;
|
|
698
811
|
try {
|
|
699
|
-
|
|
700
|
-
native
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
.run(scope, lane, existing.handle.holderId, existing.handle.epoch);
|
|
812
|
+
opts?.execution?.assertActive();
|
|
813
|
+
const { native } = await resolveLeaseTarget(scope, dbPath, opts?.execution);
|
|
814
|
+
opts?.execution?.assertActive();
|
|
815
|
+
incrementGrantDepth(native, scope, lane, existing, opts?.execution);
|
|
704
816
|
}
|
|
705
817
|
catch (err) {
|
|
706
818
|
// Native resolver or depth-update failed — roll back the refcount
|
|
@@ -709,14 +821,15 @@ export async function acquireWriterLease(scope, lane, opts) {
|
|
|
709
821
|
existing.refcount -= 1;
|
|
710
822
|
throw err;
|
|
711
823
|
}
|
|
712
|
-
return existing.handle;
|
|
824
|
+
return scopeLeaseRelease(existing.handle, dbPath, opts?.execution);
|
|
713
825
|
}
|
|
714
826
|
// Single-flight FIRST acquisition (Finding 2): if another caller is already
|
|
715
827
|
// mid-acquire for this key, await its in-flight promise and share the grant
|
|
716
828
|
// (refcount++) instead of racing a second full claim that would spin the whole
|
|
717
829
|
// acquire window and degrade to a lease-less write. Decided synchronously here
|
|
718
830
|
// — no `await` has run since the memo check above, so the two checks are atomic.
|
|
719
|
-
|
|
831
|
+
// Scoped callers must not inherit another caller's deadline or cancellation.
|
|
832
|
+
const inflight = opts?.execution ? undefined : _inflightAcquire.get(key);
|
|
720
833
|
if (inflight) {
|
|
721
834
|
const shared = await inflight;
|
|
722
835
|
// Re-check the memo: the in-flight acquire may have already released (e.g.
|
|
@@ -725,17 +838,16 @@ export async function acquireWriterLease(scope, lane, opts) {
|
|
|
725
838
|
if (entry && entry.handle === shared) {
|
|
726
839
|
entry.refcount += 1;
|
|
727
840
|
try {
|
|
728
|
-
|
|
729
|
-
native
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
.run(scope, lane, entry.handle.holderId, entry.handle.epoch);
|
|
841
|
+
opts?.execution?.assertActive();
|
|
842
|
+
const { native } = await resolveLeaseTarget(scope, dbPath, opts?.execution);
|
|
843
|
+
opts?.execution?.assertActive();
|
|
844
|
+
incrementGrantDepth(native, scope, lane, entry, opts?.execution);
|
|
733
845
|
}
|
|
734
846
|
catch (err) {
|
|
735
847
|
entry.refcount -= 1;
|
|
736
848
|
throw err;
|
|
737
849
|
}
|
|
738
|
-
return entry.handle;
|
|
850
|
+
return scopeLeaseRelease(entry.handle, dbPath, opts?.execution);
|
|
739
851
|
}
|
|
740
852
|
// The shared acquire is no longer active — fall through to a fresh acquire.
|
|
741
853
|
}
|
|
@@ -745,11 +857,11 @@ export async function acquireWriterLease(scope, lane, opts) {
|
|
|
745
857
|
// it. Only the reentrant path participates; reentrant:false callers (test cross-
|
|
746
858
|
// holder contention) deliberately bypass the memo + single-flight.
|
|
747
859
|
const acquirePromise = performFirstAcquire(scope, dbPath, lane, mode, key, opts);
|
|
748
|
-
if (reentrant) {
|
|
860
|
+
if (reentrant && !opts?.execution) {
|
|
749
861
|
_inflightAcquire.set(key, acquirePromise);
|
|
750
862
|
}
|
|
751
863
|
try {
|
|
752
|
-
return await acquirePromise;
|
|
864
|
+
return scopeLeaseRelease(await acquirePromise, dbPath, opts?.execution);
|
|
753
865
|
}
|
|
754
866
|
finally {
|
|
755
867
|
// Always clear our in-flight entry. A degraded acquire resolves to a no-op
|
|
@@ -773,7 +885,9 @@ export async function acquireWriterLease(scope, lane, opts) {
|
|
|
773
885
|
* reentrant followers re-check the memo against before sharing.
|
|
774
886
|
*/
|
|
775
887
|
async function performFirstAcquire(scope, dbPath, lane, mode, key, opts) {
|
|
776
|
-
|
|
888
|
+
opts?.execution?.assertActive();
|
|
889
|
+
const { native } = await resolveLeaseTarget(scope, dbPath, opts?.execution);
|
|
890
|
+
opts?.execution?.assertActive();
|
|
777
891
|
// Defensive bootstrap assert: the partial-unique active index MUST exist or AC1
|
|
778
892
|
// is unenforced. Cheap single-row sqlite_master lookup, runs on first acquire.
|
|
779
893
|
assertWriterLeaseActiveIndexPresent(native);
|
|
@@ -784,66 +898,116 @@ async function performFirstAcquire(scope, dbPath, lane, mode, key, opts) {
|
|
|
784
898
|
// not block for a grant longer than the lease itself would live. busy_timeout on
|
|
785
899
|
// the IMMEDIATE lock still backstops every individual claim attempt.
|
|
786
900
|
const acquireWindowMs = Math.min(ACQUIRE_DEADLINE_MS, Math.max(1, ttlMs));
|
|
787
|
-
const deadline = Date.now() + acquireWindowMs;
|
|
901
|
+
const deadline = Math.min(Date.now() + acquireWindowMs, opts?.execution?.deadlineAt ?? Infinity);
|
|
788
902
|
let enqueued = false;
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (mode === 'require') {
|
|
815
|
-
throw new LeaseUnavailableError(scope, lane, 'live holder did not release within deadline');
|
|
903
|
+
try {
|
|
904
|
+
for (;;) {
|
|
905
|
+
opts?.execution?.assertActive();
|
|
906
|
+
const epoch = tryClaimOnce(native, scope, lane, holderId, ttlMs, opts?.execution);
|
|
907
|
+
if (epoch !== null) {
|
|
908
|
+
if (enqueued)
|
|
909
|
+
dequeueWaiter(native, scope, lane, holderId, opts?.execution !== undefined);
|
|
910
|
+
const handle = new InternalLeaseHandle({
|
|
911
|
+
scope,
|
|
912
|
+
lane,
|
|
913
|
+
epoch,
|
|
914
|
+
holderId,
|
|
915
|
+
dbPath,
|
|
916
|
+
nativeDb: native,
|
|
917
|
+
ttlMs,
|
|
918
|
+
});
|
|
919
|
+
try {
|
|
920
|
+
opts?.execution?.assertActive();
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
handle.releaseRow();
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
926
|
+
_grantMemo.set(key, { handle, refcount: 1 });
|
|
927
|
+
return handle;
|
|
816
928
|
}
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
929
|
+
// Contended by a live holder. Enqueue once for ordering/aging, then back off.
|
|
930
|
+
if (!enqueued) {
|
|
931
|
+
opts?.execution?.assertActive();
|
|
932
|
+
enqueueWaiter(native, scope, lane, holderId, priority, ttlMs);
|
|
933
|
+
enqueued = true;
|
|
934
|
+
}
|
|
935
|
+
if (Date.now() >= deadline) {
|
|
936
|
+
if (enqueued)
|
|
937
|
+
dequeueWaiter(native, scope, lane, holderId, opts?.execution !== undefined);
|
|
938
|
+
opts?.execution?.assertActive();
|
|
939
|
+
if (mode === 'require' || opts?.execution) {
|
|
940
|
+
throw new LeaseUnavailableError(scope, lane, 'live holder did not release within deadline');
|
|
941
|
+
}
|
|
942
|
+
// local: degrade to today's behaviour — proceed without a lease. busy_timeout
|
|
943
|
+
// on the connection still serializes the actual write. A no-op handle writes
|
|
944
|
+
// no row, starts no heartbeat, and is never memoized — so single-flight
|
|
945
|
+
// followers find no memo entry and acquire freshly.
|
|
946
|
+
const active = native
|
|
947
|
+
.prepare(`SELECT holder_id, holder_pid, epoch, heartbeat_at, ttl_ms ` +
|
|
948
|
+
`FROM ${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ? AND active = 1`)
|
|
949
|
+
.get(scope, lane);
|
|
950
|
+
log().warn({
|
|
951
|
+
scope,
|
|
952
|
+
lane,
|
|
953
|
+
dbPath,
|
|
954
|
+
activeHolderId: active?.holder_id,
|
|
955
|
+
activeHolderPid: active?.holder_pid,
|
|
956
|
+
activeEpoch: active?.epoch,
|
|
957
|
+
activeHeartbeatAgeMs: active === undefined ? undefined : Math.max(0, Date.now() - active.heartbeat_at),
|
|
958
|
+
}, 'writer-lease acquire deadline exceeded; proceeding under busy_timeout fallback (degraded)');
|
|
959
|
+
return makeNoopHandle(scope, lane);
|
|
960
|
+
}
|
|
961
|
+
await sleepAsync(Math.min(CLAIM_RETRY_DELAY_MS, Math.max(0, deadline - Date.now())));
|
|
835
962
|
}
|
|
836
|
-
await sleepAsync(CLAIM_RETRY_DELAY_MS);
|
|
837
963
|
}
|
|
964
|
+
finally {
|
|
965
|
+
// Cancellation removes only this operation's waiter; never another holder's grant.
|
|
966
|
+
if (enqueued)
|
|
967
|
+
dequeueWaiter(native, scope, lane, holderId, opts?.execution !== undefined);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/** Bind cleanup to this caller's original grant without reopening after cancellation. */
|
|
971
|
+
function scopeLeaseRelease(handle, dbPath, execution) {
|
|
972
|
+
if (!execution)
|
|
973
|
+
return handle;
|
|
974
|
+
let released = false;
|
|
975
|
+
let cleanupPending = false;
|
|
976
|
+
return {
|
|
977
|
+
get cleanupPending() {
|
|
978
|
+
return cleanupPending;
|
|
979
|
+
},
|
|
980
|
+
scope: handle.scope,
|
|
981
|
+
lane: handle.lane,
|
|
982
|
+
epoch: handle.epoch,
|
|
983
|
+
async release() {
|
|
984
|
+
if (released)
|
|
985
|
+
return;
|
|
986
|
+
released = true;
|
|
987
|
+
cleanupPending = await releaseGrant(handle.scope, dbPath, handle.lane, handle);
|
|
988
|
+
},
|
|
989
|
+
heartbeat() {
|
|
990
|
+
execution.assertActive();
|
|
991
|
+
if (!released)
|
|
992
|
+
handle.heartbeat();
|
|
993
|
+
},
|
|
994
|
+
};
|
|
838
995
|
}
|
|
839
996
|
/** Decrement the memoized grant; free the row at depth 0. */
|
|
840
|
-
async function releaseGrant(scope, dbPath, lane) {
|
|
997
|
+
async function releaseGrant(scope, dbPath, lane, scopedHandle) {
|
|
841
998
|
const key = memoKey(scope, dbPath, lane);
|
|
842
999
|
const entry = _grantMemo.get(key);
|
|
843
|
-
if (!entry)
|
|
844
|
-
|
|
1000
|
+
if (!entry || (scopedHandle && entry.handle !== scopedHandle)) {
|
|
1001
|
+
// Stop a stale scoped timer without touching the successor's epoch or reopening.
|
|
1002
|
+
return scopedHandle instanceof InternalLeaseHandle
|
|
1003
|
+
? !scopedHandle.releaseRow(undefined, true)
|
|
1004
|
+
: false;
|
|
1005
|
+
}
|
|
845
1006
|
entry.refcount -= 1;
|
|
846
1007
|
if (entry.refcount > 0) {
|
|
1008
|
+
if (scopedHandle) {
|
|
1009
|
+
return !entry.handle.releaseScopedDepth(entry.refcount);
|
|
1010
|
+
}
|
|
847
1011
|
// Still re-entered above this frame — decrement the durable depth only. Pin the
|
|
848
1012
|
// resolver at the grant's own dbPath so the depth-write lands in the SAME file
|
|
849
1013
|
// the lease row lives in (matches the memo key, not the cwd-default).
|
|
@@ -852,22 +1016,27 @@ async function releaseGrant(scope, dbPath, lane) {
|
|
|
852
1016
|
.prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth - 1 ` +
|
|
853
1017
|
`WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
|
|
854
1018
|
.run(scope, lane, entry.handle.holderId, entry.handle.epoch);
|
|
855
|
-
return;
|
|
1019
|
+
return false;
|
|
856
1020
|
}
|
|
857
1021
|
// Depth 0 — free the row and evict the memo.
|
|
858
1022
|
_grantMemo.delete(key);
|
|
859
|
-
if (entry.handle.releaseRow())
|
|
860
|
-
return;
|
|
1023
|
+
if (entry.handle.releaseRow(undefined, scopedHandle !== undefined))
|
|
1024
|
+
return false;
|
|
1025
|
+
if (scopedHandle) {
|
|
1026
|
+
log().debug({ scope, lane, dbPath }, 'Scoped lease release deferred to TTL recovery; no replacement handle opened');
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
861
1029
|
// The domain can close and replace the shared dual-scope handle while a lease
|
|
862
1030
|
// is still unwinding. Closing SQLite does not mutate the persisted lease row;
|
|
863
1031
|
// retry through the path-aware resolver so the next writer does not wait for
|
|
864
1032
|
// the full stale-holder TTL.
|
|
865
1033
|
try {
|
|
866
1034
|
const { native } = await _nativeDbResolver(scope, dbPath);
|
|
867
|
-
entry.handle.releaseRow(native);
|
|
1035
|
+
return !entry.handle.releaseRow(native);
|
|
868
1036
|
}
|
|
869
1037
|
catch (err) {
|
|
870
1038
|
log().debug({ scope, lane, dbPath, err: err instanceof Error ? err.message : err }, 'writer-lease fresh-handle release retry failed; row will expire by TTL');
|
|
1039
|
+
return true;
|
|
871
1040
|
}
|
|
872
1041
|
}
|
|
873
1042
|
/**
|
|
@@ -887,6 +1056,24 @@ function makeNoopHandle(scope, lane) {
|
|
|
887
1056
|
},
|
|
888
1057
|
};
|
|
889
1058
|
}
|
|
1059
|
+
/** Release only this cold-open claim, refusing another native transaction when scoped. */
|
|
1060
|
+
function releaseColdOpenClaim(nativeDb, scope, lane, holderId, epoch, guardTransaction) {
|
|
1061
|
+
if (guardTransaction)
|
|
1062
|
+
nativeDb.exec('BEGIN IMMEDIATE');
|
|
1063
|
+
try {
|
|
1064
|
+
nativeDb
|
|
1065
|
+
.prepare(`UPDATE ${WRITER_LEASES_TABLE} SET active = 0, reentrancy_depth = 0 ` +
|
|
1066
|
+
`WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
|
|
1067
|
+
.run(scope, lane, holderId, epoch);
|
|
1068
|
+
if (guardTransaction)
|
|
1069
|
+
nativeDb.exec('COMMIT');
|
|
1070
|
+
}
|
|
1071
|
+
catch (error) {
|
|
1072
|
+
if (guardTransaction)
|
|
1073
|
+
nativeDb.exec('ROLLBACK');
|
|
1074
|
+
throw error;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
890
1077
|
/**
|
|
891
1078
|
* Primary surface: acquire → run `fn` → release (always, even on throw).
|
|
892
1079
|
*
|
|
@@ -897,6 +1084,14 @@ function makeNoopHandle(scope, lane) {
|
|
|
897
1084
|
* - `require` mode → an unacquirable lease throws {@link LeaseUnavailableError}
|
|
898
1085
|
* before `fn` runs.
|
|
899
1086
|
*
|
|
1087
|
+
* @remarks A supplied execution context preserves its original deadline and requires
|
|
1088
|
+
* an explicit database path. Cancellation never grants degraded write permission.
|
|
1089
|
+
* Unsettled native resolution is reported explicitly; its opener is not preempted.
|
|
1090
|
+
* @example
|
|
1091
|
+
* ```ts
|
|
1092
|
+
* const value = await withWriterLease('project', 'brain', write, options);
|
|
1093
|
+
* ```
|
|
1094
|
+
* @typeParam T - Result of the accepted write callback.
|
|
900
1095
|
* @param scope - The cleo.db scope.
|
|
901
1096
|
* @param lane - The write lane.
|
|
902
1097
|
* @param fn - The work to run while holding the lease; receives the handle.
|
|
@@ -908,6 +1103,7 @@ function makeNoopHandle(scope, lane) {
|
|
|
908
1103
|
export async function withWriterLease(scope, lane, fn, opts) {
|
|
909
1104
|
const handle = await acquireWriterLease(scope, lane, opts);
|
|
910
1105
|
try {
|
|
1106
|
+
opts?.execution?.assertActive();
|
|
911
1107
|
return await fn(handle);
|
|
912
1108
|
}
|
|
913
1109
|
finally {
|
|
@@ -985,6 +1181,139 @@ function ensureColdOpenLeaseTables(nativeDb) {
|
|
|
985
1181
|
nativeDb.exec(stmt);
|
|
986
1182
|
}
|
|
987
1183
|
}
|
|
1184
|
+
/** Operation-local membership, not a process-wide grant or domain-handle cache. */
|
|
1185
|
+
const requiredColdOpenScope = new AsyncLocalStorage();
|
|
1186
|
+
/** Refuse replacement of the exact file protected by this operation. */
|
|
1187
|
+
function assertColdOpenFile(frame) {
|
|
1188
|
+
const current = statSync(frame.path, { bigint: true });
|
|
1189
|
+
if (!frame.active || current.dev !== frame.device || current.ino !== frame.inode) {
|
|
1190
|
+
throw new LeaseUnavailableError(frame.scope, 'tasks', 'required schema ownership expired or database file replaced');
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
/** Hold schema ownership independently of a callback that can close its domain handle. */
|
|
1194
|
+
async function withRequiredColdOpenLease(scope, nativeDb, fn, opts) {
|
|
1195
|
+
opts.execution?.assertActive();
|
|
1196
|
+
const captured = opts.requiredIdentity;
|
|
1197
|
+
const path = realpathSync(captured.dbPath);
|
|
1198
|
+
const identity = statSync(path, { bigint: true });
|
|
1199
|
+
if (captured.scope !== scope ||
|
|
1200
|
+
captured.fileDevice !== String(identity.dev) ||
|
|
1201
|
+
captured.fileInode !== String(identity.ino)) {
|
|
1202
|
+
throw new LeaseUnavailableError(scope, 'tasks', 'required schema identity is missing or its database file was replaced');
|
|
1203
|
+
}
|
|
1204
|
+
const parent = requiredColdOpenScope.getStore();
|
|
1205
|
+
for (let frame = parent; frame; frame = frame.parent) {
|
|
1206
|
+
if (!frame.active)
|
|
1207
|
+
throw new LeaseUnavailableError(scope, 'tasks', 'expired lexical schema ownership');
|
|
1208
|
+
if (frame.path !== path)
|
|
1209
|
+
continue;
|
|
1210
|
+
assertColdOpenFile(frame);
|
|
1211
|
+
if (frame.scope !== scope)
|
|
1212
|
+
throw new LeaseUnavailableError(scope, 'tasks', 'nested schema scope mismatch');
|
|
1213
|
+
if (frame.execution && opts.execution && frame.execution !== opts.execution) {
|
|
1214
|
+
throw new LeaseUnavailableError(scope, 'tasks', 'nested schema ownership cannot replace execution authority');
|
|
1215
|
+
}
|
|
1216
|
+
frame.execution?.assertActive();
|
|
1217
|
+
frame.guard.assertHeld(nativeDb);
|
|
1218
|
+
const result = await fn(frame.guard);
|
|
1219
|
+
assertColdOpenFile(frame);
|
|
1220
|
+
frame.guard.assertHeld();
|
|
1221
|
+
return result;
|
|
1222
|
+
}
|
|
1223
|
+
const remaining = opts.execution
|
|
1224
|
+
? Math.max(1, opts.execution.deadlineAt - Date.now())
|
|
1225
|
+
: ACQUIRE_DEADLINE_MS;
|
|
1226
|
+
const owned = openNativeDatabase(path, { timeout: Math.min(ACQUIRE_DEADLINE_MS, remaining) });
|
|
1227
|
+
try {
|
|
1228
|
+
opts.execution?.assertActive();
|
|
1229
|
+
const afterOpen = statSync(path, { bigint: true });
|
|
1230
|
+
if (afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
|
|
1231
|
+
throw new LeaseUnavailableError(scope, 'tasks', 'database file changed during lease open');
|
|
1232
|
+
}
|
|
1233
|
+
return await runColdOpenLease(scope, owned, async (grant) => {
|
|
1234
|
+
const frame = {
|
|
1235
|
+
parent,
|
|
1236
|
+
scope,
|
|
1237
|
+
path,
|
|
1238
|
+
device: identity.dev,
|
|
1239
|
+
inode: identity.ino,
|
|
1240
|
+
get guard() {
|
|
1241
|
+
return guard;
|
|
1242
|
+
},
|
|
1243
|
+
execution: opts.execution,
|
|
1244
|
+
active: true,
|
|
1245
|
+
};
|
|
1246
|
+
const guard = {
|
|
1247
|
+
assertHeld(target) {
|
|
1248
|
+
assertColdOpenFile(frame);
|
|
1249
|
+
grant.assertHeld(target);
|
|
1250
|
+
},
|
|
1251
|
+
mutate(target, mutation) {
|
|
1252
|
+
assertColdOpenFile(frame);
|
|
1253
|
+
opts.execution?.assertActive();
|
|
1254
|
+
if (target !== nativeDb) {
|
|
1255
|
+
throw new LeaseUnavailableError(scope, 'tasks', 'schema mutation requires its captured native handle');
|
|
1256
|
+
}
|
|
1257
|
+
if (target.isTransaction) {
|
|
1258
|
+
throw new LeaseUnavailableError(scope, 'tasks', 'schema mutation cannot borrow an active transaction');
|
|
1259
|
+
}
|
|
1260
|
+
const previousTimeout = Number(target.prepare('PRAGMA busy_timeout').get()?.timeout ?? 0);
|
|
1261
|
+
if (opts.execution) {
|
|
1262
|
+
target.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.min(previousTimeout, opts.execution.deadlineAt - Date.now()))}`);
|
|
1263
|
+
}
|
|
1264
|
+
try {
|
|
1265
|
+
target.exec('BEGIN IMMEDIATE');
|
|
1266
|
+
// Read on the actual writer after its lock, so a competing epoch change
|
|
1267
|
+
// cannot occur between ownership validation and these committed writes.
|
|
1268
|
+
const assertMutationOwnership = () => {
|
|
1269
|
+
guard.assertHeld(target);
|
|
1270
|
+
const lease = target
|
|
1271
|
+
.prepare(`SELECT heartbeat_at, ttl_ms FROM main.${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = 'tasks' AND active = 1`)
|
|
1272
|
+
.get(scope);
|
|
1273
|
+
if (!lease || Number(lease.heartbeat_at) + Number(lease.ttl_ms) <= Date.now()) {
|
|
1274
|
+
throw new LeaseUnavailableError(scope, 'tasks', 'schema mutation ownership expired');
|
|
1275
|
+
}
|
|
1276
|
+
opts.execution?.assertActive();
|
|
1277
|
+
};
|
|
1278
|
+
assertMutationOwnership();
|
|
1279
|
+
const result = mutation();
|
|
1280
|
+
if (result !== null &&
|
|
1281
|
+
(typeof result === 'object' || typeof result === 'function') &&
|
|
1282
|
+
'then' in result) {
|
|
1283
|
+
throw new TypeError('schema mutation callback must be synchronous');
|
|
1284
|
+
}
|
|
1285
|
+
assertMutationOwnership();
|
|
1286
|
+
target.exec('COMMIT');
|
|
1287
|
+
return result;
|
|
1288
|
+
}
|
|
1289
|
+
catch (error) {
|
|
1290
|
+
if (target.isTransaction)
|
|
1291
|
+
target.exec('ROLLBACK');
|
|
1292
|
+
throw error;
|
|
1293
|
+
}
|
|
1294
|
+
finally {
|
|
1295
|
+
if (target.isOpen && opts.execution)
|
|
1296
|
+
target.exec(`PRAGMA busy_timeout = ${previousTimeout}`);
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
};
|
|
1300
|
+
// The random holder and epoch must be visible through the actual callback
|
|
1301
|
+
// handle. Matching path strings alone cannot establish file identity.
|
|
1302
|
+
guard.assertHeld(nativeDb);
|
|
1303
|
+
try {
|
|
1304
|
+
const result = await requiredColdOpenScope.run(frame, () => fn(guard));
|
|
1305
|
+
guard.assertHeld();
|
|
1306
|
+
return result;
|
|
1307
|
+
}
|
|
1308
|
+
finally {
|
|
1309
|
+
frame.active = false;
|
|
1310
|
+
}
|
|
1311
|
+
}, opts, true);
|
|
1312
|
+
}
|
|
1313
|
+
finally {
|
|
1314
|
+
owned.close();
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
988
1317
|
/**
|
|
989
1318
|
* Lease the dual-scope-db COLD-OPEN critical section (Seam 0 — the T5158 heal).
|
|
990
1319
|
*
|
|
@@ -1012,6 +1341,17 @@ function ensureColdOpenLeaseTables(nativeDb) {
|
|
|
1012
1341
|
* degrade to running `fn` (busy_timeout still serializes the migrate write-txn).
|
|
1013
1342
|
* - `require` → throw {@link LeaseUnavailableError} if the row cannot be taken.
|
|
1014
1343
|
*
|
|
1344
|
+
* @typeParam T - Actual result of the admitted migration callback.
|
|
1345
|
+
* @remarks Scoped callbacks receive one original deadline and must cooperate at
|
|
1346
|
+
* their own asynchronous boundaries. Required ownership pins the captured file generation,
|
|
1347
|
+
* uses an independently owned handle, and refuses off/degraded execution. Explicit
|
|
1348
|
+
* checkpoints and publication checks do not fence arbitrary callback writes.
|
|
1349
|
+
* A committed callback result survives later
|
|
1350
|
+
* cancellation; cancellation never grants degraded migration permission.
|
|
1351
|
+
* @example
|
|
1352
|
+
* ```ts
|
|
1353
|
+
* const result = await withColdOpenLease('project', native, migrate, options);
|
|
1354
|
+
* ```
|
|
1015
1355
|
* @param scope - The cleo.db scope being cold-opened.
|
|
1016
1356
|
* @param nativeDb - The native handle the cold-open just created (pragmas applied).
|
|
1017
1357
|
* @param fn - The cold-open body to run while holding the lease.
|
|
@@ -1024,11 +1364,25 @@ function ensureColdOpenLeaseTables(nativeDb) {
|
|
|
1024
1364
|
* @task T12042 (E6-L12b — no process-global side effect)
|
|
1025
1365
|
*/
|
|
1026
1366
|
export async function withColdOpenLease(scope, nativeDb, fn, opts) {
|
|
1027
|
-
|
|
1367
|
+
return opts?.requiredIdentity
|
|
1368
|
+
? withRequiredColdOpenLease(scope, nativeDb, fn, opts)
|
|
1369
|
+
: runColdOpenLease(scope, nativeDb, fn, opts);
|
|
1370
|
+
}
|
|
1371
|
+
/** Shared row acquisition; required callers cannot take the compatibility fallback. */
|
|
1372
|
+
async function runColdOpenLease(scope, nativeDb, fn, opts, required = false) {
|
|
1373
|
+
opts?.execution?.assertActive();
|
|
1374
|
+
const mode = required ? 'require' : effectiveMode();
|
|
1375
|
+
const rejectMutation = () => {
|
|
1376
|
+
throw new LeaseUnavailableError(scope, 'tasks', 'schema mutation requires operation-owned authority');
|
|
1377
|
+
};
|
|
1378
|
+
const unleased = {
|
|
1379
|
+
assertHeld: () => opts?.execution?.assertActive(),
|
|
1380
|
+
mutate: rejectMutation,
|
|
1381
|
+
};
|
|
1028
1382
|
// `off` mode — pure pass-through. busy_timeout=30000 on the connection still
|
|
1029
1383
|
// serializes the migrate/reconcile write-txn exactly as before the lease.
|
|
1030
1384
|
if (mode === 'off') {
|
|
1031
|
-
return fn();
|
|
1385
|
+
return fn(unleased);
|
|
1032
1386
|
}
|
|
1033
1387
|
// The lease tables MUST exist before the claim txn — the migration that creates
|
|
1034
1388
|
// them runs inside `fn`, so bootstrap them idempotently here first.
|
|
@@ -1042,43 +1396,58 @@ export async function withColdOpenLease(scope, nativeDb, fn, opts) {
|
|
|
1042
1396
|
// Bounded acquire window — never wait longer than the lease TTL. busy_timeout on
|
|
1043
1397
|
// the IMMEDIATE lock backstops every individual claim attempt.
|
|
1044
1398
|
const acquireWindowMs = Math.min(ACQUIRE_DEADLINE_MS, Math.max(1, ttlMs));
|
|
1045
|
-
const deadline = Date.now() + acquireWindowMs;
|
|
1399
|
+
const deadline = Math.min(Date.now() + acquireWindowMs, opts?.execution?.deadlineAt ?? Infinity);
|
|
1046
1400
|
let enqueued = false;
|
|
1047
1401
|
let epoch = null;
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
enqueued
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
if (
|
|
1063
|
-
|
|
1402
|
+
try {
|
|
1403
|
+
for (;;) {
|
|
1404
|
+
opts?.execution?.assertActive();
|
|
1405
|
+
epoch = tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs, opts?.execution);
|
|
1406
|
+
if (epoch !== null) {
|
|
1407
|
+
if (enqueued)
|
|
1408
|
+
dequeueWaiter(nativeDb, scope, lane, holderId, opts?.execution !== undefined);
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
if (!enqueued) {
|
|
1412
|
+
opts?.execution?.assertActive();
|
|
1413
|
+
enqueueWaiter(nativeDb, scope, lane, holderId, priority, ttlMs);
|
|
1414
|
+
enqueued = true;
|
|
1415
|
+
}
|
|
1416
|
+
if (Date.now() >= deadline) {
|
|
1417
|
+
if (enqueued)
|
|
1418
|
+
dequeueWaiter(nativeDb, scope, lane, holderId, opts?.execution !== undefined);
|
|
1419
|
+
opts?.execution?.assertActive();
|
|
1420
|
+
if (mode === 'require' || opts?.execution) {
|
|
1421
|
+
throw new LeaseUnavailableError(scope, lane, 'cold-open: live holder did not release within deadline');
|
|
1422
|
+
}
|
|
1423
|
+
// local/supervisor: degrade to today's behaviour — run the cold-open under
|
|
1424
|
+
// busy_timeout, which still serializes the migrate write-txn.
|
|
1425
|
+
log().warn({ scope }, 'cold-open writer-lease acquire deadline exceeded; proceeding under ' +
|
|
1426
|
+
'busy_timeout fallback (degraded)');
|
|
1427
|
+
return fn(unleased);
|
|
1064
1428
|
}
|
|
1065
|
-
|
|
1066
|
-
// busy_timeout, which still serializes the migrate write-txn.
|
|
1067
|
-
log().warn({ scope }, 'cold-open writer-lease acquire deadline exceeded; proceeding under ' +
|
|
1068
|
-
'busy_timeout fallback (degraded)');
|
|
1069
|
-
return fn();
|
|
1429
|
+
await sleepAsync(Math.min(CLAIM_RETRY_DELAY_MS, Math.max(0, deadline - Date.now())));
|
|
1070
1430
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1431
|
+
// Retain an actual committed callback result even if cancellation arrives afterwards.
|
|
1432
|
+
opts?.execution?.assertActive();
|
|
1433
|
+
return await fn({
|
|
1434
|
+
mutate: rejectMutation,
|
|
1435
|
+
assertHeld(target = nativeDb) {
|
|
1436
|
+
const row = target
|
|
1437
|
+
.prepare(`SELECT holder_id, epoch FROM main.${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ? AND active = 1`)
|
|
1438
|
+
.get(scope, lane);
|
|
1439
|
+
if (!row || row.holder_id !== holderId || row.epoch !== epoch) {
|
|
1440
|
+
throw new LeaseUnavailableError(scope, lane, 'required schema ownership is stale or belongs to another database');
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1076
1444
|
}
|
|
1077
1445
|
finally {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1446
|
+
if (enqueued)
|
|
1447
|
+
dequeueWaiter(nativeDb, scope, lane, holderId, opts?.execution !== undefined);
|
|
1448
|
+
if (epoch !== null) {
|
|
1449
|
+
releaseColdOpenClaim(nativeDb, scope, lane, holderId, epoch, opts?.execution !== undefined);
|
|
1450
|
+
}
|
|
1082
1451
|
}
|
|
1083
1452
|
}
|
|
1084
1453
|
//# sourceMappingURL=writer-lease.js.map
|