@cortexkit/opencode-magic-context 0.44.1 → 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 (216) hide show
  1. package/README.md +6 -0
  2. package/dist/agents/dreamer.d.ts +4 -6
  3. package/dist/agents/dreamer.d.ts.map +1 -1
  4. package/dist/config/live-child-output-cap.d.ts.map +1 -1
  5. package/dist/config/schema/magic-context.d.ts +63 -10
  6. package/dist/config/schema/magic-context.d.ts.map +1 -1
  7. package/dist/{conflict-warning-hook-d14ah73v.js → conflict-warning-hook-hmpzxep6.js} +3 -3
  8. package/dist/{derive-budgets-gd2270s0.js → derive-budgets-vjmvh6pz.js} +3 -1
  9. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  10. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +11 -0
  11. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  12. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  14. package/dist/features/magic-context/dreamer/docs-proposals.d.ts +2 -0
  15. package/dist/features/magic-context/dreamer/docs-proposals.d.ts.map +1 -1
  16. package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/lease.d.ts +1 -1
  18. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/manual-summary.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +1 -1
  21. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/map-memories.d.ts +5 -0
  23. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  24. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +5 -0
  25. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  26. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +2 -2
  27. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
  28. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +8 -1
  29. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  30. package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
  31. package/dist/features/magic-context/dreamer/task-executor.d.ts +5 -0
  32. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  33. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  34. package/dist/features/magic-context/dreamer/task-prompts.d.ts +18 -2
  35. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  36. package/dist/features/magic-context/dreamer/task-registry.d.ts +12 -0
  37. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  38. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -1
  39. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  40. package/dist/features/magic-context/dreamer/token-budget.d.ts +33 -0
  41. package/dist/features/magic-context/dreamer/token-budget.d.ts.map +1 -0
  42. package/dist/features/magic-context/dreamer/verify-diff.d.ts +15 -0
  43. package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -0
  44. package/dist/features/magic-context/dreamer/verify-gate.d.ts.map +1 -1
  45. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
  46. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  47. package/dist/features/magic-context/dreamer/verify-read-cap.d.ts +4 -0
  48. package/dist/features/magic-context/dreamer/verify-read-cap.d.ts.map +1 -0
  49. package/dist/features/magic-context/dreamer/verify.d.ts +5 -0
  50. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  51. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  52. package/dist/features/magic-context/memory/project-identity-cache.d.ts +6 -0
  53. package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -0
  54. package/dist/features/magic-context/memory/project-identity.d.ts +11 -22
  55. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  56. package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -0
  57. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  58. package/dist/features/magic-context/memory/verification-paths.d.ts +1 -0
  59. package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
  60. package/dist/features/magic-context/message-index-async.d.ts +1 -1
  61. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  62. package/dist/features/magic-context/project-embedding-registry.d.ts +5 -0
  63. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  64. package/dist/features/magic-context/search.d.ts.map +1 -1
  65. package/dist/features/magic-context/session-activity.d.ts +10 -0
  66. package/dist/features/magic-context/session-activity.d.ts.map +1 -0
  67. package/dist/features/magic-context/session-project-backfill.d.ts +4 -1
  68. package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
  69. package/dist/features/magic-context/session-project-storage.d.ts +5 -5
  70. package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
  71. package/dist/features/magic-context/smart-notes/compiler.d.ts +5 -0
  72. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  73. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +1 -0
  74. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
  75. package/dist/features/magic-context/smart-notes/storage.d.ts +1 -1
  76. package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
  77. package/dist/features/magic-context/smart-notes/types.d.ts +2 -0
  78. package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
  79. package/dist/features/magic-context/storage-db.d.ts +2 -1
  80. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  81. package/dist/features/magic-context/storage-embedding-measurements.d.ts.map +1 -1
  82. package/dist/features/magic-context/storage-identity-merge.d.ts +4 -1
  83. package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
  84. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  85. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -1
  86. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -1
  89. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/compartment-runner-types.d.ts +1 -0
  93. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/compartment-runner.d.ts +1 -0
  95. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/derive-budgets.d.ts +4 -0
  97. package/dist/hooks/magic-context/derive-budgets.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/embed-session-state.d.ts +3 -0
  100. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/event-handler.d.ts +1 -0
  102. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/event-payloads.d.ts +1 -0
  104. package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/lkg-persist.d.ts +1 -4
  109. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/lkg-replay.d.ts +6 -4
  111. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/producer-window-guard.d.ts +4 -1
  115. package/dist/hooks/magic-context/producer-window-guard.d.ts.map +1 -1
  116. package/dist/hooks/magic-context/rust-mode-transform.d.ts +1 -0
  117. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/storage-busy-refusal.d.ts +13 -0
  119. package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -0
  120. package/dist/hooks/magic-context/strip-content.d.ts +1 -0
  121. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  123. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  124. package/dist/{index-a9vbtdzv.js → index-33q7s9y2.js} +1 -1
  125. package/dist/{index-cz88kw0n.js → index-4an2p8qa.js} +183 -39
  126. package/dist/{index-d107az47.js → index-638hf49n.js} +1 -1
  127. package/dist/{index-stb5fg12.js → index-9y5p0d50.js} +31 -1
  128. package/dist/{index-s6b7h12g.js → index-dgg5y7er.js} +73 -2
  129. package/dist/{index-6w9p73jz.js → index-jqcbs2k1.js} +10 -0
  130. package/dist/{index-0rp6pj54.js → index-nf9sk8sh.js} +7 -5
  131. package/dist/index-q5p1aqvv.js +7260 -0
  132. package/dist/index-rdsaxnf2.js +125 -0
  133. package/dist/{index-jtdrvmcn.js → index-yhm60r53.js} +37544 -37072
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +8402 -14429
  136. package/dist/plugin/dream-timer.d.ts.map +1 -1
  137. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  138. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  139. package/dist/plugin/messages-transform.d.ts +10 -9
  140. package/dist/plugin/messages-transform.d.ts.map +1 -1
  141. package/dist/plugin/rpc-handlers.d.ts +1 -0
  142. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  143. package/dist/{read-session-chunk-fkdv2g5b.js → read-session-chunk-n072mcq4.js} +2 -2
  144. package/dist/{recomp-orchestrator-k4h5n0p3.js → recomp-orchestrator-19tr6zya.js} +6 -5
  145. package/dist/{send-session-notification-g2k03zyn.js → send-session-notification-04hvd54c.js} +2 -2
  146. package/dist/shared/model-suggestion-retry.d.ts +1 -1
  147. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  148. package/dist/shared/prompt-async-transport.d.ts +7 -7
  149. package/dist/shared/prompt-async-transport.d.ts.map +1 -1
  150. package/dist/shared/rpc-server.d.ts.map +1 -1
  151. package/dist/shared/rpc-utils.d.ts +21 -0
  152. package/dist/shared/rpc-utils.d.ts.map +1 -1
  153. package/dist/shared/run-token-log.d.ts +13 -0
  154. package/dist/shared/run-token-log.d.ts.map +1 -0
  155. package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -1
  156. package/dist/shared/sqlite.d.ts +19 -0
  157. package/dist/shared/sqlite.d.ts.map +1 -1
  158. package/dist/shared/subagent-runner.d.ts +3 -1
  159. package/dist/shared/subagent-runner.d.ts.map +1 -1
  160. package/dist/shared/user-facing-codes.d.ts +5 -0
  161. package/dist/shared/user-facing-codes.d.ts.map +1 -1
  162. package/dist/{task-config-mgf5nsh3.js → task-config-2bm68628.js} +1 -1
  163. package/dist/transform-postprocess-phase-z2mfc5w1.js +36 -0
  164. package/dist/v2/hidden-completion.d.ts +9 -1
  165. package/dist/v2/hidden-completion.d.ts.map +1 -1
  166. package/dist/v2/hooks/context.d.ts.map +1 -1
  167. package/dist/v2/hooks/hidden-child.d.ts +1 -0
  168. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  169. package/dist/v2/hooks/hidden-terminal-error.d.ts +8 -0
  170. package/dist/v2/hooks/hidden-terminal-error.d.ts.map +1 -0
  171. package/dist/v2/hooks/lkg-system.d.ts +11 -0
  172. package/dist/v2/hooks/lkg-system.d.ts.map +1 -0
  173. package/dist/v2/hooks/session-project-backfill.d.ts +1 -1
  174. package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -1
  175. package/dist/v2/hooks/storage-gate.d.ts +11 -5
  176. package/dist/v2/hooks/storage-gate.d.ts.map +1 -1
  177. package/dist/v2/hooks/tools.d.ts +1 -1
  178. package/dist/v2/hooks/tools.d.ts.map +1 -1
  179. package/dist/v2/host-service.d.ts +2 -1
  180. package/dist/v2/host-service.d.ts.map +1 -1
  181. package/dist/v2/server.js +24948 -22659
  182. package/dist/v2/store-reader.d.ts +2 -0
  183. package/dist/v2/store-reader.d.ts.map +1 -1
  184. package/dist/{wrapup-orchestrator-35y56636.js → wrapup-orchestrator-sy4m56a7.js} +7 -6
  185. package/package.json +1 -1
  186. package/src/agents/dreamer.ts +4 -6
  187. package/src/agents/hidden-agent-registrations.ts +3 -3
  188. package/src/agents/permissions.test.ts +3 -4
  189. package/src/config/latch-permanence-guard.test.ts +13 -0
  190. package/src/config/live-child-output-cap.test.ts +26 -0
  191. package/src/config/live-child-output-cap.ts +6 -2
  192. package/src/config/schema/magic-context.ts +21 -6
  193. package/src/features/magic-context/dreamer/task-registry.ts +13 -0
  194. package/src/shared/data-path.test.ts +1 -1
  195. package/src/shared/logger.test.ts +1 -0
  196. package/src/shared/model-suggestion-retry.ts +9 -2
  197. package/src/shared/models-dev-cache.test.ts +30 -1
  198. package/src/shared/prompt-async-transport.test.ts +254 -1
  199. package/src/shared/prompt-async-transport.ts +153 -44
  200. package/src/shared/rpc-async-probes.test.ts +230 -0
  201. package/src/shared/rpc-server.ts +6 -4
  202. package/src/shared/rpc-utils.test.ts +27 -1
  203. package/src/shared/rpc-utils.ts +161 -9
  204. package/src/shared/run-token-log.test.ts +65 -0
  205. package/src/shared/run-token-log.ts +39 -0
  206. package/src/shared/sqlite-acquisition-fence.test.ts +132 -0
  207. package/src/shared/sqlite-acquisition-routing.test.ts +102 -0
  208. package/src/shared/sqlite-pass-scope.test.ts +153 -0
  209. package/src/shared/sqlite-transaction-mode-guard.test.ts +42 -10
  210. package/src/shared/sqlite-write-locker-test-support.ts +1 -0
  211. package/src/shared/sqlite.ts +245 -56
  212. package/src/shared/storage-busy.test.ts +72 -0
  213. package/src/shared/subagent-runner.ts +4 -0
  214. package/src/shared/user-facing-codes.ts +6 -0
  215. package/src/shared/windows-hidden-spawns.test.ts +132 -0
  216. package/src/shared/write-transaction-attribution.test.ts +14 -11
