@bd7pil/opencode-deep-memory 0.3.5 → 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
@@ -256,6 +256,10 @@ var PluginState = class {
256
256
  _pendingResumes = /* @__PURE__ */ new Map();
257
257
  _pendingEnrichments = /* @__PURE__ */ new Set();
258
258
  _lastUserText = /* @__PURE__ */ new Map();
259
+ _pendingNotify = null;
260
+ _toolSignatures = /* @__PURE__ */ new Map();
261
+ _ccrCache = /* @__PURE__ */ new Map();
262
+ _lastInputTokens = 0;
259
263
  agentOf(sessionID) {
260
264
  return this._agents.get(sessionID);
261
265
  }
@@ -341,6 +345,65 @@ var PluginState = class {
341
345
  this._lastUserText.delete(sessionID);
342
346
  return text;
343
347
  }
348
+ /**
349
+ * Merge stats into the pending notification.
350
+ * Called from messages.transform (compression) and system.transform (injection).
351
+ * Creates a new PendingNotify on first call per turn.
352
+ */
353
+ mergeNotify(patch) {
354
+ if (!this._pendingNotify) {
355
+ this._pendingNotify = { ...patch, setAt: Date.now() };
356
+ return;
357
+ }
358
+ if (patch.compression) {
359
+ this._pendingNotify.compression = patch.compression;
360
+ }
361
+ if (patch.injection) {
362
+ this._pendingNotify.injection = patch.injection;
363
+ }
364
+ if (patch.messageCount !== void 0) {
365
+ this._pendingNotify.messageCount = patch.messageCount;
366
+ }
367
+ if (patch.protectedHead !== void 0) {
368
+ this._pendingNotify.protectedHead = patch.protectedHead;
369
+ }
370
+ if (patch.protectedTail !== void 0) {
371
+ this._pendingNotify.protectedTail = patch.protectedTail;
372
+ }
373
+ }
374
+ consumePendingNotify() {
375
+ const n = this._pendingNotify;
376
+ this._pendingNotify = null;
377
+ return n;
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
+ }
344
407
  };
345
408
  function createPluginState() {
346
409
  return new PluginState();
@@ -777,7 +840,8 @@ async function composeSystemPayload(opts) {
777
840
  stable: `<deep-memory-stable>
778
841
  <tool-hint>${TOOL_HINT}</tool-hint>
779
842
  </deep-memory-stable>`,
780
- volatile: ""
843
+ volatile: "",
844
+ stats: { searchEntries: 0, repoMapEntries: 0, hasCheckpoint: false }
781
845
  };
782
846
  }
783
847
  const staticBudget = Math.floor(budget.memorySummary * 0.4);
@@ -794,6 +858,7 @@ ${staticMemory || "(empty)"}
794
858
  </constraints>
795
859
  </deep-memory-stable>`;
796
860
  let volatileContent = "";
861
+ let searchEntries = 0;
797
862
  if (userQuery && searchService && searchBudget > 0) {
798
863
  try {
799
864
  const results = await searchService.search(userQuery, { scope: "all", limit: 20, applyDecay: true });
@@ -808,12 +873,14 @@ ${staticMemory || "(empty)"}
808
873
  })),
809
874
  { budget: searchBudget }
810
875
  );
876
+ searchEntries = allocated.length;
811
877
  volatileContent = allocated.map((a) => a.rendered).join("\n");
812
878
  }
813
879
  } catch {
814
880
  }
815
881
  }
816
882
  let checkpointContent = "";
883
+ let hasCheckpoint = false;
817
884
  if (budget.checkpointSummary > 0) {
818
885
  const checkpointPath = memoryFilePath("project", "checkpoint", projectPath);
819
886
  checkpointContent = budgetedRead(checkpointPath, budget.checkpointSummary, [
@@ -823,6 +890,7 @@ ${staticMemory || "(empty)"}
823
890
  "Gotchas",
824
891
  "File Changes"
825
892
  ]);
893
+ hasCheckpoint = !!checkpointContent;
826
894
  }
827
895
  let volatile = `<deep-memory-volatile>
828
896
  <relevant>
@@ -834,9 +902,11 @@ ${volatileContent || "(none)"}
834
902
  ${checkpointContent}
