@cortexkit/opencode-magic-context 0.44.0 → 0.44.2

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 (237) 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/agents/magic-context-prompt.d.ts +2 -0
  5. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  6. package/dist/{announcement-1a8bvswd.js → announcement-n8hs7kjw.js} +2 -2
  7. package/dist/config/live-child-output-cap.d.ts +18 -0
  8. package/dist/config/live-child-output-cap.d.ts.map +1 -0
  9. package/dist/config/schema/magic-context.d.ts +64 -11
  10. package/dist/config/schema/magic-context.d.ts.map +1 -1
  11. package/dist/{conflict-warning-hook-48f7np9d.js → conflict-warning-hook-1msyvb51.js} +6 -6
  12. package/dist/{derive-budgets-8kgevxmx.js → derive-budgets-gd2270s0.js} +3 -3
  13. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  14. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +11 -0
  15. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  16. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/docs-proposals.d.ts +2 -0
  18. package/dist/features/magic-context/dreamer/docs-proposals.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/open-opencode-db.d.ts +2 -0
  25. package/dist/features/magic-context/dreamer/open-opencode-db.d.ts.map +1 -1
  26. package/dist/features/magic-context/dreamer/primer-seed.d.ts +11 -5
  27. package/dist/features/magic-context/dreamer/primer-seed.d.ts.map +1 -1
  28. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +7 -2
  29. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  30. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +2 -2
  31. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
  32. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +8 -1
  33. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  34. package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
  35. package/dist/features/magic-context/dreamer/task-executor.d.ts +5 -0
  36. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  37. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  38. package/dist/features/magic-context/dreamer/task-prompts.d.ts +18 -2
  39. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  40. package/dist/features/magic-context/dreamer/task-registry.d.ts +12 -0
  41. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  42. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -1
  43. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  44. package/dist/features/magic-context/dreamer/token-budget.d.ts +33 -0
  45. package/dist/features/magic-context/dreamer/token-budget.d.ts.map +1 -0
  46. package/dist/features/magic-context/dreamer/verify-diff.d.ts +15 -0
  47. package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -0
  48. package/dist/features/magic-context/dreamer/verify-gate.d.ts.map +1 -1
  49. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
  50. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  51. package/dist/features/magic-context/dreamer/verify-read-cap.d.ts +4 -0
  52. package/dist/features/magic-context/dreamer/verify-read-cap.d.ts.map +1 -0
  53. package/dist/features/magic-context/dreamer/verify.d.ts +5 -0
  54. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  55. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  56. package/dist/features/magic-context/memory/project-identity-cache.d.ts +6 -0
  57. package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -0
  58. package/dist/features/magic-context/memory/project-identity.d.ts +15 -22
  59. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  60. package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -0
  61. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  62. package/dist/features/magic-context/memory/verification-paths.d.ts +1 -0
  63. package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
  64. package/dist/features/magic-context/mural/mural-font.generated.d.ts.map +1 -1
  65. package/dist/features/magic-context/project-embedding-registry.d.ts +5 -0
  66. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  67. package/dist/features/magic-context/search.d.ts.map +1 -1
  68. package/dist/features/magic-context/session-activity.d.ts +10 -0
  69. package/dist/features/magic-context/session-activity.d.ts.map +1 -0
  70. package/dist/features/magic-context/session-cache-ttl.d.ts +10 -0
  71. package/dist/features/magic-context/session-cache-ttl.d.ts.map +1 -0
  72. package/dist/features/magic-context/session-project-backfill.d.ts +4 -1
  73. package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
  74. package/dist/features/magic-context/smart-notes/compiler.d.ts +5 -0
  75. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  76. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +1 -0
  77. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
  78. package/dist/features/magic-context/smart-notes/storage.d.ts +1 -1
  79. package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
  80. package/dist/features/magic-context/smart-notes/types.d.ts +2 -0
  81. package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
  82. package/dist/features/magic-context/storage-db.d.ts +2 -1
  83. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  84. package/dist/features/magic-context/storage-identity-merge.d.ts +4 -1
  85. package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
  86. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  87. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -1
  88. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/compartment-runner-historian.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.d.ts +1 -0
  93. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/embed-session-state.d.ts +3 -0
  96. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/event-handler.d.ts +1 -0
  98. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/event-payloads.d.ts +1 -0
  100. package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  104. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/lkg-replay.d.ts +6 -4
  107. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts +12 -0
  111. package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts.map +1 -0
  112. package/dist/hooks/magic-context/read-session-chunk.d.ts +16 -0
  113. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/read-session-db.d.ts +5 -1
  115. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  116. package/dist/hooks/magic-context/read-session-raw.d.ts +10 -0
  117. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  119. package/dist/hooks/magic-context/storage-busy-refusal.d.ts +7 -0
  120. package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -0
  121. package/dist/hooks/magic-context/strip-content.d.ts +1 -0
  122. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  123. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/transform.d.ts +1 -0
  125. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  126. package/dist/{index-5f4qsnaq.js → index-18tjha8g.js} +2 -2
  127. package/dist/{index-4b30nf6z.js → index-32b096cc.js} +1 -1
  128. package/dist/{index-tjq5jztd.js → index-3bygzk2d.js} +1 -1
  129. package/dist/{index-cj5qe2nw.js → index-3x9caerx.js} +8 -6
  130. package/dist/index-40s1xz22.js +125 -0
  131. package/dist/{index-pye48m79.js → index-egkfvhms.js} +130 -42
  132. package/dist/{index-2zf5fv89.js → index-esxfcz5z.js} +171 -12
  133. package/dist/index-gtzcmv90.js +7260 -0
  134. package/dist/{index-6w9p73jz.js → index-jqcbs2k1.js} +10 -0
  135. package/dist/{index-v6wsttqt.js → index-kr5qehbd.js} +1 -1
  136. package/dist/{index-dx0xnd4q.js → index-kwe03yde.js} +1 -1
  137. package/dist/{index-c906f3ff.js → index-r9vkp4n9.js} +4 -4
  138. package/dist/{index-6z9g64js.js → index-s70wtd8k.js} +4 -1
  139. package/dist/{index-01ft0h3p.js → index-stb5fg12.js} +2 -2
  140. package/dist/{index-rdey8znw.js → index-ttdn47aq.js} +35260 -32540
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +15123 -23504
  143. package/dist/{message-time-backfill-rm9wmcem.js → message-time-backfill-q374fxc5.js} +3 -3
  144. package/dist/plugin/dream-timer.d.ts.map +1 -1
  145. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  146. package/dist/plugin/messages-transform.d.ts +10 -9
  147. package/dist/plugin/messages-transform.d.ts.map +1 -1
  148. package/dist/plugin/rpc-handlers.d.ts +1 -0
  149. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  150. package/dist/{read-session-chunk-ktz5zt70.js → read-session-chunk-wmvgnd24.js} +6 -4
  151. package/dist/{recomp-orchestrator-ahekzt0s.js → recomp-orchestrator-rk40ff15.js} +10 -9
  152. package/dist/{safe-notification-target-6w0esgd6.js → safe-notification-target-50gm9c58.js} +3 -3
  153. package/dist/{send-session-notification-w7xbnrsm.js → send-session-notification-6xsrcxnp.js} +5 -5
  154. package/dist/shared/cache-ttl-display.d.ts +3 -1
  155. package/dist/shared/cache-ttl-display.d.ts.map +1 -1
  156. package/dist/shared/harness.d.ts +11 -0
  157. package/dist/shared/harness.d.ts.map +1 -1
  158. package/dist/shared/model-cache-ttl.d.ts +9 -0
  159. package/dist/shared/model-cache-ttl.d.ts.map +1 -0
  160. package/dist/shared/model-suggestion-retry.d.ts +1 -1
  161. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  162. package/dist/shared/prompt-async-transport.d.ts +7 -7
  163. package/dist/shared/prompt-async-transport.d.ts.map +1 -1
  164. package/dist/shared/rpc-server.d.ts.map +1 -1
  165. package/dist/shared/rpc-types.d.ts +1 -1
  166. package/dist/shared/rpc-types.d.ts.map +1 -1
  167. package/dist/shared/rpc-utils.d.ts +21 -0
  168. package/dist/shared/rpc-utils.d.ts.map +1 -1
  169. package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -1
  170. package/dist/shared/sqlite.d.ts +12 -0
  171. package/dist/shared/sqlite.d.ts.map +1 -1
  172. package/dist/shared/status-view.d.ts +1 -1
  173. package/dist/shared/status-view.d.ts.map +1 -1
  174. package/dist/shared/subagent-runner.d.ts +3 -1
  175. package/dist/shared/subagent-runner.d.ts.map +1 -1
  176. package/dist/shared/user-facing-codes.d.ts +5 -0
  177. package/dist/shared/user-facing-codes.d.ts.map +1 -1
  178. package/dist/{task-config-mgf5nsh3.js → task-config-2bm68628.js} +1 -1
  179. package/dist/tools/ctx-expand/render.d.ts.map +1 -1
  180. package/dist/transform-postprocess-phase-t7xgdhdf.js +36 -0
  181. package/dist/v2/hidden-completion.d.ts +2 -0
  182. package/dist/v2/hidden-completion.d.ts.map +1 -1
  183. package/dist/v2/hooks/context.d.ts.map +1 -1
  184. package/dist/v2/hooks/hidden-child.d.ts +1 -0
  185. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  186. package/dist/v2/hooks/lkg-system.d.ts +11 -0
  187. package/dist/v2/hooks/lkg-system.d.ts.map +1 -0
  188. package/dist/v2/hooks/session-project-backfill.d.ts +1 -1
  189. package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -1
  190. package/dist/v2/hooks/storage-gate.d.ts +11 -5
  191. package/dist/v2/hooks/storage-gate.d.ts.map +1 -1
  192. package/dist/v2/host-service.d.ts +2 -1
  193. package/dist/v2/host-service.d.ts.map +1 -1
  194. package/dist/v2/server.js +95347 -93236
  195. package/dist/v2/store-reader.d.ts +2 -0
  196. package/dist/v2/store-reader.d.ts.map +1 -1
  197. package/dist/wrapup-orchestrator-h8tr10c6.js +19 -0
  198. package/package.json +1 -1
  199. package/src/agents/dreamer.ts +4 -6
  200. package/src/agents/hidden-agent-registrations.ts +3 -3
  201. package/src/agents/magic-context-prompt.test.ts +125 -0
  202. package/src/agents/magic-context-prompt.ts +12 -7
  203. package/src/agents/permissions.test.ts +3 -4
  204. package/src/config/latch-permanence-guard.test.ts +9 -0
  205. package/src/config/live-child-output-cap.test.ts +101 -0
  206. package/src/config/live-child-output-cap.ts +74 -0
  207. package/src/config/live-run-config.test.ts +24 -11
  208. package/src/config/schema/magic-context.test.ts +16 -1
  209. package/src/config/schema/magic-context.ts +27 -10
  210. package/src/features/magic-context/dreamer/task-registry.ts +13 -0
  211. package/src/shared/cache-ttl-display.ts +14 -20
  212. package/src/shared/harness.ts +14 -0
  213. package/src/shared/logger.test.ts +1 -0
  214. package/src/shared/model-cache-ttl.test.ts +40 -0
  215. package/src/shared/model-cache-ttl.ts +58 -0
  216. package/src/shared/model-suggestion-retry.ts +9 -2
  217. package/src/shared/prompt-async-transport.test.ts +254 -1
  218. package/src/shared/prompt-async-transport.ts +153 -44
  219. package/src/shared/prompt-surface-a1-golden.md +13 -13
  220. package/src/shared/rpc-async-probes.test.ts +230 -0
  221. package/src/shared/rpc-server.ts +6 -4
  222. package/src/shared/rpc-types.ts +1 -1
  223. package/src/shared/rpc-utils.test.ts +27 -1
  224. package/src/shared/rpc-utils.ts +161 -9
  225. package/src/shared/sqlite-acquisition-fence.test.ts +132 -0
  226. package/src/shared/sqlite-acquisition-routing.test.ts +94 -0
  227. package/src/shared/sqlite-pass-scope.test.ts +182 -0
  228. package/src/shared/sqlite-transaction-mode-guard.test.ts +42 -10
  229. package/src/shared/sqlite-write-locker-test-support.ts +1 -0
  230. package/src/shared/sqlite.ts +142 -56
  231. package/src/shared/status-view.ts +1 -1
  232. package/src/shared/storage-busy.test.ts +95 -0
  233. package/src/shared/subagent-runner.ts +4 -0
  234. package/src/shared/user-facing-codes.ts +6 -0
  235. package/src/shared/windows-hidden-spawns.test.ts +132 -0
  236. package/src/shared/write-transaction-attribution.test.ts +1 -0
  237. package/dist/wrapup-orchestrator-9srk0kjf.js +0 -18
