@bd7pil/opencode-deep-memory 0.3.6 → 0.4.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/README.md +81 -24
- package/dist/index.js +583 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -132,9 +132,9 @@ function checkpointRawPath(projectPath, _sessionID, _legacyDataRoot) {
|
|
|
132
132
|
return path2.join(projectMemoryDir(projectPath), "checkpoint.raw.json");
|
|
133
133
|
}
|
|
134
134
|
function hashProject(absProjectPath) {
|
|
135
|
-
const { createHash:
|
|
135
|
+
const { createHash: createHash4 } = __require("crypto");
|
|
136
136
|
const normalized = path2.resolve(absProjectPath);
|
|
137
|
-
return
|
|
137
|
+
return createHash4("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// src/shared/tokens.ts
|
|
@@ -257,6 +257,9 @@ var PluginState = class {
|
|
|
257
257
|
_pendingEnrichments = /* @__PURE__ */ new Set();
|
|
258
258
|
_lastUserText = /* @__PURE__ */ new Map();
|
|
259
259
|
_pendingNotify = null;
|
|
260
|
+
_toolSignatures = /* @__PURE__ */ new Map();
|
|
261
|
+
_ccrCache = /* @__PURE__ */ new Map();
|
|
262
|
+
_lastInputTokens = 0;
|
|
260
263
|
agentOf(sessionID) {
|
|
261
264
|
return this._agents.get(sessionID);
|
|
262
265
|
}
|
|
@@ -373,6 +376,34 @@ var PluginState = class {
|
|
|
373
376
|
this._pendingNotify = null;
|
|
374
377
|
return n;
|
|
375
378
|
}
|
|
379
|
+
recordToolSignature(callID, signature) {
|
|
380
|
+
this._toolSignatures.set(callID, signature);
|
|
381
|
+
}
|
|
382
|
+
isDuplicateTool(signature) {
|
|
383
|
+
for (const existing of this._toolSignatures.values()) {
|
|
384
|
+
if (existing === signature) return true;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
getToolSignature(callID) {
|
|
389
|
+
return this._toolSignatures.get(callID);
|
|
390
|
+
}
|
|
391
|
+
ccStore(hash2, entry) {
|
|
392
|
+
if (this._ccrCache.size > 200) {
|
|
393
|
+
const oldest = [...this._ccrCache.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt).slice(0, 50);
|
|
394
|
+
for (const [k] of oldest) this._ccrCache.delete(k);
|
|
395
|
+
}
|
|
396
|
+
this._ccrCache.set(hash2, entry);
|
|
397
|
+
}
|
|
398
|
+
ccrGet(hash2) {
|
|
399
|
+
return this._ccrCache.get(hash2);
|
|
400
|
+
}
|
|
401
|
+
recordInputTokens(tokens) {
|
|
402
|
+
this._lastInputTokens = tokens;
|
|
403
|
+
}
|
|
404
|
+
lastInputTokens() {
|
|
405
|
+
return this._lastInputTokens;
|
|
406
|
+
}
|
|
376
407
|
};
|
|
377
408
|
function createPluginState() {
|
|
378
409
|
return new PluginState();
|
|
@@ -2203,6 +2234,9 @@ var SearchService = class {
|
|
|
2203
2234
|
}
|
|
2204
2235
|
};
|
|
2205
2236
|
|
|
2237
|
+
// src/tools/index.ts
|
|
2238
|
+
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
2239
|
+
|
|
2206
2240
|
// src/tools/memory-search.ts
|
|
2207
2241
|
import { tool } from "@opencode-ai/plugin";
|
|
2208
2242
|
function createMemorySearchTool(service) {
|
|
@@ -14789,6 +14823,35 @@ ${part.thinking || part.text || "[empty]"}
|
|
|
14789
14823
|
return output;
|
|
14790
14824
|
}
|
|
14791
14825
|
|
|
14826
|
+
// src/compress/ccr.ts
|
|
14827
|
+
import { createHash as createHash2 } from "crypto";
|
|
14828
|
+
var CCR_TTL_MS = 5 * 60 * 1e3;
|
|
14829
|
+
function ccrStore(state, original, compressed, toolName, callID) {
|
|
14830
|
+
const hash2 = sha256(original).slice(0, 24);
|
|
14831
|
+
state.ccStore(hash2, {
|
|
14832
|
+
hash: hash2,
|
|
14833
|
+
original,
|
|
14834
|
+
compressed,
|
|
14835
|
+
createdAt: Date.now(),
|
|
14836
|
+
toolName,
|
|
14837
|
+
callID
|
|
14838
|
+
});
|
|
14839
|
+
return hash2;
|
|
14840
|
+
}
|
|
14841
|
+
function ccrRetrieve(state, hash2) {
|
|
14842
|
+
const entry = state.ccrGet(hash2);
|
|
14843
|
+
if (!entry) return void 0;
|
|
14844
|
+
if (Date.now() - entry.createdAt > CCR_TTL_MS) return void 0;
|
|
14845
|
+
return entry.original;
|
|
14846
|
+
}
|
|
14847
|
+
function ccrInjectMarker(compressed, hash2) {
|
|
14848
|
+
return `${compressed}
|
|
14849
|
+
[ccr:${hash2}]`;
|
|
14850
|
+
}
|
|
14851
|
+
function sha256(data) {
|
|
14852
|
+
return createHash2("sha256").update(data).digest("hex");
|
|
14853
|
+
}
|
|
14854
|
+
|
|
14792
14855
|
// src/tools/index.ts
|
|
14793
14856
|
function createMemoryTools(service, opts) {
|
|
14794
14857
|
const search = createMemorySearchTool(service);
|
|
@@ -14802,6 +14865,19 @@ function createMemoryTools(service, opts) {
|
|
|
14802
14865
|
memory_expand: expand
|
|
14803
14866
|
};
|
|
14804
14867
|
}
|
|
14868
|
+
function createDeepExpandTool(state) {
|
|
14869
|
+
return tool4({
|
|
14870
|
+
description: "Retrieve original content that was previously compressed. Use hash from [ccr:...] markers.",
|
|
14871
|
+
args: {
|
|
14872
|
+
hash: tool4.schema.string().describe("The hash from the [ccr:HASH] marker")
|
|
14873
|
+
},
|
|
14874
|
+
execute: async (args) => {
|
|
14875
|
+
const original = ccrRetrieve(state, args.hash);
|
|
14876
|
+
if (original) return { title: "Expanded content", output: original };
|
|
14877
|
+
return { title: "Not found", output: "Content expired or hash not found." };
|
|
14878
|
+
}
|
|
14879
|
+
});
|
|
14880
|
+
}
|
|
14805
14881
|
|
|
14806
14882
|
// src/extract/capture.ts
|
|
14807
14883
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -14960,17 +15036,17 @@ function extractFileChanges(messages) {
|
|
|
14960
15036
|
for (const msg of messages) {
|
|
14961
15037
|
for (const part of msg.parts) {
|
|
14962
15038
|
if (part.type !== "tool" || !part.tool) continue;
|
|
14963
|
-
const
|
|
14964
|
-
if (
|
|
15039
|
+
const tool5 = part.tool.toLowerCase();
|
|
15040
|
+
if (tool5 === "write" || tool5 === "edit") {
|
|
14965
15041
|
const filePath = part.args?.filePath || part.args?.path || "";
|
|
14966
15042
|
if (filePath) {
|
|
14967
|
-
const key = `${filePath}:${
|
|
15043
|
+
const key = `${filePath}:${tool5}`;
|
|
14968
15044
|
if (!seen.has(key)) {
|
|
14969
15045
|
seen.add(key);
|
|
14970
|
-
changes.push({ path: filePath, operation:
|
|
15046
|
+
changes.push({ path: filePath, operation: tool5 });
|
|
14971
15047
|
}
|
|
14972
15048
|
}
|
|
14973
|
-
} else if (
|
|
15049
|
+
} else if (tool5 === "bash" || tool5 === "execute") {
|
|
14974
15050
|
const cmd = part.args?.command || "";
|
|
14975
15051
|
const match = BASH_FILE_OP_RE.exec(cmd);
|
|
14976
15052
|
if (match?.[1]) {
|
|
@@ -15131,6 +15207,488 @@ function createCompactingHandler(args) {
|
|
|
15131
15207
|
};
|
|
15132
15208
|
}
|
|
15133
15209
|
|
|
15210
|
+
// src/compress/pressure.ts
|
|
15211
|
+
var DEFAULT_MAX_CONTEXT = 128e3;
|
|
15212
|
+
var THRESHOLDS = {
|
|
15213
|
+
low: 0.5,
|
|
15214
|
+
medium: 0.7,
|
|
15215
|
+
high: 0.85,
|
|
15216
|
+
critical: 0.95
|
|
15217
|
+
};
|
|
15218
|
+
var MODEL_CONTEXT_LIMITS = {
|
|
15219
|
+
"deepseek-chat": 64e3,
|
|
15220
|
+
"deepseek-reasoner": 64e3,
|
|
15221
|
+
"mimo-v2.5-pro": 128e3,
|
|
15222
|
+
"mimo-v2.5": 128e3,
|
|
15223
|
+
"claude-sonnet-4-20250514": 2e5,
|
|
15224
|
+
"gpt-4o": 128e3
|
|
15225
|
+
};
|
|
15226
|
+
function estimateTokens2(text) {
|
|
15227
|
+
let cjk = 0;
|
|
15228
|
+
let other = 0;
|
|
15229
|
+
for (const ch of text) {
|
|
15230
|
+
if (/[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef\u3040-\u309f\u30a0-\u30ff]/.test(ch)) {
|
|
15231
|
+
cjk++;
|
|
15232
|
+
} else {
|
|
15233
|
+
other++;
|
|
15234
|
+
}
|
|
15235
|
+
}
|
|
15236
|
+
return Math.ceil(cjk * 0.7 + other / 3.8);
|
|
15237
|
+
}
|
|
15238
|
+
function extractTokensFromMessages(messages) {
|
|
15239
|
+
let total = 0;
|
|
15240
|
+
for (const msg of messages) {
|
|
15241
|
+
for (const part of msg.parts) {
|
|
15242
|
+
if (typeof part !== "object" || part === null) continue;
|
|
15243
|
+
const p = part;
|
|
15244
|
+
if (p["type"] === "text" && typeof p["text"] === "string") {
|
|
15245
|
+
total += estimateTokens2(p["text"]);
|
|
15246
|
+
} else if (p["type"] === "tool") {
|
|
15247
|
+
const state = p["state"];
|
|
15248
|
+
if (state?.["output"] && typeof state["output"] === "string") {
|
|
15249
|
+
total += estimateTokens2(state["output"]);
|
|
15250
|
+
}
|
|
15251
|
+
if (state?.["error"] && typeof state["error"] === "string") {
|
|
15252
|
+
total += estimateTokens2(state["error"]);
|
|
15253
|
+
}
|
|
15254
|
+
} else if (p["type"] === "reasoning" || p["type"] === "thinking") {
|
|
15255
|
+
if (typeof p["text"] === "string") {
|
|
15256
|
+
total += estimateTokens2(p["text"]);
|
|
15257
|
+
}
|
|
15258
|
+
}
|
|
15259
|
+
}
|
|
15260
|
+
}
|
|
15261
|
+
return total;
|
|
15262
|
+
}
|
|
15263
|
+
function extractInputTokensFromMessages(messages) {
|
|
15264
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
15265
|
+
const msg = messages[i];
|
|
15266
|
+
for (const part of msg.parts) {
|
|
15267
|
+
if (typeof part !== "object" || part === null) continue;
|
|
15268
|
+
const p = part;
|
|
15269
|
+
if (p["type"] === "step-finish") {
|
|
15270
|
+
const tokens = p.tokens;
|
|
15271
|
+
if (tokens?.input && tokens.input > 0) {
|
|
15272
|
+
return tokens.input;
|
|
15273
|
+
}
|
|
15274
|
+
}
|
|
15275
|
+
}
|
|
15276
|
+
}
|
|
15277
|
+
return 0;
|
|
15278
|
+
}
|
|
15279
|
+
function detectPressure(messages, modelId) {
|
|
15280
|
+
const maxContext = (modelId ? MODEL_CONTEXT_LIMITS[modelId] : void 0) ?? DEFAULT_MAX_CONTEXT;
|
|
15281
|
+
const inputTokens = extractInputTokensFromMessages(messages);
|
|
15282
|
+
const estimated = inputTokens > 0 ? inputTokens : extractTokensFromMessages(messages);
|
|
15283
|
+
const ratio = Math.min(estimated / maxContext, 1);
|
|
15284
|
+
let level;
|
|
15285
|
+
if (ratio >= THRESHOLDS.critical) level = "critical";
|
|
15286
|
+
else if (ratio >= THRESHOLDS.high) level = "high";
|
|
15287
|
+
else if (ratio >= THRESHOLDS.medium) level = "medium";
|
|
15288
|
+
else level = "low";
|
|
15289
|
+
return { level, ratio, estimatedTokens: estimated };
|
|
15290
|
+
}
|
|
15291
|
+
|
|
15292
|
+
// src/compress/dedup.ts
|
|
15293
|
+
var PROTECTED_TOOLS = /* @__PURE__ */ new Set(["question", "edit", "write", "todowrite", "todoread", "memory_store", "memory_search", "memory_forget"]);
|
|
15294
|
+
function createToolSignature(tool5, args) {
|
|
15295
|
+
if (!args) return tool5;
|
|
15296
|
+
const sorted = Object.keys(args).sort().map((k) => `${k}:${JSON.stringify(args[k])}`).join(",");
|
|
15297
|
+
return `${tool5}::${sorted}`;
|
|
15298
|
+
}
|
|
15299
|
+
function deduplicateToolOutputs(messages, state) {
|
|
15300
|
+
let deduped = 0;
|
|
15301
|
+
const seen = /* @__PURE__ */ new Map();
|
|
15302
|
+
for (const msg of messages) {
|
|
15303
|
+
for (const part of msg.parts) {
|
|
15304
|
+
if (typeof part !== "object" || part === null) continue;
|
|
15305
|
+
const p = part;
|
|
15306
|
+
if (p["type"] !== "tool") continue;
|
|
15307
|
+
const toolName = p["tool"];
|
|
15308
|
+
const callID = p["callID"];
|
|
15309
|
+
if (!toolName || !callID) continue;
|
|
15310
|
+
if (PROTECTED_TOOLS.has(toolName)) continue;
|
|
15311
|
+
const status = p["state"]?.["status"];
|
|
15312
|
+
if (status !== "completed") continue;
|
|
15313
|
+
const toolState = p["state"];
|
|
15314
|
+
const input = toolState["input"];
|
|
15315
|
+
const signature = createToolSignature(toolName, input);
|
|
15316
|
+
const existing = seen.get(signature);
|
|
15317
|
+
if (existing && existing !== callID) {
|
|
15318
|
+
toolState["output"] = "[superseded by duplicate call]";
|
|
15319
|
+
state.recordToolSignature(callID, signature);
|
|
15320
|
+
deduped++;
|
|
15321
|
+
} else {
|
|
15322
|
+
seen.set(signature, callID);
|
|
15323
|
+
state.recordToolSignature(callID, signature);
|
|
15324
|
+
}
|
|
15325
|
+
}
|
|
15326
|
+
}
|
|
15327
|
+
return deduped;
|
|
15328
|
+
}
|
|
15329
|
+
|
|
15330
|
+
// src/compress/error-purge.ts
|
|
15331
|
+
var ERROR_PURGE_TURN_THRESHOLD = 4;
|
|
15332
|
+
function purgeOldErrors(messages) {
|
|
15333
|
+
let purged = 0;
|
|
15334
|
+
const totalMessages = messages.length;
|
|
15335
|
+
for (let i = 0; i < totalMessages; i++) {
|
|
15336
|
+
const msg = messages[i];
|
|
15337
|
+
for (const part of msg.parts) {
|
|
15338
|
+
if (typeof part !== "object" || part === null) continue;
|
|
15339
|
+
const p = part;
|
|
15340
|
+
if (p["type"] !== "tool") continue;
|
|
15341
|
+
const toolState = p["state"];
|
|
15342
|
+
if (toolState?.["status"] !== "error") continue;
|
|
15343
|
+
const age = totalMessages - i;
|
|
15344
|
+
if (age < ERROR_PURGE_TURN_THRESHOLD) continue;
|
|
15345
|
+
if (typeof toolState["input"] === "object" && toolState["input"] !== null) {
|
|
15346
|
+
const input = toolState["input"];
|
|
15347
|
+
for (const key of Object.keys(input)) {
|
|
15348
|
+
if (key === "command" || key === "query" || key === "path" || key === "filePath") continue;
|
|
15349
|
+
input[key] = "[purged]";
|
|
15350
|
+
}
|
|
15351
|
+
}
|
|
15352
|
+
purged++;
|
|
15353
|
+
}
|
|
15354
|
+
}
|
|
15355
|
+
return purged;
|
|
15356
|
+
}
|
|
15357
|
+
|
|
15358
|
+
// src/compress/tool-compress.ts
|
|
15359
|
+
var TOOL_COMPRESS_STRATEGIES = {
|
|
15360
|
+
read: compressFileRead,
|
|
15361
|
+
bash: compressBash,
|
|
15362
|
+
grep: compressSearchResults,
|
|
15363
|
+
glob: compressGlob,
|
|
15364
|
+
ripgrep: compressSearchResults,
|
|
15365
|
+
rg: compressSearchResults,
|
|
15366
|
+
find: compressGlob,
|
|
15367
|
+
search: compressSearchResults,
|
|
15368
|
+
grep_app_searchGitHub: compressSearchResults,
|
|
15369
|
+
searxng_searxng_web_search: compressSearchResults,
|
|
15370
|
+
websearch_web_search_exa: compressSearchResults,
|
|
15371
|
+
tavily_tavily_search: compressSearchResults
|
|
15372
|
+
};
|
|
15373
|
+
var DEFAULT_HEAD_LINES = 50;
|
|
15374
|
+
var DEFAULT_TAIL_LINES = 20;
|
|
15375
|
+
var MAX_LINE_LENGTH = 500;
|
|
15376
|
+
function compressToolOutput(toolName, output) {
|
|
15377
|
+
if (!output || output.length < 500) return output;
|
|
15378
|
+
const strategy = TOOL_COMPRESS_STRATEGIES[toolName];
|
|
15379
|
+
if (strategy) return strategy(output);
|
|
15380
|
+
return compressGeneric(output);
|
|
15381
|
+
}
|
|
15382
|
+
function compressFileRead(output) {
|
|
15383
|
+
const lines = output.split("\n");
|
|
15384
|
+
if (lines.length <= 100) return output;
|
|
15385
|
+
const head = lines.slice(0, DEFAULT_HEAD_LINES);
|
|
15386
|
+
const tail = lines.slice(-DEFAULT_TAIL_LINES);
|
|
15387
|
+
const keyLines = extractKeyLines(lines.slice(DEFAULT_HEAD_LINES, -DEFAULT_TAIL_LINES));
|
|
15388
|
+
const parts = [...head, "...[truncated]", ...keyLines.slice(0, 10), "...[truncated]", ...tail];
|
|
15389
|
+
return parts.join("\n");
|
|
15390
|
+
}
|
|
15391
|
+
function compressBash(output) {
|
|
15392
|
+
const lines = output.split("\n");
|
|
15393
|
+
if (lines.length <= 50) return output;
|
|
15394
|
+
const errorLines = lines.filter((l) => /error|fail|exception|fatal|panic/i.test(l)).slice(0, 5);
|
|
15395
|
+
const tail = lines.slice(-30);
|
|
15396
|
+
return [...errorLines, ...tail].join("\n");
|
|
15397
|
+
}
|
|
15398
|
+
function compressSearchResults(output) {
|
|
15399
|
+
const lines = output.split("\n");
|
|
15400
|
+
if (lines.length <= 30) return output;
|
|
15401
|
+
const grouped = groupByFile(lines);
|
|
15402
|
+
const result = [];
|
|
15403
|
+
let count = 0;
|
|
15404
|
+
for (const [file2, matches] of grouped) {
|
|
15405
|
+
if (count >= 20) break;
|
|
15406
|
+
result.push(`--- ${file2} ---`);
|
|
15407
|
+
const kept = matches.slice(0, 5);
|
|
15408
|
+
for (const m of kept) {
|
|
15409
|
+
result.push(truncateLine(m, MAX_LINE_LENGTH));
|
|
15410
|
+
count++;
|
|
15411
|
+
}
|
|
15412
|
+
if (matches.length > 5) result.push(` ...[${matches.length - 5} more matches]`);
|
|
15413
|
+
}
|
|
15414
|
+
if (count >= 20 && lines.length > 30) {
|
|
15415
|
+
result.push(`
|
|
15416
|
+
...[${lines.length - count} more lines truncated]`);
|
|
15417
|
+
}
|
|
15418
|
+
return result.join("\n");
|
|
15419
|
+
}
|
|
15420
|
+
function compressGlob(output) {
|
|
15421
|
+
const lines = output.split("\n").filter((l) => l.trim());
|
|
15422
|
+
if (lines.length <= 30) return output;
|
|
15423
|
+
const head = lines.slice(0, 30);
|
|
15424
|
+
return [...head, `
|
|
15425
|
+
...[${lines.length - 30} more files]`].join("\n");
|
|
15426
|
+
}
|
|
15427
|
+
function compressGeneric(output) {
|
|
15428
|
+
const lines = output.split("\n");
|
|
15429
|
+
if (lines.length <= 50) {
|
|
15430
|
+
if (output.length <= 2e3) return output;
|
|
15431
|
+
return output.slice(0, 1500) + "\n...[truncated]" + output.slice(-500);
|
|
15432
|
+
}
|
|
15433
|
+
const head = lines.slice(0, 30);
|
|
15434
|
+
const tail = lines.slice(-15);
|
|
15435
|
+
const errorLines = lines.filter((l) => /error|fail|exception|fatal/i.test(l)).slice(0, 5);
|
|
15436
|
+
return [...head, "...[truncated]", ...errorLines, "...[truncated]", ...tail].join("\n");
|
|
15437
|
+
}
|
|
15438
|
+
function extractKeyLines(lines) {
|
|
15439
|
+
return lines.filter(
|
|
15440
|
+
(l) => /\b(function |class |def |import |export |interface |type |const |let |var |return |throw |Error|Exception)\b/.test(l) || /error|warn|fail|exception/i.test(l)
|
|
15441
|
+
);
|
|
15442
|
+
}
|
|
15443
|
+
function groupByFile(lines) {
|
|
15444
|
+
const groups = /* @__PURE__ */ new Map();
|
|
15445
|
+
let currentFile = "unknown";
|
|
15446
|
+
for (const line of lines) {
|
|
15447
|
+
const fileMatch = line.match(/^(\/[^\s:]+):/);
|
|
15448
|
+
if (fileMatch) {
|
|
15449
|
+
currentFile = fileMatch[1];
|
|
15450
|
+
}
|
|
15451
|
+
if (!groups.has(currentFile)) groups.set(currentFile, []);
|
|
15452
|
+
groups.get(currentFile).push(line);
|
|
15453
|
+
}
|
|
15454
|
+
return groups;
|
|
15455
|
+
}
|
|
15456
|
+
function truncateLine(line, maxLen) {
|
|
15457
|
+
if (line.length <= maxLen) return line;
|
|
15458
|
+
return line.slice(0, maxLen - 15) + "...[truncated]";
|
|
15459
|
+
}
|
|
15460
|
+
|
|
15461
|
+
// src/compress/json-crush.ts
|
|
15462
|
+
import { createHash as createHash3 } from "crypto";
|
|
15463
|
+
function crushJsonArray(content, maxItems = 15) {
|
|
15464
|
+
try {
|
|
15465
|
+
const parsed = JSON.parse(content);
|
|
15466
|
+
if (!Array.isArray(parsed)) return content;
|
|
15467
|
+
if (parsed.length <= maxItems) return content;
|
|
15468
|
+
const firstFraction = 0.3;
|
|
15469
|
+
const lastFraction = 0.15;
|
|
15470
|
+
const firstCount = Math.max(1, Math.floor(maxItems * firstFraction));
|
|
15471
|
+
const lastCount = Math.max(1, Math.floor(maxItems * lastFraction));
|
|
15472
|
+
const midCount = maxItems - firstCount - lastCount;
|
|
15473
|
+
const first = parsed.slice(0, firstCount);
|
|
15474
|
+
const last = parsed.slice(-lastCount);
|
|
15475
|
+
const mid = deduplicateMiddle(parsed.slice(firstCount, -lastCount), midCount);
|
|
15476
|
+
const result = [...first, ...mid, ...last];
|
|
15477
|
+
const dropped = parsed.length - result.length;
|
|
15478
|
+
if (dropped > 0) {
|
|
15479
|
+
const hash2 = sha2562(content).slice(0, 12);
|
|
15480
|
+
result.push({ _ccr_dropped: `[${dropped} items offloaded, hash=${hash2}]` });
|
|
15481
|
+
}
|
|
15482
|
+
return JSON.stringify(result, null, 2);
|
|
15483
|
+
} catch {
|
|
15484
|
+
return content;
|
|
15485
|
+
}
|
|
15486
|
+
}
|
|
15487
|
+
function deduplicateMiddle(items, maxCount) {
|
|
15488
|
+
if (items.length <= maxCount) return items;
|
|
15489
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15490
|
+
const unique = [];
|
|
15491
|
+
for (const item of items) {
|
|
15492
|
+
const key = typeof item === "object" ? JSON.stringify(item) : String(item);
|
|
15493
|
+
if (!seen.has(key)) {
|
|
15494
|
+
seen.add(key);
|
|
15495
|
+
unique.push(item);
|
|
15496
|
+
if (unique.length >= maxCount) break;
|
|
15497
|
+
}
|
|
15498
|
+
}
|
|
15499
|
+
return unique;
|
|
15500
|
+
}
|
|
15501
|
+
function sha2562(data) {
|
|
15502
|
+
return createHash3("sha256").update(data).digest("hex");
|
|
15503
|
+
}
|
|
15504
|
+
|
|
15505
|
+
// src/compress/message-prune.ts
|
|
15506
|
+
var PRUNE_THRESHOLD = 8;
|
|
15507
|
+
function pruneOldMessages(messages) {
|
|
15508
|
+
let pruned = 0;
|
|
15509
|
+
const protectedTail = messages.length - PRUNE_THRESHOLD;
|
|
15510
|
+
for (let i = 3; i < protectedTail; i++) {
|
|
15511
|
+
const msg = messages[i];
|
|
15512
|
+
if (msg.info.role !== "assistant") continue;
|
|
15513
|
+
for (const part of msg.parts) {
|
|
15514
|
+
if (typeof part !== "object" || part === null) continue;
|
|
15515
|
+
const p = part;
|
|
15516
|
+
if (p["type"] !== "text" || typeof p["text"] !== "string") continue;
|
|
15517
|
+
const text = p["text"];
|
|
15518
|
+
if (text.length < 500) continue;
|
|
15519
|
+
if (text === "[cleared]" || text === "[stripped]" || text.startsWith("[compressed")) continue;
|
|
15520
|
+
const keyInfo = extractKeyInfo(text);
|
|
15521
|
+
if (keyInfo.length < text.length * 0.6) {
|
|
15522
|
+
p["text"] = keyInfo + "\n[compressed from " + text.length + " chars]";
|
|
15523
|
+
pruned++;
|
|
15524
|
+
}
|
|
15525
|
+
}
|
|
15526
|
+
}
|
|
15527
|
+
return pruned;
|
|
15528
|
+
}
|
|
15529
|
+
function extractKeyInfo(text) {
|
|
15530
|
+
const lines = text.split("\n");
|
|
15531
|
+
const keyLines = [];
|
|
15532
|
+
let inCodeBlock = false;
|
|
15533
|
+
for (const line of lines) {
|
|
15534
|
+
if (line.trim().startsWith("```")) {
|
|
15535
|
+
inCodeBlock = !inCodeBlock;
|
|
15536
|
+
if (inCodeBlock) keyLines.push(line);
|
|
15537
|
+
continue;
|
|
15538
|
+
}
|
|
15539
|
+
if (inCodeBlock) {
|
|
15540
|
+
if (keyLines.length < 30 && line.trim()) keyLines.push(line);
|
|
15541
|
+
continue;
|
|
15542
|
+
}
|
|
15543
|
+
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)) {
|
|
15544
|
+
keyLines.push(line);
|
|
15545
|
+
}
|
|
15546
|
+
}
|
|
15547
|
+
if (keyLines.length < 3) {
|
|
15548
|
+
return lines.slice(0, 5).join("\n");
|
|
15549
|
+
}
|
|
15550
|
+
return keyLines.join("\n");
|
|
15551
|
+
}
|
|
15552
|
+
|
|
15553
|
+
// src/compress/nudge.ts
|
|
15554
|
+
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;
|
|
15558
|
+
return true;
|
|
15559
|
+
}
|
|
15560
|
+
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>';
|
|
15563
|
+
}
|
|
15564
|
+
return '\n<dm-nudge level="high">Context is getting large. Consider compressing old tool outputs and messages to free space.</dm-nudge>';
|
|
15565
|
+
}
|
|
15566
|
+
|
|
15567
|
+
// src/compress/detector.ts
|
|
15568
|
+
function detectContentType(content) {
|
|
15569
|
+
const trimmed = content.trimStart();
|
|
15570
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
15571
|
+
try {
|
|
15572
|
+
JSON.parse(content);
|
|
15573
|
+
return "json";
|
|
15574
|
+
} catch {
|
|
15575
|
+
}
|
|
15576
|
+
}
|
|
15577
|
+
if (/^diff --git |^@@ -\d+,\d+ \+\d+,\d+ @@|^[+-]{3} \//m.test(content)) return "diff";
|
|
15578
|
+
if (/Traceback \(most recent call last\)|at \S+\.\S+\(|Error: |Exception: |TypeError: |ReferenceError: /m.test(content)) return "error-trace";
|
|
15579
|
+
if (/<[a-z][\s\S]*>/i.test(content) && /<(html|div|span|body|head|script|style)[\s>]/i.test(content)) return "html";
|
|
15580
|
+
const lines = content.split("\n");
|
|
15581
|
+
const logLineCount = lines.filter((l) => /^\d{4}-\d{2}-\d{2}|^\[\d{4}|ERROR|WARN|INFO|DEBUG|FATAL|TRACE/.test(l)).length;
|
|
15582
|
+
if (lines.length > 5 && logLineCount / lines.length > 0.3) return "log";
|
|
15583
|
+
const codePatterns = /\b(function |class |def |import |from .+ import|const |let |var |export |interface |type |struct |fn |func |pub |private |protected )\b/;
|
|
15584
|
+
const codeLines = lines.filter((l) => codePatterns.test(l)).length;
|
|
15585
|
+
if (lines.length > 10 && codeLines / lines.length > 0.15) return "code";
|
|
15586
|
+
return "text";
|
|
15587
|
+
}
|
|
15588
|
+
|
|
15589
|
+
// src/compress/index.ts
|
|
15590
|
+
function runCompressionPipeline(ctx) {
|
|
15591
|
+
const { messages, state, logger } = ctx;
|
|
15592
|
+
const pressure = detectPressure(messages, ctx.modelId);
|
|
15593
|
+
state.recordInputTokens(pressure.estimatedTokens);
|
|
15594
|
+
const stats = {
|
|
15595
|
+
toolDedup: 0,
|
|
15596
|
+
errorPurge: 0,
|
|
15597
|
+
toolOutputCompressed: 0,
|
|
15598
|
+
jsonCrushed: 0,
|
|
15599
|
+
messagePruned: 0,
|
|
15600
|
+
ccrStored: 0,
|
|
15601
|
+
nudgeInjected: false,
|
|
15602
|
+
pressureLevel: pressure.level,
|
|
15603
|
+
estimatedTokens: pressure.estimatedTokens
|
|
15604
|
+
};
|
|
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);
|
|
15621
|
+
stats.messagePruned = pruneOldMessages(messages);
|
|
15622
|
+
}
|
|
15623
|
+
if (shouldInjectNudge(pressure.level, messages.length, 0)) {
|
|
15624
|
+
const lastMsg = messages[messages.length - 1];
|
|
15625
|
+
if (lastMsg) {
|
|
15626
|
+
const textParts = lastMsg.parts.filter(
|
|
15627
|
+
(p) => typeof p === "object" && p !== null && p.type === "text"
|
|
15628
|
+
);
|
|
15629
|
+
const lastTextPart = textParts[textParts.length - 1];
|
|
15630
|
+
if (lastTextPart && typeof lastTextPart.text === "string") {
|
|
15631
|
+
lastTextPart.text += buildNudgeText(pressure.level);
|
|
15632
|
+
stats.nudgeInjected = true;
|
|
15633
|
+
}
|
|
15634
|
+
}
|
|
15635
|
+
}
|
|
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 });
|
|
15638
|
+
}
|
|
15639
|
+
return { stats };
|
|
15640
|
+
}
|
|
15641
|
+
function compressOldToolOutputs(messages, state) {
|
|
15642
|
+
let compressed = 0;
|
|
15643
|
+
const protectedTail = messages.length - 8;
|
|
15644
|
+
for (let i = 3; i < protectedTail; i++) {
|
|
15645
|
+
const msg = messages[i];
|
|
15646
|
+
for (const part of msg.parts) {
|
|
15647
|
+
if (typeof part !== "object" || part === null) continue;
|
|
15648
|
+
const p = part;
|
|
15649
|
+
if (p.type !== "tool") continue;
|
|
15650
|
+
if (p.state?.status !== "completed") continue;
|
|
15651
|
+
if (!p.state.output) continue;
|
|
15652
|
+
if (p.state.output === "[superseded by duplicate call]") continue;
|
|
15653
|
+
if (p.state.output.startsWith("[compressed")) continue;
|
|
15654
|
+
const toolName = p.tool || "unknown";
|
|
15655
|
+
const output = p.state.output;
|
|
15656
|
+
const result = compressToolOutput(toolName, output);
|
|
15657
|
+
if (result.length < output.length * 0.7) {
|
|
15658
|
+
const hash2 = ccrStore(state, output, result, toolName, p.callID);
|
|
15659
|
+
p.state.output = ccrInjectMarker(result, hash2);
|
|
15660
|
+
compressed++;
|
|
15661
|
+
}
|
|
15662
|
+
}
|
|
15663
|
+
}
|
|
15664
|
+
return compressed;
|
|
15665
|
+
}
|
|
15666
|
+
function crushJsonToolOutputs(messages, state) {
|
|
15667
|
+
let crushed = 0;
|
|
15668
|
+
const protectedTail = messages.length - 8;
|
|
15669
|
+
for (let i = 3; i < protectedTail; i++) {
|
|
15670
|
+
const msg = messages[i];
|
|
15671
|
+
for (const part of msg.parts) {
|
|
15672
|
+
if (typeof part !== "object" || part === null) continue;
|
|
15673
|
+
const p = part;
|
|
15674
|
+
if (p.type !== "tool") continue;
|
|
15675
|
+
if (p.state?.status !== "completed") continue;
|
|
15676
|
+
if (!p.state.output) continue;
|
|
15677
|
+
if (p.state.output.startsWith("[compressed")) continue;
|
|
15678
|
+
if (p.state.output.startsWith("[superseded")) continue;
|
|
15679
|
+
if (detectContentType(p.state.output) !== "json") continue;
|
|
15680
|
+
const original = p.state.output;
|
|
15681
|
+
const crushed_output = crushJsonArray(original);
|
|
15682
|
+
if (crushed_output.length < original.length * 0.7) {
|
|
15683
|
+
const hash2 = ccrStore(state, original, crushed_output, p.tool, p.callID);
|
|
15684
|
+
p.state.output = ccrInjectMarker(crushed_output, hash2);
|
|
15685
|
+
crushed++;
|
|
15686
|
+
}
|
|
15687
|
+
}
|
|
15688
|
+
}
|
|
15689
|
+
return crushed;
|
|
15690
|
+
}
|
|
15691
|
+
|
|
15134
15692
|
// src/hooks/messages-transform.ts
|
|
15135
15693
|
var KEEP_RECENT = 8;
|
|
15136
15694
|
var PROTECTED_HEAD = 3;
|
|
@@ -15288,6 +15846,23 @@ function createMessagesTransformHandler(state, logger) {
|
|
|
15288
15846
|
repairOrphanedToolCalls(messages);
|
|
15289
15847
|
if (Object.values(stats).some((v) => v > 0)) {
|
|
15290
15848
|
logger?.debug("messages.transform: stripped", stats);
|
|
15849
|
+
}
|
|
15850
|
+
const pipelineResult = runCompressionPipeline({
|
|
15851
|
+
messages: output.messages,
|
|
15852
|
+
state,
|
|
15853
|
+
logger
|
|
15854
|
+
});
|
|
15855
|
+
const ds = pipelineResult.stats;
|
|
15856
|
+
if (ds.toolDedup > 0 || ds.errorPurge > 0 || ds.toolOutputCompressed > 0 || ds.jsonCrushed > 0 || ds.messagePruned > 0 || ds.nudgeInjected) {
|
|
15857
|
+
logger?.debug("messages.transform: deep compression", { ...ds });
|
|
15858
|
+
state.mergeNotify({
|
|
15859
|
+
compression: stats,
|
|
15860
|
+
deepCompression: ds,
|
|
15861
|
+
messageCount: messages.length,
|
|
15862
|
+
protectedHead: PROTECTED_HEAD,
|
|
15863
|
+
protectedTail: KEEP_RECENT
|
|
15864
|
+
});
|
|
15865
|
+
} else if (Object.values(stats).some((v) => v > 0)) {
|
|
15291
15866
|
state.mergeNotify({
|
|
15292
15867
|
compression: stats,
|
|
15293
15868
|
messageCount: messages.length,
|
|
@@ -15903,7 +16478,7 @@ var deepMemoryPlugin = async (input) => {
|
|
|
15903
16478
|
});
|
|
15904
16479
|
}
|
|
15905
16480
|
},
|
|
15906
|
-
tool: memoryTools,
|
|
16481
|
+
tool: { ...memoryTools, deep_expand: createDeepExpandTool(state) },
|
|
15907
16482
|
"tool.execute.after": async (input2, output) => {
|
|
15908
16483
|
if (input2.tool !== "read") return;
|
|
15909
16484
|
const filePath = input2.args?.path ?? input2.args?.filePath;
|