@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.
Files changed (285) hide show
  1. package/dist/cli.js +14 -3
  2. package/dist/config/deprecated-config.d.ts +4 -0
  3. package/dist/config/deprecated-config.d.ts.map +1 -0
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/project-security.d.ts +2 -2
  6. package/dist/config/schema/magic-context.d.ts +36 -3
  7. package/dist/config/schema/magic-context.d.ts.map +1 -1
  8. package/dist/config-init.js +14 -3
  9. package/dist/{context-mrvm92n8.js → context-c72brxe5.js} +2410 -1801
  10. package/dist/{derive-budgets-rjjz5f7y.js → derive-budgets-s1p37t2g.js} +2 -2
  11. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  12. package/dist/features/magic-context/historian-failure-reason.d.ts +7 -0
  13. package/dist/features/magic-context/historian-failure-reason.d.ts.map +1 -0
  14. package/dist/features/magic-context/memory-maintainer/launcher.d.ts +13 -4
  15. package/dist/features/magic-context/memory-maintainer/launcher.d.ts.map +1 -1
  16. package/dist/features/magic-context/memory-maintainer/manual-commands.d.ts.map +1 -1
  17. package/dist/features/magic-context/memory-maintainer/model-chain.d.ts +47 -0
  18. package/dist/features/magic-context/memory-maintainer/model-chain.d.ts.map +1 -0
  19. package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts +17 -4
  20. package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts.map +1 -1
  21. package/dist/features/magic-context/memory-maintainer/pipeline.d.ts +6 -0
  22. package/dist/features/magic-context/memory-maintainer/pipeline.d.ts.map +1 -1
  23. package/dist/features/magic-context/message-index-async.d.ts +21 -3
  24. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  25. package/dist/features/magic-context/message-index.d.ts +7 -1
  26. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  27. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  28. package/dist/features/magic-context/pi-content-decisions.d.ts +7 -1
  29. package/dist/features/magic-context/pi-content-decisions.d.ts.map +1 -1
  30. package/dist/features/magic-context/search.d.ts +29 -0
  31. package/dist/features/magic-context/search.d.ts.map +1 -1
  32. package/dist/features/magic-context/storage-clone.d.ts +2 -0
  33. package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
  34. package/dist/features/magic-context/storage-compartment-history-version.d.ts +4 -0
  35. package/dist/features/magic-context/storage-compartment-history-version.d.ts.map +1 -0
  36. package/dist/features/magic-context/storage-db.d.ts +1 -1
  37. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  38. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
  39. package/dist/features/magic-context/storage-meta-persisted.d.ts +23 -5
  40. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  41. package/dist/features/magic-context/storage-meta-session.d.ts +7 -1
  42. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  43. package/dist/features/magic-context/storage-session-tables.d.ts +5 -1
  44. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  45. package/dist/features/magic-context/storage-tag-identity-revision.d.ts +50 -0
  46. package/dist/features/magic-context/storage-tag-identity-revision.d.ts.map +1 -0
  47. package/dist/features/magic-context/storage-tags.d.ts +159 -5
  48. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  49. package/dist/features/magic-context/tagger.d.ts +2 -0
  50. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  51. package/dist/features/magic-context/transform-decision-log.d.ts +60 -2
  52. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts +34 -0
  54. package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts.map +1 -0
  55. package/dist/hooks/magic-context/active-anthropic-turn.d.ts +2 -0
  56. package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/apply-operations.d.ts +10 -1
  58. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/auto-search-runner.d.ts +2 -2
  60. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/compartment-runner-types.d.ts +2 -2
  62. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/compartment-runner.d.ts +4 -0
  64. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/compartment-trigger-error.d.ts +11 -0
  66. package/dist/hooks/magic-context/compartment-trigger-error.d.ts.map +1 -0
  67. package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
  68. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +1 -0
  70. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/degraded-pass-refusal.d.ts +2 -0
  72. package/dist/hooks/magic-context/degraded-pass-refusal.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/edit-marker.d.ts +2 -0
  74. package/dist/hooks/magic-context/edit-marker.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  76. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/history-boundary-repair.d.ts +11 -2
  79. package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
  80. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/lkg-measured-request.d.ts +3 -1
  82. package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/lkg-persist.d.ts +10 -13
  84. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +62 -0
  86. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -0
  87. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +38 -0
  88. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -0
  89. package/dist/hooks/magic-context/lkg-replay.d.ts +16 -9
  90. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/lkg-slot.d.ts +141 -2
  92. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/opaque-thinking.d.ts +12 -0
  94. package/dist/hooks/magic-context/opaque-thinking.d.ts.map +1 -0
  95. package/dist/hooks/magic-context/pass-outcome.d.ts +1 -0
  96. package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/pending-drop-eligibility.d.ts +14 -0
  98. package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -0
  99. package/dist/hooks/magic-context/pending-materialization.d.ts +10 -0
  100. package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -0
  101. package/dist/hooks/magic-context/postprocess-read-cache.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/provider-visible-parts.d.ts +51 -0
  103. package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -0
  104. package/dist/hooks/magic-context/read-session-chunk.d.ts +22 -1
  105. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/read-session-db.d.ts +3 -0
  107. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/read-session-formatting.d.ts +5 -0
  109. package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/read-session-raw.d.ts +123 -1
  111. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/render-options.d.ts +19 -0
  115. package/dist/hooks/magic-context/render-options.d.ts.map +1 -0
  116. package/dist/hooks/magic-context/sentinel.d.ts +41 -1
  117. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/strict-thinking-prefix.d.ts +8 -0
  119. package/dist/hooks/magic-context/strict-thinking-prefix.d.ts.map +1 -0
  120. package/dist/hooks/magic-context/strip-content.d.ts +38 -24
  121. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/supersession-reclaim.d.ts +5 -3
  123. package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/system-date-refresh.d.ts +29 -0
  125. package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -0
  126. package/dist/hooks/magic-context/system-prompt-dates.d.ts +6 -0
  127. package/dist/hooks/magic-context/system-prompt-dates.d.ts.map +1 -0
  128. package/dist/hooks/magic-context/system-prompt-delivery.d.ts +47 -0
  129. package/dist/hooks/magic-context/system-prompt-delivery.d.ts.map +1 -0
  130. package/dist/hooks/magic-context/system-prompt-hash.d.ts +18 -5
  131. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  132. package/dist/hooks/magic-context/tag-messages.d.ts +19 -0
  133. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  134. package/dist/hooks/magic-context/tag-owner-warmup.d.ts +14 -0
  135. package/dist/hooks/magic-context/tag-owner-warmup.d.ts.map +1 -0
  136. package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts +18 -0
  137. package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts.map +1 -0
  138. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +8 -4
  139. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  140. package/dist/hooks/magic-context/token-count-exact.d.ts +35 -0
  141. package/dist/hooks/magic-context/token-count-exact.d.ts.map +1 -0
  142. package/dist/hooks/magic-context/tool-drop-target.d.ts +2 -0
  143. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  144. package/dist/hooks/magic-context/tool-reclaim.d.ts +5 -2
  145. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
  146. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  147. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +9 -0
  148. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  149. package/dist/hooks/magic-context/transform.d.ts +21 -11
  150. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  151. package/dist/hooks/magic-context/unresolved-history-boundary.d.ts +5 -1
  152. package/dist/hooks/magic-context/unresolved-history-boundary.d.ts.map +1 -1
  153. package/dist/{index-g2z2k5m4.js → index-27ddyx39.js} +2 -2
  154. package/dist/{index-btp5tdc8.js → index-48e9znrc.js} +1248 -570
  155. package/dist/{index-5cep2r9n.js → index-5ty9rnbx.js} +60 -258
  156. package/dist/{index-z2g20msd.js → index-azbmnekv.js} +4 -4
  157. package/dist/{index-1j34a56j.js → index-dwh99z66.js} +293 -36
  158. package/dist/{index-qbkzdkk0.js → index-k59af64t.js} +6 -2
  159. package/dist/{index-5b7x80ec.js → index-kahkxcqw.js} +12532 -10348
  160. package/dist/{index-53g38ggt.js → index-kmbme1v1.js} +1 -1
  161. package/dist/{index-amycb7b1.js → index-r7s7d350.js} +3 -3
  162. package/dist/{index-33gprt9g.js → index-yjv8dpht.js} +1 -1
  163. package/dist/index-ykq24x1n.js +1477 -0
  164. package/dist/index.d.ts.map +1 -1
  165. package/dist/index.js +114 -2
  166. package/dist/{message-time-backfill-phrwp2fz.js → message-time-backfill-j96x1sab.js} +3 -3
  167. package/dist/model-resolution-cvyrrcjw.js +8 -0
  168. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  169. package/dist/{read-session-chunk-2xm4sc4y.js → read-session-chunk-t8bg6wkz.js} +6 -4
  170. package/dist/recomp-orchestrator-k760qy1n.js +21 -0
  171. package/dist/{safe-notification-target-ph3xd1ve.js → safe-notification-target-1jkf0m82.js} +2 -2
  172. package/dist/{send-session-notification-rnc4nrsr.js → send-session-notification-njjf3b4s.js} +5 -5
  173. package/dist/shared/auto-search-lifecycle.d.ts +14 -0
  174. package/dist/shared/auto-search-lifecycle.d.ts.map +1 -1
  175. package/dist/shared/logger.d.ts.map +1 -1
  176. package/dist/shared/model-suggestion-retry.d.ts +21 -0
  177. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  178. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  179. package/dist/shared/models-dev-temperature.d.ts +18 -0
  180. package/dist/shared/models-dev-temperature.d.ts.map +1 -0
  181. package/dist/shared/processed-image-eligibility.d.ts +22 -0
  182. package/dist/shared/processed-image-eligibility.d.ts.map +1 -0
  183. package/dist/shared/project-directory-key.d.ts +16 -0
  184. package/dist/shared/project-directory-key.d.ts.map +1 -1
  185. package/dist/shared/prompt-surface-runtime.d.ts +5 -0
  186. package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
  187. package/dist/shared/reclaim-distance.d.ts +33 -0
  188. package/dist/shared/reclaim-distance.d.ts.map +1 -0
  189. package/dist/shared/recovery-reasoning-prefix.d.ts +9 -0
  190. package/dist/shared/recovery-reasoning-prefix.d.ts.map +1 -0
  191. package/dist/shared/sqlite.d.ts +35 -1
  192. package/dist/shared/sqlite.d.ts.map +1 -1
  193. package/dist/shared/stable-json.d.ts +7 -0
  194. package/dist/shared/stable-json.d.ts.map +1 -1
  195. package/dist/shared/storage-file-mutex.d.ts +9 -0
  196. package/dist/shared/storage-file-mutex.d.ts.map +1 -0
  197. package/dist/shared/storage-permissions.d.ts +6 -0
  198. package/dist/shared/storage-permissions.d.ts.map +1 -1
  199. package/dist/shared/subagent-temperature.d.ts +46 -0
  200. package/dist/shared/subagent-temperature.d.ts.map +1 -0
  201. package/dist/shared/tag-owner-id.d.ts +1 -0
  202. package/dist/shared/tag-owner-id.d.ts.map +1 -1
  203. package/dist/shared/tag-transcript.d.ts +9 -0
  204. package/dist/shared/tag-transcript.d.ts.map +1 -1
  205. package/dist/shared/tail-hygiene-json.d.ts +22 -0
  206. package/dist/shared/tail-hygiene-json.d.ts.map +1 -0
  207. package/dist/shared/user-facing-codes.d.ts +2 -2
  208. package/dist/shared/window-geometry.d.ts +9 -66
  209. package/dist/shared/window-geometry.d.ts.map +1 -1
  210. package/dist/standalone-plugin-marker.d.ts +8 -0
  211. package/dist/standalone-plugin-marker.d.ts.map +1 -0
  212. package/dist/tools/ctx-expand/search-archive.d.ts +4 -0
  213. package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -0
  214. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  215. package/dist/tools/ctx-reduce/constants.d.ts +1 -2
  216. package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
  217. package/dist/tools/ctx-reduce/tools.d.ts +1 -1
  218. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  219. package/dist/v2/hidden-completion.d.ts +20 -1
  220. package/dist/v2/hidden-completion.d.ts.map +1 -1
  221. package/dist/v2/hooks/context.d.ts +1 -1
  222. package/dist/v2/hooks/context.d.ts.map +1 -1
  223. package/dist/v2/hooks/copied-tag-notation.d.ts +19 -0
  224. package/dist/v2/hooks/copied-tag-notation.d.ts.map +1 -0
  225. package/dist/v2/hooks/generate.d.ts +9 -2
  226. package/dist/v2/hooks/generate.d.ts.map +1 -1
  227. package/dist/v2/hooks/hidden-child.d.ts +22 -1
  228. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  229. package/dist/v2/hooks/payload.d.ts.map +1 -1
  230. package/dist/v2/hooks/system-prompt-response.d.ts +5 -0
  231. package/dist/v2/hooks/system-prompt-response.d.ts.map +1 -0
  232. package/dist/v2/server.d.ts.map +1 -1
  233. package/dist/v2/server.js +26593 -21940
  234. package/dist/v2/store-reader.d.ts +27 -1
  235. package/dist/v2/store-reader.d.ts.map +1 -1
  236. package/dist/web/main.js +401 -23
  237. package/dist/{wrapup-orchestrator-yknks3ad.js → wrapup-orchestrator-b2gswatj.js} +10 -10
  238. package/package.json +1 -1
  239. package/src/config/deprecated-config.ts +18 -0
  240. package/src/config/index.test.ts +13 -37
  241. package/src/config/index.ts +2 -2
  242. package/src/config/latch-permanence-guard.test.ts +4 -0
  243. package/src/config/project-security.test.ts +17 -0
  244. package/src/config/project-security.ts +3 -3
  245. package/src/config/schema/magic-context.test.ts +50 -0
  246. package/src/config/schema/magic-context.ts +44 -7
  247. package/src/shared/auto-search-lifecycle.ts +43 -0
  248. package/src/shared/logger.ts +10 -2
  249. package/src/shared/model-suggestion-retry.ts +5 -1
  250. package/src/shared/models-dev-cache.ts +1 -7
  251. package/src/shared/models-dev-temperature.ts +107 -0
  252. package/src/shared/processed-image-eligibility.test.ts +68 -0
  253. package/src/shared/processed-image-eligibility.ts +41 -0
  254. package/src/shared/processed-image-position-review.json +20 -0
  255. package/src/shared/processed-image-position-review.test.ts +19 -0
  256. package/src/shared/project-directory-key.ts +20 -0
  257. package/src/shared/prompt-surface-runtime.ts +12 -0
  258. package/src/shared/reclaim-distance.test.ts +111 -0
  259. package/src/shared/reclaim-distance.ts +91 -0
  260. package/src/shared/recovery-reasoning-prefix.test.ts +34 -0
  261. package/src/shared/recovery-reasoning-prefix.ts +35 -0
  262. package/src/shared/root-async-context.probe.mjs +36 -0
  263. package/src/shared/root-async-context.test.ts +49 -0
  264. package/src/shared/sqlite-acquisition-routing.test.ts +4 -3
  265. package/src/shared/sqlite-busy-budget-review.test.ts +291 -0
  266. package/src/shared/sqlite-foreground-wait.test.ts +304 -0
  267. package/src/shared/sqlite-pass-scope.test.ts +4 -4
  268. package/src/shared/sqlite-writer-diagnostics.test.ts +52 -12
  269. package/src/shared/sqlite.ts +428 -46
  270. package/src/shared/stable-json.ts +79 -7
  271. package/src/shared/storage-file-mutex.test.ts +50 -0
  272. package/src/shared/storage-file-mutex.ts +130 -0
  273. package/src/shared/storage-permissions.ts +46 -0
  274. package/src/shared/subagent-temperature.ts +110 -0
  275. package/src/shared/tag-owner-id.ts +13 -0
  276. package/src/shared/tag-transcript.ts +118 -0
  277. package/src/shared/tail-hygiene-json.test.ts +117 -0
  278. package/src/shared/tail-hygiene-json.ts +217 -0
  279. package/src/shared/user-facing-codes.ts +2 -2
  280. package/src/shared/window-geometry.test.ts +102 -236
  281. package/src/shared/window-geometry.ts +23 -413
  282. package/dist/index-ramz5kkm.js +0 -514
  283. package/dist/model-resolution-wazv26n0.js +0 -6
  284. package/dist/recomp-orchestrator-frmwj9hz.js +0 -21
  285. package/src/shared/__fixtures__/window-overlay.v1.json +0 -337
