Skip to content

Commit 913f9b7

Browse files
committed
feat(plugin): weighted compressor selection with per-message depth tracking
The compressor now scores compartments using a weighted formula (0.7 × age + 0.3 × inverse compression depth) instead of always picking the oldest. Per-message compression depth is tracked in a new compression_depth table and incremented after each successful compression pass, preventing the earliest compartments from being re-compressed indefinitely while untouched middle history exists. Includes a 3× token expansion guard that falls back to the oldest contiguous subset when scored picks span too wide a range.
1 parent a1d832c commit 913f9b7

10 files changed

Lines changed: 329 additions & 24 deletions
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/// <reference types="bun-types" />
2+
3+
import { afterEach, describe, expect, it } from "bun:test";
4+
import { mkdtempSync, rmSync } from "node:fs";
5+
import { tmpdir } from "node:os";
6+
import { join } from "node:path";
7+
import {
8+
clearCompressionDepth,
9+
closeDatabase,
10+
getAverageCompressionDepth,
11+
getMaxCompressionDepth,
12+
incrementCompressionDepth,
13+
openDatabase,
14+
} from "./storage";
15+
16+
const tempDirs: string[] = [];
17+
const originalXdgDataHome = process.env.XDG_DATA_HOME;
18+
19+
function makeTempDir(prefix: string): string {
20+
const dir = mkdtempSync(join(tmpdir(), prefix));
21+
tempDirs.push(dir);
22+
return dir;
23+
}
24+
25+
function useTempDataHome(prefix: string): void {
26+
process.env.XDG_DATA_HOME = makeTempDir(prefix);
27+
}
28+
29+
afterEach(() => {
30+
closeDatabase();
31+
process.env.XDG_DATA_HOME = originalXdgDataHome;
32+
33+
for (const dir of tempDirs) {
34+
rmSync(dir, { recursive: true, force: true });
35+
}
36+
tempDirs.length = 0;
37+
});
38+
39+
describe("compression-depth-storage", () => {
40+
it("increments depth across a range and treats missing ordinals as zero in averages", () => {
41+
useTempDataHome("compression-depth-range-");
42+
const db = openDatabase();
43+
44+
incrementCompressionDepth(db, "ses-depth", 2, 4);
45+
46+
expect(getAverageCompressionDepth(db, "ses-depth", 1, 4)).toBe(0.75);
47+
expect(getAverageCompressionDepth(db, "ses-depth", 2, 4)).toBe(1);
48+
expect(getMaxCompressionDepth(db, "ses-depth")).toBe(1);
49+
});
50+
51+
it("accumulates repeated compression passes", () => {
52+
useTempDataHome("compression-depth-repeat-");
53+
const db = openDatabase();
54+
55+
incrementCompressionDepth(db, "ses-depth", 1, 3);
56+
incrementCompressionDepth(db, "ses-depth", 2, 4);
57+
58+
expect(getAverageCompressionDepth(db, "ses-depth", 1, 4)).toBe(1.5);
59+
expect(getMaxCompressionDepth(db, "ses-depth")).toBe(2);
60+
});
61+
62+
it("clears session depth rows", () => {
63+
useTempDataHome("compression-depth-clear-");
64+
const db = openDatabase();
65+
66+
incrementCompressionDepth(db, "ses-depth", 1, 5);
67+
clearCompressionDepth(db, "ses-depth");
68+
69+
expect(getAverageCompressionDepth(db, "ses-depth", 1, 5)).toBe(0);
70+
expect(getMaxCompressionDepth(db, "ses-depth")).toBe(0);
71+
});
72+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { Database } from "bun:sqlite";
2+
3+
type PreparedStatement = ReturnType<Database["prepare"]>;
4+
5+
const incrementDepthStatements = new WeakMap<Database, PreparedStatement>();
6+
const totalDepthStatements = new WeakMap<Database, PreparedStatement>();
7+
const maxDepthStatements = new WeakMap<Database, PreparedStatement>();
8+
const clearDepthStatements = new WeakMap<Database, PreparedStatement>();
9+
10+
interface TotalDepthRow {
11+
total_depth: number;
12+
}
13+
14+
interface MaxDepthRow {
15+
max_depth: number;
16+
}
17+
18+
function getIncrementDepthStatement(db: Database): PreparedStatement {
19+
let stmt = incrementDepthStatements.get(db);
20+
if (!stmt) {
21+
stmt = db.prepare(
22+
"INSERT INTO compression_depth (session_id, message_ordinal, depth) VALUES (?, ?, 1) ON CONFLICT(session_id, message_ordinal) DO UPDATE SET depth = depth + 1",
23+
);
24+
incrementDepthStatements.set(db, stmt);
25+
}
26+
return stmt;
27+
}
28+
29+
function getTotalDepthStatement(db: Database): PreparedStatement {
30+
let stmt = totalDepthStatements.get(db);
31+
if (!stmt) {
32+
stmt = db.prepare(
33+
"SELECT COALESCE(SUM(depth), 0) AS total_depth FROM compression_depth WHERE session_id = ? AND message_ordinal BETWEEN ? AND ?",
34+
);
35+
totalDepthStatements.set(db, stmt);
36+
}
37+
return stmt;
38+
}
39+
40+
function getMaxDepthStatement(db: Database): PreparedStatement {
41+
let stmt = maxDepthStatements.get(db);
42+
if (!stmt) {
43+
stmt = db.prepare(
44+
"SELECT COALESCE(MAX(depth), 0) AS max_depth FROM compression_depth WHERE session_id = ?",
45+
);
46+
maxDepthStatements.set(db, stmt);
47+
}
48+
return stmt;
49+
}
50+
51+
function getClearDepthStatement(db: Database): PreparedStatement {
52+
let stmt = clearDepthStatements.get(db);
53+
if (!stmt) {
54+
stmt = db.prepare("DELETE FROM compression_depth WHERE session_id = ?");
55+
clearDepthStatements.set(db, stmt);
56+
}
57+
return stmt;
58+
}
59+
60+
export function incrementCompressionDepth(
61+
db: Database,
62+
sessionId: string,
63+
startOrdinal: number,
64+
endOrdinal: number,
65+
): void {
66+
if (endOrdinal < startOrdinal) {
67+
return;
68+
}
69+
70+
db.transaction(() => {
71+
const stmt = getIncrementDepthStatement(db);
72+
for (let ordinal = startOrdinal; ordinal <= endOrdinal; ordinal += 1) {
73+
stmt.run(sessionId, ordinal);
74+
}
75+
})();
76+
}
77+
78+
export function getAverageCompressionDepth(
79+
db: Database,
80+
sessionId: string,
81+
startOrdinal: number,
82+
endOrdinal: number,
83+
): number {
84+
if (endOrdinal < startOrdinal) {
85+
return 0;
86+
}
87+
88+
const row = getTotalDepthStatement(db).get(
89+
sessionId,
90+
startOrdinal,
91+
endOrdinal,
92+
) as TotalDepthRow | null;
93+
const totalDepth = typeof row?.total_depth === "number" ? row.total_depth : 0;
94+
const messageCount = endOrdinal - startOrdinal + 1;
95+
return totalDepth / messageCount;
96+
}
97+
98+
export function getMaxCompressionDepth(db: Database, sessionId: string): number {
99+
const row = getMaxDepthStatement(db).get(sessionId) as MaxDepthRow | null;
100+
return typeof row?.max_depth === "number" ? row.max_depth : 0;
101+
}
102+
103+
export function clearCompressionDepth(db: Database, sessionId: string): void {
104+
getClearDepthStatement(db).run(sessionId);
105+
}

packages/plugin/src/features/magic-context/message-index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "../../hooks/magic-context/read-session-chunk";
77
import type { RawMessage } from "../../hooks/magic-context/read-session-raw";
88
import { removeSystemReminders } from "../../shared/system-directive";
9+
import { clearCompressionDepth } from "./compression-depth-storage";
910

1011
type PreparedStatement = ReturnType<Database["prepare"]>;
1112

@@ -122,6 +123,7 @@ export function clearIndexedMessages(db: Database, sessionId: string): void {
122123
db.transaction(() => {
123124
getDeleteFtsStatement(db).run(sessionId);
124125
getDeleteIndexStatement(db).run(sessionId);
126+
clearCompressionDepth(db, sessionId);
125127
})();
126128
}
127129

packages/plugin/src/features/magic-context/storage-db.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe("storage-db", () => {
5151
expect(isDatabasePersisted(db)).toBe(true);
5252
});
5353

54-
it("#when called first time #then creates all 5 required tables", () => {
54+
it("#when called first time #then creates required tables", () => {
5555
useTempDataHome("storage-db-tables-");
5656

5757
const db = openDatabase();
@@ -61,7 +61,13 @@ describe("storage-db", () => {
6161
.all() as Array<{ name: string }>;
6262
const tableNames = tables.map((t) => t.name);
6363
expect(tableNames).toEqual(
64-
expect.arrayContaining(["tags", "pending_ops", "source_contents", "session_meta"]),
64+
expect.arrayContaining([
65+
"tags",
66+
"pending_ops",
67+
"source_contents",
68+
"compression_depth",
69+
"session_meta",
70+
]),
6571
);
6672
});
6773

@@ -80,6 +86,7 @@ describe("storage-db", () => {
8086
"idx_pending_ops_session",
8187
"idx_source_contents_session",
8288
"idx_compartments_session",
89+
"idx_compression_depth_session",
8390
"idx_session_facts_session",
8491
"idx_session_notes_session",
8592
]),

packages/plugin/src/features/magic-context/storage-db.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ export function initializeDatabase(db: Database): void {
6060
created_at INTEGER NOT NULL,
6161
UNIQUE(session_id, sequence)
6262
);
63+
CREATE INDEX IF NOT EXISTS idx_compartments_session ON compartments(session_id);
64+
65+
CREATE TABLE IF NOT EXISTS compression_depth (
66+
session_id TEXT NOT NULL,
67+
message_ordinal INTEGER NOT NULL,
68+
depth INTEGER NOT NULL DEFAULT 0,
69+
PRIMARY KEY(session_id, message_ordinal)
70+
);
71+
CREATE INDEX IF NOT EXISTS idx_compression_depth_session ON compression_depth(session_id);
6372
6473
CREATE TABLE IF NOT EXISTS session_facts (
6574
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -245,7 +254,6 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
245254
created_at INTEGER NOT NULL
246255
);
247256
248-
CREATE INDEX IF NOT EXISTS idx_compartments_session ON compartments(session_id);
249257
CREATE INDEX IF NOT EXISTS idx_session_facts_session ON session_facts(session_id);
250258
CREATE INDEX IF NOT EXISTS idx_recomp_compartments_session ON recomp_compartments(session_id);
251259
CREATE INDEX IF NOT EXISTS idx_recomp_facts_session ON recomp_facts(session_id);

packages/plugin/src/features/magic-context/storage-meta-session.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Database } from "bun:sqlite";
2+
import { clearCompressionDepth } from "./compression-depth-storage";
23
import { clearIndexedMessages } from "./message-index";
34
import {
45
BOOLEAN_META_KEYS,
@@ -70,6 +71,7 @@ export function clearSession(db: Database, sessionId: string): void {
7071
db.prepare("DELETE FROM tags WHERE session_id = ?").run(sessionId);
7172
db.prepare("DELETE FROM session_meta WHERE session_id = ?").run(sessionId);
7273
db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
74+
clearCompressionDepth(db, sessionId);
7375
db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
7476
db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
7577
db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);

packages/plugin/src/features/magic-context/storage-meta.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describe("storage-meta", () => {
8383
//#then
8484
// 2 transactions: outer clearSession + nested clearIndexedMessages
8585
expect(db.transaction).toHaveBeenCalledTimes(2);
86-
expect(db.prepare).toHaveBeenCalledTimes(11);
86+
expect(db.prepare).toHaveBeenCalledTimes(12);
8787
});
8888
});
8989
});

packages/plugin/src/features/magic-context/storage.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ export {
77
replaceAllCompartmentState,
88
type SessionFact,
99
} from "./compartment-storage";
10+
export {
11+
clearCompressionDepth,
12+
getAverageCompressionDepth,
13+
getMaxCompressionDepth,
14+
incrementCompressionDepth,
15+
} from "./compression-depth-storage";
1016
export {
1117
clearIndexedMessages,
1218
deleteIndexedMessage,

0 commit comments

Comments
 (0)