@retaw106/mmc-oc 2.2.1 → 2.3.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 (211) hide show
  1. package/dist/cli.js +2 -2
  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 +3 -3
  7. package/dist/config/schema/magic-context.d.ts.map +1 -1
  8. package/dist/config-init.js +2 -2
  9. package/dist/{context-mrvm92n8.js → context-wt7g4f8k.js} +1641 -1543
  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/message-index-async.d.ts +21 -3
  15. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  16. package/dist/features/magic-context/message-index.d.ts +7 -1
  17. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  18. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  19. package/dist/features/magic-context/pi-content-decisions.d.ts +7 -1
  20. package/dist/features/magic-context/pi-content-decisions.d.ts.map +1 -1
  21. package/dist/features/magic-context/search.d.ts +29 -0
  22. package/dist/features/magic-context/search.d.ts.map +1 -1
  23. package/dist/features/magic-context/storage-clone.d.ts +2 -0
  24. package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
  25. package/dist/features/magic-context/storage-compartment-history-version.d.ts +4 -0
  26. package/dist/features/magic-context/storage-compartment-history-version.d.ts.map +1 -0
  27. package/dist/features/magic-context/storage-db.d.ts +1 -1
  28. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  29. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
  30. package/dist/features/magic-context/storage-meta-persisted.d.ts +23 -5
  31. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  32. package/dist/features/magic-context/storage-meta-session.d.ts +7 -1
  33. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  34. package/dist/features/magic-context/storage-session-tables.d.ts +5 -1
  35. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  36. package/dist/features/magic-context/storage-tag-identity-revision.d.ts +50 -0
  37. package/dist/features/magic-context/storage-tag-identity-revision.d.ts.map +1 -0
  38. package/dist/features/magic-context/storage-tags.d.ts +155 -5
  39. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  40. package/dist/features/magic-context/tagger.d.ts +2 -0
  41. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  42. package/dist/features/magic-context/transform-decision-log.d.ts +3 -0
  43. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/active-anthropic-turn.d.ts +2 -0
  45. package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/apply-operations.d.ts +7 -0
  47. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/auto-search-runner.d.ts +2 -2
  49. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/history-boundary-repair.d.ts +11 -2
  52. package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/lkg-measured-request.d.ts +3 -1
  55. package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -1
  56. package/dist/hooks/magic-context/lkg-persist.d.ts +10 -13
  57. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
  58. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +62 -0
  59. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -0
  60. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +38 -0
  61. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -0
  62. package/dist/hooks/magic-context/lkg-replay.d.ts +16 -9
  63. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/lkg-slot.d.ts +141 -2
  65. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/opaque-thinking.d.ts +12 -0
  67. package/dist/hooks/magic-context/opaque-thinking.d.ts.map +1 -0
  68. package/dist/hooks/magic-context/pass-outcome.d.ts +1 -0
  69. package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/pending-materialization.d.ts +8 -0
  71. package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -0
  72. package/dist/hooks/magic-context/postprocess-read-cache.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/provider-visible-parts.d.ts +45 -0
  74. package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -0
  75. package/dist/hooks/magic-context/read-session-chunk.d.ts +22 -1
  76. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/read-session-db.d.ts +3 -0
  78. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/read-session-formatting.d.ts +5 -0
  80. package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/read-session-raw.d.ts +123 -1
  82. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/sentinel.d.ts +41 -1
  86. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/strict-thinking-prefix.d.ts +8 -0
  88. package/dist/hooks/magic-context/strict-thinking-prefix.d.ts.map +1 -0
  89. package/dist/hooks/magic-context/strip-content.d.ts +38 -24
  90. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/system-date-refresh.d.ts +29 -0
  92. package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -0
  93. package/dist/hooks/magic-context/system-prompt-dates.d.ts +6 -0
  94. package/dist/hooks/magic-context/system-prompt-dates.d.ts.map +1 -0
  95. package/dist/hooks/magic-context/system-prompt-delivery.d.ts +47 -0
  96. package/dist/hooks/magic-context/system-prompt-delivery.d.ts.map +1 -0
  97. package/dist/hooks/magic-context/system-prompt-hash.d.ts +18 -5
  98. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/tag-messages.d.ts +19 -0
  100. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/tag-owner-warmup.d.ts +14 -0
  102. package/dist/hooks/magic-context/tag-owner-warmup.d.ts.map +1 -0
  103. package/dist/hooks/magic-context/token-count-exact.d.ts +35 -0
  104. package/dist/hooks/magic-context/token-count-exact.d.ts.map +1 -0
  105. package/dist/hooks/magic-context/tool-drop-target.d.ts +2 -0
  106. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +7 -0
  108. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/transform.d.ts +21 -4
  110. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  111. package/dist/index-18yrysyj.js +1477 -0
  112. package/dist/{index-g2z2k5m4.js → index-3gj1ak4c.js} +2 -2
  113. package/dist/{index-5cep2r9n.js → index-5ty9rnbx.js} +60 -258
  114. package/dist/{index-amycb7b1.js → index-a5mw0xp3.js} +3 -3
  115. package/dist/{index-1j34a56j.js → index-czba8tw3.js} +266 -32
  116. package/dist/{index-qbkzdkk0.js → index-k59af64t.js} +6 -2
  117. package/dist/{index-53g38ggt.js → index-kmbme1v1.js} +1 -1
  118. package/dist/{index-5b7x80ec.js → index-kw21z5pe.js} +6055 -3973
  119. package/dist/{index-33gprt9g.js → index-yjv8dpht.js} +1 -1
  120. package/dist/{index-btp5tdc8.js → index-z2w1eq3y.js} +840 -456
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +114 -2
  123. package/dist/{message-time-backfill-phrwp2fz.js → message-time-backfill-sykevdax.js} +3 -3
  124. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  125. package/dist/{read-session-chunk-2xm4sc4y.js → read-session-chunk-863d6c06.js} +6 -4
  126. package/dist/recomp-orchestrator-wrjy3qym.js +21 -0
  127. package/dist/{safe-notification-target-ph3xd1ve.js → safe-notification-target-1jkf0m82.js} +2 -2
  128. package/dist/{send-session-notification-rnc4nrsr.js → send-session-notification-cz6ev088.js} +5 -5
  129. package/dist/shared/auto-search-lifecycle.d.ts +14 -0
  130. package/dist/shared/auto-search-lifecycle.d.ts.map +1 -1
  131. package/dist/shared/logger.d.ts.map +1 -1
  132. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  133. package/dist/shared/models-dev-temperature.d.ts +18 -0
  134. package/dist/shared/models-dev-temperature.d.ts.map +1 -0
  135. package/dist/shared/processed-image-eligibility.d.ts +22 -0
  136. package/dist/shared/processed-image-eligibility.d.ts.map +1 -0
  137. package/dist/shared/project-directory-key.d.ts +16 -0
  138. package/dist/shared/project-directory-key.d.ts.map +1 -1
  139. package/dist/shared/recovery-reasoning-prefix.d.ts +9 -0
  140. package/dist/shared/recovery-reasoning-prefix.d.ts.map +1 -0
  141. package/dist/shared/sqlite.d.ts +33 -1
  142. package/dist/shared/sqlite.d.ts.map +1 -1
  143. package/dist/shared/stable-json.d.ts.map +1 -1
  144. package/dist/shared/storage-file-mutex.d.ts +9 -0
  145. package/dist/shared/storage-file-mutex.d.ts.map +1 -0
  146. package/dist/shared/storage-permissions.d.ts +6 -0
  147. package/dist/shared/storage-permissions.d.ts.map +1 -1
  148. package/dist/shared/subagent-temperature.d.ts +46 -0
  149. package/dist/shared/subagent-temperature.d.ts.map +1 -0
  150. package/dist/shared/tag-owner-id.d.ts +1 -0
  151. package/dist/shared/tag-owner-id.d.ts.map +1 -1
  152. package/dist/shared/tag-transcript.d.ts +9 -0
  153. package/dist/shared/tag-transcript.d.ts.map +1 -1
  154. package/dist/shared/window-geometry.d.ts +9 -66
  155. package/dist/shared/window-geometry.d.ts.map +1 -1
  156. package/dist/standalone-plugin-marker.d.ts +8 -0
  157. package/dist/standalone-plugin-marker.d.ts.map +1 -0
  158. package/dist/tools/ctx-expand/search-archive.d.ts +4 -0
  159. package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -0
  160. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  161. package/dist/tools/ctx-reduce/constants.d.ts +1 -2
  162. package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
  163. package/dist/tools/ctx-reduce/tools.d.ts +1 -1
  164. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  165. package/dist/v2/hooks/context.d.ts +1 -1
  166. package/dist/v2/hooks/context.d.ts.map +1 -1
  167. package/dist/v2/hooks/copied-tag-notation.d.ts +19 -0
  168. package/dist/v2/hooks/copied-tag-notation.d.ts.map +1 -0
  169. package/dist/v2/hooks/generate.d.ts +9 -2
  170. package/dist/v2/hooks/generate.d.ts.map +1 -1
  171. package/dist/v2/hooks/payload.d.ts.map +1 -1
  172. package/dist/v2/hooks/system-prompt-response.d.ts +5 -0
  173. package/dist/v2/hooks/system-prompt-response.d.ts.map +1 -0
  174. package/dist/v2/server.d.ts.map +1 -1
  175. package/dist/v2/server.js +34550 -30855
  176. package/dist/web/main.js +373 -19
  177. package/dist/{wrapup-orchestrator-yknks3ad.js → wrapup-orchestrator-bmybc5p5.js} +10 -10
  178. package/package.json +1 -1
  179. package/src/config/deprecated-config.ts +18 -0
  180. package/src/config/index.test.ts +13 -37
  181. package/src/config/index.ts +2 -2
  182. package/src/config/project-security.ts +3 -3
  183. package/src/config/schema/magic-context.ts +10 -6
  184. package/src/shared/auto-search-lifecycle.ts +43 -0
  185. package/src/shared/logger.ts +10 -2
  186. package/src/shared/models-dev-cache.ts +1 -7
  187. package/src/shared/models-dev-temperature.ts +107 -0
  188. package/src/shared/processed-image-eligibility.test.ts +68 -0
  189. package/src/shared/processed-image-eligibility.ts +41 -0
  190. package/src/shared/processed-image-position-review.json +20 -0
  191. package/src/shared/processed-image-position-review.test.ts +19 -0
  192. package/src/shared/project-directory-key.ts +20 -0
  193. package/src/shared/recovery-reasoning-prefix.test.ts +34 -0
  194. package/src/shared/recovery-reasoning-prefix.ts +35 -0
  195. package/src/shared/sqlite-acquisition-routing.test.ts +4 -3
  196. package/src/shared/sqlite-foreground-wait.test.ts +68 -0
  197. package/src/shared/sqlite-pass-scope.test.ts +4 -4
  198. package/src/shared/sqlite-writer-diagnostics.test.ts +52 -12
  199. package/src/shared/sqlite.ts +380 -39
  200. package/src/shared/stable-json.ts +37 -7
  201. package/src/shared/storage-file-mutex.test.ts +50 -0
  202. package/src/shared/storage-file-mutex.ts +130 -0
  203. package/src/shared/storage-permissions.ts +46 -0
  204. package/src/shared/subagent-temperature.ts +110 -0
  205. package/src/shared/tag-owner-id.ts +13 -0
  206. package/src/shared/tag-transcript.ts +118 -0
  207. package/src/shared/window-geometry.test.ts +102 -236
  208. package/src/shared/window-geometry.ts +23 -413
  209. package/dist/index-ramz5kkm.js +0 -514
  210. package/dist/recomp-orchestrator-frmwj9hz.js +0 -21
  211. package/src/shared/__fixtures__/window-overlay.v1.json +0 -337
