@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 +281 -330
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
|
419
|
+
recordNudge(sessionID, messageCount) {
|
|
420
|
+
this._lastNudgeMessageCount.set(sessionID, messageCount);
|
|
414
421
|
}
|
|
415
|
-
messagesSinceLastNudge(currentMessageCount) {
|
|
416
|
-
|
|
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
|
-
|
|
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
|
|
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 /
|
|
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/
|
|
15303
|
-
var
|
|
15304
|
-
|
|
15305
|
-
"
|
|
15306
|
-
|
|
15307
|
-
|
|
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
|
|
15324
|
-
|
|
15325
|
-
|
|
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
|
|
15331
|
+
return "";
|
|
15373
15332
|
}
|
|
15374
|
-
|
|
15375
|
-
|
|
15376
|
-
|
|
15377
|
-
|
|
15378
|
-
|
|
15379
|
-
|
|
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
|
|
15382
|
-
|
|
15383
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15389
|
-
|
|
15390
|
-
|
|
15391
|
-
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
|
|
15398
|
-
|
|
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
|
-
|
|
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/
|
|
15703
|
-
|
|
15704
|
-
|
|
15705
|
-
|
|
15706
|
-
|
|
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
|
-
|
|
15713
|
-
|
|
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
|
-
|
|
15719
|
-
|
|
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.
|
|
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
|
-
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
15755
|
-
|
|
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
|
|
15758
|
+
return false;
|
|
15811
15759
|
}
|
|
15812
15760
|
|
|
15813
15761
|
// src/hooks/messages-transform.ts
|
|
15814
|
-
var
|
|
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 (
|
|
15847
|
+
return async (input, output) => {
|
|
15900
15848
|
const messages = output.messages;
|
|
15901
|
-
if (messages.length <=
|
|
15902
|
-
if (messages.length <=
|
|
15903
|
-
const protectedTailStart = messages.length -
|
|
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 =
|
|
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
|
-
|
|
15932
|
-
|
|
15933
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
15942
|
+
protectedTail: KEEP_RECENT
|
|
15992
15943
|
});
|
|
15993
15944
|
}
|
|
15994
15945
|
};
|