@cortexkit/opencode-magic-context 0.44.2 → 0.44.4

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 (116) hide show
  1. package/dist/{conflict-warning-hook-1msyvb51.js → conflict-warning-hook-hmpzxep6.js} +3 -3
  2. package/dist/{derive-budgets-gd2270s0.js → derive-budgets-ncwn1jk9.js} +4 -2
  3. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  4. package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
  5. package/dist/features/magic-context/dreamer/lease.d.ts +1 -1
  6. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  7. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  8. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  9. package/dist/features/magic-context/memory/embedding-openai.d.ts +1 -0
  10. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  11. package/dist/features/magic-context/memory/project-identity.d.ts +8 -4
  12. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  13. package/dist/features/magic-context/message-index-async.d.ts +1 -1
  14. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  15. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  16. package/dist/features/magic-context/session-project-storage.d.ts +5 -5
  17. package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
  18. package/dist/features/magic-context/storage-db.d.ts +6 -1
  19. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-embedding-measurements.d.ts.map +1 -1
  21. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -1
  22. package/dist/hooks/magic-context/compartment-render-epoch.d.ts +12 -1
  23. package/dist/hooks/magic-context/compartment-render-epoch.d.ts.map +1 -1
  24. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -1
  25. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  26. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/compartment-runner-types.d.ts +1 -0
  28. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  29. package/dist/hooks/magic-context/derive-budgets.d.ts +4 -0
  30. package/dist/hooks/magic-context/derive-budgets.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/drop-stale-reduce-calls.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/event-resolvers.d.ts +7 -0
  33. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/inject-compartments.d.ts +5 -0
  37. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/lkg-persist.d.ts +1 -4
  39. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/producer-window-guard.d.ts +4 -1
  42. package/dist/hooks/magic-context/producer-window-guard.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/read-session-chunk.d.ts +4 -2
  44. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/read-session-raw.d.ts +2 -2
  46. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/rust-mode-transform.d.ts +1 -0
  48. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/storage-busy-refusal.d.ts +6 -0
  50. package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/tokenizer-calibration.d.ts +1 -1
  52. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +1 -0
  54. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  56. package/dist/{index-r9vkp4n9.js → index-33q7s9y2.js} +1 -1
  57. package/dist/{index-egkfvhms.js → index-4an2p8qa.js} +80 -14
  58. package/dist/{index-18tjha8g.js → index-638hf49n.js} +1 -1
  59. package/dist/{index-gtzcmv90.js → index-mrc0twf3.js} +12 -6
  60. package/dist/{index-stb5fg12.js → index-msqr3x6m.js} +32 -2
  61. package/dist/{index-y5145x8k.js → index-n96ag4rk.js} +15 -1
  62. package/dist/{index-esxfcz5z.js → index-qm36k34q.js} +56 -13
  63. package/dist/{index-ttdn47aq.js → index-sqp1hz9v.js} +311 -82
  64. package/dist/{index-40s1xz22.js → index-wprsye9w.js} +1 -1
  65. package/dist/{index-3x9caerx.js → index-zx7pmr39.js} +3 -3
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +530 -250
  68. package/dist/plugin/dream-timer.d.ts.map +1 -1
  69. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  70. package/dist/plugin/messages-transform.d.ts.map +1 -1
  71. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  72. package/dist/{read-session-chunk-wmvgnd24.js → read-session-chunk-339hg90t.js} +3 -3
  73. package/dist/{recomp-orchestrator-rk40ff15.js → recomp-orchestrator-0ers6smx.js} +7 -7
  74. package/dist/{send-session-notification-6xsrcxnp.js → send-session-notification-04hvd54c.js} +2 -2
  75. package/dist/shared/rpc-utils.d.ts +7 -0
  76. package/dist/shared/rpc-utils.d.ts.map +1 -1
  77. package/dist/shared/run-token-log.d.ts +13 -0
  78. package/dist/shared/run-token-log.d.ts.map +1 -0
  79. package/dist/shared/sqlite.d.ts +7 -0
  80. package/dist/shared/sqlite.d.ts.map +1 -1
  81. package/dist/shared/transcript.d.ts +2 -0
  82. package/dist/shared/transcript.d.ts.map +1 -1
  83. package/dist/shared/user-answer.d.ts +4 -0
  84. package/dist/shared/user-answer.d.ts.map +1 -0
  85. package/dist/{transform-postprocess-phase-t7xgdhdf.js → transform-postprocess-phase-ttdkrm6r.js} +7 -7
  86. package/dist/v2/hidden-completion.d.ts +7 -1
  87. package/dist/v2/hidden-completion.d.ts.map +1 -1
  88. package/dist/v2/hooks/context.d.ts.map +1 -1
  89. package/dist/v2/hooks/hidden-terminal-error.d.ts +8 -0
  90. package/dist/v2/hooks/hidden-terminal-error.d.ts.map +1 -0
  91. package/dist/v2/hooks/payload.d.ts.map +1 -1
  92. package/dist/v2/hooks/store.d.ts +1 -1
  93. package/dist/v2/hooks/store.d.ts.map +1 -1
  94. package/dist/v2/hooks/tools.d.ts +1 -1
  95. package/dist/v2/hooks/tools.d.ts.map +1 -1
  96. package/dist/v2/server.js +916 -221
  97. package/dist/v2/store-reader.d.ts +2 -1
  98. package/dist/v2/store-reader.d.ts.map +1 -1
  99. package/dist/{wrapup-orchestrator-h8tr10c6.js → wrapup-orchestrator-x0jvvrqn.js} +8 -8
  100. package/package.json +1 -1
  101. package/src/config/latch-permanence-guard.test.ts +4 -0
  102. package/src/shared/data-path.test.ts +1 -1
  103. package/src/shared/models-dev-cache.test.ts +30 -1
  104. package/src/shared/rpc-utils.test.ts +27 -2
  105. package/src/shared/rpc-utils.ts +44 -15
  106. package/src/shared/run-token-log.test.ts +65 -0
  107. package/src/shared/run-token-log.ts +39 -0
  108. package/src/shared/sqlite-acquisition-routing.test.ts +19 -11
  109. package/src/shared/sqlite-pass-scope.test.ts +115 -144
  110. package/src/shared/sqlite.ts +123 -20
  111. package/src/shared/storage-busy.test.ts +34 -57
  112. package/src/shared/tag-transcript.ts +4 -1
  113. package/src/shared/transcript.ts +3 -0
  114. package/src/shared/user-answer.test.ts +40 -0
  115. package/src/shared/user-answer.ts +17 -0
  116. package/src/shared/write-transaction-attribution.test.ts +13 -11
