@bd7pil/opencode-deep-memory 0.8.1 → 0.8.3

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/README.md CHANGED
@@ -61,7 +61,7 @@ OpenCode auto-installs on startup. Memory appears at `.deep-memory/` in your pro
61
61
 
62
62
  ## Context compression
63
63
 
64
- Two layers, fully automatic, no LLM calls.
64
+ Three layers, fully automatic, no LLM calls.
65
65
 
66
66
  ### Layer 1: Deterministic stripping (always active)
67
67
 
@@ -92,11 +92,16 @@ Thresholds are absolute, not percentage-based — they work consistently across
92
92
  | Command outputs | Keep errors + tail | [Edgee][] |
93
93
  | Search results | Keep top-20, group by file | [Edgee][] |
94
94
  | JSON arrays | Head + dedup middle + tail | [Headroom][] |
95
+ | Subagent output | Headers + key lines + tail with [ccr:] preservation | [Claude Code][] |
96
+ | Skill output | Frontmatter + MUST rules + structure headers | [Claude Code][] |
97
+ | Nested JSON objects | Compress child arrays >30 items | This project |
95
98
  | Old assistant text | Preserve structure, compress prose | [LLMLingua][] |
96
99
 
97
- All compressed content is **reversible** via CCR (Compress-Cache-Retrieve) — originals cached with SHA-256 hash, retrievable via `deep_expand` tool.
100
+ All compressed content is **reversible** via CCR (Compress-Cache-Retrieve) — originals cached for 30 minutes with SHA-256 hash, retrievable via `deep_expand` tool.
98
101
 
99
- **Never touched**: user messages, recent 4K tokens, protected tools (question, edit, write, todowrite, memory_*).
102
+ **No compression** on protected tools: `question`, `edit`, `write`, `todowrite`, `memory_*`, `deep_expand`, `task`, `skill`. These tools' outputs contain verification data (LSP diagnostics, subagent decisions) essential for the agent to function correctly.
103
+
104
+ **Post-compression re-read**: after compression modifies content, recent modified files are listed in a `<dm-nudge>` so the agent can re-verify if needed — inspired by Claude Code's `onCompact` callback.
100
105
 
101
106
  ## Memory nudge
102
107
 
package/dist/index.js CHANGED
@@ -264,6 +264,7 @@ var PluginState = class {
264
264
  _lastMemoryNudgeMessageCount = /* @__PURE__ */ new Map();
265
265
  _lastCCRCleanup = 0;
266
266
  _modelContextWindow = 0;
267
+ _recentEdits = /* @__PURE__ */ new Set();
267
268
  agentOf(sessionID) {
268
269
  return this._agents.get(sessionID);
269
270
  }
@@ -432,12 +433,15 @@ var PluginState = class {
432
433
  const last = this._lastMemoryNudgeMessageCount.get(sessionID);
433
434
  return last != null ? currentMessageCount - last : Number.POSITIVE_INFINITY;
434
435
  }
435
- setModelContextWindow(tokens) {
436
- if (tokens > 0) this._modelContextWindow = tokens;
437
- }
438
436
  getModelContextWindow() {
439
437
  return this._modelContextWindow;
440
438
  }
439
+ trackEdit(filePath) {
440
+ if (filePath) this._recentEdits.add(filePath);
441
+ }
442
+ getRecentEdits() {
443
+ return Array.from(this._recentEdits);
444
+ }
441
445
  };
