opencode-acp 1.18.5-pr.490.223 → 1.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -920,8 +920,6 @@ 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",
925
923
  "compress.completionReserveTokens",
926
924
  "compress.emergencyTruncation",
927
925
  "gc",
@@ -1319,7 +1317,7 @@ function validateConfigTypes(config) {
1319
1317
  if (typeof compress.images !== "object" || compress.images === null || Array.isArray(compress.images)) {
1320
1318
  errors.push({
1321
1319
  key: "compress.images",
1322
- expected: "object { enabled?: boolean; minBytes?: integer (>= 0); ttlDays?: number (> 0); largeBytes?: integer (>= 0); nudge?: boolean }",
1320
+ expected: "object { enabled?: boolean; minBytes?: integer (>= 0); ttlDays?: number (> 0) }",
1323
1321
  actual: JSON.stringify(compress.images)
1324
1322
  });
1325
1323
  } else {
@@ -1344,20 +1342,6 @@ function validateConfigTypes(config) {
1344
1342
  actual: JSON.stringify(compress.images.ttlDays)
1345
1343
  });
1346
1344
  }
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
- }
1361
1345
  }
1362
1346
  }
1363
1347
  if (compress.completionReserveTokens !== void 0 && typeof compress.completionReserveTokens !== "number") {
@@ -1758,9 +1742,7 @@ var DEFAULT_COMPRESS_REASONING = {
1758
1742
  var DEFAULT_COMPRESS_IMAGES = {
1759
1743
  enabled: true,
1760
1744
  minBytes: 16384,
1761
- ttlDays: 7,
1762
- largeBytes: 524288,
1763
- nudge: true
1745
+ ttlDays: 7
1764
1746
  };
1765
1747
  var DEFAULT_PROTECTED_TOOLS = [
1766
1748
  "task",
@@ -2043,9 +2025,7 @@ function mergeCompress(base, override) {
2043
2025
  images: {
2044
2026
  enabled: override.images?.enabled ?? base.images?.enabled ?? DEFAULT_COMPRESS_IMAGES.enabled,
2045
2027
  minBytes: override.images?.minBytes ?? base.images?.minBytes ?? DEFAULT_COMPRESS_IMAGES.minBytes,
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
2028
+ ttlDays: override.images?.ttlDays ?? base.images?.ttlDays ?? DEFAULT_COMPRESS_IMAGES.ttlDays
2049
2029
  },
2050
2030
  completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens,
2051
2031
  emergencyTruncation: override.emergencyTruncation ?? base.emergencyTruncation
@@ -2441,18 +2421,174 @@ function isProtectedUserMessage(_config, message) {
2441
2421
  return false;
2442
2422
  }
2443
2423
 
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";
2424
+ // lib/token-utils.ts
2425
+ var anthropicCountTokens = _anthropicTokenizer.countTokens ?? _anthropicTokenizer.default?.countTokens;
2426
+ function getCurrentTokenUsage(state, messages) {
2427
+ for (let i = messages.length - 1; i >= 0; i--) {
2428
+ const msg = messages[i];
2429
+ if (msg.info.role !== "assistant") {
2430
+ continue;
2431
+ }
2432
+ const assistantInfo = msg.info;
2433
+ const input = assistantInfo.tokens?.input || 0;
2434
+ const output = assistantInfo.tokens?.output || 0;
2435
+ const reasoning = assistantInfo.tokens?.reasoning || 0;
2436
+ const cacheRead = assistantInfo.tokens?.cache?.read || 0;
2437
+ const cacheWrite = assistantInfo.tokens?.cache?.write || 0;
2438
+ if (input <= 0 && output <= 0) {
2439
+ continue;
2440
+ }
2441
+ if (state.lastCompaction > 0 && (msg.info.time.created < state.lastCompaction || msg.info.summary === true && msg.info.time.created === state.lastCompaction)) {
2442
+ return 0;
2443
+ }
2444
+ return input + cacheRead + cacheWrite + output + reasoning;
2445
+ }
2446
+ let estimated = 0;
2447
+ for (const m of messages) {
2448
+ estimated += countAllMessageTokens(m);
2449
+ }
2450
+ return estimated;
2451
+ }
2452
+ function estimateSystemPromptTokens(messages) {
2453
+ let firstInput;
2454
+ for (const msg of messages) {
2455
+ if (msg.info.role !== "assistant") continue;
2456
+ const assistantInfo = msg.info;
2457
+ const t = assistantInfo.tokens;
2458
+ if (!t) continue;
2459
+ const input = (t.input || 0) + (t.cache?.read || 0) + (t.cache?.write || 0);
2460
+ if (input > 0) {
2461
+ firstInput = input;
2462
+ break;
2463
+ }
2464
+ }
2465
+ if (firstInput === void 0) return 0;
2466
+ let firstUserText = "";
2467
+ for (const msg of messages) {
2468
+ if (msg.info.role !== "user") continue;
2469
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
2470
+ for (const part of parts) {
2471
+ if (part.type === "text" && typeof part.text === "string") {
2472
+ firstUserText += part.text;
2473
+ }
2474
+ }
2475
+ if (firstUserText) break;
2476
+ }
2477
+ return Math.max(0, firstInput - countTokens2(firstUserText));
2478
+ }
2479
+ function getCurrentParams(state, messages, logger) {
2480
+ const userMsg = getLastUserMessage(messages);
2481
+ if (!userMsg) {
2482
+ logger.debug("No user message found when determining current params");
2483
+ return {
2484
+ providerId: void 0,
2485
+ modelId: void 0,
2486
+ agent: void 0,
2487
+ variant: void 0
2488
+ };
2489
+ }
2490
+ const userInfo = userMsg.info;
2491
+ const agent = userInfo.agent;
2492
+ const providerId = userInfo.model.providerID;
2493
+ const modelId = userInfo.model.modelID;
2494
+ const variant = userInfo.model.variant;
2495
+ return { providerId, modelId, agent, variant };
2496
+ }
2497
+ function countTokens2(text) {
2498
+ if (!text) return 0;
2499
+ try {
2500
+ return anthropicCountTokens(text);
2501
+ } catch {
2502
+ return Math.round(text.length / 4);
2503
+ }
2504
+ }
2505
+ function estimateTokensBatch(texts) {
2506
+ if (texts.length === 0) return 0;
2507
+ return countTokens2(texts.join(" "));
2508
+ }
2509
+ var COMPACTED_TOOL_OUTPUT_PLACEHOLDER = "[Old tool result content cleared]";
2510
+ function stringifyToolContent(value) {
2511
+ return typeof value === "string" ? value : JSON.stringify(value);
2512
+ }
2513
+ function extractCompletedToolOutput(part) {
2514
+ if (part?.type !== "tool" || part.state?.status !== "completed" || part.state?.output === void 0) {
2515
+ return void 0;
2516
+ }
2517
+ if (part.state?.time?.compacted) {
2518
+ return COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
2519
+ }
2520
+ return stringifyToolContent(part.state.output);
2521
+ }
2522
+ function extractToolContent(part) {
2523
+ const contents = [];
2524
+ if (part?.type !== "tool") {
2525
+ return contents;
2526
+ }
2527
+ if (part.state?.input !== void 0) {
2528
+ contents.push(stringifyToolContent(part.state.input));
2529
+ }
2530
+ const completedOutput = extractCompletedToolOutput(part);
2531
+ if (completedOutput !== void 0) {
2532
+ contents.push(completedOutput);
2533
+ } else if (part.state?.status === "error" && part.state?.error) {
2534
+ contents.push(stringifyToolContent(part.state.error));
2535
+ }
2536
+ return contents;
2537
+ }
2538
+ function countAllMessageTokens(msg) {
2539
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
2540
+ const texts = [];
2541
+ for (const part of parts) {
2542
+ if (part.type === "text") {
2543
+ texts.push(part.text);
2544
+ } else {
2545
+ texts.push(...extractToolContent(part));
2546
+ }
2547
+ }
2548
+ if (texts.length === 0) return 0;
2549
+ return estimateTokensBatch(texts);
2550
+ }
2551
+ function countMessageCharacters(msg) {
2552
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
2553
+ let total = 0;
2554
+ for (const part of parts) {
2555
+ if (part.type === "text" && typeof part.text === "string") {
2556
+ total += part.text.length;
2557
+ } else {
2558
+ for (const content of extractToolContent(part)) {
2559
+ total += content.length;
2560
+ }
2561
+ }
2562
+ }
2563
+ return total;
2564
+ }
2565
+ function estimateAllMessageTokensFast(msg) {
2566
+ return Math.round(countMessageCharacters(msg) / 4);
2567
+ }
2568
+
2569
+ // lib/prompts/extensions/tool.ts
2570
+ var RANGE_FORMAT_EXTENSION = `
2571
+
2572
+ THE FORMAT OF COMPRESS
2573
+
2574
+ \`\`\`
2575
+ {
2576
+ topic?: string, // OPTIONAL fallback topic for entries without their own.
2577
+ // Omit when every content entry specifies its own topic.
2578
+ content: [ // One or more ranges to compress
2579
+ {
2580
+ topic?: string, // OPTIONAL per-entry topic for this range.
2581
+ // Falls back to top-level topic.
2582
+ // Give each entry its own topic when compressing
2583
+ // unrelated ranges in one call.
2584
+ startId: string, // Boundary ID at range start: mNNNNN or bN
2585
+ endId: string, // Boundary ID at range end: mNNNNN or bN
2586
+ summary: string // Complete technical summary replacing all content in range
2587
+ }
2588
+ ]
2589
+ }
2590
+ \`\`\`
2591
+ Each entry needs a topic \u2014 either its own or the top-level fallback.`;
2456
2592
 
2457
2593
  // lib/state/persistence.ts
2458
2594
  import * as fs from "fs/promises";
@@ -2606,7 +2742,7 @@ function loadPruneMessagesState(persisted) {
2606
2742
  deactivatedByUser: block.deactivatedByUser === true,
2607
2743
  deactivatedByUserDeep: block.deactivatedByUserDeep === true ? true : void 0,
2608
2744
  compressedTokens: typeof block.compressedTokens === "number" && Number.isFinite(block.compressedTokens) ? Math.max(0, block.compressedTokens) : 0,
2609
- summaryTokens: typeof block.summaryTokens === "number" && Number.isFinite(block.summaryTokens) ? Math.max(0, block.summaryTokens) : typeof block.summary === "string" ? countTokens(block.summary) : 0,
2745
+ summaryTokens: typeof block.summaryTokens === "number" && Number.isFinite(block.summaryTokens) ? Math.max(0, block.summaryTokens) : typeof block.summary === "string" ? countTokens2(block.summary) : 0,
2610
2746
  durationMs: typeof block.durationMs === "number" && Number.isFinite(block.durationMs) ? Math.max(0, block.durationMs) : 0,
2611
2747
  mode: block.mode === "range" || block.mode === "message" ? block.mode : void 0,
2612
2748
  topic: typeof block.topic === "string" ? block.topic : "",
@@ -2831,7 +2967,6 @@ function saveSessionState(sessionState, logger, sessionName) {
2831
2967
  modelProviderID: sessionState.modelProviderID,
2832
2968
  modelID: sessionState.modelID,
2833
2969
  imageArchive: sessionState.imageArchive,
2834
- imageSurgeNudged: sessionState.imageSurgeNudged,
2835
2970
  stickyTruncations: sessionState.stickyTruncations
2836
2971
  };
2837
2972
  const key = saveQueueKey(sessionState.sessionId, sessionState.storageDir);
@@ -2929,527 +3064,40 @@ async function loadSessionState(sessionId, logger, storageDir) {
2929
3064
  (entry) => typeof entry === "string"
2930
3065
  );
2931
3066
  const dedupedIterationAnchors = [...new Set(validIterationAnchors)];
2932
- if (validIterationAnchors.length !== rawIterationNudgeAnchors.length) {
2933
- logger.warn("Filtered out malformed iterationNudgeAnchors entries", {
2934
- sessionId,
2935
- original: rawIterationNudgeAnchors.length,
2936
- valid: validIterationAnchors.length
2937
- });
2938
- }
2939
- state.nudges.iterationNudgeAnchors = dedupedIterationAnchors;
2940
- const persistedMessageIds = state.messageIds;
2941
- if (persistedMessageIds) {
2942
- ;
2943
- state._persistedMessageIds = persistedMessageIds;
2944
- }
2945
- const persistedLastCompaction = state.lastCompaction;
2946
- if (persistedLastCompaction !== void 0) {
2947
- ;
2948
- state._persistedLastCompaction = persistedLastCompaction;
2949
- }
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));
3067
+ if (validIterationAnchors.length !== rawIterationNudgeAnchors.length) {
3068
+ logger.warn("Filtered out malformed iterationNudgeAnchors entries", {
3069
+ sessionId,
3070
+ original: rawIterationNudgeAnchors.length,
3071
+ valid: validIterationAnchors.length
3072
+ });
3401
3073
  }
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
- }
3074
+ state.nudges.iterationNudgeAnchors = dedupedIterationAnchors;
3075
+ const persistedMessageIds = state.messageIds;
3076
+ if (persistedMessageIds) {
3077
+ ;
3078
+ state._persistedMessageIds = persistedMessageIds;
3418
3079
  }
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
3080
+ const persistedLastCompaction = state.lastCompaction;
3081
+ if (persistedLastCompaction !== void 0) {
3082
+ ;
3083
+ state._persistedLastCompaction = persistedLastCompaction;
3444
3084
  }
3445
- ]
3085
+ logger.info("Loaded session state from disk", {
3086
+ sessionId
3087
+ });
3088
+ return state;
3089
+ } catch (error) {
3090
+ logger.warn("Failed to load session state", {
3091
+ sessionId,
3092
+ error: error?.message
3093
+ });
3094
+ return null;
3095
+ }
3446
3096
  }