@@ -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,94 @@
1
+ import { expect, spyOn, 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 { Database, withPrivilegedWriter, withSqliteTransformPass } from "./sqlite";
6
+ import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
7
+
8
+ for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
9
+ test(`shared SQLite retries ${mode} acquisition before running any writes`, async () => {
10
+ const dir = mkdtempSync(join(tmpdir(), "mc-routed-acquisition-"));
11
+ const path = join(dir, "context.db");
12
+ const db = new Database(path);
13
+ db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=50; CREATE TABLE result(value TEXT)");
14
+ const locker = await startSqliteWriteLocker(path, 650);
15
+ let calls = 0;
16
+ const write = () => {
17
+ calls++;
18
+ db.prepare("INSERT INTO result VALUES (?)").run("once");
19
+ return "managed";
20
+ };
21
+ try {
22
+ withSqliteTransformPass(() => {
23
+ if (mode === "literal") {
24
+ db.exec("BEGIN IMMEDIATE");
25
+ write();
26
+ db.exec("COMMIT");
27
+ } else {
28
+ const transaction = db.transaction(write);
29
+ expect(mode === "default" ? transaction() : transaction[mode]()).toBe(
30
+ "managed",
31
+ );
32
+ }
33
+ });
34
+ expect(calls).toBe(1);
35
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([{ value: "once" }]);
36
+ } finally {
37
+ await locker.exited;
38
+ db.close();
39
+ rmSync(dir, { recursive: true, force: true });
40
+ }
41
+ }, 30000);
42
+ }
43
+
44
+ test("routed transactions preserve nesting, receiver, arguments and rollback without callback retries", () => {
45
+ const db = new Database(":memory:");
46
+ db.exec("CREATE TABLE result(value TEXT)");
47
+ let calls = 0;
48
+ const transaction = db.transaction(function (this: { prefix: string }, value: string) {
49
+ calls++;
50
+ db.prepare("INSERT INTO result VALUES (?)").run(this.prefix + value);
51
+ db.transaction(() => {
52
+ db.exec("INSERT INTO result VALUES ('inner')");
53
+ }).immediate();
54
+ throw Object.assign(new Error("busy after writes"), { code: "SQLITE_BUSY" });
55
+ });
56
+ try {
57
+ expect(() =>
58
+ withSqliteTransformPass(() => transaction.call({ prefix: "outer-" }, "value")),
59
+ ).toThrow("busy after writes");
60
+ expect(calls).toBe(1);
61
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
62
+ } finally {
63
+ db.close();
64
+ }
65
+ });
66
+
67
+ test("exhausted routed acquisition never enters the callback or multiplies privileged retries", () => {
68
+ const dir = mkdtempSync(join(tmpdir(), "mc-routed-exhaustion-"));
69
+ const path = join(dir, "context.db");
70
+ const blocker = new Database(path);
71
+ const db = new Database(path);
72
+ blocker.exec("PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT)");
73
+ db.exec("PRAGMA busy_timeout=0");
74
+ blocker.exec("BEGIN IMMEDIATE");
75
+ const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
76
+ let callbacks = 0;
77
+ try {
78
+ for (const run of [
79
+ () => db.transaction(() => callbacks++)(),
80
+ () => withPrivilegedWriter(db, () => callbacks++),
81
+ ]) {
82
+ wait.mockClear();
83
+ expect(() => withSqliteTransformPass(run)).toThrow("after 3 attempts");
84
+ expect(wait).toHaveBeenCalledTimes(2);
85
+ expect(callbacks).toBe(0);
86
+ }
87
+ } finally {
88
+ wait.mockRestore();
89
+ blocker.exec("ROLLBACK");
90
+ blocker.close();
91
+ db.close();
92
+ rmSync(dir, { recursive: true, force: true });
93
+ }
94
+ });
@@ -0,0 +1,182 @@
1
+ import { expect, spyOn, 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
+ withoutSqliteTransformPass,
8
+ withPrivilegedWriter,
9
+ withSqliteTransformPass,
10
+ } from "./sqlite";
11
+
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;
20
+ try {
21
+ expect(() => withPrivilegedWriter(db, () => callbacks++)).toThrow("original busy");
22
+ expect(exec).toHaveBeenCalledTimes(1);
23
+ expect(wait).not.toHaveBeenCalled();
24
+ expect(callbacks).toBe(0);
25
+ } finally {
26
+ exec.mockRestore();
27
+ wait.mockRestore();
28
+ db.close();
29
+ }
30
+ });
31
+
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");
35
+ 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
+ blocker.exec("BEGIN IMMEDIATE");
40
+ let callbacks = 0;
41
+ try {
42
+ 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);
48
+ } finally {
49
+ blocker.exec("ROLLBACK");
50
+ blocker.close();
51
+ writer.close();
52
+ rmSync(dir, { recursive: true, force: true });
53
+ }
54
+ }, 30000);
55
+
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
+ });
72
+ 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);
101
+ } finally {
102
+ release();
103
+ exec.mockRestore();
104
+ wait.mockRestore();
105
+ db.close();
106
+ }
107
+ });
108
+
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");
115
+ 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();
124
+ } finally {
125
+ exec.mockRestore();
126
+ wait.mockRestore();
127
+ db.close();
128
+ }
129
+ });
130
+
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')");
142
+ try {
143
+ const rows = withSqliteTransformPass(() =>
144
+ reader.transaction(() => reader.prepare("SELECT value FROM result").all()).deferred(),
145
+ );
146
+ expect(rows).toEqual([{ value: "committed" }]);
147
+ } finally {
148
+ writer.exec("ROLLBACK");
149
+ writer.close();
150
+ reader.close();
151
+ rmSync(dir, { recursive: true, force: true });
152
+ }
153
+ });
154
+
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>;
166
+ 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;
176
+ } finally {
177
+ release();
178
+ exec.mockRestore();
179
+ wait.mockRestore();
180
+ db.close();
181
+ }
182
+ });
@@ -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();