@@ -71,14 +71,21 @@ export function registerSlowWriteReporter(reporter: SlowWriteReporter): void {
71
71
  */
72
72
  type SqliteDiagnosticSink = (message: string) => void;
73
73
  let sqliteDiagnosticSink: SqliteDiagnosticSink | undefined;
74
+ let sqliteDiagnosticHost: () => string = () => "unknown";
74
75
 
75
- export function registerSqliteDiagnosticSink(sink: SqliteDiagnosticSink): void {
76
+ export function registerSqliteDiagnosticSink(
77
+ sink: SqliteDiagnosticSink,
78
+ host?: () => string,
79
+ ): void {
76
80
  sqliteDiagnosticSink = sink;
81
+ if (host) sqliteDiagnosticHost = host;
77
82
  }
78
83
 
79
84
  function reportSqliteDiagnostic(message: string): void {
80
85
  try {
81
- sqliteDiagnosticSink?.(`[magic-context] ${message}`);
86
+ sqliteDiagnosticSink?.(
87
+ `[magic-context] ${message} pid=${process.pid} host=${sqliteDiagnosticHost()}`,
88
+ );
82
89
  } catch {
83
90
  // Diagnostics must never change the outcome of the write they describe.
84
91
  }
@@ -169,13 +176,51 @@ export class SqliteRuntimeUnavailableError extends Error {
169
176
  }
170
177
  }
171
178
 
179
+ // Node requests this warning during node:sqlite's module evaluation, although
180
+ // warning delivery happens later. Intercept only the request while importing;
181
+ // replacing warning listeners would not suppress the default stderr output.
182
+ function isNodeSqliteExperimentalWarning(args: unknown[]): boolean {
183
+ const [warning, options] = args;
184
+ const message = warning instanceof Error ? warning.message : warning;
185
+ const type =
186
+ typeof options === "string"
187
+ ? options
188
+ : options && typeof options === "object"
189
+ ? (options as { type?: unknown }).type
190
+ : undefined;
191
+ return (
192
+ type === "ExperimentalWarning" &&
193
+ typeof message === "string" &&
194
+ message.startsWith("SQLite is an experimental feature")
195
+ );
196
+ }
197
+
198
+ async function importSqliteModuleForRuntime(
199
+ runtime: SqliteRuntime,
200
+ specifier: string,
201
+ importer: (specifier: string) => Promise<SqliteModule>,
202
+ ): Promise<SqliteModule> {
203
+ if (runtime !== "Node.js") return importer(specifier);
204
+
205
+ const originalEmitWarning = process.emitWarning;
206
+ process.emitWarning = function (this: unknown, ...args: unknown[]): void {
207
+ if (isNodeSqliteExperimentalWarning(args)) return;
208
+ Reflect.apply(originalEmitWarning, this, args);
209
+ } as typeof process.emitWarning;
210
+ try {
211
+ return await importer(specifier);
212
+ } finally {
213
+ process.emitWarning = originalEmitWarning;
214
+ }
215
+ }
216
+
172
217
  export async function loadSqliteModule(
173
218
  runtime: SqliteRuntime = detectSqliteRuntime(),
174
219
  importer: (specifier: string) => Promise<SqliteModule> = importSqliteModule,
175
220
  ): Promise<SqliteModule> {
176
221
  const specifier = runtime === "Bun" ? bunSpec : nodeSpec;
177
222
  try {
178
- return await importer(specifier);
223
+ return await importSqliteModuleForRuntime(runtime, specifier, importer);
179
224
  } catch (error) {
180
225
  if (isModuleNotFoundError(error, specifier)) {
181
226
  throw new SqliteRuntimeUnavailableError(runtime, specifier, error);
@@ -234,6 +279,110 @@ export interface SqliteMemoryStats {
234
279
  const trackedSqliteConnections = new Map<number, TrackedSqliteConnection>();
235
280
  let nextSqliteConnectionSequence = 1;
236
281
 
282
+ /**
283
+ * Columns of Magic Context's `tags` table (context.db) that never decide which
284
+ * message a tag belongs to, its number, its type, or its reasoning token estimate.
285
+ */
286
+ const TAG_COLUMNS_OUTSIDE_IDENTITY = new Set([
287
+ "status",
288
+ "drop_mode",
289
+ "byte_size",
290
+ "input_byte_size",
291
+ "reasoning_byte_size",
292
+ "token_count",
293
+ "input_token_count",
294
+ "caveman_depth",
295
+ "entry_fingerprint",
296
+ "tool_name",
297
+ "harness",
298
+ ]);
299
+
300
+ let tagIdentityWriteGeneration = 0;
301
+
302
+ /**
303
+ * Whether a write statement may change an existing `tags` row's identity
304
+ * (session, number, type, owner message, reasoning token count) or remove a
305
+ * row. Plain appends without an explicit `id`, and updates that assign only
306
+ * columns in {@link TAG_COLUMNS_OUTSIDE_IDENTITY}, cannot; everything else that
307
+ * names the table is treated as if it could, including DDL and multi-statement
308
+ * scripts. Erring towards true only costs a cache rebuild.
309
+ */
310
+ export function mayChangeTagIdentity(sql: string): boolean {
311
+ if (!/\btags\b/i.test(sql) || /^\s*(?:SELECT|EXPLAIN|PRAGMA)\b/i.test(sql)) return false;
312
+ if (/^\s*WITH\b/i.test(sql) && !/\b(?:INSERT|UPDATE|DELETE|REPLACE)\b/i.test(sql)) return false;
313
+ // One statement only: a script can hide a DELETE behind a harmless first statement.
314
+ if (/;\s*\S/.test(sql) || /\bREPLACE\b/i.test(sql) || /\bON\s+CONFLICT\b/i.test(sql))
315
+ return true;
316
+ const insert = /^\s*INSERT\s+INTO\s+(?:main\.)?["`]?tags["`]?\s*\(([^)]*)\)/i.exec(sql);
317
+ if (insert) {
318
+ const columns = (insert[1] ?? "").split(",").map((column) =>
319
+ column
320
+ .trim()
321
+ .replace(/["`[\]]/g, "")
322
+ .toLowerCase(),
323
+ );
324
+ return columns.includes("id") || columns.includes("rowid");
325
+ }
326
+ const update = /^\s*UPDATE\s+(?:main\.)?["`]?tags["`]?\s+SET\s+([\s\S]*?)\bWHERE\b/i.exec(sql);
327
+ if (update && !/\bSELECT\b/i.test(sql)) {
328
+ // Provably identity-free only when every top-level assignment is a plain
329
+ // `column = ...` naming an allowed column. A tuple assignment such as
330
+ // `(tag_number, reasoning_token_count) = (...)`, or anything this cannot
331
+ // read, counts as an identity write.
332
+ const assignments = topLevelCommaSegments(update[1] ?? "");
333
+ if (!assignments) return true;
334
+ return !assignments.every((assignment) => {
335
+ const column = /^\s*["`]?(\w+)["`]?\s*=(?!=)/.exec(assignment)?.[1];
336
+ return column !== undefined && TAG_COLUMNS_OUTSIDE_IDENTITY.has(column.toLowerCase());
337
+ });
338
+ }
339
+ return true;
340
+ }
341
+
342
+ /**
343
+ * Split SQL text at commas outside parentheses and quotes. Returns null when
344
+ * the parentheses or quotes do not balance.
345
+ */
346
+ function topLevelCommaSegments(text: string): string[] | null {
347
+ const segments: string[] = [];
348
+ let depth = 0;
349
+ let quote: string | null = null;
350
+ let start = 0;
351
+ for (let index = 0; index < text.length; index += 1) {
352
+ const char = text[index];
353
+ if (quote) {
354
+ // A doubled quote inside a quoted run is an escaped quote.
355
+ if (char === quote) {
356
+ if (text[index + 1] === quote) index += 1;
357
+ else quote = null;
358
+ }
359
+ continue;
360
+ }
361
+ if (char === "'" || char === '"' || char === "`") quote = char;
362
+ else if (char === "(") depth += 1;
363
+ else if (char === ")") {
364
+ depth -= 1;
365
+ if (depth < 0) return null;
366
+ } else if (char === "," && depth === 0) {
367
+ segments.push(text.slice(start, index));
368
+ start = index + 1;
369
+ }
370
+ }
371
+ if (quote || depth !== 0) return null;
372
+ segments.push(text.slice(start));
373
+ return segments;
374
+ }
375
+
376
+ /**
377
+ * Counts executions, on any connection this process opened, of statements for
378
+ * which {@link mayChangeTagIdentity} is true. A cache built from `tags` stays
379
+ * valid for this process's own writes while the count is unchanged; commits by
380
+ * other connections show up in `PRAGMA data_version` instead.
381
+ */
382
+ export function getTagIdentityWriteGeneration(): number {
383
+ return tagIdentityWriteGeneration;
384
+ }
385
+
237
386
  /** Route native Bun and Node transaction entry through the same acquisition retry.
238
387
  * Native Bun transaction wrappers execute BEGIN internally, bypassing an exec override,
239
388
  * so both runtimes use this small synchronous wrapper instead. Writable handles
@@ -250,15 +399,31 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
250
399
  // A transaction that ended without passing through exec (for
251
400
  // example an automatic rollback) must not be timed as this one.
252
401
  openWriterTransactions.delete(db);
253
- if (transformPassScope.getStore()?.active || backgroundWriterScope.getStore())
402
+ if (transformPassScope.getStore()?.active || backgroundWriterScope.getStore()) {
254
403
  acquireShort(db, () => nativeExec(sql), sql.trim());
255
- else nativeExec(sql);
404
+ } else {
405
+ nativeExec(sql);
406
+ openWriterTransactions.set(db, {
407
+ site: sql.trim(),
408
+ lane: "background",
409
+ acquireMs: 0,
410
+ attempts: 1,
411
+ acquiredAt: performance.now(),
412
+ });
413
+ }
256
414
  return db;
257
415
  }
258
416
  if (WRITER_TRANSACTION_END.test(sql) && openWriterTransactions.has(db))
259
417
  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));
418
+ if (
419
+ !/^\s*(?:SELECT|EXPLAIN|PRAGMA|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(sql)
420
+ ) {
421
+ try {
422
+ return withAutocommitTimeout(db, () => nativeExec(sql));
423
+ } finally {
424
+ if (mayChangeTagIdentity(sql)) tagIdentityWriteGeneration += 1;
425
+ }
426
+ }
262
427
  return nativeExec(sql);
263
428
  },
264
429
  });
@@ -271,15 +436,22 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
271
436
  // SELECT/EXPLAIN cannot acquire a writer. Treat CTEs conservatively:
272
437
  // WITH can introduce INSERT/UPDATE/DELETE as well as a read query.
273
438
  if (!readonly && !/^\s*(?:SELECT|EXPLAIN|PRAGMA)\b/i.test(sql)) {
439
+ const changesTagIdentity = mayChangeTagIdentity(sql);
274
440
  for (const method of ["run", "get", "all"] as const) {
275
441
  const execute = statement[method].bind(statement);
276
442
  Object.defineProperty(statement, method, {
277
443
  configurable: true,
278
444
  // biome-ignore lint/suspicious/noExplicitAny: retain SQLite's native binding overloads.
279
- value: (...args: any[]) =>
280
- withAutocommitTimeout(db, () => execute(...args)),
445
+ value: (...args: any[]) => {
446
+ try {
447
+ return withAutocommitTimeout(db, () => execute(...args));
448
+ } finally {
449
+ if (changesTagIdentity) tagIdentityWriteGeneration += 1;
450
+ }
451
+ },
281
452
  });
282
453
  }
454
+ if (changesTagIdentity) wrapRemainingTagWriteForms(statement);
283
455
  }
284
456
  return statement;
285
457
  },
@@ -330,6 +502,43 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
330
502
  });
331
503
  }
332
504
 
505
+ /**
506
+ * Count tag identity writes made through the statement forms the transaction
507
+ * routing does not wrap. `iterate()` runs the statement while its result is
508
+ * consumed, so the generation moves both when iteration starts and when it
509
+ * ends: a summary read in between is then rebuilt on its next read.
510
+ */
511
+ // biome-ignore lint/suspicious/noExplicitAny: statements from both backends share these method names.
512
+ function wrapRemainingTagWriteForms(statement: any): void {
513
+ if (typeof statement.iterate === "function") {
514
+ const iterate = statement.iterate.bind(statement);
515
+ Object.defineProperty(statement, "iterate", {
516
+ configurable: true,
517
+ value: function* (...args: unknown[]) {
518
+ tagIdentityWriteGeneration += 1;
519
+ try {
520
+ yield* iterate(...args);
521
+ } finally {
522
+ tagIdentityWriteGeneration += 1;
523
+ }
524
+ },
525
+ });
526
+ }
527
+ if (typeof statement.values === "function") {
528
+ const values = statement.values.bind(statement);
529
+ Object.defineProperty(statement, "values", {
530
+ configurable: true,
531
+ value: (...args: unknown[]) => {
532
+ try {
533
+ return values(...args);
534
+ } finally {
535
+ tagIdentityWriteGeneration += 1;
536
+ }
537
+ },
538
+ });
539
+ }
540
+ }
541
+
333
542
  function trackSqliteConnection(
334
543
  db: BetterSqlite3.Database,
335
544
  filename: unknown,
@@ -431,6 +640,23 @@ function buildNodeSqliteDatabaseClass(DatabaseSync: any): typeof BetterSqlite3 {
431
640
 
432
641
  export const Database: typeof BetterSqlite3 = TrackedDatabase;
433
642
 
643
+ /** The constructor path, without querying SQLite on the prompt thread. */
644
+ export function getSqliteDatabasePath(db: Database): string | null {
645
+ for (const connection of trackedSqliteConnections.values()) {
646
+ if (connection.reference.deref() === db) {
647
+ return connection.filename === ":memory:" || connection.filename === "<buffer>"
648
+ ? null
649
+ : connection.filename;
650
+ }
651
+ }
652
+ // Native Bun/better-sqlite3 handles can reach readers without our constructor
653
+ // wrapper. Their filename is still available without a SQLite query.
654
+ const native = db as unknown as { filename?: unknown; name?: unknown };
655
+ const path = native.filename ?? native.name;
656
+ if (typeof path === "string" && path !== ":memory:" && path !== "") return path;
657
+ return null;
658
+ }
659
+
434
660
  function pragmaValue(db: BetterSqlite3.Database, name: string): unknown {
435
661
  const row = db.prepare(`PRAGMA ${name}`).get() as Record<string, unknown> | undefined;
436
662
  if (!row) return undefined;
@@ -665,22 +891,31 @@ export function withSqliteBackgroundWriter<T>(operation: () => T): T {
665
891
  export function withSqliteTransformPass<T>(operation: () => T): T {
666
892
  const existing = transformPassScope.getStore();
667
893
  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;
894
+ const reservation = reserveForegroundSqliteTurn();
895
+ const run = (release: () => void): T => {
896
+ const lease = { active: true, remainingWaitMs: FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS };
897
+ return transformPassScope.run(lease, () => {
898
+ try {
899
+ const result = operation();
900
+ if (result && typeof (result as { then?: unknown }).then === "function") {
901
+ return Promise.resolve(result).finally(() => {
902
+ lease.active = false;
903
+ release();
904
+ }) as T;
905
+ }
906
+ lease.active = false;
907
+ release();
908
+ return result;
909
+ } catch (error) {
910
+ lease.active = false;
911
+ release();
912
+ throw error;
676
913
  }
677
- lease.active = false;
678
- return result;
679
- } catch (error) {
680
- lease.active = false;
681
- throw error;
682
- }
683
- });
914
+ });
915
+ };
916
+ // Foreground callers racing maintenance are awaited transforms. Idle
917
+ // synchronous callers keep the existing immediate return/throw behavior.
918
+ return typeof reservation === "function" ? run(reservation) : (reservation.then(run) as T);
684
919
  }
685
920
 
686
921
  /** Start detached maintenance work without borrowing a foreground pass's budget. */
@@ -720,10 +955,10 @@ export class SqliteAcquisitionBusyError extends Error {
720
955
  }
721
956
 
722
957
  const SHORT_BUSY_TIMEOUT_MS = 25;
723
- const FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS = 250;
958
+ const FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS = 2_000;
724
959
  const FOREGROUND_ACQUISITION_BUDGET_MS = 16_500;
725
960
  /** Waiting this long for the writer lock, or holding it this long, is logged. */
726
- const SLOW_WRITER_LOG_MS = 250;
961
+ const SLOW_WRITER_LOG_MS = 100;
727
962
  const WRITER_TRANSACTION_END = /^\s*(?:COMMIT|END|ROLLBACK)(?:\s+TRANSACTION)?\s*;?\s*$/i;
728
963
 
729
964
  type WriterLane = "foreground" | "background";
@@ -771,25 +1006,33 @@ function foregroundWaitLease() {
771
1006
  function withAutocommitTimeout<T>(db: Database, operation: () => T): T {
772
1007
  assertTransformWrite();
773
1008
  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
- );
1009
+ if (isInTransaction(db)) return operation();
1010
+ const scoped = lease || backgroundWriterScope.getStore();
1011
+ const previous = scoped ? pragmaNumber(db, "busy_timeout") : null;
1012
+ if (scoped)
1013
+ db.exec(
1014
+ `PRAGMA busy_timeout=${lease ? Math.floor(Math.min(lease.remainingWaitMs, lease.guard?.remainingMs() ?? Infinity)) : SHORT_BUSY_TIMEOUT_MS}`,
1015
+ );
779
1016
  const started = performance.now();
1017
+ let completed = false;
780
1018
  try {
781
- return operation();
1019
+ const result = operation();
1020
+ completed = true;
1021
+ return result;
782
1022
  } catch (error) {
783
1023
  if (lease && isTransientSqliteError(error))
784
1024
  throw new SqliteAcquisitionBusyError(error, "autocommit");
785
1025
  throw error;
786
1026
  } finally {
787
- if (lease)
788
- lease.remainingWaitMs = Math.max(
789
- 0,
790
- lease.remainingWaitMs - (performance.now() - started),
791
- );
1027
+ const elapsed = performance.now() - started;
1028
+ if (lease) lease.remainingWaitMs = Math.max(0, lease.remainingWaitMs - elapsed);
792
1029
  if (previous !== null) db.exec(`PRAGMA busy_timeout=${previous}`);
1030
+ // Native autocommit does not expose the instant its lock was acquired.
1031
+ // Label the upper bound so a waiting statement is not blamed as a holder.
1032
+ if (elapsed >= SLOW_WRITER_LOG_MS)
1033
+ reportSqliteDiagnostic(
1034
+ `sqlite writer site=autocommit lane=${lease ? "foreground" : "background"} elapsed_ms=${Math.round(elapsed)} timing=statement_including_acquisition outcome=${completed ? "completed" : "failed"} caller=${writerCaller()}`,
1035
+ );
793
1036
  }
794
1037
  }
795
1038
 
@@ -851,7 +1094,9 @@ function acquireShort(db: Database, acquire: () => unknown, site: string): void
851
1094
  * `acquireShort`, then report how long the writer waited for the lock
852
1095
  * (`acquire_ms`) and how long it then held it (`hold_ms`) when either crosses
853
1096
  * the threshold. A long hold blocks every other process's writer, so it is
854
- * worth seeing even when the lock itself came quickly.
1097
+ * worth seeing even when the lock itself came quickly. This interval includes
1098
+ * COMMIT: an automatic PASSIVE checkpoint can run after writer-lock release,
1099
+ * so hold_ms is a conservative upper bound, not proof of continuous ownership.
855
1100
  */
856
1101
  function endWriterTransaction(db: Database, end: () => unknown, sql: string): unknown {
857
1102
  let ended = false;
@@ -872,13 +1117,32 @@ function endWriterTransaction(db: Database, end: () => unknown, sql: string): un
872
1117
  ? "committed"
873
1118
  : "failed";
874
1119
  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}`,
1120
+ `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
1121
  );
877
1122
  }