3447
- \`\`\`
3448
- Each entry needs a topic \u2014 either its own or the top-level fallback.`;
3449
3097
 
3450
3098
  // lib/state/state.ts
3451
- import { existsSync as existsSync4 } from "fs";
3452
- import { join as join4 } from "path";
3099
+ import { existsSync as existsSync3 } from "fs";
3100
+ import { join as join3 } from "path";
3453
3101
  import { cwd } from "process";
3454
3102
 
3455
3103
  // lib/message-ids.ts
@@ -4994,7 +4642,7 @@ function appendMissingBlockSummaries(summary, _missingBlockIds, _summaryByBlockI
4994
4642
  }
4995
4643
 
4996
4644
  // lib/state/rebuild.ts
4997
- import { createHash as createHash2 } from "crypto";
4645
+ import { createHash } from "crypto";
4998
4646
  function collectCompressInvocations(messages) {
4999
4647
  const invocations = [];
5000
4648
  for (const message of messages) {
@@ -5065,7 +4713,7 @@ function fingerprintMessage(message) {
5065
4713
  }
5066
4714
  return { type: part.type };
5067
4715
  });
5068
- return createHash2("sha256").update(JSON.stringify({ role: message.info.role, parts })).digest("hex");
4716
+ return createHash("sha256").update(JSON.stringify({ role: message.info.role, parts })).digest("hex");
5069
4717
  }
