@cortexkit/opencode-magic-context 0.44.2 → 0.44.3

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 (73) 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-vjmvh6pz.js} +3 -1
  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/memory/project-identity.d.ts +3 -0
  8. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  9. package/dist/features/magic-context/message-index-async.d.ts +1 -1
  10. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  11. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  12. package/dist/features/magic-context/session-project-storage.d.ts +5 -5
  13. package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
  14. package/dist/features/magic-context/storage-embedding-measurements.d.ts.map +1 -1
  15. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -1
  16. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  17. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  18. package/dist/hooks/magic-context/compartment-runner-types.d.ts +1 -0
  19. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  20. package/dist/hooks/magic-context/derive-budgets.d.ts +4 -0
  21. package/dist/hooks/magic-context/derive-budgets.d.ts.map +1 -1
  22. package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -1
  23. package/dist/hooks/magic-context/lkg-persist.d.ts +1 -4
  24. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
  25. package/dist/hooks/magic-context/producer-window-guard.d.ts +4 -1
  26. package/dist/hooks/magic-context/producer-window-guard.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/rust-mode-transform.d.ts +1 -0
  28. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  29. package/dist/hooks/magic-context/storage-busy-refusal.d.ts +6 -0
  30. package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -1
  31. package/dist/{index-r9vkp4n9.js → index-33q7s9y2.js} +1 -1
  32. package/dist/{index-egkfvhms.js → index-4an2p8qa.js} +80 -14
  33. package/dist/{index-18tjha8g.js → index-638hf49n.js} +1 -1
  34. package/dist/{index-stb5fg12.js → index-9y5p0d50.js} +31 -1
  35. package/dist/{index-esxfcz5z.js → index-dgg5y7er.js} +1 -1
  36. package/dist/{index-3x9caerx.js → index-nf9sk8sh.js} +3 -3
  37. package/dist/{index-gtzcmv90.js → index-q5p1aqvv.js} +5 -5
  38. package/dist/{index-40s1xz22.js → index-rdsaxnf2.js} +1 -1
  39. package/dist/{index-ttdn47aq.js → index-yhm60r53.js} +134 -49
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +453 -226
  42. package/dist/plugin/dream-timer.d.ts.map +1 -1
  43. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  44. package/dist/plugin/messages-transform.d.ts.map +1 -1
  45. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  46. package/dist/{read-session-chunk-wmvgnd24.js → read-session-chunk-n072mcq4.js} +2 -2
  47. package/dist/{recomp-orchestrator-rk40ff15.js → recomp-orchestrator-19tr6zya.js} +6 -6
  48. package/dist/{send-session-notification-6xsrcxnp.js → send-session-notification-04hvd54c.js} +2 -2
  49. package/dist/shared/run-token-log.d.ts +13 -0
  50. package/dist/shared/run-token-log.d.ts.map +1 -0
  51. package/dist/shared/sqlite.d.ts +7 -0
  52. package/dist/shared/sqlite.d.ts.map +1 -1
  53. package/dist/{transform-postprocess-phase-t7xgdhdf.js → transform-postprocess-phase-z2mfc5w1.js} +6 -6
  54. package/dist/v2/hidden-completion.d.ts +7 -1
  55. package/dist/v2/hidden-completion.d.ts.map +1 -1
  56. package/dist/v2/hooks/context.d.ts.map +1 -1
  57. package/dist/v2/hooks/hidden-terminal-error.d.ts +8 -0
  58. package/dist/v2/hooks/hidden-terminal-error.d.ts.map +1 -0
  59. package/dist/v2/hooks/tools.d.ts +1 -1
  60. package/dist/v2/hooks/tools.d.ts.map +1 -1
  61. package/dist/v2/server.js +584 -153
  62. package/dist/{wrapup-orchestrator-h8tr10c6.js → wrapup-orchestrator-sy4m56a7.js} +7 -7
  63. package/package.json +1 -1
  64. package/src/config/latch-permanence-guard.test.ts +4 -0
  65. package/src/shared/data-path.test.ts +1 -1
  66. package/src/shared/models-dev-cache.test.ts +30 -1
  67. package/src/shared/run-token-log.test.ts +65 -0
  68. package/src/shared/run-token-log.ts +39 -0
  69. package/src/shared/sqlite-acquisition-routing.test.ts +19 -11
  70. package/src/shared/sqlite-pass-scope.test.ts +115 -144
  71. package/src/shared/sqlite.ts +123 -20
  72. package/src/shared/storage-busy.test.ts +34 -57
  73. package/src/shared/write-transaction-attribution.test.ts +13 -11
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  runManagedWrapup2
3
- } from "./index-3x9caerx.js";
4
- import"./index-40s1xz22.js";
5
- import"./index-ttdn47aq.js";
3
+ } from "./index-nf9sk8sh.js";
4
+ import"./index-rdsaxnf2.js";
5
+ import"./index-yhm60r53.js";
6
6
  import"./index-sd3egckz.js";
