@retaw106/mmc-oc 2.2.1 → 2.4.0
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/cli.js +14 -3
- package/dist/config/deprecated-config.d.ts +4 -0
- package/dist/config/deprecated-config.d.ts.map +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +2 -2
- package/dist/config/schema/magic-context.d.ts +36 -3
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config-init.js +14 -3
- package/dist/{context-mrvm92n8.js → context-c72brxe5.js} +2410 -1801
- package/dist/{derive-budgets-rjjz5f7y.js → derive-budgets-s1p37t2g.js} +2 -2
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/historian-failure-reason.d.ts +7 -0
- package/dist/features/magic-context/historian-failure-reason.d.ts.map +1 -0
- package/dist/features/magic-context/memory-maintainer/launcher.d.ts +13 -4
- package/dist/features/magic-context/memory-maintainer/launcher.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/manual-commands.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/model-chain.d.ts +47 -0
- package/dist/features/magic-context/memory-maintainer/model-chain.d.ts.map +1 -0
- package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts +17 -4
- package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/pipeline.d.ts +6 -0
- package/dist/features/magic-context/memory-maintainer/pipeline.d.ts.map +1 -1
- package/dist/features/magic-context/message-index-async.d.ts +21 -3
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +7 -1
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/pi-content-decisions.d.ts +7 -1
- package/dist/features/magic-context/pi-content-decisions.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +29 -0
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/storage-clone.d.ts +2 -0
- package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
- package/dist/features/magic-context/storage-compartment-history-version.d.ts +4 -0
- package/dist/features/magic-context/storage-compartment-history-version.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +23 -5
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts +7 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts +5 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tag-identity-revision.d.ts +50 -0
- package/dist/features/magic-context/storage-tag-identity-revision.d.ts.map +1 -0
- package/dist/features/magic-context/storage-tags.d.ts +159 -5
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/tagger.d.ts +2 -0
- package/dist/features/magic-context/tagger.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +60 -2
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts +34 -0
- package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts.map +1 -0
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts +2 -0
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts +10 -1
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts +2 -2
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +2 -2
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts +4 -0
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger-error.d.ts +11 -0
- package/dist/hooks/magic-context/compartment-trigger-error.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +1 -0
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/degraded-pass-refusal.d.ts +2 -0
- package/dist/hooks/magic-context/degraded-pass-refusal.d.ts.map +1 -1
- package/dist/hooks/magic-context/edit-marker.d.ts +2 -0
- package/dist/hooks/magic-context/edit-marker.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/history-boundary-repair.d.ts +11 -2
- package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-measured-request.d.ts +3 -1
- package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-persist.d.ts +10 -13
- package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +62 -0
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +38 -0
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-replay.d.ts +16 -9
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +141 -2
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/opaque-thinking.d.ts +12 -0
- package/dist/hooks/magic-context/opaque-thinking.d.ts.map +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
- package/dist/hooks/magic-context/pending-drop-eligibility.d.ts +14 -0
- package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -0
- package/dist/hooks/magic-context/pending-materialization.d.ts +10 -0
- package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -0
- package/dist/hooks/magic-context/postprocess-read-cache.d.ts.map +1 -1
- package/dist/hooks/magic-context/provider-visible-parts.d.ts +51 -0
- package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts +22 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +3 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-formatting.d.ts +5 -0
- package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +123 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -1
- package/dist/hooks/magic-context/render-options.d.ts +19 -0
- package/dist/hooks/magic-context/render-options.d.ts.map +1 -0
- package/dist/hooks/magic-context/sentinel.d.ts +41 -1
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/strict-thinking-prefix.d.ts +8 -0
- package/dist/hooks/magic-context/strict-thinking-prefix.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts +38 -24
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/supersession-reclaim.d.ts +5 -3
- package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-date-refresh.d.ts +29 -0
- package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-dates.d.ts +6 -0
- package/dist/hooks/magic-context/system-prompt-dates.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-delivery.d.ts +47 -0
- package/dist/hooks/magic-context/system-prompt-delivery.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +18 -5
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +19 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-owner-warmup.d.ts +14 -0
- package/dist/hooks/magic-context/tag-owner-warmup.d.ts.map +1 -0
- package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts +18 -0
- package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts.map +1 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +8 -4
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/token-count-exact.d.ts +35 -0
- package/dist/hooks/magic-context/token-count-exact.d.ts.map +1 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts +2 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-reclaim.d.ts +5 -2
- package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +9 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +21 -11
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/unresolved-history-boundary.d.ts +5 -1
- package/dist/hooks/magic-context/unresolved-history-boundary.d.ts.map +1 -1
- package/dist/{index-g2z2k5m4.js → index-27ddyx39.js} +2 -2
- package/dist/{index-btp5tdc8.js → index-48e9znrc.js} +1248 -570
- package/dist/{index-5cep2r9n.js → index-5ty9rnbx.js} +60 -258
- package/dist/{index-z2g20msd.js → index-azbmnekv.js} +4 -4
- package/dist/{index-1j34a56j.js → index-dwh99z66.js} +293 -36
- package/dist/{index-qbkzdkk0.js → index-k59af64t.js} +6 -2
- package/dist/{index-5b7x80ec.js → index-kahkxcqw.js} +12532 -10348
- package/dist/{index-53g38ggt.js → index-kmbme1v1.js} +1 -1
- package/dist/{index-amycb7b1.js → index-r7s7d350.js} +3 -3
- package/dist/{index-33gprt9g.js → index-yjv8dpht.js} +1 -1
- package/dist/index-ykq24x1n.js +1477 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -2
- package/dist/{message-time-backfill-phrwp2fz.js → message-time-backfill-j96x1sab.js} +3 -3
- package/dist/model-resolution-cvyrrcjw.js +8 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-2xm4sc4y.js → read-session-chunk-t8bg6wkz.js} +6 -4
- package/dist/recomp-orchestrator-k760qy1n.js +21 -0
- package/dist/{safe-notification-target-ph3xd1ve.js → safe-notification-target-1jkf0m82.js} +2 -2
- package/dist/{send-session-notification-rnc4nrsr.js → send-session-notification-njjf3b4s.js} +5 -5
- package/dist/shared/auto-search-lifecycle.d.ts +14 -0
- package/dist/shared/auto-search-lifecycle.d.ts.map +1 -1
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/model-suggestion-retry.d.ts +21 -0
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/models-dev-temperature.d.ts +18 -0
- package/dist/shared/models-dev-temperature.d.ts.map +1 -0
- package/dist/shared/processed-image-eligibility.d.ts +22 -0
- package/dist/shared/processed-image-eligibility.d.ts.map +1 -0
- package/dist/shared/project-directory-key.d.ts +16 -0
- package/dist/shared/project-directory-key.d.ts.map +1 -1
- package/dist/shared/prompt-surface-runtime.d.ts +5 -0
- package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
- package/dist/shared/reclaim-distance.d.ts +33 -0
- package/dist/shared/reclaim-distance.d.ts.map +1 -0
- package/dist/shared/recovery-reasoning-prefix.d.ts +9 -0
- package/dist/shared/recovery-reasoning-prefix.d.ts.map +1 -0
- package/dist/shared/sqlite.d.ts +35 -1
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/stable-json.d.ts +7 -0
- package/dist/shared/stable-json.d.ts.map +1 -1
- package/dist/shared/storage-file-mutex.d.ts +9 -0
- package/dist/shared/storage-file-mutex.d.ts.map +1 -0
- package/dist/shared/storage-permissions.d.ts +6 -0
- package/dist/shared/storage-permissions.d.ts.map +1 -1
- package/dist/shared/subagent-temperature.d.ts +46 -0
- package/dist/shared/subagent-temperature.d.ts.map +1 -0
- package/dist/shared/tag-owner-id.d.ts +1 -0
- package/dist/shared/tag-owner-id.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts +9 -0
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/shared/tail-hygiene-json.d.ts +22 -0
- package/dist/shared/tail-hygiene-json.d.ts.map +1 -0
- package/dist/shared/user-facing-codes.d.ts +2 -2
- package/dist/shared/window-geometry.d.ts +9 -66
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/standalone-plugin-marker.d.ts +8 -0
- package/dist/standalone-plugin-marker.d.ts.map +1 -0
- package/dist/tools/ctx-expand/search-archive.d.ts +4 -0
- package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -0
- package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/constants.d.ts +1 -2
- package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts +20 -1
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/copied-tag-notation.d.ts +19 -0
- package/dist/v2/hooks/copied-tag-notation.d.ts.map +1 -0
- package/dist/v2/hooks/generate.d.ts +9 -2
- package/dist/v2/hooks/generate.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +22 -1
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/system-prompt-response.d.ts +5 -0
- package/dist/v2/hooks/system-prompt-response.d.ts.map +1 -0
- package/dist/v2/server.d.ts.map +1 -1
- package/dist/v2/server.js +26593 -21940
- package/dist/v2/store-reader.d.ts +27 -1
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/web/main.js +401 -23
- package/dist/{wrapup-orchestrator-yknks3ad.js → wrapup-orchestrator-b2gswatj.js} +10 -10
- package/package.json +1 -1
- package/src/config/deprecated-config.ts +18 -0
- package/src/config/index.test.ts +13 -37
- package/src/config/index.ts +2 -2
- package/src/config/latch-permanence-guard.test.ts +4 -0
- package/src/config/project-security.test.ts +17 -0
- package/src/config/project-security.ts +3 -3
- package/src/config/schema/magic-context.test.ts +50 -0
- package/src/config/schema/magic-context.ts +44 -7
- package/src/shared/auto-search-lifecycle.ts +43 -0
- package/src/shared/logger.ts +10 -2
- package/src/shared/model-suggestion-retry.ts +5 -1
- package/src/shared/models-dev-cache.ts +1 -7
- package/src/shared/models-dev-temperature.ts +107 -0
- package/src/shared/processed-image-eligibility.test.ts +68 -0
- package/src/shared/processed-image-eligibility.ts +41 -0
- package/src/shared/processed-image-position-review.json +20 -0
- package/src/shared/processed-image-position-review.test.ts +19 -0
- package/src/shared/project-directory-key.ts +20 -0
- package/src/shared/prompt-surface-runtime.ts +12 -0
- package/src/shared/reclaim-distance.test.ts +111 -0
- package/src/shared/reclaim-distance.ts +91 -0
- package/src/shared/recovery-reasoning-prefix.test.ts +34 -0
- package/src/shared/recovery-reasoning-prefix.ts +35 -0
- package/src/shared/root-async-context.probe.mjs +36 -0
- package/src/shared/root-async-context.test.ts +49 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +4 -3
- package/src/shared/sqlite-busy-budget-review.test.ts +291 -0
- package/src/shared/sqlite-foreground-wait.test.ts +304 -0
- package/src/shared/sqlite-pass-scope.test.ts +4 -4
- package/src/shared/sqlite-writer-diagnostics.test.ts +52 -12
- package/src/shared/sqlite.ts +428 -46
- package/src/shared/stable-json.ts +79 -7
- package/src/shared/storage-file-mutex.test.ts +50 -0
- package/src/shared/storage-file-mutex.ts +130 -0
- package/src/shared/storage-permissions.ts +46 -0
- package/src/shared/subagent-temperature.ts +110 -0
- package/src/shared/tag-owner-id.ts +13 -0
- package/src/shared/tag-transcript.ts +118 -0
- package/src/shared/tail-hygiene-json.test.ts +117 -0
- package/src/shared/tail-hygiene-json.ts +217 -0
- package/src/shared/user-facing-codes.ts +2 -2
- package/src/shared/window-geometry.test.ts +102 -236
- package/src/shared/window-geometry.ts +23 -413
- package/dist/index-ramz5kkm.js +0 -514
- package/dist/model-resolution-wazv26n0.js +0 -6
- package/dist/recomp-orchestrator-frmwj9hz.js +0 -21
- package/src/shared/__fixtures__/window-overlay.v1.json +0 -337
package/src/shared/sqlite.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* `run()` → {changes,lastInsertRowid}) is identical and was verified directly.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
42
|
+
import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
|
|
43
43
|
import { statSync } from "node:fs";
|
|
44
44
|
// Type import only — runtime is loaded dynamically below. @types/better-sqlite3
|
|
45
45
|
// has the richest definitions and is a structural superset of the API surface
|
|
@@ -47,6 +47,16 @@ import { statSync } from "node:fs";
|
|
|
47
47
|
// node:sqlite at runtime (both expose prepare/run/get/all/exec/close).
|
|
48
48
|
import type BetterSqlite3 from "better-sqlite3";
|
|
49
49
|
|
|
50
|
+
// Capture the root before creating request-local AsyncLocalStorage scopes.
|
|
51
|
+
// Shared workers and timers must not inherit callbacks that reject stale
|
|
52
|
+
// database writes: those callbacks can retain an obsolete request's inputs.
|
|
53
|
+
const resourceRoot = new AsyncResource("magic-context:root");
|
|
54
|
+
|
|
55
|
+
/** Create host-owned resources without inheriting request-local state. */
|
|
56
|
+
export function inRootAsyncContext<T>(operation: () => T): T {
|
|
57
|
+
return resourceRoot.runInAsyncScope(operation);
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
/**
|
|
51
61
|
* Slow-write reporting is injected rather than imported: this module is also
|
|
52
62
|
* executed directly by Node (the `node:sqlite` smoke in CI, Pi, Desktop), and
|
|
@@ -71,14 +81,21 @@ export function registerSlowWriteReporter(reporter: SlowWriteReporter): void {
|
|
|
71
81
|
*/
|
|
72
82
|
type SqliteDiagnosticSink = (message: string) => void;
|
|
73
83
|
let sqliteDiagnosticSink: SqliteDiagnosticSink | undefined;
|
|
84
|
+
let sqliteDiagnosticHost: () => string = () => "unknown";
|
|
74
85
|
|
|
75
|
-
export function registerSqliteDiagnosticSink(
|
|
86
|
+
export function registerSqliteDiagnosticSink(
|
|
87
|
+
sink: SqliteDiagnosticSink,
|
|
88
|
+
host?: () => string,
|
|
89
|
+
): void {
|
|
76
90
|
sqliteDiagnosticSink = sink;
|
|
91
|
+
if (host) sqliteDiagnosticHost = host;
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
function reportSqliteDiagnostic(message: string): void {
|
|
80
95
|
try {
|
|
81
|
-
sqliteDiagnosticSink?.(
|
|
96
|
+
sqliteDiagnosticSink?.(
|
|
97
|
+
`[magic-context] ${message} pid=${process.pid} host=${sqliteDiagnosticHost()}`,
|
|
98
|
+
);
|
|
82
99
|
} catch {
|
|
83
100
|
// Diagnostics must never change the outcome of the write they describe.
|
|
84
101
|
}
|
|
@@ -169,13 +186,51 @@ export class SqliteRuntimeUnavailableError extends Error {
|
|
|
169
186
|
}
|
|
170
187
|
}
|
|
171
188
|
|
|
189
|
+
// Node requests this warning during node:sqlite's module evaluation, although
|
|
190
|
+
// warning delivery happens later. Intercept only the request while importing;
|
|
191
|
+
// replacing warning listeners would not suppress the default stderr output.
|
|
192
|
+
function isNodeSqliteExperimentalWarning(args: unknown[]): boolean {
|
|
193
|
+
const [warning, options] = args;
|
|
194
|
+
const message = warning instanceof Error ? warning.message : warning;
|
|
195
|
+
const type =
|
|
196
|
+
typeof options === "string"
|
|
197
|
+
? options
|
|
198
|
+
: options && typeof options === "object"
|
|
199
|
+
? (options as { type?: unknown }).type
|
|
200
|
+
: undefined;
|
|
201
|
+
return (
|
|
202
|
+
type === "ExperimentalWarning" &&
|
|
203
|
+
typeof message === "string" &&
|
|
204
|
+
message.startsWith("SQLite is an experimental feature")
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function importSqliteModuleForRuntime(
|
|
209
|
+
runtime: SqliteRuntime,
|
|
210
|
+
specifier: string,
|
|
211
|
+
importer: (specifier: string) => Promise<SqliteModule>,
|
|
212
|
+
): Promise<SqliteModule> {
|
|
213
|
+
if (runtime !== "Node.js") return importer(specifier);
|
|
214
|
+
|
|
215
|
+
const originalEmitWarning = process.emitWarning;
|
|
216
|
+
process.emitWarning = function (this: unknown, ...args: unknown[]): void {
|
|
217
|
+
if (isNodeSqliteExperimentalWarning(args)) return;
|
|
218
|
+
Reflect.apply(originalEmitWarning, this, args);
|
|
219
|
+
} as typeof process.emitWarning;
|
|
220
|
+
try {
|
|
221
|
+
return await importer(specifier);
|
|
222
|
+
} finally {
|
|
223
|
+
process.emitWarning = originalEmitWarning;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
172
227
|
export async function loadSqliteModule(
|
|
173
228
|
runtime: SqliteRuntime = detectSqliteRuntime(),
|
|
174
229
|
importer: (specifier: string) => Promise<SqliteModule> = importSqliteModule,
|
|
175
230
|
): Promise<SqliteModule> {
|
|
176
231
|
const specifier = runtime === "Bun" ? bunSpec : nodeSpec;
|
|
177
232
|
try {
|
|
178
|
-
return await
|
|
233
|
+
return await importSqliteModuleForRuntime(runtime, specifier, importer);
|
|
179
234
|
} catch (error) {
|
|
180
235
|
if (isModuleNotFoundError(error, specifier)) {
|
|
181
236
|
throw new SqliteRuntimeUnavailableError(runtime, specifier, error);
|
|
@@ -234,6 +289,110 @@ export interface SqliteMemoryStats {
|
|
|
234
289
|
const trackedSqliteConnections = new Map<number, TrackedSqliteConnection>();
|
|
235
290
|
let nextSqliteConnectionSequence = 1;
|
|
236
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Columns of Magic Context's `tags` table (context.db) that never decide which
|
|
294
|
+
* message a tag belongs to, its number, its type, or its reasoning token estimate.
|
|
295
|
+
*/
|
|
296
|
+
const TAG_COLUMNS_OUTSIDE_IDENTITY = new Set([
|
|
297
|
+
"status",
|
|
298
|
+
"drop_mode",
|
|
299
|
+
"byte_size",
|
|
300
|
+
"input_byte_size",
|
|
301
|
+
"reasoning_byte_size",
|
|
302
|
+
"token_count",
|
|
303
|
+
"input_token_count",
|
|
304
|
+
"caveman_depth",
|
|
305
|
+
"entry_fingerprint",
|
|
306
|
+
"tool_name",
|
|
307
|
+
"harness",
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
let tagIdentityWriteGeneration = 0;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Whether a write statement may change an existing `tags` row's identity
|
|
314
|
+
* (session, number, type, owner message, reasoning token count) or remove a
|
|
315
|
+
* row. Plain appends without an explicit `id`, and updates that assign only
|
|
316
|
+
* columns in {@link TAG_COLUMNS_OUTSIDE_IDENTITY}, cannot; everything else that
|
|
317
|
+
* names the table is treated as if it could, including DDL and multi-statement
|
|
318
|
+
* scripts. Erring towards true only costs a cache rebuild.
|
|
319
|
+
*/
|
|
320
|
+
export function mayChangeTagIdentity(sql: string): boolean {
|
|
321
|
+
if (!/\btags\b/i.test(sql) || /^\s*(?:SELECT|EXPLAIN|PRAGMA)\b/i.test(sql)) return false;
|
|
322
|
+
if (/^\s*WITH\b/i.test(sql) && !/\b(?:INSERT|UPDATE|DELETE|REPLACE)\b/i.test(sql)) return false;
|
|
323
|
+
// One statement only: a script can hide a DELETE behind a harmless first statement.
|
|
324
|
+
if (/;\s*\S/.test(sql) || /\bREPLACE\b/i.test(sql) || /\bON\s+CONFLICT\b/i.test(sql))
|
|
325
|
+
return true;
|
|
326
|
+
const insert = /^\s*INSERT\s+INTO\s+(?:main\.)?["`]?tags["`]?\s*\(([^)]*)\)/i.exec(sql);
|
|
327
|
+
if (insert) {
|
|
328
|
+
const columns = (insert[1] ?? "").split(",").map((column) =>
|
|
329
|
+
column
|
|
330
|
+
.trim()
|
|
331
|
+
.replace(/["`[\]]/g, "")
|
|
332
|
+
.toLowerCase(),
|
|
333
|
+
);
|
|
334
|
+
return columns.includes("id") || columns.includes("rowid");
|
|
335
|
+
}
|
|
336
|
+
const update = /^\s*UPDATE\s+(?:main\.)?["`]?tags["`]?\s+SET\s+([\s\S]*?)\bWHERE\b/i.exec(sql);
|
|
337
|
+
if (update && !/\bSELECT\b/i.test(sql)) {
|
|
338
|
+
// Provably identity-free only when every top-level assignment is a plain
|
|
339
|
+
// `column = ...` naming an allowed column. A tuple assignment such as
|
|
340
|
+
// `(tag_number, reasoning_token_count) = (...)`, or anything this cannot
|
|
341
|
+
// read, counts as an identity write.
|
|
342
|
+
const assignments = topLevelCommaSegments(update[1] ?? "");
|
|
343
|
+
if (!assignments) return true;
|
|
344
|
+
return !assignments.every((assignment) => {
|
|
345
|
+
const column = /^\s*["`]?(\w+)["`]?\s*=(?!=)/.exec(assignment)?.[1];
|
|
346
|
+
return column !== undefined && TAG_COLUMNS_OUTSIDE_IDENTITY.has(column.toLowerCase());
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Split SQL text at commas outside parentheses and quotes. Returns null when
|
|
354
|
+
* the parentheses or quotes do not balance.
|
|
355
|
+
*/
|
|
356
|
+
function topLevelCommaSegments(text: string): string[] | null {
|
|
357
|
+
const segments: string[] = [];
|
|
358
|
+
let depth = 0;
|
|
359
|
+
let quote: string | null = null;
|
|
360
|
+
let start = 0;
|
|
361
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
362
|
+
const char = text[index];
|
|
363
|
+
if (quote) {
|
|
364
|
+
// A doubled quote inside a quoted run is an escaped quote.
|
|
365
|
+
if (char === quote) {
|
|
366
|
+
if (text[index + 1] === quote) index += 1;
|
|
367
|
+
else quote = null;
|
|
368
|
+
}
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (char === "'" || char === '"' || char === "`") quote = char;
|
|
372
|
+
else if (char === "(") depth += 1;
|
|
373
|
+
else if (char === ")") {
|
|
374
|
+
depth -= 1;
|
|
375
|
+
if (depth < 0) return null;
|
|
376
|
+
} else if (char === "," && depth === 0) {
|
|
377
|
+
segments.push(text.slice(start, index));
|
|
378
|
+
start = index + 1;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (quote || depth !== 0) return null;
|
|
382
|
+
segments.push(text.slice(start));
|
|
383
|
+
return segments;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Counts executions, on any connection this process opened, of statements for
|
|
388
|
+
* which {@link mayChangeTagIdentity} is true. A cache built from `tags` stays
|
|
389
|
+
* valid for this process's own writes while the count is unchanged; commits by
|
|
390
|
+
* other connections show up in `PRAGMA data_version` instead.
|
|
391
|
+
*/
|
|
392
|
+
export function getTagIdentityWriteGeneration(): number {
|
|
393
|
+
return tagIdentityWriteGeneration;
|
|
394
|
+
}
|
|
395
|
+
|
|
237
396
|
/** Route native Bun and Node transaction entry through the same acquisition retry.
|
|
238
397
|
* Native Bun transaction wrappers execute BEGIN internally, bypassing an exec override,
|
|
239
398
|
* so both runtimes use this small synchronous wrapper instead. Writable handles
|
|
@@ -250,15 +409,33 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
|
|
|
250
409
|
// A transaction that ended without passing through exec (for
|
|
251
410
|
// example an automatic rollback) must not be timed as this one.
|
|
252
411
|
openWriterTransactions.delete(db);
|
|
253
|
-
if (transformPassScope.getStore()?.active || backgroundWriterScope.getStore())
|
|
412
|
+
if (transformPassScope.getStore()?.active || backgroundWriterScope.getStore()) {
|
|
254
413
|
acquireShort(db, () => nativeExec(sql), sql.trim());
|
|
255
|
-
else
|
|
414
|
+
} else {
|
|
415
|
+
nativeExec(sql);
|
|
416
|
+
openWriterTransactions.set(db, {
|
|
417
|
+
site: sql.trim(),
|
|
418
|
+
lane: "background",
|
|
419
|
+
acquireMs: 0,
|
|
420
|
+
attempts: 1,
|
|
421
|
+
acquiredAt: performance.now(),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
256
424
|
return db;
|
|
257
425
|
}
|
|
258
426
|
if (WRITER_TRANSACTION_END.test(sql) && openWriterTransactions.has(db))
|
|
259
427
|
return endWriterTransaction(db, () => nativeExec(sql), sql);
|
|
260
|
-
if (
|
|
261
|
-
|
|
428
|
+
if (
|
|
429
|
+
!/^\s*(?:SELECT|EXPLAIN|PRAGMA|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(sql)
|
|
430
|
+
) {
|
|
431
|
+
try {
|
|
432
|
+
// exec can commit a script prefix before BUSY. Run it once;
|
|
433
|
+
// only prepared statements are safe to probe and retry.
|
|
434
|
+
return withAutocommitTimeout(db, () => nativeExec(sql), false);
|
|
435
|
+
} finally {
|
|
436
|
+
if (mayChangeTagIdentity(sql)) tagIdentityWriteGeneration += 1;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
262
439
|
return nativeExec(sql);
|
|
263
440
|
},
|
|
264
441
|
});
|
|
@@ -271,15 +448,22 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
|
|
|
271
448
|
// SELECT/EXPLAIN cannot acquire a writer. Treat CTEs conservatively:
|
|
272
449
|
// WITH can introduce INSERT/UPDATE/DELETE as well as a read query.
|
|
273
450
|
if (!readonly && !/^\s*(?:SELECT|EXPLAIN|PRAGMA)\b/i.test(sql)) {
|
|
451
|
+
const changesTagIdentity = mayChangeTagIdentity(sql);
|
|
274
452
|
for (const method of ["run", "get", "all"] as const) {
|
|
275
453
|
const execute = statement[method].bind(statement);
|
|
276
454
|
Object.defineProperty(statement, method, {
|
|
277
455
|
configurable: true,
|
|
278
456
|
// biome-ignore lint/suspicious/noExplicitAny: retain SQLite's native binding overloads.
|
|
279
|
-
value: (...args: any[]) =>
|
|
280
|
-
|
|
457
|
+
value: (...args: any[]) => {
|
|
458
|
+
try {
|
|
459
|
+
return withAutocommitTimeout(db, () => execute(...args));
|
|
460
|
+
} finally {
|
|
461
|
+
if (changesTagIdentity) tagIdentityWriteGeneration += 1;
|
|
462
|
+
}
|
|
463
|
+
},
|
|
281
464
|
});
|
|
282
465
|
}
|
|
466
|
+
if (changesTagIdentity) wrapRemainingTagWriteForms(statement);
|
|
283
467
|
}
|
|
284
468
|
return statement;
|
|
285
469
|
},
|
|
@@ -330,6 +514,43 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
|
|
|
330
514
|
});
|
|
331
515
|
}
|
|
332
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Count tag identity writes made through the statement forms the transaction
|
|
519
|
+
* routing does not wrap. `iterate()` runs the statement while its result is
|
|
520
|
+
* consumed, so the generation moves both when iteration starts and when it
|
|
521
|
+
* ends: a summary read in between is then rebuilt on its next read.
|
|
522
|
+
*/
|
|
523
|
+
// biome-ignore lint/suspicious/noExplicitAny: statements from both backends share these method names.
|
|
524
|
+
function wrapRemainingTagWriteForms(statement: any): void {
|
|
525
|
+
if (typeof statement.iterate === "function") {
|
|
526
|
+
const iterate = statement.iterate.bind(statement);
|
|
527
|
+
Object.defineProperty(statement, "iterate", {
|
|
528
|
+
configurable: true,
|
|
529
|
+
value: function* (...args: unknown[]) {
|
|
530
|
+
tagIdentityWriteGeneration += 1;
|
|
531
|
+
try {
|
|
532
|
+
yield* iterate(...args);
|
|
533
|
+
} finally {
|
|
534
|
+
tagIdentityWriteGeneration += 1;
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
if (typeof statement.values === "function") {
|
|
540
|
+
const values = statement.values.bind(statement);
|
|
541
|
+
Object.defineProperty(statement, "values", {
|
|
542
|
+
configurable: true,
|
|
543
|
+
value: (...args: unknown[]) => {
|
|
544
|
+
try {
|
|
545
|
+
return values(...args);
|
|
546
|
+
} finally {
|
|
547
|
+
tagIdentityWriteGeneration += 1;
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
333
554
|
function trackSqliteConnection(
|
|
334
555
|
db: BetterSqlite3.Database,
|
|
335
556
|
filename: unknown,
|
|
@@ -377,7 +598,9 @@ function trackSqliteConnection(
|
|
|
377
598
|
|
|
378
599
|
const TrackedDatabase = new Proxy(DatabaseImpl, {
|
|
379
600
|
construct(target, args) {
|
|
380
|
-
const db =
|
|
601
|
+
const db = inRootAsyncContext(
|
|
602
|
+
() => Reflect.construct(target, args, target) as BetterSqlite3.Database,
|
|
603
|
+
);
|
|
381
604
|
return trackSqliteConnection(db, args[0], args[1]);
|
|
382
605
|
},
|
|
383
606
|
}) as typeof BetterSqlite3;
|
|
@@ -431,6 +654,23 @@ function buildNodeSqliteDatabaseClass(DatabaseSync: any): typeof BetterSqlite3 {
|
|
|
431
654
|
|
|
432
655
|
export const Database: typeof BetterSqlite3 = TrackedDatabase;
|
|
433
656
|
|
|
657
|
+
/** The constructor path, without querying SQLite on the prompt thread. */
|
|
658
|
+
export function getSqliteDatabasePath(db: Database): string | null {
|
|
659
|
+
for (const connection of trackedSqliteConnections.values()) {
|
|
660
|
+
if (connection.reference.deref() === db) {
|
|
661
|
+
return connection.filename === ":memory:" || connection.filename === "<buffer>"
|
|
662
|
+
? null
|
|
663
|
+
: connection.filename;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Native Bun/better-sqlite3 handles can reach readers without our constructor
|
|
667
|
+
// wrapper. Their filename is still available without a SQLite query.
|
|
668
|
+
const native = db as unknown as { filename?: unknown; name?: unknown };
|
|
669
|
+
const path = native.filename ?? native.name;
|
|
670
|
+
if (typeof path === "string" && path !== ":memory:" && path !== "") return path;
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
|
|
434
674
|
function pragmaValue(db: BetterSqlite3.Database, name: string): unknown {
|
|
435
675
|
const row = db.prepare(`PRAGMA ${name}`).get() as Record<string, unknown> | undefined;
|
|
436
676
|
if (!row) return undefined;
|
|
@@ -665,22 +905,31 @@ export function withSqliteBackgroundWriter<T>(operation: () => T): T {
|
|
|
665
905
|
export function withSqliteTransformPass<T>(operation: () => T): T {
|
|
666
906
|
const existing = transformPassScope.getStore();
|
|
667
907
|
if (existing?.active) return operation();
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
908
|
+
const reservation = reserveForegroundSqliteTurn();
|
|
909
|
+
const run = (release: () => void): T => {
|
|
910
|
+
const lease = { active: true, remainingWaitMs: FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS };
|
|
911
|
+
return transformPassScope.run(lease, () => {
|
|
912
|
+
try {
|
|
913
|
+
const result = operation();
|
|
914
|
+
if (result && typeof (result as { then?: unknown }).then === "function") {
|
|
915
|
+
return Promise.resolve(result).finally(() => {
|
|
916
|
+
lease.active = false;
|
|
917
|
+
release();
|
|
918
|
+
}) as T;
|
|
919
|
+
}
|
|
920
|
+
lease.active = false;
|
|
921
|
+
release();
|
|
922
|
+
return result;
|
|
923
|
+
} catch (error) {
|
|
924
|
+
lease.active = false;
|
|
925
|
+
release();
|
|
926
|
+
throw error;
|
|
676
927
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
});
|
|
928
|
+
});
|
|
929
|
+
};
|
|
930
|
+
// Foreground callers racing maintenance are awaited transforms. Idle
|
|
931
|
+
// synchronous callers keep the existing immediate return/throw behavior.
|
|
932
|
+
return typeof reservation === "function" ? run(reservation) : (reservation.then(run) as T);
|
|
684
933
|
}
|
|
685
934
|
|
|
686
935
|
/** Start detached maintenance work without borrowing a foreground pass's budget. */
|
|
@@ -720,10 +969,10 @@ export class SqliteAcquisitionBusyError extends Error {
|
|
|
720
969
|
}
|
|
721
970
|
|
|
722
971
|
const SHORT_BUSY_TIMEOUT_MS = 25;
|
|
723
|
-
const FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS =
|
|
972
|
+
const FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS = 2_000;
|
|
724
973
|
const FOREGROUND_ACQUISITION_BUDGET_MS = 16_500;
|
|
725
974
|
/** Waiting this long for the writer lock, or holding it this long, is logged. */
|
|
726
|
-
const SLOW_WRITER_LOG_MS =
|
|
975
|
+
const SLOW_WRITER_LOG_MS = 100;
|
|
727
976
|
const WRITER_TRANSACTION_END = /^\s*(?:COMMIT|END|ROLLBACK)(?:\s+TRANSACTION)?\s*;?\s*$/i;
|
|
728
977
|
|
|
729
978
|
type WriterLane = "foreground" | "background";
|
|
@@ -764,32 +1013,67 @@ function foregroundWaitLease() {
|
|
|
764
1013
|
: undefined;
|
|
765
1014
|
}
|
|
766
1015
|
|
|
767
|
-
/**
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
function withAutocommitTimeout<T>(db: Database, operation: () => T): T {
|
|
1016
|
+
/** Probe a prepared statement's writer lock without charging uncontended work
|
|
1017
|
+
* against the foreground lock-wait budget. An exec script may commit some of
|
|
1018
|
+
* its statements before failing, so it cannot be retried without duplicating
|
|
1019
|
+
* writes; conservatively charge its single attempt's full execution time. */
|
|
1020
|
+
function withAutocommitTimeout<T>(db: Database, operation: () => T, singleStatement = true): T {
|
|
772
1021
|
assertTransformWrite();
|
|
773
1022
|
const lease = foregroundWaitLease();
|
|
774
|
-
if (isInTransaction(db)
|
|
775
|
-
const
|
|
776
|
-
db
|
|
777
|
-
`PRAGMA busy_timeout=${lease ? Math.floor(lease.remainingWaitMs) : SHORT_BUSY_TIMEOUT_MS}`,
|
|
778
|
-
);
|
|
1023
|
+
if (isInTransaction(db)) return operation();
|
|
1024
|
+
const scoped = lease || backgroundWriterScope.getStore();
|
|
1025
|
+
const previous = scoped ? pragmaNumber(db, "busy_timeout") : null;
|
|
779
1026
|
const started = performance.now();
|
|
1027
|
+
let completed = false;
|
|
1028
|
+
const execute = () => {
|
|
1029
|
+
const result = operation();
|
|
1030
|
+
completed = true;
|
|
1031
|
+
return result;
|
|
1032
|
+
};
|
|
780
1033
|
try {
|
|
781
|
-
return
|
|
1034
|
+
if (!scoped) return execute();
|
|
1035
|
+
if (singleStatement) {
|
|
1036
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
1037
|
+
try {
|
|
1038
|
+
return execute();
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
if (!isTransientSqliteError(error) || isInTransaction(db)) throw error;
|
|
1041
|
+
}
|
|
1042
|
+
assertTransformWrite();
|
|
1043
|
+
}
|
|
1044
|
+
const timeout = Math.floor(
|
|
1045
|
+
Math.min(
|
|
1046
|
+
lease?.remainingWaitMs ?? SHORT_BUSY_TIMEOUT_MS,
|
|
1047
|
+
lease?.guard?.remainingMs() ?? Infinity,
|
|
1048
|
+
),
|
|
1049
|
+
);
|
|
1050
|
+
db.exec(`PRAGMA busy_timeout=${timeout}`);
|
|
1051
|
+
const waitStarted = performance.now();
|
|
1052
|
+
try {
|
|
1053
|
+
return execute();
|
|
1054
|
+
} finally {
|
|
1055
|
+
// Deduct the prepared statement's contended retry or the exec
|
|
1056
|
+
// script's execution from the remaining foreground wait budget;
|
|
1057
|
+
// the native API cannot separate lock waiting from execution.
|
|
1058
|
+
if (lease)
|
|
1059
|
+
lease.remainingWaitMs = Math.max(
|
|
1060
|
+
0,
|
|
1061
|
+
lease.remainingWaitMs - (performance.now() - waitStarted),
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
782
1064
|
} catch (error) {
|
|
783
1065
|
if (lease && isTransientSqliteError(error))
|
|
784
1066
|
throw new SqliteAcquisitionBusyError(error, "autocommit");
|
|
785
1067
|
throw error;
|
|
786
1068
|
} finally {
|
|
787
|
-
|
|
788
|
-
lease.remainingWaitMs = Math.max(
|
|
789
|
-
0,
|
|
790
|
-
lease.remainingWaitMs - (performance.now() - started),
|
|
791
|
-
);
|
|
1069
|
+
const elapsed = performance.now() - started;
|
|
792
1070
|
if (previous !== null) db.exec(`PRAGMA busy_timeout=${previous}`);
|
|
1071
|
+
// Native autocommit does not expose the instant its lock was acquired.
|
|
1072
|
+
// Label the upper bound so a waiting statement is not blamed as a holder.
|
|
1073
|
+
if (elapsed >= SLOW_WRITER_LOG_MS)
|
|
1074
|
+
reportSqliteDiagnostic(
|
|
1075
|
+
`sqlite writer site=autocommit lane=${lease ? "foreground" : "background"} elapsed_ms=${Math.round(elapsed)} timing=statement_including_acquisition outcome=${completed ? "completed" : "failed"} caller=${writerCaller()}`,
|
|
1076
|
+
);
|
|
793
1077
|
}
|
|
794
1078
|
}
|
|
795
1079
|
|
|
@@ -851,7 +1135,9 @@ function acquireShort(db: Database, acquire: () => unknown, site: string): void
|
|
|
851
1135
|
* `acquireShort`, then report how long the writer waited for the lock
|
|
852
1136
|
* (`acquire_ms`) and how long it then held it (`hold_ms`) when either crosses
|
|
853
1137
|
* the threshold. A long hold blocks every other process's writer, so it is
|
|
854
|
-
* worth seeing even when the lock itself came quickly.
|
|
1138
|
+
* worth seeing even when the lock itself came quickly. This interval includes
|
|
1139
|
+
* COMMIT: an automatic PASSIVE checkpoint can run after writer-lock release,
|
|
1140
|
+
* so hold_ms is a conservative upper bound, not proof of continuous ownership.
|
|
855
1141
|
*/
|
|
856
1142
|
function endWriterTransaction(db: Database, end: () => unknown, sql: string): unknown {
|
|
857
1143
|
let ended = false;
|
|
@@ -872,13 +1158,32 @@ function endWriterTransaction(db: Database, end: () => unknown, sql: string): un
|
|
|
872
1158
|
? "committed"
|
|
873
1159
|
: "failed";
|
|
874
1160
|
reportSqliteDiagnostic(
|
|
875
|
-
`sqlite writer site=${open.site} lane=${open.lane} acquire_ms=${Math.round(open.acquireMs)} hold_ms=${Math.round(holdMs)} attempts=${open.attempts} outcome=${outcome}`,
|
|
1161
|
+
`sqlite writer site=${open.site} lane=${open.lane} acquire_ms=${Math.round(open.acquireMs)} hold_ms=${Math.round(holdMs)} attempts=${open.attempts} outcome=${outcome} timing=through_commit_upper_bound caller=${writerCaller()}`,
|
|
876
1162
|
);
|
|
877
1163
|
}
|
|
878
1164
|
}
|
|
879
1165
|
}
|
|
880
1166
|
}
|
|
881
1167
|
|
|
1168
|
+
// Capture a stack only for slow writes. Synchronous COMMIT retains the writer's
|
|
1169
|
+
// caller, so fast transactions need neither stack allocation nor source mapping.
|
|
1170
|
+
function writerCaller(): string {
|
|
1171
|
+
return (
|
|
1172
|
+
new Error().stack
|
|
1173
|
+
?.split("\n")
|
|
1174
|
+
.find(
|
|
1175
|
+
(line) =>
|
|
1176
|
+
line.includes(" at ") &&
|
|
1177
|
+
!/[/\\]sqlite\.[tj]s[:)]/.test(line) &&
|
|
1178
|
+
!/\b(?:writerCaller|endWriterTransaction|withAutocommitTimeout|value)\b/.test(
|
|
1179
|
+
line,
|
|
1180
|
+
),
|
|
1181
|
+
)
|
|
1182
|
+
?.trim()
|
|
1183
|
+
.replace(/\s+/g, "_") ?? "unknown"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
882
1187
|
/** Begin an immediate transaction with bounded asynchronous retries on writer contention.
|
|
883
1188
|
* Callers must recheck any lease or source snapshot after this function returns.
|
|
884
1189
|
* Resolves with the `performance.now()` time at which BEGIN IMMEDIATE succeeded,
|
|
@@ -1071,3 +1376,80 @@ export function withPrivilegedWriter<T>(db: Database, operation: () => T): T {
|
|
|
1071
1376
|
throw error;
|
|
1072
1377
|
}
|
|
1073
1378
|
}
|
|
1379
|
+
|
|
1380
|
+
// Process-local admission only; SQLite remains the cross-process writer arbiter.
|
|
1381
|
+
const indexCoordinationKey = Symbol.for("magic-context.index-foreground-coordination.v1");
|
|
1382
|
+
const indexCoordinationHeap = globalThis as unknown as Record<
|
|
1383
|
+
symbol,
|
|
1384
|
+
SharedArrayBuffer | undefined
|
|
1385
|
+
>;
|
|
1386
|
+
const indexCoordinationBuffer =
|
|
1387
|
+
indexCoordinationHeap[indexCoordinationKey] ??
|
|
1388
|
+
new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT);
|
|
1389
|
+
indexCoordinationHeap[indexCoordinationKey] = indexCoordinationBuffer;
|
|
1390
|
+
const indexCoordinationState = new Int32Array(indexCoordinationBuffer);
|
|
1391
|
+
// Slots: foreground interest, current maintenance owner, next owner token.
|
|
1392
|
+
|
|
1393
|
+
export function indexCoordinationInput(): {
|
|
1394
|
+
indexCoordination: SharedArrayBuffer;
|
|
1395
|
+
indexOwner: number;
|
|
1396
|
+
} {
|
|
1397
|
+
return {
|
|
1398
|
+
indexCoordination: indexCoordinationBuffer,
|
|
1399
|
+
indexOwner: Atomics.add(indexCoordinationState, 2, 1) + 1,
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const yieldIndexTurn = () => new Promise<void>((resolve) => setTimeout(resolve, 1));
|
|
1404
|
+
|
|
1405
|
+
/** Announce interest before waiting, so no maintenance chunk overtakes a foreground turn. */
|
|
1406
|
+
function reserveForegroundSqliteTurn(): (() => void) | Promise<() => void> {
|
|
1407
|
+
Atomics.add(indexCoordinationState, 0, 1);
|
|
1408
|
+
const release = () => {
|
|
1409
|
+
Atomics.sub(indexCoordinationState, 0, 1);
|
|
1410
|
+
Atomics.notify(indexCoordinationState, 0);
|
|
1411
|
+
};
|
|
1412
|
+
if (Atomics.load(indexCoordinationState, 1) === 0) return release;
|
|
1413
|
+
return (async () => {
|
|
1414
|
+
while (Atomics.load(indexCoordinationState, 1) !== 0) await yieldIndexTurn();
|
|
1415
|
+
return release;
|
|
1416
|
+
})();
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
export function releaseIndexOwner(input: {
|
|
1420
|
+
indexCoordination?: SharedArrayBuffer;
|
|
1421
|
+
indexOwner?: number;
|
|
1422
|
+
}): void {
|
|
1423
|
+
if (!input.indexCoordination || !input.indexOwner) return;
|
|
1424
|
+
const shared = new Int32Array(input.indexCoordination);
|
|
1425
|
+
Atomics.compareExchange(shared, 1, input.indexOwner, 0);
|
|
1426
|
+
Atomics.notify(shared, 1);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/** One synchronous transaction per permit; release the permit before yielding. */
|
|
1430
|
+
export async function withIndexChunk<T>(
|
|
1431
|
+
input: { indexCoordination?: SharedArrayBuffer; indexOwner?: number },
|
|
1432
|
+
operation: () => T,
|
|
1433
|
+
): Promise<T> {
|
|
1434
|
+
if (!input.indexCoordination || !input.indexOwner) return operation();
|
|
1435
|
+
const shared = new Int32Array(input.indexCoordination);
|
|
1436
|
+
for (;;) {
|
|
1437
|
+
if (
|
|
1438
|
+
Atomics.load(shared, 0) === 0 &&
|
|
1439
|
+
Atomics.compareExchange(shared, 1, 0, input.indexOwner) === 0
|
|
1440
|
+
) {
|
|
1441
|
+
if (Atomics.load(shared, 0) === 0) break;
|
|
1442
|
+
releaseIndexOwner(input);
|
|
1443
|
+
}
|
|
1444
|
+
await yieldIndexTurn();
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
return await operation();
|
|
1448
|
+
} finally {
|
|
1449
|
+
releaseIndexOwner(input);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
export async function yieldIndexChunk(): Promise<void> {
|
|
1454
|
+
await yieldIndexTurn();
|
|
1455
|
+
}
|