@cortexkit/opencode-magic-context 0.44.1 → 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 (180) 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-1msyvb51.js} +3 -3
  8. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  9. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +11 -0
  10. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  11. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/docs-proposals.d.ts +2 -0
  13. package/dist/features/magic-context/dreamer/docs-proposals.d.ts.map +1 -1
  14. package/dist/features/magic-context/dreamer/manual-summary.d.ts.map +1 -1
  15. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +1 -1
  16. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/map-memories.d.ts +5 -0
  18. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +5 -0
  20. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +2 -2
  22. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +8 -1
  24. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
  26. package/dist/features/magic-context/dreamer/task-executor.d.ts +5 -0
  27. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  28. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  29. package/dist/features/magic-context/dreamer/task-prompts.d.ts +18 -2
  30. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  31. package/dist/features/magic-context/dreamer/task-registry.d.ts +12 -0
  32. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  33. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -1
  34. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  35. package/dist/features/magic-context/dreamer/token-budget.d.ts +33 -0
  36. package/dist/features/magic-context/dreamer/token-budget.d.ts.map +1 -0
  37. package/dist/features/magic-context/dreamer/verify-diff.d.ts +15 -0
  38. package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -0
  39. package/dist/features/magic-context/dreamer/verify-gate.d.ts.map +1 -1
  40. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
  41. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  42. package/dist/features/magic-context/dreamer/verify-read-cap.d.ts +4 -0
  43. package/dist/features/magic-context/dreamer/verify-read-cap.d.ts.map +1 -0
  44. package/dist/features/magic-context/dreamer/verify.d.ts +5 -0
  45. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  46. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  47. package/dist/features/magic-context/memory/project-identity-cache.d.ts +6 -0
  48. package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -0
  49. package/dist/features/magic-context/memory/project-identity.d.ts +8 -22
  50. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  51. package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -0
  52. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  53. package/dist/features/magic-context/memory/verification-paths.d.ts +1 -0
  54. package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
  55. package/dist/features/magic-context/project-embedding-registry.d.ts +5 -0
  56. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  57. package/dist/features/magic-context/search.d.ts.map +1 -1
  58. package/dist/features/magic-context/session-activity.d.ts +10 -0
  59. package/dist/features/magic-context/session-activity.d.ts.map +1 -0
  60. package/dist/features/magic-context/session-project-backfill.d.ts +4 -1
  61. package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
  62. package/dist/features/magic-context/smart-notes/compiler.d.ts +5 -0
  63. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  64. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +1 -0
  65. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
  66. package/dist/features/magic-context/smart-notes/storage.d.ts +1 -1
  67. package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
  68. package/dist/features/magic-context/smart-notes/types.d.ts +2 -0
  69. package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
  70. package/dist/features/magic-context/storage-db.d.ts +2 -1
  71. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  72. package/dist/features/magic-context/storage-identity-merge.d.ts +4 -1
  73. package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
  74. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  75. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -1
  76. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/compartment-runner.d.ts +1 -0
  80. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/embed-session-state.d.ts +3 -0
  83. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/event-handler.d.ts +1 -0
  85. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/event-payloads.d.ts +1 -0
  87. package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/lkg-replay.d.ts +6 -4
  91. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/storage-busy-refusal.d.ts +7 -0
  96. package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -0
  97. package/dist/hooks/magic-context/strip-content.d.ts +1 -0
  98. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  101. package/dist/{index-d107az47.js → index-18tjha8g.js} +1 -1
  102. package/dist/{index-0rp6pj54.js → index-3x9caerx.js} +7 -5
  103. package/dist/index-40s1xz22.js +125 -0
  104. package/dist/{index-cz88kw0n.js → index-egkfvhms.js} +117 -39
  105. package/dist/{index-s6b7h12g.js → index-esxfcz5z.js} +73 -2
  106. package/dist/index-gtzcmv90.js +7260 -0
  107. package/dist/{index-6w9p73jz.js → index-jqcbs2k1.js} +10 -0
  108. package/dist/{index-a9vbtdzv.js → index-r9vkp4n9.js} +1 -1
  109. package/dist/{index-jtdrvmcn.js → index-ttdn47aq.js} +37431 -37044
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +8195 -14449
  112. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  113. package/dist/plugin/messages-transform.d.ts +10 -9
  114. package/dist/plugin/messages-transform.d.ts.map +1 -1
  115. package/dist/plugin/rpc-handlers.d.ts +1 -0
  116. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  117. package/dist/{read-session-chunk-fkdv2g5b.js → read-session-chunk-wmvgnd24.js} +2 -2
  118. package/dist/{recomp-orchestrator-k4h5n0p3.js → recomp-orchestrator-rk40ff15.js} +5 -4
  119. package/dist/{send-session-notification-g2k03zyn.js → send-session-notification-6xsrcxnp.js} +2 -2
  120. package/dist/shared/model-suggestion-retry.d.ts +1 -1
  121. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  122. package/dist/shared/prompt-async-transport.d.ts +7 -7
  123. package/dist/shared/prompt-async-transport.d.ts.map +1 -1
  124. package/dist/shared/rpc-server.d.ts.map +1 -1
  125. package/dist/shared/rpc-utils.d.ts +21 -0
  126. package/dist/shared/rpc-utils.d.ts.map +1 -1
  127. package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -1
  128. package/dist/shared/sqlite.d.ts +12 -0
  129. package/dist/shared/sqlite.d.ts.map +1 -1
  130. package/dist/shared/subagent-runner.d.ts +3 -1
  131. package/dist/shared/subagent-runner.d.ts.map +1 -1
  132. package/dist/shared/user-facing-codes.d.ts +5 -0
  133. package/dist/shared/user-facing-codes.d.ts.map +1 -1
  134. package/dist/{task-config-mgf5nsh3.js → task-config-2bm68628.js} +1 -1
  135. package/dist/transform-postprocess-phase-t7xgdhdf.js +36 -0
  136. package/dist/v2/hidden-completion.d.ts +2 -0
  137. package/dist/v2/hidden-completion.d.ts.map +1 -1
  138. package/dist/v2/hooks/context.d.ts.map +1 -1
  139. package/dist/v2/hooks/hidden-child.d.ts +1 -0
  140. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  141. package/dist/v2/hooks/lkg-system.d.ts +11 -0
  142. package/dist/v2/hooks/lkg-system.d.ts.map +1 -0
  143. package/dist/v2/hooks/session-project-backfill.d.ts +1 -1
  144. package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -1
  145. package/dist/v2/hooks/storage-gate.d.ts +11 -5
  146. package/dist/v2/hooks/storage-gate.d.ts.map +1 -1
  147. package/dist/v2/host-service.d.ts +2 -1
  148. package/dist/v2/host-service.d.ts.map +1 -1
  149. package/dist/v2/server.js +24481 -22623
  150. package/dist/v2/store-reader.d.ts +2 -0
  151. package/dist/v2/store-reader.d.ts.map +1 -1
  152. package/dist/{wrapup-orchestrator-35y56636.js → wrapup-orchestrator-h8tr10c6.js} +6 -5
  153. package/package.json +1 -1
  154. package/src/agents/dreamer.ts +4 -6
  155. package/src/agents/hidden-agent-registrations.ts +3 -3
  156. package/src/agents/permissions.test.ts +3 -4
  157. package/src/config/latch-permanence-guard.test.ts +9 -0
  158. package/src/config/live-child-output-cap.test.ts +26 -0
  159. package/src/config/live-child-output-cap.ts +6 -2
  160. package/src/config/schema/magic-context.ts +21 -6
  161. package/src/features/magic-context/dreamer/task-registry.ts +13 -0
  162. package/src/shared/logger.test.ts +1 -0
  163. package/src/shared/model-suggestion-retry.ts +9 -2
  164. package/src/shared/prompt-async-transport.test.ts +254 -1
  165. package/src/shared/prompt-async-transport.ts +153 -44
  166. package/src/shared/rpc-async-probes.test.ts +230 -0
  167. package/src/shared/rpc-server.ts +6 -4
  168. package/src/shared/rpc-utils.test.ts +27 -1
  169. package/src/shared/rpc-utils.ts +161 -9
  170. package/src/shared/sqlite-acquisition-fence.test.ts +132 -0
  171. package/src/shared/sqlite-acquisition-routing.test.ts +94 -0
  172. package/src/shared/sqlite-pass-scope.test.ts +182 -0
  173. package/src/shared/sqlite-transaction-mode-guard.test.ts +42 -10
  174. package/src/shared/sqlite-write-locker-test-support.ts +1 -0
  175. package/src/shared/sqlite.ts +142 -56
  176. package/src/shared/storage-busy.test.ts +95 -0
  177. package/src/shared/subagent-runner.ts +4 -0
  178. package/src/shared/user-facing-codes.ts +6 -0
  179. package/src/shared/windows-hidden-spawns.test.ts +132 -0
  180. package/src/shared/write-transaction-attribution.test.ts +1 -0