@@ -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(sink: SqliteDiagnosticSink): void {
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?.(`[magic-context] ${message}`);
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 importer(specifier);
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 nativeExec(sql);
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 (!/^\s*(?:SELECT|EXPLAIN|PRAGMA|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(sql))
261
- return withAutocommitTimeout(db, () => nativeExec(sql));
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
- withAutocommitTimeout(db, () => execute(...args)),
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 = Reflect.construct(target, args, target) as BetterSqlite3.Database;
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 lease = { active: true, remainingWaitMs: FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS };
669
- return transformPassScope.run(lease, () => {
670
- try {
671
- const result = operation();
672
- if (result && typeof (result as { then?: unknown }).then === "function") {
673
- return Promise.resolve(result).finally(() => {
674
- lease.active = false;
675
- }) as T;
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
- lease.active = false;
678
- return result;
679
- } catch (error) {
680
- lease.active = false;
681
- throw error;
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 = 250;
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 = 250;
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
- /** Bound implicit writer admission too, including prepared INSERT OR IGNORE and
768
- * writes with RETURNING. Charge successful execution time conservatively: the
769
- * native API cannot separate lock waiting from statement execution. Exhaustion
770
- * switches to zero-wait acquisition, not a refusal when the writer is free. */
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) || (!lease && !backgroundWriterScope.getStore())) return operation();
775
- const previous = pragmaNumber(db, "busy_timeout");
776
- db.exec(
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 operation();
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
- if (lease)
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
+ }