@bd7pil/opencode-deep-memory 0.3.6 → 0.4.1

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/dist/index.js CHANGED
@@ -132,9 +132,9 @@ function checkpointRawPath(projectPath, _sessionID, _legacyDataRoot) {
132
132
  return path2.join(projectMemoryDir(projectPath), "checkpoint.raw.json");
133
133
  }
134
134
  function hashProject(absProjectPath) {
135
- const { createHash: createHash2 } = __require("crypto");
135
+ const { createHash: createHash4 } = __require("crypto");
136
136
  const normalized = path2.resolve(absProjectPath);
137
- return createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
137
+ return createHash4("sha256").update(normalized).digest("hex").slice(0, 16);
138
138
  }
139
139
 
140
140
  // src/shared/tokens.ts
@@ -257,6 +257,10 @@ var PluginState = class {
257
257
  _pendingEnrichments = /* @__PURE__ */ new Set();
258
258
  _lastUserText = /* @__PURE__ */ new Map();
259
259
  _pendingNotify = null;
260
+ _toolSignatures = /* @__PURE__ */ new Map();
261
+ _ccrCache = /* @__PURE__ */ new Map();
262
+ _lastInputTokens = 0;
263
+ _lastNudgeMessageCount = 0;
260
264
  agentOf(sessionID) {
261
265
  return this._agents.get(sessionID);
262
266
  }
@@ -373,6 +377,40 @@ var PluginState = class {
373
377
  this._pendingNotify = null;
374
378
  return n;
375
379
  }
380
+ recordToolSignature(callID, signature) {
381
+ this._toolSignatures.set(callID, signature);
382
+ }
383
+ isDuplicateTool(signature) {
384
+ for (const existing of this._toolSignatures.values()) {
385
+ if (existing === signature) return true;
386
+ }
387
+ return false;
388
+ }
389
+ getToolSignature(callID) {
390
+ return this._toolSignatures.get(callID);
391
+ }
392
+ ccStore(hash2, entry) {
393
+ if (this._ccrCache.size > 200) {
394
+ const oldest = [...this._ccrCache.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt).slice(0, 50);
395
+ for (const [k] of oldest) this._ccrCache.delete(k);
396
+ }
397
+ this._ccrCache.set(hash2, entry);
398
+ }
399
+ ccrGet(hash2) {
400
+ return this._ccrCache.get(hash2);
401
+ }
402
+ recordInputTokens(tokens) {
403
+ this._lastInputTokens = tokens;
404
+ }
405
+ lastInputTokens() {
406
+ return this._lastInputTokens;
407
+ }
408
+ recordNudge(messageCount) {
409
+ this._lastNudgeMessageCount = messageCount;
410
+ }
411
+ messagesSinceLastNudge(currentMessageCount) {
412
+ return currentMessageCount - this._lastNudgeMessageCount;
413
+ }
376
414
  };
377
415
  function createPluginState() {
378
416
  return new PluginState();
@@ -2203,6 +2241,9 @@ var SearchService = class {
2203
2241
  }
2204
2242
  };
2205
2243
 
2244
+ // src/tools/index.ts
2245
+ import { tool as tool4 } from "@opencode-ai/plugin";
2246
+
2206
2247
  // src/tools/memory-search.ts
2207
2248
  import { tool } from "@opencode-ai/plugin";
2208
2249
  function createMemorySearchTool(service) {
@@ -14789,6 +14830,35 @@ ${part.thinking || part.text || "[empty]"}
14789
14830
  return output;
14790
14831
  }
14791
14832
 
