@bd7pil/opencode-deep-memory 0.8.2 → 0.8.4

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
  }
@@ -435,6 +436,12 @@ var PluginState = class {
435
436
  getModelContextWindow() {
436
437
  return this._modelContextWindow;
437
438
  }
439
+ trackEdit(filePath) {
440
+ if (filePath) this._recentEdits.add(filePath);
441
+ }
442
+ getRecentEdits() {
443
+ return Array.from(this._recentEdits);
444
+ }
438
445
  };
439
446
  function createPluginState() {
440
447
  return new PluginState();
@@ -14871,7 +14878,7 @@ ${part.thinking || part.text || "[empty]"}
14871
14878
 
14872
14879
  // src/compress/ccr.ts
14873
14880
  import { createHash as createHash2 } from "crypto";
14874
- var CCR_TTL_MS = 5 * 60 * 1e3;
14881
+ var CCR_TTL_MS = 30 * 60 * 1e3;
14875
14882
  function ccrStore(state, original, compressed, toolName, callID) {
14876
14883
  const hash2 = sha256(original).slice(0, 24);
14877
14884
  state.ccStore(hash2, {
@@ -15465,6 +15472,28 @@ function createToolSignature(tool5, args) {
15465
15472
  return `${tool5}::${sorted}`;
15466
15473
  }
15467
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
+
15468
15497
  // src/compress/tool-compress.ts
15469
15498
  var TOOL_COMPRESS_STRATEGIES = {
15470
15499
  read: compressFileRead,
@@ -15478,7 +15507,12 @@ var TOOL_COMPRESS_STRATEGIES = {
15478
15507
  grep_app_searchGitHub: compressSearchResults,
15479
15508
  searxng_searxng_web_search: compressSearchResults,
15480
15509
  websearch_web_search_exa: compressSearchResults,
15481
- 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
15482
15516
  };
15483
15517
  var DEFAULT_HEAD_LINES = 50;
15484
15518
  var DEFAULT_TAIL_LINES = 20;
@@ -15567,6 +15601,136 @@ function truncateLine(line, maxLen) {
15567
15601
  if (line.length <= maxLen) return line;
15568
15602
  return line.slice(0, maxLen - 15) + "...[truncated]";
15569
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
+ }
15570
15734
 
15571
15735
  // src/compress/json-crush.ts
15572
15736
  import { createHash as createHash3 } from "crypto";
@@ -15612,28 +15776,6 @@ function sha2562(data) {
15612
15776
  return createHash3("sha256").update(data).digest("hex");
15613
15777
  }
15614
15778
 
15615
- // src/compress/detector.ts
15616
- function detectContentType(content) {
15617
- const trimmed = content.trimStart();
15618
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
15619
- try {
15620
- JSON.parse(content);
15621
- return "json";
15622
- } catch {
15623
- }
15624
- }
15625
- if (/^diff --git |^@@ -\d+,\d+ \+\d+,\d+ @@|^[+-]{3} \//m.test(content)) return "diff";
15626
- if (/Traceback \(most recent call last\)|at \S+\.\S+\(|Error: |Exception: |TypeError: |ReferenceError: /m.test(content)) return "error-trace";
15627
- if (/<[a-z][\s\S]*>/i.test(content) && /<(html|div|span|body|head|script|style)[\s>]/i.test(content)) return "html";
15628
- const lines = content.split("\n");
15629
- 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;
15630
- if (lines.length > 5 && logLineCount / lines.length > 0.3) return "log";
15631
- const codePatterns = /\b(function |class |def |import |from .+ import|const |let |var |export |interface |type |struct |fn |func |pub |private |protected )\b/;
15632
- const codeLines = lines.filter((l) => codePatterns.test(l)).length;
15633
- if (lines.length > 10 && codeLines / lines.length > 0.15) return "code";
15634
- return "text";
15635
- }
15636
-
15637
15779
  // src/compress/single-pass.ts
15638
15780
  var PROTECTED_TOOLS = /* @__PURE__ */ new Set([
15639
15781
  "question",
@@ -15644,7 +15786,8 @@ var PROTECTED_TOOLS = /* @__PURE__ */ new Set([
15644
15786
  "memory_search",
15645
15787
  "memory_forget",
15646
15788
  "memory_expand",
15647
- "deep_expand"
15789
+ "deep_expand",
15790
+ "skill"
15648
15791
  ]);
15649
15792
  var NEVER_DEDUP = /* @__PURE__ */ new Set(["read", "bash", "grep", "glob", "find", "search"]);
15650
15793
  var ERROR_PURGE_TURN_THRESHOLD = 4;
@@ -15707,7 +15850,7 @@ function singlePassCompress(messages, state, protectedTail) {
15707
15850
  const toolName = p["tool"];
15708
15851
  const callID = p["callID"];
15709
15852
  const toolState = p["state"];
15710
- if (toolState?.["status"] === "error") {
15853
+ if (toolState?.["status"] === "error" && !PROTECTED_TOOLS.has(toolName ?? "")) {
15711
15854
  const age = totalMessages - i;
15712
15855
  if (age >= ERROR_PURGE_TURN_THRESHOLD) {
15713
15856
  if (typeof toolState["input"] === "object" && toolState["input"] !== null) {
@@ -15751,7 +15894,7 @@ function singlePassCompress(messages, state, protectedTail) {
15751
15894
  seen.set(signature, { msgIdx: i, outputHash });
15752
15895
  }
15753
15896
  }
15754
- if (output.length >= 200) {
15897
+ if (output.length >= 200 && !PROTECTED_TOOLS.has(toolName)) {
15755
15898
  const result = compressToolOutput(toolName, output);
15756
15899
  if (result.length < output.length * 0.85) {
15757
15900
  const hash2 = ccrStore(state, output, result, toolName, callID);
@@ -15760,7 +15903,7 @@ function singlePassCompress(messages, state, protectedTail) {
15760
15903
  continue;
15761
15904
  }
15762
15905
  }
15763
- if (output.length >= 200 && detectContentType(output) === "json") {
15906
+ if (output.length >= 200 && detectContentType(output) === "json" && !PROTECTED_TOOLS.has(toolName)) {
15764
15907
  const crushed = crushJsonArray(output);
15765
15908
  if (crushed.length < output.length * 0.85) {
15766
15909
  const hash2 = ccrStore(state, output, crushed, toolName, callID);
@@ -16047,6 +16190,23 @@ function createMessagesTransformHandler(state, logger) {
16047
16190
  protectedHead: PROTECTED_HEAD,
16048
16191
  protectedTail: KEEP_RECENT
16049
16192
  });
16193
+ const recentEdits = state.getRecentEdits();
16194
+ if (recentEdits.length > 0) {
16195
+ const fileList = recentEdits.slice(0, 5).join(", ");
16196
+ 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>";
16197
+ for (let k = output.messages.length - 1; k >= 0; k--) {
16198
+ const msg = output.messages[k];
16199
+ if (msg.info.role !== "assistant") continue;
16200
+ for (const part of msg.parts) {
16201
+ const p = part;
16202
+ if (p["type"] === "text" && typeof p["text"] === "string") {
16203
+ p.text += nudge;
16204
+ break;
16205
+ }
16206
+ }
16207
+ break;
16208
+ }
16209
+ }
16050
16210
  } else if (Object.values(stats).some((v) => v > 0)) {
16051
16211
  state.mergeNotify({
16052
16212
  compression: stats,
@@ -16673,12 +16833,16 @@ var deepMemoryPlugin = async (input) => {
16673
16833
  },
16674
16834
  tool: { ...memoryTools, deep_expand: createDeepExpandTool(state) },
16675
16835
  "tool.execute.after": async (input2, output) => {
16676
- if (input2.tool !== "read") return;
16677
16836
  const filePath = input2.args?.path ?? input2.args?.filePath;
16678
16837
  if (!filePath) return;
16679
- const lang = getLanguage(filePath);
16680
- if (!lang) return;
16681
- tracker.recordRead(filePath, output.output || "");
16838
+ if (input2.tool === "read") {
16839
+ const lang = getLanguage(filePath);
16840
+ if (!lang) return;
16841
+ tracker.recordRead(filePath, output.output || "");
16842
+ }
16843
+ if (input2.tool === "edit" || input2.tool === "write") {
16844
+ state.trackEdit(filePath);
16845
+ }
16682
16846
  },
16683
16847
  "experimental.session.compacting": createCompactingHandler({
16684
16848
  client: input.client,