@@ -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();
@@ -39,6 +39,7 @@
39
39
  * `run()` → {changes,lastInsertRowid}) is identical and was verified directly.
40
40
  */
41
41
 
42
+ import { AsyncLocalStorage } from "node:async_hooks";
42
43
  import { statSync } from "node:fs";
43
44
  // Type import only — runtime is loaded dynamically below. @types/better-sqlite3
44
45
  // has the richest definitions and is a structural superset of the API surface
@@ -211,6 +212,70 @@ export interface SqliteMemoryStats {
211
212
  const trackedSqliteConnections = new Map<number, TrackedSqliteConnection>();
212
213
  let nextSqliteConnectionSequence = 1;
213
214
 
215
+ /** Route native Bun and Node transaction entry through the same acquisition retry.
216
+ * Native Bun transaction wrappers execute BEGIN internally, bypassing an exec override,
217
+ * so both runtimes use this small synchronous wrapper instead. Writable handles
218
+ * default to IMMEDIATE; explicit deferred calls and readonly handles keep read
219
+ * snapshots without acquiring the writer lock. */
220
+ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean): void {
221
+ const nativeExec = db.exec.bind(db);
222
+ Object.defineProperty(db, "exec", {
223
+ configurable: true,
224
+ writable: true,
225
+ value: (sql: string) => {
226
+ if (/^\s*BEGIN\s+(?:IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*;?\s*$/i.test(sql)) {
227
+ retryAcquisition(() => nativeExec(sql), sql.trim());
228
+ return db;
229
+ }
230
+ return nativeExec(sql);
231
+ },
232
+ });
233
+ Object.defineProperty(db, "transaction", {
234
+ configurable: true,
235
+ writable: true,
236
+ // biome-ignore lint/suspicious/noExplicitAny: preserve the native callback's receiver and argument types.
237
+ value: (fn: (...args: any[]) => any) => {
238
+ const make = (mode: "IMMEDIATE" | "EXCLUSIVE" | "DEFERRED") =>
239
+ function (this: unknown, ...args: unknown[]): unknown {
240
+ const nested = isInTransaction(db);
241
+ // Equal savepoint names stack LIFO, matching both native backends.
242
+ const savepoint = "mc_tx_sp";
243
+ db.exec(nested ? `SAVEPOINT ${savepoint}` : `BEGIN ${mode}`);
244
+ try {
245
+ const result = fn.apply(this, args);
246
+ db.exec(nested ? `RELEASE ${savepoint}` : "COMMIT");
247
+ return result;
248
+ } catch (error) {
249
+ if (isInTransaction(db)) {
250
+ if (nested) {
251
+ db.exec(`ROLLBACK TO ${savepoint}`);
252
+ db.exec(`RELEASE ${savepoint}`);
253
+ } else db.exec("ROLLBACK");
254
+ }
255
+ throw error;
256
+ }
257
+ };
258
+ const defaultTransaction = make(readonly ? "DEFERRED" : "IMMEDIATE");
259
+ const variants = {
260
+ default: defaultTransaction,
261
+ deferred: make("DEFERRED"),
262
+ immediate: make("IMMEDIATE"),
263
+ exclusive: make("EXCLUSIVE"),
264
+ database: db,
265
+ };
266
+ for (const transaction of [
267
+ variants.default,
268
+ variants.deferred,
269
+ variants.immediate,
270
+ variants.exclusive,
271
+ ]) {
272
+ Object.assign(transaction, variants);
273
+ }
274
+ return defaultTransaction;
275
+ },
276
+ });
277
+ }
278
+
214
279
  function trackSqliteConnection(
215
280
  db: BetterSqlite3.Database,
216
281
  filename: unknown,
@@ -232,6 +297,7 @@ function trackSqliteConnection(
232
297
  ((options as { readonly?: unknown }).readonly === true ||
233
298
  (options as { readOnly?: unknown }).readOnly === true),
234
299
  };
300
+ installTransactionRouting(db, metadata.readonly);
235
301
  Object.defineProperty(db, "close", {
236
302
  configurable: true,
237
303
  value: (...args: unknown[]) => {
@@ -260,18 +326,10 @@ const TrackedDatabase = new Proxy(DatabaseImpl, {
260
326
  * Wrap node:sqlite's `DatabaseSync` so it presents the better-sqlite3/bun
261
327
  * surface the rest of the codebase calls:
262
328
  * - translate the `{ readonly }` constructor option → node:sqlite's `readOnly`
263
- * - add a `transaction(fn)` helper that matches better-sqlite3 semantics,
264
- * using `db.isTransaction` to pick BEGIN (top-level) vs SAVEPOINT (nested),
265
- * so it composes correctly with manual `BEGIN IMMEDIATE` blocks too.
329
+ * Transaction routing is installed for both runtimes by trackSqliteConnection.
266
330
  */
267
331
  // biome-ignore lint/suspicious/noExplicitAny: node:sqlite has no shipped types here; the public export is cast to the better-sqlite3 shape.
268
332
  function buildNodeSqliteDatabaseClass(DatabaseSync: any): typeof BetterSqlite3 {
269
- // Single constant savepoint name is correct for arbitrary nesting depth:
270
- // SQLite savepoints with the same name stack LIFO — RELEASE / ROLLBACK TO
271
- // always target the most recent. node:sqlite is synchronous + single-process
272
- // per connection, so there is no concurrent-savepoint hazard.
273
- const SAVEPOINT = "mc_tx_sp";
274
-
275
333
  class NodeSqliteDatabase extends DatabaseSync {
276
334
  constructor(filename?: string | Buffer, options?: BetterSqlite3.Options) {
277
335
  const translated: Record<string, unknown> = { ...options };
@@ -306,52 +364,6 @@ function buildNodeSqliteDatabaseClass(DatabaseSync: any): typeof BetterSqlite3 {
306
364
  }
307
365
  return stmt;
308
366
  }
309
-
310
- // biome-ignore lint/suspicious/noExplicitAny: mirrors better-sqlite3's generic transaction(fn) signature.
311
- transaction<F extends (...args: any[]) => any>(fn: F): F {
312
- // biome-ignore lint/suspicious/noExplicitAny: faithful pass-through of this/args to fn.
313
- const self = this as any;
314
- const execute = (
315
- mode: "" | "DEFERRED" | "IMMEDIATE" | "EXCLUSIVE",
316
- receiver: unknown,
317
- args: unknown[],
318
- ) => {
319
- const nested = self.isTransaction === true;
320
- self.exec(nested ? `SAVEPOINT ${SAVEPOINT}` : `BEGIN${mode ? ` ${mode}` : ""}`);
321
- try {
322
- const result = fn.apply(receiver, args);
323
- self.exec(nested ? `RELEASE ${SAVEPOINT}` : "COMMIT");
324
- return result;
325
- } catch (error) {
326
- if (nested) {
327
- // ROLLBACK TO unwinds the savepoint's changes but leaves
328
- // it on the stack; RELEASE then pops it (better-sqlite3
329
- // does both).
330
- self.exec(`ROLLBACK TO ${SAVEPOINT}`);
331
- self.exec(`RELEASE ${SAVEPOINT}`);
332
- } else {
333
- self.exec("ROLLBACK");
334
- }
335
- throw error;
336
- }
337
- };
338
- const wrapped = function (this: unknown, ...args: unknown[]): unknown {
339
- return execute("", this, args);
340
- };
341
- wrapped.default = function (this: unknown, ...args: unknown[]): unknown {
342
- return execute("", this, args);
343
- };
344
- wrapped.deferred = function (this: unknown, ...args: unknown[]): unknown {
345
- return execute("DEFERRED", this, args);
346
- };
347
- wrapped.immediate = function (this: unknown, ...args: unknown[]): unknown {
348
- return execute("IMMEDIATE", this, args);
349
- };
350
- wrapped.exclusive = function (this: unknown, ...args: unknown[]): unknown {
351
- return execute("EXCLUSIVE", this, args);
352
- };
353
- return wrapped as unknown as F;
354
- }
355
367
  }
356
368
 
357
369
  return NodeSqliteDatabase as unknown as typeof BetterSqlite3;
@@ -476,6 +488,80 @@ export type Database = BetterSqlite3.Database;
476
488
  export type Statement = BetterSqlite3.Statement<unknown[], unknown>;
477
489
 
478
490
  const privilegeDepth = new WeakMap<Database, number>();
491
+ const transformPassScope = new AsyncLocalStorage<{ active: boolean } | undefined>();
492
+
493
+ /** Only an awaited foreground transform may spend the extra acquisition budget.
494
+ * The mutable lease also expires for detached descendants when that pass ends. */
495
+ export function withSqliteTransformPass<T>(operation: () => T): T {
496
+ const lease = { active: true };
497
+ return transformPassScope.run(lease, () => {
498
+ try {
499
+ const result = operation();
500
+ if (result && typeof (result as { then?: unknown }).then === "function") {
501
+ return Promise.resolve(result).finally(() => {
502
+ lease.active = false;
503
+ }) as T;
504
+ }
505
+ lease.active = false;
506
+ return result;
507
+ } catch (error) {
508
+ lease.active = false;
509
+ throw error;
510
+ }
511
+ });
512
+ }
513
+
514
+ /** Start detached maintenance work without borrowing a foreground pass's budget. */
515
+ export function withoutSqliteTransformPass<T>(operation: () => T): T {
516
+ return transformPassScope.run(undefined, operation);
517
+ }
518
+
519
+ /** Bun names SQLite codes; node:sqlite exposes the numeric (possibly extended) errcode. */
520
+ export function isTransientSqliteError(error: unknown): boolean {
521
+ if (!error || typeof error !== "object") return false;
522
+ const value = error as { code?: unknown; errcode?: unknown };
523
+ return (
524
+ (typeof value.code === "string" && /^(SQLITE_BUSY|SQLITE_LOCKED)(_|$)/.test(value.code)) ||
525
+ (typeof value.errcode === "number" && [5, 6].includes(value.errcode & 0xff))
526
+ );
527
+ }
528
+
529
+ export class SqliteAcquisitionBusyError extends Error {
530
+ readonly code = "SQLITE_BUSY";
531
+ readonly stage: string;
532
+ constructor(cause: unknown, stage = "BEGIN IMMEDIATE") {
533
+ super("SQLite writer acquisition remained busy after 3 attempts", { cause });
534
+ this.name = "SqliteAcquisitionBusyError";
535
+ this.stage = stage;
536
+ }
537
+ }
538
+
539
+ /** Retry only acquisition: no privilege flag, callback or in-memory mutation has run yet. */
540
+ function retryAcquisition(acquire: () => unknown, stage: string): void {
541
+ // Maintenance writers retry on their next tick. Multiplying their native
542
+ // busy timeout here would stall every session sharing the host event loop.
543
+ if (!transformPassScope.getStore()?.active) {
544
+ acquire();
545
+ return;
546
+ }
547
+ const delays = [500, 1000];
548
+ for (let attempt = 0; ; attempt++) {
549
+ try {
550
+ acquire();
551
+ return;
552
+ } catch (error) {
553
+ // Handles created by this module already retry inside exec. Do not
554
+ // apply the attempt budget twice; externally supplied handles still
555
+ // need the acquisition retry performed by withPrivilegedWriter.
556
+ if (error instanceof SqliteAcquisitionBusyError || !isTransientSqliteError(error))
557
+ throw error;
558
+ if (attempt === delays.length) throw new SqliteAcquisitionBusyError(error, stage);
559
+ // The database API is synchronous on both hosts. With busy_timeout=5000,
560
+ // three attempts plus these waits bound one acquisition to 16.5 seconds.
561
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delays[attempt]);
562
+ }
563
+ }
564
+ }
479
565
 
480
566
  function isInTransaction(db: Database): boolean {
481
567
  const candidate = db as unknown as { inTransaction?: unknown; isTransaction?: unknown };
@@ -501,7 +587,7 @@ export function withPrivilegedWriter<T>(db: Database, operation: () => T): T {
501
587
  if (nested) {
502
588
  db.exec(`SAVEPOINT ${savepoint}`);
503
589
  } else {
504
- db.exec("BEGIN IMMEDIATE");
590
+ retryAcquisition(() => db.exec("BEGIN IMMEDIATE"), "BEGIN IMMEDIATE");
505
591
  }
506
592
  privilegeDepth.set(db, previousDepth + 1);
507
593
  try {
@@ -0,0 +1,95 @@
1
+ import { afterAll, afterEach, describe, expect, spyOn, test } from "bun:test";
2
+ import {
3
+ Database,
4
+ isTransientSqliteError,
5
+ withSqliteTransformPass,
6
+ withPrivilegedWriter as writePrivileged,
7
+ } from "./sqlite";
8
+
9
+ const withPrivilegedWriter = <T>(db: Database, operation: () => T): T =>
10
+ withSqliteTransformPass(() => writePrivileged(db, operation));
11
+
12
+ const wait = spyOn(Atomics, "wait");
13
+ afterEach(() => wait.mockClear());
14
+ afterAll(() => wait.mockRestore());
15
+
16
+ describe("writer acquisition backoff", () => {
17
+ for (const clearsOn of [2, 3]) {
18
+ test(`acquisition clears on attempt ${clearsOn} and invokes callback exactly once`, () => {
19
+ const db = new Database(":memory:");
20
+ db.exec(
21
+ "CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER); CREATE TABLE result(value TEXT)",
22
+ );
23
+ const exec = db.exec.bind(db);
24
+ let attempts = 0;
25
+ let callbacks = 0;
26
+ const delays: number[] = [];
27
+ wait.mockImplementation((_array, _index, _value, timeout) => {
28
+ delays.push(timeout!);
29
+ return "timed-out";
30
+ });
31
+ const intercepted = spyOn(db, "exec").mockImplementation((sql) => {
32
+ if (sql === "BEGIN IMMEDIATE" && ++attempts < clearsOn)
33
+ throw Object.assign(new Error("locked"), { code: "SQLITE_BUSY" });
34
+ return exec(sql);
35
+ });
36
+ try {
37
+ const result = withPrivilegedWriter(db, () => {
38
+ callbacks++;
39
+ db.prepare("INSERT INTO result VALUES (?)").run("managed");
40
+ return JSON.stringify(db.prepare("SELECT * FROM result").all());
41
+ });
42
+ expect(result).toBe('[{"value":"managed"}]');
43
+ expect(attempts).toBe(clearsOn);
44
+ expect(callbacks).toBe(1);
45
+ expect(delays).toEqual([500, 1000].slice(0, clearsOn - 1));
46
+ } finally {
47
+ intercepted.mockRestore();
48
+ db.close();
49
+ }
50
+ });
51
+ }
52
+ test("exhausted acquisition never invokes callback", () => {
53
+ const db = new Database(":memory:");
54
+ let calls = 0;
55
+ wait.mockImplementation(() => "timed-out");
56
+ const exec = spyOn(db, "exec").mockImplementation(() => {
57
+ throw Object.assign(new Error("original lock"), { code: "SQLITE_LOCKED" });
58
+ });
59
+ try {
60
+ expect(() => withPrivilegedWriter(db, () => calls++)).toThrow("after 3 attempts");
61
+ expect(exec).toHaveBeenCalledTimes(3);
62
+ expect(calls).toBe(0);
63
+ } finally {
64
+ exec.mockRestore();
65
+ db.close();
66
+ }
67
+ });
68
+ test("busy callback rolls back without retrying mutations", () => {
69
+ const db = new Database(":memory:");
70
+ db.exec(
71
+ "CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER); CREATE TABLE result(value TEXT)",
72
+ );
73
+ let calls = 0;
74
+ try {
75
+ expect(() =>
76
+ withPrivilegedWriter(db, () => {
77
+ calls++;
78
+ db.exec("INSERT INTO result VALUES ('partial')");
79
+ throw Object.assign(new Error("after acquisition"), { code: "SQLITE_BUSY" });
80
+ }),
81
+ ).toThrow("after acquisition");
82
+ expect(calls).toBe(1);
83
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
84
+ expect(wait).not.toHaveBeenCalled();
85
+ } finally {
86
+ db.close();
87
+ }
88
+ });
89
+ test("recognizes Bun extended and Node numeric contention codes only", () => {
90
+ expect(isTransientSqliteError({ code: "SQLITE_BUSY_SNAPSHOT" })).toBe(true);
91
+ expect(isTransientSqliteError({ code: "ERR_SQLITE_ERROR", errcode: 5 })).toBe(true);
92
+ expect(isTransientSqliteError({ code: "ERR_SQLITE_ERROR", errcode: 262 })).toBe(true);
93
+ expect(isTransientSqliteError({ code: "SQLITE_CORRUPT" })).toBe(false);
94
+ });
95
+ });
@@ -71,6 +71,8 @@ export interface SubagentRunOptions {
71
71
  temperature?: number | undefined;
72
72
  /** Requested output-token budget for the provider request. */
73
73
  maxOutputTokens?: number | undefined;
74
+ /** Per-child cumulative prompt tokens for tool-loop dreamer tasks. */
75
+ tokenBudget?: number | undefined;
74
76
  cwd?: string | undefined;
75
77
  signal?: AbortSignal | undefined;
76
78
  /**
@@ -208,6 +210,8 @@ export type SubagentRunResult =
208
210
  | {
209
211
  ok: false;
210
212
  reason:
213
+ | "step_limit"
214
+ | "token_budget"
211
215
  | "invalid_prompt"
212
216
  | "timeout"
213
217
  | "abort"
@@ -38,6 +38,11 @@ export const USER_FACING_FAILURES = {
38
38
  sentence: "Memory maintenance stopped at its hidden agent step limit.",
39
39
  action: "This task needs less work per run; changing the model connection will not help.",
40
40
  },
41
+ dream_token_budget: {
42
+ code: "MC-D11",
43
+ sentence: "Memory maintenance reached its prompt-token budget.",
44
+ action: "The unfinished items will be retried on the next run.",
45
+ },
41
46
  dream_empty_completion: {
42
47
  code: "MC-D03",
43
48
  sentence: "Memory maintenance received no usable response.",
@@ -285,6 +290,7 @@ const DREAM_FAILURE_KEYS = {
285
290
  provider_timeout: "dream_provider_timeout",
286
291
  provider_error: "dream_provider_error",
287
292
  step_limit: "dream_step_limit",
293
+ token_budget: "dream_token_budget",
288
294
  empty_completion: "dream_empty_completion",
289
295
  no_models: "dream_no_models",
290
296
  child_aborted: "dream_child_aborted",
@@ -0,0 +1,132 @@
1
+ import { describe, 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 subprocessNames = new Set([
8
+ "exec",
9
+ "execSync",
10
+ "execFile",
11
+ "execFileSync",
12
+ "spawn",
13
+ "spawnSync",
14
+ ]);
15
+
16
+ function sourceFiles(dir: string): string[] {
17
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
18
+ const path = join(dir, entry.name);
19
+ return entry.isDirectory() ? sourceFiles(path) : path.endsWith(".ts") ? [path] : [];
20
+ });
21
+ }
22
+
23
+ function missingHiddenOptions(path: string): string[] {
24
+ const text = readFileSync(path, "utf8");
25
+ if (!/node:child_process|Bun\.spawn/.test(text)) return [];
26
+ const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
27
+ const imports = new Set<string>();
28
+ const variables = new Map<string, ts.Expression>();
29
+ for (const statement of source.statements) {
30
+ if (
31
+ ts.isImportDeclaration(statement) &&
32
+ statement.moduleSpecifier.getText(source).includes("node:child_process")
33
+ ) {
34
+ const bindings = statement.importClause?.namedBindings;
35
+ if (bindings && ts.isNamedImports(bindings)) {
36
+ for (const element of bindings.elements) {
37
+ if (subprocessNames.has(element.propertyName?.text ?? element.name.text))
38
+ imports.add(element.name.text);
39
+ }
40
+ }
41
+ }
42
+ }
43
+ function collect(node: ts.Node): void {
44
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer)
45
+ variables.set(node.name.text, node.initializer);
46
+ ts.forEachChild(node, collect);
47
+ }
48
+ collect(source);
49
+ function hidden(node: ts.Expression | undefined, seen = new Set<string>()): boolean {
50
+ if (!node) return false;
51
+ if (ts.isIdentifier(node)) {
52
+ if (seen.has(node.text)) return false;
53
+ seen.add(node.text);
54
+ return hidden(variables.get(node.text), seen);
55
+ }
56
+ if (!ts.isObjectLiteralExpression(node)) return false;
57
+ // Later spreads can override an earlier flag, so inspect the effective last assignment.
58
+ for (const property of [...node.properties].reverse()) {
59
+ if (
60
+ ts.isPropertyAssignment(property) &&
61
+ property.name.getText(source) === "windowsHide"
62
+ ) {
63
+ return property.initializer.kind === ts.SyntaxKind.TrueKeyword;
64
+ }
65
+ if (ts.isSpreadAssignment(property) && hidden(property.expression, new Set(seen)))
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ const failures: string[] = [];
71
+ function visit(node: ts.Node): void {
72
+ if (ts.isCallExpression(node)) {
73
+ const expression = node.expression;
74
+ let name: string | undefined;
75
+ if (
76
+ ts.isIdentifier(expression) &&
77
+ (imports.has(expression.text) ||
78
+ [
79
+ "execFileAsync",
80
+ "execFileSyncForIdentity",
81
+ "rpcIdentityExecFileSync",
82
+ "execFileForVerificationPaths",
83
+ ].includes(expression.text) ||
84
+ (expression.text === "exec" && path.endsWith("rpc-utils.ts")))
85
+ )
86
+ name = expression.text;
87
+ if (ts.isPropertyAccessExpression(expression)) {
88
+ const owner = expression.expression.getText(source);
89
+ if (
90
+ owner === "Bun" &&
91
+ (expression.name.text === "spawn" || expression.name.text === "spawnSync")
92
+ )
93
+ name = "Bun.spawn";
94
+ if (
95
+ (owner === "deps" || owner === "this") &&
96
+ (subprocessNames.has(expression.name.text) ||
97
+ expression.name.text === "spawnImpl")
98
+ )
99
+ name = expression.name.text;
100
+ }
101
+ if (name) {
102
+ const index =
103
+ name === "Bun.spawn"
104
+ ? node.arguments.length === 1
105
+ ? 0
106
+ : 1
107
+ : name === "execSync" || (name === "exec" && !path.endsWith("rpc-utils.ts"))
108
+ ? 1
109
+ : 2;
110
+ // A single-argument Bun.spawnSync({cmd, ...}) passes options as its first argument.
111
+ const option = node.arguments[index];
112
+ if (!hidden(option)) {
113
+ const line =
114
+ source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
115
+ failures.push(`${relative(root, path)}:${line} ${name}`);
116
+ }
117
+ }
118
+ }
119
+ ts.forEachChild(node, visit);
120
+ }
121
+ visit(source);
122
+ return failures;
123
+ }
124
+
125
+ describe("Windows child processes", () => {
126
+ test("every plugin, Pi and CLI subprocess hides its Windows console", () => {
127
+ const failures = ["plugin", "pi-plugin", "cli"].flatMap((pkg) =>
128
+ sourceFiles(join(root, "packages", pkg, "src")).flatMap(missingHiddenOptions),
129
+ );
130
+ expect(failures).toEqual([]);
131
+ });
132
+ });
@@ -190,6 +190,7 @@ describe("write transaction attribution fences", () => {
190
190
  logger.flushLogger();
191
191
  `;
192
192
  const child = Bun.spawn({
193
+ windowsHide: true,
193
194
  cmd: ["bun", "--eval", scenario],
194
195
  cwd: import.meta.dir,
195
196
  env: {