14833
+ // src/compress/ccr.ts
14834
+ import { createHash as createHash2 } from "crypto";
14835
+ var CCR_TTL_MS = 5 * 60 * 1e3;
14836
+ function ccrStore(state, original, compressed, toolName, callID) {
14837
+ const hash2 = sha256(original).slice(0, 24);
14838
+ state.ccStore(hash2, {
14839
+ hash: hash2,
14840
+ original,
14841
+ compressed,
14842
+ createdAt: Date.now(),
14843
+ toolName,
14844
+ callID
14845
+ });
14846
+ return hash2;
14847
+ }
14848
+ function ccrRetrieve(state, hash2) {
14849
+ const entry = state.ccrGet(hash2);
14850
+ if (!entry) return void 0;
14851
+ if (Date.now() - entry.createdAt > CCR_TTL_MS) return void 0;
14852
+ return entry.original;
14853
+ }
14854
+ function ccrInjectMarker(compressed, hash2) {
14855
+ return `${compressed}
14856
+ [ccr:${hash2}]`;
14857
+ }
14858
+ function sha256(data) {
14859
+ return createHash2("sha256").update(data).digest("hex");
14860
+ }
14861
+
14792
14862
  // src/tools/index.ts
14793
14863
  function createMemoryTools(service, opts) {
14794
14864
  const search = createMemorySearchTool(service);
@@ -14802,6 +14872,19 @@ function createMemoryTools(service, opts) {
14802
14872
  memory_expand: expand
14803
14873
  };
14804
14874
  }
14875
+ function createDeepExpandTool(state) {
14876
+ return tool4({
14877
+ description: "Retrieve original content that was previously compressed. Use hash from [ccr:...] markers.",
14878
+ args: {
14879
+ hash: tool4.schema.string().describe("The hash from the [ccr:HASH] marker")
14880
+ },
14881
+ execute: async (args) => {
14882
+ const original = ccrRetrieve(state, args.hash);
14883
+ if (original) return { title: "Expanded content", output: original };
14884
+ return { title: "Not found", output: "Content expired or hash not found." };
14885
+ }
14886
+ });
14887
+ }
14805
14888
 
14806
14889
  // src/extract/capture.ts