878
1123
  }
879
1124
  }
880
1125
  }
881
1126
 
1127
+ // Capture a stack only for slow writes. Synchronous COMMIT retains the writer's
1128
+ // caller, so fast transactions need neither stack allocation nor source mapping.
1129
+ function writerCaller(): string {
1130
+ return (
1131
+ new Error().stack
1132
+ ?.split("\n")
1133
+ .find(
1134
+ (line) =>
1135
+ line.includes(" at ") &&
1136
+ !/[/\\]sqlite\.[tj]s[:)]/.test(line) &&
1137
+ !/\b(?:writerCaller|endWriterTransaction|withAutocommitTimeout|value)\b/.test(
1138
+ line,
1139
+ ),
1140
+ )
1141
+ ?.trim()
1142
+ .replace(/\s+/g, "_") ?? "unknown"
1143
+ );
1144
+ }
1145
+
882
1146
  /** Begin an immediate transaction with bounded asynchronous retries on writer contention.
883
1147
  * Callers must recheck any lease or source snapshot after this function returns.
884
1148
  * Resolves with the `performance.now()` time at which BEGIN IMMEDIATE succeeded,
@@ -1071,3 +1335,80 @@ export function withPrivilegedWriter<T>(db: Database, operation: () => T): T {
1071
1335
  throw error;
1072
1336
  }
1073
1337
  }
1338
+
1339
+ // Process-local admission only; SQLite remains the cross-process writer arbiter.
1340
+ const indexCoordinationKey = Symbol.for("magic-context.index-foreground-coordination.v1");
1341
+ const indexCoordinationHeap = globalThis as unknown as Record<
1342
+ symbol,
1343
+ SharedArrayBuffer | undefined
1344
+ >;
1345
+ const indexCoordinationBuffer =
1346
+ indexCoordinationHeap[indexCoordinationKey] ??
1347
+ new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT);
1348
+ indexCoordinationHeap[indexCoordinationKey] = indexCoordinationBuffer;
1349
+ const indexCoordinationState = new Int32Array(indexCoordinationBuffer);
1350
+ // Slots: foreground interest, current maintenance owner, next owner token.
1351
+
1352
+ export function indexCoordinationInput(): {
1353
+ indexCoordination: SharedArrayBuffer;
1354
+ indexOwner: number;
1355
+ } {
1356
+ return {
1357
+ indexCoordination: indexCoordinationBuffer,
1358
+ indexOwner: Atomics.add(indexCoordinationState, 2, 1) + 1,
1359
+ };
1360
+ }
1361
+
1362
+ const yieldIndexTurn = () => new Promise<void>((resolve) => setTimeout(resolve, 1));
1363
+
1364
+ /** Announce interest before waiting, so no maintenance chunk overtakes a foreground turn. */
1365
+ function reserveForegroundSqliteTurn(): (() => void) | Promise<() => void> {
1366
+ Atomics.add(indexCoordinationState, 0, 1);
1367
+ const release = () => {
1368
+ Atomics.sub(indexCoordinationState, 0, 1);
1369
+ Atomics.notify(indexCoordinationState, 0);
1370
+ };
1371
+ if (Atomics.load(indexCoordinationState, 1) === 0) return release;
1372
+ return (async () => {
1373
+ while (Atomics.load(indexCoordinationState, 1) !== 0) await yieldIndexTurn();
1374
+ return release;
1375
+ })();
1376
+ }
1377
+
1378
+ export function releaseIndexOwner(input: {
1379
+ indexCoordination?: SharedArrayBuffer;
1380
+ indexOwner?: number;
1381
+ }): void {
1382
+ if (!input.indexCoordination || !input.indexOwner) return;
1383
+ const shared = new Int32Array(input.indexCoordination);
1384
+ Atomics.compareExchange(shared, 1, input.indexOwner, 0);
1385
+ Atomics.notify(shared, 1);
1386
+ }
1387
+
1388
+ /** One synchronous transaction per permit; release the permit before yielding. */
1389
+ export async function withIndexChunk<T>(
1390
+ input: { indexCoordination?: SharedArrayBuffer; indexOwner?: number },
1391
+ operation: () => T,
1392
+ ): Promise<T> {
1393
+ if (!input.indexCoordination || !input.indexOwner) return operation();
1394
+ const shared = new Int32Array(input.indexCoordination);
1395
+ for (;;) {
1396
+ if (
1397
+ Atomics.load(shared, 0) === 0 &&
1398
+ Atomics.compareExchange(shared, 1, 0, input.indexOwner) === 0
1399
+ ) {
1400
+ if (Atomics.load(shared, 0) === 0) break;
1401
+ releaseIndexOwner(input);
1402
+ }
1403
+ await yieldIndexTurn();
1404
+ }
1405
+ try {
1406
+ return await operation();
1407
+ } finally {
1408
+ releaseIndexOwner(input);
1409
+ }
1410
+ }
1411
+
1412
+ export async function yieldIndexChunk(): Promise<void> {
1413
+ await yieldIndexTurn();
1414
+ }
@@ -18,12 +18,38 @@
18
18
  * signing), build a separate utility — do NOT widen this contract.