7
- import"./index-stb5fg12.js";
8
- import"./index-r9vkp4n9.js";
9
- import"./index-esxfcz5z.js";
7
+ import"./index-9y5p0d50.js";
8
+ import"./index-33q7s9y2.js";
9
+ import"./index-dgg5y7er.js";
10
10
  import"./index-y5145x8k.js";
11
- import"./index-egkfvhms.js";
11
+ import"./index-4an2p8qa.js";
12
12
  import"./index-kr5qehbd.js";
13
13
  import"./index-jdxxq8cz.js";
14
14
  import"./index-kwe03yde.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.44.2",
3
+ "version": "0.44.3",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -53,6 +53,10 @@ const KNOWN_SLOTS: Record<string, KnownSlot> = {
53
53
  classification: "VERDICT",
54
54
  reason: "Correct: circuit state expires and a half-open probe re-evaluates the endpoint.",
55
55
  },
56
+ "packages/plugin/src/features/magic-context/memory/project-identity.ts:homeProjectPermission": {
57
+ classification: "VERDICT",
58
+ reason: "Boot config publishes the home-project permission; the setter replaces it on another host initialization, while explicit resolution arguments override it.",
59
+ },
56
60
  "packages/plugin/src/features/magic-context/memory/project-identity.ts:directoryFallbackCache":
