@retaw106/mmc-oc 2.4.0 → 2.5.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 (132) hide show
  1. package/dist/{context-c72brxe5.js → context-xx09sh1g.js} +1279 -408
  2. package/dist/features/magic-context/message-index-async.d.ts +6 -2
  3. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  4. package/dist/features/magic-context/message-index.d.ts +5 -8
  5. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  6. package/dist/features/magic-context/search.d.ts +5 -1
  7. package/dist/features/magic-context/search.d.ts.map +1 -1
  8. package/dist/features/magic-context/storage-db.d.ts +2 -0
  9. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  10. package/dist/features/magic-context/storage-host-instruction-retention.d.ts +4 -0
  11. package/dist/features/magic-context/storage-host-instruction-retention.d.ts.map +1 -0
  12. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  13. package/dist/features/magic-context/storage-meta-shared.d.ts +2 -2
  14. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  15. package/dist/features/magic-context/storage-session-tables.d.ts +9 -1
  16. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  17. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  18. package/dist/hooks/magic-context/auto-search-hint.d.ts +3 -0
  19. package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
  20. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  21. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  22. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -2
  23. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  24. package/dist/hooks/magic-context/diagnostic-log-state.d.ts +4 -0
  25. package/dist/hooks/magic-context/diagnostic-log-state.d.ts.map +1 -0
  26. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/file-token-coverage.d.ts +11 -0
  28. package/dist/hooks/magic-context/file-token-coverage.d.ts.map +1 -0
  29. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/host-served-rows.d.ts +10 -0
  31. package/dist/hooks/magic-context/host-served-rows.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/inject-compartments.d.ts +1 -1
  33. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/lkg-slot.d.ts +1 -1
  37. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/nudge-evaluation-log.d.ts +3 -0
  39. package/dist/hooks/magic-context/nudge-evaluation-log.d.ts.map +1 -0
  40. package/dist/hooks/magic-context/pass-outcome.d.ts +2 -0
  41. package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/protected-tail-boundary.d.ts +2 -0
  44. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +3 -3
  47. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/system-date-refresh.d.ts +3 -2
  49. package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts +4 -0
  52. package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts.map +1 -0
  53. package/dist/hooks/magic-context/tag-content-primitives.d.ts +1 -1
  54. package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/tag-messages.d.ts +15 -2
  56. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/text-complete.d.ts +1 -1
  58. package/dist/hooks/magic-context/text-complete.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/todo-view.d.ts +2 -2
  60. package/dist/hooks/magic-context/tool-drop-target.d.ts +3 -2
  61. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +1 -0
  63. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +6 -0
  65. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/transform-stage-logger.d.ts +3 -0
  67. package/dist/hooks/magic-context/transform-stage-logger.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/transform.d.ts +4 -2
  69. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  70. package/dist/{index-kahkxcqw.js → index-0d40b4q8.js} +14740 -14292
  71. package/dist/{index-48e9znrc.js → index-1vxffj5j.js} +75 -22
  72. package/dist/{index-ykq24x1n.js → index-bpjjcdy9.js} +1 -1
  73. package/dist/{index-r7s7d350.js → index-k25thxq7.js} +1 -1
  74. package/dist/{index-27ddyx39.js → index-mjf0p7yq.js} +1 -1
  75. package/dist/{index-dwh99z66.js → index-pte3cfs9.js} +194 -5
  76. package/dist/index.js +2 -2
  77. package/dist/{message-time-backfill-j96x1sab.js → message-time-backfill-g9py24yt.js} +1 -1
  78. package/dist/plugin/subagent-tool-policy.d.ts +4 -0
  79. package/dist/plugin/subagent-tool-policy.d.ts.map +1 -1
  80. package/dist/{read-session-chunk-t8bg6wkz.js → read-session-chunk-eb3fy45y.js} +3 -3
  81. package/dist/{recomp-orchestrator-k760qy1n.js → recomp-orchestrator-mmpy5cb7.js} +6 -6
  82. package/dist/{send-session-notification-njjf3b4s.js → send-session-notification-zpqs213d.js} +3 -3
  83. package/dist/shared/model-suggestion-retry.d.ts +2 -0
  84. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  85. package/dist/shared/rpc-discovery-cleanup.d.ts +3 -0
  86. package/dist/shared/rpc-discovery-cleanup.d.ts.map +1 -0
  87. package/dist/shared/rpc-server.d.ts +2 -0
  88. package/dist/shared/rpc-server.d.ts.map +1 -1
  89. package/dist/shared/search-text.d.ts +8 -0
  90. package/dist/shared/search-text.d.ts.map +1 -0
  91. package/dist/shared/sqlite.d.ts +6 -1
  92. package/dist/shared/sqlite.d.ts.map +1 -1
  93. package/dist/shared/storage-file-mutex.d.ts.map +1 -1
  94. package/dist/shared/system-directive.d.ts +2 -0
  95. package/dist/shared/system-directive.d.ts.map +1 -1
  96. package/dist/v2/fold/native-replay.d.ts +11 -2
  97. package/dist/v2/fold/native-replay.d.ts.map +1 -1
  98. package/dist/v2/fold/policy.d.ts.map +1 -1
  99. package/dist/v2/fold/restore.d.ts.map +1 -1
  100. package/dist/v2/hooks/context.d.ts +1 -1
  101. package/dist/v2/hooks/context.d.ts.map +1 -1
  102. package/dist/v2/hooks/generate.d.ts +36 -1
  103. package/dist/v2/hooks/generate.d.ts.map +1 -1
  104. package/dist/v2/hooks/lkg-system.d.ts +35 -4
  105. package/dist/v2/hooks/lkg-system.d.ts.map +1 -1
  106. package/dist/v2/hooks/payload.d.ts +10 -1
  107. package/dist/v2/hooks/payload.d.ts.map +1 -1
  108. package/dist/v2/hooks/store.d.ts +2 -0
  109. package/dist/v2/hooks/store.d.ts.map +1 -1
  110. package/dist/v2/hooks/types.d.ts +12 -0
  111. package/dist/v2/hooks/types.d.ts.map +1 -1
  112. package/dist/v2/server.js +9033 -7461
  113. package/dist/v2/store-reader.d.ts +11 -0
  114. package/dist/v2/store-reader.d.ts.map +1 -1
  115. package/dist/web/main.js +229 -8
  116. package/dist/{wrapup-orchestrator-b2gswatj.js → wrapup-orchestrator-jq589pgr.js} +6 -6
  117. package/package.json +1 -1
  118. package/src/config/latch-permanence-guard.test.ts +4 -0
  119. package/src/shared/model-suggestion-retry.test.ts +52 -0
  120. package/src/shared/model-suggestion-retry.ts +8 -3
  121. package/src/shared/rpc-discovery-cleanup.test.ts +125 -0
  122. package/src/shared/rpc-discovery-cleanup.ts +57 -0
  123. package/src/shared/rpc-server.ts +36 -13
  124. package/src/shared/search-text.test.ts +30 -0
  125. package/src/shared/search-text.ts +62 -0
  126. package/src/shared/sqlite-turn-deadline.test.ts +286 -0
  127. package/src/shared/sqlite.ts +237 -9
  128. package/src/shared/storage-file-mutex.test.ts +51 -2
  129. package/src/shared/storage-file-mutex.ts +11 -1
  130. package/src/shared/system-directive.test.ts +37 -0
  131. package/src/shared/system-directive.ts +19 -1
  132. package/src/shared/tag-transcript.ts +16 -4
