@bd7pil/opencode-deep-memory 0.3.6 → 0.4.0

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