@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 +8 -3
- package/dist/index.js +196 -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,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
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
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,
|