@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.
- package/README.md +116 -325
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/agents/permissions.d.ts +29 -14
- package/dist/agents/permissions.d.ts.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/migrate-experimental.d.ts +29 -0
- package/dist/config/migrate-experimental.d.ts.map +1 -0
- package/dist/config/schema/magic-context.d.ts +80 -104
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-embedding.d.ts +34 -0
- package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-events.d.ts +50 -0
- package/dist/features/magic-context/compartment-events.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-storage.d.ts +22 -0
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/queue.d.ts +13 -2
- package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/runner.d.ts +11 -0
- package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
- package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/project-key-files.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
- package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
- package/dist/features/magic-context/memory/constants.d.ts +4 -0
- package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/index.d.ts +1 -1
- package/dist/features/magic-context/memory/index.d.ts.map +1 -1
- package/dist/features/magic-context/memory/memory-migration.d.ts +133 -0
- package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -0
- package/dist/features/magic-context/memory/project-identity.d.ts +38 -7
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts +15 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
- package/dist/features/magic-context/memory/types.d.ts +3 -1
- package/dist/features/magic-context/memory/types.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +7 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/project-docs-hash.d.ts +6 -0
- package/dist/features/magic-context/project-docs-hash.d.ts.map +1 -0
- package/dist/features/magic-context/project-identity.d.ts +2 -0
- package/dist/features/magic-context/project-identity.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +51 -7
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-historian-runs.d.ts +73 -0
- package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -0
- package/dist/features/magic-context/storage-identity-rekey-map.d.ts +11 -0
- package/dist/features/magic-context/storage-identity-rekey-map.d.ts.map +1 -0
- package/dist/features/magic-context/storage-m0-mutation-log.d.ts +22 -0
- package/dist/features/magic-context/storage-m0-mutation-log.d.ts.map +1 -0
- package/dist/features/magic-context/storage-memory-mutation-log.d.ts +25 -0
- package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts +44 -0
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -0
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-project-state.d.ts +19 -0
- package/dist/features/magic-context/storage-project-state.d.ts.map +1 -0
- package/dist/features/magic-context/storage-subagent-invocations.d.ts +9 -0
- package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +21 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage-v22-backfill-failures.d.ts +24 -0
- package/dist/features/magic-context/storage-v22-backfill-failures.d.ts.map +1 -0
- package/dist/features/magic-context/storage.d.ts +12 -3
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
- package/dist/features/magic-context/tagger.d.ts +15 -1
- package/dist/features/magic-context/tagger.d.ts.map +1 -1
- package/dist/features/magic-context/types.d.ts +21 -0
- package/dist/features/magic-context/types.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/features/magic-context/v22-deferred-backfill.d.ts +46 -0
- package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -0
- package/dist/features/magic-context/work-metrics.d.ts +66 -0
- package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +9 -0
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +13 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-parser.d.ts +25 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-prompt.d.ts +27 -16
- package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +6 -2
- package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts +9 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +67 -4
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/decay-curve.d.ts +78 -0
- package/dist/hooks/magic-context/decay-curve.d.ts.map +1 -0
- package/dist/hooks/magic-context/decay-render.d.ts +67 -0
- package/dist/hooks/magic-context/decay-render.d.ts.map +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +17 -0
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts +2 -0
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +9 -21
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +126 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -1
- package/dist/hooks/magic-context/live-session-state.d.ts +9 -0
- package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/m0-token-breakdown.d.ts +35 -0
- package/dist/hooks/magic-context/m0-token-breakdown.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts +9 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +7 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +104 -0
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -0
- package/dist/hooks/magic-context/reference-retrieval.d.ts +61 -0
- package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -0
- package/dist/hooks/magic-context/reference-seeds.generated.d.ts +8 -0
- package/dist/hooks/magic-context/reference-seeds.generated.d.ts.map +1 -0
- package/dist/hooks/magic-context/send-session-notification.d.ts +1 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +5 -6
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tokenizer-calibration.d.ts +6 -0
- package/dist/hooks/magic-context/tokenizer-calibration.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +0 -7
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +18 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +9 -7
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/upgrade-reminder.d.ts +73 -0
- package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9258 -3915
- package/dist/plugin/conflict-warning-hook.d.ts +13 -0
- package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/shared/announcement.d.ts +1 -1
- package/dist/shared/announcement.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +1 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +27 -5
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +1 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +30 -2
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +9 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/sqlite-helpers.d.ts +7 -7
- package/dist/shared/sqlite.d.ts +23 -14
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts +10 -1
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/tools/ctx-expand/tools.d.ts +5 -1
- package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts +16 -1
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/package.json +2 -4
- package/src/shared/announcement.ts +6 -7
- package/src/shared/rpc-client.test.ts +49 -2
- package/src/shared/rpc-client.ts +19 -9
- package/src/shared/rpc-notifications.test.ts +54 -1
- package/src/shared/rpc-notifications.ts +82 -13
- package/src/shared/rpc-server.ts +33 -4
- package/src/shared/rpc-types.ts +30 -2
- package/src/shared/rpc-utils.ts +10 -0
- package/src/shared/sqlite-helpers.ts +9 -9
- package/src/shared/sqlite.ts +99 -80
- package/src/shared/tag-transcript.test.ts +280 -0
- package/src/shared/tag-transcript.ts +162 -33
- package/src/tui/data/context-db.ts +75 -11
- package/src/tui/index.tsx +223 -32
- package/src/tui/slots/sidebar-content.tsx +366 -34
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +0 -87
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +0 -1
- package/dist/shared/native-binding.d.ts +0 -87
- package/dist/shared/native-binding.d.ts.map +0 -1
- package/src/shared/native-binding.ts +0 -311
package/src/shared/sqlite.ts
CHANGED
|
@@ -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
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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 —
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
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("
|
|
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,
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
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
|
|
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 */
|
|
70
|
+
: await import(/* @vite-ignore */ nodeSpec);
|
|
78
71
|
|
|
79
|
-
// Different export shapes between the two
|
|
80
|
-
// - bun:sqlite
|
|
81
|
-
//
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
});
|