@@ -1,182 +1,153 @@
1
- import { expect, spyOn, test } from "bun:test";
1
+ import { expect, test } from "bun:test";
2
2
  import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import {
6
+ beginSqliteWriterAsync,
6
7
  Database,
7
- withoutSqliteTransformPass,
8
+ isTransientSqliteError,
9
+ SqliteAcquisitionBusyError,
10
+ withAsyncPrivilegedWriter,
8
11
  withPrivilegedWriter,
12
+ withSqliteBackgroundWriter,
9
13
  withSqliteTransformPass,
10
14
  } from "./sqlite";
15
+ import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
11
16
 
12
- test("background privileged acquisition attempts once without backoff", () => {
13
- const db = new Database(":memory:");
14
- const error = Object.assign(new Error("original busy"), { code: "SQLITE_BUSY" });
15
- const exec = spyOn(db, "exec").mockImplementation(() => {
16
- throw error;
17
- });
18
- const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
19
- let callbacks = 0;
17
+ function fixture() {
18
+ const dir = mkdtempSync(join(tmpdir(), "mc-busy-yield-"));
19
+ const path = join(dir, "context.db");
20
+ const db = new Database(path);
21
+ db.exec(
22
+ "PRAGMA journal_mode=WAL; CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
23
+ );
24
+ db.exec("PRAGMA busy_timeout=5000");
25
+ return {
26
+ db,
27
+ path,
28
+ close: () => {
29
+ db.close();
30
+ rmSync(dir, { recursive: true, force: true });
31
+ },
32
+ };
33
+ }
34
+
35
+ test("tool-style transaction outside a pass waits for a brief lock", async () => {
36
+ const { db, path, close } = fixture();
37
+ const locker = await startSqliteWriteLocker(path, 300);
20
38
  try {
21
- expect(() => withPrivilegedWriter(db, () => callbacks++)).toThrow("original busy");
22
- expect(exec).toHaveBeenCalledTimes(1);
23
- expect(wait).not.toHaveBeenCalled();
24
- expect(callbacks).toBe(0);
39
+ const start = performance.now();
40
+ db.transaction(() =>
41
+ db.prepare("INSERT INTO context_privilege_state(id, enabled) VALUES (1, 0)").run(),
42
+ ).immediate();
43
+ expect(performance.now() - start).toBeGreaterThanOrEqual(250);
44
+ expect(db.prepare("SELECT COUNT(*) AS count FROM context_privilege_state").get()).toEqual({
45
+ count: 1,
46
+ });
47
+ expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
25
48
  } finally {
26
- exec.mockRestore();
27
- wait.mockRestore();
28
- db.close();
49
+ await locker.exited;
50
+ close();
29
51
  }
30
- });
52
+ }, 30000);
31
53
 