5070
4718
  function mapForkIds(state, parent, parentMessages, forkMessages) {
5071
4719
  const parentRefs = parent.messageIds?.byRef;
@@ -5192,7 +4840,7 @@ function rebuildRangeInvocation(state, input, searchContext, invocation, protect
5192
4840
  ]);
5193
4841
  const blockId = allocateBlockId(state);
5194
4842
  const storedSummary = wrapCompressedSummary(blockId, plan.entry.summary);
5195
- const summaryTokens = countTokens(storedSummary);
4843
+ const summaryTokens = countTokens2(storedSummary);
5196
4844
  applyCompressionState(
5197
4845
  state,
5198
4846
  {
@@ -5254,7 +4902,7 @@ function rebuildMessageInvocation(state, input, searchContext, invocation, gcCon
5254
4902
  }
5255
4903
  const blockId = allocateBlockId(state);
5256
4904
  const storedSummary = wrapCompressedSummary(blockId, entry.summary);
5257
- const summaryTokens = countTokens(storedSummary);
4905
+ const summaryTokens = countTokens2(storedSummary);
5258
4906
  applyCompressionState(
5259
4907
  state,
5260
4908
  {
@@ -5489,7 +5137,6 @@ function createSessionState() {
5489
5137
  noContextLimitWarned: false,
5490
5138
  imageArchive: {},
5491
5139
  imagePruneDone: false,
5492
- imageSurgeNudged: {},
5493
5140
  stickyTruncations: {}
5494
5141
  };
5495
5142
  }
@@ -5536,7 +5183,6 @@ function resetSessionState(state) {
5536
5183
  state.noContextLimitWarned = false;
5537
5184
  state.imageArchive = {};
5538
5185
  state.imagePruneDone = false;
5539
- state.imageSurgeNudged = {};
5540
5186
  state.stickyTruncations = {};
5541
5187
  }
5542
5188
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
@@ -5554,8 +5200,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
5554
5200
  state.nudges.turnNudgeAnchors = collectTurnNudgeAnchors(messages);
5555
5201
  const persisted = await loadSessionState(sessionId, logger, state.storageDir);
5556
5202
  if (persisted === null) {
5557
- const defaultPath = join4(getDefaultStorageDir(), `${sessionId}.json`);
5558
- if (state.storageDir && existsSync4(defaultPath)) {
5203
+ const defaultPath = join3(getDefaultStorageDir(), `${sessionId}.json`);
5204
+ if (state.storageDir && existsSync3(defaultPath)) {
5559
5205
  logger.warn(
5560
5206
  "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",
5561
5207
  { sessionId, storageDir: state.storageDir, defaultPath }
@@ -5638,13 +5284,6 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
5638
5284
  }
5639
5285
  }
5640
5286
  }
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
5287
  const persistedSticky = persisted.stickyTruncations;
5649
5288
  if (persistedSticky && typeof persistedSticky === "object") {
5650
5289
  for (const [callID, output] of Object.entries(persistedSticky)) {
@@ -5828,7 +5467,7 @@ function cacheSystemPromptTokens(state, messages) {
5828
5467
  }
5829
5468
  break;
5830
5469
  }
5831
- const estimatedSystemTokens = Math.max(0, firstInputTokens - countTokens(firstUserText));
5470
+ const estimatedSystemTokens = Math.max(0, firstInputTokens - countTokens2(firstUserText));
5832
5471
  state.systemPromptTokens = estimatedSystemTokens > 0 ? estimatedSystemTokens : void 0;
5833
5472
  }
5834
5473
 
@@ -7065,6 +6704,156 @@ function buildPhantomSkipNotice(identification) {
7065
6704
  return `Skipped ${identification.phantomIndices.length} already-compressed ${noun} (${skippedNums}) \u2014 they are no-ops; the remaining entries were compressed.`;
7066
6705
  }
7067
6706
 
6707
+ // lib/compress/image-archive.ts
6708
+ import { createHash as createHash2 } from "crypto";
6709
+ import {
6710
+ existsSync as existsSync4,
6711
+ mkdirSync as mkdirSync2,
6712
+ readdirSync,
6713
+ rmdirSync,
6714
+ statSync as statSync2,
6715
+ unlinkSync,
6716
+ writeFileSync as writeFileSync2
6717
+ } from "fs";
6718
+ import { join as join4 } from "path";
6719
+ var DATA_URL_REGEX = /^data:(image\/[a-z0-9.+-]+);base64,(.+)$/is;
6720
+ var IMAGE_EXTENSIONS = {
6721
+ "image/png": "png",
6722
+ "image/jpeg": "jpg",
6723
+ "image/jpg": "jpg",
6724
+ "image/gif": "gif",
6725
+ "image/webp": "webp",
6726
+ "image/avif": "avif",
6727
+ "image/bmp": "bmp"
6728
+ };
6729
+ function sha8(input) {
6730
+ return createHash2("sha256").update(input).digest("hex").slice(0, 8);
6731
+ }
6732
+ function parseImageDataUrl(url) {
6733
+ if (typeof url !== "string") return void 0;
6734
+ const match = DATA_URL_REGEX.exec(url);
6735
+ if (!match) return void 0;
6736
+ return { mediaType: match[1].toLowerCase(), base64: match[2] };
6737
+ }
6738
+ function extForMediaType(mediaType) {
6739
+ return IMAGE_EXTENSIONS[mediaType.toLowerCase()];
6740
+ }
6741
+ function archiveDirName(sessionId) {
6742
+ let name = sessionId.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 100);
6743
+ if (name !== sessionId) {
6744
+ name = `${name}-${sha8(sessionId)}`;
6745
+ }
6746
+ return name || "-";
6747
+ }
6748
+ function archiveRoot(storageDir, sessionId) {
6749
+ return join4(storageDir ?? getDefaultStorageDir(), "images", archiveDirName(sessionId));
6750
+ }
6751
+ function archiveSessionImages(state, messages, images, logger) {
6752
+ if (!images.enabled || !state.sessionId) return 0;
6753
+ const root = archiveRoot(state.storageDir, state.sessionId);
6754
+ let archived = 0;
6755
+ for (const message of messages) {
6756
+ const rawId = message.info.id;
6757
+ if (!rawId) continue;
6758
+ const parts = Array.isArray(message.parts) ? message.parts : [];
6759
+ for (let partIndex = 0; partIndex < parts.length; partIndex++) {
6760
+ const part = parts[partIndex];
6761
+ if (part.type !== "file") continue;
6762
+ const parsed = parseImageDataUrl(part.url);
6763
+ if (!parsed) continue;
6764
+ const ext = extForMediaType(parsed.mediaType);
6765
+ if (!ext) continue;
6766
+ const fileName = `${sha8(rawId)}-p${partIndex}-${sha8(parsed.base64)}.${ext}`;
6767
+ const filePath = join4(root, fileName);
6768
+ const existing = state.imageArchive[rawId]?.find((e) => e.path === filePath);
6769
+ if (existing) continue;
6770
+ let bytes;
6771
+ try {
6772
+ if (existsSync4(filePath)) {
6773
+ bytes = statSync2(filePath).size;
6774
+ } else {
6775
+ const buffer = Buffer.from(parsed.base64, "base64");
6776
+ bytes = buffer.byteLength;
6777
+ if (bytes < images.minBytes) continue;
6778
+ mkdirSync2(root, { recursive: true });
6779
+ writeFileSync2(filePath, buffer, { mode: 384 });
6780
+ }
6781
+ } catch (error) {
6782
+ logger.warn(
6783
+ `[#478] failed to archive image ${filePath}: ${error instanceof Error ? error.message : String(error)}`
6784
+ );
6785
+ continue;
6786
+ }
6787
+ const entry = {
6788
+ path: filePath,
6789
+ mediaType: parsed.mediaType,
6790
+ bytes
6791
+ };
6792
+ if (!state.imageArchive[rawId]) {
6793
+ state.imageArchive[rawId] = [];
6794
+ }
6795
+ state.imageArchive[rawId].push(entry);
6796
+ state.stats.imagesArchived = (state.stats.imagesArchived ?? 0) + 1;
6797
+ state.stats.imageBytesArchived = (state.stats.imageBytesArchived ?? 0) + bytes;
6798
+ archived++;
6799
+ }
6800
+ }
6801
+ return archived;
6802
+ }
6803
+ function pruneImageArchive(state, images, logger) {
6804
+ if (state.imagePruneDone || !state.sessionId) return;
6805
+ state.imagePruneDone = true;
6806
+ try {
6807
+ const root = archiveRoot(state.storageDir, state.sessionId);
6808
+ if (!existsSync4(root)) return;
6809
+ const cutoff = Date.now() - images.ttlDays * 864e5;
6810
+ const prunedPaths = /* @__PURE__ */ new Set();
6811
+ for (const name of readdirSync(root)) {
6812
+ const filePath = join4(root, name);
6813
+ try {
6814
+ if (statSync2(filePath).mtimeMs < cutoff) {
6815
+ unlinkSync(filePath);
6816
+ prunedPaths.add(filePath);
6817
+ }
6818
+ } catch {
6819
+ }
6820
+ }
6821
+ if (prunedPaths.size > 0) {
6822
+ for (const [rawId, entries] of Object.entries(state.imageArchive)) {
6823
+ const kept = entries.filter((e) => !prunedPaths.has(e.path));
6824
+ if (kept.length === 0) {
6825
+ delete state.imageArchive[rawId];
6826
+ } else if (kept.length !== entries.length) {
6827
+ state.imageArchive[rawId] = kept;
6828
+ }
6829
+ }
6830
+ logger.info(`[#478] TTL sweep removed ${prunedPaths.size} expired archived image(s)`);
6831
+ }
6832
+ if (readdirSync(root).length === 0) {
6833
+ rmdirSync(root);
6834
+ }
6835
+ } catch (error) {
6836
+ logger.warn(
6837
+ `[#478] image TTL sweep failed: ${error instanceof Error ? error.message : String(error)}`
6838
+ );
6839
+ }
6840
+ }
6841
+ function appendArchivedImagePointers(summary, selection, state, enabled) {
6842
+ if (!enabled) return summary;
6843
+ const lines = [];
6844
+ for (const messageId of selection.messageIds) {
6845
+ const entries = state.imageArchive[messageId];
6846
+ if (!entries || entries.length === 0) continue;
6847
+ const ref = state.messageIds.byRawId.get(messageId) ?? messageId;
6848
+ for (const entry of entries) {
6849
+ lines.push(`- ${ref}: ${entry.path} (${entry.mediaType}, ${entry.bytes} bytes)`);
6850
+ }
6851
+ }
6852
+ if (lines.length === 0) return summary;
6853
+ 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:";
6854
+ return summary + heading + "\n" + lines.join("\n");
6855
+ }
6856
+
7068
6857
  // lib/compress/keep-markers.ts
7069
6858
  var KEEP_REGEX = /\[\[KEEP:(m\d+)\]\]/g;
7070
6859
  var REF_REGEX = /\[\[REF:(m\d+)\|([^\]]+)\]\]/g;
@@ -7816,7 +7605,6 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
7816
7605
  let messageTokens = 0;
7817
7606
  let protectedTokens = 0;
7818
7607
  let reasoningTokens = 0;
7819
- let imageTokens = 0;
7820
7608
  const perMessage = [];
7821
7609
  const perTool = [];
7822
7610
  const perCode = [];
@@ -7874,12 +7662,6 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
7874
7662
  const tokens = Math.round(part.text.length / 4);
7875
7663
  msgTotal += tokens;
7876
7664
  reasoningTokens += tokens;
7877
- } else if (part.type === "file") {
7878
- const imgTokens = estimateImagePartTokens(part);
7879
- if (imgTokens > 0) {
7880
- msgTotal += imgTokens;
7881
- imageTokens += imgTokens;
7882
- }
7883
7665
  }
7884
7666
  }
