@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 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: (1) Use memory_search to recall past decisions before re-deciding. (2) After encountering a tool error and fixing it, use memory_store with type="gotcha" to save the error+fix pair. (3) When the user states a constraint or rule, use memory_store with type="constraint".';
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(["question", "edit", "write", "todowrite", "todoread", "memory_store", "memory_search", "memory_forget"]);
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 (const msg of messages) {
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 && existing !== callID) {
15322
- toolState["output"] = "[superseded by duplicate call]";
15323
- state.recordToolSignature(callID, signature);
15324
- deduped++;
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, callID);
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]" || text.startsWith("[compressed")) continue;
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}|^\[\d{4}|ERROR|WARN|INFO|DEBUG|FATAL|TRACE/.test(l)).length;
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
- const lastMsg = messages[messages.length - 1];
15619
- if (lastMsg) {
15620
- const textParts = lastMsg.parts.filter(
15621
- (p) => typeof p === "object" && p !== null && p.type === "text"
15622
- );
15623
- const lastTextPart = textParts[textParts.length - 1];
15624
- if (lastTextPart && typeof lastTextPart.text === "string") {
15625
- lastTextPart.text += buildNudgeText(pressure.level);
15626
- stats.nudgeInjected = true;
15627
- state.recordNudge(messages.length);
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 KEEP_RECENT = 8;
15694
- var PROTECTED_HEAD = 3;
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 <= KEEP_RECENT) return;
15781
- if (messages.length <= KEEP_RECENT + PROTECTED_HEAD) return;
15782
- const protectedTailStart = messages.length - KEEP_RECENT;
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 = PROTECTED_HEAD; i < protectedTailStart; 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: PROTECTED_HEAD,
15863
- protectedTail: KEEP_RECENT
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: PROTECTED_HEAD,
15870
- protectedTail: KEEP_RECENT
15988
+ protectedHead: PROTECTED_HEAD2,
15989
+ protectedTail: KEEP_RECENT2
15871
15990
  });
15872
15991
  }
15873
15992
  };