Skip to content

Commit 0feaf50

Browse files
committed
feat(plugin): track reasoning bytes per tag and include in compartment trigger projection
Adds reasoning_byte_size column to tags table, computed during tagging from thinking/reasoning parts. The compartment trigger now projects reasoning clearing savings alongside tool drops and pending ctx_reduce ops, so the system can delay historian when reasoning cleanup alone would free enough space. Zero additional compute cost — reasoning bytes are calculated from parts we already iterate during tagging.
1 parent 6d604b6 commit 0feaf50

File tree

14 files changed

+63
-14
lines changed

14 files changed

+63
-14
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
262262
ensureColumn(db, "session_meta", "memory_block_cache", "TEXT DEFAULT ''");
263263
ensureColumn(db, "session_meta", "memory_block_count", "INTEGER DEFAULT 0");
264264
ensureColumn(db, "dream_queue", "retry_count", "INTEGER DEFAULT 0");
265+
ensureColumn(db, "tags", "reasoning_byte_size", "INTEGER DEFAULT 0");
265266
}
266267

267268
function ensureColumn(db: Database, table: string, column: string, definition: string): void {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ function makeMemoryDatabase(): Database {
2222
type TEXT,
2323
status TEXT DEFAULT 'active',
2424
byte_size INTEGER,
25-
tag_number INTEGER,
25+
tag_number INTEGER NOT NULL,
26+
reasoning_byte_size INTEGER NOT NULL DEFAULT 0,
2627
UNIQUE(session_id, id)
2728
);
2829
CREATE TABLE IF NOT EXISTS pending_ops (
@@ -113,8 +114,8 @@ describe("storage-tags", () => {
113114
db = makeMemoryDatabase();
114115
insertTag(db, "ses-1", "msg-1", "message", 100, 1);
115116
db.prepare(
116-
"INSERT INTO tags (session_id, message_id, type, byte_size, tag_number) VALUES (?, NULL, ?, ?, NULL)",
117-
).run("ses-1", "message", 200);
117+
"INSERT INTO tags (session_id, message_id, type, byte_size, tag_number) VALUES (?, NULL, ?, ?, ?)",
118+
).run("ses-1", "message", 200, 99);
118119

119120
const tags = getTagsBySession(db, "ses-1");
120121

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function getInsertTagStatement(db: Database): PreparedStatement {
1414
let stmt = insertTagStatements.get(db);
1515
if (!stmt) {
1616
stmt = db.prepare(
17-
"INSERT INTO tags (session_id, message_id, type, byte_size, tag_number) VALUES (?, ?, ?, ?, ?)",
17+
"INSERT INTO tags (session_id, message_id, type, byte_size, reasoning_byte_size, tag_number) VALUES (?, ?, ?, ?, ?, ?)",
1818
);
1919
insertTagStatements.set(db, stmt);
2020
}
@@ -78,6 +78,7 @@ interface TagRow {
7878
type: string;
7979
status: string;
8080
byte_size: number;
81+
reasoning_byte_size: number;
8182
session_id: string;
8283
tag_number: number;
8384
}
@@ -102,6 +103,7 @@ function isTagRow(row: unknown): row is TagRow {
102103
typeof r.session_id === "string" &&
103104
typeof r.tag_number === "number"
104105
);
106+
// reasoning_byte_size may be missing on old rows (ensureColumn adds DEFAULT 0)
105107
}
106108

107109
function toTagEntry(row: TagRow): TagEntry {
@@ -114,6 +116,7 @@ function toTagEntry(row: TagRow): TagEntry {
114116
type,
115117
status,
116118
byteSize: row.byte_size,
119+
reasoningByteSize: row.reasoning_byte_size ?? 0,
117120
sessionId: row.session_id,
118121
};
119122
}
@@ -141,8 +144,9 @@ export function insertTag(
141144
type: TagEntry["type"],
142145
byteSize: number,
143146
tagNumber: number,
147+
reasoningByteSize: number = 0,
144148
): number {
145-
getInsertTagStatement(db).run(sessionId, messageId, type, byteSize, tagNumber);
149+
getInsertTagStatement(db).run(sessionId, messageId, type, byteSize, reasoningByteSize, tagNumber);
146150

147151
return tagNumber;
148152
}
@@ -199,7 +203,7 @@ export function getMaxTagNumberBySession(db: Database, sessionId: string): numbe
199203
export function getTagsBySession(db: Database, sessionId: string): TagEntry[] {
200204
const rows = db
201205
.prepare(
202-
"SELECT id, message_id, type, status, byte_size, session_id, tag_number FROM tags WHERE session_id = ? ORDER BY tag_number ASC, id ASC",
206+
"SELECT id, message_id, type, status, byte_size, reasoning_byte_size, session_id, tag_number FROM tags WHERE session_id = ? ORDER BY tag_number ASC, id ASC",
203207
)
204208
.all(sessionId)
205209
.filter(isTagRow);
@@ -210,7 +214,7 @@ export function getTagsBySession(db: Database, sessionId: string): TagEntry[] {
210214
export function getTagById(db: Database, sessionId: string, tagId: number): TagEntry | null {
211215
const result = db
212216
.prepare(
213-
"SELECT id, message_id, type, status, byte_size, session_id, tag_number FROM tags WHERE session_id = ? AND tag_number = ?",
217+
"SELECT id, message_id, type, status, byte_size, reasoning_byte_size, session_id, tag_number FROM tags WHERE session_id = ? AND tag_number = ?",
214218
)
215219
.get(sessionId, tagId);
216220

@@ -228,7 +232,7 @@ export function getTopNBySize(db: Database, sessionId: string, n: number): TagEn
228232

229233
const rows = db
230234
.prepare(
231-
"SELECT id, message_id, type, status, byte_size, session_id, tag_number FROM tags WHERE session_id = ? AND status = 'active' ORDER BY byte_size DESC, tag_number ASC LIMIT ?",
235+
"SELECT id, message_id, type, status, byte_size, reasoning_byte_size, session_id, tag_number FROM tags WHERE session_id = ? AND status = 'active' ORDER BY byte_size DESC, tag_number ASC LIMIT ?",
232236
)
233237
.all(sessionId, n)
234238
.filter(isTagRow);

packages/plugin/src/features/magic-context/tagger.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function createMockDb(options?: { failCounterWrite?: boolean; rollbackTransactio
2323
messageId: string,
2424
type: TagEntry["type"],
2525
byteSize: number,
26+
_reasoningByteSize: number,
2627
_tagNumber: number,
2728
) => {
2829
const tag: StoredTag = {
@@ -31,6 +32,7 @@ function createMockDb(options?: { failCounterWrite?: boolean; rollbackTransactio
3132
type,
3233
status: "active",
3334
byteSize,
35+
reasoningByteSize: _reasoningByteSize ?? 0,
3436
sessionId,
3537
tagNumber: _tagNumber,
3638
};

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface Tagger {
88
type: TagEntry["type"],
99
byteSize: number,
1010
db: Database,
11+
reasoningByteSize?: number,
1112
): number;
1213
getTag(sessionId: string, messageId: string): number | undefined;
1314
bindTag(sessionId: string, messageId: string, tagNumber: number): void;
@@ -76,6 +77,7 @@ export function createTagger(): Tagger {
7677
type: TagEntry["type"],
7778
byteSize: number,
7879
db: Database,
80+
reasoningByteSize: number = 0,
7981
): number {
8082
const sessionAssignments = getSessionAssignments(sessionId);
8183

@@ -88,7 +90,7 @@ export function createTagger(): Tagger {
8890
const next = current + 1;
8991

9092
db.transaction(() => {
91-
insertTag(db, sessionId, messageId, type, byteSize, next);
93+
insertTag(db, sessionId, messageId, type, byteSize, next, reasoningByteSize);
9294
getUpsertCounterStatement(db).run(sessionId, next);
9395
})();
9496

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface TagEntry {
44
type: "message" | "tool" | "file";
55
status: "active" | "dropped" | "compacted";
66
byteSize: number;
7+
reasoningByteSize: number;
78
sessionId: string;
89
}
910

packages/plugin/src/hooks/magic-context/command-handler.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ function createTestDb(): Database {
1515
type TEXT,
1616
status TEXT DEFAULT 'active',
1717
byte_size INTEGER,
18-
tag_number INTEGER,
18+
tag_number INTEGER NOT NULL,
19+
reasoning_byte_size INTEGER NOT NULL DEFAULT 0,
1920
UNIQUE(session_id, tag_number)
2021
);
2122

packages/plugin/src/hooks/magic-context/compartment-trigger.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ function estimateProjectedPostDropPercentage(
4545
usage: ContextUsage,
4646
autoDropToolAge?: number,
4747
protectedTags?: number,
48+
clearReasoningAge?: number,
49+
clearedReasoningThroughTag?: number,
4850
): number | null {
4951
const activeTags = getTagsBySession(db, sessionId).filter((tag) => tag.status === "active");
5052
const totalActiveBytes = activeTags.reduce((sum, tag) => sum + tag.byteSize, 0);
@@ -62,8 +64,9 @@ function estimateProjectedPostDropPercentage(
6264
}
6365

6466
// 2. Heuristic auto-drop: old tool outputs outside protected tail
67+
// 3. Reasoning clearing: reasoning bytes on message tags between watermark and age cutoff
68+
const maxTag = activeTags.reduce((max, t) => Math.max(max, t.tagNumber), 0);
6569
if (autoDropToolAge !== undefined && protectedTags !== undefined) {
66-
const maxTag = activeTags.reduce((max, t) => Math.max(max, t.tagNumber), 0);
6770
const toolAgeCutoff = maxTag - autoDropToolAge;
6871
const protectedCutoff = maxTag - protectedTags;
6972
const pendingDropTagIds = new Set(pendingDrops.map((op) => op.tagId));
@@ -78,6 +81,19 @@ function estimateProjectedPostDropPercentage(
7881
}
7982
}
8083

84+
if (clearReasoningAge !== undefined && clearedReasoningThroughTag !== undefined) {
85+
const reasoningAgeCutoff = maxTag - clearReasoningAge;
86+
for (const tag of activeTags) {
87+
if (tag.type !== "message") continue;
88+
// Only count reasoning not yet cleared (between watermark and age cutoff)
89+
if (tag.tagNumber <= clearedReasoningThroughTag) continue;
90+
if (tag.tagNumber > reasoningAgeCutoff) continue;
91+
if (tag.reasoningByteSize > 0) {
92+
droppableBytes += tag.reasoningByteSize;
93+
}
94+
}
95+
}
96+
8197
if (droppableBytes === 0) return null;
8298

8399
const dropRatio = droppableBytes / totalActiveBytes;
@@ -159,6 +175,7 @@ export function checkCompartmentTrigger(
159175
compartmentTokenBudget: number = DEFAULT_COMPARTMENT_TOKEN_BUDGET,
160176
autoDropToolAge?: number,
161177
protectedTagCount?: number,
178+
clearReasoningAge?: number,
162179
): CompartmentTriggerResult {
163180
if (sessionMeta.compartmentInProgress) {
164181
return { shouldFire: false };
@@ -175,6 +192,8 @@ export function checkCompartmentTrigger(
175192
usage,
176193
autoDropToolAge,
177194
protectedTagCount,
195+
clearReasoningAge,
196+
sessionMeta.clearedReasoningThroughTag,
178197
);
179198
const relativePostDropTarget = executeThresholdPercentage * POST_DROP_TARGET_RATIO;
180199

packages/plugin/src/hooks/magic-context/event-handler.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ describe("createEventHandler", () => {
388388
type: "message",
389389
status: "active",
390390
byteSize: 64,
391+
reasoningByteSize: 0,
391392
sessionId: "ses-removed",
392393
},
393394
]);

packages/plugin/src/hooks/magic-context/event-handler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface EventHandlerDeps {
5858
config: {
5959
protected_tags: number;
6060
auto_drop_tool_age?: number;
61+
clear_reasoning_age?: number;
6162
execute_threshold_percentage?: number | { default: number; [modelKey: string]: number };
6263
cache_ttl: CacheTtlConfig;
6364
modelContextLimitsCache?: Map<string, number>;
@@ -290,6 +291,7 @@ export function createEventHandler(deps: EventHandlerDeps) {
290291
undefined, // compartmentTokenBudget — use default
291292
deps.config.auto_drop_tool_age ?? 100,
292293
deps.config.protected_tags,
294+
deps.config.clear_reasoning_age ?? 50,
293295
);
294296

295297
if (triggerResult.shouldFire) {

0 commit comments

Comments
 (0)