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