442
446
  function createPluginState() {
443
447
  return new PluginState();
@@ -14874,7 +14878,7 @@ ${part.thinking || part.text || "[empty]"}
14874
14878
 
14875
14879
  // src/compress/ccr.ts
14876
14880
  import { createHash as createHash2 } from "crypto";
14877
- var CCR_TTL_MS = 5 * 60 * 1e3;
14881
+ var CCR_TTL_MS = 30 * 60 * 1e3;
14878
14882
  function ccrStore(state, original, compressed, toolName, callID) {
14879
14883
  const hash2 = sha256(original).slice(0, 24);
14880
14884
  state.ccStore(hash2, {
@@ -15369,14 +15373,13 @@ function extractInputTokensFromMessages(messages) {
15369
15373
  for (const part of msg.parts) {
15370
15374
  if (typeof part !== "object" || part === null) continue;
15371
15375
  const p = part;
15372
- if (p["type"] === "step-finish") {
15373
- const tokens = p;
15374
- const input = tokens.tokens?.input ?? 0;
15375
- const cached2 = tokens.tokens?.cached ?? 0;
15376
- const total = input + cached2;
15377
- if (total > best) best = total;
15378
- if (best > 0) return best;
15379
- }
15376
+ if (p["type"] !== "step-finish") continue;
15377
+ const tokens = p;
15378
+ const input = tokens.tokens?.input ?? 0;
15379
+ const cached2 = tokens.tokens?.cache?.read ?? 0;
15380
+ const total = input + cached2;
15381
+ if (total > best) best = total;
15382
+ if (best > 0) return best;
15380
15383
  }
15381
15384
  }
15382
15385
  return best;
@@ -15469,6 +15472,28 @@ function createToolSignature(tool5, args) {
15469
15472
  return `${tool5}::${sorted}`;
15470
15473
  }
15471
15474
 
15475
+ // src/compress/detector.ts
15476
+ function detectContentType(content) {
15477
+ const trimmed = content.trimStart();
15478
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
15479
+ try {
15480
+ JSON.parse(content);
15481
+ return "json";
15482
+ } catch {
15483
+ }
15484
+ }
15485
+ if (/^diff --git |^@@ -\d+,\d+ \+\d+,\d+ @@|^[+-]{3} \//m.test(content)) return "diff";
15486
+ if (/Traceback \(most recent call last\)|at \S+\.\S+\(|Error: |Exception: |TypeError: |ReferenceError: /m.test(content)) return "error-trace";
15487
+ if (/<[a-z][\s\S]*>/i.test(content) && /<(html|div|span|body|head|script|style)[\s>]/i.test(content)) return "html";
15488
+ const lines = content.split("\n");
15489
+ const logLineCount = lines.filter((l) => /^\s*(\d{4}-\d{2}-\d{2}|\[\d{4}|ERROR\b|WARN\b|INFO\b|DEBUG\b|FATAL\b|TRACE\b)/.test(l)).length;
15490
+ if (lines.length > 5 && logLineCount / lines.length > 0.3) return "log";
15491
+ const codePatterns = /\b(function |class |def |import |from .+ import|const |let |var |export |interface |type |struct |fn |func |pub |private |protected )\b/;
15492
+ const codeLines = lines.filter((l) => codePatterns.test(l)).length;
15493
+ if (lines.length > 10 && codeLines / lines.length > 0.15) return "code";
15494
+ return "text";
15495
+ }
15496
+
15472
15497
  // src/compress/tool-compress.ts
15473
15498
  var TOOL_COMPRESS_STRATEGIES = {
15474
15499
  read: compressFileRead,
@@ -15482,7 +15507,12 @@ var TOOL_COMPRESS_STRATEGIES = {
15482
15507
  grep_app_searchGitHub: compressSearchResults,
15483
15508
  searxng_searxng_web_search: compressSearchResults,
15484
15509
  websearch_web_search_exa: compressSearchResults,
15485
- tavily_tavily_search: compressSearchResults
15510
+ tavily_tavily_search: compressSearchResults,
15511
+ background_output: compressAgentOutput,
15512
+ task: compressAgentOutput,
15513
+ skill: compressSkillOutput,
15514
+ session_read: compressAgentOutput,
15515
+ webfetch: compressAgentOutput
15486
15516
  };
15487
15517
  var DEFAULT_HEAD_LINES = 50;
15488
15518
  var DEFAULT_TAIL_LINES = 20;
@@ -15571,6 +15601,136 @@ function truncateLine(line, maxLen) {
15571
15601
  if (line.length <= maxLen) return line;
15572
15602
  return line.slice(0, maxLen - 15) + "...[truncated]";
15573
15603
  }
15604
+ function compressJsonOutput(output) {
15605
+ try {
15606
+ const parsed = JSON.parse(output);
15607
+ if (Array.isArray(parsed)) {
15608
+ return compressJsonArray(parsed);
15609
+ }
15610
+ if (typeof parsed === "object" && parsed !== null) {
15611
+ return compressJsonObject(parsed);
15612
+ }
15613
+ return output;
15614
+ } catch {
15615
+ return output;
15616
+ }
15617
+ }
15618
+ function compressJsonArray(arr) {
15619
+ const head = 30;
15620
+ const tail = 15;
15621
+ const maxItems = 50;
15622
+ if (arr.length <= maxItems) return JSON.stringify(arr, null, 2);
15623
+ const kept = [...arr.slice(0, head), { _truncated: true, total: arr.length }, ...arr.slice(-tail)];
15624
+ return JSON.stringify(kept, null, 2);
15625
+ }
15626
+ function compressJsonObject(obj) {
15627
+ const MAX_CHILD_ITEMS = 30;
15628
+ let modified = false;
15629
+ const result = {};
15630
+ for (const [key, value] of Object.entries(obj)) {
15631
+ if (Array.isArray(value) && value.length > MAX_CHILD_ITEMS) {
15632
+ result[key] = {
15633
+ _truncated: true,
15634
+ total: value.length,
15635
+ items: [...value.slice(0, 10), toStringPlaceholder(value.slice(10, 20)), ...value.slice(-10)]
15636
+ };
15637
+ modified = true;
15638
+ } else {
15639
+ result[key] = value;
15640
+ }
15641
+ }
15642
+ if (modified) {
15643
+ return JSON.stringify(result, null, 2);
15644
+ }
15645
+ return JSON.stringify(obj, null, 2);
15646
+ }
15647
+ function toStringPlaceholder(items) {
15648
+ return { _skipped: items.length };
15649
+ }
15650
+ function compressAgentOutput(output) {
15651
+ if (detectContentType(output) === "json") {
15652
+ return compressJsonOutput(output);
15653
+ }
15654
+ const lines = output.split("\n");
15655
+ if (lines.length <= 40 && output.length <= 3e3) return output;
15656
+ const MAX_SECTION_LINES = 5;
15657
+ const result = [];
15658
+ for (let i = 0; i < lines.length; i++) {
15659
+ const line = lines[i];
15660
+ if (line.includes("[ccr:") || line.includes("[superseded")) {
15661
+ result.push(line);
15662
+ continue;
15663
+ }
15664
+ const isHeader = /^#{1,4}\s/.test(line) || /^---/.test(line) || /^\*\*$/.test(line);
15665
+ const hasCode = line.includes("```");
15666
+ const hasKey = /\b(error|fail|success|completed|result|summary|warning)\b/i.test(line);
15667
+ if (isHeader || hasCode || hasKey) {
15668
+ result.push(truncateLine(line, 300));
15669
+ continue;
15670
+ }
15671
+ if (i < 5 || i >= lines.length - 10) {
15672
+ result.push(truncateLine(line, 300));
15673
+ continue;
15674
+ }
15675
+ const inSection = result.length > 0 && result[result.length - 1] !== "";
15676
+ if (!inSection) {
15677
+ if (line.trim()) {
15678
+ result.push(line);
15679
+ }
15680
+ } else {
15681
+ const recentLines = result.slice(-MAX_SECTION_LINES).filter((l) => l.trim() && l !== "...");
15682
+ if (recentLines.length >= MAX_SECTION_LINES) {
15683
+ result.push("...[truncated]");
15684
+ while (i < lines.length && !/^#{1,4}\s/.test(lines[i]) && !lines[i].includes("```") && !/\b(error|fail|summary)\b/i.test(lines[i])) {
15685
+ i++;
15686
+ }
15687
+ i--;
15688
+ } else {
15689
+ result.push(truncateLine(line, 300));
15690
+ }
15691
+ }
15692
+ }
15693
+ return result.join("\n");
15694
+ }
15695
+ function compressSkillOutput(output) {
15696
+ const lines = output.split("\n");
15697
+ if (lines.length <= 60 && output.length <= 4e3) return output;
15698
+ const result = [];
15699
+ const FRONTMATTER_END = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
15700
+ for (let i = 0; i < lines.length; i++) {
15701
+ if (i <= FRONTMATTER_END || i < 10) {
15702
+ result.push(lines[i]);
15703
+ continue;
15704
+ }
15705
+ if (i >= lines.length - 10) {
15706
+ result.push(lines[i]);
15707
+ continue;
15708
+ }
15709
+ const line = lines[i];
15710
+ if (/^#{1,4}\s/.test(line) || /^```/.test(line) || /^---/.test(line)) {
15711
+ result.push(line);
15712
+ continue;
15713
+ }
15714
+ if (/\b(must|must not|required|forbidden|never|always)\b/i.test(line)) {
15715
+ result.push(line);
15716
+ continue;
15717
+ }
15718
+ const recentNonEmpty = result.slice(-8).filter((l) => l.trim());
15719
+ if (recentNonEmpty.length >= 8 && !result[result.length - 1].startsWith("...")) {
15720
+ result.push("...[truncated]");
15721
+ while (i < lines.length && !/^#{1,4}\s/.test(lines[i]) && !/^```/.test(lines[i])) {
15722
+ i++;
15723
+ }
15724
+ i--;
15725
+ } else {
15726
+ result.push(line);
15727
+ }
15728
+ }
15729
+ if (result.length < lines.length * 0.7) {
15730
+ return result.join("\n");
15731
+ }
15732
+ return output;
15733
+ }
15574
15734
 
15575
15735
  // src/compress/json-crush.ts
15576
15736
  import { createHash as createHash3 } from "crypto";
@@ -15616,28 +15776,6 @@ function sha2562(data) {
15616
15776
  return createHash3("sha256").update(data).digest("hex");
15617
15777
  }
15618
15778
 
15619
- // src/compress/detector.ts
15620
- function detectContentType(content) {
15621
- const trimmed = content.trimStart();
15622
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
15623
- try {
15624
- JSON.parse(content);
15625
- return "json";
15626
- } catch {
15627
- }
15628
- }
15629
- if (/^diff --git |^@@ -\d+,\d+ \+\d+,\d+ @@|^[+-]{3} \//m.test(content)) return "diff";
15630
- if (/Traceback \(most recent call last\)|at \S+\.\S+\(|Error: |Exception: |TypeError: |ReferenceError: /m.test(content)) return "error-trace";
15631
- if (/<[a-z][\s\S]*>/i.test(content) && /<(html|div|span|body|head|script|style)[\s>]/i.test(content)) return "html";
15632
- const lines = content.split("\n");
15633
- const logLineCount = lines.filter((l) => /^\s*(\d{4}-\d{2}-\d{2}|\[\d{4}|ERROR\b|WARN\b|INFO\b|DEBUG\b|FATAL\b|TRACE\b)/.test(l)).length;
15634
- if (lines.length > 5 && logLineCount / lines.length > 0.3) return "log";
15635
- const codePatterns = /\b(function |class |def |import |from .+ import|const |let |var |export |interface |type |struct |fn |func |pub |private |protected )\b/;
15636
- const codeLines = lines.filter((l) => codePatterns.test(l)).length;
15637
- if (lines.length > 10 && codeLines / lines.length > 0.15) return "code";
15638
- return "text";
15639
- }
15640
-
15641
15779
  // src/compress/single-pass.ts
15642
15780
  var PROTECTED_TOOLS = /* @__PURE__ */ new Set([
15643
15781
  "question",
@@ -15648,7 +15786,9 @@ var PROTECTED_TOOLS = /* @__PURE__ */ new Set([
15648
15786
  "memory_search",
15649
15787
  "memory_forget",
15650
15788
  "memory_expand",
15651
- "deep_expand"
15789
+ "deep_expand",
15790
+ "task",
15791
+ "skill"
15652
15792
  ]);
15653
15793
  var NEVER_DEDUP = /* @__PURE__ */ new Set(["read", "bash", "grep", "glob", "find", "search"]);
15654
15794
  var ERROR_PURGE_TURN_THRESHOLD = 4;
@@ -15711,7 +15851,7 @@ function singlePassCompress(messages, state, protectedTail) {
15711
15851
  const toolName = p["tool"];
15712
15852
  const callID = p["callID"];
15713
15853
  const toolState = p["state"];
15714
- if (toolState?.["status"] === "error") {
15854
+ if (toolState?.["status"] === "error" && !PROTECTED_TOOLS.has(toolName ?? "")) {
15715
15855
  const age = totalMessages - i;
15716
15856
  if (age >= ERROR_PURGE_TURN_THRESHOLD) {
15717
15857
  if (typeof toolState["input"] === "object" && toolState["input"] !== null) {
@@ -15755,7 +15895,7 @@ function singlePassCompress(messages, state, protectedTail) {
15755
15895
  seen.set(signature, { msgIdx: i, outputHash });
15756
15896
  }
15757
15897
  }
15758
- if (output.length >= 200) {
15898
+ if (output.length >= 200 && !PROTECTED_TOOLS.has(toolName)) {
15759
15899
  const result = compressToolOutput(toolName, output);
15760
15900
  if (result.length < output.length * 0.85) {
15761
15901
  const hash2 = ccrStore(state, output, result, toolName, callID);
@@ -15764,7 +15904,7 @@ function singlePassCompress(messages, state, protectedTail) {
15764
15904
  continue;
15765
15905
  }
15766
15906
  }
15767
- if (output.length >= 200 && detectContentType(output) === "json") {
15907
+ if (output.length >= 200 && detectContentType(output) === "json" && !PROTECTED_TOOLS.has(toolName)) {
15768
15908
  const crushed = crushJsonArray(output);
15769
15909
  if (crushed.length < output.length * 0.85) {
15770
15910
  const hash2 = ccrStore(state, output, crushed, toolName, callID);
@@ -16051,6 +16191,23 @@ function createMessagesTransformHandler(state, logger) {
16051
16191
  protectedHead: PROTECTED_HEAD,
16052
16192
  protectedTail: KEEP_RECENT
16053
16193
  });
16194
+ const recentEdits = state.getRecentEdits();
16195
+ if (recentEdits.length > 0) {
16196
+ const fileList = recentEdits.slice(0, 5).join(", ");
16197
+ const nudge = '\n\n<dm-nudge level="medium">Context was compressed. Recent files may have shifted: ' + fileList + ". Use `read` to re-verify if needed.</dm-nudge>";
16198
+ for (let k = output.messages.length - 1; k >= 0; k--) {
16199
+ const msg = output.messages[k];
16200
+ if (msg.info.role !== "assistant") continue;
16201
+ for (const part of msg.parts) {
16202
+ const p = part;
16203
+ if (p["type"] === "text" && typeof p["text"] === "string") {
16204
+ p.text += nudge;
16205
+ break;
16206
+ }
16207
+ }
16208
+ break;
16209
+ }
16210
+ }
16054
16211
  } else if (Object.values(stats).some((v) => v > 0)) {
16055
16212
  state.mergeNotify({
16056
16213
  compression: stats,
@@ -16182,29 +16339,6 @@ function createNotifyHandler(client, logger) {
16182
16339
  };
16183
16340
  }
16184
16341
 
16185
- // src/shared/model-limits.ts
16186
- var KNOWN_MODEL_LIMITS = {
16187
- "deepseek-v4-pro": 1e6,
16188
- "deepseek-v4": 1e6,
16189
- "deepseek-v3": 64e3,
16190
- "deepseek-r1": 64e3,
16191
- "claude-opus-4": 2e5,
16192
- "claude-sonnet-4": 2e5,
16193
- "gpt-4o": 128e3,
16194
- "o1": 2e5,
16195
- "o3-mini": 2e5,
16196
- "gemini-2.5-pro": 1e6,
16197
- "gemini-2.5-flash": 1e6,
16198
- "qwen-max": 131072
16199
- };
16200
- function lookupModelLimit(modelID) {
16201
- if (KNOWN_MODEL_LIMITS[modelID]) return KNOWN_MODEL_LIMITS[modelID];
16202
- for (const [key, limit] of Object.entries(KNOWN_MODEL_LIMITS)) {
16203
- if (modelID.includes(key)) return limit;
16204
- }
16205
- return void 0;
16206
- }
16207
-
16208
16342
  // src/extract/enrich.ts
16209
16343
  import { stat } from "fs/promises";
16210
16344
 
@@ -16536,14 +16670,10 @@ var deepMemoryPlugin = async (input) => {
16536
16670
  const defaultModel = configResult.data?.model;
16537
16671
  if (typeof defaultModel === "string" && defaultModel.includes("/")) {
16538
16672
  const slashIdx = defaultModel.indexOf("/");
16539
- const providerID = defaultModel.slice(0, slashIdx);
16540
- const modelID = defaultModel.slice(slashIdx + 1);
16541
- state.recordFallbackModel({ providerID, modelID });
16542
- const limit = lookupModelLimit(modelID);
16543
- if (limit) {
16544
- state.setModelContextWindow(limit);
16545
- logger.debug("resolved model context window", { modelID, limit });
16546
- }
16673
+ state.recordFallbackModel({
16674
+ providerID: defaultModel.slice(0, slashIdx),
16675
+ modelID: defaultModel.slice(slashIdx + 1)
16676
+ });
16547
16677
  }
16548
16678
  }).catch((err) => {
16549
16679
  logger.debug("config.get failed, dream/distill will omit model", {
@@ -16704,12 +16834,16 @@ var deepMemoryPlugin = async (input) => {
16704
16834
  },
16705
16835
  tool: { ...memoryTools, deep_expand: createDeepExpandTool(state) },
16706
16836
  "tool.execute.after": async (input2, output) => {
16707
- if (input2.tool !== "read") return;
16708
16837
  const filePath = input2.args?.path ?? input2.args?.filePath;
16709
16838
  if (!filePath) return;
16710
- const lang = getLanguage(filePath);
16711
- if (!lang) return;
16712
- tracker.recordRead(filePath, output.output || "");
16839
+ if (input2.tool === "read") {
16840
+ const lang = getLanguage(filePath);
16841
+ if (!lang) return;
16842
+ tracker.recordRead(filePath, output.output || "");
16843
+ }
16844
+ if (input2.tool === "edit" || input2.tool === "write") {
16845
+ state.trackEdit(filePath);
16846
+ }
16713
16847
  },
16714
16848
  "experimental.session.compacting": createCompactingHandler({
16715
16849
  client: input.client,