@bd7pil/opencode-deep-memory 0.4.1 → 0.4.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/dist/index.js +154 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -390,6 +390,10 @@ var PluginState = class {
|
|
|
390
390
|
return this._toolSignatures.get(callID);
|
|
391
391
|
}
|
|
392
392
|
ccStore(hash2, entry) {
|
|
393
|
+
const now = Date.now();
|
|
394
|
+
for (const [k, v] of this._ccrCache) {
|
|
395
|
+
if (now - v.createdAt > 3e5) this._ccrCache.delete(k);
|
|
396
|
+
}
|
|
393
397
|
if (this._ccrCache.size > 200) {
|
|
394
398
|
const oldest = [...this._ccrCache.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt).slice(0, 50);
|
|
395
399
|
for (const [k] of oldest) this._ccrCache.delete(k);
|
|
@@ -836,7 +840,7 @@ ${lines.join("\n")}
|
|
|
836
840
|
}
|
|
837
841
|
|
|
838
842
|
// src/inject/system-payload.ts
|
|
839
|
-
var TOOL_HINT = 'Memory tools available: memory_search, memory_store, memory_forget. Guidelines
|
|
843
|
+
var TOOL_HINT = 'Memory tools available: memory_search, memory_store, memory_forget, memory_expand, deep_expand. Guidelines:\n (1) BEFORE making ANY technical decision, search: memory_search(query="decision OR decided OR chose OR \u9009\u62E9 OR \u51B3\u5B9A", scope="project")\n (2) BEFORE fixing an error, search for known pitfalls: memory_search(query="gotcha OR error OR bug OR \u5751 OR \u9519\u8BEF", scope="project")\n (3) AFTER fixing an error, store it: memory_store(type="gotcha", content="[error]: ... \u2192 [fix]: ...", scope="project")\n (4) WHEN user states a constraint/rule, store it: memory_store(type="constraint", content="...", scope="project")\n (5) WHEN a technical decision is made, store it: memory_store(type="decision", content="[decision]: ... \u2192 [reason]: ...", scope="project")';
|
|
840
844
|
async function composeSystemPayload(opts) {
|
|
841
845
|
const { state, sessionID, projectPath, mode, searchService, userQuery, logger, tracker } = opts;
|
|
842
846
|
const agent = sessionID ? state.agentOf(sessionID) : void 0;
|
|
@@ -15294,7 +15298,21 @@ function detectPressure(messages, modelId) {
|
|
|
15294
15298
|
}
|
|
15295
15299
|
|
|
15296
15300
|
// src/compress/dedup.ts
|
|
15297
|
-
var PROTECTED_TOOLS = /* @__PURE__ */ new Set([
|
|
15301
|
+
var PROTECTED_TOOLS = /* @__PURE__ */ new Set([
|
|
15302
|
+
"question",
|
|
15303
|
+
"edit",
|
|
15304
|
+
"write",
|
|
15305
|
+
"todowrite",
|
|
15306
|
+
"todoread",
|
|
15307
|
+
"memory_store",
|
|
15308
|
+
"memory_search",
|
|
15309
|
+
"memory_forget",
|
|
15310
|
+
"memory_expand",
|
|
15311
|
+
"deep_expand"
|
|
15312
|
+
]);
|
|
15313
|
+
var NEVER_DEDUP = /* @__PURE__ */ new Set(["read", "bash", "grep", "glob", "find", "search"]);
|
|
15314
|
+
var KEEP_RECENT = 8;
|
|
15315
|
+
var PROTECTED_HEAD = 3;
|
|
15298
15316
|
function createToolSignature(tool5, args) {
|
|
15299
15317
|
if (!args) return tool5;
|
|
15300
15318
|
const sorted = Object.keys(args).sort().map((k) => `${k}:${JSON.stringify(args[k])}`).join(",");
|
|
@@ -15302,8 +15320,12 @@ function createToolSignature(tool5, args) {
|
|
|
15302
15320
|
}
|
|
15303
15321
|
function deduplicateToolOutputs(messages, state) {
|
|
15304
15322
|
let deduped = 0;
|
|
15323
|
+
const totalMessages = messages.length;
|
|
15324
|
+
if (totalMessages <= KEEP_RECENT + PROTECTED_HEAD) return 0;
|
|
15325
|
+
const protectedTailStart = totalMessages - KEEP_RECENT;
|
|
15305
15326
|
const seen = /* @__PURE__ */ new Map();
|
|
15306
|
-
for (
|
|
15327
|
+
for (let i = PROTECTED_HEAD; i < protectedTailStart; i++) {
|
|
15328
|
+
const msg = messages[i];
|
|
15307
15329
|
for (const part of msg.parts) {
|
|
15308
15330
|
if (typeof part !== "object" || part === null) continue;
|
|
15309
15331
|
const p = part;
|
|
@@ -15312,24 +15334,54 @@ function deduplicateToolOutputs(messages, state) {
|
|
|
15312
15334
|
const callID = p["callID"];
|
|
15313
15335
|
if (!toolName || !callID) continue;
|
|
15314
15336
|
if (PROTECTED_TOOLS.has(toolName)) continue;
|
|
15337
|
+
if (NEVER_DEDUP.has(toolName)) continue;
|
|
15315
15338
|
const status = p["state"]?.["status"];
|
|
15316
15339
|
if (status !== "completed") continue;
|
|
15317
15340
|
const toolState = p["state"];
|
|
15341
|
+
const output = toolState["output"];
|
|
15342
|
+
if (typeof output !== "string") continue;
|
|
15343
|
+
if (output === "[superseded by duplicate call]") continue;
|
|
15344
|
+
if (output.includes("[ccr:")) continue;
|
|
15318
15345
|
const input = toolState["input"];
|
|
15319
15346
|
const signature = createToolSignature(toolName, input);
|
|
15347
|
+
const outputHash = simpleHash(output);
|
|
15320
15348
|
const existing = seen.get(signature);
|
|
15321
|
-
if (existing
|
|
15322
|
-
|
|
15323
|
-
|
|
15324
|
-
|
|
15349
|
+
if (existing) {
|
|
15350
|
+
if (existing.outputHash === outputHash) {
|
|
15351
|
+
const prevMsg = messages[existing.msgIdx];
|
|
15352
|
+
for (const prevPart of prevMsg.parts) {
|
|
15353
|
+
if (typeof prevPart !== "object" || prevPart === null) continue;
|
|
15354
|
+
const pp = prevPart;
|
|
15355
|
+
if (pp["type"] !== "tool") continue;
|
|
15356
|
+
const ppState = pp["state"];
|
|
15357
|
+
if (ppState?.["output"] === "[superseded by duplicate call]") continue;
|
|
15358
|
+
if (typeof ppState?.["output"] === "string" && simpleHash(ppState["output"]) === outputHash) {
|
|
15359
|
+
ppState["output"] = "[superseded by duplicate call]";
|
|
15360
|
+
deduped++;
|
|
15361
|
+
}
|
|
15362
|
+
}
|
|
15363
|
+
}
|
|
15364
|
+
seen.set(signature, { msgIdx: i, outputHash });
|
|
15325
15365
|
} else {
|
|
15326
|
-
seen.set(signature,
|
|
15327
|
-
state.recordToolSignature(callID, signature);
|
|
15366
|
+
seen.set(signature, { msgIdx: i, outputHash });
|
|
15328
15367
|
}
|
|
15329
15368
|
}
|
|
15330
15369
|
}
|
|
15331
15370
|
return deduped;
|
|
15332
15371
|
}
|
|
15372
|
+
function simpleHash(s) {
|
|
15373
|
+
const len = s.length;
|
|
15374
|
+
const sampleSize = 500;
|
|
15375
|
+
let h = len;
|
|
15376
|
+
for (let i = 0; i < Math.min(len, sampleSize); i++) {
|
|
15377
|
+
h = h * 31 + s.charCodeAt(i) | 0;
|
|
15378
|
+
}
|
|
15379
|
+
const tailStart = Math.max(sampleSize, len - sampleSize);
|
|
15380
|
+
for (let i = tailStart; i < len; i++) {
|
|
15381
|
+
h = h * 31 + s.charCodeAt(i) | 0;
|
|
15382
|
+
}
|
|
15383
|
+
return `${len}:${h.toString(36)}`;
|
|
15384
|
+
}
|
|
15333
15385
|
|
|
15334
15386
|
// src/compress/error-purge.ts
|
|
15335
15387
|
var ERROR_PURGE_TURN_THRESHOLD = 4;
|
|
@@ -15520,7 +15572,8 @@ function pruneOldMessages(messages) {
|
|
|
15520
15572
|
if (p["type"] !== "text" || typeof p["text"] !== "string") continue;
|
|
15521
15573
|
const text = p["text"];
|
|
15522
15574
|
if (text.length < 500) continue;
|
|
15523
|
-
if (text === "[cleared]" || text === "[stripped]"
|
|
15575
|
+
if (text === "[cleared]" || text === "[stripped]") continue;
|
|
15576
|
+
if (text.includes("[compressed from")) continue;
|
|
15524
15577
|
const keyInfo = extractKeyInfo(text);
|
|
15525
15578
|
if (keyInfo.length < text.length * 0.6) {
|
|
15526
15579
|
p["text"] = keyInfo + "\n[compressed from " + text.length + " chars]";
|
|
@@ -15568,6 +15621,60 @@ function buildNudgeText(level) {
|
|
|
15568
15621
|
return "";
|
|
15569
15622
|
}
|
|
15570
15623
|
|
|
15624
|
+
// src/compress/memory-nudge.ts
|
|
15625
|
+
var MEMORY_NUDGE_COOLDOWN = 3;
|
|
15626
|
+
var DECISION_PATTERNS = [
|
|
15627
|
+
/\b(?:decided|decision|chose|chosen|picked|selected)\b/i,
|
|
15628
|
+
/\b(?:采用|选择|决定|确定|选用)\b/,
|
|
15629
|
+
/\b(?:use|using|go with|went with)\b.*\b(?:because|since|due to)\b/i
|
|
15630
|
+
];
|
|
15631
|
+
var CONSTRAINT_PATTERNS = [
|
|
15632
|
+
/\b(?:must not|cannot|should not|do not|never|always)\b/i,
|
|
15633
|
+
/\b(?:constraint|restriction|limitation|requirement)\b/i,
|
|
15634
|
+
/\b(?:不能|必须|禁止|约束|限制|要求|务必)\b/
|
|
15635
|
+
];
|
|
15636
|
+
var ERROR_FIX_PATTERNS = [
|
|
15637
|
+
/\b(?:fix|fixed|resolve|resolved|patch|corrected)\b/i,
|
|
15638
|
+
/\b(?:修复|修复了|解决|解决了)\b/,
|
|
15639
|
+
/\b(?:the (?:bug|error|issue) (?:was|is)|root cause)\b/i
|
|
15640
|
+
];
|
|
15641
|
+
function detectMemoryNudge(messages, messagesSinceLastNudge) {
|
|
15642
|
+
if (messagesSinceLastNudge < MEMORY_NUDGE_COOLDOWN) {
|
|
15643
|
+
return { injected: false, type: null };
|
|
15644
|
+
}
|
|
15645
|
+
const protectedTail = Math.max(0, messages.length - 3);
|
|
15646
|
+
const recentMessages = messages.slice(protectedTail);
|
|
15647
|
+
const recentAssistantText = recentMessages.filter((m) => m.info.role === "assistant").flatMap((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text || "")).join("\n");
|
|
15648
|
+
const recentUserText = recentMessages.filter((m) => m.info.role === "user").flatMap((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text || "")).join("\n");
|
|
15649
|
+
const hasRecentToolError = recentMessages.some(
|
|
15650
|
+
(m) => m.parts.some((p) => p.type === "tool" && p.state?.status === "error")
|
|
15651
|
+
);
|
|
15652
|
+
if (hasRecentToolError && ERROR_FIX_PATTERNS.some((p) => p.test(recentAssistantText))) {
|
|
15653
|
+
return { injected: true, type: "gotcha" };
|
|
15654
|
+
}
|
|
15655
|
+
if (CONSTRAINT_PATTERNS.some((p) => p.test(recentUserText))) {
|
|
15656
|
+
return { injected: true, type: "constraint" };
|
|
15657
|
+
}
|
|
15658
|
+
if (DECISION_PATTERNS.some((p) => p.test(recentAssistantText))) {
|
|
15659
|
+
return { injected: true, type: "decision" };
|
|
15660
|
+
}
|
|
15661
|
+
return { injected: false, type: null };
|
|
15662
|
+
}
|
|
15663
|
+
function buildMemoryNudge(type) {
|
|
15664
|
+
switch (type) {
|
|
15665
|
+
case "gotcha":
|
|
15666
|
+
return `
|
|
15667
|
+
<memory-nudge type="gotcha">You just fixed an error. Use memory_store(type="gotcha") to save what went wrong and how you fixed it, so future sessions don't repeat this mistake.</memory-nudge>`;
|
|
15668
|
+
case "constraint":
|
|
15669
|
+
return '\n<memory-nudge type="constraint">The user expressed a constraint or rule. Use memory_store(type="constraint") to persist it across sessions.</memory-nudge>';
|
|
15670
|
+
case "decision":
|
|
15671
|
+
return `
|
|
15672
|
+
<memory-nudge type="decision">A technical decision was made. Use memory_store(type="decision") to record what was decided and why, so future sessions don't re-decide.</memory-nudge>`;
|
|
15673
|
+
default:
|
|
15674
|
+
return "";
|
|
15675
|
+
}
|
|
15676
|
+
}
|
|
15677
|
+
|
|
15571
15678
|
// src/compress/detector.ts
|
|
15572
15679
|
function detectContentType(content) {
|
|
15573
15680
|
const trimmed = content.trimStart();
|
|
@@ -15582,7 +15689,7 @@ function detectContentType(content) {
|
|
|
15582
15689
|
if (/Traceback \(most recent call last\)|at \S+\.\S+\(|Error: |Exception: |TypeError: |ReferenceError: /m.test(content)) return "error-trace";
|
|
15583
15690
|
if (/<[a-z][\s\S]*>/i.test(content) && /<(html|div|span|body|head|script|style)[\s>]/i.test(content)) return "html";
|
|
15584
15691
|
const lines = content.split("\n");
|
|
15585
|
-
const logLineCount = lines.filter((l) => /^\d{4}-\d{2}-\d{2}
|
|
15692
|
+
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;
|
|
15586
15693
|
if (lines.length > 5 && logLineCount / lines.length > 0.3) return "log";
|
|
15587
15694
|
const codePatterns = /\b(function |class |def |import |from .+ import|const |let |var |export |interface |type |struct |fn |func |pub |private |protected )\b/;
|
|
15588
15695
|
const codeLines = lines.filter((l) => codePatterns.test(l)).length;
|
|
@@ -15608,24 +15715,23 @@ function runCompressionPipeline(ctx) {
|
|
|
15608
15715
|
};
|
|
15609
15716
|
stats.toolDedup = deduplicateToolOutputs(messages, state);
|
|
15610
15717
|
stats.errorPurge = purgeOldErrors(messages);
|
|
15611
|
-
stats.toolOutputCompressed = compressOldToolOutputs(messages, state);
|
|
15612
15718
|
stats.jsonCrushed = crushJsonToolOutputs(messages, state);
|
|
15719
|
+
stats.toolOutputCompressed = compressOldToolOutputs(messages, state);
|
|
15613
15720
|
if (pressure.level === "medium" || pressure.level === "high") {
|
|
15614
15721
|
stats.messagePruned = pruneOldMessages(messages);
|
|
15615
15722
|
}
|
|
15616
15723
|
const messagesSinceNudge = state.messagesSinceLastNudge(messages.length);
|
|
15617
15724
|
if (shouldInjectNudge(pressure.level, messagesSinceNudge)) {
|
|
15618
|
-
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
|
|
15622
|
-
|
|
15623
|
-
|
|
15624
|
-
|
|
15625
|
-
|
|
15626
|
-
|
|
15627
|
-
|
|
15628
|
-
}
|
|
15725
|
+
if (injectIntoLastAssistant(messages, buildNudgeText(pressure.level))) {
|
|
15726
|
+
stats.nudgeInjected = true;
|
|
15727
|
+
state.recordNudge(messages.length);
|
|
15728
|
+
}
|
|
15729
|
+
}
|
|
15730
|
+
const memoryNudge = detectMemoryNudge(messages, state.messagesSinceLastNudge(messages.length));
|
|
15731
|
+
if (memoryNudge.injected) {
|
|
15732
|
+
if (injectIntoLastAssistant(messages, buildMemoryNudge(memoryNudge.type))) {
|
|
15733
|
+
state.recordNudge(messages.length);
|
|
15734
|
+
logger?.debug("compress: memory nudge", { type: memoryNudge.type });
|
|
15629
15735
|
}
|
|
15630
15736
|
}
|
|
15631
15737
|
const active = stats.toolDedup > 0 || stats.errorPurge > 0 || stats.toolOutputCompressed > 0 || stats.jsonCrushed > 0 || stats.messagePruned > 0 || stats.nudgeInjected;
|
|
@@ -15636,6 +15742,21 @@ function runCompressionPipeline(ctx) {
|
|
|
15636
15742
|
}
|
|
15637
15743
|
return { stats };
|
|
15638
15744
|
}
|
|
15745
|
+
function injectIntoLastAssistant(messages, text) {
|
|
15746
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
15747
|
+
const msg = messages[i];
|
|
15748
|
+
if (msg.info.role !== "assistant") continue;
|
|
15749
|
+
const textParts = msg.parts.filter(
|
|
15750
|
+
(p) => typeof p === "object" && p !== null && p.type === "text"
|
|
15751
|
+
);
|
|
15752
|
+
const lastTextPart = textParts[textParts.length - 1];
|
|
15753
|
+
if (lastTextPart && typeof lastTextPart.text === "string") {
|
|
15754
|
+
lastTextPart.text += text;
|
|
15755
|
+
return true;
|
|
15756
|
+
}
|
|
15757
|
+
}
|
|
15758
|
+
return false;
|
|
15759
|
+
}
|
|
15639
15760
|
function compressOldToolOutputs(messages, state) {
|
|
15640
15761
|
let compressed = 0;
|
|
15641
15762
|
const protectedTail = messages.length - 8;
|
|
@@ -15648,7 +15769,6 @@ function compressOldToolOutputs(messages, state) {
|
|
|
15648
15769
|
if (p.state?.status !== "completed") continue;
|
|
15649
15770
|
if (!p.state.output) continue;
|
|
15650
15771
|
if (p.state.output === "[superseded by duplicate call]") continue;
|
|
15651
|
-
if (p.state.output.startsWith("[compressed")) continue;
|
|
15652
15772
|
if (p.state.output.includes("[ccr:")) continue;
|
|
15653
15773
|
const toolName = p.tool || "unknown";
|
|
15654
15774
|
const output = p.state.output;
|
|
@@ -15673,7 +15793,6 @@ function crushJsonToolOutputs(messages, state) {
|
|
|
15673
15793
|
if (p.type !== "tool") continue;
|
|
15674
15794
|
if (p.state?.status !== "completed") continue;
|
|
15675
15795
|
if (!p.state.output) continue;
|
|
15676
|
-
if (p.state.output.startsWith("[compressed")) continue;
|
|
15677
15796
|
if (p.state.output.startsWith("[superseded")) continue;
|
|
15678
15797
|
if (p.state.output.includes("[ccr:")) continue;
|
|
15679
15798
|
if (detectContentType(p.state.output) !== "json") continue;
|
|
@@ -15690,8 +15809,8 @@ function crushJsonToolOutputs(messages, state) {
|
|
|
15690
15809
|
}
|
|
15691
15810
|
|
|
15692
15811
|
// src/hooks/messages-transform.ts
|
|
15693
|
-
var
|
|
15694
|
-
var
|
|
15812
|
+
var KEEP_RECENT2 = 8;
|
|
15813
|
+
var PROTECTED_HEAD2 = 3;
|
|
15695
15814
|
var SYSTEM_INJECTION_PATTERNS = [
|
|
15696
15815
|
/^$/,
|
|
15697
15816
|
/^<!-- OMO_INTERNAL_INITIATOR -->$/,
|
|
@@ -15777,9 +15896,9 @@ function repairOrphanedToolCalls(messages) {
|
|
|
15777
15896
|
function createMessagesTransformHandler(state, logger) {
|
|
15778
15897
|
return async (_input, output) => {
|
|
15779
15898
|
const messages = output.messages;
|
|
15780
|
-
if (messages.length <=
|
|
15781
|
-
if (messages.length <=
|
|
15782
|
-
const protectedTailStart = messages.length -
|
|
15899
|
+
if (messages.length <= KEEP_RECENT2) return;
|
|
15900
|
+
if (messages.length <= KEEP_RECENT2 + PROTECTED_HEAD2) return;
|
|
15901
|
+
const protectedTailStart = messages.length - KEEP_RECENT2;
|
|
15783
15902
|
const stats = {
|
|
15784
15903
|
reasoning_cleared: 0,
|
|
15785
15904
|
metadata_stripped: 0,
|
|
@@ -15787,7 +15906,7 @@ function createMessagesTransformHandler(state, logger) {
|
|
|
15787
15906
|
tool_errors_truncated: 0,
|
|
15788
15907
|
thinking_stripped: 0
|
|
15789
15908
|
};
|
|
15790
|
-
for (let i =
|
|
15909
|
+
for (let i = PROTECTED_HEAD2; i < protectedTailStart; i++) {
|
|
15791
15910
|
const msg = messages[i];
|
|
15792
15911
|
if (!msg?.parts?.length) continue;
|
|
15793
15912
|
if (msg.info.role === "user") continue;
|
|
@@ -15859,15 +15978,15 @@ function createMessagesTransformHandler(state, logger) {
|
|
|
15859
15978
|
compression: stats,
|
|
15860
15979
|
deepCompression: ds,
|
|
15861
15980
|
messageCount: messages.length,
|
|
15862
|
-
protectedHead:
|
|
15863
|
-
protectedTail:
|
|
15981
|
+
protectedHead: PROTECTED_HEAD2,
|
|
15982
|
+
protectedTail: KEEP_RECENT2
|
|
15864
15983
|
});
|
|
15865
15984
|
} else if (Object.values(stats).some((v) => v > 0)) {
|
|
15866
15985
|
state.mergeNotify({
|
|
15867
15986
|
compression: stats,
|
|
15868
15987
|
messageCount: messages.length,
|
|
15869
|
-
protectedHead:
|
|
15870
|
-
protectedTail:
|
|
15988
|
+
protectedHead: PROTECTED_HEAD2,
|
|
15989
|
+
protectedTail: KEEP_RECENT2
|
|
15871
15990
|
});
|
|
15872
15991
|
}
|
|
15873
15992
|
};
|