@bd7pil/opencode-deep-memory 0.4.4 → 0.5.1

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
@@ -260,7 +260,9 @@ var PluginState = class {
260
260
  _toolSignatures = /* @__PURE__ */ new Map();
261
261
  _ccrCache = /* @__PURE__ */ new Map();
262
262
  _lastInputTokens = 0;
263
- _lastNudgeMessageCount = 0;
263
+ _lastNudgeMessageCount = /* @__PURE__ */ new Map();
264
+ _lastCCRCleanup = 0;
265
+ _modelContextWindow = 0;
264
266
  agentOf(sessionID) {
265
267
  return this._agents.get(sessionID);
266
268
  }
@@ -271,6 +273,7 @@ var PluginState = class {
271
273
  this._agents.delete(sessionID);
272
274
  this._models.delete(sessionID);
273
275
  this._lastUserText.delete(sessionID);
276
+ this._lastNudgeMessageCount.delete(sessionID);
274
277
  }
275
278
  recordModel(sessionID, model) {
276
279
  this._models.set(sessionID, model);
@@ -391,12 +394,16 @@ var PluginState = class {
391
394
  }
392
395
  ccStore(hash2, entry) {
393
396
  const now = Date.now();
394
- for (const [k, v] of this._ccrCache) {
395
- if (now - v.createdAt > 3e5) this._ccrCache.delete(k);
396
- }
397
- if (this._ccrCache.size > 200) {
398
- const oldest = [...this._ccrCache.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt).slice(0, 50);
399
- for (const [k] of oldest) this._ccrCache.delete(k);
397
+ if (now - this._lastCCRCleanup > 3e4) {
398
+ for (const [k, v] of this._ccrCache) {
399
+ if (now - v.createdAt > 3e5) this._ccrCache.delete(k);
400
+ }
401
+ if (this._ccrCache.size > 200) {
402
+ const excess = this._ccrCache.size - 150;
403
+ const oldest = [...this._ccrCache.keys()].slice(0, excess);
404
+ for (const k of oldest) this._ccrCache.delete(k);
405
+ }
406
+ this._lastCCRCleanup = now;
400
407
  }
401
408
  this._ccrCache.set(hash2, entry);
402
409
  }
@@ -409,11 +416,18 @@ var PluginState = class {
409
416
  lastInputTokens() {
410
417
  return this._lastInputTokens;
411
418
  }
412
- recordNudge(messageCount) {
413
- this._lastNudgeMessageCount = messageCount;
419
+ recordNudge(sessionID, messageCount) {
420
+ this._lastNudgeMessageCount.set(sessionID, messageCount);
414
421
  }
415
- messagesSinceLastNudge(currentMessageCount) {
416
- return currentMessageCount - this._lastNudgeMessageCount;
422
+ messagesSinceLastNudge(sessionID, currentMessageCount) {
423
+ const last = this._lastNudgeMessageCount.get(sessionID);
424
+ return last != null ? currentMessageCount - last : Number.POSITIVE_INFINITY;
425
+ }
426
+ setModelContextWindow(tokens) {
427
+ if (tokens > 0) this._modelContextWindow = tokens;
428
+ }
429
+ getModelContextWindow() {
430
+ return this._modelContextWindow;
417
431
  }
418
432
  };
419
433
  function createPluginState() {
@@ -15228,12 +15242,16 @@ var THRESHOLDS = {
15228
15242
  var calibratedMaxContext = 0;
15229
15243
  function calibrateFromCompaction(lastInputTokens) {
15230
15244
  if (lastInputTokens <= 0) return;
15231
- const derived = Math.round(lastInputTokens / OPENCODE_COMPACTION_RATIO);
15232
- calibratedMaxContext = derived;
15245
+ calibratedMaxContext = Math.round(lastInputTokens / OPENCODE_COMPACTION_RATIO);
15233
15246
  }
15234
15247
  function getCalibratedMaxContext() {
15235
15248
  return calibratedMaxContext;
15236
15249
  }
15250
+ function maxContextFrom(modelContextWindow) {
15251
+ if (modelContextWindow > 0) return modelContextWindow;
15252
+ if (calibratedMaxContext > 0) return calibratedMaxContext;
15253
+ return FALLBACK_MAX_CONTEXT;
15254
+ }
15237
15255
  function estimateTokens2(text) {
15238
15256
  let cjk = 0;
15239
15257
  let other = 0;
@@ -15287,130 +15305,91 @@ function extractInputTokensFromMessages(messages) {
15287
15305
  }
15288
15306
  return 0;
15289
15307
  }
15290
- function detectPressure(messages) {
15291
- const maxContext = calibratedMaxContext || FALLBACK_MAX_CONTEXT;
15308
+ function detectPressure(messages, modelContextWindow) {
15309
+ const ctx = maxContextFrom(modelContextWindow || 0);
15292
15310
  const inputTokens = extractInputTokensFromMessages(messages);
15293
15311
  const estimated = inputTokens > 0 ? inputTokens : extractTokensFromMessages(messages);
15294
- const ratio = Math.min(estimated / maxContext, 1);
15312
+ const ratio = Math.min(estimated / ctx, 1);
15295
15313
  let level;
15296
15314
  if (ratio >= THRESHOLDS.high) level = "high";
15297
15315
  else if (ratio >= THRESHOLDS.medium) level = "medium";
15298
15316
  else level = "low";
15299
- return { level, ratio, estimatedTokens: estimated, maxContext };
15317
+ return { level, ratio, estimatedTokens: estimated, maxContext: ctx };
15300
15318
  }
15301
15319
 
15302
- // src/compress/dedup.ts
15303
- var PROTECTED_TOOLS = /* @__PURE__ */ new Set([
15304
- "question",
15305
- "edit",
15306
- "write",
15307
- "todowrite",
15308
- "todoread",
15309
- "memory_store",
15310
- "memory_search",
15311
- "memory_forget",
15312
- "memory_expand",
15313
- "deep_expand"
15314
- ]);
15315
- var NEVER_DEDUP = /* @__PURE__ */ new Set(["read", "bash", "grep", "glob", "find", "search"]);
15316
- var KEEP_RECENT = 8;
15317
- var PROTECTED_HEAD = 3;
15318
- function createToolSignature(tool5, args) {
15319
- if (!args) return tool5;
15320
- const sorted = Object.keys(args).sort().map((k) => `${k}:${JSON.stringify(args[k])}`).join(",");
15321
- return `${tool5}::${sorted}`;
15320
+ // src/compress/nudge.ts
15321
+ var NUDGE_COOLDOWN = 5;
15322
+ function shouldInjectNudge(level, messagesSinceLastNudge) {
15323
+ if (level !== "high") return false;
15324
+ if (messagesSinceLastNudge < NUDGE_COOLDOWN) return false;
15325
+ return true;
15322
15326
  }
15323
- function deduplicateToolOutputs(messages, state) {
15324
- let deduped = 0;
15325
- const totalMessages = messages.length;
15326
- if (totalMessages <= KEEP_RECENT + PROTECTED_HEAD) return 0;
15327
- const protectedTailStart = totalMessages - KEEP_RECENT;
15328
- const seen = /* @__PURE__ */ new Map();
15329
- for (let i = PROTECTED_HEAD; i < protectedTailStart; i++) {
15330
- const msg = messages[i];
15331
- for (const part of msg.parts) {
15332
- if (typeof part !== "object" || part === null) continue;
15333
- const p = part;
15334
- if (p["type"] !== "tool") continue;
15335
- const toolName = p["tool"];
15336
- const callID = p["callID"];
15337
- if (!toolName || !callID) continue;
15338
- if (PROTECTED_TOOLS.has(toolName)) continue;
15339
- if (NEVER_DEDUP.has(toolName)) continue;
15340
- const status = p["state"]?.["status"];
15341
- if (status !== "completed") continue;
15342
- const toolState = p["state"];
15343
- const output = toolState["output"];
15344
- if (typeof output !== "string") continue;
15345
- if (output === "[superseded by duplicate call]") continue;
15346
- if (output.includes("[ccr:")) continue;
15347
- const input = toolState["input"];
15348
- const signature = createToolSignature(toolName, input);
15349
- const outputHash = simpleHash(output);
15350
- const existing = seen.get(signature);
15351
- if (existing) {
15352
- if (existing.outputHash === outputHash) {
15353
- const prevMsg = messages[existing.msgIdx];
15354
- for (const prevPart of prevMsg.parts) {
15355
- if (typeof prevPart !== "object" || prevPart === null) continue;
15356
- const pp = prevPart;
15357
- if (pp["type"] !== "tool") continue;
15358
- const ppState = pp["state"];
15359
- if (ppState?.["output"] === "[superseded by duplicate call]") continue;
15360
- if (typeof ppState?.["output"] === "string" && simpleHash(ppState["output"]) === outputHash) {
15361
- ppState["output"] = "[superseded by duplicate call]";
15362
- deduped++;
15363
- }
15364
- }
15365
- }
15366
- seen.set(signature, { msgIdx: i, outputHash });
15367
- } else {
15368
- seen.set(signature, { msgIdx: i, outputHash });
15369
- }
15370
- }
15327
+ function buildNudgeText(level) {
15328
+ if (level === "high") {
15329
+ 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>';
15371
15330
  }
15372
- return deduped;
15331
+ return "";
15373
15332
  }
15374
- function simpleHash(s) {
15375
- const len = s.length;
15376
- const sampleSize = 500;
15377
- let h = len;
15378
- for (let i = 0; i < Math.min(len, sampleSize); i++) {
15379
- h = h * 31 + s.charCodeAt(i) | 0;
15333
+
15334
+ // src/compress/memory-nudge.ts
15335
+ var MEMORY_NUDGE_COOLDOWN = 3;
15336
+ var DECISION_PATTERNS = [
15337
+ /\b(?:decided|decision|chose|chosen|picked|selected)\b/i,
15338
+ /\b(?:采用|选择|决定|确定|选用)\b/,
15339
+ /\b(?:use|using|go with|went with)\b.*\b(?:because|since|due to)\b/i
15340
+ ];
15341
+ var CONSTRAINT_PATTERNS = [
15342
+ /\b(?:must not|cannot|should not|do not|never|always)\b/i,
15343
+ /\b(?:constraint|restriction|limitation|requirement)\b/i,
15344
+ /\b(?:不能|必须|禁止|约束|限制|要求|务必)\b/
15345
+ ];
15346
+ var ERROR_FIX_PATTERNS = [
15347
+ /\b(?:fix|fixed|resolve|resolved|patch|corrected)\b/i,
15348
+ /\b(?:修复|修复了|解决|解决了)\b/,
15349
+ /\b(?:the (?:bug|error|issue) (?:was|is)|root cause)\b/i
15350
+ ];
15351
+ function detectMemoryNudge(messages, messagesSinceLastNudge) {
15352
+ if (messagesSinceLastNudge < MEMORY_NUDGE_COOLDOWN) {
15353
+ return { injected: false, type: null };
15380
15354
  }
15381
- const tailStart = Math.max(sampleSize, len - sampleSize);
15382
- for (let i = tailStart; i < len; i++) {
15383
- h = h * 31 + s.charCodeAt(i) | 0;
15355
+ const protectedTail = Math.max(0, messages.length - 3);
15356
+ const recentMessages = messages.slice(protectedTail);
15357
+ const recentAssistantText = recentMessages.filter((m) => m.info.role === "assistant").flatMap((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text || "")).join("\n");
15358
+ const recentUserText = recentMessages.filter((m) => m.info.role === "user").flatMap((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text || "")).join("\n");
15359
+ const hasRecentToolError = recentMessages.some(
15360
+ (m) => m.parts.some((p) => p.type === "tool" && p.state?.status === "error")
15361
+ );
15362
+ if (hasRecentToolError && ERROR_FIX_PATTERNS.some((p) => p.test(recentAssistantText))) {
15363
+ return { injected: true, type: "gotcha" };
15384
15364
  }
15385
- return `${len}:${h.toString(36)}`;
15365
+ if (CONSTRAINT_PATTERNS.some((p) => p.test(recentUserText))) {
15366
+ return { injected: true, type: "constraint" };
15367
+ }
15368
+ if (DECISION_PATTERNS.some((p) => p.test(recentAssistantText))) {
15369
+ return { injected: true, type: "decision" };
15370
+ }
15371
+ return { injected: false, type: null };
15386
15372
  }
15387
-
15388
- // src/compress/error-purge.ts
15389
- var ERROR_PURGE_TURN_THRESHOLD = 4;
15390
- function purgeOldErrors(messages) {
15391
- let purged = 0;
15392
- const totalMessages = messages.length;
15393
- for (let i = 0; i < totalMessages; i++) {
15394
- const msg = messages[i];
15395
- for (const part of msg.parts) {
15396
- if (typeof part !== "object" || part === null) continue;
15397
- const p = part;
15398
- if (p["type"] !== "tool") continue;
15399
- const toolState = p["state"];
15400
- if (toolState?.["status"] !== "error") continue;
15401
- const age = totalMessages - i;
15402
- if (age < ERROR_PURGE_TURN_THRESHOLD) continue;
15403
- if (typeof toolState["input"] === "object" && toolState["input"] !== null) {
15404
- const input = toolState["input"];
15405
- for (const key of Object.keys(input)) {
15406
- if (key === "command" || key === "query" || key === "path" || key === "filePath") continue;
15407
- input[key] = "[purged]";
15408
- }
15409
- }
15410
- purged++;
15411
- }
15373
+ function buildMemoryNudge(type) {
15374
+ switch (type) {
15375
+ case "gotcha":
15376
+ return `
15377
+ <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>`;
15378
+ case "constraint":
15379
+ 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>';
15380
+ case "decision":
15381
+ return `
15382
+ <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>`;
15383
+ default:
15384
+ return "";
15412
15385
  }
15413
- return purged;
15386
+ }
15387
+
15388
+ // src/compress/dedup.ts
15389
+ function createToolSignature(tool5, args) {
15390
+ if (!args) return tool5;
15391
+ const sorted = Object.keys(args).sort().map((k) => `${k}:${JSON.stringify(args[k])}`).join(",");
15392
+ return `${tool5}::${sorted}`;
15414
15393
  }
15415
15394
 
15416
15395
  // src/compress/tool-compress.ts
@@ -15560,123 +15539,6 @@ function sha2562(data) {
15560
15539
  return createHash3("sha256").update(data).digest("hex");
15561
15540
  }
15562
15541
 
15563
- // src/compress/message-prune.ts
15564
- var PRUNE_THRESHOLD = 8;
15565
- function pruneOldMessages(messages) {
15566
- let pruned = 0;
15567
- const protectedTail = messages.length - PRUNE_THRESHOLD;
15568
- for (let i = 3; i < protectedTail; i++) {
15569
- const msg = messages[i];
15570
- if (msg.info.role !== "assistant") continue;
15571
- for (const part of msg.parts) {
15572
- if (typeof part !== "object" || part === null) continue;
15573
- const p = part;
15574
- if (p["type"] !== "text" || typeof p["text"] !== "string") continue;
15575
- const text = p["text"];
15576
- if (text.length < 500) continue;
15577
- if (text === "[cleared]" || text === "[stripped]") continue;
15578
- if (text.includes("[compressed from")) continue;
15579
- const keyInfo = extractKeyInfo(text);
15580
- if (keyInfo.length < text.length * 0.6) {
15581
- p["text"] = keyInfo + "\n[compressed from " + text.length + " chars]";
15582
- pruned++;
15583
- }
15584
- }
15585
- }
15586
- return pruned;
15587
- }
15588
- function extractKeyInfo(text) {
15589
- const lines = text.split("\n");
15590
- const keyLines = [];
15591
- let inCodeBlock = false;
15592
- for (const line of lines) {
15593
- if (line.trim().startsWith("```")) {
15594
- inCodeBlock = !inCodeBlock;
15595
- if (inCodeBlock) keyLines.push(line);
15596
- continue;
15597
- }
15598
- if (inCodeBlock) {
15599
- if (keyLines.length < 30 && line.trim()) keyLines.push(line);
15600
- continue;
15601
- }
15602
- if (/^#{1,3}\s/.test(line) || /error|fail|warning|important|critical|decision|constraint/i.test(line) || /^\s*[-*]\s/.test(line) || /^\s*\d+\.\s/.test(line)) {
15603
- keyLines.push(line);
15604
- }
15605
- }
15606
- if (keyLines.length < 3) {
15607
- return lines.slice(0, 5).join("\n");
15608
- }
15609
- return keyLines.join("\n");
15610
- }
15611
-
15612
- // src/compress/nudge.ts
15613
- var NUDGE_COOLDOWN = 5;
15614
- function shouldInjectNudge(level, messagesSinceLastNudge) {
15615
- if (level !== "high") return false;
15616
- if (messagesSinceLastNudge < NUDGE_COOLDOWN) return false;
15617
- return true;
15618
- }
15619
- function buildNudgeText(level) {
15620
- if (level === "high") {
15621
- 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>';
15622
- }
15623
- return "";
15624
- }
15625
-
15626
- // src/compress/memory-nudge.ts
15627
- var MEMORY_NUDGE_COOLDOWN = 3;
15628
- var DECISION_PATTERNS = [
15629
- /\b(?:decided|decision|chose|chosen|picked|selected)\b/i,
15630
- /\b(?:采用|选择|决定|确定|选用)\b/,
15631
- /\b(?:use|using|go with|went with)\b.*\b(?:because|since|due to)\b/i
15632
- ];
15633
- var CONSTRAINT_PATTERNS = [
15634
- /\b(?:must not|cannot|should not|do not|never|always)\b/i,
15635
- /\b(?:constraint|restriction|limitation|requirement)\b/i,
15636
- /\b(?:不能|必须|禁止|约束|限制|要求|务必)\b/
15637
- ];
15638
- var ERROR_FIX_PATTERNS = [
15639
- /\b(?:fix|fixed|resolve|resolved|patch|corrected)\b/i,
15640
- /\b(?:修复|修复了|解决|解决了)\b/,
15641
- /\b(?:the (?:bug|error|issue) (?:was|is)|root cause)\b/i
15642
- ];
15643
- function detectMemoryNudge(messages, messagesSinceLastNudge) {
15644
- if (messagesSinceLastNudge < MEMORY_NUDGE_COOLDOWN) {
15645
- return { injected: false, type: null };
15646
- }
15647
- const protectedTail = Math.max(0, messages.length - 3);
15648
- const recentMessages = messages.slice(protectedTail);
15649
- const recentAssistantText = recentMessages.filter((m) => m.info.role === "assistant").flatMap((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text || "")).join("\n");
15650
- const recentUserText = recentMessages.filter((m) => m.info.role === "user").flatMap((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text || "")).join("\n");
15651
- const hasRecentToolError = recentMessages.some(
15652
- (m) => m.parts.some((p) => p.type === "tool" && p.state?.status === "error")
15653
- );
15654
- if (hasRecentToolError && ERROR_FIX_PATTERNS.some((p) => p.test(recentAssistantText))) {
15655
- return { injected: true, type: "gotcha" };
15656
- }
15657
- if (CONSTRAINT_PATTERNS.some((p) => p.test(recentUserText))) {
15658
- return { injected: true, type: "constraint" };
15659
- }
15660
- if (DECISION_PATTERNS.some((p) => p.test(recentAssistantText))) {
15661
- return { injected: true, type: "decision" };
15662
- }
15663
- return { injected: false, type: null };
15664
- }
15665
- function buildMemoryNudge(type) {
15666
- switch (type) {
15667
- case "gotcha":
15668
- return `
15669
- <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>`;
15670
- case "constraint":
15671
- 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>';
15672
- case "decision":
15673
- return `
15674
- <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>`;
15675
- default:
15676
- return "";
15677
- }
15678
- }
15679
-
15680
15542
  // src/compress/detector.ts
15681
15543
  function detectContentType(content) {
15682
15544
  const trimmed = content.trimStart();
@@ -15699,44 +15561,181 @@ function detectContentType(content) {
15699
15561
  return "text";
15700
15562
  }
15701
15563
 
15702
- // src/compress/index.ts
15703
- function runCompressionPipeline(ctx) {
15704
- const { messages, state, logger } = ctx;
15705
- const pressure = detectPressure(messages);
15706
- state.recordInputTokens(pressure.estimatedTokens);
15564
+ // src/compress/single-pass.ts
15565
+ var PROTECTED_TOOLS = /* @__PURE__ */ new Set([
15566
+ "question",
15567
+ "edit",
15568
+ "write",
15569
+ "todowrite",
15570
+ "memory_store",
15571
+ "memory_search",
15572
+ "memory_forget",
15573
+ "memory_expand",
15574
+ "deep_expand"
15575
+ ]);
15576
+ var NEVER_DEDUP = /* @__PURE__ */ new Set(["read", "bash", "grep", "glob", "find", "search"]);
15577
+ var ERROR_PURGE_TURN_THRESHOLD = 4;
15578
+ var PROTECTED_HEAD = 2;
15579
+ function simpleHash(s) {
15580
+ const len = s.length;
15581
+ const sampleSize = 500;
15582
+ let h = len;
15583
+ for (let i = 0; i < Math.min(len, sampleSize); i++) {
15584
+ h = h * 31 + s.charCodeAt(i) | 0;
15585
+ }
15586
+ const tailStart = Math.max(sampleSize, len - sampleSize);
15587
+ for (let i = tailStart; i < len; i++) {
15588
+ h = h * 31 + s.charCodeAt(i) | 0;
15589
+ }
15590
+ return `${len}:${h.toString(36)}`;
15591
+ }
15592
+ function singlePassCompress(messages, state, protectedTail) {
15707
15593
  const stats = {
15708
15594
  toolDedup: 0,
15709
15595
  errorPurge: 0,
15710
15596
  toolOutputCompressed: 0,
15711
15597
  jsonCrushed: 0,
15712
- messagePruned: 0,
15713
- ccrStored: 0,
15598
+ ccrStored: 0
15599
+ };
15600
+ const totalMessages = messages.length;
15601
+ if (totalMessages <= PROTECTED_HEAD) return stats;
15602
+ const seen = /* @__PURE__ */ new Map();
15603
+ for (let i = PROTECTED_HEAD; i < totalMessages; i++) {
15604
+ const msg = messages[i];
15605
+ if (!msg?.parts?.length) continue;
15606
+ if (msg.info.role === "user") continue;
15607
+ for (let j = msg.parts.length - 1; j >= 0; j--) {
15608
+ const part = msg.parts[j];
15609
+ if (typeof part !== "object" || part === null) continue;
15610
+ const p = part;
15611
+ if (p["type"] !== "tool") continue;
15612
+ const toolName = p["tool"];
15613
+ const callID = p["callID"];
15614
+ const toolState = p["state"];
15615
+ if (toolState?.["status"] === "error") {
15616
+ const age = totalMessages - i;
15617
+ if (age >= ERROR_PURGE_TURN_THRESHOLD) {
15618
+ if (typeof toolState["input"] === "object" && toolState["input"] !== null) {
15619
+ const input = toolState["input"];
15620
+ for (const key of Object.keys(input)) {
15621
+ if (key === "command" || key === "query" || key === "path" || key === "filePath") continue;
15622
+ delete input[key];
15623
+ }
15624
+ }
15625
+ stats.errorPurge++;
15626
+ }
15627
+ }
15628
+ if (i >= protectedTail) continue;
15629
+ if (!toolName || !callID) continue;
15630
+ if (toolState?.["status"] !== "completed") continue;
15631
+ const output = typeof toolState?.["output"] === "string" ? toolState["output"] : void 0;
15632
+ if (!output) continue;
15633
+ if (output === "[superseded by duplicate call]") continue;
15634
+ if (output.includes("[ccr:")) continue;
15635
+ if (!PROTECTED_TOOLS.has(toolName) && !NEVER_DEDUP.has(toolName)) {
15636
+ const input = toolState["input"];
15637
+ const signature = createToolSignature(toolName, input);
15638
+ const outputHash = simpleHash(output);
15639
+ const existing = seen.get(signature);
15640
+ if (existing) {
15641
+ if (existing.outputHash === outputHash) {
15642
+ const prevMsg = messages[existing.msgIdx];
15643
+ for (const prevPart of prevMsg.parts) {
15644
+ if (typeof prevPart !== "object" || prevPart === null) continue;
15645
+ const pp = prevPart;
15646
+ const ppState = pp["state"];
15647
+ if (ppState?.["output"] === "[superseded by duplicate call]") continue;
15648
+ if (typeof ppState?.["output"] === "string" && simpleHash(ppState["output"]) === outputHash) {
15649
+ ppState["output"] = "[superseded by duplicate call]";
15650
+ stats.toolDedup++;
15651
+ }
15652
+ }
15653
+ }
15654
+ seen.set(signature, { msgIdx: i, outputHash });
15655
+ } else {
15656
+ seen.set(signature, { msgIdx: i, outputHash });
15657
+ }
15658
+ }
15659
+ if (output.length >= 500) {
15660
+ const result = compressToolOutput(toolName, output);
15661
+ if (result.length < output.length * 0.7) {
15662
+ const hash2 = ccrStore(state, output, result, toolName, callID);
15663
+ toolState["output"] = ccrInjectMarker(result, hash2);
15664
+ stats.toolOutputCompressed++;
15665
+ continue;
15666
+ }
15667
+ }
15668
+ if (output.length >= 500 && detectContentType(output) === "json") {
15669
+ const crushed = crushJsonArray(output);
15670
+ if (crushed.length < output.length * 0.7) {
15671
+ const hash2 = ccrStore(state, output, crushed, toolName, callID);
15672
+ toolState["output"] = ccrInjectMarker(crushed, hash2);
15673
+ stats.jsonCrushed++;
15674
+ }
15675
+ }
15676
+ }
15677
+ }
15678
+ return stats;
15679
+ }
15680
+
15681
+ // src/compress/index.ts
15682
+ var KEEP_RECENT_TOKENS = 4e3;
15683
+ function estimateMessageTokens(msg) {
15684
+ let t = 0;
15685
+ for (const part of msg.parts) {
15686
+ if (typeof part !== "object" || part === null) continue;
15687
+ const p = part;
15688
+ const text = typeof p["text"] === "string" ? p["text"] : "";
15689
+ if (p["type"] === "tool") {
15690
+ const s = p["state"];
15691
+ const out = typeof s?.["output"] === "string" ? s["output"] : "";
15692
+ t += Math.ceil((text.length + out.length) / 4);
15693
+ } else {
15694
+ t += Math.ceil(text.length / 4);
15695
+ }
15696
+ }
15697
+ return t;
15698
+ }
15699
+ function computeProtectedTail(messages) {
15700
+ let tokens = 0;
15701
+ for (let i = messages.length - 1; i >= 0; i--) {
15702
+ tokens += estimateMessageTokens(messages[i]);
15703
+ if (tokens >= KEEP_RECENT_TOKENS) return i;
15704
+ }
15705
+ return 0;
15706
+ }
15707
+ function runCompressionPipeline(ctx) {
15708
+ const { messages, state, sessionID, logger } = ctx;
15709
+ const pressure = detectPressure(messages, state.getModelContextWindow());
15710
+ state.recordInputTokens(pressure.estimatedTokens);
15711
+ const protectedTail = computeProtectedTail(messages);
15712
+ const spStats = singlePassCompress(messages, state, protectedTail);
15713
+ const stats = {
15714
+ toolDedup: spStats.toolDedup,
15715
+ errorPurge: spStats.errorPurge,
15716
+ toolOutputCompressed: spStats.toolOutputCompressed,
15717
+ jsonCrushed: spStats.jsonCrushed,
15718
+ ccrStored: spStats.ccrStored,
15714
15719
  nudgeInjected: false,
15715
15720
  pressureLevel: pressure.level,
15716
15721
  estimatedTokens: pressure.estimatedTokens
15717
15722
  };
15718
- stats.toolDedup = deduplicateToolOutputs(messages, state);
15719
- stats.errorPurge = purgeOldErrors(messages);
15720
- stats.jsonCrushed = crushJsonToolOutputs(messages, state);
15721
- stats.toolOutputCompressed = compressOldToolOutputs(messages, state);
15722
- if (pressure.level === "medium" || pressure.level === "high") {
15723
- stats.messagePruned = pruneOldMessages(messages);
15724
- }
15725
- const messagesSinceNudge = state.messagesSinceLastNudge(messages.length);
15723
+ const sid = sessionID || "default";
15724
+ const messagesSinceNudge = state.messagesSinceLastNudge(sid, messages.length);
15726
15725
  if (shouldInjectNudge(pressure.level, messagesSinceNudge)) {
15727
15726
  if (injectIntoLastAssistant(messages, buildNudgeText(pressure.level))) {
15728
15727
  stats.nudgeInjected = true;
15729
- state.recordNudge(messages.length);
15728
+ state.recordNudge(sid, messages.length);
15730
15729
  }
15731
15730
  }
15732
- const memoryNudge = detectMemoryNudge(messages, state.messagesSinceLastNudge(messages.length));
15731
+ const memoryNudge = detectMemoryNudge(messages, state.messagesSinceLastNudge(sid, messages.length));
15733
15732
  if (memoryNudge.injected) {
15734
15733
  if (injectIntoLastAssistant(messages, buildMemoryNudge(memoryNudge.type))) {
15735
- state.recordNudge(messages.length);
15734
+ state.recordNudge(sid, messages.length);
15736
15735
  logger?.debug("compress: memory nudge", { type: memoryNudge.type });
15737
15736
  }
15738
15737
  }
15739
- const active = stats.toolDedup > 0 || stats.errorPurge > 0 || stats.toolOutputCompressed > 0 || stats.jsonCrushed > 0 || stats.messagePruned > 0 || stats.nudgeInjected;
15738
+ const active = stats.toolDedup > 0 || stats.errorPurge > 0 || stats.toolOutputCompressed > 0 || stats.jsonCrushed > 0 || stats.nudgeInjected;
15740
15739
  if (active) {
15741
15740
  logger?.debug("compress: pipeline result", { ...stats });
15742
15741
  } else {
@@ -15748,70 +15747,19 @@ function injectIntoLastAssistant(messages, text) {
15748
15747
  for (let i = messages.length - 1; i >= 0; i--) {
15749
15748
  const msg = messages[i];
15750
15749
  if (msg.info.role !== "assistant") continue;
15751
- const textParts = msg.parts.filter(
15752
- (p) => typeof p === "object" && p !== null && p.type === "text"
15753
- );
15754
- const lastTextPart = textParts[textParts.length - 1];
15755
- if (lastTextPart && typeof lastTextPart.text === "string") {
15756
- lastTextPart.text += text;
15757
- return true;
15758
- }
15759
- }
15760
- return false;
15761
- }
15762
- function compressOldToolOutputs(messages, state) {
15763
- let compressed = 0;
15764
- const protectedTail = messages.length - 8;
15765
- for (let i = 3; i < protectedTail; i++) {
15766
- const msg = messages[i];
15767
- for (const part of msg.parts) {
15768
- if (typeof part !== "object" || part === null) continue;
15769
- const p = part;
15770
- if (p.type !== "tool") continue;
15771
- if (p.state?.status !== "completed") continue;
15772
- if (!p.state.output) continue;
15773
- if (p.state.output === "[superseded by duplicate call]") continue;
15774
- if (p.state.output.includes("[ccr:")) continue;
15775
- const toolName = p.tool || "unknown";
15776
- const output = p.state.output;
15777
- const result = compressToolOutput(toolName, output);
15778
- if (result.length < output.length * 0.7) {
15779
- const hash2 = ccrStore(state, output, result, toolName, p.callID);
15780
- p.state.output = ccrInjectMarker(result, hash2);
15781
- compressed++;
15782
- }
15783
- }
15784
- }
15785
- return compressed;
15786
- }
15787
- function crushJsonToolOutputs(messages, state) {
15788
- let crushed = 0;
15789
- const protectedTail = messages.length - 8;
15790
- for (let i = 3; i < protectedTail; i++) {
15791
- const msg = messages[i];
15792
- for (const part of msg.parts) {
15793
- if (typeof part !== "object" || part === null) continue;
15794
- const p = part;
15795
- if (p.type !== "tool") continue;
15796
- if (p.state?.status !== "completed") continue;
15797
- if (!p.state.output) continue;
15798
- if (p.state.output.startsWith("[superseded")) continue;
15799
- if (p.state.output.includes("[ccr:")) continue;
15800
- if (detectContentType(p.state.output) !== "json") continue;
15801
- const original = p.state.output;
15802
- const crushed_output = crushJsonArray(original);
15803
- if (crushed_output.length < original.length * 0.7) {
15804
- const hash2 = ccrStore(state, original, crushed_output, p.tool, p.callID);
15805
- p.state.output = ccrInjectMarker(crushed_output, hash2);
15806
- crushed++;
15750
+ for (let j = msg.parts.length - 1; j >= 0; j--) {
15751
+ const p = msg.parts[j];
15752
+ if (p["type"] === "text" && typeof p["text"] === "string") {
15753
+ p["text"] += text;
15754
+ return true;
15807
15755
  }
15808
15756
  }
15809
15757
  }
15810
- return crushed;
15758
+ return false;
15811
15759
  }
15812
15760
 
15813
15761
  // src/hooks/messages-transform.ts
15814
- var KEEP_RECENT2 = 8;
15762
+ var KEEP_RECENT = 8;
15815
15763
  var PROTECTED_HEAD2 = 3;
15816
15764
  var SYSTEM_INJECTION_PATTERNS = [
15817
15765
  /^$/,
@@ -15896,11 +15844,11 @@ function repairOrphanedToolCalls(messages) {
15896
15844
  }
15897
15845
  }
15898
15846
  function createMessagesTransformHandler(state, logger) {
15899
- return async (_input, output) => {
15847
+ return async (input, output) => {
15900
15848
  const messages = output.messages;
15901
- if (messages.length <= KEEP_RECENT2) return;
15902
- if (messages.length <= KEEP_RECENT2 + PROTECTED_HEAD2) return;
15903
- const protectedTailStart = messages.length - KEEP_RECENT2;
15849
+ if (messages.length <= KEEP_RECENT) return;
15850
+ if (messages.length <= KEEP_RECENT + PROTECTED_HEAD2) return;
15851
+ const protectedTailStart = messages.length - KEEP_RECENT;
15904
15852
  const stats = {
15905
15853
  reasoning_cleared: 0,
15906
15854
  metadata_stripped: 0,
@@ -15908,11 +15856,12 @@ function createMessagesTransformHandler(state, logger) {
15908
15856
  tool_errors_truncated: 0,
15909
15857
  thinking_stripped: 0
15910
15858
  };
15859
+ const toRemove = [];
15911
15860
  for (let i = PROTECTED_HEAD2; i < protectedTailStart; i++) {
15912
15861
  const msg = messages[i];
15913
15862
  if (!msg?.parts?.length) continue;
15914
15863
  if (msg.info.role === "user") continue;
15915
- for (let j = 0; j < msg.parts.length; j++) {
15864
+ for (let j = msg.parts.length - 1; j >= 0; j--) {
15916
15865
  const part = msg.parts[j];
15917
15866
  if (typeof part !== "object" || part === null) continue;
15918
15867
  const p = part;
@@ -15928,10 +15877,9 @@ function createMessagesTransformHandler(state, logger) {
15928
15877
  stats.metadata_stripped++;
15929
15878
  }
15930
15879
  }
15931
- if (typeof p["text"] === "string" && p["text"] !== "[cleared]") {
15932
- p["text"] = "[cleared]";
15933
- stats.reasoning_cleared++;
15934
- }
15880
+ msg.parts.splice(j, 1);
15881
+ stats.reasoning_cleared++;
15882
+ continue;
15935
15883
  }
15936
15884
  if (partType === "tool") {
15937
15885
  const meta = p["metadata"];
@@ -15959,11 +15907,13 @@ function createMessagesTransformHandler(state, logger) {
15959
15907
  }
15960
15908
  }
15961
15909
  if (isSystemInjected(msg)) {
15962
- msg.parts.length = 0;
15963
- msg.parts.push({ type: "text", text: "[stripped]" });
15910
+ toRemove.push(i);
15964
15911
  stats.system_neutralized++;
15965
15912
  }
15966
15913
  }
15914
+ for (let r = toRemove.length - 1; r >= 0; r--) {
15915
+ messages.splice(toRemove[r], 1);
15916
+ }
15967
15917
  repairOrphanedToolCalls(messages);
15968
15918
  if (Object.values(stats).some((v) => v > 0)) {
15969
15919
  logger?.debug("messages.transform: stripped", stats);
@@ -15971,24 +15921,25 @@ function createMessagesTransformHandler(state, logger) {
15971
15921
  const pipelineResult = runCompressionPipeline({
15972
15922
  messages: output.messages,
15973
15923
  state,
15924
+ sessionID: input["sessionID"],
15974
15925
  logger
15975
15926
  });
15976
15927
  const ds = pipelineResult.stats;
15977
- if (ds.toolDedup > 0 || ds.errorPurge > 0 || ds.toolOutputCompressed > 0 || ds.jsonCrushed > 0 || ds.messagePruned > 0 || ds.nudgeInjected) {
15928
+ if (ds.toolDedup > 0 || ds.errorPurge > 0 || ds.toolOutputCompressed > 0 || ds.jsonCrushed > 0 || ds.nudgeInjected) {
15978
15929
  logger?.debug("messages.transform: deep compression", { ...ds });
15979
15930
  state.mergeNotify({
15980
15931
  compression: stats,
15981
15932
  deepCompression: ds,
15982
15933
  messageCount: messages.length,
15983
15934
  protectedHead: PROTECTED_HEAD2,
15984
- protectedTail: KEEP_RECENT2
15935
+ protectedTail: KEEP_RECENT
15985
15936
  });
15986
15937
  } else if (Object.values(stats).some((v) => v > 0)) {
15987
15938
  state.mergeNotify({
15988
15939
  compression: stats,
15989
15940
  messageCount: messages.length,
15990
15941
  protectedHead: PROTECTED_HEAD2,
15991
- protectedTail: KEEP_RECENT2
15942
+ protectedTail: KEEP_RECENT
15992
15943
  });
15993
15944
  }
15994
15945
  };