835
903
  </last-checkpoint>`;
836
904
  }
905
+ let repoMapSymbols = 0;
837
906
  if (tracker && budget.repomap > 0) {
838
907
  const repomapEntries = tracker.getTopSymbols(budget.repomap);
839
908
  if (repomapEntries.length > 0) {
909
+ repoMapSymbols = repomapEntries.length;
840
910
  volatile += "\n" + formatRepoMap(repomapEntries);
841
911
  }
842
912
  }
@@ -849,7 +919,7 @@ ${checkpointContent}
849
919
  stableSize: stable.length,
850
920
  volatileSize: volatile.length
851
921
  });
852
- return { stable, volatile };
922
+ return { stable, volatile, stats: { searchEntries, repoMapEntries: repoMapSymbols, hasCheckpoint } };
853
923
  }
854
924
 
855
925
  // src/hooks/system-transform.ts
@@ -870,7 +940,7 @@ function createSystemTransformHandler(state, projectPath, searchService, logger,
870
940
  }
871
941
  }
872
942
  const userQuery = state.consumeLastUserText(sessionID);
873
- const { stable, volatile } = await composeSystemPayload({
943
+ const { stable, volatile, stats: payloadStats } = await composeSystemPayload({
874
944
  state,
875
945
  sessionID,
876
946
  projectPath,
@@ -896,6 +966,17 @@ function createSystemTransformHandler(state, projectPath, searchService, logger,
896
966
  stableSize: stable.length,
897
967
  volatileSize: volatile.length
898
968
  });
969
+ state.mergeNotify({
970
+ injection: {
971
+ stableSize: stable.length,
972
+ volatileSize: volatile.length,
973
+ tier,
974
+ mode,
975
+ searchEntries: payloadStats.searchEntries,
976
+ repoMapEntries: payloadStats.repoMapEntries,
977
+ hasCheckpoint: payloadStats.hasCheckpoint
978
+ }
979
+ });
899
980
  };
900
981
  }
901
982
 
@@ -2153,6 +2234,9 @@ var SearchService = class {
2153
2234
  }
2154
2235
  };
2155
2236
 
2237
+ // src/tools/index.ts
2238
+ import { tool as tool4 } from "@opencode-ai/plugin";
2239
+
2156
2240
  // src/tools/memory-search.ts
2157
2241
  import { tool } from "@opencode-ai/plugin";
2158
2242
  function createMemorySearchTool(service) {
@@ -14739,6 +14823,35 @@ ${part.thinking || part.text || "[empty]"}
14739
14823
  return output;
14740
14824
  }
14741
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
+
14742
14855
  // src/tools/index.ts
14743
14856
  function createMemoryTools(service, opts) {
14744
14857
  const search = createMemorySearchTool(service);
@@ -14752,6 +14865,19 @@ function createMemoryTools(service, opts) {
14752
14865
  memory_expand: expand
14753
14866
  };
14754
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
+ }
14755
14881
 
14756
14882
  // src/extract/capture.ts
14757
14883
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
@@ -14910,17 +15036,17 @@ function extractFileChanges(messages) {
14910
15036
  for (const msg of messages) {
14911
15037
  for (const part of msg.parts) {
14912
15038
  if (part.type !== "tool" || !part.tool) continue;
14913
- const tool4 = part.tool.toLowerCase();
14914
- if (tool4 === "write" || tool4 === "edit") {
15039
+ const tool5 = part.tool.toLowerCase();
15040
+ if (tool5 === "write" || tool5 === "edit") {
14915
15041
  const filePath = part.args?.filePath || part.args?.path || "";
14916
15042
  if (filePath) {
14917
- const key = `${filePath}:${tool4}`;
15043
+ const key = `${filePath}:${tool5}`;
14918
15044
  if (!seen.has(key)) {
14919
15045
  seen.add(key);
14920
- changes.push({ path: filePath, operation: tool4 });
15046
+ changes.push({ path: filePath, operation: tool5 });
14921
15047
  }
14922
15048
  }
14923
- } else if (tool4 === "bash" || tool4 === "execute") {
15049
+ } else if (tool5 === "bash" || tool5 === "execute") {
14924
15050
  const cmd = part.args?.command || "";
14925
15051
  const match = BASH_FILE_OP_RE.exec(cmd);
14926
15052
  if (match?.[1]) {
@@ -15081,6 +15207,488 @@ function createCompactingHandler(args) {
15081
15207
  };
15082
15208
  }
15083
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
+
15084
15692
  // src/hooks/messages-transform.ts
15085
15693
  var KEEP_RECENT = 8;
15086
15694
  var PROTECTED_HEAD = 3;
@@ -15166,7 +15774,7 @@ function repairOrphanedToolCalls(messages) {
15166
15774
  }
15167
15775
  }
15168
15776
  }
15169
- function createMessagesTransformHandler(_state, logger) {
15777
+ function createMessagesTransformHandler(state, logger) {
15170
15778
  return async (_input, output) => {
15171
15779
  const messages = output.messages;
15172
15780
  if (messages.length <= KEEP_RECENT) return;
@@ -15239,6 +15847,149 @@ function createMessagesTransformHandler(_state, logger) {
15239
15847
  if (Object.values(stats).some((v) => v > 0)) {
15240
15848
  logger?.debug("messages.transform: stripped", stats);
15241
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)) {
15866
+ state.mergeNotify({
15867
+ compression: stats,
15868
+ messageCount: messages.length,
15869
+ protectedHead: PROTECTED_HEAD,
15870
+ protectedTail: KEEP_RECENT
15871
+ });
15872
+ }
15873
+ };
15874
+ }
15875
+
15876
+ // src/hooks/notify.ts
15877
+ var COOLDOWN_MS = 5e3;
15878
+ function formatK(n) {
15879
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}K`;
15880
+ return String(n);
15881
+ }
15882
+ function totalStripped(c) {
15883
+ return c.reasoning_cleared + c.metadata_stripped + c.system_neutralized + c.tool_errors_truncated + c.thinking_stripped;
15884
+ }
15885
+ function renderProgressBar(total, processed, width = 40) {
15886
+ if (total === 0) return `\u2502${"\u2591".repeat(width)}\u2502`;
15887
+ const filled = Math.round(processed / total * width);
15888
+ const bar = "\u2588".repeat(Math.min(filled, width)) + "\u2591".repeat(Math.max(0, width - filled));
15889
+ return `\u2502${bar}\u2502`;
15890
+ }
15891
+ function formatCompressionBlock(c, msgCount, head, tail) {
15892
+ const lines = [];
15893
+ if (msgCount && msgCount > 0) {
15894
+ const protectedZones = (head ?? 0) + (tail ?? 0);
15895
+ const scannable = Math.max(0, msgCount - protectedZones);
15896
+ const affected = Math.min(scannable, totalStripped(c));
15897
+ lines.push(renderProgressBar(scannable, affected));
15898
+ }
15899
+ const parts = [];
15900
+ if (c.reasoning_cleared > 0) parts.push(`reasoning -${formatK(c.reasoning_cleared)}`);
15901
+ if (c.metadata_stripped > 0) parts.push(`metadata -${formatK(c.metadata_stripped)}`);
15902
+ if (c.tool_errors_truncated > 0) parts.push(`tool_err -${formatK(c.tool_errors_truncated)}`);
15903
+ if (c.thinking_stripped > 0) parts.push(`thinking -${formatK(c.thinking_stripped)}`);
15904
+ if (c.system_neutralized > 0) parts.push(`sys_inject -${formatK(c.system_neutralized)}`);
15905
+ if (parts.length > 0) lines.push(` ${parts.join(" | ")}`);
15906
+ return lines.join("\n");
15907
+ }
15908
+ function formatInjectionBlock(i) {
15909
+ const cacheStatus = i.stableSize > 0 ? "\u2713" : "\u2014";
15910
+ const lines = [
15911
+ ` m[0] stable ${formatK(i.stableSize)}B ${cacheStatus} m[1] volatile ${formatK(i.volatileSize)}B`,
15912
+ ` tier=${i.tier} | mode=${i.mode}`
15913
+ ];
15914
+ const details = [];
15915
+ if (i.repoMapEntries > 0) details.push(`repo-map: ${i.repoMapEntries} symbols`);
15916
+ if (i.searchEntries > 0) details.push(`memory: ${i.searchEntries} entries`);
15917
+ if (i.hasCheckpoint) details.push(`checkpoint \u2713`);
15918
+ if (details.length > 0) lines.push(` ${details.join(" | ")}`);
15919
+ return lines.join("\n");
15920
+ }
15921
+ function chooseLevel(n) {
15922
+ if (!n.compression && n.injection) return "minimal";
15923
+ if (!n.compression) return "minimal";
15924
+ const hasRichContext = n.injection && (n.injection.repoMapEntries > 0 || n.injection.searchEntries > 0 || n.injection.hasCheckpoint);
15925
+ if (hasRichContext && n.messageCount && n.messageCount > 20) return "extended";
15926
+ return "detailed";
15927
+ }
15928
+ function formatNotify(n) {
15929
+ const level = chooseLevel(n);
15930
+ if (level === "minimal") {
15931
+ const parts = ["\u25A3 deep-memory"];
15932
+ if (n.compression) parts.push(`-${formatK(totalStripped(n.compression))} stripped`);
15933
+ if (n.injection) parts.push(`+${formatK(n.injection.stableSize + n.injection.volatileSize)}B injected`);
15934
+ return parts.join(" | ");
15935
+ }
15936
+ if (level === "extended") {
15937
+ const sections2 = [];
15938
+ if (n.compression) {
15939
+ sections2.push("\u2500 Compression \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
15940
+ if (n.messageCount) {
15941
+ const head = n.protectedHead ?? 0;
15942
+ const tail = n.protectedTail ?? 0;
15943
+ sections2.push(` messages: ${n.messageCount} (protected: head=${head} tail=${tail})`);
15944
+ }
15945
+ sections2.push(formatCompressionBlock(n.compression, n.messageCount, n.protectedHead, n.protectedTail));
15946
+ }
15947
+ if (n.injection) {
15948
+ sections2.push("\u2500 Injection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
15949
+ sections2.push(formatInjectionBlock(n.injection));
15950
+ const budgetUsed = n.injection.stableSize + n.injection.volatileSize;
15951
+ const maxBudget = 4e3;
15952
+ const pct = Math.round(budgetUsed / maxBudget * 100);
15953
+ sections2.push(` budget: ${formatK(budgetUsed)}B / ${formatK(maxBudget)}B (${pct}%)`);
15954
+ }
15955
+ return ["\u25A3 deep-memory", ...sections2].join("\n");
15956
+ }
15957
+ const sections = [];
15958
+ if (n.compression) {
15959
+ sections.push("\u2500 Compression \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
15960
+ sections.push(formatCompressionBlock(n.compression, n.messageCount, n.protectedHead, n.protectedTail));
15961
+ }
15962
+ if (n.injection) {
15963
+ sections.push("\u2500 Injection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
15964
+ sections.push(formatInjectionBlock(n.injection));
15965
+ }
15966
+ return ["\u25A3 deep-memory", ...sections].join("\n");
15967
+ }
15968
+ function createNotifyHandler(client, logger) {
15969
+ let lastNotifyAt = 0;
15970
+ return async (sessionID, notify) => {
15971
+ const hasCompression = notify.compression && totalStripped(notify.compression) > 0;
15972
+ const hasInjection = !!notify.injection;
15973
+ if (!hasCompression && !hasInjection) return;
15974
+ if (notify.setAt - lastNotifyAt < COOLDOWN_MS) return;
15975
+ lastNotifyAt = Date.now();
15976
+ const message = formatNotify(notify);
15977
+ const title = hasCompression ? "deep-memory | compressed" : "deep-memory | injected";
15978
+ try {
15979
+ await client.tui.showToast({
15980
+ body: {
15981
+ title,
15982
+ message,
15983
+ variant: "info",
15984
+ duration: 5e3
15985
+ }
15986
+ });
15987
+ logger?.debug("notify: sent", { level: chooseLevel(notify) });
15988
+ } catch (err) {
15989
+ logger?.debug("notify: failed (non-fatal)", {
15990
+ error: err instanceof Error ? err.message : String(err)
15991
+ });
15992
+ }
15242
15993
  };
15243
15994
  }
15244
15995
 
@@ -15595,8 +16346,12 @@ var deepMemoryPlugin = async (input) => {
15595
16346
  });
15596
16347
  });
15597
16348
  const memoryTools = createMemoryTools(searchService, { projectPath });
16349
+ const notify = createNotifyHandler(input.client, logger.for("notify"));
15598
16350
  const hooks = {
15599
- "chat.params": createChatParamsHandler(state, logger.for("chat-params")),
16351
+ "chat.params": createChatParamsHandler(
16352
+ state,
16353
+ logger.for("chat-params")
16354
+ ),
15600
16355
  "chat.message": createChatMessageHandler({
15601
16356
  projectPath,
15602
16357
  state,
@@ -15666,6 +16421,18 @@ var deepMemoryPlugin = async (input) => {
15666
16421
  } else {
15667
16422
  logger.debug("event session.idle (no pending enrichment)");
15668
16423
  }
16424
+ if (idleSessionID) {
16425
+ const pending = state.consumePendingNotify();
16426
+ if (pending) {
16427
+ try {
16428
+ await notify(idleSessionID, pending);
16429
+ } catch (err) {
16430
+ logger.debug("idle notify failed (non-fatal)", {
16431
+ error: err instanceof Error ? err.message : String(err)
16432
+ });
16433
+ }
16434
+ }
16435
+ }
15669
16436
  return;
15670
16437
  }
15671
16438
  if (event.type === "session.compacted") {
@@ -15711,7 +16478,7 @@ var deepMemoryPlugin = async (input) => {
15711
16478
  });
15712
16479
  }
15713
16480
  },
15714
- tool: memoryTools,
16481
+ tool: { ...memoryTools, deep_expand: createDeepExpandTool(state) },
15715
16482
  "tool.execute.after": async (input2, output) => {
15716
16483
  if (input2.tool !== "read") return;
15717
16484
  const filePath = input2.args?.path ?? input2.args?.filePath;