57
61
  {
58
62
  classification: "VERDICT",
@@ -29,7 +29,7 @@ const savedEnv = {
29
29
  describe("data-path", () => {
30
30
  beforeEach(() => {
31
31
  process.env.XDG_CACHE_HOME = undefined;
32
- process.env.XDG_DATA_HOME = undefined;
32
+ process.env.XDG_DATA_HOME = process.env.MAGIC_CONTEXT_TEST_DATA_DIR;
33
33
  delete process.env.MAGIC_CONTEXT_TEST_DATA_DIR;
34
34
  delete process.env.NODE_ENV;
35
35
  process.env.LOCALAPPDATA = undefined;
@@ -2,7 +2,14 @@ import { afterEach, beforeEach, describe, 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
- import { resolveKnownHistorianContextLimit } from "../hooks/magic-context/derive-budgets";
5
+ import {
6
+ resolveHistorianProducerLimits,
7
+ resolveKnownHistorianContextLimit,
8
+ } from "../hooks/magic-context/derive-budgets";
9
+ import {
10
+ historianProducerReserve,
11
+ producerInputTokenLimit,
12
+ } from "../hooks/magic-context/producer-window-guard";
6
13
  import {
7
14
  clearModelsDevCache,
8
15
  getModelsDevCacheState,
@@ -231,6 +238,28 @@ describe("models-dev-cache (SDK-only)", () => {
231
238
  expect(getSdkContextLimit("openai", "gpt-5.4-mini")).toBe(922000);
232
239
  });
233
240
 
241
+ test("issue 567: production resolver keeps input cap separate from context reserve", async () => {
242
+ await refreshModelLimitsFromApi(
243
+ makeClient([
244
+ {
245
+ id: "auth-provider",
246
+ models: {
247
+ model: { limit: { context: 400_000, input: 272_000, output: 128_000 } },
248
+ },
249
+ },
250
+ ]),
251
+ );
252
+ const resolved = getSdkContextLimit("auth-provider", "model", undefined, {
253
+ reservation: "none",
254
+ });
255
+ const limits = resolveHistorianProducerLimits("auth-provider/model");
256
+ const reserve = historianProducerReserve(limits.context, undefined, 128_000);
257
+ expect(resolved).toBe(272_000);
258
+ expect(limits).toEqual({ context: 400_000, input: 272_000 });
259
+ expect(reserve).toBe(100_000);
260
+ expect(producerInputTokenLimit(limits.context, reserve, limits.input)).toBe(263_840);
261
+ });
262
+
234
263
  test("explicit SDK-resolved config limit wins over a larger catalog or detected value", async () => {
235
264
  // OpenCode's config.providers() has already applied opencode.json over its
236
265
  // catalog. The cache must preserve that 120K effective value, and a later
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { runTokenLog } from "./run-token-log";
3
+
4
+ describe("run token log host usage", () => {
5
+ test("OpenCode assistant message tokens retain reasoning and cache", () => {
6
+ const message = {
7
+ info: {
8
+ finish: "length",
9
+ tokens: { input: 10, output: 50, reasoning: 48, cache: { read: 7, write: 0 } },
10
+ },
11
+ };
12
+ expect(runTokenLog(message.info.tokens, 64, message.info.finish)).toEqual({
13
+ input: 10,
14
+ output: 50,
15
+ reasoning: 48,
16
+ cache_read: 7,
17
+ cache_write: 0,
18
+ max_tokens: 64,
19
+ finish_reason: "length",
20
+ });
21
+ });
22
+
23
+ test("OpenCode hidden-completion usage preserves omitted reasoning as null", () => {
24
+ const row = {
25
+ data: {
26
+ finish: "stop",
27
+ tokens: { input: 20, output: 4, cache: { read: 0, write: 3 } },
28
+ },
29
+ };
30
+ expect(runTokenLog(row.data.tokens, undefined, row.data.finish)).toEqual({
31
+ input: 20,
32
+ output: 4,
33
+ reasoning: null,
34
+ cache_read: 0,
35
+ cache_write: 3,
36
+ max_tokens: null,
37
+ finish_reason: "stop",
38
+ });
39
+ });
40
+
41
+ test("Pi stdout usage and absent usage remain distinguishable", () => {
42
+ const message = {
43
+ stopReason: "length",
44
+ usage: { input: 3, output: 12, cacheRead: 2, cacheWrite: 0 },
45
+ };
46
+ expect(runTokenLog(message.usage, 12, message.stopReason)).toEqual({
47
+ input: 3,
48
+ output: 12,
49
+ reasoning: null,
50
+ cache_read: 2,
51
+ cache_write: 0,
52
+ max_tokens: 12,
53
+ finish_reason: "length",
54
+ });
55
+ expect(runTokenLog(undefined)).toEqual({
56
+ input: null,
57
+ output: null,
58
+ reasoning: null,
59
+ cache_read: null,
60
+ cache_write: null,
61
+ max_tokens: null,
62
+ finish_reason: null,
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,39 @@
1
+ export interface RunTokenLog {
2
+ input: number | null;
3
+ output: number | null;
4
+ reasoning: number | null;
5
+ cache_read: number | null;
6
+ cache_write: number | null;
7
+ max_tokens: number | null;
8
+ finish_reason: string | null;
9
+ }
10
+
11
+ function count(value: unknown): number | null {
12
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
13
+ }
14
+
15
+ /** Keep provider omissions distinct from measured zero in diagnostic logs. */
16
+ export function runTokenLog(tokens: unknown, maxTokens?: number, finish?: unknown): RunTokenLog {
17
+ const usage = tokens && typeof tokens === "object" ? (tokens as Record<string, unknown>) : {};
18
+ const cache =
19
+ usage.cache && typeof usage.cache === "object"
20
+ ? (usage.cache as Record<string, unknown>)
21
+ : {};
22
+ return {
23
+ input: count(usage.input ?? usage.input_tokens),
24
+ output: count(usage.output ?? usage.output_tokens),
25
+ reasoning: count(usage.reasoning ?? usage.reasoning_tokens),
26
+ cache_read: count(
27
+ usage.cacheRead ?? usage.cache_read ?? usage.cached_input_tokens ?? cache.read,
28
+ ),
29
+ cache_write: count(
30
+ usage.cacheWrite ?? usage.cache_write ?? usage.cache_write_tokens ?? cache.write,
31
+ ),
32
+ max_tokens: count(maxTokens),
33
+ finish_reason: typeof finish === "string" ? finish : null,
34
+ };
35
+ }
36
+
37
+ export function formatRunTokenLog(tokens: RunTokenLog): string {
38
+ return `tokens=${JSON.stringify(tokens)}`;
39
+ }
@@ -1,8 +1,13 @@
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
- import { Database, withPrivilegedWriter, withSqliteTransformPass } from "./sqlite";
5
+ import {
6
+ Database,
7
+ withAsyncPrivilegedWriter,
8
+ withPrivilegedWriter,
9
+ withSqliteTransformPass,
10
+ } from "./sqlite";
6
11
  import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
7
12
 
8
13
  for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
@@ -10,7 +15,9 @@ for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
10
15
  const dir = mkdtempSync(join(tmpdir(), "mc-routed-acquisition-"));
11
16
  const path = join(dir, "context.db");
12
17
  const db = new Database(path);
13
- db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=50; CREATE TABLE result(value TEXT)");
18
+ db.exec(
19
+ "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=50; CREATE TABLE result(value TEXT); CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
20
+ );
14
21
  const locker = await startSqliteWriteLocker(path, 650);
15
22
  let calls = 0;
16
23
  const write = () => {
@@ -19,7 +26,8 @@ for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
19
26
  return "managed";
20
27
  };
21
28
  try {
22
- withSqliteTransformPass(() => {
29
+ await withSqliteTransformPass(async () => {
30
+ await withAsyncPrivilegedWriter(db, () => undefined);
23
31
  if (mode === "literal") {
24
32
  db.exec("BEGIN IMMEDIATE");
25
33
  write();
@@ -43,7 +51,9 @@ for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
43
51
 
44
52
  test("routed transactions preserve nesting, receiver, arguments and rollback without callback retries", () => {
45
53
  const db = new Database(":memory:");
46
- db.exec("CREATE TABLE result(value TEXT)");
54
+ db.exec(
55
+ "CREATE TABLE result(value TEXT); CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
56
+ );
47
57
  let calls = 0;
48
58
  const transaction = db.transaction(function (this: { prefix: string }, value: string) {
49
59
  calls++;
@@ -69,23 +79,21 @@ test("exhausted routed acquisition never enters the callback or multiplies privi
69
79
  const path = join(dir, "context.db");
70
80
  const blocker = new Database(path);
71
81
  const db = new Database(path);
72
- blocker.exec("PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT)");
82
+ blocker.exec(
83
+ "PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT); CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
84
+ );
73
85
  db.exec("PRAGMA busy_timeout=0");
74
86
  blocker.exec("BEGIN IMMEDIATE");
75
- const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
76
87
  let callbacks = 0;
77
88
  try {
78
89
  for (const run of [
79
90
  () => db.transaction(() => callbacks++)(),
80
91
  () => withPrivilegedWriter(db, () => callbacks++),
81
92
  ]) {
82
- wait.mockClear();
83
- expect(() => withSqliteTransformPass(run)).toThrow("after 3 attempts");
84
- expect(wait).toHaveBeenCalledTimes(2);
93
+ expect(() => withSqliteTransformPass(run)).toThrow("acquisition remained busy");
85
94
  expect(callbacks).toBe(0);
86
95
  }
87
96
  } finally {
88
- wait.mockRestore();
89
97
  blocker.exec("ROLLBACK");
90
98
  blocker.close();
91
99
  db.close();
@@ -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);