@cortexkit/opencode-magic-context 0.21.8 → 0.22.0

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 (207) hide show
  1. package/README.md +116 -325
  2. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  3. package/dist/agents/permissions.d.ts +29 -14
  4. package/dist/agents/permissions.d.ts.map +1 -1
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/migrate-experimental.d.ts +29 -0
  7. package/dist/config/migrate-experimental.d.ts.map +1 -0
  8. package/dist/config/schema/magic-context.d.ts +80 -104
  9. package/dist/config/schema/magic-context.d.ts.map +1 -1
  10. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  11. package/dist/features/magic-context/compartment-embedding.d.ts +34 -0
  12. package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -0
  13. package/dist/features/magic-context/compartment-events.d.ts +50 -0
  14. package/dist/features/magic-context/compartment-events.d.ts.map +1 -0
  15. package/dist/features/magic-context/compartment-storage.d.ts +22 -0
  16. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/queue.d.ts +13 -2
  19. package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/runner.d.ts +11 -0
  21. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
  23. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  24. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  25. package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
  26. package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
  27. package/dist/features/magic-context/key-files/project-key-files.d.ts.map +1 -1
  28. package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
  29. package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/constants.d.ts +4 -0
  31. package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
  32. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  33. package/dist/features/magic-context/memory/index.d.ts +1 -1
  34. package/dist/features/magic-context/memory/index.d.ts.map +1 -1
  35. package/dist/features/magic-context/memory/memory-migration.d.ts +133 -0
  36. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -0
  37. package/dist/features/magic-context/memory/project-identity.d.ts +38 -7
  38. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  40. package/dist/features/magic-context/memory/storage-memory.d.ts +15 -1
  41. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  42. package/dist/features/magic-context/memory/types.d.ts +3 -1
  43. package/dist/features/magic-context/memory/types.d.ts.map +1 -1
  44. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  45. package/dist/features/magic-context/migrations.d.ts +7 -0
  46. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  47. package/dist/features/magic-context/project-docs-hash.d.ts +6 -0
  48. package/dist/features/magic-context/project-docs-hash.d.ts.map +1 -0
  49. package/dist/features/magic-context/project-identity.d.ts +2 -0
  50. package/dist/features/magic-context/project-identity.d.ts.map +1 -0
  51. package/dist/features/magic-context/storage-db.d.ts +51 -7
  52. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  53. package/dist/features/magic-context/storage-historian-runs.d.ts +73 -0
  54. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -0
  55. package/dist/features/magic-context/storage-identity-rekey-map.d.ts +11 -0
  56. package/dist/features/magic-context/storage-identity-rekey-map.d.ts.map +1 -0
  57. package/dist/features/magic-context/storage-m0-mutation-log.d.ts +22 -0
  58. package/dist/features/magic-context/storage-m0-mutation-log.d.ts.map +1 -0
  59. package/dist/features/magic-context/storage-memory-mutation-log.d.ts +25 -0
  60. package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -0
  61. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  62. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  63. package/dist/features/magic-context/storage-meta-shared.d.ts +44 -0
  64. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  65. package/dist/features/magic-context/storage-meta.d.ts +1 -0
  66. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  67. package/dist/features/magic-context/storage-project-state.d.ts +19 -0
  68. package/dist/features/magic-context/storage-project-state.d.ts.map +1 -0
  69. package/dist/features/magic-context/storage-subagent-invocations.d.ts +9 -0
  70. package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -1
  71. package/dist/features/magic-context/storage-tags.d.ts +21 -1
  72. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  73. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts +24 -0
  74. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts.map +1 -0
  75. package/dist/features/magic-context/storage.d.ts +12 -3
  76. package/dist/features/magic-context/storage.d.ts.map +1 -1
  77. package/dist/features/magic-context/subagent-token-capture.d.ts +1 -1
  78. package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
  79. package/dist/features/magic-context/tagger.d.ts +15 -1
  80. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  81. package/dist/features/magic-context/types.d.ts +21 -0
  82. package/dist/features/magic-context/types.d.ts.map +1 -1
  83. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  84. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  85. package/dist/features/magic-context/v22-deferred-backfill.d.ts +46 -0
  86. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -0
  87. package/dist/features/magic-context/work-metrics.d.ts +66 -0
  88. package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
  89. package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
  90. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/cache-busting-signals.d.ts +9 -0
  92. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/command-handler.d.ts +13 -1
  94. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/compartment-parser.d.ts +25 -0
  96. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/compartment-prompt.d.ts +27 -16
  98. package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +6 -2
  101. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts +9 -1
  104. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/compartment-runner-types.d.ts +67 -4
  106. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/decay-curve.d.ts +78 -0
  110. package/dist/hooks/magic-context/decay-curve.d.ts.map +1 -0
  111. package/dist/hooks/magic-context/decay-render.d.ts +67 -0
  112. package/dist/hooks/magic-context/decay-render.d.ts.map +1 -0
  113. package/dist/hooks/magic-context/event-handler.d.ts +1 -1
  114. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  115. package/dist/hooks/magic-context/event-resolvers.d.ts +17 -0
  116. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  117. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/historian-prompt.generated.d.ts +2 -0
  119. package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -0
  120. package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
  121. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/hook.d.ts +9 -21
  123. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/inject-compartments.d.ts +126 -0
  125. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  126. package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -1
  127. package/dist/hooks/magic-context/live-session-state.d.ts +9 -0
  128. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  129. package/dist/hooks/magic-context/m0-token-breakdown.d.ts +35 -0
  130. package/dist/hooks/magic-context/m0-token-breakdown.d.ts.map +1 -0
  131. package/dist/hooks/magic-context/read-session-chunk.d.ts +9 -0
  132. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  133. package/dist/hooks/magic-context/read-session-db.d.ts +7 -0
  134. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  135. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +104 -0
  136. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -0
  137. package/dist/hooks/magic-context/reference-retrieval.d.ts +61 -0
  138. package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -0
  139. package/dist/hooks/magic-context/reference-seeds.generated.d.ts +8 -0
  140. package/dist/hooks/magic-context/reference-seeds.generated.d.ts.map +1 -0
  141. package/dist/hooks/magic-context/send-session-notification.d.ts +1 -1
  142. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  143. package/dist/hooks/magic-context/system-prompt-hash.d.ts +5 -6
  144. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  145. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  146. package/dist/hooks/magic-context/tokenizer-calibration.d.ts +6 -0
  147. package/dist/hooks/magic-context/tokenizer-calibration.d.ts.map +1 -1
  148. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +0 -7
  149. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  150. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +18 -0
  151. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  152. package/dist/hooks/magic-context/transform.d.ts +9 -7
  153. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  154. package/dist/hooks/magic-context/upgrade-reminder.d.ts +73 -0
  155. package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -0
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +9258 -3915
  158. package/dist/plugin/conflict-warning-hook.d.ts +13 -0
  159. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  160. package/dist/plugin/dream-timer.d.ts.map +1 -1
  161. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  162. package/dist/plugin/messages-transform.d.ts.map +1 -1
  163. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  164. package/dist/plugin/tool-registry.d.ts.map +1 -1
  165. package/dist/shared/announcement.d.ts +1 -1
  166. package/dist/shared/announcement.d.ts.map +1 -1
  167. package/dist/shared/rpc-client.d.ts +1 -0
  168. package/dist/shared/rpc-client.d.ts.map +1 -1
  169. package/dist/shared/rpc-notifications.d.ts +27 -5
  170. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  171. package/dist/shared/rpc-server.d.ts +1 -0
  172. package/dist/shared/rpc-server.d.ts.map +1 -1
  173. package/dist/shared/rpc-types.d.ts +30 -2
  174. package/dist/shared/rpc-types.d.ts.map +1 -1
  175. package/dist/shared/rpc-utils.d.ts +9 -0
  176. package/dist/shared/rpc-utils.d.ts.map +1 -1
  177. package/dist/shared/sqlite-helpers.d.ts +7 -7
  178. package/dist/shared/sqlite.d.ts +23 -14
  179. package/dist/shared/sqlite.d.ts.map +1 -1
  180. package/dist/shared/tag-transcript.d.ts +10 -1
  181. package/dist/shared/tag-transcript.d.ts.map +1 -1
  182. package/dist/tools/ctx-expand/tools.d.ts +5 -1
  183. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  184. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  185. package/dist/tui/data/context-db.d.ts +16 -1
  186. package/dist/tui/data/context-db.d.ts.map +1 -1
  187. package/package.json +2 -4
  188. package/src/shared/announcement.ts +6 -7
  189. package/src/shared/rpc-client.test.ts +49 -2
  190. package/src/shared/rpc-client.ts +19 -9
  191. package/src/shared/rpc-notifications.test.ts +54 -1
  192. package/src/shared/rpc-notifications.ts +82 -13
  193. package/src/shared/rpc-server.ts +33 -4
  194. package/src/shared/rpc-types.ts +30 -2
  195. package/src/shared/rpc-utils.ts +10 -0
  196. package/src/shared/sqlite-helpers.ts +9 -9
  197. package/src/shared/sqlite.ts +99 -80
  198. package/src/shared/tag-transcript.test.ts +280 -0
  199. package/src/shared/tag-transcript.ts +162 -33
  200. package/src/tui/data/context-db.ts +75 -11
  201. package/src/tui/index.tsx +223 -32
  202. package/src/tui/slots/sidebar-content.tsx +366 -34
  203. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +0 -87
  204. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +0 -1
  205. package/dist/shared/native-binding.d.ts +0 -87
  206. package/dist/shared/native-binding.d.ts.map +0 -1
  207. package/src/shared/native-binding.ts +0 -311