14807
14890
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
@@ -14960,17 +15043,17 @@ function extractFileChanges(messages) {
14960
15043
  for (const msg of messages) {
14961
15044
  for (const part of msg.parts) {
14962
15045
  if (part.type !== "tool" || !part.tool) continue;
14963
- const tool4 = part.tool.toLowerCase();
14964
- if (tool4 === "write" || tool4 === "edit") {
15046
+ const tool5 = part.tool.toLowerCase();
15047
+ if (tool5 === "write" || tool5 === "edit") {
14965
15048
  const filePath = part.args?.filePath || part.args?.path || "";
14966
15049
  if (filePath) {
14967
- const key = `${filePath}:${tool4}`;
15050
+ const key = `${filePath}:${tool5}`;
14968
15051
  if (!seen.has(key)) {
14969
15052
  seen.add(key);
14970
- changes.push({ path: filePath, operation: tool4 });
15053
+ changes.push({ path: filePath, operation: tool5 });
14971
15054
  }
14972
15055
  }
14973
- } else if (tool4 === "bash" || tool4 === "execute") {
15056
+ } else if (tool5 === "bash" || tool5 === "execute") {
14974
15057
  const cmd = part.args?.command || "";
14975
15058
  const match = BASH_FILE_OP_RE.exec(cmd);
14976
15059
  if (match?.[1]) {
@@ -15131,6 +15214,481 @@ function createCompactingHandler(args) {
15131
15214
  };
15132
15215
  }
15133
15216
 
15217
+ // src/compress/pressure.ts
15218
+ var DEFAULT_MAX_CONTEXT = 128e3;
15219
+ var THRESHOLDS = {
15220
+ medium: 0.3,
15221
+ high: 0.5
15222
+ };
15223
+ var MODEL_CONTEXT_LIMITS = {
15224
+ "deepseek-chat": 64e3,
15225
+ "deepseek-reasoner": 64e3,
15226
+ "mimo-v2.5-pro": 128e3,
15227
+ "mimo-v2.5": 128e3,
15228
+ "claude-sonnet-4-20250514": 2e5,
15229
+ "gpt-4o": 128e3
15230
+ };
15231
+ function estimateTokens2(text) {
15232
+ let cjk = 0;
15233
+ let other = 0;
15234
+ for (const ch of text) {
15235
+ if (/[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef\u3040-\u309f\u30a0-\u30ff]/.test(ch)) {
15236
+ cjk++;
15237
+ } else {
15238
+ other++;
15239
+ }
15240
+ }
15241
+ return Math.ceil(cjk * 0.7 + other / 3.8);
15242
+ }
15243
+ function extractTokensFromMessages(messages) {
15244
+ let total = 0;
15245
+ for (const msg of messages) {
15246
+ for (const part of msg.parts) {
15247
+ if (typeof part !== "object" || part === null) continue;
15248
+ const p = part;
15249
+ if (p["type"] === "text" && typeof p["text"] === "string") {
15250
+ total += estimateTokens2(p["text"]);
15251
+ } else if (p["type"] === "tool") {
15252
+ const state = p["state"];
15253
+ if (state?.["output"] && typeof state["output"] === "string") {
15254
+ total += estimateTokens2(state["output"]);
15255
+ }
15256
+ if (state?.["error"] && typeof state["error"] === "string") {
15257
+ total += estimateTokens2(state["error"]);
15258
+ }
15259
+ } else if (p["type"] === "reasoning" || p["type"] === "thinking") {
15260
+ if (typeof p["text"] === "string") {
15261
+ total += estimateTokens2(p["text"]);
15262
+ }
15263
+ }
15264
+ }
15265
+ }
15266
+ return total;
15267
+ }
15268
+ function extractInputTokensFromMessages(messages) {
15269
+ for (let i = messages.length - 1; i >= 0; i--) {
15270
+ const msg = messages[i];
15271
+ for (const part of msg.parts) {
15272
+ if (typeof part !== "object" || part === null) continue;
15273
+ const p = part;
15274
+ if (p["type"] === "step-finish") {
15275
+ const tokens = p.tokens;
15276
+ if (tokens?.input && tokens.input > 0) {
15277
+ return tokens.input;
15278
+ }
15279
+ }
15280
+ }
15281
+ }
15282
+ return 0;
15283
+ }
15284
+ function detectPressure(messages, modelId) {
15285
+ const maxContext = (modelId ? MODEL_CONTEXT_LIMITS[modelId] : void 0) ?? DEFAULT_MAX_CONTEXT;
15286
+ const inputTokens = extractInputTokensFromMessages(messages);
15287
+ const estimated = inputTokens > 0 ? inputTokens : extractTokensFromMessages(messages);
15288
+ const ratio = Math.min(estimated / maxContext, 1);
15289
+ let level;
15290
+ if (ratio >= THRESHOLDS.high) level = "high";
15291
+ else if (ratio >= THRESHOLDS.medium) level = "medium";
15292
+ else level = "low";
15293
+ return { level, ratio, estimatedTokens: estimated };
15294
+ }
15295
+
15296
+ // src/compress/dedup.ts
15297
+ var PROTECTED_TOOLS = /* @__PURE__ */ new Set(["question", "edit", "write", "todowrite", "todoread", "memory_store", "memory_search", "memory_forget"]);
15298
+ function createToolSignature(tool5, args) {
15299
+ if (!args) return tool5;
15300
+ const sorted = Object.keys(args).sort().map((k) => `${k}:${JSON.stringify(args[k])}`).join(",");
15301
+ return `${tool5}::${sorted}`;
15302
+ }
15303
+ function deduplicateToolOutputs(messages, state) {
15304
+ let deduped = 0;
15305
+ const seen = /* @__PURE__ */ new Map();
15306
+ for (const msg of messages) {
15307
+ for (const part of msg.parts) {
15308
+ if (typeof part !== "object" || part === null) continue;
15309
+ const p = part;
15310
+ if (p["type"] !== "tool") continue;
15311
+ const toolName = p["tool"];
15312
+ const callID = p["callID"];
15313
+ if (!toolName || !callID) continue;
15314
+ if (PROTECTED_TOOLS.has(toolName)) continue;
15315
+ const status = p["state"]?.["status"];
15316
+ if (status !== "completed") continue;
15317
+ const toolState = p["state"];
15318
+ const input = toolState["input"];
15319
+ const signature = createToolSignature(toolName, input);
15320
+ const existing = seen.get(signature);
15321
+ if (existing && existing !== callID) {
15322
+ toolState["output"] = "[superseded by duplicate call]";
15323
+ state.recordToolSignature(callID, signature);
15324
+ deduped++;
15325
+ } else {
15326
+ seen.set(signature, callID);
15327
+ state.recordToolSignature(callID, signature);
15328
+ }
15329
+ }
15330
+ }
15331
+ return deduped;
15332
+ }
15333
+
15334
+ // src/compress/error-purge.ts
15335
+ var ERROR_PURGE_TURN_THRESHOLD = 4;
15336
+ function purgeOldErrors(messages) {
15337
+ let purged = 0;
15338
+ const totalMessages = messages.length;
15339
+ for (let i = 0; i < totalMessages; i++) {
15340
+ const msg = messages[i];
15341
+ for (const part of msg.parts) {
15342
+ if (typeof part !== "object" || part === null) continue;
15343
+ const p = part;
15344
+ if (p["type"] !== "tool") continue;
15345
+ const toolState = p["state"];
15346
+ if (toolState?.["status"] !== "error") continue;
15347
+ const age = totalMessages - i;
15348
+ if (age < ERROR_PURGE_TURN_THRESHOLD) continue;
15349
+ if (typeof toolState["input"] === "object" && toolState["input"] !== null) {
15350
+ const input = toolState["input"];
15351
+ for (const key of Object.keys(input)) {
15352
+ if (key === "command" || key === "query" || key === "path" || key === "filePath") continue;
15353
+ input[key] = "[purged]";
15354
+ }
15355
+ }
15356
+ purged++;
15357
+ }
15358
+ }
15359
+ return purged;
15360
+ }
15361
+
15362
+ // src/compress/tool-compress.ts
15363
+ var TOOL_COMPRESS_STRATEGIES = {
15364
+ read: compressFileRead,
15365
+ bash: compressBash,
15366
+ grep: compressSearchResults,
15367
+ glob: compressGlob,
15368
+ ripgrep: compressSearchResults,
15369
+ rg: compressSearchResults,
15370
+ find: compressGlob,
15371
+ search: compressSearchResults,
15372
+ grep_app_searchGitHub: compressSearchResults,
15373
+ searxng_searxng_web_search: compressSearchResults,
15374
+ websearch_web_search_exa: compressSearchResults,
15375
+ tavily_tavily_search: compressSearchResults
15376
+ };
15377
+ var DEFAULT_HEAD_LINES = 50;
15378
+ var DEFAULT_TAIL_LINES = 20;
15379
+ var MAX_LINE_LENGTH = 500;
15380
+ function compressToolOutput(toolName, output) {
15381
+ if (!output || output.length < 500) return output;
15382
+ const strategy = TOOL_COMPRESS_STRATEGIES[toolName];
15383
+ if (strategy) return strategy(output);
15384
+ return compressGeneric(output);
15385
+ }
15386
+ function compressFileRead(output) {
15387
+ const lines = output.split("\n");
15388
+ if (lines.length <= 100) return output;
15389
+ const head = lines.slice(0, DEFAULT_HEAD_LINES);
15390
+ const tail = lines.slice(-DEFAULT_TAIL_LINES);
15391
+ const keyLines = extractKeyLines(lines.slice(DEFAULT_HEAD_LINES, -DEFAULT_TAIL_LINES));
15392
+ const parts = [...head, "...[truncated]", ...keyLines.slice(0, 10), "...[truncated]", ...tail];
15393
+ return parts.join("\n");
15394
+ }
15395
+ function compressBash(output) {
15396
+ const lines = output.split("\n");
15397
+ if (lines.length <= 50) return output;
15398
+ const errorLines = lines.filter((l) => /error|fail|exception|fatal|panic/i.test(l)).slice(0, 5);
15399
+ const tail = lines.slice(-30);
15400
+ return [...errorLines, ...tail].join("\n");
15401
+ }
15402
+ function compressSearchResults(output) {
15403
+ const lines = output.split("\n");
15404
+ if (lines.length <= 30) return output;
15405
+ const grouped = groupByFile(lines);
15406
+ const result = [];
15407
+ let count = 0;
15408
+ for (const [file2, matches] of grouped) {
15409
+ if (count >= 20) break;
15410
+ result.push(`--- ${file2} ---`);
15411
+ const kept = matches.slice(0, 5);
15412
+ for (const m of kept) {
15413
+ result.push(truncateLine(m, MAX_LINE_LENGTH));
15414
+ count++;
15415
+ }
15416
+ if (matches.length > 5) result.push(` ...[${matches.length - 5} more matches]`);
15417
+ }
15418
+ if (count >= 20 && lines.length > 30) {
15419
+ result.push(`
15420
+ ...[${lines.length - count} more lines truncated]`);
15421
+ }
15422
+ return result.join("\n");
15423
+ }
15424
+ function compressGlob(output) {
15425
+ const lines = output.split("\n").filter((l) => l.trim());
15426
+ if (lines.length <= 30) return output;
15427
+ const head = lines.slice(0, 30);
15428
+ return [...head, `
15429
+ ...[${lines.length - 30} more files]`].join("\n");
15430
+ }
15431
+ function compressGeneric(output) {
15432
+ const lines = output.split("\n");
15433
+ if (lines.length <= 50) {
15434
+ if (output.length <= 2e3) return output;
15435
+ return output.slice(0, 1500) + "\n...[truncated]" + output.slice(-500);
15436
+ }
15437
+ const head = lines.slice(0, 30);
15438
+ const tail = lines.slice(-15);
15439
+ const errorLines = lines.filter((l) => /error|fail|exception|fatal/i.test(l)).slice(0, 5);
15440
+ return [...head, "...[truncated]", ...errorLines, "...[truncated]", ...tail].join("\n");
15441
+ }
15442
+ function extractKeyLines(lines) {
15443
+ return lines.filter(
15444
+ (l) => /\b(function |class |def |import |export |interface |type |const |let |var |return |throw |Error|Exception)\b/.test(l) || /error|warn|fail|exception/i.test(l)
15445
+ );
15446
+ }
15447
+ function groupByFile(lines) {
15448
+ const groups = /* @__PURE__ */ new Map();
15449
+ let currentFile = "unknown";
15450
+ for (const line of lines) {
15451
+ const fileMatch = line.match(/^(\/[^\s:]+):/);
15452
+ if (fileMatch) {
15453
+ currentFile = fileMatch[1];
15454
+ }
15455
+ if (!groups.has(currentFile)) groups.set(currentFile, []);
15456
+ groups.get(currentFile).push(line);
15457
+ }
15458
+ return groups;
15459
+ }
15460
+ function truncateLine(line, maxLen) {
15461
+ if (line.length <= maxLen) return line;
15462
+ return line.slice(0, maxLen - 15) + "...[truncated]";
15463
+ }
15464
+
15465
+ // src/compress/json-crush.ts
15466
+ import { createHash as createHash3 } from "crypto";
15467
+ function crushJsonArray(content, maxItems = 15) {
15468
+ try {
15469
+ const parsed = JSON.parse(content);
15470
+ if (!Array.isArray(parsed)) return content;
15471
+ if (parsed.length <= maxItems) return content;
15472
+ const firstFraction = 0.3;
15473
+ const lastFraction = 0.15;
15474
+ const firstCount = Math.max(1, Math.floor(maxItems * firstFraction));
15475
+ const lastCount = Math.max(1, Math.floor(maxItems * lastFraction));
15476
+ const midCount = maxItems - firstCount - lastCount;
15477
+ const first = parsed.slice(0, firstCount);
15478
+ const last = parsed.slice(-lastCount);
15479
+ const mid = deduplicateMiddle(parsed.slice(firstCount, -lastCount), midCount);
15480
+ const result = [...first, ...mid, ...last];
15481
+ const dropped = parsed.length - result.length;
15482
+ if (dropped > 0) {
15483
+ const hash2 = sha2562(content).slice(0, 12);
15484
+ result.push({ _ccr_dropped: `[${dropped} items offloaded, hash=${hash2}]` });
15485
+ }
15486
+ return JSON.stringify(result, null, 2);
15487
+ } catch {
15488
+ return content;
15489
+ }
15490
+ }
15491
+ function deduplicateMiddle(items, maxCount) {
15492
+ if (items.length <= maxCount) return items;
15493
+ const seen = /* @__PURE__ */ new Set();
15494
+ const unique = [];
15495
+ for (const item of items) {
15496
+ const key = typeof item === "object" ? JSON.stringify(item) : String(item);
15497
+ if (!seen.has(key)) {
15498
+ seen.add(key);
15499
+ unique.push(item);
15500
+ if (unique.length >= maxCount) break;
15501
+ }
15502
+ }
15503
+ return unique;
15504
+ }
15505
+ function sha2562(data) {
15506
+ return createHash3("sha256").update(data).digest("hex");
15507
+ }
15508
+
15509
+ // src/compress/message-prune.ts
15510
+ var PRUNE_THRESHOLD = 8;
15511
+ function pruneOldMessages(messages) {
15512
+ let pruned = 0;
15513
+ const protectedTail = messages.length - PRUNE_THRESHOLD;
15514
+ for (let i = 3; i < protectedTail; i++) {
15515
+ const msg = messages[i];
15516
+ if (msg.info.role !== "assistant") continue;
15517
+ for (const part of msg.parts) {
15518
+ if (typeof part !== "object" || part === null) continue;
15519
+ const p = part;
15520
+ if (p["type"] !== "text" || typeof p["text"] !== "string") continue;
15521
+ const text = p["text"];
15522
+ if (text.length < 500) continue;
15523
+ if (text === "[cleared]" || text === "[stripped]" || text.startsWith("[compressed")) continue;
15524
+ const keyInfo = extractKeyInfo(text);
15525
+ if (keyInfo.length < text.length * 0.6) {
15526
+ p["text"] = keyInfo + "\n[compressed from " + text.length + " chars]";
15527
+ pruned++;
15528
+ }
15529
+ }
15530
+ }
15531
+ return pruned;
15532
+ }
15533
+ function extractKeyInfo(text) {
15534
+ const lines = text.split("\n");
15535
+ const keyLines = [];
15536
+ let inCodeBlock = false;
15537
+ for (const line of lines) {
15538
+ if (line.trim().startsWith("```")) {
15539
+ inCodeBlock = !inCodeBlock;
15540
+ if (inCodeBlock) keyLines.push(line);
15541
+ continue;
15542
+ }
15543
+ if (inCodeBlock) {
15544
+ if (keyLines.length < 30 && line.trim()) keyLines.push(line);
15545
+ continue;
15546
+ }
15547
+ if (/^#{1,3}\s/.test(line) || /error|fail|warning|important|critical|decision|constraint/i.test(line) || /^\s*[-*]\s/.test(line) || /^\s*\d+\.\s/.test(line)) {
15548
+ keyLines.push(line);
15549
+ }
15550
+ }
15551
+ if (keyLines.length < 3) {
15552
+ return lines.slice(0, 5).join("\n");
15553
+ }
15554
+ return keyLines.join("\n");
15555
+ }
15556
+
15557
+ // src/compress/nudge.ts
15558
+ var NUDGE_COOLDOWN = 5;
15559
+ function shouldInjectNudge(level, messagesSinceLastNudge) {
15560
+ if (level !== "high") return false;
15561
+ if (messagesSinceLastNudge < NUDGE_COOLDOWN) return false;
15562
+ return true;
15563
+ }
15564
+ function buildNudgeText(level) {
15565
+ if (level === "high") {
15566
+ return '\n<dm-nudge level="high">Context pressure is high. Consider summarizing old completed tasks and moving on. Use memory_store to persist important findings before they are compressed.</dm-nudge>';
15567
+ }
15568
+ return "";
15569
+ }
15570
+
15571
+ // src/compress/detector.ts
15572
+ function detectContentType(content) {
15573
+ const trimmed = content.trimStart();
15574
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
15575
+ try {
15576
+ JSON.parse(content);
15577
+ return "json";
15578
+ } catch {
15579
+ }
15580
+ }
15581
+ if (/^diff --git |^@@ -\d+,\d+ \+\d+,\d+ @@|^[+-]{3} \//m.test(content)) return "diff";
15582
+ if (/Traceback \(most recent call last\)|at \S+\.\S+\(|Error: |Exception: |TypeError: |ReferenceError: /m.test(content)) return "error-trace";
15583
+ if (/<[a-z][\s\S]*>/i.test(content) && /<(html|div|span|body|head|script|style)[\s>]/i.test(content)) return "html";
15584
+ const lines = content.split("\n");
15585
+ const logLineCount = lines.filter((l) => /^\d{4}-\d{2}-\d{2}|^\[\d{4}|ERROR|WARN|INFO|DEBUG|FATAL|TRACE/.test(l)).length;
15586
+ if (lines.length > 5 && logLineCount / lines.length > 0.3) return "log";
15587
+ const codePatterns = /\b(function |class |def |import |from .+ import|const |let |var |export |interface |type |struct |fn |func |pub |private |protected )\b/;
15588
+ const codeLines = lines.filter((l) => codePatterns.test(l)).length;
15589
+ if (lines.length > 10 && codeLines / lines.length > 0.15) return "code";
15590
+ return "text";
15591
+ }
15592
+
15593
+ // src/compress/index.ts
15594
+ function runCompressionPipeline(ctx) {
15595
+ const { messages, state, logger } = ctx;
15596
+ const pressure = detectPressure(messages, ctx.modelId);
15597
+ state.recordInputTokens(pressure.estimatedTokens);
15598
+ const stats = {
15599
+ toolDedup: 0,
15600
+ errorPurge: 0,
15601
+ toolOutputCompressed: 0,
15602
+ jsonCrushed: 0,
15603
+ messagePruned: 0,
15604
+ ccrStored: 0,
15605
+ nudgeInjected: false,
15606
+ pressureLevel: pressure.level,
15607
+ estimatedTokens: pressure.estimatedTokens
15608
+ };
15609
+ stats.toolDedup = deduplicateToolOutputs(messages, state);
15610
+ stats.errorPurge = purgeOldErrors(messages);
15611
+ stats.toolOutputCompressed = compressOldToolOutputs(messages, state);
15612
+ stats.jsonCrushed = crushJsonToolOutputs(messages, state);
15613
+ if (pressure.level === "medium" || pressure.level === "high") {
15614
+ stats.messagePruned = pruneOldMessages(messages);
15615
+ }
15616
+ const messagesSinceNudge = state.messagesSinceLastNudge(messages.length);
15617
+ if (shouldInjectNudge(pressure.level, messagesSinceNudge)) {
15618
+ const lastMsg = messages[messages.length - 1];
15619
+ if (lastMsg) {
15620
+ const textParts = lastMsg.parts.filter(
15621
+ (p) => typeof p === "object" && p !== null && p.type === "text"
15622
+ );
15623
+ const lastTextPart = textParts[textParts.length - 1];
15624
+ if (lastTextPart && typeof lastTextPart.text === "string") {
15625
+ lastTextPart.text += buildNudgeText(pressure.level);
15626
+ stats.nudgeInjected = true;
15627
+ state.recordNudge(messages.length);
15628
+ }
15629
+ }
15630
+ }
15631
+ const active = stats.toolDedup > 0 || stats.errorPurge > 0 || stats.toolOutputCompressed > 0 || stats.jsonCrushed > 0 || stats.messagePruned > 0 || stats.nudgeInjected;
15632
+ if (active) {
15633
+ logger?.debug("compress: pipeline result", { ...stats });
15634
+ } else {
15635
+ logger?.debug("compress: no action needed", { ratio: pressure.ratio.toFixed(2) });
15636
+ }
15637
+ return { stats };
15638
+ }
15639
+ function compressOldToolOutputs(messages, state) {
15640
+ let compressed = 0;
15641
+ const protectedTail = messages.length - 8;
15642
+ for (let i = 3; i < protectedTail; i++) {
15643
+ const msg = messages[i];
15644
+ for (const part of msg.parts) {
15645
+ if (typeof part !== "object" || part === null) continue;
15646
+ const p = part;
15647
+ if (p.type !== "tool") continue;
15648
+ if (p.state?.status !== "completed") continue;
15649
+ if (!p.state.output) continue;
15650
+ if (p.state.output === "[superseded by duplicate call]") continue;
15651
+ if (p.state.output.startsWith("[compressed")) continue;
15652
+ if (p.state.output.includes("[ccr:")) continue;
15653
+ const toolName = p.tool || "unknown";
15654
+ const output = p.state.output;
15655
+ const result = compressToolOutput(toolName, output);
15656
+ if (result.length < output.length * 0.7) {
15657
+ const hash2 = ccrStore(state, output, result, toolName, p.callID);
15658
+ p.state.output = ccrInjectMarker(result, hash2);
15659
+ compressed++;
15660
+ }
15661
+ }
15662
+ }
15663
+ return compressed;
15664
+ }
15665
+ function crushJsonToolOutputs(messages, state) {
15666
+ let crushed = 0;
15667
+ const protectedTail = messages.length - 8;
15668
+ for (let i = 3; i < protectedTail; i++) {
15669
+ const msg = messages[i];
15670
+ for (const part of msg.parts) {
15671
+ if (typeof part !== "object" || part === null) continue;
15672
+ const p = part;
15673
+ if (p.type !== "tool") continue;
15674
+ if (p.state?.status !== "completed") continue;
15675
+ if (!p.state.output) continue;
15676
+ if (p.state.output.startsWith("[compressed")) continue;
15677
+ if (p.state.output.startsWith("[superseded")) continue;
15678
+ if (p.state.output.includes("[ccr:")) continue;
15679
+ if (detectContentType(p.state.output) !== "json") continue;
15680
+ const original = p.state.output;
15681
+ const crushed_output = crushJsonArray(original);
15682
+ if (crushed_output.length < original.length * 0.7) {
15683
+ const hash2 = ccrStore(state, original, crushed_output, p.tool, p.callID);
15684
+ p.state.output = ccrInjectMarker(crushed_output, hash2);
15685
+ crushed++;
15686
+ }
15687
+ }
15688
+ }
15689
+ return crushed;
15690
+ }
15691
+
15134
15692
  // src/hooks/messages-transform.ts
15135
15693
  var KEEP_RECENT = 8;
15136
15694
  var PROTECTED_HEAD = 3;
@@ -15288,6 +15846,23 @@ function createMessagesTransformHandler(state, logger) {
15288
15846
  repairOrphanedToolCalls(messages);
15289
15847
  if (Object.values(stats).some((v) => v > 0)) {
15290
15848
  logger?.debug("messages.transform: stripped", stats);
15849
+ }
15850
+ const pipelineResult = runCompressionPipeline({
15851
+ messages: output.messages,
15852
+ state,
15853
+ logger
15854
+ });
15855
+ const ds = pipelineResult.stats;
15856
+ if (ds.toolDedup > 0 || ds.errorPurge > 0 || ds.toolOutputCompressed > 0 || ds.jsonCrushed > 0 || ds.messagePruned > 0 || ds.nudgeInjected) {
15857
+ logger?.debug("messages.transform: deep compression", { ...ds });
15858
+ state.mergeNotify({
15859
+ compression: stats,
15860
+ deepCompression: ds,
15861
+ messageCount: messages.length,
15862
+ protectedHead: PROTECTED_HEAD,
15863
+ protectedTail: KEEP_RECENT
15864
+ });
15865
+ } else if (Object.values(stats).some((v) => v > 0)) {
15291
15866
  state.mergeNotify({
15292
15867
  compression: stats,
15293
15868
  messageCount: messages.length,
@@ -15903,7 +16478,7 @@ var deepMemoryPlugin = async (input) => {
15903
16478
  });
15904
16479
  }
15905
16480
  },
15906
- tool: memoryTools,
16481
+ tool: { ...memoryTools, deep_expand: createDeepExpandTool(state) },
15907
16482
  "tool.execute.after": async (input2, output) => {
15908
16483
  if (input2.tool !== "read") return;
15909
16484
  const filePath = input2.args?.path ?? input2.args?.filePath;