19
19
  */
20
20
  export function stableStringify(value: unknown, seen = new WeakSet<object>()): string {
21
- if (value === undefined) return "undefined";
22
- if (value === null || typeof value !== "object") return JSON.stringify(value) ?? String(value);
23
- if (seen.has(value)) return '"[Circular]"';
21
+ // Collect pieces and join once. Joining at every level copied each nested
22
+ // value's text once per enclosing level, so a large value N levels deep cost
23
+ // N copies of itself.
24
+ const pieces: string[] = [];
25
+ writeStable(value, seen, pieces);
26
+ return pieces.join("");
27
+ }
28
+
29
+ function writeStable(value: unknown, seen: WeakSet<object>, pieces: string[]): void {
30
+ if (value === undefined) {
31
+ pieces.push("undefined");
32
+ return;
33
+ }
34
+ if (value === null || typeof value !== "object") {
35
+ pieces.push(JSON.stringify(value) ?? String(value));
36
+ return;
37
+ }
38
+ // Any object met a second time, shared or cyclic, is written as "[Circular]".
39
+ if (seen.has(value)) {
40
+ pieces.push('"[Circular]"');
41
+ return;
42
+ }
24
43
  seen.add(value);
25
44
  if (Array.isArray(value)) {
26
- return `[${value.map((item) => stableStringify(item, seen)).join(",")}]`;
45
+ pieces.push("[");
46
+ for (let index = 0; index < value.length; index += 1) {
47
+ if (index > 0) pieces.push(",");
48
+ // A hole writes nothing between its commas, as joining a mapped array did.
49
+ if (index in value) writeStable(value[index], seen, pieces);
50
+ }
51
+ pieces.push("]");
52
+ return;
27
53
  }
28
54
  // Code-point sort (NOT localeCompare). Stable across runtimes/locales.
29
55
  const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => {
@@ -31,7 +57,11 @@ export function stableStringify(value: unknown, seen = new WeakSet<object>()): s
31
57
  if (a > b) return 1;
32
58
  return 0;
33
59
  });