32
- test("background transaction exhausts one five-second busy timeout", () => {
33
- const dir = mkdtempSync(join(tmpdir(), "mc-background-busy-"));
34
- const path = join(dir, "context.db");
54
+ test("named background acquisition gives up quickly and leaves work for retry", () => {
55
+ const { db, path, close } = fixture();
35
56
  const blocker = new Database(path);
36
- const writer = new Database(path);
37
- blocker.exec("PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT)");
38
- writer.exec("PRAGMA busy_timeout=5000");
39
57
  blocker.exec("BEGIN IMMEDIATE");
40
- let callbacks = 0;
58
+ let calls = 0;
59
+ let retryQueued = false;
41
60
  try {
42
61
  const start = performance.now();
43
- expect(() => writer.transaction(() => callbacks++)()).toThrow();
44
- const elapsed = performance.now() - start;
45
- expect(callbacks).toBe(0);
46
- expect(elapsed).toBeGreaterThanOrEqual(4500);
47
- expect(elapsed).toBeLessThan(7500);
62
+ try {
63
+ withSqliteBackgroundWriter(() => db.transaction(() => calls++)());
64
+ } catch (error) {
65
+ if (!isTransientSqliteError(error)) throw error;
66
+ retryQueued = true;
67
+ }
68
+ expect(performance.now() - start).toBeLessThan(250);
69
+ expect(retryQueued).toBe(true);
70
+ expect(calls).toBe(0);
71
+ expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
48
72
  } finally {
49
73
  blocker.exec("ROLLBACK");
50
74
  blocker.close();
51
- writer.close();
52
- rmSync(dir, { recursive: true, force: true });
75
+ close();
53
76
  }
54
- }, 30000);
77
+ });
55
78
 
56
- test("foreground retry scope crosses awaits, nests and excludes detached maintenance", async () => {
57
- const db = new Database(":memory:");
58
- const error = Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
59
- const exec = spyOn(db, "exec").mockImplementation(() => {
60
- throw error;
61
- });
62
- const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
63
- const acquire = () =>
64
- withPrivilegedWriter(db, () => {
65
- throw new Error("callback must not run");
66
- });
67
- let late: Promise<void> | undefined;
68
- let release!: () => void;
69
- const gate = new Promise<void>((resolve) => {
70
- release = resolve;
71
- });
79
+ test("foreground admission keeps the loop ticking while a separate writer holds the lock", async () => {
80
+ const { db, path, close } = fixture();
81
+ const locker = await startSqliteWriteLocker(path, 650);
82
+ let timerFired = false;
83
+ let calls = 0;
72
84
  try {
73
- await withSqliteTransformPass(async () => {
74
- await Promise.resolve();
75
- expect(() => withSqliteTransformPass(acquire)).toThrow("after 3 attempts");
76
- expect(exec).toHaveBeenCalledTimes(3);
77
- exec.mockClear();
78
- wait.mockClear();
79
- withoutSqliteTransformPass(() => {
80
- expect(acquire).toThrow("busy");
81
- expect(exec).toHaveBeenCalledTimes(1);
82
- expect(wait).not.toHaveBeenCalled();
83
- });
84
- exec.mockClear();
85
- expect(acquire).toThrow("after 3 attempts");
86
- expect(exec).toHaveBeenCalledTimes(3);
87
- late = (async () => {
88
- await gate;
89
- exec.mockClear();
90
- wait.mockClear();
91
- expect(acquire).toThrow("busy");
92
- expect(exec).toHaveBeenCalledTimes(1);
93
- expect(wait).not.toHaveBeenCalled();
94
- })();
95
- });
96
- release();
97
- await late;
98
- exec.mockClear();
99
- expect(acquire).toThrow("busy");
100
- expect(exec).toHaveBeenCalledTimes(1);
85
+ setTimeout(() => {
86
+ timerFired = true;
87
+ }, 100);
88
+ await withSqliteTransformPass(() =>
89
+ withAsyncPrivilegedWriter(db, () => {
90
+ calls++;
91
+ }),
92
+ );
93
+ expect(timerFired).toBe(true);
94
+ expect(calls).toBe(1);
95
+ expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
101
96
  } finally {
102
- release();
103
- exec.mockRestore();
104
- wait.mockRestore();
105
- db.close();
97
+ await locker.exited;
98
+ close();
106
99
  }
107
- });
100
+ }, 30000);
108
101
 