@@ -393,11 +393,187 @@ export function getTagIdentityWriteGeneration(): number {
393
393
  return tagIdentityWriteGeneration;
394
394
  }
395
395
 
396
- /** Route native Bun and Node transaction entry through the same acquisition retry.
397
- * Native Bun transaction wrappers execute BEGIN internally, bypassing an exec override,
398
- * so both runtimes use this small synchronous wrapper instead. Writable handles
399
- * default to IMMEDIATE; explicit deferred calls and readonly handles keep read
400
- * snapshots without acquiring the writer lock. */
396
+ /** Only Pi serving connections need a mirror of their native busy handler. */
397
+ const busyTimeoutStates = new WeakMap<Database, { current: number }>();
398
+ const BUSY_TIMEOUT_QUERY =
399
+ /^\s*PRAGMA\s+(?:(?:\w+|"[^"]+"|`[^`]+`|\[[^\]]+\])\s*\.\s*)?(?:busy_timeout|"busy_timeout"|`busy_timeout`|\[busy_timeout\])\s*;?\s*$/i;
400
+ const BUSY_TIMEOUT_SETTER =
401
+ /^\s*PRAGMA\s+(?:(?:\w+|"[^"]+"|`[^`]+`|\[[^\]]+\])\s*\.\s*)?(?:busy_timeout|"busy_timeout"|`busy_timeout`|\[busy_timeout\])\s*(?==|\()/i;
402
+
403
+ function literalBusyTimeout(sql: string): number | undefined {
404
+ const head = BUSY_TIMEOUT_SETTER.exec(sql);
405
+ if (!head) return undefined;
406
+ const match = /^(?:=\s*|\(\s*)(?:'(-?\d+)'|"(-?\d+)"|(-?\d+))\s*\)?\s*;?\s*$/.exec(
407
+ sql.slice(head[0].length),
408
+ );
409
+ if (!match) return undefined;
410
+ const value = Number(match[1] ?? match[2] ?? match[3]);
411
+ return value < 0 ? 0 : value <= 0x7fffffff ? value : undefined;
412
+ }
413
+
414
+ /** Split timeout-setting scripts without mistaking quoted text or comments for pragmas. */
415
+ function busyTimeoutStatements(sql: string): string[] {
416
+ const statements: string[] = [];
417
+ let statement = "";
418
+ let quote = "";
419
+ for (let i = 0; i < sql.length; i++) {
420
+ const char = sql[i];
421
+ if (quote) {
422
+ statement += char;
423
+ if (char === quote) {
424
+ if (sql[i + 1] === quote) statement += sql[++i];
425
+ else quote = "";
426
+ }
427
+ } else if (char === "'" || char === '"' || char === "`" || char === "[") {
428
+ quote = char === "[" ? "]" : char;
429
+ statement += char;
430
+ } else if (char === "-" && sql[i + 1] === "-") {
431
+ while (i + 1 < sql.length && sql[i + 1] !== "\n") i++;
432
+ statement += " ";
433
+ } else if (char === "/" && sql[i + 1] === "*") {
434
+ i += 2;
435
+ while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) i++;
436
+ i++;
437
+ statement += " ";
438
+ } else if (char === ";") {
439
+ statements.push(statement);
440
+ statement = "";
441
+ } else statement += char;
442
+ }
443
+ statements.push(statement);
444
+ return statements.filter((part) => part.trim().length > 0);
445
+ }
446
+
447
+ /** Install the native deadline layer only on a connection that will serve Pi turns. */
448
+ function installNativeDeadlineRouting(db: BetterSqlite3.Database): void {
449
+ const rawExec = db.exec.bind(db);
450
+ const rawPrepare = db.prepare.bind(db);
451
+ const readTimeout = (): number => {
452
+ const statement = rawPrepare("PRAGMA busy_timeout");
453
+ try {
454
+ return Object.values(statement.get() as Record<string, number>)[0];
455
+ } finally {
456
+ (statement as Partial<FinalizableStatement>).finalize?.();
457
+ }
458
+ };
459
+ const timeoutState = { current: readTimeout() };
460
+ busyTimeoutStates.set(db, timeoutState);
461
+ const setBusyTimeout = (timeout: number): void => {
462
+ rawExec(`PRAGMA busy_timeout=${timeout}`);
463
+ timeoutState.current = timeout;
464
+ };
465
+ const withinDeadline = <T>(operation: () => T): T => {
466
+ const deadline = nativeWaitScope?.getStore();
467
+ if (!deadline?.active || !deadline.remainingMs) return operation();
468
+ const remaining = deadline.remainingMs();
469
+ if (remaining === Infinity) return operation();
470
+ const previous = timeoutState.current;
471
+ const timeout = Math.min(previous, Math.max(0, Math.floor(remaining)));
472
+ if (timeout === previous) return operation();
473
+ setBusyTimeout(timeout);
474
+ try {
475
+ return operation();
476
+ } finally {
477
+ setBusyTimeout(previous);
478
+ }
479
+ };
480
+ Object.defineProperty(db, "exec", {
481
+ configurable: true,
482
+ writable: true,
483
+ value: (sql: string) => {
484
+ const parts = /\bbusy_timeout\b/i.test(sql) ? busyTimeoutStatements(sql) : [];
485
+ if (parts.length === 1 && BUSY_TIMEOUT_QUERY.test(parts[0])) return rawExec(sql);
486
+ const singleSetter = parts.length === 1 && BUSY_TIMEOUT_SETTER.test(parts[0]);
487
+ const setting = singleSetter ? literalBusyTimeout(parts[0]) : undefined;
488
+ if (singleSetter) {
489
+ const result = rawExec(sql);
490
+ timeoutState.current = setting ?? readTimeout();
491
+ return result;
492
+ }
493
+ if (!/\bbusy_timeout\b/i.test(sql)) return withinDeadline(() => rawExec(sql));
494
+ const setters = parts.filter((part) => BUSY_TIMEOUT_SETTER.test(part));
495
+ return withinDeadline(() => {
496
+ try {
497
+ const result = rawExec(sql);
498
+ if (setters.length)
499
+ timeoutState.current =
500
+ literalBusyTimeout(setters[setters.length - 1]) ?? readTimeout();
501
+ return result;
502
+ } catch (error) {
503
+ // exec can apply a timeout-setting prefix before a later statement
504
+ // fails. Only an ambiguous setting needs a native readback.
505
+ if (setters.length) timeoutState.current = readTimeout();
506
+ throw error;
507
+ }
508
+ });
509
+ },
510
+ });
511
+ // Timeout introspection cannot wait on a database lock. Its getter must
512
+ // report the configured timeout, not a temporary cap that a caller might save.
513
+ const nativePrepare = (sql: string) =>
514
+ BUSY_TIMEOUT_QUERY.test(sql) || BUSY_TIMEOUT_SETTER.test(sql)
515
+ ? rawPrepare(sql)
516
+ : withinDeadline(() => rawPrepare(sql));
517
+ Object.defineProperty(db, "prepare", {
518
+ configurable: true,
519
+ writable: true,
520
+ value: (sql: string) => {
521
+ // biome-ignore lint/suspicious/noExplicitAny: Bun adds values() to the common statement interface.
522
+ const statement: any = nativePrepare(sql);
523
+ const normalized = /\bbusy_timeout\b/i.test(sql)
524
+ ? (busyTimeoutStatements(sql)[0] ?? sql)
525
+ : sql;
526
+ const setting = literalBusyTimeout(normalized);
527
+ const timeoutSetter = BUSY_TIMEOUT_SETTER.test(normalized);
528
+ const timeoutQuery = BUSY_TIMEOUT_QUERY.test(normalized);
529
+ // SQLite applies this PRAGMA while preparing it, before get/run.
530
+ if (timeoutSetter) timeoutState.current = setting ?? readTimeout();
531
+ for (const method of ["run", "get", "all", "values"] as const) {
532
+ const execute = statement[method]?.bind(statement);
533
+ if (!execute) continue;
534
+ Object.defineProperty(statement, method, {
535
+ configurable: true,
536
+ // biome-ignore lint/suspicious/noExplicitAny: retain native binding overloads.
537
+ value: (...args: any[]) => {
538
+ if (timeoutQuery) return execute(...args);
539
+ if (!timeoutSetter) return withinDeadline(() => execute(...args));
540
+ const result = execute(...args);
541
+ timeoutState.current = readTimeout();
542
+ return result;
543
+ },
544
+ });
545
+ }
546
+ if (typeof statement.iterate === "function") {
547
+ const iterate = statement.iterate.bind(statement);
548
+ Object.defineProperty(statement, "iterate", {
549
+ configurable: true,
550
+ // biome-ignore lint/suspicious/noExplicitAny: retain native binding overloads.
551
+ value: (...args: any[]) => {
552
+ const iterator =
553
+ !timeoutSetter && !timeoutQuery
554
+ ? withinDeadline(() => iterate(...args))
555
+ : iterate(...args);
556
+ const next = iterator.next.bind(iterator);
557
+ if (timeoutSetter) timeoutState.current = readTimeout();
558
+ iterator.next = () => {
559
+ if (timeoutQuery) return next();
560
+ if (!timeoutSetter) return withinDeadline(next);
561
+ const result = next();
562
+ timeoutState.current = readTimeout();
563
+ return result;
564
+ };
565
+ return iterator;
566
+ },
567
+ });
568
+ }
569
+ return statement;
570
+ },
571
+ });
572
+ }
573
+
574
+ /** Route native Bun and Node transactions through the same acquisition retry.
575
+ * Native Bun wrappers execute BEGIN internally, so both runtimes use this wrapper.
576
+ * Deadline routing, when selected, is installed underneath it at connection open. */
401
577
  function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean): void {
402
578
  const nativeExec = db.exec.bind(db);
403
579
  Object.defineProperty(db, "exec", {
@@ -555,6 +731,7 @@ function trackSqliteConnection(
555
731
  db: BetterSqlite3.Database,
556
732
  filename: unknown,
557
733
  options: unknown,
734
+ turnDeadlines = false,
558
735
  ): BetterSqlite3.Database {
559
736
  const originalClose = db.close.bind(db) as (...args: unknown[]) => unknown;
560
737
  const sequence = nextSqliteConnectionSequence++;
@@ -572,6 +749,7 @@ function trackSqliteConnection(
572
749
  ((options as { readonly?: unknown }).readonly === true ||
573
750
  (options as { readOnly?: unknown }).readOnly === true),
574
751
  };
752
+ if (turnDeadlines) installNativeDeadlineRouting(db);
575
753
  installTransactionRouting(db, metadata.readonly);
576
754
  Object.defineProperty(db, "close", {
577
755
  configurable: true,
@@ -654,6 +832,18 @@ function buildNodeSqliteDatabaseClass(DatabaseSync: any): typeof BetterSqlite3 {
654
832
 
655
833
  export const Database: typeof BetterSqlite3 = TrackedDatabase;
656
834
 
835
+ /** Open a Pi foreground connection with native-deadline routing before statements are prepared. */
836
+ export function createSqliteTurnDatabase(
837
+ filename?: string | Buffer,
838
+ options?: BetterSqlite3.Options,
839
+ ): Database {
840
+ return trackSqliteConnection(new DatabaseImpl(filename, options), filename, options, true);
841
+ }
842
+
843
+ export function hasSqliteTurnDeadlineRouting(db: Database): boolean {
844
+ return busyTimeoutStates.has(db);
845
+ }
846
+
657
847
  /** The constructor path, without querying SQLite on the prompt thread. */
658
848
  export function getSqliteDatabasePath(db: Database): string | null {
659
849
  for (const connection of trackedSqliteConnections.values()) {
@@ -678,6 +868,10 @@ function pragmaValue(db: BetterSqlite3.Database, name: string): unknown {
678
868
  }
679
869
 
680
870
  function pragmaNumber(db: BetterSqlite3.Database, name: string): number | null {
871
+ if (name === "busy_timeout") {
872
+ const state = busyTimeoutStates.get(db);
873
+ if (state) return state.current;
874
+ }
681
875
  try {
682
876
  const value = pragmaValue(db, name);
683
877
  return typeof value === "number" && Number.isFinite(value) ? value : null;
@@ -884,6 +1078,38 @@ export function prepareCachedStatement(db: Database, sql: string): Statement {
884
1078
  }
885
1079
 
886
1080
  const privilegeDepth = new WeakMap<Database, number>();
1081
+ interface NativeWaitLease {
1082
+ active: boolean;
1083
+ remainingMs?: () => number;
1084
+ }
1085
+ let nativeWaitScope: AsyncLocalStorage<NativeWaitLease | undefined> | undefined;
1086
+
1087
+ function releaseNativeWaitLease(lease: NativeWaitLease): void {
1088
+ lease.active = false;
1089
+ // Detached descendants can retain their async-local lease after the turn.
1090
+ // Do not retain the completed request through its deadline callback.
1091
+ lease.remainingMs = undefined;
1092
+ }
1093
+
1094
+ /** Bound native lock waits for an awaited turn, without changing background timeouts. */
1095
+ export function withSqliteTurnDeadline<T>(remainingMs: () => number, operation: () => T): T {
1096
+ const scope = (nativeWaitScope ??= new AsyncLocalStorage<NativeWaitLease | undefined>());
1097
+ const lease: NativeWaitLease = { active: true, remainingMs };
1098
+ return scope.run(lease, () => {
1099
+ try {
1100
+ const result = operation();
1101
+ if (result && typeof (result as { then?: unknown }).then === "function")
1102
+ return Promise.resolve(result).finally(
1103
+ releaseNativeWaitLease.bind(null, lease),
1104
+ ) as T;
1105
+ releaseNativeWaitLease(lease);
1106
+ return result;
1107
+ } catch (error) {
1108
+ releaseNativeWaitLease(lease);
1109
+ throw error;
1110
+ }
1111
+ });
1112
+ }
887
1113
  const transformPassScope = new AsyncLocalStorage<
888
1114
  | {
889
1115
  active: boolean;
@@ -897,7 +1123,8 @@ const backgroundWriterScope = new AsyncLocalStorage<boolean>();
897
1123
 
898
1124
  /** Opt in only a writer that can safely defer its work after a busy acquisition. */
899
1125
  export function withSqliteBackgroundWriter<T>(operation: () => T): T {
900
- return backgroundWriterScope.run(true, operation);
1126
+ if (!nativeWaitScope?.getStore()) return backgroundWriterScope.run(true, operation);
1127
+ return nativeWaitScope.run(undefined, () => backgroundWriterScope.run(true, operation));
901
1128
  }
902
1129
 
903
1130
  /** Only an awaited foreground transform may spend the extra acquisition budget.
@@ -934,7 +1161,8 @@ export function withSqliteTransformPass<T>(operation: () => T): T {
934
1161
 
935
1162
  /** Start detached maintenance work without borrowing a foreground pass's budget. */
936
1163
  export function withoutSqliteTransformPass<T>(operation: () => T): T {
937
- return transformPassScope.run(undefined, operation);
1164
+ if (!nativeWaitScope?.getStore()) return transformPassScope.run(undefined, operation);
1165
+ return nativeWaitScope.run(undefined, () => transformPassScope.run(undefined, operation));
938
1166
  }
939
1167
 
940
1168
  /** Attach the adapter's existing generation/cancellation/deadline guard to writes. */
@@ -1233,7 +1461,7 @@ export async function withAsyncPrivilegedWriter<T>(
1233
1461
  operation: () => T,
1234
1462
  options: {
1235
1463
  signal?: AbortSignal;
1236
- beforeRetry?: (error: SqliteAcquisitionBusyError) => void;
1464
+ beforeRetry?: (error: SqliteAcquisitionBusyError) => void | Promise<void>;
1237
1465
  jitter?: boolean;
1238
1466
  budgetMs?: number;
1239
1467
  assertCurrentPass?: () => void;
@@ -1276,7 +1504,7 @@ export async function withAsyncPrivilegedWriter<T>(
1276
1504
  if (entered || !(error instanceof SqliteAcquisitionBusyError)) throw error;
1277
1505
  // Adapters may serve a validated saved request instead of waiting.
1278
1506
  // This runs only after a failed BEGIN, never after callback writes.
1279
- options.beforeRetry?.(error);
1507
+ await options.beforeRetry?.(error);
1280
1508
  const elapsed = performance.now() - started;
1281
1509
  if (elapsed >= budgetMs) {
1282
1510
  reportSqliteDiagnostic(
@@ -1,9 +1,10 @@
1
- import { afterAll, describe, expect, test } from "bun:test";
1
+ import { afterAll, describe, expect, spyOn, test } from "bun:test";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
- import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { mkdtempSync, readdirSync, readFileSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { withStorageFileMutex } from "./storage-file-mutex";
7
+ import * as permissions from "./storage-permissions";
7
8
 
8
9
  const directories: string[] = [];
9
10
  const newDirectory = () => {
@@ -16,6 +17,54 @@ afterAll(() => {
16
17
  });
17
18
 
18
19
  describe("withStorageFileMutex", () => {
20
+ for (const removals of [1, 2, 3]) {
21
+ test(`ticket creation tolerates at most two directory cleanup races (${removals})`, () => {
22
+ const directory = newDirectory();
23
+ const write = permissions.writeStorageFileSync;
24
+ let attempts = 0;
25
+ let worked = false;
26
+ const writer = spyOn(permissions, "writeStorageFileSync").mockImplementation(
27
+ (...args) => {
28
+ if (attempts++ < removals) rmdirSync(directory);
29
+ return write(...args);
30
+ },
31
+ );
32
+ try {
33
+ const acquire = () =>
34
+ withStorageFileMutex(directory, () => {
35
+ worked = true;
36
+ return "acquired";
37
+ });
38
+ if (removals <= 2) {
39
+ expect(acquire()).toBe("acquired");
40
+ // Includes the successful contender's atomic ticket rewrite.
41
+ expect(attempts).toBe(removals + 2);
42
+ expect(readdirSync(directory)).toEqual([]);
43
+ } else {
44
+ expect(acquire).toThrow("ENOENT");
45
+ expect(attempts).toBe(3);
46
+ }
47
+ expect(worked).toBe(removals <= 2);
48
+ } finally {
49
+ writer.mockRestore();
50
+ }
51
+ });
52
+ }
53
+
54
+ test("does not retry other ticket write failures", () => {
55
+ const directory = newDirectory();
56
+ const error = Object.assign(new Error("denied"), { code: "EACCES" });
57
+ const writer = spyOn(permissions, "writeStorageFileSync").mockImplementation(() => {
58
+ throw error;
59
+ });
60
+ try {
61
+ expect(() => withStorageFileMutex(directory, () => 42)).toThrow(error);
62
+ expect(writer).toHaveBeenCalledTimes(1);
63
+ } finally {
64
+ writer.mockRestore();
65
+ }
66
+ });
67
+
19
68
  test("serializes two critical sections", () => {
20
69
  const directory = newDirectory();
21
70
  const orderPath = join(directory, "order");
@@ -68,7 +68,17 @@ export function withStorageFileMutex<T>(directory: string, work: () => T, timeou
68
68
  const ticket: Ticket = { choosing: true, number: 0, birth, checksum: "" };
69
69
  const encodedTicket = () =>
70
70
  JSON.stringify({ ...ticket, checksum: ticketChecksum(name, ticket) });
71
- writeStorageFileSync(ownPath, encodedTicket(), { flag: "wx", forcePrivate: true });
71
+ for (let attempt = 0; ; attempt++) {
72
+ try {
73
+ writeStorageFileSync(ownPath, encodedTicket(), { flag: "wx", forcePrivate: true });
74
+ break;
75
+ } catch (error) {
76
+ // A session cleanup can remove an empty directory between mkdir and
77
+ // ticket creation. No ticket exists yet, so retry before contending.
78
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT" || attempt >= 2) throw error;
79
+ ensurePrivateStorageDirectorySync(directory);
80
+ }
81
+ }
72
82
  const deadline = performance.now() + timeoutMs;
73
83
  const peers = (): [string, Ticket | null][] => {
74
84
  const found: [string, Ticket | null][] = [];
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { removeNestedBlocks, removeSystemReminders } from "./system-directive";
3
+
4
+ describe("removeSystemReminders", () => {
5
+ test("removes nested reminders without leaking the outer tail", () => {
6
+ expect(
7
+ removeSystemReminders(
8
+ "before <system-reminder>outer <system-reminder>inner</system-reminder> tail</system-reminder> after",
9
+ ),
10
+ ).toBe("before after");
11
+ });
12
+
13
+ test("removes unclosed reminders through the end", () => {
14
+ expect(
15
+ removeSystemReminders(
16
+ " keep <system-reminder>hidden <system-reminder>nested</system-reminder> tail",
17
+ ),
18
+ ).toBe("keep");
19
+ });
20
+
21
+ test("handles attributes, case, self-closing tags and adjacent blocks", () => {
22
+ expect(
23
+ removeSystemReminders(
24
+ 'a<SYSTEM-REMINDER id="x">hidden</SYSTEM-REMINDER><system-reminder/>b<system-reminder>hidden</system-reminder>c',
25
+ ),
26
+ ).toBe("abc");
27
+ });
28
+
29
+ test("preserves ordinary content and other tags", () => {
30
+ expect(removeSystemReminders(" <other>visible</other> ")).toBe("<other>visible</other>");
31
+ });
32
+ });
33
+
34
+ test("removeNestedBlocks tracks multiple tag names and only closes the matching top", () => {
35
+ expect(removeNestedBlocks("a<one>x<two>y</one>z</two>tail</one>b", ["one", "two"])).toBe("ab");
36
+ expect(removeNestedBlocks(" a<one/>b ", ["one"])).toBe(" ab ");
37
+ });
@@ -5,5 +5,23 @@ export function isSystemDirective(text: string): boolean {
5
5
  }
6
6
 
7
7
  export function removeSystemReminders(text: string): string {
8
- return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, "").trim();
8
+ return removeNestedBlocks(text, ["system-reminder"]).trim();
9
+ }
10
+
11
+ /** Remove matching tagged regions, including nested regions and regions with no closing tag. */
12
+ export function removeNestedBlocks(text: string, names: readonly string[]): string {
13
+ const tags = new RegExp(`<(/?)(${names.join("|")})(?:\\s[^<>]*?)?\\s*(/?)>`, "gi");
14
+ const stack: string[] = [];
15
+ let end = 0;
16
+ let result = "";
17
+ for (const match of text.matchAll(tags)) {
18
+ if (stack.length === 0) result += text.slice(end, match.index);
19
+ const name = match[2].toLowerCase();
20
+ if (match[1]) {
21
+ if (stack[stack.length - 1] === name) stack.pop();
22
+ } else if (!match[3]) stack.push(name);
23
+ end = match.index + match[0].length;
24
+ }
25
+ if (stack.length === 0) result += text.slice(end);
26
+ return result;
9
27
  }
@@ -835,12 +835,17 @@ class ConversationEndGuard {
835
835
  }
836
836
  }
837
837
 
838
- wouldStrand(occurrences: readonly ToolOccurrence[]): boolean {
838
+ wouldStrand(
839
+ occurrences: readonly ToolOccurrence[],
840
+ plannedRemovals?: ReadonlySet<unknown>,
841
+ ): boolean {
839
842
  const end = this.end;
840
843
  if (!end || !occurrences.some((occ) => occ.message === end)) return false;
841
844
  const leaving = new Set(occurrences.map((occ) => partKey(occ.part)));
842
845
  const survives = (part: TranscriptPart) =>
843
- !leaving.has(partKey(part)) && !this.removed.has(partKey(part));
846
+ !leaving.has(partKey(part)) &&
847
+ !this.removed.has(partKey(part)) &&
848
+ !plannedRemovals?.has(partKey(part));
844
849
  return end.info.role === "assistant"
845
850
  ? !end.parts.some((part) => part.kind === "tool_result" && survives(part))
846
851
  : !end.parts.some((part) => partHasContent(part) && survives(part));
@@ -1379,8 +1384,11 @@ function buildAggregateTarget(
1379
1384
  }
1380
1385
  return null;
1381
1386
  },
1382
- wouldStrandConversationEnd(): boolean {
1383
- return conversationEnd?.wouldStrand(occurrences) ?? false;
1387
+ wouldStrandConversationEnd(plannedRemovals): boolean {
1388
+ return conversationEnd?.wouldStrand(occurrences, plannedRemovals) ?? false;
1389
+ },
1390
+ notePlannedRemoval(plannedRemovals): void {
1391
+ for (const occ of occurrences) plannedRemovals.add(partKey(occ.part));
1384
1392
  },
1385
1393
  editMarker(): "truncated" | "absent" {
1386
1394
  // Edit-marker: preserve the tool_use input's filePath + a region
@@ -1443,6 +1451,10 @@ function buildTextTarget(
1443
1451
  return {
1444
1452
  mutationParts: [{ message: { info: message.info, parts: [] }, part }],
1445
1453
  textPrefix,
1454
+ canSetContent(content): boolean {
1455
+ const current = part.getText();
1456
+ return typeof current === "string" && current !== content;
1457
+ },
1446
1458
  setContent(content: string): boolean {
1447
1459
  return part.setText(content);
1448
1460
  },