34
- return `{${entries
35
- .map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child, seen)}`)
36
- .join(",")}}`;
60
+ pieces.push("{");
61
+ entries.forEach(([key, child], index) => {
62
+ if (index > 0) pieces.push(",");
63
+ pieces.push(JSON.stringify(key), ":");
64
+ writeStable(child, seen, pieces);
65
+ });
66
+ pieces.push("}");
37
67
  }
@@ -0,0 +1,50 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { withStorageFileMutex } from "./storage-file-mutex";
7
+
8
+ const directories: string[] = [];
9
+ const newDirectory = () => {
10
+ const directory = mkdtempSync(join(tmpdir(), "mmc-storage-file-mutex-"));
11
+ directories.push(directory);
12
+ return directory;
13
+ };
14
+ afterAll(() => {
15
+ for (const directory of directories) rmSync(directory, { recursive: true, force: true });
16
+ });
17
+
18
+ describe("withStorageFileMutex", () => {
19
+ test("serializes two critical sections", () => {
20
+ const directory = newDirectory();
21
+ const orderPath = join(directory, "order");
22
+ withStorageFileMutex(directory, () => {
23
+ // Only our own contender file may remain while we hold the mutex.
24
+ const locks = readdirSync(directory).filter((name) => name.endsWith(".lock"));
25
+ expect(locks.length).toBe(1);
26
+ writeFileSync(orderPath, "first");
27
+ });
28
+ withStorageFileMutex(directory, () => {
29
+ expect(readFileSync(orderPath, "utf8")).toBe("first");
30
+ writeFileSync(orderPath, "first,second");
31
+ });
32
+ expect(readFileSync(orderPath, "utf8")).toBe("first,second");
33
+ expect(readdirSync(directory).filter((name) => name.endsWith(".lock")).length).toBe(0);
34
+ });
35
+
36
+ test("reclaims a dead holder's ticket", () => {
37
+ const directory = newDirectory();
38
+ const deadName = `0-${randomUUID()}.lock`;
39
+ const ticket = { choosing: false, number: 999, birth: null };
40
+ const checksum = createHash("sha256")
41
+ .update(JSON.stringify([deadName, ticket.choosing, ticket.number, ticket.birth]))
42
+ .digest("hex");
43
+ writeFileSync(join(directory, deadName), JSON.stringify({ ...ticket, checksum }));
44
+ // pid 0 is never alive, so the stale ticket must be reclaimed and the
45
+ // critical section must run despite the holder's high ticket number.
46
+ const value = withStorageFileMutex(directory, () => 42);
47
+ expect(value).toBe(42);
48
+ expect(readdirSync(directory).filter((name) => name.endsWith(".lock")).length).toBe(0);
49
+ });
50
+ });