7885
7667
  if (isProtected && !isSummary) {
@@ -7908,8 +7690,7 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
7908
7690
  systemTokens,
7909
7691
  protectedTokens,
7910
7692
  reasoningTokens,
7911
- imageTokens,
7912
- total: systemTokens + toolTokens + summaryTokens + messageTokens + reasoningTokens + imageTokens,
7693
+ total: systemTokens + toolTokens + summaryTokens + messageTokens + reasoningTokens,
7913
7694
  largestRanges: perMessage.slice(0, 15),
7914
7695
  largestToolRanges: perTool.slice(0, 15),
7915
7696
  largestCodeRanges: perCode.slice(0, 5),
@@ -8190,8 +7971,6 @@ function computeProtectedRefs(messages, state, compress) {
8190
7971
  for (const part of msg.parts || []) {
8191
7972
  if (part.type === "text" && typeof part.text === "string") {
8192
7973
  tokens += Math.round(part.text.length / 4);
8193
- } else if (part.type === "file") {
8194
- tokens += estimateImagePartTokens(part);
8195
7974
  } else if (part.type !== "text" && part.type !== "reasoning") {
8196
7975
  tokens += Math.round(JSON.stringify(part).length / 4);
8197
7976
  }
@@ -8609,7 +8388,7 @@ function createCompressRangeTool(factoryCtx) {
8609
8388
  );
8610
8389
  preparedPlan.finalSummary = keepResult.summary;
8611
8390
  const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
8612
- const summaryTokens = countTokens(storedSummary);
8391
+ const summaryTokens = countTokens2(storedSummary);
8613
8392
  const applied = applyCompressionState(
8614
8393
  ctx.state,
8615
8394
  {
@@ -8946,73 +8725,6 @@ var syncCompressPermissionState = (state, config, hostPermissions, messages) =>
8946
8725
  );
8947
8726
  };
8948
8727
 
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
-
9016
8728
  // lib/prompts/context-limit-nudge.ts
9017
8729
  var CONTEXT_LIMIT_NUDGE = `
9018
8730
  <system-reminder>
@@ -9830,21 +9542,6 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
9830
9542
  let shouldInjectNotice = emergencyNoTargets && noticeCadenceMet;
9831
9543
  let shouldInject = shouldInjectNudge || shouldInjectNotice;
9832
9544
  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
- }
9848
9545
  if (shouldInjectNudge && config.compress.smartPlanRequired === true && state.sessionId) {
9849
9546
  const planningRanges = candidatesEnabled ? candidatePlan.candidates.map((candidate) => ({
9850
9547
  startRef: candidate.startRef,
@@ -9907,7 +9604,7 @@ ${ITERATION_CANDIDATE_GUIDANCE}` : ITERATION_CANDIDATE_GUIDANCE
9907
9604
  state.nudges.lastPerMessageNudgeTokens = currentTokens;
9908
9605
  baselineReEstablished = true;
9909
9606
  }
9910
- if (suffixMessage && !shouldInject && !imageNoticeEmitted) {
9607
+ if (suffixMessage && !shouldInject) {
9911
9608
  const tierUsage = tierUsageEarly;
9912
9609
  const tierChecks = [
9913
9610
  {
@@ -10010,7 +9707,7 @@ ${rules}`;
10010
9707
  break;
10011
9708
  }
10012
9709
  }
10013
- state.nudges.shouldInjectThisTurn = shouldInject || imageNoticeEmitted;
9710
+ state.nudges.shouldInjectThisTurn = shouldInject;
10014
9711
  const usagePct = currentTokens !== void 0 && modelContextLimit ? `${(currentTokens / modelContextLimit * 100).toFixed(1)}%` : void 0;
10015
9712
  if (shouldInjectNudge) {
10016
9713
  logger.info("Compression nudge injected", {
@@ -10070,7 +9767,7 @@ ${rules}`;
10070
9767
  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 : "";
10071
9768
  const sysPart = composition.systemTokens > 0 ? `${fmt(composition.systemTokens)} system (${pct2(composition.systemTokens)}%) | ` : "";
10072
9769
  let breakdown = `${efficiencyNote}
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}`;
9770
+ 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}`;
10074
9771
  const compressibleTokens = composition.total - composition.systemTokens - composition.protectedTokens - composition.summaryTokens;
10075
9772
  if (composition.protectedTokens > 0) {
10076
9773
  breakdown += `
@@ -11030,7 +10727,7 @@ Use this tool to:
11030
10727
  - Focus on ranges only (scope:"uncompressed")
11031
10728
  - Find all messages of a specific tool type (scope:"uncompressed", view:"messages", tool:"bash")
11032
10729
  - Check block details before decompressing (scope:"compressed")`;
11033
- function formatTokens2(n) {
10730
+ function formatTokens(n) {
11034
10731
  if (!Number.isFinite(n) || n <= 0) return "0";
11035
10732
  return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
11036
10733
  }
@@ -11074,7 +10771,7 @@ function tierBreakdown(blocks) {
11074
10771
  const parts = [];
11075
10772
  for (const t of [1, 2, 3]) {
11076
10773
  if (tierTokens[t]) {
11077
- parts.push(`T${t}: ${formatTokens2(tierTokens[t])}`);
10774
+ parts.push(`T${t}: ${formatTokens(tierTokens[t])}`);
11078
10775
  }
11079
10776
  }
11080
10777
  return parts.join(" | ");
@@ -11100,13 +10797,10 @@ function collectVisibleMessages(rawMessages, ctx) {
11100
10797
  if (!ref) return;
11101
10798
  let tokens = 0;
11102
10799
  let reasoning = 0;
11103
- let image = 0;
11104
10800
  let toolName = "";
11105
10801
  for (const part of msg.parts || []) {
11106
10802
  if (part.type === "text" && typeof part.text === "string") {
11107
10803
  tokens += Math.round(part.text.length / 4);
11108
- } else if (part.type === "file") {
11109
- image += estimateImagePartTokens(part);
11110
10804
  } else if (part.type === "tool") {
11111
10805
  const raw = JSON.stringify(part);
11112
10806
  tokens += Math.round(raw.length / 4);
@@ -11117,8 +10811,8 @@ function collectVisibleMessages(rawMessages, ctx) {
11117
10811
  reasoning += Math.round(part.text.length / 4);
11118
10812
  }
11119
10813
  }
11120
- if (tokens > 0 || reasoning > 0 || image > 0) {
11121
- result.push({ ref, tokens, tool: toolName || "text", index: idx, reasoning, image });
10814
+ if (tokens > 0 || reasoning > 0) {
10815
+ result.push({ ref, tokens, tool: toolName || "text", index: idx, reasoning });
11122
10816
  }
11123
10817
  });
11124
10818
  return {
@@ -11141,17 +10835,15 @@ function renderOverview(visibleMessages, summaryTokens, systemTokens, blocks, fe
11141
10835
  const totalTool = visibleMessages.filter((m) => m.tool !== "text" && m.tool !== "step-finish").reduce((s, m) => s + m.tokens, 0);
11142
10836
  const totalText = visibleMessages.filter((m) => m.tool === "text").reduce((s, m) => s + m.tokens, 0);
11143
10837
  const totalReasoning = visibleMessages.reduce((s, m) => s + m.reasoning, 0);
11144
- const totalImage = visibleMessages.reduce((s, m) => s + m.image, 0);
11145
- const total = systemTokens + totalTool + totalText + summaryTokens + totalReasoning + totalImage;
10838
+ const total = systemTokens + totalTool + totalText + summaryTokens + totalReasoning;
11146
10839
  const sysPct = pct(systemTokens, total);
11147
10840
  const toolPct = pct(totalTool, total);
11148
10841
  const textPct = pct(totalText, total);
11149
10842
  const summaryPct = pct(summaryTokens, total);
11150
10843
  const reasoningPct = pct(totalReasoning, total);
11151
- const imagePct = pct(totalImage, total);
11152
10844
  lines.push("CONTEXT BREAKDOWN");
11153
10845
  lines.push(
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}%)`
10846
+ ` ${formatTokens(systemTokens)} system (${sysPct}%) | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%) | ${formatTokens(totalReasoning)} reasoning (${reasoningPct}%)`
11155
10847
  );
11156
10848
  const topTypes = Array.from(toolTypeMap.entries()).map(([tool6, tokens]) => ({ tool: tool6, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
11157
10849
  if (topTypes.length > 0) {
@@ -11171,7 +10863,7 @@ function renderOverview(visibleMessages, summaryTokens, systemTokens, blocks, fe
11171
10863
  (s, b) => s + getEffectiveCompressedTokens(b, blocksById),
11172
10864
  0
11173
10865
  );
11174
- const header = `COMPRESSED BLOCKS \u2014 ${blocks.length} active (${formatTokens2(totalSummary)} summary, ${formatTokens2(totalEffective)} original)`;
10866
+ const header = `COMPRESSED BLOCKS \u2014 ${blocks.length} active (${formatTokens(totalSummary)} summary, ${formatTokens(totalEffective)} original)`;
11175
10867
  lines.push(header);
11176
10868
  const breakdown = tierBreakdown(blocks);
11177
10869
  if (breakdown) {
@@ -11190,7 +10882,7 @@ function renderOverview(visibleMessages, summaryTokens, systemTokens, blocks, fe
11190
10882
  const tier = tierLabel(b);
11191
10883
  const effTokens = getEffectiveCompressedTokens(b, blocksById);
11192
10884
  lines.push(
11193
- ` b${b.blockId} (${tier}) ${formatTokens2(effTokens)}\u2192${formatTokens2(b.summaryTokens)} ${ageStr} ${range} "${topic}"`
10885
+ ` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${ageStr} ${range} "${topic}"`
11194
10886
  );
11195
10887
  }
11196
10888
  }
@@ -11251,7 +10943,7 @@ function renderUncompressedRanges(rawMessages, ctx) {
11251
10943
  const totalMsgs = compressible.reduce((s, r) => s + r.count, 0);
11252
10944
  const lines = [];
11253
10945
  lines.push(
11254
- `UNCOMPRESSED \u2014 ${formatTokens2(totalTokens)} | ${totalMsgs} msgs in ${compressible.length} ranges`
10946
+ `UNCOMPRESSED \u2014 ${formatTokens(totalTokens)} | ${totalMsgs} msgs in ${compressible.length} ranges`
11255
10947
  );
11256
10948
  lines.push("");
11257
10949
  if (compressible.length === 0 && contextRanges.protected.length === 0) {
@@ -11308,7 +11000,7 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
11308
11000
  if (toolFilter) {
11309
11001
  filtered = filtered.filter((m) => m.tool === toolFilter);
11310
11002
  }
11311
- const sizeOf = (m) => m.tokens + m.reasoning + m.image;
11003
+ const sizeOf = (m) => m.tokens + m.reasoning;
11312
11004
  if (sort === "time") {
11313
11005
  filtered.sort((a, b) => a.index - b.index);
11314
11006
  } else if (sort === "tool") {
@@ -11318,13 +11010,13 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
11318
11010
  }
11319
11011
  const totalTokens = filtered.reduce((s, m) => s + sizeOf(m), 0);
11320
11012
  const allTokens = visibleMessages.reduce((s, m) => s + sizeOf(m), 0);
11321
- const header = toolFilter ? `UNCOMPRESSED \u2014 ${toolFilter}: ${formatTokens2(totalTokens)} | ${filtered.length} msgs | ${pct(totalTokens, allTokens)}% of visible` : `UNCOMPRESSED \u2014 ${formatTokens2(totalTokens)} | ${filtered.length} msgs`;
11013
+ const header = toolFilter ? `UNCOMPRESSED \u2014 ${toolFilter}: ${formatTokens(totalTokens)} | ${filtered.length} msgs | ${pct(totalTokens, allTokens)}% of visible` : `UNCOMPRESSED \u2014 ${formatTokens(totalTokens)} | ${filtered.length} msgs`;
11322
11014
  lines.push(header);
11323
11015
  lines.push(`Sorted by ${sort}`);
11324
11016
  lines.push("");
11325
11017
  const shown = filtered.slice(0, limit);
11326
11018
  for (const m of shown) {
11327
- lines.push(` ${m.ref} (${formatTokens2(sizeOf(m))}) ${m.tool}`);
11019
+ lines.push(` ${m.ref} (${formatTokens(sizeOf(m))}) ${m.tool}`);
11328
11020
  }
11329
11021
  if (filtered.length > shown.length) {
11330
11022
  lines.push("");
@@ -11363,7 +11055,7 @@ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
11363
11055
  0
11364
11056
  );
11365
11057
  lines.push(
11366
- `COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens2(totalEffective)} original \u2192 ${formatTokens2(totalSummary)} summary`
11058
+ `COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens(totalEffective)} original \u2192 ${formatTokens(totalSummary)} summary`
11367
11059
  );
11368
11060
  const breakdown = tierBreakdown(sorted);
11369
11061
  if (breakdown) {
@@ -11388,7 +11080,7 @@ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
11388
11080
  const tier = tierLabel(b);
11389
11081
  const effTokens = getEffectiveCompressedTokens(b, blocksById);
11390
11082
  lines.push(
11391
- ` b${b.blockId} (${tier}) ${formatTokens2(effTokens)}\u2192${formatTokens2(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}${status}`
11083
+ ` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}${status}`
11392
11084
  );
11393
11085
  lines.push(` "${topic}"`);
11394
11086
  }
@@ -11666,7 +11358,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
11666
11358
  import { join as join5 } from "path";
11667
11359
  import { existsSync as existsSync5 } from "fs";
11668
11360
  import { homedir as homedir3 } from "os";
11669
- var LOG_VERSION = true ? "1.18.5-pr.490.223" : "dev";
11361
+ var LOG_VERSION = true ? "1.18.5" : "dev";
11670
11362
  var LEVEL_RANK = {
11671
11363
  debug: 10,
11672
11364
  info: 20,
@@ -12117,8 +11809,6 @@ NOTE: Message-mode blocks created in the same batch (same runId) are restored to
12117
11809
  Decompressing one block from a batch restores all blocks in that batch.
12118
11810
  </dcp-system-reminder>
12119
11811
  `;
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.`;
12122
11812
 
12123
11813
  // lib/prompts/store.ts
12124
11814
  var PROMPT_DEFINITIONS = [
@@ -12455,7 +12145,7 @@ var PromptStore = class {
12455
12145
  };
12456
12146
 
12457
12147
  // lib/prompts/index.ts
12458
- function renderSystemPrompt(prompts, protectedToolsExtension, subagent, imageGuardExtension) {
12148
+ function renderSystemPrompt(prompts, protectedToolsExtension, subagent) {
12459
12149
  const extensions = [];
12460
12150
  if (protectedToolsExtension) {
12461
12151
  extensions.push(protectedToolsExtension.trim());
@@ -12463,9 +12153,6 @@ function renderSystemPrompt(prompts, protectedToolsExtension, subagent, imageGua
12463
12153
  if (subagent) {
12464
12154
  extensions.push(prompts.subagentExtension.trim());
12465
12155
  }
12466
- if (imageGuardExtension) {
12467
- extensions.push(imageGuardExtension.trim());
12468
- }
12469
12156
  extensions.push(prompts.decompressExtension.trim());
12470
12157
  return [prompts.system.trim(), ...extensions].filter(Boolean).join("\n\n").replace(/\n([ \t]*\n)+/g, "\n\n").trim();
12471
12158
  }
@@ -12531,7 +12218,7 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
12531
12218
  if (content === void 0) continue;
12532
12219
  if (content === "[Old tool result content cleared]") continue;
12533
12220
  if (content.includes(TRUNCATION_MARKER)) continue;
12534
- const tokens = countTokens(content);
12221
+ const tokens = countTokens2(content);
12535
12222
  if (tokens < MIN_OUTPUT_TOKENS) continue;
12536
12223
  candidates.push({ part, content, tokens });
12537
12224
  }
@@ -12552,7 +12239,7 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
12552
12239
 
12553
12240
  ` + suffix;
12554
12241
  part.state.output = truncated;
12555
- savedTokens += tokens - countTokens(truncated);
12242
+ savedTokens += tokens - countTokens2(truncated);
12556
12243
  truncatedCount++;
12557
12244
  }
12558
12245
  if (truncatedCount > 0) {
@@ -12706,7 +12393,7 @@ function enforceContextBudget(state, config, logger, messages) {
12706
12393
  const content = extractCompletedToolOutput(part);
12707
12394
  if (content === void 0) continue;
12708
12395
  if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
12709
- const tokens = countTokens(content);
12396
+ const tokens = countTokens2(content);
12710
12397
  if (tokens <= 0) continue;
12711
12398
  candidates.push({ part, content, tokens, index: mi });
12712
12399
  }
@@ -12728,20 +12415,20 @@ function enforceContextBudget(state, config, logger, messages) {
12728
12415
  ` + suffix;
12729
12416
  if (truncated.length >= c.content.length) continue;
12730
12417
  c.part.state.output = truncated;
12731
- saved += c.tokens - countTokens(truncated);
12418
+ saved += c.tokens - countTokens2(truncated);
12732
12419
  truncatedCount++;
12733
12420
  }
12734
12421
  if (estimatedTokens - saved > budget) {
12735
12422
  const clearable = candidates.filter((c) => {
12736
12423
  const out = extractCompletedToolOutput(c.part);
12737
- return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens(out) > MIN_CLEAR_TOKENS;
12424
+ return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens2(out) > MIN_CLEAR_TOKENS;
12738
12425
  }).sort((a, b) => a.index - b.index);
12739
12426
  for (const c of clearable) {
12740
12427
  if (estimatedTokens - saved <= budget) break;
12741
12428
  const current = extractCompletedToolOutput(c.part);
12742
12429
  if (current === void 0 || current === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
12743
12430
  c.part.state.output = COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
12744
- saved += countTokens(current) - countTokens(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
12431
+ saved += countTokens2(current) - countTokens2(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
12745
12432
  clearedCount++;
12746
12433
  }
12747
12434
  }
@@ -12885,10 +12572,10 @@ function analyzeTokens(state, messages) {
12885
12572
  breakdown.toolsInContextCount = toolsInContextCount;
12886
12573
  breakdown.prunedToolCount = prunedToolIds.size;
12887
12574
  breakdown.prunedMessageCount = prunedMessageCount;
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"));
12575
+ const firstUserTokens = countTokens2(firstUserText);
12576
+ breakdown.user = countTokens2(userTextParts.join("\n"));
12577
+ const toolInputTokens = countTokens2(toolInputParts.join("\n"));
12578
+ const toolOutputTokens = countTokens2(toolOutputParts.join("\n"));
12892
12579
  if (firstAssistant) {
12893
12580
  const firstInput = (firstAssistant.tokens?.input || 0) + (firstAssistant.tokens?.cache?.read || 0) + (firstAssistant.tokens?.cache?.write || 0);
12894
12581
  breakdown.system = Math.max(0, firstInput - firstUserTokens);
@@ -13100,7 +12787,7 @@ function resolveDefaultOutputPath(sessionId, cwd2) {
13100
12787
  const short = (sessionId || "session").slice(0, 8);
13101
12788
  return join7(cwd2, ".opencode", `acp-export-${short}.md`);
13102
12789
  }
13103
- function formatTokens3(n) {
12790
+ function formatTokens2(n) {
13104
12791
  if (!Number.isFinite(n) || n <= 0) return "0";
13105
12792
  return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
13106
12793
  }
@@ -13153,7 +12840,7 @@ function renderExportMarkdown(params) {
13153
12840
  const span = tokenSpans(blocks);
13154
12841
  if (blocks.length > 0) {
13155
12842
  out.push(
13156
- `- **Coverage**: ${formatTokens3(span.effective)} original \u2192 ${formatTokens3(span.summary)} summary`
12843
+ `- **Coverage**: ${formatTokens2(span.effective)} original \u2192 ${formatTokens2(span.summary)} summary`
13157
12844
  );
13158
12845
  }
13159
12846
  out.push("");
@@ -13184,7 +12871,7 @@ function renderExportMarkdown(params) {
13184
12871
  out.push(`- **Tier**: T${tier} \xB7 **Mode**: ${mode}`);
13185
12872
  out.push(`- **Messages**: ${msgCount} (effective)`);
13186
12873
  out.push(
13187
- `- **Tokens**: ${formatTokens3(effectiveTokens(block))} \u2192 ${formatTokens3(block.summaryTokens || 0)} summary`
12874
+ `- **Tokens**: ${formatTokens2(effectiveTokens(block))} \u2192 ${formatTokens2(block.summaryTokens || 0)} summary`
13188
12875
  );
13189
12876
  out.push(`- **Created**: ${created}`);
13190
12877
  out.push(`- **Age**: survived ${age} transform${age === 1 ? "" : "s"}`);
@@ -13654,7 +13341,7 @@ function mergeMarkedBlocks(state, markedIds, maxMergedLength) {
13654
13341
  const mergedRaw = bodies.join("\n---\n");
13655
13342
  const mergedBody = truncateMergedSummary(mergedRaw, maxMergedLength);
13656
13343
  const newSummary = wrapCompressedSummary(newBlockId, mergedBody);
13657
- const newSummaryTokens = countTokens(newSummary);
13344
+ const newSummaryTokens = countTokens2(newSummary);
13658
13345
  const oldest = sourceBlocks[0];
13659
13346
  const newest = sourceBlocks[sourceBlocks.length - 1];
13660
13347
  const effectiveMessageIds = /* @__PURE__ */ new Set();
@@ -13830,8 +13517,7 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
13830
13517
  const newPrompt = renderSystemPrompt(
13831
13518
  runtimePrompts,
13832
13519
  buildProtectedToolsExtension(config.compress.protectedTools),
13833
- state.isSubAgent && config.allowSubAgents,
13834
- (config.compress.images ?? DEFAULT_COMPRESS_IMAGES).nudge !== false ? IMAGE_GUARD_SYSTEM_EXTENSION : void 0
13520
+ state.isSubAgent && config.allowSubAgents
13835
13521
  );
13836
13522
  if (output.system.length > 0) {
13837
13523
  output.system[output.system.length - 1] += "\n\n" + newPrompt;
@@ -14470,7 +14156,7 @@ var server = (async (ctx) => {
14470
14156
  }
14471
14157
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
14472
14158
  logger.info("ACP plugin initialized", {
14473
- version: true ? "1.18.5-pr.490.223" : "dev",
14159
+ version: true ? "1.18.5" : "dev",
14474
14160
  workspace: ctx.directory,
14475
14161
  logLevel: logger.level,
14476
14162
  debug: config.debug,