109
- test("throwing foreground passes cannot leak retry policy to later work", async () => {
110
- const db = new Database(":memory:");
111
- const exec = spyOn(db, "exec").mockImplementation(() => {
112
- throw Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
113
- });
114
- const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
102
+ test("a foreground in-pass writer has bounded 250ms tolerance and restores its timeout", () => {
103
+ const { db, path, close } = fixture();
104
+ const blocker = new Database(path);
105
+ blocker.exec("BEGIN IMMEDIATE");
115
106
  try {
116
- await expect(
117
- withSqliteTransformPass(async () => {
118
- throw new Error("pass failed");
119
- }),
120
- ).rejects.toThrow("pass failed");
121
- expect(() => withPrivilegedWriter(db, () => undefined)).toThrow("busy");
122
- expect(exec).toHaveBeenCalledTimes(1);
123
- expect(wait).not.toHaveBeenCalled();
107
+ const start = performance.now();
108
+ expect(() =>
109
+ withSqliteTransformPass(() => withPrivilegedWriter(db, () => undefined)),
110
+ ).toThrow(SqliteAcquisitionBusyError);
111
+ expect(performance.now() - start).toBeGreaterThanOrEqual(200);
112
+ expect(performance.now() - start).toBeLessThan(400);
113
+ expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
124
114
  } finally {
125
- exec.mockRestore();
126
- wait.mockRestore();
127
- db.close();
115
+ blocker.exec("ROLLBACK");
116
+ blocker.close();
117
+ close();
128
118
  }
129
119
  });
130
120
 
131
- test("explicit deferred reads on a writable handle do not contend with the writer", () => {
132
- const dir = mkdtempSync(join(tmpdir(), "mc-deferred-read-"));
133
- const path = join(dir, "context.db");
134
- const writer = new Database(path);
135
- const reader = new Database(path);
136
- writer.exec(
137
- "PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT); INSERT INTO result VALUES ('committed')",
138
- );
139
- reader.exec("PRAGMA busy_timeout=0");
140
- writer.exec("BEGIN IMMEDIATE");
141
- writer.exec("INSERT INTO result VALUES ('uncommitted')");
121
+ test("ready background publication retries on a timer and retains its write", async () => {
122
+ const { db, path, close } = fixture();
123
+ const locker = await startSqliteWriteLocker(path, 650);
124
+ let ticked = false;
142
125
  try {
143
- const rows = withSqliteTransformPass(() =>
144
- reader.transaction(() => reader.prepare("SELECT value FROM result").all()).deferred(),
145
- );
146
- expect(rows).toEqual([{ value: "committed" }]);
126
+ setTimeout(() => {
127
+ ticked = true;
128
+ }, 100);
129
+ await beginSqliteWriterAsync(db, "historian-publish");
130
+ db.prepare("INSERT INTO context_privilege_state(id, enabled) VALUES (1, 0)").run();
131
+ db.exec("COMMIT");
132
+ expect(ticked).toBe(true);
133
+ expect(db.prepare("SELECT COUNT(*) AS count FROM context_privilege_state").get()).toEqual({
134
+ count: 1,
135
+ });
136
+ expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
147
137
  } finally {
148
- writer.exec("ROLLBACK");
149
- writer.close();
150
- reader.close();
151
- rmSync(dir, { recursive: true, force: true });
138
+ await locker.exited;
139
+ close();
152
140
  }
153
- });
141
+ }, 30000);
154
142
 
155
- test("an explicitly started foreground pass owns its lifetime independently", async () => {
156
- const db = new Database(":memory:");
157
- const exec = spyOn(db, "exec").mockImplementation(() => {
158
- throw Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
159
- });
160
- const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
161
- let release!: () => void;
162
- const gate = new Promise<void>((resolve) => {
163
- release = resolve;
164
- });
165
- let child!: Promise<void>;
143
+ test("an in-pass BEGIN tolerates a sibling writer that releases within 250ms", async () => {
144
+ const { db, path, close } = fixture();
145
+ const locker = await startSqliteWriteLocker(path, 150);
166
146
  try {
167
- await withSqliteTransformPass(async () => {
168
- child = withSqliteTransformPass(async () => {
169
- await gate;
170
- expect(() => withPrivilegedWriter(db, () => undefined)).toThrow("after 3 attempts");
171
- expect(exec).toHaveBeenCalledTimes(3);
172
- });
173
- });
174
- release();
175
- await child;
147
+ withSqliteTransformPass(() => withPrivilegedWriter(db, () => undefined));
148
+ expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
176
149
  } finally {
177
- release();
178
- exec.mockRestore();
179
- wait.mockRestore();
180
- db.close();
150
+ await locker.exited;
151
+ close();
181
152
  }
182
- });
153
+ }, 30000);
@@ -224,7 +224,9 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
224
224
  writable: true,
225
225
  value: (sql: string) => {
226
226
  if (/^\s*BEGIN\s+(?:IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*;?\s*$/i.test(sql)) {
227
- retryAcquisition(() => nativeExec(sql), sql.trim());
227
+ if (transformPassScope.getStore()?.active || backgroundWriterScope.getStore())
228
+ acquireShort(db, () => nativeExec(sql), sql.trim());
229
+ else nativeExec(sql);
228
230
  return db;
229
231
  }
230
232
  return nativeExec(sql);
@@ -489,6 +491,13 @@ export type Statement = BetterSqlite3.Statement<unknown[], unknown>;
489
491
 
490
492
  const privilegeDepth = new WeakMap<Database, number>();
491
493
  const transformPassScope = new AsyncLocalStorage<{ active: boolean } | undefined>();
494
+ const admissionScope = new AsyncLocalStorage<boolean>();
495
+ const backgroundWriterScope = new AsyncLocalStorage<boolean>();
496
+
497
+ /** Opt in only a writer that can safely defer its work after a busy acquisition. */
498
+ export function withSqliteBackgroundWriter<T>(operation: () => T): T {
499
+ return backgroundWriterScope.run(true, operation);
500
+ }
492
501
 
493
502
  /** Only an awaited foreground transform may spend the extra acquisition budget.
494
503
  * The mutable lease also expires for detached descendants when that pass ends. */
@@ -530,35 +539,123 @@ export class SqliteAcquisitionBusyError extends Error {
530
539
  readonly code = "SQLITE_BUSY";
531
540
  readonly stage: string;
532
541
  constructor(cause: unknown, stage = "BEGIN IMMEDIATE") {
533
- super("SQLite writer acquisition remained busy after 3 attempts", { cause });
542
+ super("SQLite writer acquisition remained busy", { cause });
534
543
  this.name = "SqliteAcquisitionBusyError";
535
544
  this.stage = stage;
536
545
  }
537
546
  }
538
547
 
539
- /** Retry only acquisition: no privilege flag, callback or in-memory mutation has run yet. */
540
- function retryAcquisition(acquire: () => unknown, stage: string): void {
541
- // Maintenance writers retry on their next tick. Multiplying their native
542
- // busy timeout here would stall every session sharing the host event loop.
543
- if (!transformPassScope.getStore()?.active) {
548
+ const SHORT_BUSY_TIMEOUT_MS = 25;
549
+ const FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS = 250;
550
+ const FOREGROUND_ACQUISITION_BUDGET_MS = 16_500;
551
+
552
+ /** The connection is never handed back to another caller with a shortened timeout. */
553
+ function acquireShort(db: Database, acquire: () => unknown, site: string): void {
554
+ const started = performance.now();
555
+ const previous = pragmaNumber(db, "busy_timeout");
556
+ let acquired = false;
557
+ try {
558
+ // Async admission retries after yielding, so each of its BEGIN attempts
559
+ // stays short. An ordinary BEGIN inside a transform pass cannot yield;
560
+ // give that single attempt enough time for a brief writer to finish.
561
+ const timeout =
562
+ transformPassScope.getStore()?.active &&
563
+ !admissionScope.getStore() &&
564
+ !backgroundWriterScope.getStore()
565
+ ? FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS
566
+ : SHORT_BUSY_TIMEOUT_MS;
567
+ db.exec(`PRAGMA busy_timeout=${timeout}`);
544
568
  acquire();
545
- return;
569
+ acquired = true;
570
+ } catch (error) {
571
+ if (transformPassScope.getStore()?.active && isTransientSqliteError(error))
572
+ throw new SqliteAcquisitionBusyError(error, site);
573
+ throw error;
574
+ } finally {
575
+ if (previous !== null) db.exec(`PRAGMA busy_timeout=${previous}`);
576
+ const elapsed = performance.now() - started;
577
+ if (elapsed >= 250)
578
+ console.warn(
579
+ `[${new Date().toISOString()}] [magic-context] sqlite acquisition site=${site} lane=${transformPassScope.getStore()?.active ? "foreground" : "background"} elapsed=${Math.round(elapsed)}ms attempts=1 outcome=${acquired ? "acquired" : "busy"}`,
580
+ );
546
581
  }
547
- const delays = [500, 1000];
548
- for (let attempt = 0; ; attempt++) {
582
+ }
583
+
584
+ /** Begin an immediate transaction with bounded asynchronous retries on writer contention.
585
+ * Callers must recheck any lease or source snapshot after this function returns. */
586
+ export async function beginSqliteWriterAsync(db: Database, site: string): Promise<void> {
587
+ const started = performance.now();
588
+ let attempts = 0;
589
+ for (;;) {
590
+ attempts++;
549
591
  try {
550
- acquire();
592
+ withSqliteBackgroundWriter(() => db.exec("BEGIN IMMEDIATE"));
593
+ const elapsed = performance.now() - started;
594
+ if (elapsed >= 250)
595
+ console.warn(
596
+ `[${new Date().toISOString()}] [magic-context] sqlite acquisition site=${site} lane=background elapsed=${Math.round(elapsed)}ms attempts=${attempts} outcome=acquired`,
597
+ );
551
598
  return;
552
599
  } catch (error) {
553
- // Handles created by this module already retry inside exec. Do not
554
- // apply the attempt budget twice; externally supplied handles still
555
- // need the acquisition retry performed by withPrivilegedWriter.
556
- if (error instanceof SqliteAcquisitionBusyError || !isTransientSqliteError(error))
600
+ if (!isTransientSqliteError(error)) throw error;
601
+ const elapsed = performance.now() - started;
602
+ if (elapsed >= FOREGROUND_ACQUISITION_BUDGET_MS) {
603
+ console.warn(
604
+ `[${new Date().toISOString()}] [magic-context] sqlite acquisition site=${site} lane=background elapsed=${Math.round(elapsed)}ms attempts=${attempts} outcome=busy`,
605
+ );
557
606
  throw error;
558
- if (attempt === delays.length) throw new SqliteAcquisitionBusyError(error, stage);
559
- // The database API is synchronous on both hosts. With busy_timeout=5000,
560
- // three attempts plus these waits bound one acquisition to 16.5 seconds.
561
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delays[attempt]);
607
+ }
608
+ await new Promise<void>((resolve) =>
609
+ setTimeout(
610
+ resolve,
611
+ Math.min(
612
+ attempts === 1 ? 500 : 1000,
613
+ FOREGROUND_ACQUISITION_BUDGET_MS - elapsed,
614
+ ),
615
+ ),
616
+ );
617
+ }
618
+ }
619
+ }
620
+
621
+ /** Retry admission before any turn work, yielding between short lock attempts. */
622
+ export async function withAsyncPrivilegedWriter<T>(db: Database, operation: () => T): Promise<T> {
623
+ const started = performance.now();
624
+ let attempts = 0;
625
+ for (;;) {
626
+ attempts++;
627
+ try {
628
+ const result = admissionScope.run(true, () =>
629
+ withSqliteTransformPass(() => withPrivilegedWriter(db, operation)),
630
+ );
631
+ const elapsed = performance.now() - started;
632
+ if (elapsed >= 250)
633
+ console.warn(
634
+ `[${new Date().toISOString()}] [magic-context] sqlite acquisition site=privileged_writer lane=foreground elapsed=${Math.round(elapsed)}ms attempts=${attempts} outcome=acquired`,
635
+ );
636
+ return result;
637
+ } catch (error) {
638
+ // A callback failure can occur after writes; only retry a failed BEGIN.
639
+ if (!(error instanceof SqliteAcquisitionBusyError)) throw error;
640
+ const elapsed = performance.now() - started;
641
+ if (elapsed >= FOREGROUND_ACQUISITION_BUDGET_MS) {
642
+ if (elapsed >= 250)
643
+ console.warn(
644
+ `[${new Date().toISOString()}] [magic-context] sqlite acquisition site=privileged_writer lane=foreground elapsed=${Math.round(elapsed)}ms attempts=${attempts} outcome=busy`,
645
+ );
646
+ throw error instanceof SqliteAcquisitionBusyError
647
+ ? error
648
+ : new SqliteAcquisitionBusyError(error);
649
+ }
650
+ await new Promise<void>((resolve) =>
651
+ setTimeout(
652
+ resolve,
653
+ Math.min(
654
+ attempts === 1 ? 500 : 1000,
655
+ FOREGROUND_ACQUISITION_BUDGET_MS - elapsed,
656
+ ),
657
+ ),
658
+ );
562
659
  }
563
660
  }
564
661
  }
@@ -587,7 +684,13 @@ export function withPrivilegedWriter<T>(db: Database, operation: () => T): T {
587
684
  if (nested) {
588
685
  db.exec(`SAVEPOINT ${savepoint}`);
589
686
  } else {
590
- retryAcquisition(() => db.exec("BEGIN IMMEDIATE"), "BEGIN IMMEDIATE");
687
+ try {
688
+ db.exec("BEGIN IMMEDIATE");
689
+ } catch (error) {
690
+ if (transformPassScope.getStore()?.active && isTransientSqliteError(error))
691
+ throw new SqliteAcquisitionBusyError(error);
692
+ throw error;
693
+ }
591
694
  }
592
695
  privilegeDepth.set(db, previousDepth + 1);
593
696
  try {
@@ -1,70 +1,47 @@
1
- import { afterAll, afterEach, describe, expect, spyOn, test } from "bun:test";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
2
5
  import {
3
6
  Database,
4
7
  isTransientSqliteError,
5
- withSqliteTransformPass,
6
- withPrivilegedWriter as writePrivileged,
8
+ withAsyncPrivilegedWriter,
9
+ withPrivilegedWriter,
7
10
  } from "./sqlite";
8
11
 
9
- const withPrivilegedWriter = <T>(db: Database, operation: () => T): T =>
10
- withSqliteTransformPass(() => writePrivileged(db, operation));
11
-
12
- const wait = spyOn(Atomics, "wait");
13
- afterEach(() => wait.mockClear());
14
- afterAll(() => wait.mockRestore());
15
-
16
- describe("writer acquisition backoff", () => {
17
- for (const clearsOn of [2, 3]) {
18
- test(`acquisition clears on attempt ${clearsOn} and invokes callback exactly once`, () => {
19
- const db = new Database(":memory:");
20
- db.exec(
21
- "CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER); CREATE TABLE result(value TEXT)",
22
- );
23
- const exec = db.exec.bind(db);
24
- let attempts = 0;
25
- let callbacks = 0;
26
- const delays: number[] = [];
27
- wait.mockImplementation((_array, _index, _value, timeout) => {
28
- delays.push(timeout!);
29
- return "timed-out";
30
- });
31
- const intercepted = spyOn(db, "exec").mockImplementation((sql) => {
32
- if (sql === "BEGIN IMMEDIATE" && ++attempts < clearsOn)
33
- throw Object.assign(new Error("locked"), { code: "SQLITE_BUSY" });
34
- return exec(sql);
35
- });
36
- try {
37
- const result = withPrivilegedWriter(db, () => {
38
- callbacks++;
39
- db.prepare("INSERT INTO result VALUES (?)").run("managed");
40
- return JSON.stringify(db.prepare("SELECT * FROM result").all());
41
- });
42
- expect(result).toBe('[{"value":"managed"}]');
43
- expect(attempts).toBe(clearsOn);
44
- expect(callbacks).toBe(1);
45
- expect(delays).toEqual([500, 1000].slice(0, clearsOn - 1));
46
- } finally {
47
- intercepted.mockRestore();
48
- db.close();
49
- }
50
- });
51
- }
52
- test("exhausted acquisition never invokes callback", () => {
53
- const db = new Database(":memory:");
12
+ describe("writer acquisition", () => {
13
+ test("exhausted async admission preserves the foreground budget and never invokes the callback", async () => {
14
+ const dir = mkdtempSync(join(tmpdir(), "mc-writer-budget-"));
15
+ const path = join(dir, "context.db");
16
+ const blocker = new Database(path);
17
+ blocker.exec(
18
+ "CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
19
+ );
20
+ const db = new Database(path);
21
+ db.exec("PRAGMA busy_timeout=5000");
22
+ blocker.exec("BEGIN IMMEDIATE");
54
23
  let calls = 0;
55
- wait.mockImplementation(() => "timed-out");
56
- const exec = spyOn(db, "exec").mockImplementation(() => {
57
- throw Object.assign(new Error("original lock"), { code: "SQLITE_LOCKED" });
58
- });
24
+ let ticks = 0;
25
+ const timer = setInterval(() => ticks++, 50);
26
+ const started = performance.now();
59
27
  try {
60
- expect(() => withPrivilegedWriter(db, () => calls++)).toThrow("after 3 attempts");
61
- expect(exec).toHaveBeenCalledTimes(3);
28
+ await expect(withAsyncPrivilegedWriter(db, () => calls++)).rejects.toThrow(
29
+ "acquisition remained busy",
30
+ );
31
+ expect(performance.now() - started).toBeGreaterThanOrEqual(16_500);
32
+ expect(performance.now() - started).toBeLessThan(17_500);
33
+ expect(ticks).toBeGreaterThan(100);
62
34
  expect(calls).toBe(0);
35
+ expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
63
36
  } finally {
64
- exec.mockRestore();
37
+ clearInterval(timer);
38
+ blocker.exec("ROLLBACK");
39
+ blocker.close();
65
40
  db.close();
41
+ rmSync(dir, { recursive: true, force: true });
66
42
  }
67
- });
43
+ }, 30000);
44
+
68
45
  test("busy callback rolls back without retrying mutations", () => {
69
46
  const db = new Database(":memory:");
70
47
  db.exec(
@@ -81,11 +58,11 @@ describe("writer acquisition backoff", () => {
81
58
  ).toThrow("after acquisition");
82
59
  expect(calls).toBe(1);
83
60
  expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
84
- expect(wait).not.toHaveBeenCalled();
85
61
  } finally {
86
62
  db.close();
87
63
  }
88
64
  });
65
+
89
66
  test("recognizes Bun extended and Node numeric contention codes only", () => {
90
67
  expect(isTransientSqliteError({ code: "SQLITE_BUSY_SNAPSHOT" })).toBe(true);
91
68
  expect(isTransientSqliteError({ code: "ERR_SQLITE_ERROR", errcode: 5 })).toBe(true);
@@ -1275,7 +1275,7 @@ function buildAggregateTarget(
1275
1275
  return any ? "truncated" : "absent";
1276
1276
  },
1277
1277
  // Open invocations still belong to the current turn; only complete arcs reclaim.
1278
- canDrop: complete,
1278
+ canDrop: () => complete() && !occurrences.some((occ) => occ.part.hasUserAnswer?.()),
1279
1279
  requiresToolArcSkeleton,
1280
1280
  // Non-mutating read of the invocation input (the tool_use occurrence
1281
1281
  // carries the arguments). Used by smart-drops supersession selection.
@@ -1375,6 +1375,9 @@ function buildToolTarget(
1375
1375
  // drop() replaces an invocation part's arguments with a marker, so a
1376
1376
  // new drop of one keeps its real arguments instead.
1377
1377
  cannotRemove: () => part.setToolInput !== undefined,
1378
+ // Without a paired tool call, an answer marker can forbid automatic dropping,
1379
+ // but must not make ordinary results newly eligible for automatic reclaim.
1380
+ ...(part.hasUserAnswer?.() ? { canDrop: () => false } : {}),
1378
1381
  inputStringBytes(): number | null {
1379
1382
  return part.setToolInput ? toolInputStringBytes(part.getToolInput?.() ?? null) : 0;
1380
1383
  },
@@ -78,6 +78,9 @@ export interface TranscriptPart {
78
78
  /** Discriminator for filter logic. Stable across mutations. */
79
79
  readonly kind: TranscriptPartKind;
80
80
 
81
+ /** True when the host marks this tool result as carrying a user's answer. */
82
+ hasUserAnswer?(): boolean;
83
+
81
84
  /**
82
85
  * Best-effort identifier for cross-pass tracking. May be:
83
86
  * - OpenCode part ID (e.g. "prt_..."), stable across passes.