@bd7pil/opencode-deep-memory 0.8.2 → 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 +8 -3
- package/dist/index.js +197 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
**
|
|
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 =
|
|
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,9 @@ 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
|
+
"task",
|
|
15791
|
+
"skill"
|
|
15648
15792
|
]);
|
|
15649
15793
|
var NEVER_DEDUP = /* @__PURE__ */ new Set(["read", "bash", "grep", "glob", "find", "search"]);
|
|
15650
15794
|
var ERROR_PURGE_TURN_THRESHOLD = 4;
|
|
@@ -15707,7 +15851,7 @@ function singlePassCompress(messages, state, protectedTail) {
|
|
|
15707
15851
|
const toolName = p["tool"];
|
|
15708
15852
|
const callID = p["callID"];
|
|
15709
15853
|
const toolState = p["state"];
|
|
15710
|
-
if (toolState?.["status"] === "error") {
|
|
15854
|
+
if (toolState?.["status"] === "error" && !PROTECTED_TOOLS.has(toolName ?? "")) {
|
|
15711
15855
|
const age = totalMessages - i;
|
|
15712
15856
|
if (age >= ERROR_PURGE_TURN_THRESHOLD) {
|
|
15713
15857
|
if (typeof toolState["input"] === "object" && toolState["input"] !== null) {
|
|
@@ -15751,7 +15895,7 @@ function singlePassCompress(messages, state, protectedTail) {
|
|
|
15751
15895
|
seen.set(signature, { msgIdx: i, outputHash });
|
|
15752
15896
|
}
|
|
15753
15897
|
}
|
|
15754
|
-
if (output.length >= 200) {
|
|
15898
|
+
if (output.length >= 200 && !PROTECTED_TOOLS.has(toolName)) {
|
|
15755
15899
|
const result = compressToolOutput(toolName, output);
|
|
15756
15900
|
if (result.length < output.length * 0.85) {
|
|
15757
15901
|
const hash2 = ccrStore(state, output, result, toolName, callID);
|
|
@@ -15760,7 +15904,7 @@ function singlePassCompress(messages, state, protectedTail) {
|
|
|
15760
15904
|
continue;
|
|
15761
15905
|
}
|
|
15762
15906
|
}
|
|
15763
|
-
if (output.length >= 200 && detectContentType(output) === "json") {
|
|
15907
|
+
if (output.length >= 200 && detectContentType(output) === "json" && !PROTECTED_TOOLS.has(toolName)) {
|
|
15764
15908
|
const crushed = crushJsonArray(output);
|
|
15765
15909
|
if (crushed.length < output.length * 0.85) {
|
|
15766
15910
|
const hash2 = ccrStore(state, output, crushed, toolName, callID);
|
|
@@ -16047,6 +16191,23 @@ function createMessagesTransformHandler(state, logger) {
|
|
|
16047
16191
|
protectedHead: PROTECTED_HEAD,
|
|
16048
16192
|
protectedTail: KEEP_RECENT
|
|
16049
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
|
+
}
|
|
16050
16211
|
} else if (Object.values(stats).some((v) => v > 0)) {
|
|
16051
16212
|
state.mergeNotify({
|
|
16052
16213
|
compression: stats,
|
|
@@ -16673,12 +16834,16 @@ var deepMemoryPlugin = async (input) => {
|
|
|
16673
16834
|
},
|
|
16674
16835
|
tool: { ...memoryTools, deep_expand: createDeepExpandTool(state) },
|
|
16675
16836
|
"tool.execute.after": async (input2, output) => {
|
|
16676
|
-
if (input2.tool !== "read") return;
|
|
16677
16837
|
const filePath = input2.args?.path ?? input2.args?.filePath;
|
|
16678
16838
|
if (!filePath) return;
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
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
|
+
}
|
|
16682
16847
|
},
|
|
16683
16848
|
"experimental.session.compacting": createCompactingHandler({
|
|
16684
16849
|
client: input.client,
|