@bd7pil/opencode-deep-memory 0.5.2 → 0.7.0

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
@@ -15166,6 +15166,53 @@ async function writeCheckpoint(args) {
15166
15166
 
15167
15167
  // src/hooks/compacting.ts
15168
15168
  import { readFile as readFile2 } from "fs/promises";
15169
+
15170
+ // src/extract/summarize.ts
15171
+ var HANDOFF_PREFIX = `Another OpenCode session started by the same user was working on this task. It was compacted mid-conversation to save context space. Review the summary below to understand what happened and continue from where it left off.`;
15172
+ var STRUCTURED_COMPACTION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION.
15173
+ Create a structured handoff summary for an LLM that will resume the task.
15174
+
15175
+ SUMMARY SECTIONS (include all that apply):
15176
+
15177
+ ## Task Overview
15178
+ One sentence: what the user is building, fixing, or investigating.
15179
+
15180
+ ## Current Progress
15181
+ ### Completed
15182
+ - Specific changes made (file paths, functions, tests)
15183
+ - Commands executed and their outcomes
15184
+ ### In Progress
15185
+ - What was being worked on when compaction happened
15186
+ - Partial edits, unresolved questions
15187
+
15188
+ ## Key Technical Decisions
15189
+ - Decision \u2192 Reasoning (e.g., "used Map over Array for O(1) lookup")
15190
+ - Architecture choices and tradeoffs discussed
15191
+
15192
+ ## Constraints & Requirements
15193
+ - User preferences (coding style, libraries, patterns)
15194
+ - Explicit constraints (must not, always, never)
15195
+ - Environment details (OS, Node version, dependencies)
15196
+
15197
+ ## Files Modified or Touched
15198
+ - path/to/file \u2014 what changed and why
15199
+
15200
+ ## Errors Encountered & Fixes
15201
+ - Error message \u2192 Root cause \u2192 Fix applied
15202
+ - Unresolved errors that need attention
15203
+
15204
+ ## Next Steps
15205
+ - Clear, actionable items to continue the work
15206
+ - Dependencies: what must be done first
15207
+
15208
+ ## Critical Context
15209
+ - Specific values, API keys (do NOT include real secrets), configuration
15210
+ - User's exact phrasing when it matters
15211
+
15212
+ Be concise. Prefer structured lists over prose. Focus on what the next LLM NEEDS to know to continue seamlessly.
15213
+ `;
15214
+
15215
+ // src/hooks/compacting.ts
15169
15216
  function createCompactingHandler(args) {
15170
15217
  const { client, state, projectPath, logger, tracker } = args;
15171
15218
  return async (input, output) => {
@@ -15211,6 +15258,10 @@ function createCompactingHandler(args) {
15211
15258
  logger
15212
15259
  });
15213
15260
  state.setPendingEnrichment(sessionID);
15261
+ if (capture.messageCount >= 20) {
15262
+ output.prompt = STRUCTURED_COMPACTION_PROMPT;
15263
+ }
15264
+ output.context.push(HANDOFF_PREFIX);
15214
15265
  output.context.push(
15215
15266
  `Prior conversation archived to ${checkpointPath}`
15216
15267
  );
@@ -15233,12 +15284,10 @@ function createCompactingHandler(args) {
15233
15284
  }
15234
15285
 
15235
15286
  // src/compress/pressure.ts
15236
- var FALLBACK_MAX_CONTEXT = 128e3;
15287
+ var FALLBACK_MAX_CONTEXT = 1e6;
15237
15288
  var OPENCODE_COMPACTION_RATIO = 0.75;
15238
- var THRESHOLDS = {
15239
- medium: 0.2,
15240
- high: 0.4
15241
- };
15289
+ var PRESSURE_MEDIUM_TOKENS = 5e4;
15290
+ var PRESSURE_HIGH_TOKENS = 15e4;
15242
15291
  var calibratedMaxContext = 0;
15243
15292
  function calibrateFromCompaction(lastInputTokens) {
15244
15293
  if (lastInputTokens <= 0) return;
@@ -15252,67 +15301,33 @@ function maxContextFrom(modelContextWindow) {
15252
15301
  if (calibratedMaxContext > 0) return calibratedMaxContext;
15253
15302
  return FALLBACK_MAX_CONTEXT;
15254
15303
  }
15255
- function estimateTokens2(text) {
15256
- let cjk = 0;
15257
- let other = 0;
15258
- for (const ch of text) {
15259
- if (/[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef\u3040-\u309f\u30a0-\u30ff]/.test(ch)) {
15260
- cjk++;
15261
- } else {
15262
- other++;
15263
- }
15264
- }
15265
- return Math.ceil(cjk * 0.7 + other / 3.8);
15266
- }
15267
- function extractTokensFromMessages(messages) {
15268
- let total = 0;
15269
- for (const msg of messages) {
15270
- for (const part of msg.parts) {
15271
- if (typeof part !== "object" || part === null) continue;
15272
- const p = part;
15273
- if (p["type"] === "text" && typeof p["text"] === "string") {
15274
- total += estimateTokens2(p["text"]);
15275
- } else if (p["type"] === "tool") {
15276
- const state = p["state"];
15277
- if (state?.["output"] && typeof state["output"] === "string") {
15278
- total += estimateTokens2(state["output"]);
15279
- }
15280
- if (state?.["error"] && typeof state["error"] === "string") {
15281
- total += estimateTokens2(state["error"]);
15282
- }
15283
- } else if (p["type"] === "reasoning" || p["type"] === "thinking") {
15284
- if (typeof p["text"] === "string") {
15285
- total += estimateTokens2(p["text"]);
15286
- }
15287
- }
15288
- }
15289
- }
15290
- return total;
15291
- }
15292
15304
  function extractInputTokensFromMessages(messages) {
15305
+ let best = 0;
15293
15306
  for (let i = messages.length - 1; i >= 0; i--) {
15294
15307
  const msg = messages[i];
15295
15308
  for (const part of msg.parts) {
15296
15309
  if (typeof part !== "object" || part === null) continue;
15297
15310
  const p = part;
15298
15311
  if (p["type"] === "step-finish") {
15299
- const tokens = p.tokens;
15300
- if (tokens?.input && tokens.input > 0) {
15301
- return tokens.input;
15302
- }
15312
+ const tokens = p;
15313
+ const input = tokens.tokens?.input ?? 0;
15314
+ const cached2 = tokens.tokens?.cached ?? 0;
15315
+ const total = input + cached2;
15316
+ if (total > best) best = total;
15317
+ if (best > 0) return best;
15303
15318
  }
15304
15319
  }
15305
15320
  }
15306
- return 0;
15321
+ return best;
15307
15322
  }
15308
15323
  function detectPressure(messages, modelContextWindow) {
15309
15324
  const ctx = maxContextFrom(modelContextWindow || 0);
15310
15325
  const inputTokens = extractInputTokensFromMessages(messages);
15311
- const estimated = inputTokens > 0 ? inputTokens : extractTokensFromMessages(messages);
15326
+ const estimated = inputTokens > 0 ? inputTokens : 0;
15312
15327
  const ratio = Math.min(estimated / ctx, 1);
15313
15328
  let level;
15314
- if (ratio >= THRESHOLDS.high) level = "high";
15315
- else if (ratio >= THRESHOLDS.medium) level = "medium";
15329
+ if (estimated >= PRESSURE_HIGH_TOKENS) level = "high";
15330
+ else if (estimated >= PRESSURE_MEDIUM_TOKENS) level = "medium";
15316
15331
  else level = "low";
15317
15332
  return { level, ratio, estimatedTokens: estimated, maxContext: ctx };
15318
15333
  }
@@ -15576,6 +15591,8 @@ var PROTECTED_TOOLS = /* @__PURE__ */ new Set([
15576
15591
  var NEVER_DEDUP = /* @__PURE__ */ new Set(["read", "bash", "grep", "glob", "find", "search"]);
15577
15592
  var ERROR_PURGE_TURN_THRESHOLD = 4;
15578
15593
  var PROTECTED_HEAD_SINGLE = 2;
15594
+ var ASSISTANT_COMPRESS_MIN_LENGTH = 500;
15595
+ var ASSISTANT_COMPRESS_SAVINGS_RATIO = 0.6;
15579
15596
  function simpleHash(s) {
15580
15597
  const len = s.length;
15581
15598
  const sampleSize = 500;
@@ -15589,12 +15606,32 @@ function simpleHash(s) {
15589
15606
  }
15590
15607
  return `${len}:${h.toString(36)}`;
15591
15608
  }
15609
+ function compressAssistantText(text) {
15610
+ if (text.length < ASSISTANT_COMPRESS_MIN_LENGTH) return text;
15611
+ const lines = text.split("\n");
15612
+ const head = 3;
15613
+ const tail = 3;
15614
+ const kept = [];
15615
+ for (let i = 0; i < lines.length; i++) {
15616
+ const line = lines[i];
15617
+ if (i < head || i >= lines.length - tail) {
15618
+ kept.push(line);
15619
+ continue;
15620
+ }
15621
+ if (/^#{1,3}\s/.test(line) || /error|fail|warning|critical|important/i.test(line) || /^\s*[-*]\s/.test(line) || /^\s*\d+\.\s/.test(line) || line.trim().startsWith("```") || /^\/[^\s:]+/.test(line)) {
15622
+ kept.push(line);
15623
+ }
15624
+ }
15625
+ const result = kept.join("\n");
15626
+ return result.length < text.length * ASSISTANT_COMPRESS_SAVINGS_RATIO ? result : text;
15627
+ }
15592
15628
  function singlePassCompress(messages, state, protectedTail) {
15593
15629
  const stats = {
15594
15630
  toolDedup: 0,
15595
15631
  errorPurge: 0,
15596
15632
  toolOutputCompressed: 0,
15597
15633
  jsonCrushed: 0,
15634
+ assistantCompressed: 0,
15598
15635
  ccrStored: 0
15599
15636
  };
15600
15637
  const totalMessages = messages.length;
@@ -15674,6 +15711,21 @@ function singlePassCompress(messages, state, protectedTail) {
15674
15711
  }
15675
15712
  }
