@bd7pil/opencode-deep-memory 0.4.0 → 0.4.2

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
@@ -79,10 +79,9 @@ Activates when context pressure exceeds thresholds. Inspired by
79
79
 
80
80
  | Pressure | Threshold | Actions |
81
81
  |----------|-----------|---------|
82
- | **low** | < 50% context | Layer 1 only |
83
- | **medium** | 50–70% | + tool dedup + error purge + tool output compression |
84
- | **high** | 70–85% | + JSON array crush + old message truncation + nudge |
85
- | **critical** | > 85% | + aggressive nudge (model prompted to compress) |
82
+ | **always** | every turn | tool dedup + error purge + tool output compress + JSON crush (all reversible via CCR) |
83
+ | **medium** | ≥ 30% context | + old message text truncation (lossy, extracts key info) |
84
+ | **high** | ≥ 50% context | + nudge (alerts model to save important findings)
86
85
 
87
86
  What gets compressed at medium+:
88
87
 
@@ -179,6 +178,7 @@ updated incrementally on writes.
179
178
  | `memory_forget` | Remove memory entries matching a query |
180
179
  | `memory_expand` | Decompress a sentinel reference to its original content |
181
180
  | `deep_expand` | Retrieve original content compressed by CCR (use `[ccr:HASH]` marker) |
181
+ | `deep_expand` | Retrieve original content compressed by CCR (use `[ccr:HASH]` marker) |
182
182
 
183
183
  ## Commands
184
184
 
