@cortexkit/opencode-magic-context 0.15.1 → 0.15.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.
- package/README.md +12 -6
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +20 -0
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.js +124 -53
- package/dist/shared/bounded-session-map.d.ts +45 -0
- package/dist/shared/bounded-session-map.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/shared/bounded-session-map.test.ts +97 -0
- package/src/shared/bounded-session-map.ts +84 -0
package/README.md
CHANGED
|
@@ -48,16 +48,22 @@ Keep using the **same session** for **weeks**, **months**, or even **years**. **
|
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
|
-
### ✨
|
|
51
|
+
### ✨ Recent Highlights
|
|
52
52
|
|
|
53
|
-
**
|
|
53
|
+
**Subagents now self-manage context (v0.15)** — subagent sessions (historian, dreamer, Athena council members, any `mode: "subagent"` agent) now run age-based tool drops, reasoning clearing, and structural stripping at the execute threshold — the same way primary sessions with `ctx_reduce_enabled: false` behave. Previously they had no automatic reduction and grew silently until overflow. Nudges, historian/compartment runs, and the `<session-history>` block remain primary-only — subagents stay lean and parent-driven.
|
|
54
54
|
|
|
55
|
-
**
|
|
55
|
+
**Lean sessions when `ctx_reduce_enabled: false` (v0.15)** — when you opt out of agent-driven reduction, the `§N§` tag prefix on user/assistant text and tool output is no longer injected, saving several thousand tokens per long session. The injected prompt guidance also switches to the no-reduce variant so the agent isn't told about a tool it can't use. DB tag records still exist (heuristic cleanup, persistence, and replay all depend on them); only the agent-visible prefix is skipped.
|
|
56
|
+
|
|
57
|
+
**User Memories (v0.14)** — enabled by default under `dreamer.user_memories`. Historian extracts behavioral observations about you alongside its normal compartment output (communication style, expertise level, review focus, working patterns). Recurring observations are promoted by the dreamer to stable user memories that appear in all sessions via `<user-profile>`. Set `dreamer.user_memories.enabled: false` to opt out. Requires dreamer.
|
|
58
|
+
|
|
59
|
+
**Key File Pinning (v0.14)** — under `dreamer.pin_key_files`, still opt-in. Dreamer analyzes which files your agent reads most frequently across the session. Core orientation files (architecture, config, types) that get re-read after every context drop are pinned into the system prompt as `<key-files>`, so the agent always has them without needing to re-read from disk. Files are read fresh on each cache-busting pass. Enable with `dreamer.pin_key_files.enabled: true`.
|
|
56
60
|
|
|
57
61
|
> Migrating from an earlier version? Running `bunx --bun @cortexkit/opencode-magic-context@latest doctor` rewrites old `experimental.user_memories.*` and `experimental.pin_key_files.*` keys into their new `dreamer.*` homes, preserving any `enabled` state you had.
|
|
58
62
|
|
|
59
63
|
### 🧪 New Experimental Features
|
|
60
64
|
|
|
65
|
+
**Age-tier caveman text compression (v0.15)** — opt-in companion to `ctx_reduce_enabled: false`. Older user/assistant text parts are progressively compressed using deterministic [caveman rules](https://github.com/cortexkit/opencode-magic-context/blob/master/packages/plugin/src/hooks/magic-context/caveman.ts) — the oldest 20% go to ultra-compressed, next 20% to full, next 20% to lite, newest 40% untouched. Tier shifts always recompress from the pristine original, never from an already-cavemaned intermediate, so the result is stable across passes. Cache-safe by design. Enable with `experimental.caveman_text_compression: { enabled: true }`. Only active when `ctx_reduce_enabled: false`.
|
|
66
|
+
|
|
61
67
|
**Temporal Awareness** — gives the agent real-time perception. Each user message gets a small `<!-- +5m -->`/`<!-- +2h 15m -->`/`<!-- +3d 4h -->` gap marker showing time since the previous message, and every compartment in `<session-history>` carries `start-date`/`end-date` attributes. Lets the agent reason correctly about how long a build ran, when a decision was made, or how stale a prior session is. Cache-safe — markers derive from immutable timestamps. Enable with `experimental.temporal_awareness: true`.
|
|
62
68
|
|
|
63
69
|
**Git Commit Indexing** — indexes HEAD git commits (skipping merges) from the project and makes them searchable through `ctx_search`. Commits are embedded so semantic queries like "when did we change the auth pattern" or "why did we pick X over Y" surface the right work. HEAD-only, windowed to the last year by default, capped at 2000 commits per project with oldest evicted. Enable with `experimental.git_commit_indexing.enabled: true`.
|
|
@@ -161,7 +167,7 @@ Use `--force` to force-clear the plugin cache even when versions match (fixes br
|
|
|
161
167
|
bunx --bun @cortexkit/opencode-magic-context@latest doctor --force
|
|
162
168
|
```
|
|
163
169
|
|
|
164
|
-
Hit a real bug? Use `--issue` to collect environment, sanitized config, and the last
|
|
170
|
+
Hit a real bug? Use `--issue` to collect environment, sanitized config, and the last 400 log lines into a ready-to-submit report. It can also open the issue directly via `gh` if you have it installed:
|
|
165
171
|
|
|
166
172
|
```bash
|
|
167
173
|
bunx --bun @cortexkit/opencode-magic-context@latest doctor --issue
|
|
@@ -272,7 +278,7 @@ A **separate compressor** pass fires when the rendered history block exceeds the
|
|
|
272
278
|
|
|
273
279
|
### Nudging
|
|
274
280
|
|
|
275
|
-
As context usage grows, Magic Context sends rolling reminders suggesting the agent reduce. Cadence tightens as usage approaches the threshold — from gentle reminders to urgent warnings. If the agent recently called `ctx_reduce`, reminders are suppressed.
|
|
281
|
+
As context usage grows, Magic Context sends rolling reminders suggesting the agent reduce. Cadence tightens as usage approaches the threshold — from gentle reminders to urgent warnings. If the agent recently called `ctx_reduce`, reminders are suppressed. At 85% Magic Context force-materializes queued drops and emergency cleanup; at 95% it blocks the turn until background historian completes.
|
|
276
282
|
|
|
277
283
|
### Cross-session memory
|
|
278
284
|
|
|
@@ -308,7 +314,7 @@ Stable user memories are visible and manageable in the dashboard's User Memories
|
|
|
308
314
|
|
|
309
315
|
When running in OpenCode's terminal UI, Magic Context shows a live sidebar panel with:
|
|
310
316
|
|
|
311
|
-
- **Context breakdown bar** — visual token split across System Prompt, Compartments, Facts, Memories, and
|
|
317
|
+
- **Context breakdown bar** — visual token split across System Prompt, Compartments, Facts, Memories, Conversation, Tool Calls, Tool Definitions (measured from the `tool.definition` hook), and Overhead. Cool palette for structured injections, warm palette for user/tool traffic.
|
|
312
318
|
- **Historian status** — idle, running, or compartment/fact counts
|
|
313
319
|
- **Memory counts** — total project memories and how many are injected
|
|
314
320
|
- **Dreamer status** — last run time
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"magic-context-prompt.d.ts","sourceRoot":"","sources":["../../src/agents/magic-context-prompt.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,KAAK,SAAS,GACR,UAAU,GACV,OAAO,GACP,YAAY,GACZ,iBAAiB,GACjB,QAAQ,GACR,QAAQ,GACR,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"magic-context-prompt.d.ts","sourceRoot":"","sources":["../../src/agents/magic-context-prompt.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,KAAK,SAAS,GACR,UAAU,GACV,OAAO,GACP,YAAY,GACZ,iBAAiB,GACjB,QAAQ,GACR,QAAQ,GACR,eAAe,CAAC;AA4MtB;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAOlF;AAID,wBAAgB,wBAAwB,CACpC,KAAK,EAAE,SAAS,GAAG,IAAI,EACvB,aAAa,EAAE,MAAM,EACrB,gBAAgB,UAAO,EACvB,cAAc,UAAQ,EACtB,iBAAiB,UAAO,EACxB,wBAAwB,UAAQ,GACjC,MAAM,CAWR"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction-marker.d.ts","sourceRoot":"","sources":["../../../src/features/magic-context/compaction-marker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AA+BH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,MAAM,CAE3E;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,MAAM,CAExE;
|
|
1
|
+
{"version":3,"file":"compaction-marker.d.ts","sourceRoot":"","sources":["../../../src/features/magic-context/compaction-marker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AA+BH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,MAAM,CAE3E;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,MAAM,CAExE;AAsGD,wBAAgB,uBAAuB,IAAI,IAAI,CAY9C;AAID,UAAU,mBAAmB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACnC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACnB,mBAAmB,GAAG,IAAI,CAgC5B;AAID,UAAU,qBAAqB;IAC3B,uDAAuD;IACvD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mDAAmD;IACnD,gBAAgB,EAAE,MAAM,CAAC;IACzB,iDAAiD;IACjD,gBAAgB,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,aAAa,EAAE,MAAM,CAAC;CACzB;AAID,MAAM,WAAW,0BAA0B;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAClC,IAAI,EAAE,0BAA0B,GACjC,qBAAqB,GAAG,IAAI,CAwF9B;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAgB5E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../../src/features/magic-context/dreamer/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AA2C3D,UAAU,6BAA6B;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,6BAA6B,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAE9F;AAMD,MAAM,WAAW,cAAc;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE;QACH,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,OAAO,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,EAAE,CAAC;CACP;AAqUD,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACjC,EAAE,EAAE,QAAQ,CAAC;IACb,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChC,6FAA6F;IAC7F,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wBAAwB,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,uBAAuB,CAAC,EAAE,6BAA6B,CAAC;CAC3D,GAAG,OAAO,CAAC,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../../src/features/magic-context/dreamer/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AA2C3D,UAAU,6BAA6B;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,6BAA6B,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAE9F;AAMD,MAAM,WAAW,cAAc;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE;QACH,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,OAAO,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,EAAE,CAAC;CACP;AAqUD,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACjC,EAAE,EAAE,QAAQ,CAAC;IACb,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChC,6FAA6F;IAC7F,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wBAAwB,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,uBAAuB,CAAC,EAAE,6BAA6B,CAAC;CAC3D,GAAG,OAAO,CAAC,cAAc,CAAC,CAqV1B;AA0LD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC1C,EAAE,EAAE,QAAQ,CAAC;IACb,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,wBAAwB,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,uBAAuB,CAAC,EAAE,6BAA6B,CAAC;CAC3D,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAyDjC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../../src/features/magic-context/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../../src/features/magic-context/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAsQ3C;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAkChD"}
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
export declare function initializeDatabase(db: Database): void;
|
|
3
|
+
/**
|
|
4
|
+
* Heal NULL columns added via ensureColumn against pre-existing rows.
|
|
5
|
+
*
|
|
6
|
+
* SQLite does NOT backfill column defaults when ALTER TABLE ADD COLUMN runs
|
|
7
|
+
* on an already-populated table — old rows get NULL regardless of the
|
|
8
|
+
* DEFAULT clause. isSessionMetaRow used to require strict typeof === "string"
|
|
9
|
+
* / "number", which NULL fails, so rows with NULL columns were rejected,
|
|
10
|
+
* getOrCreateSessionMeta returned zeroed defaults (lastResponseTime=0,
|
|
11
|
+
* cacheTtl="5m"), the scheduler returned "execute" forever, and every
|
|
12
|
+
* execute pass mutated message content — a sustained cache-bust cascade.
|
|
13
|
+
*
|
|
14
|
+
* The validator now tolerates NULL, but we normalize the data too so every
|
|
15
|
+
* code path sees well-formed values. Each UPDATE is best-effort: if a column
|
|
16
|
+
* doesn't exist yet (migration ran on a DB older than the ensureColumn call),
|
|
17
|
+
* the UPDATE throws and we move on — the next schema upgrade runs ensureColumn
|
|
18
|
+
* first, then this heal again.
|
|
19
|
+
*
|
|
20
|
+
* Exported so migration v5 can call it. Not exported from any barrel.
|
|
21
|
+
*/
|
|
22
|
+
export declare function healAllNullColumns(db: Database): void;
|
|
3
23
|
export declare function openDatabase(): Database;
|
|
4
24
|
export declare function isDatabasePersisted(db: Database): boolean;
|
|
5
25
|
export declare function getDatabasePersistenceError(db: Database): string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-db.d.ts","sourceRoot":"","sources":["../../../src/features/magic-context/storage-db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAkBtC,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"storage-db.d.ts","sourceRoot":"","sources":["../../../src/features/magic-context/storage-db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAkBtC,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CA8TrD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAIrD;AAsHD,wBAAgB,YAAY,IAAI,QAAQ,CAsCvC;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAEzD;AAED,wBAAgB,2BAA2B,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAEvE;AAED,wBAAgB,aAAa,IAAI,IAAI,CAUpC;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inject-compartments.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/inject-compartments.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAU3C,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"inject-compartments.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/inject-compartments.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAU3C,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,2CAA2C,CAAC;AAKxF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGlD,MAAM,WAAW,4BAA4B;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB,EAAE,MAAM,CAAC;IAC9B,uBAAuB,EAAE,MAAM,CAAC;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACvB;AAmBD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAkBvF;AAED,MAAM,WAAW,0BAA0B;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,MAAM,CAAC;CAClC;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CA6BnE;AAoFD,wBAAgB,2BAA2B,CACvC,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,WAAW,EAAE,EACvB,cAAc,EAAE,OAAO,EACvB,WAAW,CAAC,EAAE,MAAM,EACpB,qBAAqB,CAAC,EAAE,MAAM,EAC9B,iBAAiB,CAAC,EAAE,OAAO,GAC5B,4BAA4B,GAAG,IAAI,CAgLrC;AAED,wBAAgB,0BAA0B,CACtC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,WAAW,EAAE,EACvB,QAAQ,EAAE,4BAA4B,GACvC,0BAA0B,CAgC5B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-prompt-hash.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/system-prompt-hash.ts"],"names":[],"mappings":"AAQA,OAAO,EACH,KAAK,eAAe,EAGvB,MAAM,sCAAsC,CAAC;AAiB9C;;;;;GAKG;AACH,wBAAgB,4BAA4B,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE;IACR,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CACnD,GACF,IAAI,CAKN;AA8BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAChD,EAAE,EAAE,eAAe,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE,OAAO,CAAC;IACxB,6FAA6F;IAC7F,UAAU,EAAE,OAAO,CAAC;IACpB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,kFAAkF;IAClF,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,2EAA2E;IAC3E,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,2DAA2D;IAC3D,kCAAkC,CAAC,EAAE,MAAM,CAAC;IAC5C,yFAAyF;IACzF,6BAA6B,CAAC,EAAE,OAAO,CAAC;CAC3C,GAAG;IACA,OAAO,EAAE,CAAC,KAAK,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxF,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7C,
|
|
1
|
+
{"version":3,"file":"system-prompt-hash.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/system-prompt-hash.ts"],"names":[],"mappings":"AAQA,OAAO,EACH,KAAK,eAAe,EAGvB,MAAM,sCAAsC,CAAC;AAiB9C;;;;;GAKG;AACH,wBAAgB,4BAA4B,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE;IACR,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CACnD,GACF,IAAI,CAKN;AA8BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAChD,EAAE,EAAE,eAAe,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE,OAAO,CAAC;IACxB,6FAA6F;IAC7F,UAAU,EAAE,OAAO,CAAC;IACpB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,kFAAkF;IAClF,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,2EAA2E;IAC3E,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,2DAA2D;IAC3D,kCAAkC,CAAC,EAAE,MAAM,CAAC;IAC5C,yFAAyF;IACzF,6BAA6B,CAAC,EAAE,OAAO,CAAC;CAC3C,GAAG;IACA,OAAO,EAAE,CAAC,KAAK,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxF,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7C,CA4SA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag-content-primitives.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/tag-content-primitives.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"tag-content-primitives.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/tag-content-primitives.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAuCvD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOpD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,gBAAgB,CAItE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/transform.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAC;AAExE,OAAO,EACH,KAAK,eAAe,EAIpB,KAAK,aAAa,EAErB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/transform.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAC;AAExE,OAAO,EACH,KAAK,eAAe,EAIpB,KAAK,aAAa,EAErB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAcxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAyB7C,OAAO,EAAE,yBAAyB,EAAE,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAS9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AA+B1D,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAOnF;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC1C,SAAS,EAAE,MAAM,GAClB,GAAG,CAAC,MAAM,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEzD;AAwBD,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,CAAC;IACrB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,YAAY,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzE,MAAM,EAAE,CACJ,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,YAAY,EAC1B,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,OAAO,aAAa,EAC5B,aAAa,CAAC,EAAE,QAAQ,EAAE,EAC1B,qBAAqB,CAAC,EAAE,MAAM,EAC9B,oBAAoB,CAAC,EAAE,OAAO,oCAAoC,EAAE,WAAW,KAC9E,YAAY,GAAG,IAAI,CAAC;IACzB,EAAE,EAAE,eAAe,CAAC;IACpB,eAAe,EAAE,mBAAmB,CAAC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,MAAM,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE;QACX,OAAO,EAAE,OAAO,CAAC;QACjB,qBAAqB,EAAE,MAAM,CAAC;KACjC,CAAC;IACF;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,MAAM,MAAM,CAAC;IACvC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,0BAA0B,CAAC,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACtF,sBAAsB,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACtF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,CACpB,SAAS,EAAE,MAAM,KAChB,OAAO,6BAA6B,EAAE,kBAAkB,CAAC;IAC9D,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACxD,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;kEAE8D;IAC9D,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC;yFACqF;IACrF,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;mEAC+D;IAC/D,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,2FAA2F;IAC3F,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,0FAA0F;IAC1F,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC;;;+CAG2C;IAC3C,UAAU,CAAC,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,cAAc,EAAE,MAAM,CAAC;QACvB,aAAa,EAAE,OAAO,CAAC;QACvB,gBAAgB,EAAE,OAAO,CAAC;QAC1B,iBAAiB,EAAE,OAAO,CAAC;KAC9B,CAAC;IACF;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE;QACrB,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KACpB,CAAC;CACL;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,IAK3C,QAAQ,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAC7B,QAAQ;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,KAChC,OAAO,CAAC,IAAI,CAAC,CAuyBnB"}
|
package/dist/index.js
CHANGED
|
@@ -149981,7 +149981,9 @@ function byteSize(value) {
|
|
|
149981
149981
|
return encoder.encode(value).length;
|
|
149982
149982
|
}
|
|
149983
149983
|
function stripTagPrefix(value) {
|
|
149984
|
-
|
|
149984
|
+
let stripped = value.replace(MALFORMED_TAG_PREFIX_REGEX, "");
|
|
149985
|
+
stripped = stripped.replace(TAG_PREFIX_REGEX, "");
|
|
149986
|
+
return stripped;
|
|
149985
149987
|
}
|
|
149986
149988
|
function prependTag(tagId, value) {
|
|
149987
149989
|
const stripped = stripTagPrefix(value);
|
|
@@ -149993,10 +149995,11 @@ function isThinkingPart(part) {
|
|
|
149993
149995
|
const candidate = part;
|
|
149994
149996
|
return candidate.type === "thinking" || candidate.type === "reasoning";
|
|
149995
149997
|
}
|
|
149996
|
-
var encoder, TAG_PREFIX_REGEX;
|
|
149998
|
+
var encoder, TAG_PREFIX_REGEX, MALFORMED_TAG_PREFIX_REGEX;
|
|
149997
149999
|
var init_tag_content_primitives = __esm(() => {
|
|
149998
150000
|
encoder = new TextEncoder;
|
|
149999
150001
|
TAG_PREFIX_REGEX = /^(?:\u00A7\d+\u00A7\s*)+/;
|
|
150002
|
+
MALFORMED_TAG_PREFIX_REGEX = /^(?:\u00A7\d+">\u00A7(?:\d+\u00A7)?\s*)+/;
|
|
150000
150003
|
});
|
|
150001
150004
|
|
|
150002
150005
|
// src/hooks/magic-context/tag-part-guards.ts
|
|
@@ -150397,6 +150400,7 @@ function runMigrations(db) {
|
|
|
150397
150400
|
var MIGRATIONS;
|
|
150398
150401
|
var init_migrations = __esm(() => {
|
|
150399
150402
|
init_logger();
|
|
150403
|
+
init_storage_db();
|
|
150400
150404
|
MIGRATIONS = [
|
|
150401
150405
|
{
|
|
150402
150406
|
version: 1,
|
|
@@ -150563,6 +150567,13 @@ var init_migrations = __esm(() => {
|
|
|
150563
150567
|
END;
|
|
150564
150568
|
`);
|
|
150565
150569
|
}
|
|
150570
|
+
},
|
|
150571
|
+
{
|
|
150572
|
+
version: 5,
|
|
150573
|
+
description: "One-shot heal of NULL session_meta columns",
|
|
150574
|
+
up: (db) => {
|
|
150575
|
+
healAllNullColumns(db);
|
|
150576
|
+
}
|
|
150566
150577
|
}
|
|
150567
150578
|
];
|
|
150568
150579
|
});
|
|
@@ -150639,12 +150650,10 @@ function initializeDatabase(db) {
|
|
|
150639
150650
|
updated_at INTEGER NOT NULL
|
|
150640
150651
|
);
|
|
150641
150652
|
|
|
150642
|
-
|
|
150643
|
-
|
|
150644
|
-
|
|
150645
|
-
|
|
150646
|
-
created_at INTEGER NOT NULL
|
|
150647
|
-
);
|
|
150653
|
+
-- session_notes and smart_notes were merged into the unified notes table
|
|
150654
|
+
-- by migration v1 (see features/magic-context/migrations.ts). The old tables
|
|
150655
|
+
-- are never recreated; fresh DBs create only notes, upgraded DBs have
|
|
150656
|
+
-- their old tables migrated and dropped by the migration runner.
|
|
150648
150657
|
|
|
150649
150658
|
CREATE TABLE IF NOT EXISTS memories (
|
|
150650
150659
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -150708,20 +150717,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
|
|
|
150708
150717
|
);
|
|
150709
150718
|
CREATE INDEX IF NOT EXISTS idx_dream_runs_project ON dream_runs(project_path, finished_at DESC);
|
|
150710
150719
|
|
|
150711
|
-
|
|
150712
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
150713
|
-
project_path TEXT NOT NULL,
|
|
150714
|
-
content TEXT NOT NULL,
|
|
150715
|
-
surface_condition TEXT NOT NULL,
|
|
150716
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
150717
|
-
created_session_id TEXT,
|
|
150718
|
-
created_at INTEGER NOT NULL,
|
|
150719
|
-
updated_at INTEGER NOT NULL,
|
|
150720
|
-
last_checked_at INTEGER,
|
|
150721
|
-
ready_at INTEGER,
|
|
150722
|
-
ready_reason TEXT
|
|
150723
|
-
);
|
|
150724
|
-
CREATE INDEX IF NOT EXISTS idx_smart_notes_project_status ON smart_notes(project_path, status);
|
|
150720
|
+
-- (smart_notes: see note above; merged into unified notes table by migration v1)
|
|
150725
150721
|
|
|
150726
150722
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
150727
150723
|
content,
|
|
@@ -150821,7 +150817,6 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
|
|
|
150821
150817
|
CREATE INDEX IF NOT EXISTS idx_session_facts_session ON session_facts(session_id);
|
|
150822
150818
|
CREATE INDEX IF NOT EXISTS idx_recomp_compartments_session ON recomp_compartments(session_id);
|
|
150823
150819
|
CREATE INDEX IF NOT EXISTS idx_recomp_facts_session ON recomp_facts(session_id);
|
|
150824
|
-
CREATE INDEX IF NOT EXISTS idx_session_notes_session ON session_notes(session_id);
|
|
150825
150820
|
CREATE INDEX IF NOT EXISTS idx_memories_project_status_category ON memories(project_path, status, category);
|
|
150826
150821
|
CREATE INDEX IF NOT EXISTS idx_memories_project_status_expires ON memories(project_path, status, expires_at);
|
|
150827
150822
|
CREATE INDEX IF NOT EXISTS idx_memories_project_category_hash ON memories(project_path, category, normalized_hash);
|
|
@@ -150867,6 +150862,8 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
|
|
|
150867
150862
|
ensureColumn(db, "session_meta", "recomp_partial_range_end", "INTEGER DEFAULT 0");
|
|
150868
150863
|
ensureColumn(db, "session_meta", "detected_context_limit", "INTEGER DEFAULT 0");
|
|
150869
150864
|
ensureColumn(db, "session_meta", "needs_emergency_recovery", "INTEGER DEFAULT 0");
|
|
150865
|
+
}
|
|
150866
|
+
function healAllNullColumns(db) {
|
|
150870
150867
|
healNullTextColumns(db);
|
|
150871
150868
|
healNullIntegerColumns(db);
|
|
150872
150869
|
healMissingMemoryBlockIds(db);
|
|
@@ -152428,6 +152425,51 @@ var init_constants = __esm(() => {
|
|
|
152428
152425
|
};
|
|
152429
152426
|
});
|
|
152430
152427
|
|
|
152428
|
+
// src/shared/bounded-session-map.ts
|
|
152429
|
+
class BoundedSessionMap {
|
|
152430
|
+
maxEntries;
|
|
152431
|
+
store = new Map;
|
|
152432
|
+
constructor(maxEntries) {
|
|
152433
|
+
if (!Number.isFinite(maxEntries) || maxEntries < 1) {
|
|
152434
|
+
throw new Error(`BoundedSessionMap: maxEntries must be >= 1, got ${maxEntries}`);
|
|
152435
|
+
}
|
|
152436
|
+
this.maxEntries = maxEntries;
|
|
152437
|
+
}
|
|
152438
|
+
get(sessionId) {
|
|
152439
|
+
const value = this.store.get(sessionId);
|
|
152440
|
+
if (value === undefined)
|
|
152441
|
+
return;
|
|
152442
|
+
this.store.delete(sessionId);
|
|
152443
|
+
this.store.set(sessionId, value);
|
|
152444
|
+
return value;
|
|
152445
|
+
}
|
|
152446
|
+
peek(sessionId) {
|
|
152447
|
+
return this.store.get(sessionId);
|
|
152448
|
+
}
|
|
152449
|
+
has(sessionId) {
|
|
152450
|
+
return this.store.has(sessionId);
|
|
152451
|
+
}
|
|
152452
|
+
set(sessionId, value) {
|
|
152453
|
+
if (this.store.has(sessionId)) {
|
|
152454
|
+
this.store.delete(sessionId);
|
|
152455
|
+
} else if (this.store.size >= this.maxEntries) {
|
|
152456
|
+
const oldest = this.store.keys().next().value;
|
|
152457
|
+
if (oldest !== undefined)
|
|
152458
|
+
this.store.delete(oldest);
|
|
152459
|
+
}
|
|
152460
|
+
this.store.set(sessionId, value);
|
|
152461
|
+
}
|
|
152462
|
+
delete(sessionId) {
|
|
152463
|
+
return this.store.delete(sessionId);
|
|
152464
|
+
}
|
|
152465
|
+
clear() {
|
|
152466
|
+
this.store.clear();
|
|
152467
|
+
}
|
|
152468
|
+
get size() {
|
|
152469
|
+
return this.store.size;
|
|
152470
|
+
}
|
|
152471
|
+
}
|
|
152472
|
+
|
|
152431
152473
|
// src/hooks/magic-context/temporal-awareness.ts
|
|
152432
152474
|
function formatGap(seconds) {
|
|
152433
152475
|
if (!Number.isFinite(seconds) || seconds < TEMPORAL_AWARENESS_THRESHOLD_SECONDS) {
|
|
@@ -152774,7 +152816,7 @@ function findFirstTextPart(parts) {
|
|
|
152774
152816
|
function isDroppedPlaceholder(text) {
|
|
152775
152817
|
return /^\[dropped \u00A7\d+\u00A7\]$/.test(text.trim());
|
|
152776
152818
|
}
|
|
152777
|
-
var injectionCache, CONSTRAINT_KEYWORDS;
|
|
152819
|
+
var INJECTION_CACHE_MAX = 100, injectionCache, CONSTRAINT_KEYWORDS;
|
|
152778
152820
|
var init_inject_compartments = __esm(() => {
|
|
152779
152821
|
init_compartment_storage();
|
|
152780
152822
|
init_constants();
|
|
@@ -152783,7 +152825,7 @@ var init_inject_compartments = __esm(() => {
|
|
|
152783
152825
|
init_read_session_db();
|
|
152784
152826
|
init_read_session_formatting();
|
|
152785
152827
|
init_temporal_awareness();
|
|
152786
|
-
injectionCache = new
|
|
152828
|
+
injectionCache = new BoundedSessionMap(INJECTION_CACHE_MAX);
|
|
152787
152829
|
CONSTRAINT_KEYWORDS = /\b(must|never|always|cannot|should not|must not)\b/i;
|
|
152788
152830
|
});
|
|
152789
152831
|
|
|
@@ -153309,6 +153351,28 @@ function generatePartId(timestampMs, counter = 0n) {
|
|
|
153309
153351
|
function getOpenCodeDbPath3() {
|
|
153310
153352
|
return join13(getDataDir(), "opencode", "opencode.db");
|
|
153311
153353
|
}
|
|
153354
|
+
function isOpenCodeSchemaCompatible(db, dbPath) {
|
|
153355
|
+
if (cachedSchemaCompatible?.path === dbPath) {
|
|
153356
|
+
return cachedSchemaCompatible.compatible;
|
|
153357
|
+
}
|
|
153358
|
+
try {
|
|
153359
|
+
const messageCols = new Set(db.prepare("PRAGMA table_info(message)").all().map((r) => r.name ?? "").filter((n) => n.length > 0));
|
|
153360
|
+
const partCols = new Set(db.prepare("PRAGMA table_info(part)").all().map((r) => r.name ?? "").filter((n) => n.length > 0));
|
|
153361
|
+
const missingMessage = REQUIRED_MESSAGE_COLUMNS.filter((c) => !messageCols.has(c));
|
|
153362
|
+
const missingPart = REQUIRED_PART_COLUMNS.filter((c) => !partCols.has(c));
|
|
153363
|
+
if (missingMessage.length > 0 || missingPart.length > 0) {
|
|
153364
|
+
log(`[magic-context] compaction-marker: OpenCode DB schema missing required columns ` + `(message: [${missingMessage.join(", ")}], part: [${missingPart.join(", ")}]). ` + `Marker injection disabled for this process. ` + `This usually means OpenCode was updated and magic-context is out of date.`);
|
|
153365
|
+
cachedSchemaCompatible = { path: dbPath, compatible: false };
|
|
153366
|
+
return false;
|
|
153367
|
+
}
|
|
153368
|
+
cachedSchemaCompatible = { path: dbPath, compatible: true };
|
|
153369
|
+
return true;
|
|
153370
|
+
} catch (error48) {
|
|
153371
|
+
log(`[magic-context] compaction-marker: schema probe failed: ${error48 instanceof Error ? error48.message : String(error48)}. ` + `Marker injection disabled until next process restart.`);
|
|
153372
|
+
cachedSchemaCompatible = { path: dbPath, compatible: false };
|
|
153373
|
+
return false;
|
|
153374
|
+
}
|
|
153375
|
+
}
|
|
153312
153376
|
function getWritableOpenCodeDb() {
|
|
153313
153377
|
const dbPath = getOpenCodeDbPath3();
|
|
153314
153378
|
if (cachedWriteDb?.path === dbPath) {
|
|
@@ -153327,18 +153391,15 @@ function getWritableOpenCodeDb() {
|
|
|
153327
153391
|
}
|
|
153328
153392
|
function findBoundaryUserMessage(sessionId, endOrdinal) {
|
|
153329
153393
|
const db = getWritableOpenCodeDb();
|
|
153330
|
-
const rows = db.prepare(
|
|
153331
|
-
|
|
153332
|
-
|
|
153333
|
-
|
|
153334
|
-
|
|
153335
|
-
|
|
153336
|
-
|
|
153337
|
-
}
|
|
153338
|
-
});
|
|
153394
|
+
const rows = db.prepare(`SELECT id, time_created, data
|
|
153395
|
+
FROM message
|
|
153396
|
+
WHERE session_id = ?
|
|
153397
|
+
AND NOT (COALESCE(json_extract(data, '$.summary'), 0) = 1
|
|
153398
|
+
AND COALESCE(json_extract(data, '$.finish'), '') = 'stop')
|
|
153399
|
+
ORDER BY time_created ASC, id ASC
|
|
153400
|
+
LIMIT ?`).all(sessionId, endOrdinal);
|
|
153339
153401
|
let bestMatch = null;
|
|
153340
|
-
for (
|
|
153341
|
-
const row = filtered[i];
|
|
153402
|
+
for (const row of rows) {
|
|
153342
153403
|
try {
|
|
153343
153404
|
const info = JSON.parse(row.data);
|
|
153344
153405
|
if (info.role === "user") {
|
|
@@ -153349,12 +153410,15 @@ function findBoundaryUserMessage(sessionId, endOrdinal) {
|
|
|
153349
153410
|
return bestMatch;
|
|
153350
153411
|
}
|
|
153351
153412
|
function injectCompactionMarker(args) {
|
|
153413
|
+
const db = getWritableOpenCodeDb();
|
|
153414
|
+
if (!isOpenCodeSchemaCompatible(db, getOpenCodeDbPath3())) {
|
|
153415
|
+
return null;
|
|
153416
|
+
}
|
|
153352
153417
|
const boundary = findBoundaryUserMessage(args.sessionId, args.endOrdinal);
|
|
153353
153418
|
if (!boundary) {
|
|
153354
153419
|
log(`[magic-context] compaction-marker: no user message found at or before ordinal ${args.endOrdinal}`);
|
|
153355
153420
|
return null;
|
|
153356
153421
|
}
|
|
153357
|
-
const db = getWritableOpenCodeDb();
|
|
153358
153422
|
const boundaryTime = boundary.timeCreated;
|
|
153359
153423
|
const summaryMsgId = generateMessageId(boundaryTime + 1, 1n);
|
|
153360
153424
|
const compactionPartId = generatePartId(boundaryTime, 1n);
|
|
@@ -153405,10 +153469,19 @@ function removeCompactionMarker(state) {
|
|
|
153405
153469
|
return false;
|
|
153406
153470
|
}
|
|
153407
153471
|
}
|
|
153408
|
-
var BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", cachedWriteDb = null;
|
|
153472
|
+
var BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", cachedWriteDb = null, REQUIRED_MESSAGE_COLUMNS, REQUIRED_PART_COLUMNS, cachedSchemaCompatible = null;
|
|
153409
153473
|
var init_compaction_marker = __esm(() => {
|
|
153410
153474
|
init_data_path();
|
|
153411
153475
|
init_logger();
|
|
153476
|
+
REQUIRED_MESSAGE_COLUMNS = ["id", "session_id", "time_created", "time_updated", "data"];
|
|
153477
|
+
REQUIRED_PART_COLUMNS = [
|
|
153478
|
+
"id",
|
|
153479
|
+
"message_id",
|
|
153480
|
+
"session_id",
|
|
153481
|
+
"time_created",
|
|
153482
|
+
"time_updated",
|
|
153483
|
+
"data"
|
|
153484
|
+
];
|
|
153412
153485
|
});
|
|
153413
153486
|
|
|
153414
153487
|
// src/hooks/magic-context/compaction-marker-manager.ts
|
|
@@ -164485,7 +164558,8 @@ async function runDream(args) {
|
|
|
164485
164558
|
smartNotesPending: result.smartNotesPending,
|
|
164486
164559
|
memoryChanges: persistedMemoryChanges
|
|
164487
164560
|
});
|
|
164488
|
-
const
|
|
164561
|
+
const POST_TASK_NAMES = new Set(["smart-notes", "user memories", "key files"]);
|
|
164562
|
+
const hasSuccessfulTask = result.tasks.some((t) => !t.error && !POST_TASK_NAMES.has(t.name));
|
|
164489
164563
|
if (hasSuccessfulTask) {
|
|
164490
164564
|
setDreamState(args.db, `last_dream_at:${args.projectIdentity}`, String(result.finishedAt));
|
|
164491
164565
|
setDreamState(args.db, "last_dream_at", String(result.finishedAt));
|
|
@@ -169438,7 +169512,8 @@ function createNudgePlacementStore(db) {
|
|
|
169438
169512
|
|
|
169439
169513
|
// src/hooks/magic-context/transform.ts
|
|
169440
169514
|
init_storage_meta_persisted();
|
|
169441
|
-
var
|
|
169515
|
+
var MESSAGE_TOKENS_CACHE_MAX = 100;
|
|
169516
|
+
var messageTokensBySession = new BoundedSessionMap(MESSAGE_TOKENS_CACHE_MAX);
|
|
169442
169517
|
function getMessageTokensCache(sessionId) {
|
|
169443
169518
|
let cache = messageTokensBySession.get(sessionId);
|
|
169444
169519
|
if (!cache) {
|
|
@@ -169628,11 +169703,11 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so ma
|
|
|
169628
169703
|
}
|
|
169629
169704
|
}
|
|
169630
169705
|
logTransformTiming(sessionId, "emergencyRecoveryBlock", tFirstPass);
|
|
169706
|
+
const projectIdentity = deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined;
|
|
169631
169707
|
let pendingCompartmentInjection = null;
|
|
169632
169708
|
if (fullFeatureMode) {
|
|
169633
169709
|
const tInj = performance.now();
|
|
169634
|
-
|
|
169635
|
-
pendingCompartmentInjection = prepareCompartmentInjection(db, sessionId, messages, isCacheBusting, projectPath, deps.memoryConfig?.injectionBudgetTokens, deps.experimentalTemporalAwareness);
|
|
169710
|
+
pendingCompartmentInjection = prepareCompartmentInjection(db, sessionId, messages, isCacheBusting, projectIdentity, deps.memoryConfig?.injectionBudgetTokens, deps.experimentalTemporalAwareness);
|
|
169636
169711
|
logTransformTiming(sessionId, "prepareCompartmentInjection", tInj);
|
|
169637
169712
|
}
|
|
169638
169713
|
let targets = new Map;
|
|
@@ -169747,7 +169822,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so ma
|
|
|
169747
169822
|
messages,
|
|
169748
169823
|
pendingCompartmentInjection,
|
|
169749
169824
|
fallbackModelId,
|
|
169750
|
-
projectPath:
|
|
169825
|
+
projectPath: projectIdentity,
|
|
169751
169826
|
injectionBudgetTokens: deps.memoryConfig?.injectionBudgetTokens,
|
|
169752
169827
|
getNotificationParams: rawGetNotifParams ? () => rawGetNotifParams(sessionId) : undefined,
|
|
169753
169828
|
cacheAlreadyBusting: isCacheBusting || schedulerDecisionEarly === "execute",
|
|
@@ -170539,7 +170614,7 @@ Use \`ctx_memory\` to manage cross-session project memories. Write new memories
|
|
|
170539
170614
|
- Discovered a non-obvious build/test command \u2192 \`ctx_memory(action="write", category="WORKFLOW_RULES", content="Always use scripts/release.sh for releases")\`
|
|
170540
170615
|
- Learned a constraint the hard way \u2192 \`ctx_memory(action="write", category="CONSTRAINTS", content="Dashboard Tauri build needs RGBA PNGs, not grayscale")\`
|
|
170541
170616
|
Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
|
|
170542
|
-
Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start=N end=M>\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
|
|
170617
|
+
Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start="N" end="M">\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
|
|
170543
170618
|
**Search before asking the user**: If you can't remember or don't know something that might have been discussed before or stored in project memory, use \`ctx_search\` before asking the user. Examples:
|
|
170544
170619
|
- Can't remember where a related codebase or dependency lives \u2192 \`ctx_search(query="opencode source code path")\`
|
|
170545
170620
|
- Forgot a prior architectural decision or constraint \u2192 \`ctx_search(query="why did we choose SQLite over postgres")\`
|
|
@@ -170552,15 +170627,14 @@ NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
|
|
|
170552
170627
|
NEVER drop user messages \u2014 they are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
|
|
170553
170628
|
NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
|
|
170554
170629
|
Before your turn finishes, consider using \`ctx_reduce\` to drop large tool outputs you no longer need.`;
|
|
170555
|
-
var BASE_INTRO_NO_REDUCE = (dropToolStructure) => `
|
|
170556
|
-
Use \`ctx_note\` for deferred intentions \u2014 things to tackle later, not right now. NOT for task tracking (use todos). Notes survive context compression and you'll be reminded at natural work boundaries (after commits, historian runs, todo completion).
|
|
170630
|
+
var BASE_INTRO_NO_REDUCE = (dropToolStructure) => `Use \`ctx_note\` for deferred intentions \u2014 things to tackle later, not right now. NOT for task tracking (use todos). Notes survive context compression and you'll be reminded at natural work boundaries (after commits, historian runs, todo completion).
|
|
170557
170631
|
Use \`ctx_memory\` to manage cross-session project memories. Write new memories or delete stale ones. Memories persist across sessions and are automatically injected into new sessions.
|
|
170558
170632
|
**Save to memory proactively**: If you spent multiple turns finding something (a file path, a DB location, a config pattern, a workaround), save it with \`ctx_memory\` so future sessions don't repeat the search. Examples:
|
|
170559
170633
|
- Found a project's source code path after searching \u2192 \`ctx_memory(action="write", category="ENVIRONMENT", content="OpenCode source is at ~/Work/OSS/opencode")\`
|
|
170560
170634
|
- Discovered a non-obvious build/test command \u2192 \`ctx_memory(action="write", category="WORKFLOW_RULES", content="Always use scripts/release.sh for releases")\`
|
|
170561
170635
|
- Learned a constraint the hard way \u2192 \`ctx_memory(action="write", category="CONSTRAINTS", content="Dashboard Tauri build needs RGBA PNGs, not grayscale")\`
|
|
170562
170636
|
Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
|
|
170563
|
-
Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start=N end=M>\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
|
|
170637
|
+
Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start="N" end="M">\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
|
|
170564
170638
|
**Search before asking the user**: If you can't remember or don't know something that might have been discussed before or stored in project memory, use \`ctx_search\` before asking the user. Examples:
|
|
170565
170639
|
- Can't remember where a related codebase or dependency lives \u2192 \`ctx_search(query="opencode source code path")\`
|
|
170566
170640
|
- Forgot a prior architectural decision or constraint \u2192 \`ctx_search(query="why did we choose SQLite over postgres")\`
|
|
@@ -170924,13 +170998,10 @@ ${sections.join(`
|
|
|
170924
170998
|
if (systemContent.length === 0)
|
|
170925
170999
|
return;
|
|
170926
171000
|
const currentHash = new Bun.CryptoHasher("md5").update(systemContent).digest("hex");
|
|
170927
|
-
|
|
170928
|
-
try {
|
|
170929
|
-
sessionMeta = getOrCreateSessionMeta(deps.db, sessionId);
|
|
170930
|
-
} catch (error48) {
|
|
170931
|
-
sessionLog(sessionId, "system-prompt-hash DB update failed:", error48);
|
|
171001
|
+
if (!sessionMetaEarly) {
|
|
170932
171002
|
return;
|
|
170933
171003
|
}
|
|
171004
|
+
const sessionMeta = sessionMetaEarly;
|
|
170934
171005
|
const previousHash = sessionMeta.systemPromptHash;
|
|
170935
171006
|
if (previousHash !== "" && previousHash !== "0" && previousHash !== currentHash) {
|
|
170936
171007
|
sessionLog(sessionId, `system prompt hash changed: ${previousHash} \u2192 ${currentHash} (len=${systemContent.length}), triggering flush`);
|
|
@@ -171743,7 +171814,7 @@ init_read_session_chunk();
|
|
|
171743
171814
|
import { tool } from "@opencode-ai/plugin";
|
|
171744
171815
|
|
|
171745
171816
|
// src/tools/ctx-expand/constants.ts
|
|
171746
|
-
var CTX_EXPAND_DESCRIPTION = "Decompress a compartment range to see the original conversation transcript. " +
|
|
171817
|
+
var CTX_EXPAND_DESCRIPTION = "Decompress a compartment range to see the original conversation transcript. " + 'Use start/end from <compartment start="N" end="M"> attributes. ' + "Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.";
|
|
171747
171818
|
var CTX_EXPAND_TOKEN_BUDGET = 15000;
|
|
171748
171819
|
|
|
171749
171820
|
// src/tools/ctx-expand/tools.ts
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded LRU map keyed by session id.
|
|
3
|
+
*
|
|
4
|
+
* Rationale: magic-context maintains several module-scope Maps that track
|
|
5
|
+
* per-session state (prepared injection cache, per-message token cache, etc.).
|
|
6
|
+
* These are cleared on the `session.deleted` event, but sessions that are
|
|
7
|
+
* never explicitly deleted — because OpenCode crashed, the user force-quit,
|
|
8
|
+
* the session was archived rather than deleted, or the session simply outlived
|
|
9
|
+
* the plugin process's interest in it — leak entries for the lifetime of the
|
|
10
|
+
* plugin process.
|
|
11
|
+
*
|
|
12
|
+
* In long-running OpenCode instances with thousands of sessions over time,
|
|
13
|
+
* an unbounded `Map<sessionId, LargeObject>` can retain tens of megabytes
|
|
14
|
+
* indefinitely. A session-scoped LRU with a generous cap (e.g. 100) covers
|
|
15
|
+
* any realistic working-set of active sessions a user actually cares about,
|
|
16
|
+
* while evicting cold session ids that will either never return or be
|
|
17
|
+
* rebuilt from durable SQLite state on their next transform pass.
|
|
18
|
+
*
|
|
19
|
+
* Implementation notes:
|
|
20
|
+
* - Built on `Map` which preserves insertion order. On every `set`/`get`
|
|
21
|
+
* touch we delete+reinsert to move the key to the tail (most-recent).
|
|
22
|
+
* - Eviction drops the oldest entry (first in iteration order).
|
|
23
|
+
* - The cached value type is generic — callers decide what per-session state
|
|
24
|
+
* to store. For injection/token state, all three properties of the cached
|
|
25
|
+
* object are safe to throw away: they are either recomputable from the
|
|
26
|
+
* messages array on the next pass, or reloadable from SQLite.
|
|
27
|
+
*/
|
|
28
|
+
export declare class BoundedSessionMap<V> {
|
|
29
|
+
private readonly maxEntries;
|
|
30
|
+
private readonly store;
|
|
31
|
+
constructor(maxEntries: number);
|
|
32
|
+
get(sessionId: string): V | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Peek without touching recency — useful for `has`-style checks that
|
|
35
|
+
* should not rearrange LRU order. Use sparingly; `get` is the normal
|
|
36
|
+
* access path.
|
|
37
|
+
*/
|
|
38
|
+
peek(sessionId: string): V | undefined;
|
|
39
|
+
has(sessionId: string): boolean;
|
|
40
|
+
set(sessionId: string, value: V): void;
|
|
41
|
+
delete(sessionId: string): boolean;
|
|
42
|
+
clear(): void;
|
|
43
|
+
get size(): number;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=bounded-session-map.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounded-session-map.d.ts","sourceRoot":"","sources":["../../src/shared/bounded-session-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,iBAAiB,CAAC,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;gBAElC,UAAU,EAAE,MAAM;IAO9B,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IASrC;;;;OAIG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAItC,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI/B,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAYtC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIlC,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI,MAAM,CAEjB;CACJ"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { BoundedSessionMap } from "./bounded-session-map";
|
|
3
|
+
|
|
4
|
+
describe("BoundedSessionMap", () => {
|
|
5
|
+
it("rejects non-positive caps", () => {
|
|
6
|
+
expect(() => new BoundedSessionMap(0)).toThrow();
|
|
7
|
+
expect(() => new BoundedSessionMap(-5)).toThrow();
|
|
8
|
+
expect(() => new BoundedSessionMap(Number.NaN)).toThrow();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("stores and retrieves values", () => {
|
|
12
|
+
const map = new BoundedSessionMap<number>(3);
|
|
13
|
+
map.set("a", 1);
|
|
14
|
+
map.set("b", 2);
|
|
15
|
+
expect(map.get("a")).toBe(1);
|
|
16
|
+
expect(map.get("b")).toBe(2);
|
|
17
|
+
expect(map.get("missing")).toBeUndefined();
|
|
18
|
+
expect(map.size).toBe(2);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("evicts the oldest entry when cap is exceeded", () => {
|
|
22
|
+
const map = new BoundedSessionMap<string>(3);
|
|
23
|
+
map.set("a", "alpha");
|
|
24
|
+
map.set("b", "bravo");
|
|
25
|
+
map.set("c", "charlie");
|
|
26
|
+
map.set("d", "delta"); // evicts "a"
|
|
27
|
+
expect(map.has("a")).toBe(false);
|
|
28
|
+
expect(map.has("b")).toBe(true);
|
|
29
|
+
expect(map.has("c")).toBe(true);
|
|
30
|
+
expect(map.has("d")).toBe(true);
|
|
31
|
+
expect(map.size).toBe(3);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("treats get() as a touch for LRU ordering", () => {
|
|
35
|
+
const map = new BoundedSessionMap<string>(3);
|
|
36
|
+
map.set("a", "alpha");
|
|
37
|
+
map.set("b", "bravo");
|
|
38
|
+
map.set("c", "charlie");
|
|
39
|
+
// Touch "a" — now "b" is the oldest.
|
|
40
|
+
expect(map.get("a")).toBe("alpha");
|
|
41
|
+
map.set("d", "delta");
|
|
42
|
+
expect(map.has("b")).toBe(false);
|
|
43
|
+
expect(map.has("a")).toBe(true);
|
|
44
|
+
expect(map.has("c")).toBe(true);
|
|
45
|
+
expect(map.has("d")).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("peek() does NOT touch recency", () => {
|
|
49
|
+
const map = new BoundedSessionMap<number>(3);
|
|
50
|
+
map.set("a", 1);
|
|
51
|
+
map.set("b", 2);
|
|
52
|
+
map.set("c", 3);
|
|
53
|
+
expect(map.peek("a")).toBe(1);
|
|
54
|
+
// Adding a fourth entry should still evict "a" since peek didn't touch it.
|
|
55
|
+
map.set("d", 4);
|
|
56
|
+
expect(map.has("a")).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("set() on existing key refreshes recency without growing size", () => {
|
|
60
|
+
const map = new BoundedSessionMap<number>(3);
|
|
61
|
+
map.set("a", 1);
|
|
62
|
+
map.set("b", 2);
|
|
63
|
+
map.set("c", 3);
|
|
64
|
+
map.set("a", 100); // refresh "a" to most-recent with new value
|
|
65
|
+
expect(map.size).toBe(3);
|
|
66
|
+
expect(map.get("a")).toBe(100);
|
|
67
|
+
map.set("d", 4); // evicts "b" (now oldest)
|
|
68
|
+
expect(map.has("b")).toBe(false);
|
|
69
|
+
expect(map.has("a")).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("delete() removes entries and returns true when present", () => {
|
|
73
|
+
const map = new BoundedSessionMap<number>(3);
|
|
74
|
+
map.set("a", 1);
|
|
75
|
+
expect(map.delete("a")).toBe(true);
|
|
76
|
+
expect(map.delete("a")).toBe(false);
|
|
77
|
+
expect(map.size).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("clear() drops all entries", () => {
|
|
81
|
+
const map = new BoundedSessionMap<number>(3);
|
|
82
|
+
map.set("a", 1);
|
|
83
|
+
map.set("b", 2);
|
|
84
|
+
map.clear();
|
|
85
|
+
expect(map.size).toBe(0);
|
|
86
|
+
expect(map.get("a")).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("tolerates cap=1 edge case (every set evicts previous)", () => {
|
|
90
|
+
const map = new BoundedSessionMap<number>(1);
|
|
91
|
+
map.set("a", 1);
|
|
92
|
+
map.set("b", 2);
|
|
93
|
+
expect(map.has("a")).toBe(false);
|
|
94
|
+
expect(map.get("b")).toBe(2);
|
|
95
|
+
expect(map.size).toBe(1);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded LRU map keyed by session id.
|
|
3
|
+
*
|
|
4
|
+
* Rationale: magic-context maintains several module-scope Maps that track
|
|
5
|
+
* per-session state (prepared injection cache, per-message token cache, etc.).
|
|
6
|
+
* These are cleared on the `session.deleted` event, but sessions that are
|
|
7
|
+
* never explicitly deleted — because OpenCode crashed, the user force-quit,
|
|
8
|
+
* the session was archived rather than deleted, or the session simply outlived
|
|
9
|
+
* the plugin process's interest in it — leak entries for the lifetime of the
|
|
10
|
+
* plugin process.
|
|
11
|
+
*
|
|
12
|
+
* In long-running OpenCode instances with thousands of sessions over time,
|
|
13
|
+
* an unbounded `Map<sessionId, LargeObject>` can retain tens of megabytes
|
|
14
|
+
* indefinitely. A session-scoped LRU with a generous cap (e.g. 100) covers
|
|
15
|
+
* any realistic working-set of active sessions a user actually cares about,
|
|
16
|
+
* while evicting cold session ids that will either never return or be
|
|
17
|
+
* rebuilt from durable SQLite state on their next transform pass.
|
|
18
|
+
*
|
|
19
|
+
* Implementation notes:
|
|
20
|
+
* - Built on `Map` which preserves insertion order. On every `set`/`get`
|
|
21
|
+
* touch we delete+reinsert to move the key to the tail (most-recent).
|
|
22
|
+
* - Eviction drops the oldest entry (first in iteration order).
|
|
23
|
+
* - The cached value type is generic — callers decide what per-session state
|
|
24
|
+
* to store. For injection/token state, all three properties of the cached
|
|
25
|
+
* object are safe to throw away: they are either recomputable from the
|
|
26
|
+
* messages array on the next pass, or reloadable from SQLite.
|
|
27
|
+
*/
|
|
28
|
+
export class BoundedSessionMap<V> {
|
|
29
|
+
private readonly maxEntries: number;
|
|
30
|
+
private readonly store = new Map<string, V>();
|
|
31
|
+
|
|
32
|
+
constructor(maxEntries: number) {
|
|
33
|
+
if (!Number.isFinite(maxEntries) || maxEntries < 1) {
|
|
34
|
+
throw new Error(`BoundedSessionMap: maxEntries must be >= 1, got ${maxEntries}`);
|
|
35
|
+
}
|
|
36
|
+
this.maxEntries = maxEntries;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get(sessionId: string): V | undefined {
|
|
40
|
+
const value = this.store.get(sessionId);
|
|
41
|
+
if (value === undefined) return undefined;
|
|
42
|
+
// Touch: move to most-recent position.
|
|
43
|
+
this.store.delete(sessionId);
|
|
44
|
+
this.store.set(sessionId, value);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Peek without touching recency — useful for `has`-style checks that
|
|
50
|
+
* should not rearrange LRU order. Use sparingly; `get` is the normal
|
|
51
|
+
* access path.
|
|
52
|
+
*/
|
|
53
|
+
peek(sessionId: string): V | undefined {
|
|
54
|
+
return this.store.get(sessionId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
has(sessionId: string): boolean {
|
|
58
|
+
return this.store.has(sessionId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
set(sessionId: string, value: V): void {
|
|
62
|
+
if (this.store.has(sessionId)) {
|
|
63
|
+
// Refresh recency.
|
|
64
|
+
this.store.delete(sessionId);
|
|
65
|
+
} else if (this.store.size >= this.maxEntries) {
|
|
66
|
+
// Evict oldest entry. Map iteration is insertion-ordered.
|
|
67
|
+
const oldest = this.store.keys().next().value;
|
|
68
|
+
if (oldest !== undefined) this.store.delete(oldest);
|
|
69
|
+
}
|
|
70
|
+
this.store.set(sessionId, value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
delete(sessionId: string): boolean {
|
|
74
|
+
return this.store.delete(sessionId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
clear(): void {
|
|
78
|
+
this.store.clear();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get size(): number {
|
|
82
|
+
return this.store.size;
|
|
83
|
+
}
|
|
84
|
+
}
|