15676
15713
  }
15714
+ if (i < protectedTail && msg.info.role === "assistant") {
15715
+ for (let j = msg.parts.length - 1; j >= 0; j--) {
15716
+ const part = msg.parts[j];
15717
+ if (typeof part !== "object" || part === null) continue;
15718
+ const p = part;
15719
+ if (p["type"] !== "text") continue;
15720
+ const text = p["text"];
15721
+ if (typeof text !== "string") continue;
15722
+ const compressed = compressAssistantText(text);
15723
+ if (compressed !== text) {
15724
+ p["text"] = compressed;
15725
+ stats.assistantCompressed++;
15726
+ }
15727
+ }
15728
+ }
15677
15729
  }
15678
15730
  return stats;
15679
15731
  }
@@ -15715,6 +15767,7 @@ function runCompressionPipeline(ctx) {
15715
15767
  errorPurge: spStats.errorPurge,
15716
15768
  toolOutputCompressed: spStats.toolOutputCompressed,
15717
15769
  jsonCrushed: spStats.jsonCrushed,
15770
+ assistantCompressed: spStats.assistantCompressed,
15718
15771
  ccrStored: spStats.ccrStored,
15719
15772
  nudgeInjected: false,
15720
15773
  pressureLevel: pressure.level,
@@ -15735,7 +15788,7 @@ function runCompressionPipeline(ctx) {
15735
15788
  logger?.debug("compress: memory nudge", { type: memoryNudge.type });
15736
15789
  }
15737
15790
  }