package/dist/index.js CHANGED
@@ -260,6 +260,7 @@ var PluginState = class {
260
260
  _toolSignatures = /* @__PURE__ */ new Map();
261
261
  _ccrCache = /* @__PURE__ */ new Map();
262
262
  _lastInputTokens = 0;
263
+ _lastNudgeMessageCount = 0;
263
264
  agentOf(sessionID) {
264
265
  return this._agents.get(sessionID);
265
266
  }
@@ -404,6 +405,12 @@ var PluginState = class {
404
405
  lastInputTokens() {
405
406
  return this._lastInputTokens;
406
407
  }
408
+ recordNudge(messageCount) {
409
+ this._lastNudgeMessageCount = messageCount;
410
+ }
411
+ messagesSinceLastNudge(currentMessageCount) {
412
+ return currentMessageCount - this._lastNudgeMessageCount;
413
+ }
407
414
  };
408
415
  function createPluginState() {
409
416
  return new PluginState();
@@ -829,7 +836,7 @@ ${lines.join("\n")}
829
836
  }
830
837
 
831
838
  // src/inject/system-payload.ts
832
- 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".';
839
+ 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")';
833
840
  async function composeSystemPayload(opts) {
834
841
  const { state, sessionID, projectPath, mode, searchService, userQuery, logger, tracker } = opts;
835
842
  const agent = sessionID ? state.agentOf(sessionID) : void 0;
@@ -15210,10 +15217,8 @@ function createCompactingHandler(args) {
15210
15217
  // src/compress/pressure.ts
15211
15218
  var DEFAULT_MAX_CONTEXT = 128e3;
15212
15219
  var THRESHOLDS = {
15213
- low: 0.5,
15214
- medium: 0.7,
15215
- high: 0.85,
15216
- critical: 0.95
15220
+ medium: 0.3,
15221
+ high: 0.5
15217
15222
  };
15218
15223
  var MODEL_CONTEXT_LIMITS = {
15219
15224
  "deepseek-chat": 64e3,
@@ -15282,15 +15287,28 @@ function detectPressure(messages, modelId) {
15282
15287
  const estimated = inputTokens > 0 ? inputTokens : extractTokensFromMessages(messages);
15283
15288
  const ratio = Math.min(estimated / maxContext, 1);
15284
15289
  let level;
15285
- if (ratio >= THRESHOLDS.critical) level = "critical";
15286
- else if (ratio >= THRESHOLDS.high) level = "high";
15290
+ if (ratio >= THRESHOLDS.high) level = "high";
15287
15291
  else if (ratio >= THRESHOLDS.medium) level = "medium";
15288
15292
  else level = "low";
15289
15293
  return { level, ratio, estimatedTokens: estimated };
15290
15294
  }
15291
15295
 
15292
15296
  // src/compress/dedup.ts
15293
- var PROTECTED_TOOLS = /* @__PURE__ */ new Set(["question", "edit", "write", "todowrite", "todoread", "memory_store", "memory_search", "memory_forget"]);
15297
+ var PROTECTED_TOOLS = /* @__PURE__ */ new Set([
15298
+ "question",
15299
+ "edit",
15300
+ "write",
15301
+ "todowrite",
15302
+ "todoread",
15303
+ "memory_store",
15304
+ "memory_search",
15305
+ "memory_forget",
15306
+ "memory_expand",
15307
+ "deep_expand"
15308
+ ]);
15309
+ var NEVER_DEDUP = /* @__PURE__ */ new Set(["read", "bash", "grep", "glob", "find", "search"]);
15310
+ var KEEP_RECENT = 8;
15311
+ var PROTECTED_HEAD = 3;
15294
15312
  function createToolSignature(tool5, args) {
15295
15313
  if (!args) return tool5;
15296
15314
  const sorted = Object.keys(args).sort().map((k) => `${k}:${JSON.stringify(args[k])}`).join(",");
@@ -15298,8 +15316,12 @@ function createToolSignature(tool5, args) {
15298
15316
  }
15299
15317
  function deduplicateToolOutputs(messages, state) {
15300
15318
  let deduped = 0;
15319
+ const totalMessages = messages.length;
15320
+ if (totalMessages <= KEEP_RECENT + PROTECTED_HEAD) return 0;
15321
+ const protectedTailStart = totalMessages - KEEP_RECENT;
15301
15322
  const seen = /* @__PURE__ */ new Map();
15302
- for (const msg of messages) {
15323
+ for (let i = PROTECTED_HEAD; i < protectedTailStart; i++) {
15324
+ const msg = messages[i];
15303
15325
  for (const part of msg.parts) {
15304
15326
  if (typeof part !== "object" || part === null) continue;
15305
15327
  const p = part;
@@ -15308,24 +15330,48 @@ function deduplicateToolOutputs(messages, state) {
15308
15330
  const callID = p["callID"];
15309
15331
  if (!toolName || !callID) continue;
15310
15332
  if (PROTECTED_TOOLS.has(toolName)) continue;
15333
+ if (NEVER_DEDUP.has(toolName)) continue;
15311
15334
  const status = p["state"]?.["status"];
15312
15335
  if (status !== "completed") continue;
15313
15336
  const toolState = p["state"];
15337
+ const output = toolState["output"];
15338
+ if (typeof output !== "string") continue;
15339
+ if (output === "[superseded by duplicate call]") continue;
15340
+ if (output.includes("[ccr:")) continue;
15314
15341
  const input = toolState["input"];
15315
15342
  const signature = createToolSignature(toolName, input);
15343
+ const outputHash = simpleHash(output);
15316
15344
  const existing = seen.get(signature);
15317
- if (existing && existing !== callID) {
15318
- toolState["output"] = "[superseded by duplicate call]";
15319
- state.recordToolSignature(callID, signature);
15320
- deduped++;
15345
+ if (existing) {
15346
+ if (existing.outputHash === outputHash) {
15347
+ const prevMsg = messages[existing.msgIdx];
15348
+ for (const prevPart of prevMsg.parts) {
15349
+ if (typeof prevPart !== "object" || prevPart === null) continue;
15350
+ const pp = prevPart;
15351
+ if (pp["type"] !== "tool") continue;
15352
+ const ppState = pp["state"];
15353
+ if (ppState?.["output"] === "[superseded by duplicate call]") continue;
15354
+ if (typeof ppState?.["output"] === "string" && simpleHash(ppState["output"]) === outputHash) {
15355
+ ppState["output"] = "[superseded by duplicate call]";
15356
+ deduped++;
15357
+ }
15358
+ }
15359
+ }
15360
+ seen.set(signature, { msgIdx: i, outputHash });
15321
15361
  } else {
15322
- seen.set(signature, callID);
15323
- state.recordToolSignature(callID, signature);
15362
+ seen.set(signature, { msgIdx: i, outputHash });
15324
15363
  }
15325
15364
  }
15326
15365
  }
15327
15366
  return deduped;
15328
15367
  }
15368
+ function simpleHash(s) {
15369
+ let h = 0;
15370
+ for (let i = 0; i < Math.min(s.length, 1e3); i++) {
15371
+ h = h * 31 + s.charCodeAt(i) | 0;
15372
+ }
15373
+ return h.toString(36);
15374
+ }
15329
15375
 
15330
15376
  // src/compress/error-purge.ts
15331
15377
  var ERROR_PURGE_TURN_THRESHOLD = 4;
@@ -15552,16 +15598,70 @@ function extractKeyInfo(text) {
15552
15598
 
15553
15599
  // src/compress/nudge.ts
15554
15600
  var NUDGE_COOLDOWN = 5;
15555
- function shouldInjectNudge(level, messageCount, lastNudgeAt) {
15556
- if (level !== "high" && level !== "critical") return false;
15557
- if (messageCount - lastNudgeAt < NUDGE_COOLDOWN) return false;
15601
+ function shouldInjectNudge(level, messagesSinceLastNudge) {
15602
+ if (level !== "high") return false;
15603
+ if (messagesSinceLastNudge < NUDGE_COOLDOWN) return false;
15558
15604
  return true;
15559
15605
  }
15560
15606
  function buildNudgeText(level) {
15561
- if (level === "critical") {
15562
- return '\n<dm-nudge level="critical">Context is nearly full. Use deep_compress tool to compress old messages before the conversation becomes unusable.</dm-nudge>';
15607
+ if (level === "high") {
15608
+ return '\n<dm-nudge level="high">Context pressure is high. Consider summarizing old completed tasks and moving on. Use memory_store to persist important findings before they are compressed.</dm-nudge>';
15609
+ }
15610
+ return "";
15611
+ }
15612
+
15613
+ // src/compress/memory-nudge.ts
15614
+ var MEMORY_NUDGE_COOLDOWN = 3;
15615
+ var DECISION_PATTERNS = [
15616
+ /\b(?:decided|decision|chose|chosen|picked|selected)\b/i,
15617
+ /\b(?:采用|选择|决定|确定|选用)\b/,
15618
+ /\b(?:use|using|go with|went with)\b.*\b(?:because|since|due to)\b/i
15619
+ ];
15620
+ var CONSTRAINT_PATTERNS = [
15621
+ /\b(?:must not|cannot|should not|do not|never|always)\b/i,
15622
+ /\b(?:constraint|restriction|limitation|requirement)\b/i,
15623
+ /\b(?:不能|必须|禁止|约束|限制|要求|务必)\b/
15624
+ ];
15625
+ var ERROR_FIX_PATTERNS = [
15626
+ /\b(?:fix|fixed|resolve|resolved|patch|corrected)\b/i,
15627
+ /\b(?:修复|修复了|解决|解决了)\b/,
15628
+ /\b(?:the (?:bug|error|issue) (?:was|is)|root cause)\b/i
15629
+ ];
15630
+ function detectMemoryNudge(messages, messagesSinceLastNudge) {
15631
+ if (messagesSinceLastNudge < MEMORY_NUDGE_COOLDOWN) {
15632
+ return { injected: false, type: null };
15633
+ }
15634
+ const protectedTail = Math.max(0, messages.length - 3);
15635
+ const recentMessages = messages.slice(protectedTail);
15636
+ const recentAssistantText = recentMessages.filter((m) => m.info.role === "assistant").flatMap((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text || "")).join("\n");
15637
+ const recentUserText = recentMessages.filter((m) => m.info.role === "user").flatMap((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text || "")).join("\n");
15638
+ const hasRecentToolError = recentMessages.some(
15639
+ (m) => m.parts.some((p) => p.type === "tool" && p.state?.status === "error")
15640
+ );
15641
+ if (hasRecentToolError && ERROR_FIX_PATTERNS.some((p) => p.test(recentAssistantText))) {
15642
+ return { injected: true, type: "gotcha" };
15643
+ }
15644
+ if (CONSTRAINT_PATTERNS.some((p) => p.test(recentUserText))) {
15645
+ return { injected: true, type: "constraint" };
15646
+ }
15647
+ if (DECISION_PATTERNS.some((p) => p.test(recentAssistantText))) {
15648
+ return { injected: true, type: "decision" };
15649
+ }
15650
+ return { injected: false, type: null };
15651
+ }
15652
+ function buildMemoryNudge(type) {
15653
+ switch (type) {
15654
+ case "gotcha":
15655
+ return `
15656
+ <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>`;
15657
+ case "constraint":
15658
+ 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>';
15659
+ case "decision":
15660
+ return `
15661
+ <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>`;
15662
+ default:
15663
+ return "";
15563
15664
  }
15564
- return '\n<dm-nudge level="high">Context is getting large. Consider compressing old tool outputs and messages to free space.</dm-nudge>';
15565
15665
  }
15566
15666
 
15567
15667
  // src/compress/detector.ts
@@ -15602,25 +15702,15 @@ function runCompressionPipeline(ctx) {
15602
15702
  pressureLevel: pressure.level,
15603
15703
  estimatedTokens: pressure.estimatedTokens
15604
15704
  };
15605
- if (pressure.level === "low") {
15606
- logger?.debug("compress: low pressure, skipping", { ratio: pressure.ratio.toFixed(2) });
15607
- return { stats };
15608
- }
15609
- logger?.debug("compress: pipeline running", {
15610
- level: pressure.level,
15611
- ratio: pressure.ratio.toFixed(2),
15612
- tokens: pressure.estimatedTokens
15613
- });
15614
- if (pressure.level === "medium" || pressure.level === "high" || pressure.level === "critical") {
15615
- stats.toolDedup = deduplicateToolOutputs(messages, state);
15616
- stats.errorPurge = purgeOldErrors(messages);
15617
- stats.toolOutputCompressed = compressOldToolOutputs(messages, state);
15618
- }
15619
- if (pressure.level === "high" || pressure.level === "critical") {
15620
- stats.jsonCrushed = crushJsonToolOutputs(messages, state);
15705
+ stats.toolDedup = deduplicateToolOutputs(messages, state);
15706
+ stats.errorPurge = purgeOldErrors(messages);
15707
+ stats.toolOutputCompressed = compressOldToolOutputs(messages, state);
15708
+ stats.jsonCrushed = crushJsonToolOutputs(messages, state);
15709
+ if (pressure.level === "medium" || pressure.level === "high") {
15621
15710
  stats.messagePruned = pruneOldMessages(messages);
15622
15711
  }
15623
- if (shouldInjectNudge(pressure.level, messages.length, 0)) {
15712
+ const messagesSinceNudge = state.messagesSinceLastNudge(messages.length);
15713
+ if (shouldInjectNudge(pressure.level, messagesSinceNudge)) {
15624
15714
  const lastMsg = messages[messages.length - 1];
15625
15715
  if (lastMsg) {
15626
15716
  const textParts = lastMsg.parts.filter(
@@ -15630,11 +15720,30 @@ function runCompressionPipeline(ctx) {
15630
15720
  if (lastTextPart && typeof lastTextPart.text === "string") {
15631
15721
  lastTextPart.text += buildNudgeText(pressure.level);
15632
15722
  stats.nudgeInjected = true;
15723
+ state.recordNudge(messages.length);
15633
15724
  }
15634
15725
  }
15635
15726
  }
15636
- if (stats.toolDedup > 0 || stats.errorPurge > 0 || stats.toolOutputCompressed > 0 || stats.jsonCrushed > 0 || stats.messagePruned > 0 || stats.nudgeInjected) {
15637
- logger?.debug("compress: pipeline complete", { ...stats });
15727
+ const memoryNudge = detectMemoryNudge(messages, state.messagesSinceLastNudge(messages.length));
15728
+ if (memoryNudge.injected) {
15729
+ const lastMsg = messages[messages.length - 1];
15730
+ if (lastMsg) {
15731
+ const textParts = lastMsg.parts.filter(
15732
+ (p) => typeof p === "object" && p !== null && p.type === "text"
15733
+ );
15734
+ const lastTextPart = textParts[textParts.length - 1];
15735
+ if (lastTextPart && typeof lastTextPart.text === "string") {
15736
+ lastTextPart.text += buildMemoryNudge(memoryNudge.type);
15737
+ state.recordNudge(messages.length);
15738
+ logger?.debug("compress: memory nudge", { type: memoryNudge.type });
15739
+ }
15740
+ }
15741
+ }
15742
+ const active = stats.toolDedup > 0 || stats.errorPurge > 0 || stats.toolOutputCompressed > 0 || stats.jsonCrushed > 0 || stats.messagePruned > 0 || stats.nudgeInjected;
15743
+ if (active) {
15744
+ logger?.debug("compress: pipeline result", { ...stats });
15745
+ } else {
15746
+ logger?.debug("compress: no action needed", { ratio: pressure.ratio.toFixed(2) });
15638
15747
  }
15639
15748
  return { stats };
15640
15749
  }
@@ -15651,6 +15760,7 @@ function compressOldToolOutputs(messages, state) {
15651
15760
  if (!p.state.output) continue;
15652
15761
  if (p.state.output === "[superseded by duplicate call]") continue;
15653
15762
  if (p.state.output.startsWith("[compressed")) continue;
15763
+ if (p.state.output.includes("[ccr:")) continue;
15654
15764
  const toolName = p.tool || "unknown";
15655
15765
  const output = p.state.output;
15656
15766
  const result = compressToolOutput(toolName, output);
@@ -15676,6 +15786,7 @@ function crushJsonToolOutputs(messages, state) {
15676
15786
  if (!p.state.output) continue;
15677
15787
  if (p.state.output.startsWith("[compressed")) continue;
15678
15788
  if (p.state.output.startsWith("[superseded")) continue;
15789
+ if (p.state.output.includes("[ccr:")) continue;
15679
15790
  if (detectContentType(p.state.output) !== "json") continue;
15680
15791
  const original = p.state.output;
15681
15792
  const crushed_output = crushJsonArray(original);
@@ -15690,8 +15801,8 @@ function crushJsonToolOutputs(messages, state) {
15690
15801
  }
15691
15802
 
15692
15803
  // src/hooks/messages-transform.ts
15693
- var KEEP_RECENT = 8;
15694
- var PROTECTED_HEAD = 3;
15804
+ var KEEP_RECENT2 = 8;
15805
+ var PROTECTED_HEAD2 = 3;
15695
15806
  var SYSTEM_INJECTION_PATTERNS = [
15696
15807
  /^$/,
15697
15808
  /^<!-- OMO_INTERNAL_INITIATOR -->$/,
@@ -15777,9 +15888,9 @@ function repairOrphanedToolCalls(messages) {
15777
15888
  function createMessagesTransformHandler(state, logger) {
15778
15889
  return async (_input, output) => {
15779
15890
  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;
15891
+ if (messages.length <= KEEP_RECENT2) return;
15892
+ if (messages.length <= KEEP_RECENT2 + PROTECTED_HEAD2) return;
15893
+ const protectedTailStart = messages.length - KEEP_RECENT2;
15783
15894
  const stats = {
15784
15895
  reasoning_cleared: 0,
15785
15896
  metadata_stripped: 0,
@@ -15787,7 +15898,7 @@ function createMessagesTransformHandler(state, logger) {
15787
15898
  tool_errors_truncated: 0,
15788
15899
  thinking_stripped: 0
15789
15900
  };
15790
- for (let i = PROTECTED_HEAD; i < protectedTailStart; i++) {
15901
+ for (let i = PROTECTED_HEAD2; i < protectedTailStart; i++) {
15791
15902
  const msg = messages[i];
15792
15903
  if (!msg?.parts?.length) continue;
15793
15904
  if (msg.info.role === "user") continue;
@@ -15859,15 +15970,15 @@ function createMessagesTransformHandler(state, logger) {
15859
15970
  compression: stats,
15860
15971
  deepCompression: ds,
15861
15972
  messageCount: messages.length,
15862
- protectedHead: PROTECTED_HEAD,
15863
- protectedTail: KEEP_RECENT
15973
+ protectedHead: PROTECTED_HEAD2,
15974
+ protectedTail: KEEP_RECENT2
15864
15975
  });
15865
15976
  } else if (Object.values(stats).some((v) => v > 0)) {
15866
15977
  state.mergeNotify({
15867
15978
  compression: stats,
15868
15979
  messageCount: messages.length,
15869
- protectedHead: PROTECTED_HEAD,
15870
- protectedTail: KEEP_RECENT
15980
+ protectedHead: PROTECTED_HEAD2,
15981
+ protectedTail: KEEP_RECENT2
15871
15982
  });
15872
15983
  }
15873
15984
  };