opencode-acp 1.18.4 → 1.18.5-pr.490.223
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 +7 -0
- package/README.zh-CN.md +5 -0
- package/dist/index.js +819 -388
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/image-archive.d.ts +2 -0
- package/dist/lib/compress/image-archive.d.ts.map +1 -1
- package/dist/lib/compress/image-tokens.d.ts +68 -0
- package/dist/lib/compress/image-tokens.d.ts.map +1 -0
- package/dist/lib/compress/status.d.ts.map +1 -1
- package/dist/lib/config-validation.d.ts.map +1 -1
- package/dist/lib/config.d.ts +25 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.d.ts +2 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -1
- package/dist/lib/messages/large-images.d.ts +35 -0
- package/dist/lib/messages/large-images.d.ts.map +1 -0
- package/dist/lib/messages/sticky-truncation.d.ts +55 -0
- package/dist/lib/messages/sticky-truncation.d.ts.map +1 -0
- package/dist/lib/messages/truncate-tools.d.ts.map +1 -1
- package/dist/lib/prompts/extensions/system.d.ts +6 -0
- package/dist/lib/prompts/extensions/system.d.ts.map +1 -1
- package/dist/lib/prompts/index.d.ts +1 -1
- package/dist/lib/prompts/index.d.ts.map +1 -1
- package/dist/lib/state/persistence.d.ts +4 -0
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/state/types.d.ts +17 -0
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/token-utils.d.ts +14 -0
- package/dist/lib/token-utils.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -920,7 +920,10 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
920
920
|
"compress.images.enabled",
|
|
921
921
|
"compress.images.minBytes",
|
|
922
922
|
"compress.images.ttlDays",
|
|
923
|
+
"compress.images.largeBytes",
|
|
924
|
+
"compress.images.nudge",
|
|
923
925
|
"compress.completionReserveTokens",
|
|
926
|
+
"compress.emergencyTruncation",
|
|
924
927
|
"gc",
|
|
925
928
|
"gc.algorithm",
|
|
926
929
|
"gc.promotionThreshold",
|
|
@@ -1316,7 +1319,7 @@ function validateConfigTypes(config) {
|
|
|
1316
1319
|
if (typeof compress.images !== "object" || compress.images === null || Array.isArray(compress.images)) {
|
|
1317
1320
|
errors.push({
|
|
1318
1321
|
key: "compress.images",
|
|
1319
|
-
expected: "object { enabled?: boolean; minBytes?: integer (>= 0); ttlDays?: number (> 0) }",
|
|
1322
|
+
expected: "object { enabled?: boolean; minBytes?: integer (>= 0); ttlDays?: number (> 0); largeBytes?: integer (>= 0); nudge?: boolean }",
|
|
1320
1323
|
actual: JSON.stringify(compress.images)
|
|
1321
1324
|
});
|
|
1322
1325
|
} else {
|
|
@@ -1341,6 +1344,20 @@ function validateConfigTypes(config) {
|
|
|
1341
1344
|
actual: JSON.stringify(compress.images.ttlDays)
|
|
1342
1345
|
});
|
|
1343
1346
|
}
|
|
1347
|
+
if (compress.images.largeBytes !== void 0 && (typeof compress.images.largeBytes !== "number" || !Number.isInteger(compress.images.largeBytes) || compress.images.largeBytes < 0)) {
|
|
1348
|
+
errors.push({
|
|
1349
|
+
key: "compress.images.largeBytes",
|
|
1350
|
+
expected: "integer (>= 0)",
|
|
1351
|
+
actual: JSON.stringify(compress.images.largeBytes)
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
if (compress.images.nudge !== void 0 && typeof compress.images.nudge !== "boolean") {
|
|
1355
|
+
errors.push({
|
|
1356
|
+
key: "compress.images.nudge",
|
|
1357
|
+
expected: "boolean",
|
|
1358
|
+
actual: typeof compress.images.nudge
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1344
1361
|
}
|
|
1345
1362
|
}
|
|
1346
1363
|
if (compress.completionReserveTokens !== void 0 && typeof compress.completionReserveTokens !== "number") {
|
|
@@ -1350,6 +1367,13 @@ function validateConfigTypes(config) {
|
|
|
1350
1367
|
actual: typeof compress.completionReserveTokens
|
|
1351
1368
|
});
|
|
1352
1369
|
}
|
|
1370
|
+
if (compress.emergencyTruncation !== void 0 && typeof compress.emergencyTruncation !== "boolean") {
|
|
1371
|
+
errors.push({
|
|
1372
|
+
key: "compress.emergencyTruncation",
|
|
1373
|
+
expected: "boolean",
|
|
1374
|
+
actual: typeof compress.emergencyTruncation
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1353
1377
|
if (typeof compress.completionReserveTokens === "number" && compress.completionReserveTokens < 0) {
|
|
1354
1378
|
errors.push({
|
|
1355
1379
|
key: "compress.completionReserveTokens",
|
|
@@ -1422,6 +1446,7 @@ function validateConfigTypes(config) {
|
|
|
1422
1446
|
const OVERRIDE_FIELD_TYPES = {
|
|
1423
1447
|
showCompression: "boolean",
|
|
1424
1448
|
summaryBuffer: "boolean",
|
|
1449
|
+
emergencyTruncation: "boolean",
|
|
1425
1450
|
protectTags: "boolean",
|
|
1426
1451
|
protectUserMessages: "boolean",
|
|
1427
1452
|
lastSegmentSoftBlock: "boolean",
|
|
@@ -1733,7 +1758,9 @@ var DEFAULT_COMPRESS_REASONING = {
|
|
|
1733
1758
|
var DEFAULT_COMPRESS_IMAGES = {
|
|
1734
1759
|
enabled: true,
|
|
1735
1760
|
minBytes: 16384,
|
|
1736
|
-
ttlDays: 7
|
|
1761
|
+
ttlDays: 7,
|
|
1762
|
+
largeBytes: 524288,
|
|
1763
|
+
nudge: true
|
|
1737
1764
|
};
|
|
1738
1765
|
var DEFAULT_PROTECTED_TOOLS = [
|
|
1739
1766
|
"task",
|
|
@@ -1809,6 +1836,7 @@ var defaultConfig = {
|
|
|
1809
1836
|
permission: "allow",
|
|
1810
1837
|
showCompression: true,
|
|
1811
1838
|
summaryBuffer: true,
|
|
1839
|
+
emergencyTruncation: false,
|
|
1812
1840
|
candidates: false,
|
|
1813
1841
|
maxContextLimit: "80%",
|
|
1814
1842
|
minContextLimit: "80%",
|
|
@@ -2015,9 +2043,12 @@ function mergeCompress(base, override) {
|
|
|
2015
2043
|
images: {
|
|
2016
2044
|
enabled: override.images?.enabled ?? base.images?.enabled ?? DEFAULT_COMPRESS_IMAGES.enabled,
|
|
2017
2045
|
minBytes: override.images?.minBytes ?? base.images?.minBytes ?? DEFAULT_COMPRESS_IMAGES.minBytes,
|
|
2018
|
-
ttlDays: override.images?.ttlDays ?? base.images?.ttlDays ?? DEFAULT_COMPRESS_IMAGES.ttlDays
|
|
2046
|
+
ttlDays: override.images?.ttlDays ?? base.images?.ttlDays ?? DEFAULT_COMPRESS_IMAGES.ttlDays,
|
|
2047
|
+
largeBytes: override.images?.largeBytes ?? base.images?.largeBytes ?? DEFAULT_COMPRESS_IMAGES.largeBytes,
|
|
2048
|
+
nudge: override.images?.nudge ?? base.images?.nudge ?? DEFAULT_COMPRESS_IMAGES.nudge
|
|
2019
2049
|
},
|
|
2020
|
-
completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens
|
|
2050
|
+
completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens,
|
|
2051
|
+
emergencyTruncation: override.emergencyTruncation ?? base.emergencyTruncation
|
|
2021
2052
|
};
|
|
2022
2053
|
}
|
|
2023
2054
|
function mergeCommands(base, override) {
|
|
@@ -2410,174 +2441,18 @@ function isProtectedUserMessage(_config, message) {
|
|
|
2410
2441
|
return false;
|
|
2411
2442
|
}
|
|
2412
2443
|
|
|
2413
|
-
// lib/
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
const cacheRead = assistantInfo.tokens?.cache?.read || 0;
|
|
2426
|
-
const cacheWrite = assistantInfo.tokens?.cache?.write || 0;
|
|
2427
|
-
if (input <= 0 && output <= 0) {
|
|
2428
|
-
continue;
|
|
2429
|
-
}
|
|
2430
|
-
if (state.lastCompaction > 0 && (msg.info.time.created < state.lastCompaction || msg.info.summary === true && msg.info.time.created === state.lastCompaction)) {
|
|
2431
|
-
return 0;
|
|
2432
|
-
}
|
|
2433
|
-
return input + cacheRead + cacheWrite + output + reasoning;
|
|
2434
|
-
}
|
|
2435
|
-
let estimated = 0;
|
|
2436
|
-
for (const m of messages) {
|
|
2437
|
-
estimated += countAllMessageTokens(m);
|
|
2438
|
-
}
|
|
2439
|
-
return estimated;
|
|
2440
|
-
}
|
|
2441
|
-
function estimateSystemPromptTokens(messages) {
|
|
2442
|
-
let firstInput;
|
|
2443
|
-
for (const msg of messages) {
|
|
2444
|
-
if (msg.info.role !== "assistant") continue;
|
|
2445
|
-
const assistantInfo = msg.info;
|
|
2446
|
-
const t = assistantInfo.tokens;
|
|
2447
|
-
if (!t) continue;
|
|
2448
|
-
const input = (t.input || 0) + (t.cache?.read || 0) + (t.cache?.write || 0);
|
|
2449
|
-
if (input > 0) {
|
|
2450
|
-
firstInput = input;
|
|
2451
|
-
break;
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
if (firstInput === void 0) return 0;
|
|
2455
|
-
let firstUserText = "";
|
|
2456
|
-
for (const msg of messages) {
|
|
2457
|
-
if (msg.info.role !== "user") continue;
|
|
2458
|
-
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
2459
|
-
for (const part of parts) {
|
|
2460
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
2461
|
-
firstUserText += part.text;
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
if (firstUserText) break;
|
|
2465
|
-
}
|
|
2466
|
-
return Math.max(0, firstInput - countTokens2(firstUserText));
|
|
2467
|
-
}
|
|
2468
|
-
function getCurrentParams(state, messages, logger) {
|
|
2469
|
-
const userMsg = getLastUserMessage(messages);
|
|
2470
|
-
if (!userMsg) {
|
|
2471
|
-
logger.debug("No user message found when determining current params");
|
|
2472
|
-
return {
|
|
2473
|
-
providerId: void 0,
|
|
2474
|
-
modelId: void 0,
|
|
2475
|
-
agent: void 0,
|
|
2476
|
-
variant: void 0
|
|
2477
|
-
};
|
|
2478
|
-
}
|
|
2479
|
-
const userInfo = userMsg.info;
|
|
2480
|
-
const agent = userInfo.agent;
|
|
2481
|
-
const providerId = userInfo.model.providerID;
|
|
2482
|
-
const modelId = userInfo.model.modelID;
|
|
2483
|
-
const variant = userInfo.model.variant;
|
|
2484
|
-
return { providerId, modelId, agent, variant };
|
|
2485
|
-
}
|
|
2486
|
-
function countTokens2(text) {
|
|
2487
|
-
if (!text) return 0;
|
|
2488
|
-
try {
|
|
2489
|
-
return anthropicCountTokens(text);
|
|
2490
|
-
} catch {
|
|
2491
|
-
return Math.round(text.length / 4);
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
function estimateTokensBatch(texts) {
|
|
2495
|
-
if (texts.length === 0) return 0;
|
|
2496
|
-
return countTokens2(texts.join(" "));
|
|
2497
|
-
}
|
|
2498
|
-
var COMPACTED_TOOL_OUTPUT_PLACEHOLDER = "[Old tool result content cleared]";
|
|
2499
|
-
function stringifyToolContent(value) {
|
|
2500
|
-
return typeof value === "string" ? value : JSON.stringify(value);
|
|
2501
|
-
}
|
|
2502
|
-
function extractCompletedToolOutput(part) {
|
|
2503
|
-
if (part?.type !== "tool" || part.state?.status !== "completed" || part.state?.output === void 0) {
|
|
2504
|
-
return void 0;
|
|
2505
|
-
}
|
|
2506
|
-
if (part.state?.time?.compacted) {
|
|
2507
|
-
return COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
|
|
2508
|
-
}
|
|
2509
|
-
return stringifyToolContent(part.state.output);
|
|
2510
|
-
}
|
|
2511
|
-
function extractToolContent(part) {
|
|
2512
|
-
const contents = [];
|
|
2513
|
-
if (part?.type !== "tool") {
|
|
2514
|
-
return contents;
|
|
2515
|
-
}
|
|
2516
|
-
if (part.state?.input !== void 0) {
|
|
2517
|
-
contents.push(stringifyToolContent(part.state.input));
|
|
2518
|
-
}
|
|
2519
|
-
const completedOutput = extractCompletedToolOutput(part);
|
|
2520
|
-
if (completedOutput !== void 0) {
|
|
2521
|
-
contents.push(completedOutput);
|
|
2522
|
-
} else if (part.state?.status === "error" && part.state?.error) {
|
|
2523
|
-
contents.push(stringifyToolContent(part.state.error));
|
|
2524
|
-
}
|
|
2525
|
-
return contents;
|
|
2526
|
-
}
|
|
2527
|
-
function countAllMessageTokens(msg) {
|
|
2528
|
-
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
2529
|
-
const texts = [];
|
|
2530
|
-
for (const part of parts) {
|
|
2531
|
-
if (part.type === "text") {
|
|
2532
|
-
texts.push(part.text);
|
|
2533
|
-
} else {
|
|
2534
|
-
texts.push(...extractToolContent(part));
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
if (texts.length === 0) return 0;
|
|
2538
|
-
return estimateTokensBatch(texts);
|
|
2539
|
-
}
|
|
2540
|
-
function countMessageCharacters(msg) {
|
|
2541
|
-
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
2542
|
-
let total = 0;
|
|
2543
|
-
for (const part of parts) {
|
|
2544
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
2545
|
-
total += part.text.length;
|
|
2546
|
-
} else {
|
|
2547
|
-
for (const content of extractToolContent(part)) {
|
|
2548
|
-
total += content.length;
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
return total;
|
|
2553
|
-
}
|
|
2554
|
-
function estimateAllMessageTokensFast(msg) {
|
|
2555
|
-
return Math.round(countMessageCharacters(msg) / 4);
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
// lib/prompts/extensions/tool.ts
|
|
2559
|
-
var RANGE_FORMAT_EXTENSION = `
|
|
2560
|
-
|
|
2561
|
-
THE FORMAT OF COMPRESS
|
|
2562
|
-
|
|
2563
|
-
\`\`\`
|
|
2564
|
-
{
|
|
2565
|
-
topic?: string, // OPTIONAL fallback topic for entries without their own.
|
|
2566
|
-
// Omit when every content entry specifies its own topic.
|
|
2567
|
-
content: [ // One or more ranges to compress
|
|
2568
|
-
{
|
|
2569
|
-
topic?: string, // OPTIONAL per-entry topic for this range.
|
|
2570
|
-
// Falls back to top-level topic.
|
|
2571
|
-
// Give each entry its own topic when compressing
|
|
2572
|
-
// unrelated ranges in one call.
|
|
2573
|
-
startId: string, // Boundary ID at range start: mNNNNN or bN
|
|
2574
|
-
endId: string, // Boundary ID at range end: mNNNNN or bN
|
|
2575
|
-
summary: string // Complete technical summary replacing all content in range
|
|
2576
|
-
}
|
|
2577
|
-
]
|
|
2578
|
-
}
|
|
2579
|
-
\`\`\`
|
|
2580
|
-
Each entry needs a topic \u2014 either its own or the top-level fallback.`;
|
|
2444
|
+
// lib/compress/image-archive.ts
|
|
2445
|
+
import { createHash } from "crypto";
|
|
2446
|
+
import {
|
|
2447
|
+
existsSync as existsSync3,
|
|
2448
|
+
mkdirSync as mkdirSync2,
|
|
2449
|
+
readdirSync,
|
|
2450
|
+
rmdirSync,
|
|
2451
|
+
statSync as statSync2,
|
|
2452
|
+
unlinkSync,
|
|
2453
|
+
writeFileSync as writeFileSync2
|
|
2454
|
+
} from "fs";
|
|
2455
|
+
import { join as join3 } from "path";
|
|
2581
2456
|
|
|
2582
2457
|
// lib/state/persistence.ts
|
|
2583
2458
|
import * as fs from "fs/promises";
|
|
@@ -2731,7 +2606,7 @@ function loadPruneMessagesState(persisted) {
|
|
|
2731
2606
|
deactivatedByUser: block.deactivatedByUser === true,
|
|
2732
2607
|
deactivatedByUserDeep: block.deactivatedByUserDeep === true ? true : void 0,
|
|
2733
2608
|
compressedTokens: typeof block.compressedTokens === "number" && Number.isFinite(block.compressedTokens) ? Math.max(0, block.compressedTokens) : 0,
|
|
2734
|
-
summaryTokens: typeof block.summaryTokens === "number" && Number.isFinite(block.summaryTokens) ? Math.max(0, block.summaryTokens) : typeof block.summary === "string" ?
|
|
2609
|
+
summaryTokens: typeof block.summaryTokens === "number" && Number.isFinite(block.summaryTokens) ? Math.max(0, block.summaryTokens) : typeof block.summary === "string" ? countTokens(block.summary) : 0,
|
|
2735
2610
|
durationMs: typeof block.durationMs === "number" && Number.isFinite(block.durationMs) ? Math.max(0, block.durationMs) : 0,
|
|
2736
2611
|
mode: block.mode === "range" || block.mode === "message" ? block.mode : void 0,
|
|
2737
2612
|
topic: typeof block.topic === "string" ? block.topic : "",
|
|
@@ -2955,7 +2830,9 @@ function saveSessionState(sessionState, logger, sessionName) {
|
|
|
2955
2830
|
modelContextLimit: sessionState.modelContextLimit,
|
|
2956
2831
|
modelProviderID: sessionState.modelProviderID,
|
|
2957
2832
|
modelID: sessionState.modelID,
|
|
2958
|
-
imageArchive: sessionState.imageArchive
|
|
2833
|
+
imageArchive: sessionState.imageArchive,
|
|
2834
|
+
imageSurgeNudged: sessionState.imageSurgeNudged,
|
|
2835
|
+
stickyTruncations: sessionState.stickyTruncations
|
|
2959
2836
|
};
|
|
2960
2837
|
const key = saveQueueKey(sessionState.sessionId, sessionState.storageDir);
|
|
2961
2838
|
let queue = saveQueues.get(key);
|
|
@@ -3070,22 +2947,509 @@ async function loadSessionState(sessionId, logger, storageDir) {
|
|
|
3070
2947
|
;
|
|
3071
2948
|
state._persistedLastCompaction = persistedLastCompaction;
|
|
3072
2949
|
}
|
|
3073
|
-
logger.info("Loaded session state from disk", {
|
|
3074
|
-
sessionId
|
|
3075
|
-
});
|
|
3076
|
-
return state;
|
|
3077
|
-
} catch (error) {
|
|
3078
|
-
logger.warn("Failed to load session state", {
|
|
3079
|
-
sessionId,
|
|
3080
|
-
error: error?.message
|
|
3081
|
-
});
|
|
3082
|
-
return null;
|
|
3083
|
-
}
|
|
2950
|
+
logger.info("Loaded session state from disk", {
|
|
2951
|
+
sessionId
|
|
2952
|
+
});
|
|
2953
|
+
return state;
|
|
2954
|
+
} catch (error) {
|
|
2955
|
+
logger.warn("Failed to load session state", {
|
|
2956
|
+
sessionId,
|
|
2957
|
+
error: error?.message
|
|
2958
|
+
});
|
|
2959
|
+
return null;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// lib/compress/image-archive.ts
|
|
2964
|
+
var DATA_URL_REGEX = /^data:(image\/[a-z0-9.+-]+);base64,(.+)$/is;
|
|
2965
|
+
var IMAGE_EXTENSIONS = {
|
|
2966
|
+
"image/png": "png",
|
|
2967
|
+
"image/jpeg": "jpg",
|
|
2968
|
+
"image/jpg": "jpg",
|
|
2969
|
+
"image/gif": "gif",
|
|
2970
|
+
"image/webp": "webp",
|
|
2971
|
+
"image/avif": "avif",
|
|
2972
|
+
"image/bmp": "bmp"
|
|
2973
|
+
};
|
|
2974
|
+
function sha8(input) {
|
|
2975
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
2976
|
+
}
|
|
2977
|
+
function parseImageDataUrl(url) {
|
|
2978
|
+
if (typeof url !== "string") return void 0;
|
|
2979
|
+
const match = DATA_URL_REGEX.exec(url);
|
|
2980
|
+
if (!match) return void 0;
|
|
2981
|
+
return { mediaType: match[1].toLowerCase(), base64: match[2] };
|
|
2982
|
+
}
|
|
2983
|
+
function extForMediaType(mediaType) {
|
|
2984
|
+
return IMAGE_EXTENSIONS[mediaType.toLowerCase()];
|
|
2985
|
+
}
|
|
2986
|
+
function archiveDirName(sessionId) {
|
|
2987
|
+
let name = sessionId.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 100);
|
|
2988
|
+
if (name !== sessionId) {
|
|
2989
|
+
name = `${name}-${sha8(sessionId)}`;
|
|
2990
|
+
}
|
|
2991
|
+
return name || "-";
|
|
2992
|
+
}
|
|
2993
|
+
function archiveRoot(storageDir, sessionId) {
|
|
2994
|
+
return join3(storageDir ?? getDefaultStorageDir(), "images", archiveDirName(sessionId));
|
|
2995
|
+
}
|
|
2996
|
+
function archiveSessionImages(state, messages, images, logger) {
|
|
2997
|
+
if (!images.enabled || !state.sessionId) return 0;
|
|
2998
|
+
const root = archiveRoot(state.storageDir, state.sessionId);
|
|
2999
|
+
let archived = 0;
|
|
3000
|
+
for (const message of messages) {
|
|
3001
|
+
const rawId = message.info.id;
|
|
3002
|
+
if (!rawId) continue;
|
|
3003
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
3004
|
+
for (let partIndex = 0; partIndex < parts.length; partIndex++) {
|
|
3005
|
+
const part = parts[partIndex];
|
|
3006
|
+
if (part.type !== "file") continue;
|
|
3007
|
+
const parsed = parseImageDataUrl(part.url);
|
|
3008
|
+
if (!parsed) continue;
|
|
3009
|
+
const ext = extForMediaType(parsed.mediaType);
|
|
3010
|
+
if (!ext) continue;
|
|
3011
|
+
const fileName = `${sha8(rawId)}-p${partIndex}-${sha8(parsed.base64)}.${ext}`;
|
|
3012
|
+
const filePath = join3(root, fileName);
|
|
3013
|
+
const existing = state.imageArchive[rawId]?.find((e) => e.path === filePath);
|
|
3014
|
+
if (existing) continue;
|
|
3015
|
+
let bytes;
|
|
3016
|
+
try {
|
|
3017
|
+
if (existsSync3(filePath)) {
|
|
3018
|
+
bytes = statSync2(filePath).size;
|
|
3019
|
+
} else {
|
|
3020
|
+
const buffer = Buffer.from(parsed.base64, "base64");
|
|
3021
|
+
bytes = buffer.byteLength;
|
|
3022
|
+
if (bytes < images.minBytes) continue;
|
|
3023
|
+
mkdirSync2(root, { recursive: true });
|
|
3024
|
+
writeFileSync2(filePath, buffer, { mode: 384 });
|
|
3025
|
+
}
|
|
3026
|
+
} catch (error) {
|
|
3027
|
+
logger.warn(
|
|
3028
|
+
`[#478] failed to archive image ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
3029
|
+
);
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3032
|
+
const entry = {
|
|
3033
|
+
path: filePath,
|
|
3034
|
+
mediaType: parsed.mediaType,
|
|
3035
|
+
bytes
|
|
3036
|
+
};
|
|
3037
|
+
if (!state.imageArchive[rawId]) {
|
|
3038
|
+
state.imageArchive[rawId] = [];
|
|
3039
|
+
}
|
|
3040
|
+
state.imageArchive[rawId].push(entry);
|
|
3041
|
+
state.stats.imagesArchived = (state.stats.imagesArchived ?? 0) + 1;
|
|
3042
|
+
state.stats.imageBytesArchived = (state.stats.imageBytesArchived ?? 0) + bytes;
|
|
3043
|
+
archived++;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
return archived;
|
|
3047
|
+
}
|
|
3048
|
+
function pruneImageArchive(state, images, logger) {
|
|
3049
|
+
if (state.imagePruneDone || !state.sessionId) return;
|
|
3050
|
+
state.imagePruneDone = true;
|
|
3051
|
+
try {
|
|
3052
|
+
const root = archiveRoot(state.storageDir, state.sessionId);
|
|
3053
|
+
if (!existsSync3(root)) return;
|
|
3054
|
+
const cutoff = Date.now() - images.ttlDays * 864e5;
|
|
3055
|
+
const prunedPaths = /* @__PURE__ */ new Set();
|
|
3056
|
+
for (const name of readdirSync(root)) {
|
|
3057
|
+
const filePath = join3(root, name);
|
|
3058
|
+
try {
|
|
3059
|
+
if (statSync2(filePath).mtimeMs < cutoff) {
|
|
3060
|
+
unlinkSync(filePath);
|
|
3061
|
+
prunedPaths.add(filePath);
|
|
3062
|
+
}
|
|
3063
|
+
} catch {
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
if (prunedPaths.size > 0) {
|
|
3067
|
+
for (const [rawId, entries] of Object.entries(state.imageArchive)) {
|
|
3068
|
+
const kept = entries.filter((e) => !prunedPaths.has(e.path));
|
|
3069
|
+
if (kept.length === 0) {
|
|
3070
|
+
delete state.imageArchive[rawId];
|
|
3071
|
+
} else if (kept.length !== entries.length) {
|
|
3072
|
+
state.imageArchive[rawId] = kept;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
logger.info(`[#478] TTL sweep removed ${prunedPaths.size} expired archived image(s)`);
|
|
3076
|
+
}
|
|
3077
|
+
if (readdirSync(root).length === 0) {
|
|
3078
|
+
rmdirSync(root);
|
|
3079
|
+
}
|
|
3080
|
+
} catch (error) {
|
|
3081
|
+
logger.warn(
|
|
3082
|
+
`[#478] image TTL sweep failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3083
|
+
);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
function appendArchivedImagePointers(summary, selection, state, enabled) {
|
|
3087
|
+
if (!enabled) return summary;
|
|
3088
|
+
const lines = [];
|
|
3089
|
+
for (const messageId of selection.messageIds) {
|
|
3090
|
+
const entries = state.imageArchive[messageId];
|
|
3091
|
+
if (!entries || entries.length === 0) continue;
|
|
3092
|
+
const ref = state.messageIds.byRawId.get(messageId) ?? messageId;
|
|
3093
|
+
for (const entry of entries) {
|
|
3094
|
+
lines.push(`- ${ref}: ${entry.path} (${entry.mediaType}, ${entry.bytes} bytes)`);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
if (lines.length === 0) return summary;
|
|
3098
|
+
const heading = "\n\nImages captured in this range were archived to disk (their pixels are NOT summarized). Re-read them with your file tools if you need the pixels again:";
|
|
3099
|
+
return summary + heading + "\n" + lines.join("\n");
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// lib/compress/image-tokens.ts
|
|
3103
|
+
var VISION_MAX_SIDE = 1568;
|
|
3104
|
+
var VISION_TILE_SIZE = 512;
|
|
3105
|
+
var TOKENS_PER_TILE = 167;
|
|
3106
|
+
var VISION_MAX_TILES = Math.ceil(VISION_MAX_SIDE / VISION_TILE_SIZE) ** 2;
|
|
3107
|
+
var UNKNOWN_DIM_LARGE_BYTES = 256 * 1024;
|
|
3108
|
+
function estimateVisionTokensForDimensions(width, height) {
|
|
3109
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
3110
|
+
return 0;
|
|
3111
|
+
}
|
|
3112
|
+
const scale = Math.min(1, VISION_MAX_SIDE / Math.max(width, height));
|
|
3113
|
+
const scaledWidth = Math.max(1, Math.floor(width * scale));
|
|
3114
|
+
const scaledHeight = Math.max(1, Math.floor(height * scale));
|
|
3115
|
+
const tiles = Math.ceil(scaledWidth / VISION_TILE_SIZE) * Math.ceil(scaledHeight / VISION_TILE_SIZE);
|
|
3116
|
+
return Math.min(tiles, VISION_MAX_TILES) * TOKENS_PER_TILE;
|
|
3117
|
+
}
|
|
3118
|
+
function validDimensions(width, height) {
|
|
3119
|
+
if (typeof width !== "number" || typeof height !== "number" || !Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0 || width > 1e6 || height > 1e6) {
|
|
3120
|
+
return void 0;
|
|
3121
|
+
}
|
|
3122
|
+
return { width, height };
|
|
3123
|
+
}
|
|
3124
|
+
function fourcc(bytes, offset) {
|
|
3125
|
+
return String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
|
|
3126
|
+
}
|
|
3127
|
+
function parseImageDimensions(bytes, mediaType) {
|
|
3128
|
+
const len = bytes.length;
|
|
3129
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3130
|
+
switch (mediaType.toLowerCase()) {
|
|
3131
|
+
case "image/png": {
|
|
3132
|
+
if (len < 24) return void 0;
|
|
3133
|
+
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
3134
|
+
for (let i = 0; i < sig.length; i++) {
|
|
3135
|
+
if (bytes[i] !== sig[i]) return void 0;
|
|
3136
|
+
}
|
|
3137
|
+
return validDimensions(dv.getUint32(16, false), dv.getUint32(20, false));
|
|
3138
|
+
}
|
|
3139
|
+
case "image/jpeg":
|
|
3140
|
+
case "image/jpg": {
|
|
3141
|
+
if (len < 4 || bytes[0] !== 255 || bytes[1] !== 216) return void 0;
|
|
3142
|
+
let i = 2;
|
|
3143
|
+
while (i + 9 < len) {
|
|
3144
|
+
if (bytes[i] !== 255) {
|
|
3145
|
+
i++;
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
const marker = bytes[i + 1];
|
|
3149
|
+
if (marker === 216 || marker === 217 || marker >= 218 && marker <= 223) {
|
|
3150
|
+
return void 0;
|
|
3151
|
+
}
|
|
3152
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
3153
|
+
if (i + 9 >= len) return void 0;
|
|
3154
|
+
return validDimensions(dv.getUint16(i + 8, false), dv.getUint16(i + 6, false));
|
|
3155
|
+
}
|
|
3156
|
+
const segLen = dv.getUint16(i + 2, false);
|
|
3157
|
+
if (segLen < 2) return void 0;
|
|
3158
|
+
i += 2 + segLen;
|
|
3159
|
+
}
|
|
3160
|
+
return void 0;
|
|
3161
|
+
}
|
|
3162
|
+
case "image/gif": {
|
|
3163
|
+
if (len < 10) return void 0;
|
|
3164
|
+
if (fourcc(bytes, 0) !== "GIF8") return void 0;
|
|
3165
|
+
return validDimensions(dv.getUint16(6, true), dv.getUint16(8, true));
|
|
3166
|
+
}
|
|
3167
|
+
case "image/webp": {
|
|
3168
|
+
if (len < 30) return void 0;
|
|
3169
|
+
if (fourcc(bytes, 0) !== "RIFF" || fourcc(bytes, 8) !== "WEBP") return void 0;
|
|
3170
|
+
let i = 12;
|
|
3171
|
+
while (i + 8 < len) {
|
|
3172
|
+
const id = fourcc(bytes, i);
|
|
3173
|
+
const size = dv.getUint32(i + 4, true);
|
|
3174
|
+
const payload = i + 8;
|
|
3175
|
+
if (id === "VP8 ") {
|
|
3176
|
+
if (payload + 10 > len) return void 0;
|
|
3177
|
+
return validDimensions(
|
|
3178
|
+
dv.getUint16(payload + 6, true) & 16383,
|
|
3179
|
+
dv.getUint16(payload + 8, true) & 16383
|
|
3180
|
+
);
|
|
3181
|
+
}
|
|
3182
|
+
if (id === "VP8L") {
|
|
3183
|
+
if (payload + 5 > len) return void 0;
|
|
3184
|
+
if (bytes[payload] !== 47) return void 0;
|
|
3185
|
+
const packed = bytes[payload + 1] | bytes[payload + 2] << 8 | bytes[payload + 3] << 16;
|
|
3186
|
+
return validDimensions(
|
|
3187
|
+
1 + (packed & 16383),
|
|
3188
|
+
1 + (packed >>> 14 & 16383)
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
if (id === "VP8X") {
|
|
3192
|
+
if (payload + 10 > len) return void 0;
|
|
3193
|
+
const w = bytes[payload + 4] | bytes[payload + 5] << 8 | bytes[payload + 6] << 16;
|
|
3194
|
+
const h = bytes[payload + 7] | bytes[payload + 8] << 8 | bytes[payload + 9] << 16;
|
|
3195
|
+
return validDimensions(w + 1, h + 1);
|
|
3196
|
+
}
|
|
3197
|
+
if (size === 4294967295) return void 0;
|
|
3198
|
+
i = payload + size + (size & 1);
|
|
3199
|
+
}
|
|
3200
|
+
return void 0;
|
|
3201
|
+
}
|
|
3202
|
+
case "image/bmp": {
|
|
3203
|
+
if (len < 26) return void 0;
|
|
3204
|
+
if (bytes[0] !== 66 || bytes[1] !== 77) return void 0;
|
|
3205
|
+
return validDimensions(
|
|
3206
|
+
Math.abs(dv.getInt32(18, true)),
|
|
3207
|
+
Math.abs(dv.getInt32(22, true))
|
|
3208
|
+
);
|
|
3209
|
+
}
|
|
3210
|
+
default:
|
|
3211
|
+
return void 0;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
function decodedBytesFromBase64(base64) {
|
|
3215
|
+
if (typeof base64 !== "string" || base64.length === 0) return 0;
|
|
3216
|
+
return Math.max(0, Math.floor(base64.length * 3 / 4));
|
|
3217
|
+
}
|
|
3218
|
+
function decodeBase64Prefix(base64, maxBytes) {
|
|
3219
|
+
const chars = Math.min(base64.length, Math.ceil(maxBytes * 4 / 3));
|
|
3220
|
+
const aligned = chars - chars % 4;
|
|
3221
|
+
if (aligned < 4) return void 0;
|
|
3222
|
+
try {
|
|
3223
|
+
return new Uint8Array(Buffer.from(base64.slice(0, aligned), "base64"));
|
|
3224
|
+
} catch {
|
|
3225
|
+
return void 0;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
var HEADER_SCAN_BYTES = 256 * 1024;
|
|
3229
|
+
var estimateCache = /* @__PURE__ */ new Map();
|
|
3230
|
+
var ESTIMATE_CACHE_CAP = 512;
|
|
3231
|
+
function remember(key, value) {
|
|
3232
|
+
if (estimateCache.size >= ESTIMATE_CACHE_CAP && !estimateCache.has(key)) {
|
|
3233
|
+
const oldest = estimateCache.keys().next().value;
|
|
3234
|
+
if (oldest !== void 0) {
|
|
3235
|
+
estimateCache.delete(oldest);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
estimateCache.set(key, value);
|
|
3239
|
+
}
|
|
3240
|
+
function estimateImagePartTokens(part) {
|
|
3241
|
+
if (!part || typeof part !== "object") return 0;
|
|
3242
|
+
const candidate = part;
|
|
3243
|
+
if (candidate.type !== "file") return 0;
|
|
3244
|
+
const url = typeof candidate.url === "string" ? candidate.url : void 0;
|
|
3245
|
+
if (!url) return 0;
|
|
3246
|
+
const parsed = parseImageDataUrl(url);
|
|
3247
|
+
if (!parsed) return 0;
|
|
3248
|
+
const key = `${parsed.mediaType}:${url.length}:${url.slice(-32)}`;
|
|
3249
|
+
const cached = estimateCache.get(key);
|
|
3250
|
+
if (cached !== void 0) return cached;
|
|
3251
|
+
let estimate;
|
|
3252
|
+
const prefix = decodeBase64Prefix(parsed.base64, HEADER_SCAN_BYTES);
|
|
3253
|
+
const dims = prefix ? parseImageDimensions(prefix, parsed.mediaType) : void 0;
|
|
3254
|
+
if (dims) {
|
|
3255
|
+
estimate = estimateVisionTokensForDimensions(dims.width, dims.height);
|
|
3256
|
+
} else {
|
|
3257
|
+
const bytes = decodedBytesFromBase64(parsed.base64);
|
|
3258
|
+
estimate = bytes >= UNKNOWN_DIM_LARGE_BYTES ? VISION_MAX_TILES * TOKENS_PER_TILE : Math.round(parsed.base64.length / 4);
|
|
3259
|
+
}
|
|
3260
|
+
remember(key, estimate);
|
|
3261
|
+
return estimate;
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
// lib/token-utils.ts
|
|
3265
|
+
var anthropicCountTokens = _anthropicTokenizer.countTokens ?? _anthropicTokenizer.default?.countTokens;
|
|
3266
|
+
function countPendingImageTokens(messages, startIndex) {
|
|
3267
|
+
let total = 0;
|
|
3268
|
+
for (let i = Math.max(0, startIndex); i < messages.length; i++) {
|
|
3269
|
+
const parts = Array.isArray(messages[i].parts) ? messages[i].parts : [];
|
|
3270
|
+
for (const part of parts) {
|
|
3271
|
+
if (part.type === "file") {
|
|
3272
|
+
total += estimateImagePartTokens(part);
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return total;
|
|
3277
|
+
}
|
|
3278
|
+
function getCurrentTokenUsage(state, messages) {
|
|
3279
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3280
|
+
const msg = messages[i];
|
|
3281
|
+
if (msg.info.role !== "assistant") {
|
|
3282
|
+
continue;
|
|
3283
|
+
}
|
|
3284
|
+
const assistantInfo = msg.info;
|
|
3285
|
+
const input = assistantInfo.tokens?.input || 0;
|
|
3286
|
+
const output = assistantInfo.tokens?.output || 0;
|
|
3287
|
+
const reasoning = assistantInfo.tokens?.reasoning || 0;
|
|
3288
|
+
const cacheRead = assistantInfo.tokens?.cache?.read || 0;
|
|
3289
|
+
const cacheWrite = assistantInfo.tokens?.cache?.write || 0;
|
|
3290
|
+
if (input <= 0 && output <= 0) {
|
|
3291
|
+
continue;
|
|
3292
|
+
}
|
|
3293
|
+
if (state.lastCompaction > 0 && (msg.info.time.created < state.lastCompaction || msg.info.summary === true && msg.info.time.created === state.lastCompaction)) {
|
|
3294
|
+
return 0;
|
|
3295
|
+
}
|
|
3296
|
+
return input + cacheRead + cacheWrite + output + reasoning + countPendingImageTokens(messages, i + 1);
|
|
3297
|
+
}
|
|
3298
|
+
let estimated = 0;
|
|
3299
|
+
for (const m of messages) {
|
|
3300
|
+
estimated += countAllMessageTokens(m);
|
|
3301
|
+
}
|
|
3302
|
+
return estimated;
|
|
3303
|
+
}
|
|
3304
|
+
function estimateSystemPromptTokens(messages) {
|
|
3305
|
+
let firstInput;
|
|
3306
|
+
for (const msg of messages) {
|
|
3307
|
+
if (msg.info.role !== "assistant") continue;
|
|
3308
|
+
const assistantInfo = msg.info;
|
|
3309
|
+
const t = assistantInfo.tokens;
|
|
3310
|
+
if (!t) continue;
|
|
3311
|
+
const input = (t.input || 0) + (t.cache?.read || 0) + (t.cache?.write || 0);
|
|
3312
|
+
if (input > 0) {
|
|
3313
|
+
firstInput = input;
|
|
3314
|
+
break;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
if (firstInput === void 0) return 0;
|
|
3318
|
+
let firstUserText = "";
|
|
3319
|
+
for (const msg of messages) {
|
|
3320
|
+
if (msg.info.role !== "user") continue;
|
|
3321
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
3322
|
+
for (const part of parts) {
|
|
3323
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
3324
|
+
firstUserText += part.text;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
if (firstUserText) break;
|
|
3328
|
+
}
|
|
3329
|
+
return Math.max(0, firstInput - countTokens(firstUserText));
|
|
3330
|
+
}
|
|
3331
|
+
function getCurrentParams(state, messages, logger) {
|
|
3332
|
+
const userMsg = getLastUserMessage(messages);
|
|
3333
|
+
if (!userMsg) {
|
|
3334
|
+
logger.debug("No user message found when determining current params");
|
|
3335
|
+
return {
|
|
3336
|
+
providerId: void 0,
|
|
3337
|
+
modelId: void 0,
|
|
3338
|
+
agent: void 0,
|
|
3339
|
+
variant: void 0
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
const userInfo = userMsg.info;
|
|
3343
|
+
const agent = userInfo.agent;
|
|
3344
|
+
const providerId = userInfo.model.providerID;
|
|
3345
|
+
const modelId = userInfo.model.modelID;
|
|
3346
|
+
const variant = userInfo.model.variant;
|
|
3347
|
+
return { providerId, modelId, agent, variant };
|
|
3348
|
+
}
|
|
3349
|
+
function countTokens(text) {
|
|
3350
|
+
if (!text) return 0;
|
|
3351
|
+
try {
|
|
3352
|
+
return anthropicCountTokens(text);
|
|
3353
|
+
} catch {
|
|
3354
|
+
return Math.round(text.length / 4);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
function estimateTokensBatch(texts) {
|
|
3358
|
+
if (texts.length === 0) return 0;
|
|
3359
|
+
return countTokens(texts.join(" "));
|
|
3360
|
+
}
|
|
3361
|
+
var COMPACTED_TOOL_OUTPUT_PLACEHOLDER = "[Old tool result content cleared]";
|
|
3362
|
+
function stringifyToolContent(value) {
|
|
3363
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
3364
|
+
}
|
|
3365
|
+
function extractCompletedToolOutput(part) {
|
|
3366
|
+
if (part?.type !== "tool" || part.state?.status !== "completed" || part.state?.output === void 0) {
|
|
3367
|
+
return void 0;
|
|
3368
|
+
}
|
|
3369
|
+
if (part.state?.time?.compacted) {
|
|
3370
|
+
return COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
|
|
3371
|
+
}
|
|
3372
|
+
return stringifyToolContent(part.state.output);
|
|
3373
|
+
}
|
|
3374
|
+
function extractToolContent(part) {
|
|
3375
|
+
const contents = [];
|
|
3376
|
+
if (part?.type !== "tool") {
|
|
3377
|
+
return contents;
|
|
3378
|
+
}
|
|
3379
|
+
if (part.state?.input !== void 0) {
|
|
3380
|
+
contents.push(stringifyToolContent(part.state.input));
|
|
3381
|
+
}
|
|
3382
|
+
const completedOutput = extractCompletedToolOutput(part);
|
|
3383
|
+
if (completedOutput !== void 0) {
|
|
3384
|
+
contents.push(completedOutput);
|
|
3385
|
+
} else if (part.state?.status === "error" && part.state?.error) {
|
|
3386
|
+
contents.push(stringifyToolContent(part.state.error));
|
|
3387
|
+
}
|
|
3388
|
+
return contents;
|
|
3389
|
+
}
|
|
3390
|
+
function countAllMessageTokens(msg) {
|
|
3391
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
3392
|
+
const texts = [];
|
|
3393
|
+
let imageTokens = 0;
|
|
3394
|
+
for (const part of parts) {
|
|
3395
|
+
if (part.type === "text") {
|
|
3396
|
+
texts.push(part.text);
|
|
3397
|
+
} else if (part.type === "file") {
|
|
3398
|
+
imageTokens += estimateImagePartTokens(part);
|
|
3399
|
+
} else {
|
|
3400
|
+
texts.push(...extractToolContent(part));
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
if (texts.length === 0 && imageTokens === 0) return 0;
|
|
3404
|
+
return imageTokens + (texts.length > 0 ? estimateTokensBatch(texts) : 0);
|
|
3405
|
+
}
|
|
3406
|
+
function countMessageCharacters(msg) {
|
|
3407
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
3408
|
+
let total = 0;
|
|
3409
|
+
for (const part of parts) {
|
|
3410
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
3411
|
+
total += part.text.length;
|
|
3412
|
+
} else if (part.type === "file") {
|
|
3413
|
+
total += estimateImagePartTokens(part) * 4;
|
|
3414
|
+
} else {
|
|
3415
|
+
for (const content of extractToolContent(part)) {
|
|
3416
|
+
total += content.length;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
return total;
|
|
3421
|
+
}
|
|
3422
|
+
function estimateAllMessageTokensFast(msg) {
|
|
3423
|
+
return Math.round(countMessageCharacters(msg) / 4);
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
// lib/prompts/extensions/tool.ts
|
|
3427
|
+
var RANGE_FORMAT_EXTENSION = `
|
|
3428
|
+
|
|
3429
|
+
THE FORMAT OF COMPRESS
|
|
3430
|
+
|
|
3431
|
+
\`\`\`
|
|
3432
|
+
{
|
|
3433
|
+
topic?: string, // OPTIONAL fallback topic for entries without their own.
|
|
3434
|
+
// Omit when every content entry specifies its own topic.
|
|
3435
|
+
content: [ // One or more ranges to compress
|
|
3436
|
+
{
|
|
3437
|
+
topic?: string, // OPTIONAL per-entry topic for this range.
|
|
3438
|
+
// Falls back to top-level topic.
|
|
3439
|
+
// Give each entry its own topic when compressing
|
|
3440
|
+
// unrelated ranges in one call.
|
|
3441
|
+
startId: string, // Boundary ID at range start: mNNNNN or bN
|
|
3442
|
+
endId: string, // Boundary ID at range end: mNNNNN or bN
|
|
3443
|
+
summary: string // Complete technical summary replacing all content in range
|
|
3444
|
+
}
|
|
3445
|
+
]
|
|
3084
3446
|
}
|
|
3447
|
+
\`\`\`
|
|
3448
|
+
Each entry needs a topic \u2014 either its own or the top-level fallback.`;
|
|
3085
3449
|
|
|
3086
3450
|
// lib/state/state.ts
|
|
3087
|
-
import { existsSync as
|
|
3088
|
-
import { join as
|
|
3451
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3452
|
+
import { join as join4 } from "path";
|
|
3089
3453
|
import { cwd } from "process";
|
|
3090
3454
|
|
|
3091
3455
|
// lib/message-ids.ts
|
|
@@ -4630,7 +4994,7 @@ function appendMissingBlockSummaries(summary, _missingBlockIds, _summaryByBlockI
|
|
|
4630
4994
|
}
|
|
4631
4995
|
|
|
4632
4996
|
// lib/state/rebuild.ts
|
|
4633
|
-
import { createHash } from "crypto";
|
|
4997
|
+
import { createHash as createHash2 } from "crypto";
|
|
4634
4998
|
function collectCompressInvocations(messages) {
|
|
4635
4999
|
const invocations = [];
|
|
4636
5000
|
for (const message of messages) {
|
|
@@ -4701,7 +5065,7 @@ function fingerprintMessage(message) {
|
|
|
4701
5065
|
}
|
|
4702
5066
|
return { type: part.type };
|
|
4703
5067
|
});
|
|
4704
|
-
return
|
|
5068
|
+
return createHash2("sha256").update(JSON.stringify({ role: message.info.role, parts })).digest("hex");
|
|
4705
5069
|
}
|
|
4706
5070
|
function mapForkIds(state, parent, parentMessages, forkMessages) {
|
|
4707
5071
|
const parentRefs = parent.messageIds?.byRef;
|
|
@@ -4828,7 +5192,7 @@ function rebuildRangeInvocation(state, input, searchContext, invocation, protect
|
|
|
4828
5192
|
]);
|
|
4829
5193
|
const blockId = allocateBlockId(state);
|
|
4830
5194
|
const storedSummary = wrapCompressedSummary(blockId, plan.entry.summary);
|
|
4831
|
-
const summaryTokens =
|
|
5195
|
+
const summaryTokens = countTokens(storedSummary);
|
|
4832
5196
|
applyCompressionState(
|
|
4833
5197
|
state,
|
|
4834
5198
|
{
|
|
@@ -4890,7 +5254,7 @@ function rebuildMessageInvocation(state, input, searchContext, invocation, gcCon
|
|
|
4890
5254
|
}
|
|
4891
5255
|
const blockId = allocateBlockId(state);
|
|
4892
5256
|
const storedSummary = wrapCompressedSummary(blockId, entry.summary);
|
|
4893
|
-
const summaryTokens =
|
|
5257
|
+
const summaryTokens = countTokens(storedSummary);
|
|
4894
5258
|
applyCompressionState(
|
|
4895
5259
|
state,
|
|
4896
5260
|
{
|
|
@@ -5124,7 +5488,9 @@ function createSessionState() {
|
|
|
5124
5488
|
qualityGateRetryPending: false,
|
|
5125
5489
|
noContextLimitWarned: false,
|
|
5126
5490
|
imageArchive: {},
|
|
5127
|
-
imagePruneDone: false
|
|
5491
|
+
imagePruneDone: false,
|
|
5492
|
+
imageSurgeNudged: {},
|
|
5493
|
+
stickyTruncations: {}
|
|
5128
5494
|
};
|
|
5129
5495
|
}
|
|
5130
5496
|
function resetSessionState(state) {
|
|
@@ -5170,6 +5536,8 @@ function resetSessionState(state) {
|
|
|
5170
5536
|
state.noContextLimitWarned = false;
|
|
5171
5537
|
state.imageArchive = {};
|
|
5172
5538
|
state.imagePruneDone = false;
|
|
5539
|
+
state.imageSurgeNudged = {};
|
|
5540
|
+
state.stickyTruncations = {};
|
|
5173
5541
|
}
|
|
5174
5542
|
async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
|
|
5175
5543
|
if (state.sessionId === sessionId) {
|
|
@@ -5186,8 +5554,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
5186
5554
|
state.nudges.turnNudgeAnchors = collectTurnNudgeAnchors(messages);
|
|
5187
5555
|
const persisted = await loadSessionState(sessionId, logger, state.storageDir);
|
|
5188
5556
|
if (persisted === null) {
|
|
5189
|
-
const defaultPath =
|
|
5190
|
-
if (state.storageDir &&
|
|
5557
|
+
const defaultPath = join4(getDefaultStorageDir(), `${sessionId}.json`);
|
|
5558
|
+
if (state.storageDir && existsSync4(defaultPath)) {
|
|
5191
5559
|
logger.warn(
|
|
5192
5560
|
"storagePath is set but no valid state was found there; a state file exists at the default location \u2014 move it manually to keep history",
|
|
5193
5561
|
{ sessionId, storageDir: state.storageDir, defaultPath }
|
|
@@ -5198,6 +5566,13 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
5198
5566
|
if (parentSessionId) {
|
|
5199
5567
|
try {
|
|
5200
5568
|
const parent = await loadSessionState(parentSessionId, logger);
|
|
5569
|
+
if (parent?.stickyTruncations) {
|
|
5570
|
+
for (const [callID, output] of Object.entries(parent.stickyTruncations)) {
|
|
5571
|
+
if (typeof output === "string") {
|
|
5572
|
+
state.stickyTruncations[callID] = output;
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5201
5576
|
const response = parent ? await client.session.messages({ path: { id: parentSessionId } }) : void 0;
|
|
5202
5577
|
const parentMessages = Array.isArray(response?.data) ? response.data : Array.isArray(response) ? response : [];
|
|
5203
5578
|
if (parent && parentMessages.length > 0) {
|
|
@@ -5263,6 +5638,21 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
5263
5638
|
}
|
|
5264
5639
|
}
|
|
5265
5640
|
}
|
|
5641
|
+
if (persisted.imageSurgeNudged && typeof persisted.imageSurgeNudged === "object") {
|
|
5642
|
+
for (const [key, ts] of Object.entries(persisted.imageSurgeNudged)) {
|
|
5643
|
+
if (typeof key === "string" && typeof ts === "number" && Number.isFinite(ts)) {
|
|
5644
|
+
state.imageSurgeNudged[key] = ts;
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
const persistedSticky = persisted.stickyTruncations;
|
|
5649
|
+
if (persistedSticky && typeof persistedSticky === "object") {
|
|
5650
|
+
for (const [callID, output] of Object.entries(persistedSticky)) {
|
|
5651
|
+
if (typeof output === "string") {
|
|
5652
|
+
state.stickyTruncations[callID] = output;
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5266
5656
|
const persistedAny = persisted;
|
|
5267
5657
|
if (persistedAny._persistedMessageIds) {
|
|
5268
5658
|
state.messageIds = {
|
|
@@ -5438,7 +5828,7 @@ function cacheSystemPromptTokens(state, messages) {
|
|
|
5438
5828
|
}
|
|
5439
5829
|
break;
|
|
5440
5830
|
}
|
|
5441
|
-
const estimatedSystemTokens = Math.max(0, firstInputTokens -
|
|
5831
|
+
const estimatedSystemTokens = Math.max(0, firstInputTokens - countTokens(firstUserText));
|
|
5442
5832
|
state.systemPromptTokens = estimatedSystemTokens > 0 ? estimatedSystemTokens : void 0;
|
|
5443
5833
|
}
|
|
5444
5834
|
|
|
@@ -6675,156 +7065,6 @@ function buildPhantomSkipNotice(identification) {
|
|
|
6675
7065
|
return `Skipped ${identification.phantomIndices.length} already-compressed ${noun} (${skippedNums}) \u2014 they are no-ops; the remaining entries were compressed.`;
|
|
6676
7066
|
}
|
|
6677
7067
|
|
|
6678
|
-
// lib/compress/image-archive.ts
|
|
6679
|
-
import { createHash as createHash2 } from "crypto";
|
|
6680
|
-
import {
|
|
6681
|
-
existsSync as existsSync4,
|
|
6682
|
-
mkdirSync as mkdirSync2,
|
|
6683
|
-
readdirSync,
|
|
6684
|
-
rmdirSync,
|
|
6685
|
-
statSync as statSync2,
|
|
6686
|
-
unlinkSync,
|
|
6687
|
-
writeFileSync as writeFileSync2
|
|
6688
|
-
} from "fs";
|
|
6689
|
-
import { join as join4 } from "path";
|
|
6690
|
-
var DATA_URL_REGEX = /^data:(image\/[a-z0-9.+-]+);base64,(.+)$/is;
|
|
6691
|
-
var IMAGE_EXTENSIONS = {
|
|
6692
|
-
"image/png": "png",
|
|
6693
|
-
"image/jpeg": "jpg",
|
|
6694
|
-
"image/jpg": "jpg",
|
|
6695
|
-
"image/gif": "gif",
|
|
6696
|
-
"image/webp": "webp",
|
|
6697
|
-
"image/avif": "avif",
|
|
6698
|
-
"image/bmp": "bmp"
|
|
6699
|
-
};
|
|
6700
|
-
function sha8(input) {
|
|
6701
|
-
return createHash2("sha256").update(input).digest("hex").slice(0, 8);
|
|
6702
|
-
}
|
|
6703
|
-
function parseImageDataUrl(url) {
|
|
6704
|
-
if (typeof url !== "string") return void 0;
|
|
6705
|
-
const match = DATA_URL_REGEX.exec(url);
|
|
6706
|
-
if (!match) return void 0;
|
|
6707
|
-
return { mediaType: match[1].toLowerCase(), base64: match[2] };
|
|
6708
|
-
}
|
|
6709
|
-
function extForMediaType(mediaType) {
|
|
6710
|
-
return IMAGE_EXTENSIONS[mediaType.toLowerCase()];
|
|
6711
|
-
}
|
|
6712
|
-
function archiveDirName(sessionId) {
|
|
6713
|
-
let name = sessionId.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 100);
|
|
6714
|
-
if (name !== sessionId) {
|
|
6715
|
-
name = `${name}-${sha8(sessionId)}`;
|
|
6716
|
-
}
|
|
6717
|
-
return name || "-";
|
|
6718
|
-
}
|
|
6719
|
-
function archiveRoot(storageDir, sessionId) {
|
|
6720
|
-
return join4(storageDir ?? getDefaultStorageDir(), "images", archiveDirName(sessionId));
|
|
6721
|
-
}
|
|
6722
|
-
function archiveSessionImages(state, messages, images, logger) {
|
|
6723
|
-
if (!images.enabled || !state.sessionId) return 0;
|
|
6724
|
-
const root = archiveRoot(state.storageDir, state.sessionId);
|
|
6725
|
-
let archived = 0;
|
|
6726
|
-
for (const message of messages) {
|
|
6727
|
-
const rawId = message.info.id;
|
|
6728
|
-
if (!rawId) continue;
|
|
6729
|
-
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
6730
|
-
for (let partIndex = 0; partIndex < parts.length; partIndex++) {
|
|
6731
|
-
const part = parts[partIndex];
|
|
6732
|
-
if (part.type !== "file") continue;
|
|
6733
|
-
const parsed = parseImageDataUrl(part.url);
|
|
6734
|
-
if (!parsed) continue;
|
|
6735
|
-
const ext = extForMediaType(parsed.mediaType);
|
|
6736
|
-
if (!ext) continue;
|
|
6737
|
-
const fileName = `${sha8(rawId)}-p${partIndex}-${sha8(parsed.base64)}.${ext}`;
|
|
6738
|
-
const filePath = join4(root, fileName);
|
|
6739
|
-
const existing = state.imageArchive[rawId]?.find((e) => e.path === filePath);
|
|
6740
|
-
if (existing) continue;
|
|
6741
|
-
let bytes;
|
|
6742
|
-
try {
|
|
6743
|
-
if (existsSync4(filePath)) {
|
|
6744
|
-
bytes = statSync2(filePath).size;
|
|
6745
|
-
} else {
|
|
6746
|
-
const buffer = Buffer.from(parsed.base64, "base64");
|
|
6747
|
-
bytes = buffer.byteLength;
|
|
6748
|
-
if (bytes < images.minBytes) continue;
|
|
6749
|
-
mkdirSync2(root, { recursive: true });
|
|
6750
|
-
writeFileSync2(filePath, buffer, { mode: 384 });
|
|
6751
|
-
}
|
|
6752
|
-
} catch (error) {
|
|
6753
|
-
logger.warn(
|
|
6754
|
-
`[#478] failed to archive image ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
6755
|
-
);
|
|
6756
|
-
continue;
|
|
6757
|
-
}
|
|
6758
|
-
const entry = {
|
|
6759
|
-
path: filePath,
|
|
6760
|
-
mediaType: parsed.mediaType,
|
|
6761
|
-
bytes
|
|
6762
|
-
};
|
|
6763
|
-
if (!state.imageArchive[rawId]) {
|
|
6764
|
-
state.imageArchive[rawId] = [];
|
|
6765
|
-
}
|
|
6766
|
-
state.imageArchive[rawId].push(entry);
|
|
6767
|
-
state.stats.imagesArchived = (state.stats.imagesArchived ?? 0) + 1;
|
|
6768
|
-
state.stats.imageBytesArchived = (state.stats.imageBytesArchived ?? 0) + bytes;
|
|
6769
|
-
archived++;
|
|
6770
|
-
}
|
|
6771
|
-
}
|
|
6772
|
-
return archived;
|
|
6773
|
-
}
|
|
6774
|
-
function pruneImageArchive(state, images, logger) {
|
|
6775
|
-
if (state.imagePruneDone || !state.sessionId) return;
|
|
6776
|
-
state.imagePruneDone = true;
|
|
6777
|
-
try {
|
|
6778
|
-
const root = archiveRoot(state.storageDir, state.sessionId);
|
|
6779
|
-
if (!existsSync4(root)) return;
|
|
6780
|
-
const cutoff = Date.now() - images.ttlDays * 864e5;
|
|
6781
|
-
const prunedPaths = /* @__PURE__ */ new Set();
|
|
6782
|
-
for (const name of readdirSync(root)) {
|
|
6783
|
-
const filePath = join4(root, name);
|
|
6784
|
-
try {
|
|
6785
|
-
if (statSync2(filePath).mtimeMs < cutoff) {
|
|
6786
|
-
unlinkSync(filePath);
|
|
6787
|
-
prunedPaths.add(filePath);
|
|
6788
|
-
}
|
|
6789
|
-
} catch {
|
|
6790
|
-
}
|
|
6791
|
-
}
|
|
6792
|
-
if (prunedPaths.size > 0) {
|
|
6793
|
-
for (const [rawId, entries] of Object.entries(state.imageArchive)) {
|
|
6794
|
-
const kept = entries.filter((e) => !prunedPaths.has(e.path));
|
|
6795
|
-
if (kept.length === 0) {
|
|
6796
|
-
delete state.imageArchive[rawId];
|
|
6797
|
-
} else if (kept.length !== entries.length) {
|
|
6798
|
-
state.imageArchive[rawId] = kept;
|
|
6799
|
-
}
|
|
6800
|
-
}
|
|
6801
|
-
logger.info(`[#478] TTL sweep removed ${prunedPaths.size} expired archived image(s)`);
|
|
6802
|
-
}
|
|
6803
|
-
if (readdirSync(root).length === 0) {
|
|
6804
|
-
rmdirSync(root);
|
|
6805
|
-
}
|
|
6806
|
-
} catch (error) {
|
|
6807
|
-
logger.warn(
|
|
6808
|
-
`[#478] image TTL sweep failed: ${error instanceof Error ? error.message : String(error)}`
|
|
6809
|
-
);
|
|
6810
|
-
}
|
|
6811
|
-
}
|
|
6812
|
-
function appendArchivedImagePointers(summary, selection, state, enabled) {
|
|
6813
|
-
if (!enabled) return summary;
|
|
6814
|
-
const lines = [];
|
|
6815
|
-
for (const messageId of selection.messageIds) {
|
|
6816
|
-
const entries = state.imageArchive[messageId];
|
|
6817
|
-
if (!entries || entries.length === 0) continue;
|
|
6818
|
-
const ref = state.messageIds.byRawId.get(messageId) ?? messageId;
|
|
6819
|
-
for (const entry of entries) {
|
|
6820
|
-
lines.push(`- ${ref}: ${entry.path} (${entry.mediaType}, ${entry.bytes} bytes)`);
|
|
6821
|
-
}
|
|
6822
|
-
}
|
|
6823
|
-
if (lines.length === 0) return summary;
|
|
6824
|
-
const heading = "\n\nImages captured in this range were archived to disk (their pixels are NOT summarized). Re-read them with your file tools if you need the pixels again:";
|
|
6825
|
-
return summary + heading + "\n" + lines.join("\n");
|
|
6826
|
-
}
|
|
6827
|
-
|
|
6828
7068
|
// lib/compress/keep-markers.ts
|
|
6829
7069
|
var KEEP_REGEX = /\[\[KEEP:(m\d+)\]\]/g;
|
|
6830
7070
|
var REF_REGEX = /\[\[REF:(m\d+)\|([^\]]+)\]\]/g;
|
|
@@ -7576,6 +7816,7 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
|
|
|
7576
7816
|
let messageTokens = 0;
|
|
7577
7817
|
let protectedTokens = 0;
|
|
7578
7818
|
let reasoningTokens = 0;
|
|
7819
|
+
let imageTokens = 0;
|
|
7579
7820
|
const perMessage = [];
|
|
7580
7821
|
const perTool = [];
|
|
7581
7822
|
const perCode = [];
|
|
@@ -7633,6 +7874,12 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
|
|
|
7633
7874
|
const tokens = Math.round(part.text.length / 4);
|
|
7634
7875
|
msgTotal += tokens;
|
|
7635
7876
|
reasoningTokens += tokens;
|
|
7877
|
+
} else if (part.type === "file") {
|
|
7878
|
+
const imgTokens = estimateImagePartTokens(part);
|
|
7879
|
+
if (imgTokens > 0) {
|
|
7880
|
+
msgTotal += imgTokens;
|
|
7881
|
+
imageTokens += imgTokens;
|
|
7882
|
+
}
|
|
7636
7883
|
}
|
|
7637
7884
|
}
|
|
7638
7885
|
if (isProtected && !isSummary) {
|
|
@@ -7661,7 +7908,8 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
|
|
|
7661
7908
|
systemTokens,
|
|
7662
7909
|
protectedTokens,
|
|
7663
7910
|
reasoningTokens,
|
|
7664
|
-
|
|
7911
|
+
imageTokens,
|
|
7912
|
+
total: systemTokens + toolTokens + summaryTokens + messageTokens + reasoningTokens + imageTokens,
|
|
7665
7913
|
largestRanges: perMessage.slice(0, 15),
|
|
7666
7914
|
largestToolRanges: perTool.slice(0, 15),
|
|
7667
7915
|
largestCodeRanges: perCode.slice(0, 5),
|
|
@@ -7942,6 +8190,8 @@ function computeProtectedRefs(messages, state, compress) {
|
|
|
7942
8190
|
for (const part of msg.parts || []) {
|
|
7943
8191
|
if (part.type === "text" && typeof part.text === "string") {
|
|
7944
8192
|
tokens += Math.round(part.text.length / 4);
|
|
8193
|
+
} else if (part.type === "file") {
|
|
8194
|
+
tokens += estimateImagePartTokens(part);
|
|
7945
8195
|
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
7946
8196
|
tokens += Math.round(JSON.stringify(part).length / 4);
|
|
7947
8197
|
}
|
|
@@ -8359,7 +8609,7 @@ function createCompressRangeTool(factoryCtx) {
|
|
|
8359
8609
|
);
|
|
8360
8610
|
preparedPlan.finalSummary = keepResult.summary;
|
|
8361
8611
|
const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
|
|
8362
|
-
const summaryTokens =
|
|
8612
|
+
const summaryTokens = countTokens(storedSummary);
|
|
8363
8613
|
const applied = applyCompressionState(
|
|
8364
8614
|
ctx.state,
|
|
8365
8615
|
{
|
|
@@ -8696,6 +8946,73 @@ var syncCompressPermissionState = (state, config, hostPermissions, messages) =>
|
|
|
8696
8946
|
);
|
|
8697
8947
|
};
|
|
8698
8948
|
|
|
8949
|
+
// lib/messages/large-images.ts
|
|
8950
|
+
var IMAGE_SURGE_DEDUP_CAP = 512;
|
|
8951
|
+
function detectNewLargeImages(messages, state, largeBytes) {
|
|
8952
|
+
if (!Number.isFinite(largeBytes) || largeBytes <= 0) return [];
|
|
8953
|
+
const nudged = state.imageSurgeNudged ??= {};
|
|
8954
|
+
const found = [];
|
|
8955
|
+
for (const message of messages) {
|
|
8956
|
+
const rawId = message.info.id;
|
|
8957
|
+
if (!rawId) continue;
|
|
8958
|
+
const ref = state.messageIds.byRawId.get(rawId);
|
|
8959
|
+
if (!ref) continue;
|
|
8960
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
8961
|
+
for (let partIndex = 0; partIndex < parts.length; partIndex++) {
|
|
8962
|
+
const part = parts[partIndex];
|
|
8963
|
+
if (part.type !== "file") continue;
|
|
8964
|
+
const parsed = parseImageDataUrl(part.url);
|
|
8965
|
+
if (!parsed) continue;
|
|
8966
|
+
if (!extForMediaType(parsed.mediaType)) continue;
|
|
8967
|
+
const bytes = decodedBytesFromBase64(parsed.base64);
|
|
8968
|
+
if (bytes < largeBytes) continue;
|
|
8969
|
+
const slotPrefix = `${sha8(rawId)}:p${partIndex}:`;
|
|
8970
|
+
let slotSeen = false;
|
|
8971
|
+
for (const key2 of Object.keys(nudged)) {
|
|
8972
|
+
if (key2.startsWith(slotPrefix)) {
|
|
8973
|
+
slotSeen = true;
|
|
8974
|
+
break;
|
|
8975
|
+
}
|
|
8976
|
+
}
|
|
8977
|
+
if (slotSeen) continue;
|
|
8978
|
+
const key = `${slotPrefix}${sha8(parsed.base64)}`;
|
|
8979
|
+
if (nudged[key] !== void 0) continue;
|
|
8980
|
+
nudged[key] = Date.now();
|
|
8981
|
+
found.push({
|
|
8982
|
+
ref,
|
|
8983
|
+
mediaType: parsed.mediaType,
|
|
8984
|
+
bytes,
|
|
8985
|
+
estimatedTokens: estimateImagePartTokens(part)
|
|
8986
|
+
});
|
|
8987
|
+
}
|
|
8988
|
+
}
|
|
8989
|
+
const keys = Object.keys(nudged);
|
|
8990
|
+
if (keys.length > IMAGE_SURGE_DEDUP_CAP) {
|
|
8991
|
+
for (const key of keys.slice(0, keys.length - IMAGE_SURGE_DEDUP_CAP)) {
|
|
8992
|
+
delete nudged[key];
|
|
8993
|
+
}
|
|
8994
|
+
}
|
|
8995
|
+
return found;
|
|
8996
|
+
}
|
|
8997
|
+
function formatBytes(bytes) {
|
|
8998
|
+
if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
8999
|
+
return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
|
9000
|
+
}
|
|
9001
|
+
function formatLargeImageNotice(images) {
|
|
9002
|
+
const items = images.map(
|
|
9003
|
+
(image) => `- ${image.ref}: ${image.mediaType}, ${formatBytes(image.bytes)} (\u2248${formatTokens(image.estimatedTokens)} vision tokens; far more if your provider bills base64 as text)`
|
|
9004
|
+
).join("\n");
|
|
9005
|
+
return `
|
|
9006
|
+
|
|
9007
|
+
\u{1F5BC}\uFE0F Large image(s) just entered context:
|
|
9008
|
+
${items}
|
|
9009
|
+
Before doing anything else, make room: call the \`compress\` tool NOW on older conversation ranges you no longer need (acp_status lists them). Once you are done analyzing the image(s), compress the range(s) containing them \u2014 archived copies stay re-readable via their disk pointers.`;
|
|
9010
|
+
}
|
|
9011
|
+
function formatTokens(n) {
|
|
9012
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
9013
|
+
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
9014
|
+
}
|
|
9015
|
+
|
|
8699
9016
|
// lib/prompts/context-limit-nudge.ts
|
|
8700
9017
|
var CONTEXT_LIMIT_NUDGE = `
|
|
8701
9018
|
<system-reminder>
|
|
@@ -9513,6 +9830,21 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
9513
9830
|
let shouldInjectNotice = emergencyNoTargets && noticeCadenceMet;
|
|
9514
9831
|
let shouldInject = shouldInjectNudge || shouldInjectNotice;
|
|
9515
9832
|
let armedSmartPlan;
|
|
9833
|
+
const imagesConfig = config.compress?.images ?? DEFAULT_COMPRESS_IMAGES;
|
|
9834
|
+
let imageNoticeEmitted = false;
|
|
9835
|
+
if (suffixMessage && imagesConfig.nudge !== false && imagesConfig.largeBytes > 0) {
|
|
9836
|
+
const newLargeImages = detectNewLargeImages(messages, state, imagesConfig.largeBytes);
|
|
9837
|
+
if (newLargeImages.length > 0) {
|
|
9838
|
+
appendToLastTextPart(suffixMessage, formatLargeImageNotice(newLargeImages));
|
|
9839
|
+
imageNoticeEmitted = true;
|
|
9840
|
+
saveSessionState(state, logger).catch(() => {
|
|
9841
|
+
});
|
|
9842
|
+
logger.info("Large image notice injected", {
|
|
9843
|
+
session: state.sessionId,
|
|
9844
|
+
images: newLargeImages.map((image) => `${image.ref}:${image.mediaType}:${image.bytes}B`)
|
|
9845
|
+
});
|
|
9846
|
+
}
|
|
9847
|
+
}
|
|
9516
9848
|
if (shouldInjectNudge && config.compress.smartPlanRequired === true && state.sessionId) {
|
|
9517
9849
|
const planningRanges = candidatesEnabled ? candidatePlan.candidates.map((candidate) => ({
|
|
9518
9850
|
startRef: candidate.startRef,
|
|
@@ -9575,7 +9907,7 @@ ${ITERATION_CANDIDATE_GUIDANCE}` : ITERATION_CANDIDATE_GUIDANCE
|
|
|
9575
9907
|
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
9576
9908
|
baselineReEstablished = true;
|
|
9577
9909
|
}
|
|
9578
|
-
if (suffixMessage && !shouldInject) {
|
|
9910
|
+
if (suffixMessage && !shouldInject && !imageNoticeEmitted) {
|
|
9579
9911
|
const tierUsage = tierUsageEarly;
|
|
9580
9912
|
const tierChecks = [
|
|
9581
9913
|
{
|
|
@@ -9678,7 +10010,7 @@ ${rules}`;
|
|
|
9678
10010
|
break;
|
|
9679
10011
|
}
|
|
9680
10012
|
}
|
|
9681
|
-
state.nudges.shouldInjectThisTurn = shouldInject;
|
|
10013
|
+
state.nudges.shouldInjectThisTurn = shouldInject || imageNoticeEmitted;
|
|
9682
10014
|
const usagePct = currentTokens !== void 0 && modelContextLimit ? `${(currentTokens / modelContextLimit * 100).toFixed(1)}%` : void 0;
|
|
9683
10015
|
if (shouldInjectNudge) {
|
|
9684
10016
|
logger.info("Compression nudge injected", {
|
|
@@ -9738,7 +10070,7 @@ ${rules}`;
|
|
|
9738
10070
|
const efficiencyNote = effectiveTipsVariant !== "maxLimit" ? candidatesEnabled ? "\nThis is an efficiency nudge to compress early when content is no longer needed and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.\n\n" + COMPRESS_PHILOSOPHY : "\nThis is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.\n\n" + COMPRESS_PHILOSOPHY : "";
|
|
9739
10071
|
const sysPart = composition.systemTokens > 0 ? `${fmt(composition.systemTokens)} system (${pct2(composition.systemTokens)}%) | ` : "";
|
|
9740
10072
|
let breakdown = `${efficiencyNote}
|
|
9741
|
-
Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct2(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct2(plainTextTokens)}%) | ${fmt(composition.reasoningTokens)} reasoning (${pct2(composition.reasoningTokens)}%)${growthStr}`;
|
|
10073
|
+
Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct2(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct2(plainTextTokens)}%) | ${fmt(composition.reasoningTokens)} reasoning (${pct2(composition.reasoningTokens)}%) | ${fmt(composition.imageTokens)} images (${pct2(composition.imageTokens)}%)${growthStr}`;
|
|
9742
10074
|
const compressibleTokens = composition.total - composition.systemTokens - composition.protectedTokens - composition.summaryTokens;
|
|
9743
10075
|
if (composition.protectedTokens > 0) {
|
|
9744
10076
|
breakdown += `
|
|
@@ -10698,7 +11030,7 @@ Use this tool to:
|
|
|
10698
11030
|
- Focus on ranges only (scope:"uncompressed")
|
|
10699
11031
|
- Find all messages of a specific tool type (scope:"uncompressed", view:"messages", tool:"bash")
|
|
10700
11032
|
- Check block details before decompressing (scope:"compressed")`;
|
|
10701
|
-
function
|
|
11033
|
+
function formatTokens2(n) {
|
|
10702
11034
|
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
10703
11035
|
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
10704
11036
|
}
|
|
@@ -10742,7 +11074,7 @@ function tierBreakdown(blocks) {
|
|
|
10742
11074
|
const parts = [];
|
|
10743
11075
|
for (const t of [1, 2, 3]) {
|
|
10744
11076
|
if (tierTokens[t]) {
|
|
10745
|
-
parts.push(`T${t}: ${
|
|
11077
|
+
parts.push(`T${t}: ${formatTokens2(tierTokens[t])}`);
|
|
10746
11078
|
}
|
|
10747
11079
|
}
|
|
10748
11080
|
return parts.join(" | ");
|
|
@@ -10768,10 +11100,13 @@ function collectVisibleMessages(rawMessages, ctx) {
|
|
|
10768
11100
|
if (!ref) return;
|
|
10769
11101
|
let tokens = 0;
|
|
10770
11102
|
let reasoning = 0;
|
|
11103
|
+
let image = 0;
|
|
10771
11104
|
let toolName = "";
|
|
10772
11105
|
for (const part of msg.parts || []) {
|
|
10773
11106
|
if (part.type === "text" && typeof part.text === "string") {
|
|
10774
11107
|
tokens += Math.round(part.text.length / 4);
|
|
11108
|
+
} else if (part.type === "file") {
|
|
11109
|
+
image += estimateImagePartTokens(part);
|
|
10775
11110
|
} else if (part.type === "tool") {
|
|
10776
11111
|
const raw = JSON.stringify(part);
|
|
10777
11112
|
tokens += Math.round(raw.length / 4);
|
|
@@ -10782,8 +11117,8 @@ function collectVisibleMessages(rawMessages, ctx) {
|
|
|
10782
11117
|
reasoning += Math.round(part.text.length / 4);
|
|
10783
11118
|
}
|
|
10784
11119
|
}
|
|
10785
|
-
if (tokens > 0 || reasoning > 0) {
|
|
10786
|
-
result.push({ ref, tokens, tool: toolName || "text", index: idx, reasoning });
|
|
11120
|
+
if (tokens > 0 || reasoning > 0 || image > 0) {
|
|
11121
|
+
result.push({ ref, tokens, tool: toolName || "text", index: idx, reasoning, image });
|
|
10787
11122
|
}
|
|
10788
11123
|
});
|
|
10789
11124
|
return {
|
|
@@ -10806,15 +11141,17 @@ function renderOverview(visibleMessages, summaryTokens, systemTokens, blocks, fe
|
|
|
10806
11141
|
const totalTool = visibleMessages.filter((m) => m.tool !== "text" && m.tool !== "step-finish").reduce((s, m) => s + m.tokens, 0);
|
|
10807
11142
|
const totalText = visibleMessages.filter((m) => m.tool === "text").reduce((s, m) => s + m.tokens, 0);
|
|
10808
11143
|
const totalReasoning = visibleMessages.reduce((s, m) => s + m.reasoning, 0);
|
|
10809
|
-
const
|
|
11144
|
+
const totalImage = visibleMessages.reduce((s, m) => s + m.image, 0);
|
|
11145
|
+
const total = systemTokens + totalTool + totalText + summaryTokens + totalReasoning + totalImage;
|
|
10810
11146
|
const sysPct = pct(systemTokens, total);
|
|
10811
11147
|
const toolPct = pct(totalTool, total);
|
|
10812
11148
|
const textPct = pct(totalText, total);
|
|
10813
11149
|
const summaryPct = pct(summaryTokens, total);
|
|
10814
11150
|
const reasoningPct = pct(totalReasoning, total);
|
|
11151
|
+
const imagePct = pct(totalImage, total);
|
|
10815
11152
|
lines.push("CONTEXT BREAKDOWN");
|
|
10816
11153
|
lines.push(
|
|
10817
|
-
` ${
|
|
11154
|
+
` ${formatTokens2(systemTokens)} system (${sysPct}%) | ${formatTokens2(totalTool)} tool (${toolPct}%) | ${formatTokens2(totalText)} text (${textPct}%) | ${formatTokens2(summaryTokens)} summaries (${summaryPct}%) | ${formatTokens2(totalReasoning)} reasoning (${reasoningPct}%) | ${formatTokens2(totalImage)} images (${imagePct}%)`
|
|
10818
11155
|
);
|
|
10819
11156
|
const topTypes = Array.from(toolTypeMap.entries()).map(([tool6, tokens]) => ({ tool: tool6, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
|
|
10820
11157
|
if (topTypes.length > 0) {
|
|
@@ -10834,7 +11171,7 @@ function renderOverview(visibleMessages, summaryTokens, systemTokens, blocks, fe
|
|
|
10834
11171
|
(s, b) => s + getEffectiveCompressedTokens(b, blocksById),
|
|
10835
11172
|
0
|
|
10836
11173
|
);
|
|
10837
|
-
const header = `COMPRESSED BLOCKS \u2014 ${blocks.length} active (${
|
|
11174
|
+
const header = `COMPRESSED BLOCKS \u2014 ${blocks.length} active (${formatTokens2(totalSummary)} summary, ${formatTokens2(totalEffective)} original)`;
|
|
10838
11175
|
lines.push(header);
|
|
10839
11176
|
const breakdown = tierBreakdown(blocks);
|
|
10840
11177
|
if (breakdown) {
|
|
@@ -10853,7 +11190,7 @@ function renderOverview(visibleMessages, summaryTokens, systemTokens, blocks, fe
|
|
|
10853
11190
|
const tier = tierLabel(b);
|
|
10854
11191
|
const effTokens = getEffectiveCompressedTokens(b, blocksById);
|
|
10855
11192
|
lines.push(
|
|
10856
|
-
` b${b.blockId} (${tier}) ${
|
|
11193
|
+
` b${b.blockId} (${tier}) ${formatTokens2(effTokens)}\u2192${formatTokens2(b.summaryTokens)} ${ageStr} ${range} "${topic}"`
|
|
10857
11194
|
);
|
|
10858
11195
|
}
|
|
10859
11196
|
}
|
|
@@ -10914,7 +11251,7 @@ function renderUncompressedRanges(rawMessages, ctx) {
|
|
|
10914
11251
|
const totalMsgs = compressible.reduce((s, r) => s + r.count, 0);
|
|
10915
11252
|
const lines = [];
|
|
10916
11253
|
lines.push(
|
|
10917
|
-
`UNCOMPRESSED \u2014 ${
|
|
11254
|
+
`UNCOMPRESSED \u2014 ${formatTokens2(totalTokens)} | ${totalMsgs} msgs in ${compressible.length} ranges`
|
|
10918
11255
|
);
|
|
10919
11256
|
lines.push("");
|
|
10920
11257
|
if (compressible.length === 0 && contextRanges.protected.length === 0) {
|
|
@@ -10971,7 +11308,7 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
|
|
|
10971
11308
|
if (toolFilter) {
|
|
10972
11309
|
filtered = filtered.filter((m) => m.tool === toolFilter);
|
|
10973
11310
|
}
|
|
10974
|
-
const sizeOf = (m) => m.tokens + m.reasoning;
|
|
11311
|
+
const sizeOf = (m) => m.tokens + m.reasoning + m.image;
|
|
10975
11312
|
if (sort === "time") {
|
|
10976
11313
|
filtered.sort((a, b) => a.index - b.index);
|
|
10977
11314
|
} else if (sort === "tool") {
|
|
@@ -10981,13 +11318,13 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
|
|
|
10981
11318
|
}
|
|
10982
11319
|
const totalTokens = filtered.reduce((s, m) => s + sizeOf(m), 0);
|
|
10983
11320
|
const allTokens = visibleMessages.reduce((s, m) => s + sizeOf(m), 0);
|
|
10984
|
-
const header = toolFilter ? `UNCOMPRESSED \u2014 ${toolFilter}: ${
|
|
11321
|
+
const header = toolFilter ? `UNCOMPRESSED \u2014 ${toolFilter}: ${formatTokens2(totalTokens)} | ${filtered.length} msgs | ${pct(totalTokens, allTokens)}% of visible` : `UNCOMPRESSED \u2014 ${formatTokens2(totalTokens)} | ${filtered.length} msgs`;
|
|
10985
11322
|
lines.push(header);
|
|
10986
11323
|
lines.push(`Sorted by ${sort}`);
|
|
10987
11324
|
lines.push("");
|
|
10988
11325
|
const shown = filtered.slice(0, limit);
|
|
10989
11326
|
for (const m of shown) {
|
|
10990
|
-
lines.push(` ${m.ref} (${
|
|
11327
|
+
lines.push(` ${m.ref} (${formatTokens2(sizeOf(m))}) ${m.tool}`);
|
|
10991
11328
|
}
|
|
10992
11329
|
if (filtered.length > shown.length) {
|
|
10993
11330
|
lines.push("");
|
|
@@ -11026,7 +11363,7 @@ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
|
|
|
11026
11363
|
0
|
|
11027
11364
|
);
|
|
11028
11365
|
lines.push(
|
|
11029
|
-
`COMPRESSED \u2014 ${sorted.length} blocks | ${
|
|
11366
|
+
`COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens2(totalEffective)} original \u2192 ${formatTokens2(totalSummary)} summary`
|
|
11030
11367
|
);
|
|
11031
11368
|
const breakdown = tierBreakdown(sorted);
|
|
11032
11369
|
if (breakdown) {
|
|
@@ -11051,7 +11388,7 @@ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
|
|
|
11051
11388
|
const tier = tierLabel(b);
|
|
11052
11389
|
const effTokens = getEffectiveCompressedTokens(b, blocksById);
|
|
11053
11390
|
lines.push(
|
|
11054
|
-
` b${b.blockId} (${tier}) ${
|
|
11391
|
+
` b${b.blockId} (${tier}) ${formatTokens2(effTokens)}\u2192${formatTokens2(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}${status}`
|
|
11055
11392
|
);
|
|
11056
11393
|
lines.push(` "${topic}"`);
|
|
11057
11394
|
}
|
|
@@ -11329,7 +11666,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
|
11329
11666
|
import { join as join5 } from "path";
|
|
11330
11667
|
import { existsSync as existsSync5 } from "fs";
|
|
11331
11668
|
import { homedir as homedir3 } from "os";
|
|
11332
|
-
var LOG_VERSION = true ? "1.18.
|
|
11669
|
+
var LOG_VERSION = true ? "1.18.5-pr.490.223" : "dev";
|
|
11333
11670
|
var LEVEL_RANK = {
|
|
11334
11671
|
debug: 10,
|
|
11335
11672
|
info: 20,
|
|
@@ -11780,6 +12117,8 @@ NOTE: Message-mode blocks created in the same batch (same runId) are restored to
|
|
|
11780
12117
|
Decompressing one block from a batch restores all blocks in that batch.
|
|
11781
12118
|
</dcp-system-reminder>
|
|
11782
12119
|
`;
|
|
12120
|
+
var IMAGE_GUARD_SYSTEM_EXTENSION = `
|
|
12121
|
+
IMAGE HANDLING: Before reading an image file into context, make sure there is room: if context usage is high, compress older conversation ranges first. A large image consumes significant context for as long as it stays uncompressed. Once you are done analyzing an image, prefer compressing the range that contains it \u2014 archived images remain re-readable through their disk pointers.`;
|
|
11783
12122
|
|
|
11784
12123
|
// lib/prompts/store.ts
|
|
11785
12124
|
var PROMPT_DEFINITIONS = [
|
|
@@ -12116,7 +12455,7 @@ var PromptStore = class {
|
|
|
12116
12455
|
};
|
|
12117
12456
|
|
|
12118
12457
|
// lib/prompts/index.ts
|
|
12119
|
-
function renderSystemPrompt(prompts, protectedToolsExtension, subagent) {
|
|
12458
|
+
function renderSystemPrompt(prompts, protectedToolsExtension, subagent, imageGuardExtension) {
|
|
12120
12459
|
const extensions = [];
|
|
12121
12460
|
if (protectedToolsExtension) {
|
|
12122
12461
|
extensions.push(protectedToolsExtension.trim());
|
|
@@ -12124,6 +12463,9 @@ function renderSystemPrompt(prompts, protectedToolsExtension, subagent) {
|
|
|
12124
12463
|
if (subagent) {
|
|
12125
12464
|
extensions.push(prompts.subagentExtension.trim());
|
|
12126
12465
|
}
|
|
12466
|
+
if (imageGuardExtension) {
|
|
12467
|
+
extensions.push(imageGuardExtension.trim());
|
|
12468
|
+
}
|
|
12127
12469
|
extensions.push(prompts.decompressExtension.trim());
|
|
12128
12470
|
return [prompts.system.trim(), ...extensions].filter(Boolean).join("\n\n").replace(/\n([ \t]*\n)+/g, "\n\n").trim();
|
|
12129
12471
|
}
|
|
@@ -12184,11 +12526,12 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
|
|
|
12184
12526
|
const part = parts[pi];
|
|
12185
12527
|
if (part?.type !== "tool") continue;
|
|
12186
12528
|
if (part.state?.status !== "completed") continue;
|
|
12529
|
+
if (part.tool === "compress") continue;
|
|
12187
12530
|
const content = extractCompletedToolOutput(part);
|
|
12188
12531
|
if (content === void 0) continue;
|
|
12189
12532
|
if (content === "[Old tool result content cleared]") continue;
|
|
12190
12533
|
if (content.includes(TRUNCATION_MARKER)) continue;
|
|
12191
|
-
const tokens =
|
|
12534
|
+
const tokens = countTokens(content);
|
|
12192
12535
|
if (tokens < MIN_OUTPUT_TOKENS) continue;
|
|
12193
12536
|
candidates.push({ part, content, tokens });
|
|
12194
12537
|
}
|
|
@@ -12209,7 +12552,7 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
|
|
|
12209
12552
|
|
|
12210
12553
|
` + suffix;
|
|
12211
12554
|
part.state.output = truncated;
|
|
12212
|
-
savedTokens += tokens -
|
|
12555
|
+
savedTokens += tokens - countTokens(truncated);
|
|
12213
12556
|
truncatedCount++;
|
|
12214
12557
|
}
|
|
12215
12558
|
if (truncatedCount > 0) {
|
|
@@ -12224,6 +12567,74 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
|
|
|
12224
12567
|
}
|
|
12225
12568
|
}
|
|
12226
12569
|
|
|
12570
|
+
// lib/messages/sticky-truncation.ts
|
|
12571
|
+
function collectToolParts(messages) {
|
|
12572
|
+
const parts = [];
|
|
12573
|
+
for (const message of messages) {
|
|
12574
|
+
const messageParts = Array.isArray(message.parts) ? message.parts : [];
|
|
12575
|
+
for (const part of messageParts) {
|
|
12576
|
+
if (part?.type === "tool") {
|
|
12577
|
+
parts.push(part);
|
|
12578
|
+
}
|
|
12579
|
+
}
|
|
12580
|
+
}
|
|
12581
|
+
return parts;
|
|
12582
|
+
}
|
|
12583
|
+
function applyStickyTruncations(state, messages, logger) {
|
|
12584
|
+
const frozen = state.stickyTruncations;
|
|
12585
|
+
if (Object.keys(frozen).length === 0) return;
|
|
12586
|
+
let replayed = 0;
|
|
12587
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12588
|
+
for (const part of collectToolParts(messages)) {
|
|
12589
|
+
const callID = typeof part.callID === "string" ? part.callID : void 0;
|
|
12590
|
+
if (!callID) continue;
|
|
12591
|
+
seen.add(callID);
|
|
12592
|
+
const recorded = frozen[callID];
|
|
12593
|
+
if (recorded === void 0) continue;
|
|
12594
|
+
if (!part.state) continue;
|
|
12595
|
+
if (part.state.output === recorded) continue;
|
|
12596
|
+
part.state.output = recorded;
|
|
12597
|
+
replayed++;
|
|
12598
|
+
}
|
|
12599
|
+
for (const callID of Object.keys(frozen)) {
|
|
12600
|
+
if (!seen.has(callID)) delete frozen[callID];
|
|
12601
|
+
}
|
|
12602
|
+
if (replayed > 0) {
|
|
12603
|
+
logger.debug("Sticky truncation: replayed frozen tool outputs", {
|
|
12604
|
+
replayed,
|
|
12605
|
+
frozen: Object.keys(frozen).length
|
|
12606
|
+
});
|
|
12607
|
+
}
|
|
12608
|
+
}
|
|
12609
|
+
function snapshotToolOutputs(messages) {
|
|
12610
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
12611
|
+
for (const part of collectToolParts(messages)) {
|
|
12612
|
+
const callID = typeof part.callID === "string" ? part.callID : void 0;
|
|
12613
|
+
if (!callID) continue;
|
|
12614
|
+
const content = extractCompletedToolOutput(part);
|
|
12615
|
+
if (content === void 0) continue;
|
|
12616
|
+
snapshot.set(callID, content);
|
|
12617
|
+
}
|
|
12618
|
+
return snapshot;
|
|
12619
|
+
}
|
|
12620
|
+
function recordStickyTruncations(state, messages, before, logger) {
|
|
12621
|
+
let recorded = 0;
|
|
12622
|
+
for (const part of collectToolParts(messages)) {
|
|
12623
|
+
const callID = typeof part.callID === "string" ? part.callID : void 0;
|
|
12624
|
+
if (!callID) continue;
|
|
12625
|
+
if (typeof part.state?.output !== "string") continue;
|
|
12626
|
+
const prior = before.get(callID);
|
|
12627
|
+
if (prior === void 0 || prior === part.state.output) continue;
|
|
12628
|
+
if (state.stickyTruncations[callID] === part.state.output) continue;
|
|
12629
|
+
state.stickyTruncations[callID] = part.state.output;
|
|
12630
|
+
recorded++;
|
|
12631
|
+
}
|
|
12632
|
+
if (recorded > 0) {
|
|
12633
|
+
logger.info("Sticky truncation: froze truncated tool outputs", { recorded });
|
|
12634
|
+
}
|
|
12635
|
+
return recorded;
|
|
12636
|
+
}
|
|
12637
|
+
|
|
12227
12638
|
// lib/messages/enforce-budget.ts
|
|
12228
12639
|
var DEFAULT_COMPLETION_RESERVE_TOKENS = 32768;
|
|
12229
12640
|
var TRUNCATION_MARKER2 = "[truncated for context space";
|
|
@@ -12295,7 +12706,7 @@ function enforceContextBudget(state, config, logger, messages) {
|
|
|
12295
12706
|
const content = extractCompletedToolOutput(part);
|
|
12296
12707
|
if (content === void 0) continue;
|
|
12297
12708
|
if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
|
|
12298
|
-
const tokens =
|
|
12709
|
+
const tokens = countTokens(content);
|
|
12299
12710
|
if (tokens <= 0) continue;
|
|
12300
12711
|
candidates.push({ part, content, tokens, index: mi });
|
|
12301
12712
|
}
|
|
@@ -12317,20 +12728,20 @@ function enforceContextBudget(state, config, logger, messages) {
|
|
|
12317
12728
|
` + suffix;
|
|
12318
12729
|
if (truncated.length >= c.content.length) continue;
|
|
12319
12730
|
c.part.state.output = truncated;
|
|
12320
|
-
saved += c.tokens -
|
|
12731
|
+
saved += c.tokens - countTokens(truncated);
|
|
12321
12732
|
truncatedCount++;
|
|
12322
12733
|
}
|
|
12323
12734
|
if (estimatedTokens - saved > budget) {
|
|
12324
12735
|
const clearable = candidates.filter((c) => {
|
|
12325
12736
|
const out = extractCompletedToolOutput(c.part);
|
|
12326
|
-
return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER &&
|
|
12737
|
+
return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens(out) > MIN_CLEAR_TOKENS;
|
|
12327
12738
|
}).sort((a, b) => a.index - b.index);
|
|
12328
12739
|
for (const c of clearable) {
|
|
12329
12740
|
if (estimatedTokens - saved <= budget) break;
|
|
12330
12741
|
const current = extractCompletedToolOutput(c.part);
|
|
12331
12742
|
if (current === void 0 || current === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
|
|
12332
12743
|
c.part.state.output = COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
|
|
12333
|
-
saved +=
|
|
12744
|
+
saved += countTokens(current) - countTokens(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
|
|
12334
12745
|
clearedCount++;
|
|
12335
12746
|
}
|
|
12336
12747
|
}
|
|
@@ -12474,10 +12885,10 @@ function analyzeTokens(state, messages) {
|
|
|
12474
12885
|
breakdown.toolsInContextCount = toolsInContextCount;
|
|
12475
12886
|
breakdown.prunedToolCount = prunedToolIds.size;
|
|
12476
12887
|
breakdown.prunedMessageCount = prunedMessageCount;
|
|
12477
|
-
const firstUserTokens =
|
|
12478
|
-
breakdown.user =
|
|
12479
|
-
const toolInputTokens =
|
|
12480
|
-
const toolOutputTokens =
|
|
12888
|
+
const firstUserTokens = countTokens(firstUserText);
|
|
12889
|
+
breakdown.user = countTokens(userTextParts.join("\n"));
|
|
12890
|
+
const toolInputTokens = countTokens(toolInputParts.join("\n"));
|
|
12891
|
+
const toolOutputTokens = countTokens(toolOutputParts.join("\n"));
|
|
12481
12892
|
if (firstAssistant) {
|
|
12482
12893
|
const firstInput = (firstAssistant.tokens?.input || 0) + (firstAssistant.tokens?.cache?.read || 0) + (firstAssistant.tokens?.cache?.write || 0);
|
|
12483
12894
|
breakdown.system = Math.max(0, firstInput - firstUserTokens);
|
|
@@ -12689,7 +13100,7 @@ function resolveDefaultOutputPath(sessionId, cwd2) {
|
|
|
12689
13100
|
const short = (sessionId || "session").slice(0, 8);
|
|
12690
13101
|
return join7(cwd2, ".opencode", `acp-export-${short}.md`);
|
|
12691
13102
|
}
|
|
12692
|
-
function
|
|
13103
|
+
function formatTokens3(n) {
|
|
12693
13104
|
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
12694
13105
|
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
12695
13106
|
}
|
|
@@ -12742,7 +13153,7 @@ function renderExportMarkdown(params) {
|
|
|
12742
13153
|
const span = tokenSpans(blocks);
|
|
12743
13154
|
if (blocks.length > 0) {
|
|
12744
13155
|
out.push(
|
|
12745
|
-
`- **Coverage**: ${
|
|
13156
|
+
`- **Coverage**: ${formatTokens3(span.effective)} original \u2192 ${formatTokens3(span.summary)} summary`
|
|
12746
13157
|
);
|
|
12747
13158
|
}
|
|
12748
13159
|
out.push("");
|
|
@@ -12773,7 +13184,7 @@ function renderExportMarkdown(params) {
|
|
|
12773
13184
|
out.push(`- **Tier**: T${tier} \xB7 **Mode**: ${mode}`);
|
|
12774
13185
|
out.push(`- **Messages**: ${msgCount} (effective)`);
|
|
12775
13186
|
out.push(
|
|
12776
|
-
`- **Tokens**: ${
|
|
13187
|
+
`- **Tokens**: ${formatTokens3(effectiveTokens(block))} \u2192 ${formatTokens3(block.summaryTokens || 0)} summary`
|
|
12777
13188
|
);
|
|
12778
13189
|
out.push(`- **Created**: ${created}`);
|
|
12779
13190
|
out.push(`- **Age**: survived ${age} transform${age === 1 ? "" : "s"}`);
|
|
@@ -13243,7 +13654,7 @@ function mergeMarkedBlocks(state, markedIds, maxMergedLength) {
|
|
|
13243
13654
|
const mergedRaw = bodies.join("\n---\n");
|
|
13244
13655
|
const mergedBody = truncateMergedSummary(mergedRaw, maxMergedLength);
|
|
13245
13656
|
const newSummary = wrapCompressedSummary(newBlockId, mergedBody);
|
|
13246
|
-
const newSummaryTokens =
|
|
13657
|
+
const newSummaryTokens = countTokens(newSummary);
|
|
13247
13658
|
const oldest = sourceBlocks[0];
|
|
13248
13659
|
const newest = sourceBlocks[sourceBlocks.length - 1];
|
|
13249
13660
|
const effectiveMessageIds = /* @__PURE__ */ new Set();
|
|
@@ -13419,7 +13830,8 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
|
|
|
13419
13830
|
const newPrompt = renderSystemPrompt(
|
|
13420
13831
|
runtimePrompts,
|
|
13421
13832
|
buildProtectedToolsExtension(config.compress.protectedTools),
|
|
13422
|
-
state.isSubAgent && config.allowSubAgents
|
|
13833
|
+
state.isSubAgent && config.allowSubAgents,
|
|
13834
|
+
(config.compress.images ?? DEFAULT_COMPRESS_IMAGES).nudge !== false ? IMAGE_GUARD_SYSTEM_EXTENSION : void 0
|
|
13423
13835
|
);
|
|
13424
13836
|
if (output.system.length > 0) {
|
|
13425
13837
|
output.system[output.system.length - 1] += "\n\n" + newPrompt;
|
|
@@ -13594,13 +14006,32 @@ ${text}`);
|
|
|
13594
14006
|
prePruneTokens,
|
|
13595
14007
|
candidateMessages
|
|
13596
14008
|
);
|
|
13597
|
-
|
|
13598
|
-
state,
|
|
14009
|
+
const emergencyTruncationEnabled = applyCompressOverrides(
|
|
13599
14010
|
config,
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
);
|
|
13603
|
-
|
|
14011
|
+
dropReasoningModel?.providerID ?? state.modelProviderID,
|
|
14012
|
+
dropReasoningModel?.modelID ?? state.modelID
|
|
14013
|
+
).compress.emergencyTruncation;
|
|
14014
|
+
if (emergencyTruncationEnabled === true) {
|
|
14015
|
+
applyStickyTruncations(state, output.messages, logger);
|
|
14016
|
+
const toolOutputsBefore = snapshotToolOutputs(output.messages);
|
|
14017
|
+
truncateLargeToolOutputs(
|
|
14018
|
+
state,
|
|
14019
|
+
config,
|
|
14020
|
+
logger,
|
|
14021
|
+
output.messages.filter((message) => !isSyntheticMessage(message))
|
|
14022
|
+
);
|
|
14023
|
+
enforceContextBudget(state, config, logger, output.messages);
|
|
14024
|
+
const frozenCount = recordStickyTruncations(
|
|
14025
|
+
state,
|
|
14026
|
+
output.messages,
|
|
14027
|
+
toolOutputsBefore,
|
|
14028
|
+
logger
|
|
14029
|
+
);
|
|
14030
|
+
if (frozenCount > 0) {
|
|
14031
|
+
saveSessionState(state, logger).catch(() => {
|
|
14032
|
+
});
|
|
14033
|
+
}
|
|
14034
|
+
}
|
|
13604
14035
|
injectMessageIds(state, config, output.messages, compressionPriorities);
|
|
13605
14036
|
hideFailedCompressCalls(output.messages);
|
|
13606
14037
|
stripStaleMetadata(output.messages);
|
|
@@ -14039,7 +14470,7 @@ var server = (async (ctx) => {
|
|
|
14039
14470
|
}
|
|
14040
14471
|
const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
|
|
14041
14472
|
logger.info("ACP plugin initialized", {
|
|
14042
|
-
version: true ? "1.18.
|
|
14473
|
+
version: true ? "1.18.5-pr.490.223" : "dev",
|
|
14043
14474
|
workspace: ctx.directory,
|
|
14044
14475
|
logLevel: logger.level,
|
|
14045
14476
|
debug: config.debug,
|