15738
- const active = stats.toolDedup > 0 || stats.errorPurge > 0 || stats.toolOutputCompressed > 0 || stats.jsonCrushed > 0 || stats.nudgeInjected;
15791
+ const active = stats.toolDedup > 0 || stats.errorPurge > 0 || stats.toolOutputCompressed > 0 || stats.jsonCrushed > 0 || stats.assistantCompressed > 0 || stats.nudgeInjected;
15739
15792
  if (active) {
15740
15793
  logger?.debug("compress: pipeline result", { ...stats });
15741
15794
  } else {
@@ -15925,7 +15978,7 @@ function createMessagesTransformHandler(state, logger) {
15925
15978
  logger
15926
15979
  });
15927
15980
  const ds = pipelineResult.stats;
15928
- if (ds.toolDedup > 0 || ds.errorPurge > 0 || ds.toolOutputCompressed > 0 || ds.jsonCrushed > 0 || ds.nudgeInjected) {
15981
+ if (ds.toolDedup > 0 || ds.errorPurge > 0 || ds.toolOutputCompressed > 0 || ds.jsonCrushed > 0 || ds.assistantCompressed > 0 || ds.nudgeInjected) {
15929
15982
  logger?.debug("messages.transform: deep compression", { ...ds });
15930
15983
  state.mergeNotify({
15931
15984
  compression: stats,
@@ -16065,6 +16118,29 @@ function createNotifyHandler(client, logger) {
16065
16118
  };
16066
16119
  }
16067
16120
 
16121
+ // src/shared/model-limits.ts
16122
+ var KNOWN_MODEL_LIMITS = {
16123
+ "deepseek-v4-pro": 1e6,
16124
+ "deepseek-v4": 1e6,
16125
+ "deepseek-v3": 64e3,
16126
+ "deepseek-r1": 64e3,
16127
+ "claude-opus-4": 2e5,
16128
+ "claude-sonnet-4": 2e5,
16129
+ "gpt-4o": 128e3,
16130
+ "o1": 2e5,
16131
+ "o3-mini": 2e5,
16132
+ "gemini-2.5-pro": 1e6,
16133
+ "gemini-2.5-flash": 1e6,
16134
+ "qwen-max": 131072
16135
+ };
16136
+ function lookupModelLimit(modelID) {
16137
+ if (KNOWN_MODEL_LIMITS[modelID]) return KNOWN_MODEL_LIMITS[modelID];
16138
+ for (const [key, limit] of Object.entries(KNOWN_MODEL_LIMITS)) {
16139
+ if (modelID.includes(key)) return limit;
16140
+ }
16141
+ return void 0;
16142
+ }
16143
+
16068
16144
  // src/extract/enrich.ts
16069
16145
  import { stat } from "fs/promises";
16070
16146
 
@@ -16396,11 +16472,14 @@ var deepMemoryPlugin = async (input) => {
16396
16472
  const defaultModel = configResult.data?.model;
16397
16473
  if (typeof defaultModel === "string" && defaultModel.includes("/")) {
16398
16474
  const slashIdx = defaultModel.indexOf("/");
16399
- state.recordFallbackModel({
16400
- providerID: defaultModel.slice(0, slashIdx),
16401
- modelID: defaultModel.slice(slashIdx + 1)
16402
- });
16403
- logger.debug("resolved fallback model from config", { defaultModel });
16475
+ const providerID = defaultModel.slice(0, slashIdx);
16476
+ const modelID = defaultModel.slice(slashIdx + 1);
16477
+ state.recordFallbackModel({ providerID, modelID });
16478
+ const limit = lookupModelLimit(modelID);
16479
+ if (limit) {
16480
+ state.setModelContextWindow(limit);
16481
+ logger.debug("resolved model context window", { modelID, limit });
16482
+ }
16404
16483
  }
16405
16484
  }).catch((err) => {
16406
16485
  logger.debug("config.get failed, dream/distill will omit model", {