@@ -0,0 +1,132 @@
1
+ import { expect, test } from "bun:test";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
+ import { join, relative, resolve } from "node:path";
4
+ import ts from "typescript";
5
+
6
+ const root = resolve(import.meta.dir, "../../../..");
7
+ const shared = "packages/plugin/src/shared/sqlite.ts";
8
+ function files(dir: string): string[] {
9
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
10
+ const path = join(dir, entry.name);
11
+ if (entry.isDirectory()) return files(path);
12
+ return /\.tsx?$/.test(path) && !/\.test\.|test-support|\.d\.ts$/.test(path) ? [path] : [];
13
+ });
14
+ }
15
+
16
+ function scan(
17
+ path: string,
18
+ text: string,
19
+ ): { violations: string[]; transactions: number; literals: number } {
20
+ const violations: string[] = [];
21
+ let transactions = 0;
22
+ let literals = 0;
23
+ const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
24
+ const visit = (node: ts.Node) => {
25
+ if (
26
+ ts.isCallExpression(node) &&
27
+ ts.isPropertyAccessExpression(node.expression) &&
28
+ node.expression.name.text === "transaction"
29
+ )
30
+ transactions++;
31
+ if (
32
+ path !== shared &&
33
+ ts.isImportDeclaration(node) &&
34
+ ts.isStringLiteral(node.moduleSpecifier) &&
35
+ !node.importClause?.isTypeOnly &&
36
+ /^(bun:sqlite|node:sqlite|better-sqlite3)$/.test(node.moduleSpecifier.text)
37
+ ) {
38
+ violations.push(`${path}: native SQLite import bypasses shared acquisition`);
39
+ }
40
+ if (
41
+ path !== shared &&
42
+ ts.isExportDeclaration(node) &&
43
+ !node.isTypeOnly &&
44
+ node.moduleSpecifier &&
45
+ ts.isStringLiteral(node.moduleSpecifier) &&
46
+ /^(bun:sqlite|node:sqlite|better-sqlite3)$/.test(node.moduleSpecifier.text)
47
+ ) {
48
+ violations.push(`${path}: native SQLite re-export bypasses shared acquisition`);
49
+ }
50
+ if (
51
+ path !== shared &&
52
+ ts.isCallExpression(node) &&
53
+ (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
54
+ (ts.isIdentifier(node.expression) && node.expression.text === "require")) &&
55
+ /(?:bun:sqlite|node:sqlite|better-sqlite3)/.test(
56
+ node.arguments.map((arg) => arg.getText(source)).join(""),
57
+ )
58
+ ) {
59
+ violations.push(`${path}: dynamic native SQLite import bypasses shared acquisition`);
60
+ }
61
+ const sqlText = ts.isStringLiteralLike(node)
62
+ ? node.text
63
+ : ts.isTemplateExpression(node)
64
+ ? node.head.text + node.templateSpans.map((span) => span.literal.text).join("")
65
+ : undefined;
66
+ if (
67
+ sqlText !== undefined &&
68
+ /\bBEGIN\s+(?:IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*(?:;|$)/i.test(sqlText)
69
+ ) {
70
+ literals++;
71
+ if (path !== shared) {
72
+ const call = node.parent;
73
+ const routed =
74
+ !ts.isTemplateExpression(node) &&
75
+ ts.isCallExpression(call) &&
76
+ ts.isPropertyAccessExpression(call.expression) &&
77
+ call.expression.name.text === "exec" &&
78
+ /^\s*BEGIN\s+(IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*;?\s*$/i.test(sqlText);
79
+ if (!routed)
80
+ violations.push(
81
+ `${path}:${source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1}: BEGIN acquisition must be a standalone shared Database.exec call`,
82
+ );
83
+ }
84
+ }
85
+ ts.forEachChild(node, visit);
86
+ };
87
+ visit(source);
88
+ return { violations, transactions, literals };
89
+ }
90
+
91
+ test("all production transaction acquisition routes through the shared SQLite runtime", () => {
92
+ const scans = ["packages/plugin/src", "packages/pi-plugin/src", "packages/cli/src"]
93
+ .flatMap((directory) => files(join(root, directory)))
94
+ .map((path) => scan(relative(root, path), readFileSync(path, "utf8")));
95
+ expect(scans.reduce((sum, scan) => sum + scan.transactions, 0)).toBeGreaterThan(50);
96
+ expect(scans.reduce((sum, scan) => sum + scan.literals, 0)).toBeGreaterThan(20);
97
+ expect(scans.flatMap((scan) => scan.violations)).toEqual([]);
98
+ });
99
+
100
+ test("acquisition fence rejects raw native transactions and unrouted BEGIN shapes", () => {
101
+ expect(
102
+ scan(
103
+ "new-writer.ts",
104
+ 'import { Database } from "bun:sqlite"; new Database(":memory:").transaction(() => write())();',
105
+ ).violations,
106
+ ).toHaveLength(1);
107
+ expect(
108
+ scan(
109
+ "new-writer.ts",
110
+ 'const { DatabaseSync } = require("node:sqlite"); new DatabaseSync(":memory:").transaction(() => write())();',
111
+ ).violations,
112
+ ).toHaveLength(1);
113
+ expect(scan("new-writer.ts", 'export { Database } from "bun:sqlite";').violations).toHaveLength(
114
+ 1,
115
+ );
116
+ expect(scan("new-writer.ts", 'db.prepare("BEGIN IMMEDIATE").run();').violations).toHaveLength(
117
+ 1,
118
+ );
119
+ expect(
120
+ scan("new-writer.ts", 'db.exec("BEGIN IMMEDIATE; INSERT INTO result VALUES (1)");')
121
+ .violations,
122
+ ).toHaveLength(1);
123
+ expect(scan("new-writer.ts", "db.exec(`BEGIN IMMEDIATE; ${extra}`);").violations).toHaveLength(
124
+ 1,
125
+ );
126
+ expect(
127
+ scan(
128
+ "new-writer.ts",
129
+ 'import { Database } from "./shared/sqlite"; db.exec("BEGIN IMMEDIATE"); db.transaction(() => write()).immediate();',
130
+ ).violations,
131
+ ).toEqual([]);
132
+ });
@@ -0,0 +1,102 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ Database,
7
+ withAsyncPrivilegedWriter,
8
+ withPrivilegedWriter,
9
+ withSqliteTransformPass,
10
+ } from "./sqlite";
11
+ import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
12
+
13
+ for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
14
+ test(`shared SQLite retries ${mode} acquisition before running any writes`, async () => {
15
+ const dir = mkdtempSync(join(tmpdir(), "mc-routed-acquisition-"));
16
+ const path = join(dir, "context.db");
17
+ const db = new Database(path);
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
+ );
21
+ const locker = await startSqliteWriteLocker(path, 650);
22
+ let calls = 0;
23
+ const write = () => {
24
+ calls++;
25
+ db.prepare("INSERT INTO result VALUES (?)").run("once");
26
+ return "managed";
27
+ };
28
+ try {
29
+ await withSqliteTransformPass(async () => {
30
+ await withAsyncPrivilegedWriter(db, () => undefined);
31
+ if (mode === "literal") {
32
+ db.exec("BEGIN IMMEDIATE");
33
+ write();
34
+ db.exec("COMMIT");
35
+ } else {
36
+ const transaction = db.transaction(write);
37
+ expect(mode === "default" ? transaction() : transaction[mode]()).toBe(
38
+ "managed",
39
+ );
40
+ }
41
+ });
42
+ expect(calls).toBe(1);
43
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([{ value: "once" }]);
44
+ } finally {
45
+ await locker.exited;
46
+ db.close();
47
+ rmSync(dir, { recursive: true, force: true });
48
+ }
49
+ }, 30000);
50
+ }
51
+
52
+ test("routed transactions preserve nesting, receiver, arguments and rollback without callback retries", () => {
53
+ const db = new Database(":memory:");
54
+ db.exec(
55
+ "CREATE TABLE result(value TEXT); CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
56
+ );
57
+ let calls = 0;
58
+ const transaction = db.transaction(function (this: { prefix: string }, value: string) {
59
+ calls++;
60
+ db.prepare("INSERT INTO result VALUES (?)").run(this.prefix + value);
61
+ db.transaction(() => {
62
+ db.exec("INSERT INTO result VALUES ('inner')");
63
+ }).immediate();
64
+ throw Object.assign(new Error("busy after writes"), { code: "SQLITE_BUSY" });
65
+ });
66
+ try {
67
+ expect(() =>
68
+ withSqliteTransformPass(() => transaction.call({ prefix: "outer-" }, "value")),
69
+ ).toThrow("busy after writes");
70
+ expect(calls).toBe(1);
71
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
72
+ } finally {
73
+ db.close();
74
+ }
75
+ });
76
+
77
+ test("exhausted routed acquisition never enters the callback or multiplies privileged retries", () => {
78
+ const dir = mkdtempSync(join(tmpdir(), "mc-routed-exhaustion-"));
79
+ const path = join(dir, "context.db");
80
+ const blocker = new Database(path);
81
+ const db = new Database(path);
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
+ );
85
+ db.exec("PRAGMA busy_timeout=0");
86
+ blocker.exec("BEGIN IMMEDIATE");
87
+ let callbacks = 0;
88
+ try {
89
+ for (const run of [
90
+ () => db.transaction(() => callbacks++)(),
91
+ () => withPrivilegedWriter(db, () => callbacks++),
92
+ ]) {
93
+ expect(() => withSqliteTransformPass(run)).toThrow("acquisition remained busy");
94
+ expect(callbacks).toBe(0);
95
+ }
96
+ } finally {
97
+ blocker.exec("ROLLBACK");
98
+ blocker.close();
99
+ db.close();
100
+ rmSync(dir, { recursive: true, force: true });
101
+ }
102
+ });
@@ -0,0 +1,153 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ beginSqliteWriterAsync,
7
+ Database,
8
+ isTransientSqliteError,
9
+ SqliteAcquisitionBusyError,
10
+ withAsyncPrivilegedWriter,
11
+ withPrivilegedWriter,
12
+ withSqliteBackgroundWriter,
13
+ withSqliteTransformPass,
14
+ } from "./sqlite";
15
+ import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
16
+
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);
38
+ try {
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 });
48
+ } finally {
49
+ await locker.exited;
50
+ close();
51
+ }
52
+ }, 30000);
53
+
54
+ test("named background acquisition gives up quickly and leaves work for retry", () => {
55
+ const { db, path, close } = fixture();
56
+ const blocker = new Database(path);
57
+ blocker.exec("BEGIN IMMEDIATE");
58
+ let calls = 0;
59
+ let retryQueued = false;
60
+ try {
61
+ const start = performance.now();
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 });
72
+ } finally {
73
+ blocker.exec("ROLLBACK");
74
+ blocker.close();
75
+ close();
76
+ }
77
+ });
78
+
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;
84
+ try {
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 });
96
+ } finally {
97
+ await locker.exited;
98
+ close();
99
+ }
100
+ }, 30000);
101
+
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");
106
+ try {
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 });
114
+ } finally {
115
+ blocker.exec("ROLLBACK");
116
+ blocker.close();
117
+ close();
118
+ }
119
+ });
120
+
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;
125
+ try {
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 });
137
+ } finally {
138
+ await locker.exited;
139
+ close();
140
+ }
141
+ }, 30000);
142
+
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);
146
+ try {
147
+ withSqliteTransformPass(() => withPrivilegedWriter(db, () => undefined));
148
+ expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
149
+ } finally {
150
+ await locker.exited;
151
+ close();
152
+ }
153
+ }, 30000);
@@ -4,14 +4,14 @@ import { join, relative, resolve } from "node:path";
4
4
  import ts from "typescript";