@@ -3,33 +3,42 @@
3
3
  *
4
4
  * The same shipped plugin artifact must run under two different runtimes:
5
5
  * - Bun (current OpenCode releases) → uses `bun:sqlite` (built-in, fast)
6
- * - Node (OpenCode beta + future Pi plugin) → uses `better-sqlite3`
6
+ * - Node / Electron (Pi plugin, OpenCode Desktop) → uses `node:sqlite`
7
+ * (`DatabaseSync`, built into Node 22.5+ / Electron 41+, stable-enough and
8
+ * flag-free since Node 22.13/23.4).
7
9
  *
8
- * Bun cannot load `better-sqlite3` (oven-sh/bun#4290), and Node has no
9
- * `bun:sqlite` module. Static imports of either would crash at parse time
10
- * in the wrong runtime, so we use dynamic imports gated by runtime detection.
10
+ * Bun has no `node:sqlite`, and Node/Electron have no `bun:sqlite`. Static
11
+ * imports of either would crash at parse time in the wrong runtime, so we use
12
+ * dynamic imports gated by runtime detection.
11
13
  *
12
- * The Function-constructor wrapper around `import()` defeats bundler static
13
- * analysis without it, esbuild/bun build would try to resolve both modules
14
- * during the bundle step, including the one that doesn't exist in the build
15
- * runtime.
14
+ * Why `node:sqlite` instead of `better-sqlite3`: better-sqlite3 is a native
15
+ * module requiring per-ABI prebuilds, and Electron's ABI never matches the npm
16
+ * Node prebuild which forced a runtime download of an Electron-matched
17
+ * `.node` binary (a supply-chain + maintenance liability). `node:sqlite` is
18
+ * built into the runtime, so there is NOTHING to download or rebuild. Both Pi
19
+ * (plain Node 24) and OpenCode Desktop (Electron 41 → Node 24.14.1) ship it.
16
20
  *
17
- * Both libraries expose ~95% API parity:
18
- * - new Database(path, { readonly?: boolean })
21
+ * API surface we use (common across both backends, modulo the shims below):
22
+ * - new Database(path, { readonly?: boolean }) ← we map readonly→readOnly
19
23
  * - db.prepare(sql).run/get/all
20
24
  * - db.exec(multistatement)
21
- * - db.transaction(fn) → wrapped function
25
+ * - db.transaction(fn) → wrapped function ← shimmed for node:sqlite
22
26
  * - db.close()
23
27
  *
24
- * The 5% that differs (db.query, db.run, db.close(boolean), Database.open)
25
- * is either rewritten to common-subset patterns or hidden behind the helpers
26
- * in `./sqlite-helpers.ts`.
28
+ * The two backend differences we bridge for node:sqlite:
29
+ * 1. node:sqlite has no `db.transaction(fn)` helper we add a savepoint-aware
30
+ * shim (below) that matches better-sqlite3/bun semantics.
31
+ * 2. node:sqlite's constructor option is `readOnly` (camel-case), not
32
+ * better-sqlite3/bun's `readonly` — we translate it so call sites are
33
+ * unchanged.
34
+ * Everything else (named params with bare keys, ATTACH under defensive mode,
35
+ * `run()` → {changes,lastInsertRowid}) is identical and was verified directly.
27
36
  */
28
37
 
29
- // Type import only — better-sqlite3's runtime is loaded dynamically below.
30
- // @types/better-sqlite3 has richer definitions than @types/bun's bun:sqlite
31
- // types, and bun:sqlite is a structural superset for the API surface we use,
32
- // so calls typed against BetterSqlite3 work under both runtimes at runtime.
38
+ // Type import only — runtime is loaded dynamically below. @types/better-sqlite3
39
+ // has the richest definitions and is a structural superset of the API surface
40
+ // we use, so calls typed against BetterSqlite3 work under bun:sqlite and
41
+ // node:sqlite at runtime (both expose prepare/run/get/all/exec/close).
33
42
  import type BetterSqlite3 from "better-sqlite3";
34
43
 
35
44
  // Detect Bun via process.versions.bun. Both globalThis.Bun and
@@ -41,80 +50,90 @@ const isBun = typeof process !== "undefined" && typeof process.versions?.bun ===
41
50
 
42
51
  // IMPORTANT: bundler-evading dynamic imports.
43
52
  //
44
- // We can't write `await import("better-sqlite3")` directly because esbuild/bun
53
+ // We can't write `await import("node:sqlite")` directly because esbuild/bun
45
54
  // would try to resolve both modules at build time, and one of them won't exist
46
- // in the build runtime (bun:sqlite is missing in Node, better-sqlite3 isn't
47
- // shipped in Bun-only environments). Earlier versions used
48
- // `new Function("p", "return import(p)")("modname")` to defeat static
49
- // analysis, but that breaks Pi's vm-based extension loader: a Function
50
- // constructed at runtime has no module record, so `import()` inside it has
51
- // no referrer module and Node throws "A dynamic import callback was not
55
+ // in the build runtime (bun:sqlite is missing in Node, node:sqlite is missing
56
+ // in Bun). Earlier versions used `new Function("p", "return import(p)")(...)`
57
+ // to defeat static analysis, but that breaks Pi's vm-based extension loader: a
58
+ // Function constructed at runtime has no module record, so `import()` inside it
59
+ // has no referrer module and Node throws "A dynamic import callback was not
52
60
  // specified".
53
61
  //
54
62
  // The /* @vite-ignore */ + variable indirection pattern hides the specifier
55
63
  // from static analyzers while keeping a real referrer module for the
56
64
  // dynamic import — Pi's loader, esbuild, and bun build all accept it.
57
65
  const bunSpec = "bun:" + "sqlite";
58
- const betterSpec = "better-" + "sqlite3";
59
-
60
- // Under Electron, the npm-installed better-sqlite3 binary has the wrong ABI
61
- // (it's a Node prebuild but Electron embeds a different NODE_MODULE_VERSION).
62
- // resolveBetterSqliteNativeBinding() detects this and downloads + caches the
63
- // matching Electron prebuild, then returns its absolute path so we can pass
64
- // it to better-sqlite3 via the `nativeBinding` constructor option (a
65
- // documented public API). Returns null outside Electron OR when the on-disk
66
- // binary already matches the runtime ABI — in those cases the default
67
- // bindings() lookup just works.
68
- const electronNativeBinding = isBun
69
- ? null
70
- : await (async () => {
71
- const mod = await import("./native-binding");
72
- return mod.resolveBetterSqliteNativeBinding();
73
- })();
66
+ const nodeSpec = "node:" + "sqlite";
74
67
 
75
68
  const sqliteModule = isBun
76
69
  ? await import(/* @vite-ignore */ bunSpec)
77
- : await import(/* @vite-ignore */ betterSpec);
70
+ : await import(/* @vite-ignore */ nodeSpec);
78
71
 
79
- // Different export shapes between the two libraries:
80
- // - bun:sqlite → named export `Database`
81
- // - better-sqlite3 default export
82
- const RawDatabaseImpl = isBun ? sqliteModule.Database : sqliteModule.default;
72
+ // Different export shapes between the two backends:
73
+ // - bun:sqlite → named export `Database` (has its own .transaction, accepts
74
+ // `{ readonly }`) usable as-is.
75
+ // - node:sqlite named export `DatabaseSync` (no .transaction, option is
76
+ // `readOnly`) — wrapped below.
77
+ const DatabaseImpl: typeof BetterSqlite3 = isBun
78
+ ? (sqliteModule.Database as typeof BetterSqlite3)
79
+ : buildNodeSqliteDatabaseClass(sqliteModule.DatabaseSync);
83
80
 
84
- // When we resolved a non-default Electron-compatible native binding above,
85
- // transparently inject it into every `new Database(...)` call. This is the
86
- // public `nativeBinding` constructor option that better-sqlite3 ships
87
- // specifically for cross-runtime extension scenarios it makes
88
- // better-sqlite3 `require()` the binary at the supplied path directly,
89
- // bypassing the default bindings() resolver.
90
- //
91
- // Subclassing keeps the call sites untouched: existing
92
- // `new Database(filename, { readonly: true })` invocations work as-is.
93
- // Callers can still override `nativeBinding` explicitly if they need to.
94
- //
95
- // The TypeScript type intentionally references @types/better-sqlite3 because
96
- // its definitions are richer than @types/bun's bun:sqlite types and bun:sqlite
97
- // is a structural superset for the API surface we use. Calls written against
98
- // this type work correctly under both runtimes at runtime.
99
- //
100
- // @types/better-sqlite3 uses `export = Database` (CommonJS interop), which
101
- // surfaces in TypeScript as `import Database = require("better-sqlite3")`.
102
- // We capture the DatabaseConstructor type from the namespace re-export.
103
- const DatabaseImpl: typeof BetterSqlite3 =
104
- electronNativeBinding == null
105
- ? (RawDatabaseImpl as typeof BetterSqlite3)
106
- : (class DatabaseWithElectronBinding extends (RawDatabaseImpl as typeof BetterSqlite3) {
107
- constructor(filename?: string | Buffer, options?: BetterSqlite3.Options) {
108
- // Type narrowing: the surrounding ternary already proved
109
- // electronNativeBinding is non-null in this branch, but
110
- // TypeScript can't follow that across the class boundary.
111
- const fallback = electronNativeBinding as string;
112
- super(filename, {
113
- ...options,
114
- nativeBinding: options?.nativeBinding ?? fallback,
115
- });
116
- }
117
- } as typeof BetterSqlite3);
81
+ /**
82
+ * Wrap node:sqlite's `DatabaseSync` so it presents the better-sqlite3/bun
83
+ * surface the rest of the codebase calls:
84
+ * - translate the `{ readonly }` constructor option → node:sqlite's `readOnly`
85
+ * - add a `transaction(fn)` helper that matches better-sqlite3 semantics,
86
+ * using `db.isTransaction` to pick BEGIN (top-level) vs SAVEPOINT (nested),
87
+ * so it composes correctly with manual `BEGIN IMMEDIATE` blocks too.
88
+ */
89
+ // biome-ignore lint/suspicious/noExplicitAny: node:sqlite has no shipped types here; the public export is cast to the better-sqlite3 shape.
90
+ function buildNodeSqliteDatabaseClass(DatabaseSync: any): typeof BetterSqlite3 {
91
+ // Single constant savepoint name is correct for arbitrary nesting depth:
92
+ // SQLite savepoints with the same name stack LIFO — RELEASE / ROLLBACK TO
93
+ // always target the most recent. node:sqlite is synchronous + single-process
94
+ // per connection, so there is no concurrent-savepoint hazard.
95
+ const SAVEPOINT = "mc_tx_sp";
96
+
97
+ class NodeSqliteDatabase extends DatabaseSync {
98
+ constructor(filename?: string | Buffer, options?: BetterSqlite3.Options) {
99
+ const translated: Record<string, unknown> = { ...options };
100
+ if (options && "readonly" in options) {
101
+ translated.readOnly = (options as { readonly?: boolean }).readonly;
102
+ delete translated.readonly;
103
+ }
104
+ super(typeof filename === "string" ? filename : ":memory:", translated);
105
+ }
106
+
107
+ // biome-ignore lint/suspicious/noExplicitAny: mirrors better-sqlite3's generic transaction(fn) signature.
108
+ transaction<F extends (...args: any[]) => any>(fn: F): F {
109
+ // biome-ignore lint/suspicious/noExplicitAny: faithful pass-through of this/args to fn.
110
+ const self = this as any;
111
+ const wrapped = function (this: unknown, ...args: unknown[]): unknown {
112
+ const nested = self.isTransaction === true;
113
+ self.exec(nested ? `SAVEPOINT ${SAVEPOINT}` : "BEGIN");
114
+ try {
115
+ const result = fn.apply(this, args);
116
+ self.exec(nested ? `RELEASE ${SAVEPOINT}` : "COMMIT");
117
+ return result;
118
+ } catch (error) {
119
+ if (nested) {
120
+ // ROLLBACK TO unwinds the savepoint's changes but leaves
121
+ // it on the stack; RELEASE then pops it (better-sqlite3
122
+ // does both).
123
+ self.exec(`ROLLBACK TO ${SAVEPOINT}`);
124
+ self.exec(`RELEASE ${SAVEPOINT}`);
125
+ } else {
126
+ self.exec("ROLLBACK");
127
+ }
128
+ throw error;
129
+ }
130
+ };
131
+ return wrapped as unknown as F;
132
+ }
133
+ }
134
+
135
+ return NodeSqliteDatabase as unknown as typeof BetterSqlite3;
136
+ }
118
137
 
119
138
  export const Database: typeof BetterSqlite3 = DatabaseImpl;
120
139
 
@@ -0,0 +1,280 @@
1
+ /// <reference types="bun-types" />
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import type { ContextDatabase } from "../features/magic-context/storage";
5
+ import type { Tagger } from "../features/magic-context/tagger";
6
+ import { tagTranscript } from "./tag-transcript";
7
+ import type { Transcript, TranscriptPart, TranscriptPartKind } from "./transcript";
8
+
9
+ class FakeTagger implements Tagger {
10
+ private nextTag = 1;
11
+ private toolTags = new Map<string, number>();
12
+ readonly owners: string[] = [];
13
+ readonly byteSizes = new Map<number, number>();
14
+
15
+ assignTag(): number {
16
+ return this.nextTag++;
17
+ }
18
+
19
+ getTag(): number | undefined {
20
+ return undefined;
21
+ }
22
+
23
+ assignToolTag(
24
+ _sessionId: string,
25
+ callId: string,
26
+ ownerMsgId: string,
27
+ byteSize: number,
28
+ ): number {
29
+ const key = `${ownerMsgId}\0${callId}`;
30
+ const existing = this.toolTags.get(key);
31
+ if (existing !== undefined) return existing;
32
+ const tag = this.nextTag++;
33
+ this.toolTags.set(key, tag);
34
+ this.byteSizes.set(tag, byteSize);
35
+ this.owners.push(ownerMsgId);
36
+ return tag;
37
+ }
38
+
39
+ getToolTag(_sessionId: string, callId: string, ownerMsgId: string): number | undefined {
40
+ return this.toolTags.get(`${ownerMsgId}\0${callId}`);
41
+ }
42
+
43
+ bindTag(): void {}
44
+
45
+ bindToolTag(_sessionId: string, callId: string, ownerMsgId: string, tagNumber: number): void {
46
+ this.toolTags.set(`${ownerMsgId}\0${callId}`, tagNumber);
47
+ }
48
+
49
+ getAssignments(): ReadonlyMap<string, number> {
50
+ return this.toolTags;
51
+ }
52
+
53
+ resetCounter(): void {
54
+ this.nextTag = 1;
55
+ }
56
+
57
+ getCounter(): number {
58
+ return this.nextTag - 1;
59
+ }
60
+
61
+ initFromDb(): void {}
62
+
63
+ cleanup(): void {}
64
+ }
65
+
66
+ class TestPart implements TranscriptPart {
67
+ constructor(
68
+ readonly kind: TranscriptPartKind,
69
+ readonly id: string | undefined,
70
+ private text: string,
71
+ private readonly toolName = "read",
72
+ ) {}
73
+
74
+ getText(): string | undefined {
75
+ return this.text;
76
+ }
77
+
78
+ setText(newText: string): boolean {
79
+ if (this.text === newText) return false;
80
+ this.text = newText;
81
+ return true;
82
+ }
83
+
84
+ setToolOutput(newText: string): boolean {
85
+ return this.setText(newText);
86
+ }
87
+
88
+ getToolMetadata(): { toolName: string | undefined; inputByteSize: number } {
89
+ return {
90
+ toolName: this.toolName,
91
+ inputByteSize: this.kind === "tool_use" ? this.text.length : 0,
92
+ };
93
+ }
94
+
95
+ replaceWithSentinel(sentinelText: string): boolean {
96
+ return this.setText(sentinelText);
97
+ }
98
+ }
99
+
100
+ class ThrowingToolOutputPart extends TestPart {
101
+ setToolOutput(): boolean {
102
+ throw new Error("setToolOutput on assistant part");
103
+ }
104
+ }
105
+
106
+ class NonTextToolResultPart extends TestPart {
107
+ constructor(
108
+ id: string,
109
+ readonly content: unknown,
110
+ ) {
111
+ super("tool_result", id, "");
112
+ }
113
+
114
+ getText(): string | undefined {
115
+ return undefined;
116
+ }
117
+
118
+ setText(): boolean {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ class FakeDb {
124
+ readonly byteSizeUpdates: Array<{ byteSize: number; sessionId: string; tagNumber: number }> =
125
+ [];
126
+
127
+ prepare(sql: string): { run: (...args: unknown[]) => void } {
128
+ return {
129
+ run: (...args: unknown[]) => {
130
+ if (!sql.startsWith("UPDATE tags SET byte_size =")) return;
131
+ const [byteSize, sessionId, tagNumber] = args;
132
+ if (
133
+ typeof byteSize === "number" &&
134
+ typeof sessionId === "string" &&
135
+ typeof tagNumber === "number"
136
+ ) {
137
+ this.byteSizeUpdates.push({ byteSize, sessionId, tagNumber });
138
+ }
139
+ },
140
+ };
141
+ }
142
+ }
143
+
144
+ describe("tagTranscript tool aggregation", () => {
145
+ it("keeps repeated callIds in separate owner-scoped aggregate targets", () => {
146
+ const tagger = new FakeTagger();
147
+ const firstUse = new TestPart("tool_use", "read:32", '{"file":"long-a"}');
148
+ const firstResult = new TestPart("tool_result", "read:32", "r1");
149
+ const secondUse = new TestPart("tool_use", "read:32", '{"file":"long-b"}');
150
+ const secondResult = new TestPart("tool_result", "read:32", "r2");
151
+ const transcript: Transcript = {
152
+ harness: "pi",
153
+ messages: [
154
+ { info: { id: "assistant-1", role: "assistant" }, parts: [firstUse] },
155
+ { info: { id: "user-1", role: "user" }, parts: [firstResult] },
156
+ { info: { id: "assistant-2", role: "assistant" }, parts: [secondUse] },
157
+ { info: { id: "user-2", role: "user" }, parts: [secondResult] },
158
+ ],
159
+ commit() {},
160
+ };
161
+
162
+ const { targets } = tagTranscript("session-1", transcript, tagger, {} as ContextDatabase);
163
+
164
+ const firstTag = tagger.getToolTag("session-1", "read:32", "assistant-1");
165
+ const secondTag = tagger.getToolTag("session-1", "read:32", "assistant-2");
166
+ expect(firstTag).toBeDefined();
167
+ expect(secondTag).toBeDefined();
168
+ expect(firstTag).not.toBe(secondTag);
169
+ expect(tagger.owners).toEqual(["assistant-1", "assistant-2"]);
170
+ expect(targets.size).toBe(2);
171
+
172
+ expect(targets.get(firstTag ?? -1)?.drop()).toBe("removed");
173
+ expect(firstUse.getText()).toBe(`[dropped §${firstTag}§]`);
174
+ expect(firstResult.getText()).toBe(`[dropped §${firstTag}§]`);
175
+ expect(secondUse.getText()).toBe('{"file":"long-b"}');
176
+ expect(secondResult.getText()).toContain("r2");
177
+ });
178
+
179
+ it("truncates assistant tool_use parts via text fallback when setToolOutput asserts", () => {
180
+ const tagger = new FakeTagger();
181
+ const toolUse = new ThrowingToolOutputPart("tool_use", "read:99", '{"file":"long-a"}');
182
+ const transcript: Transcript = {
183
+ harness: "pi",
184
+ messages: [{ info: { id: "assistant-1", role: "assistant" }, parts: [toolUse] }],
185
+ commit() {},
186
+ };
187
+
188
+ const { targets } = tagTranscript("session-1", transcript, tagger, {} as ContextDatabase);
189
+ const tag = tagger.getToolTag("session-1", "read:99", "assistant-1");
190
+
191
+ let result: "truncated" | "absent" | undefined;
192
+ expect(() => {
193
+ result = targets.get(tag ?? -1)?.truncate?.();
194
+ }).not.toThrow();
195
+ expect(result).toBe("truncated");
196
+ expect(toolUse.getText()).toBe("[truncated]");
197
+ });
198
+
199
+ it("drops every contiguous folded tool_result block for the paired callId", () => {
200
+ const tagger = new FakeTagger();
201
+ const toolUse = new TestPart("tool_use", "read:multi", '{"file":"long-a"}');
202
+ const firstResult = new TestPart("tool_result", "read:multi", "r1");
203
+ const secondResult = new TestPart("tool_result", "read:multi", "r2");
204
+ const transcript: Transcript = {
205
+ harness: "pi",
206
+ messages: [
207
+ { info: { id: "assistant-1", role: "assistant" }, parts: [toolUse] },
208
+ { info: { id: "user-1", role: "user" }, parts: [firstResult, secondResult] },
209
+ ],
210
+ commit() {},
211
+ };
212
+
213
+ const { targets } = tagTranscript("session-1", transcript, tagger, {} as ContextDatabase);
214
+ const tag = tagger.getToolTag("session-1", "read:multi", "assistant-1");
215
+
216
+ expect(targets.size).toBe(1);
217
+ expect(targets.get(tag ?? -1)?.drop()).toBe("removed");
218
+ expect(toolUse.getText()).toBe(`[dropped §${tag}§]`);
219
+ expect(firstResult.getText()).toBe(`[dropped §${tag}§]`);
220
+ expect(secondResult.getText()).toBe(`[dropped §${tag}§]`);
221
+ });
222
+
223
+ it("pairs a reused callId result with the nearest previous unresolved owner", () => {
224
+ const tagger = new FakeTagger();
225
+ const olderUse = new TestPart("tool_use", "read:reused", '{"file":"older"}');
226
+ const nearestUse = new TestPart("tool_use", "read:reused", '{"file":"nearest"}');
227
+ const result = new TestPart("tool_result", "read:reused", "nearest result");
228
+ const transcript: Transcript = {
229
+ harness: "pi",
230
+ messages: [
231
+ { info: { id: "assistant-old", role: "assistant" }, parts: [olderUse] },
232
+ { info: { id: "assistant-near", role: "assistant" }, parts: [nearestUse] },
233
+ { info: { id: "user-result", role: "user" }, parts: [result] },
234
+ ],
235
+ commit() {},
236
+ };
237
+
238
+ const { targets } = tagTranscript("session-1", transcript, tagger, {} as ContextDatabase);
239
+
240
+ const olderTag = tagger.getToolTag("session-1", "read:reused", "assistant-old");
241
+ const nearestTag = tagger.getToolTag("session-1", "read:reused", "assistant-near");
242
+ expect(olderTag).toBeDefined();
243
+ expect(nearestTag).toBeDefined();
244
+ expect(olderTag).not.toBe(nearestTag);
245
+
246
+ expect(targets.get(nearestTag ?? -1)?.drop()).toBe("removed");
247
+ expect(olderUse.getText()).toBe('{"file":"older"}');
248
+ expect(nearestUse.getText()).toBe(`[dropped §${nearestTag}§]`);
249
+ expect(result.getText()).toBe(`[dropped §${nearestTag}§]`);
250
+ });
251
+
252
+ it("accounts non-text tool_result content when ranking tool output byte size", () => {
253
+ const tagger = new FakeTagger();
254
+ const db = new FakeDb();
255
+ const toolUse = new TestPart("tool_use", "read:image", "{}");
256
+ const caption = new TestPart("tool_result", "read:image", "c");
257
+ const image = new NonTextToolResultPart("read:image", {
258
+ type: "image",
259
+ data: "x".repeat(512),
260
+ mediaType: "image/png",
261
+ });
262
+ const transcript: Transcript = {
263
+ harness: "pi",
264
+ messages: [
265
+ { info: { id: "assistant-1", role: "assistant" }, parts: [toolUse] },
266
+ { info: { id: "user-1", role: "user" }, parts: [caption, image] },
267
+ ],
268
+ commit() {},
269
+ };
270
+
271
+ tagTranscript("session-1", transcript, tagger, db as unknown as ContextDatabase);
272
+
273
+ const tag = tagger.getToolTag("session-1", "read:image", "assistant-1");
274
+ expect(tag).toBeDefined();
275
+ expect(tagger.byteSizes.get(tag ?? -1)).toBe(2);
276
+ expect(db.byteSizeUpdates).toHaveLength(1);
277
+ expect(db.byteSizeUpdates[0]?.tagNumber).toBe(tag);
278
+ expect(db.byteSizeUpdates[0]?.byteSize).toBeGreaterThan(512);
279
+ });
280
+ });