5
5
 
6
6
  /**
7
- * A deferred transaction (`db.transaction(fn)()`, `.deferred()`, `.default()`)
8
- * starts with no lock. If it reads before it writes, the write must upgrade a read
7
+ * An explicitly deferred transaction starts with no writer lock. If it reads before it writes, the write must upgrade a read
9
8
  * snapshot to a write lock, and SQLite never runs the busy handler for that
10
9
  * upgrade: under another process's write it fails at once with SQLITE_BUSY, or
11
10
  * with SQLITE_BUSY_SNAPSHOT when another commit landed after the read, and
12
11
  * busy_timeout never applies. Many OpenCode and Pi processes share one context.db,
13
- * so every transaction that writes must start with `.immediate()` (or
14
- * `.exclusive()`), which takes the write lock at BEGIN where busy_timeout covers it.
12
+ * so writing transactions must acquire at BEGIN. The shared runtime defaults
13
+ * writable handles to IMMEDIATE; plugin/Pi call sites keep their explicit
14
+ * `.immediate()` / `.exclusive()` declarations as an additional intent fence.
15
15
  *
16
16
  * Only read-only transactions may stay deferred. Each one is listed here, keyed
17
17
  * by file and the nearest named enclosing function, with the reason it never
@@ -26,7 +26,7 @@ const READ_ONLY_DEFERRED_TRANSACTIONS: Record<string, string> = {
26
26
  };
27
27
 
28
28
  const REPOSITORY_ROOT = resolve(import.meta.dir, "../../../..");
29
- const SOURCE_ROOTS = ["packages/plugin/src", "packages/pi-plugin/src"];
29
+ const SOURCE_ROOTS = ["packages/plugin/src", "packages/pi-plugin/src", "packages/cli/src"];
30
30
  const LOCKING_MODES = new Set(["immediate", "exclusive"]);
31
31
 
32
32
  function sourceFiles(directory: string): string[] {
@@ -56,13 +56,16 @@ function isTransactionCall(node: ts.Node): node is ts.CallExpression {
56
56
  }
57
57
 
58
58
  /** True when `node` is the receiver of a called `.immediate()` / `.exclusive()`. */
59
- function isCalledWithLockingMode(node: ts.Node): boolean {
59
+ function isCalledWithLockingMode(
60
+ node: ts.Node,
61
+ modes: ReadonlySet<string> = LOCKING_MODES,
62
+ ): boolean {
60
63
  const access = node.parent;
61
64
  return (
62
65
  access !== undefined &&
63
66
  ts.isPropertyAccessExpression(access) &&
64
67
  access.expression === node &&
65
- LOCKING_MODES.has(access.name.text) &&
68
+ modes.has(access.name.text) &&
66
69
  access.parent !== undefined &&
67
70
  ts.isCallExpression(access.parent) &&
68
71
  access.parent.expression === access
@@ -92,7 +95,11 @@ function enclosingFunctionName(node: ts.Node): string {
92
95
  * A transaction stored in a variable is locking only when every use of that
93
96
  * variable calls `.immediate()` or `.exclusive()` on it.
94
97
  */
95
- function storedTransactionLocks(call: ts.CallExpression, source: ts.SourceFile): boolean {
98
+ function storedTransactionLocks(
99
+ call: ts.CallExpression,
100
+ source: ts.SourceFile,
101
+ modes: ReadonlySet<string> = LOCKING_MODES,
102
+ ): boolean {
96
103
  const declaration = call.parent;
97
104
  if (!ts.isVariableDeclaration(declaration) || !ts.isIdentifier(declaration.name)) return false;
98
105
  const name = declaration.name.text;
@@ -111,7 +118,7 @@ function storedTransactionLocks(call: ts.CallExpression, source: ts.SourceFile):
111
118
  ts.forEachChild(node, visit);
112
119
  };
113
120
  visit(scope);
114
- return uses.length > 0 && uses.every(isCalledWithLockingMode);
121
+ return uses.length > 0 && uses.every((use) => isCalledWithLockingMode(use, modes));
115
122
  }
116
123
 
117
124
  interface TransactionScan {
@@ -154,8 +161,33 @@ function repositorySources(): Array<{ path: string; text: string }> {
154
161
  }
155
162
 
156
163
  describe("SQLite transaction lock mode", () => {
164
+ it("declares every audited read-only transaction explicitly deferred", () => {
165
+ const candidates: string[] = [];
166
+ for (const file of repositorySources()) {
167
+ const source = ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, true);
168
+ const visit = (node: ts.Node): void => {
169
+ if (isTransactionCall(node)) {
170
+ const key = `${file.path}#${enclosingFunctionName(node)}`;
171
+ if (key in READ_ONLY_DEFERRED_TRANSACTIONS) {
172
+ const deferred = new Set(["deferred"]);
173
+ if (
174
+ !isCalledWithLockingMode(node, deferred) &&
175
+ !storedTransactionLocks(node, source, deferred)
176
+ )
177
+ candidates.push(key);
178
+ }
179
+ }
180
+ ts.forEachChild(node, visit);
181
+ };
182
+ visit(source);
183
+ }
184
+ expect(candidates.sort()).toEqual([]);
185
+ });
186
+
157
187
  it("starts every writing transaction with .immediate() or .exclusive()", () => {
158
- const { deferredSites, lockingSites } = scanTransactionModes(repositorySources());
188
+ const { deferredSites, lockingSites } = scanTransactionModes(
189
+ repositorySources().filter((file) => !file.path.startsWith("packages/cli/")),
190
+ );
159
191
  // The scan must actually see the codebase's transactions.
160
192
  expect(lockingSites).toBeGreaterThan(50);
161
193
 
@@ -26,6 +26,7 @@ export async function startSqliteWriteLocker(
26
26
  const child = Bun.spawn(["bun", import.meta.path, dbPath, String(holdMs)], {
27
27
  stdout: "pipe",
28
28
  stderr: "pipe",
29
+ windowsHide: true,
29
30
  });
30
31
  const reader = child.stdout.getReader();
31
32
  const decoder = new TextDecoder();