opencode-acp 1.18.3-pr.477.211 → 1.18.3-pr.479.213

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
@@ -916,6 +916,10 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
916
916
  "compress.reasoning",
917
917
  "compress.reasoning.drop",
918
918
  "compress.reasoning.threshold",
919
+ "compress.images",
920
+ "compress.images.enabled",
921
+ "compress.images.minBytes",
922
+ "compress.images.ttlDays",
919
923
  "compress.completionReserveTokens",
920
924
  "gc",
921
925
  "gc.algorithm",
@@ -1308,6 +1312,37 @@ function validateConfigTypes(config) {
1308
1312
  }
1309
1313
  }
1310
1314
  }
1315
+ if (compress.images !== void 0) {
1316
+ if (typeof compress.images !== "object" || compress.images === null || Array.isArray(compress.images)) {
1317
+ errors.push({
1318
+ key: "compress.images",
1319
+ expected: "object { enabled?: boolean; minBytes?: integer (>= 0); ttlDays?: number (> 0) }",
1320
+ actual: JSON.stringify(compress.images)
1321
+ });
1322
+ } else {
1323
+ if (compress.images.enabled !== void 0 && typeof compress.images.enabled !== "boolean") {
1324
+ errors.push({
1325
+ key: "compress.images.enabled",
1326
+ expected: "boolean",
1327
+ actual: typeof compress.images.enabled
1328
+ });
1329
+ }
1330
+ if (compress.images.minBytes !== void 0 && (typeof compress.images.minBytes !== "number" || !Number.isInteger(compress.images.minBytes) || compress.images.minBytes < 0)) {
1331
+ errors.push({
1332
+ key: "compress.images.minBytes",
1333
+ expected: "integer (>= 0)",
1334
+ actual: JSON.stringify(compress.images.minBytes)
1335
+ });
1336
+ }
1337
+ if (compress.images.ttlDays !== void 0 && (typeof compress.images.ttlDays !== "number" || !Number.isFinite(compress.images.ttlDays) || compress.images.ttlDays <= 0)) {
1338
+ errors.push({
1339
+ key: "compress.images.ttlDays",
1340
+ expected: "number (> 0)",
1341
+ actual: JSON.stringify(compress.images.ttlDays)
1342
+ });
1343
+ }
1344
+ }
1345
+ }
1311
1346
  if (compress.completionReserveTokens !== void 0 && typeof compress.completionReserveTokens !== "number") {
1312
1347
  errors.push({
1313
1348
  key: "compress.completionReserveTokens",
@@ -1695,6 +1730,11 @@ var DEFAULT_COMPRESS_REASONING = {
1695
1730
  drop: true,
1696
1731
  threshold: 2048
1697
1732
  };
1733
+ var DEFAULT_COMPRESS_IMAGES = {
1734
+ enabled: true,
1735
+ minBytes: 16384,
1736
+ ttlDays: 7
1737
+ };
1698
1738
  var DEFAULT_PROTECTED_TOOLS = [
1699
1739
  "task",
1700
1740
  "skill",
@@ -1793,7 +1833,8 @@ var defaultConfig = {
1793
1833
  preserveRecentMessages: 5,
1794
1834
  preserveRecentTokens: 5e3,
1795
1835
  preserveLastUserMessage: true,
1796
- reasoning: { ...DEFAULT_COMPRESS_REASONING }
1836
+ reasoning: { ...DEFAULT_COMPRESS_REASONING },
1837
+ images: { ...DEFAULT_COMPRESS_IMAGES }
1797
1838
  },
1798
1839
  gc: {
1799
1840
  algorithm: "truncate",
@@ -1971,6 +2012,11 @@ function mergeCompress(base, override) {
1971
2012
  drop: override.reasoning?.drop ?? base.reasoning?.drop ?? DEFAULT_COMPRESS_REASONING.drop,
1972
2013
  threshold: override.reasoning?.threshold ?? base.reasoning?.threshold ?? DEFAULT_COMPRESS_REASONING.threshold
1973
2014
  },
2015
+ images: {
2016
+ enabled: override.images?.enabled ?? base.images?.enabled ?? DEFAULT_COMPRESS_IMAGES.enabled,
2017
+ minBytes: override.images?.minBytes ?? base.images?.minBytes ?? DEFAULT_COMPRESS_IMAGES.minBytes,
2018
+ ttlDays: override.images?.ttlDays ?? base.images?.ttlDays ?? DEFAULT_COMPRESS_IMAGES.ttlDays
2019
+ },
1974
2020
  completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens
1975
2021
  };
1976
2022
  }
@@ -2909,7 +2955,7 @@ function saveSessionState(sessionState, logger, sessionName) {
2909
2955
  modelContextLimit: sessionState.modelContextLimit,
2910
2956
  modelProviderID: sessionState.modelProviderID,
2911
2957
  modelID: sessionState.modelID,
2912
- usageSanity: sessionState.usageSanity
2958
+ imageArchive: sessionState.imageArchive
2913
2959
  };
2914
2960
  const key = saveQueueKey(sessionState.sessionId, sessionState.storageDir);
2915
2961
  let queue = saveQueues.get(key);
@@ -3764,32 +3810,18 @@ function adjustBoundariesForToolPairs(context, startIdx, endIdx) {
3764
3810
  return { startIndex: newStartIdx, endIndex: newEndIdx };
3765
3811
  }
3766
3812
  function buildBoundaryRecoveryHint(context, state) {
3767
- const visibleIndexes = [];
3813
+ const visibleRefs = [];
3768
3814
  for (const [messageRef, messageId] of state.messageIds.byRef) {
3769
- if (!context.rawMessagesById.has(messageId)) continue;
3770
- const idx = parseMessageRef(messageRef);
3771
- if (idx !== null) visibleIndexes.push(idx);
3815
+ if (context.rawMessagesById.has(messageId)) {
3816
+ visibleRefs.push(messageRef);
3817
+ }
3772
3818
  }
3773
- visibleIndexes.sort((a, b) => a - b);
3774
- const renderSpan = (start, end) => start === end ? formatMessageRef(start) : `${formatMessageRef(start)}\u2013${formatMessageRef(end)}`;
3775
3819
  const parts = [];
3776
- if (visibleIndexes.length > 0) {
3777
- const spans = [];
3778
- let spanStart = visibleIndexes[0];
3779
- let spanPrev = visibleIndexes[0];
3780
- for (let i = 1; i < visibleIndexes.length; i++) {
3781
- const idx = visibleIndexes[i];
3782
- if (idx !== spanPrev + 1) {
3783
- spans.push(renderSpan(spanStart, spanPrev));
3784
- spanStart = idx;
3785
- }
3786
- spanPrev = idx;
3787
- }
3788
- spans.push(renderSpan(spanStart, spanPrev));
3789
- const MAX_SPANS = 8;
3790
- const shown = spans.slice(0, MAX_SPANS);
3791
- const overflow = spans.length - shown.length;
3792
- parts.push(`Free ranges: ${shown.join(", ")}${overflow > 0 ? ` (+${overflow} more)` : ""}.`);
3820
+ if (visibleRefs.length > 0) {
3821
+ visibleRefs.sort();
3822
+ const first = visibleRefs[0];
3823
+ const last = visibleRefs[visibleRefs.length - 1];
3824
+ parts.push(`Current visible: ${first}\u2013${last} (${visibleRefs.length} msgs).`);
3793
3825
  }
3794
3826
  const blockCount = context.summaryByBlockId.size;
3795
3827
  if (blockCount > 0) {
@@ -3798,7 +3830,7 @@ function buildBoundaryRecoveryHint(context, state) {
3798
3830
  if (parts.length === 0) {
3799
3831
  return "";
3800
3832
  }
3801
- return `${parts.join(" ")} Retry with IDs inside the free ranges.`;
3833
+ return `${parts.join(" ")} Call acp_status() to see which blocks consumed which IDs, then retry with valid IDs.`;
3802
3834
  }
3803
3835
  function clampMessageRef(requested, context, state) {
3804
3836
  if (state.messageIds.byRef.has(requested.ref)) return null;
@@ -5090,7 +5122,9 @@ function createSessionState() {
5090
5122
  systemPromptTokens: void 0,
5091
5123
  storageDir: void 0,
5092
5124
  qualityGateRetryPending: false,
5093
- noContextLimitWarned: false
5125
+ noContextLimitWarned: false,
5126
+ imageArchive: {},
5127
+ imagePruneDone: false
5094
5128
  };
5095
5129
  }
5096
5130
  function resetSessionState(state) {
@@ -5134,6 +5168,8 @@ function resetSessionState(state) {
5134
5168
  state.storageDir = void 0;
5135
5169
  state.qualityGateRetryPending = false;
5136
5170
  state.noContextLimitWarned = false;
5171
+ state.imageArchive = {};
5172
+ state.imagePruneDone = false;
5137
5173
  }
5138
5174
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
5139
5175
  if (state.sessionId === sessionId) {
@@ -5212,8 +5248,21 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
5212
5248
  state.nudges.compressBaselineSet = persisted.nudges.compressBaselineSet ?? false;
5213
5249
  state.stats = {
5214
5250
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
5215
- totalPruneTokens: persisted.stats?.totalPruneTokens || 0
5251
+ totalPruneTokens: persisted.stats?.totalPruneTokens || 0,
5252
+ imagesArchived: persisted.stats?.imagesArchived,
5253
+ imageBytesArchived: persisted.stats?.imageBytesArchived
5216
5254
  };
5255
+ if (persisted.imageArchive && typeof persisted.imageArchive === "object") {
5256
+ for (const [rawId, entries] of Object.entries(persisted.imageArchive)) {
5257
+ if (!Array.isArray(entries)) continue;
5258
+ const valid = entries.filter(
5259
+ (e) => !!e && typeof e.path === "string" && typeof e.mediaType === "string" && typeof e.bytes === "number"
5260
+ );
5261
+ if (valid.length > 0) {
5262
+ state.imageArchive[rawId] = valid;
5263
+ }
5264
+ }
5265
+ }
5217
5266
  const persistedAny = persisted;
5218
5267
  if (persistedAny._persistedMessageIds) {
5219
5268
  state.messageIds = {
@@ -5247,14 +5296,6 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
5247
5296
  state.modelProviderID = persisted.modelProviderID;
5248
5297
  state.modelID = persisted.modelID;
5249
5298
  }
5250
- if (persisted.usageSanity) {
5251
- const { reported, estimated } = persisted.usageSanity;
5252
- if (Number.isFinite(reported) && Number.isFinite(estimated) && reported >= 0 && estimated >= 0) {
5253
- state.usageSanity = { reported, estimated };
5254
- } else {
5255
- state.usageSanity = void 0;
5256
- }
5257
- }
5258
5299
  const applied = applyPendingCompressionDurations(state);
5259
5300
  if (applied > 0) {
5260
5301
  await saveSessionState(state, logger);
@@ -6634,6 +6675,156 @@ function buildPhantomSkipNotice(identification) {
6634
6675
  return `Skipped ${identification.phantomIndices.length} already-compressed ${noun} (${skippedNums}) \u2014 they are no-ops; the remaining entries were compressed.`;
6635
6676
  }
6636
6677
 
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
+
6637
6828
  // lib/compress/keep-markers.ts
6638
6829
  var KEEP_REGEX = /\[\[KEEP:(m\d+)\]\]/g;
6639
6830
  var REF_REGEX = /\[\[REF:(m\d+)\|([^\]]+)\]\]/g;
@@ -6807,56 +6998,13 @@ ${guidance}
6807
6998
  ${afterClose}`;
6808
6999
  }
6809
7000
 
6810
- // lib/usage-sanity.ts
6811
- var RELAY_INFLATION_RATIO = 1.5;
6812
- var PAYLOAD_FLOOR_TOKENS = 5e3;
6813
- function estimatePayloadTokens(messages) {
6814
- let total = 0;
6815
- for (const msg of messages) {
6816
- if (isSyntheticMessage(msg)) continue;
6817
- if (isIgnoredUserMessage(msg)) continue;
6818
- total += estimateAllMessageTokensFast(msg);
6819
- }
6820
- return total;
6821
- }
6822
- function resolveCurrentUsage(state, messages, logger) {
6823
- const reported = getCurrentTokenUsage(state, messages);
6824
- if (reported === 0) {
6825
- return { tokens: 0, source: "reported", inflationDetected: false };
6826
- }
6827
- const estimated = estimatePayloadTokens(messages) + (state.systemPromptTokens ?? 0);
6828
- const prev = state.usageSanity;
6829
- let inflationDetected = false;
6830
- if (prev !== void 0 && prev.reported > 0 && prev.estimated >= PAYLOAD_FLOOR_TOKENS && estimated >= PAYLOAD_FLOOR_TOKENS) {
6831
- const reportRatio = reported / prev.reported;
6832
- const payloadRatio = estimated / prev.estimated;
6833
- if (reportRatio > RELAY_INFLATION_RATIO && payloadRatio <= RELAY_INFLATION_RATIO) {
6834
- inflationDetected = true;
6835
- }
6836
- }
6837
- state.usageSanity = { reported, estimated };
6838
- if (inflationDetected) {
6839
- logger?.warn("Relay usage inflation detected \u2014 re-estimating context size", {
6840
- session: state.sessionId,
6841
- reported,
6842
- previousReported: prev.reported,
6843
- reportRatio: +(reported / prev.reported).toFixed(3),
6844
- estimated,
6845
- previousEstimated: prev.estimated,
6846
- payloadRatio: +(estimated / prev.estimated).toFixed(3)
6847
- });
6848
- return { tokens: estimated, source: "reestimated", inflationDetected: true };
6849
- }
6850
- return { tokens: reported, source: "reported", inflationDetected: false };
6851
- }
6852
-
6853
7001
  // lib/messages/utils.ts
6854
- import { createHash as createHash2 } from "crypto";
7002
+ import { createHash as createHash3 } from "crypto";
6855
7003
  var SUMMARY_ID_HASH_LENGTH = 16;
6856
7004
  var DCP_PAIRED_TAG_REGEX = /<(?:dcp|acp)[^>]*>[\s\S]*?<\/(?:dcp|acp)[^>]*>/gi;
6857
7005
  var DCP_UNPAIRED_TAG_REGEX = /<\/?(?:dcp|acp)[^>]*>/gi;
6858
7006
  var generateStableId = (prefix, seed) => {
6859
- const hash = createHash2("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
7007
+ const hash = createHash3("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
6860
7008
  return `${prefix}_${hash}`;
6861
7009
  };
6862
7010
  var createSyntheticMessage = (baseMessage, content, stableSeed, role = "user") => {
@@ -7200,7 +7348,7 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
7200
7348
  );
7201
7349
  const maxContextLimit = resolvedMaxContextLimit === void 0 ? void 0 : resolvedMaxContextLimit + summaryTokenExtension;
7202
7350
  const minContextLimit = resolveContextTokenLimit(config, state, providerId, modelId, "min");
7203
- const currentTokens = resolveCurrentUsage(state, messages).tokens;
7351
+ const currentTokens = getCurrentTokenUsage(state, messages);
7204
7352
  let overMaxLimit = maxContextLimit === void 0 ? false : currentTokens > maxContextLimit;
7205
7353
  const overMinLimit = minContextLimit === void 0 ? false : currentTokens >= minContextLimit;
7206
7354
  if (overMaxLimit) {
@@ -8123,7 +8271,12 @@ function createCompressRangeTool(factoryCtx) {
8123
8271
  entry: plan.entry,
8124
8272
  selection: plan.selection,
8125
8273
  anchorMessageId: plan.anchorMessageId,
8126
- finalSummary: completedSummary.expandedSummary,
8274
+ finalSummary: appendArchivedImagePointers(
8275
+ completedSummary.expandedSummary,
8276
+ plan.selection,
8277
+ ctx.state,
8278
+ ctx.config.compress.images?.enabled !== false
8279
+ ),
8127
8280
  consumedBlockIds: mergeConsumedBlockIds
8128
8281
  });
8129
8282
  }
@@ -9352,8 +9505,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
9352
9505
  const allInProtectedZone = protectedRefs.size > 0 && unprotectedCompressible.length === 0;
9353
9506
  const allBelowMin = contextRanges.compressible.length > 0 && recommendedRanges.length === 0;
9354
9507
  const noExecutableCandidates = candidatesEnabled && nudgeAllowed && candidateMessages !== void 0 && candidatePlan.candidates.length === 0;
9355
- const noRangesAtAll = contextRanges.compressible.length === 0 && contextRanges.protected.length === 0;
9356
- const nothingToCompress = (candidatesEnabled && candidateMessages !== void 0 ? noExecutableCandidates : allProtected || allInProtectedZone || allBelowMin) || noRangesAtAll;
9508
+ const nothingToCompress = candidatesEnabled && candidateMessages !== void 0 ? noExecutableCandidates : allProtected || allInProtectedZone || allBelowMin;
9357
9509
  const emergencyNoTargets = emergencyOverride && nothingToCompress;
9358
9510
  const noticeCadenceMet = state.nudges.lastNudgeShownTokens === void 0 || growthSinceBaseline !== void 0 && growthSinceBaseline >= growthFloor;
9359
9511
  let shouldInjectNudge = nudgeAllowed && !nothingToCompress;
@@ -9558,7 +9710,7 @@ ${rules}`;
9558
9710
  } else if (nudgeAllowed && nothingToCompress) {
9559
9711
  logger.info("Nudge suppressed: nothing to compress", {
9560
9712
  session: state.sessionId,
9561
- reason: allProtected ? "all_protected" : allInProtectedZone ? "in_protected_zone" : allBelowMin ? "below_effective_floor" : noRangesAtAll ? "no_compressible_ranges" : "no_executable_candidates",
9713
+ reason: allProtected ? "all_protected" : allInProtectedZone ? "in_protected_zone" : allBelowMin ? "below_effective_floor" : "no_executable_candidates",
9562
9714
  currentTokens,
9563
9715
  usagePct,
9564
9716
  compressibleRanges: contextRanges.compressible.length
@@ -11173,10 +11325,10 @@ function hideFailedCompressCalls(messages) {
11173
11325
 
11174
11326
  // lib/logger.ts
11175
11327
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
11176
- import { join as join4 } from "path";
11177
- import { existsSync as existsSync4 } from "fs";
11328
+ import { join as join5 } from "path";
11329
+ import { existsSync as existsSync5 } from "fs";
11178
11330
  import { homedir as homedir3 } from "os";
11179
- var LOG_VERSION = true ? "1.18.3-pr.477.211" : "dev";
11331
+ var LOG_VERSION = true ? "1.18.3-pr.479.213" : "dev";
11180
11332
  var LEVEL_RANK = {
11181
11333
  debug: 10,
11182
11334
  info: 20,
@@ -11196,8 +11348,8 @@ var Logger = class {
11196
11348
  constructor(enabled, level) {
11197
11349
  const resolved = level !== void 0 && LEVEL_RANK[level] !== void 0 ? level : void 0;
11198
11350
  this.level = resolved ?? (enabled ? "debug" : "warn");
11199
- const configHome = process.env.XDG_CONFIG_HOME || join4(homedir3(), ".config");
11200
- this.logDir = join4(configHome, "opencode", "logs", "acp");
11351
+ const configHome = process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config");
11352
+ this.logDir = join5(configHome, "opencode", "logs", "acp");
11201
11353
  }
11202
11354
  /** Back-compat view of the old flag: true only at full debug verbosity. */
11203
11355
  get enabled() {
@@ -11207,7 +11359,7 @@ var Logger = class {
11207
11359
  return LEVEL_RANK[level] >= LEVEL_RANK[this.level];
11208
11360
  }
11209
11361
  async ensureLogDir() {
11210
- if (!existsSync4(this.logDir)) {
11362
+ if (!existsSync5(this.logDir)) {
11211
11363
  await mkdir2(this.logDir, { recursive: true });
11212
11364
  }
11213
11365
  }
@@ -11259,11 +11411,11 @@ var Logger = class {
11259
11411
  const dataStr = this.formatData(data);
11260
11412
  const logLine = `${timestamp} ${level.padEnd(5)} ${component}: ${message}${dataStr ? " | " + dataStr : ""} | v=${LOG_VERSION}
11261
11413
  `;
11262
- const dailyLogDir = join4(this.logDir, "daily");
11263
- if (!existsSync4(dailyLogDir)) {
11414
+ const dailyLogDir = join5(this.logDir, "daily");
11415
+ if (!existsSync5(dailyLogDir)) {
11264
11416
  await mkdir2(dailyLogDir, { recursive: true });
11265
11417
  }
11266
- const logFile = join4(dailyLogDir, `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
11418
+ const logFile = join5(dailyLogDir, `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
11267
11419
  await writeFile2(logFile, logLine, { flag: "a" });
11268
11420
  } catch (error) {
11269
11421
  }
@@ -11374,18 +11526,18 @@ var Logger = class {
11374
11526
  async saveContext(sessionId, messages) {
11375
11527
  if (this.level !== "debug") return;
11376
11528
  try {
11377
- const contextDir = join4(this.logDir, "context", sessionId);
11378
- if (!existsSync4(contextDir)) {
11529
+ const contextDir = join5(this.logDir, "context", sessionId);
11530
+ if (!existsSync5(contextDir)) {
11379
11531
  await mkdir2(contextDir, { recursive: true });
11380
11532
  }
11381
11533
  const minimized = this.minimizeForDebug(messages).filter(
11382
11534
  (msg) => msg.parts && msg.parts.length > 0
11383
11535
  );
11384
11536
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
11385
- const contextFile = join4(contextDir, `${timestamp}.json`);
11537
+ const contextFile = join5(contextDir, `${timestamp}.json`);
11386
11538
  await writeFile2(contextFile, JSON.stringify(minimized, null, 2));
11387
- const versionFile = join4(contextDir, "_version");
11388
- if (!existsSync4(versionFile)) {
11539
+ const versionFile = join5(contextDir, "_version");
11540
+ if (!existsSync5(versionFile)) {
11389
11541
  await writeFile2(versionFile, LOG_VERSION);
11390
11542
  }
11391
11543
  } catch (error) {
@@ -11394,8 +11546,8 @@ var Logger = class {
11394
11546
  };
11395
11547
 
11396
11548
  // lib/prompts/store.ts
11397
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync as statSync2 } from "fs";
11398
- import { join as join5, dirname as dirname2 } from "path";
11549
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3, statSync as statSync3 } from "fs";
11550
+ import { join as join6, dirname as dirname2 } from "path";
11399
11551
  import { homedir as homedir4 } from "os";
11400
11552
 
11401
11553
  // lib/prompts/system.ts
@@ -11703,10 +11855,10 @@ function createBundledRuntimePrompts(candidatesEnabled) {
11703
11855
  function findOpencodeDir2(startDir) {
11704
11856
  let current = startDir;
11705
11857
  while (current !== "/") {
11706
- const candidate = join5(current, ".opencode");
11707
- if (existsSync5(candidate)) {
11858
+ const candidate = join6(current, ".opencode");
11859
+ if (existsSync6(candidate)) {
11708
11860
  try {
11709
- if (statSync2(candidate).isDirectory()) {
11861
+ if (statSync3(candidate).isDirectory()) {
11710
11862
  return candidate;
11711
11863
  }
11712
11864
  } catch {
@@ -11721,13 +11873,13 @@ function findOpencodeDir2(startDir) {
11721
11873
  return null;
11722
11874
  }
11723
11875
  function resolvePromptPaths(workingDirectory) {
11724
- const configHome = process.env.XDG_CONFIG_HOME || join5(homedir4(), ".config");
11725
- const globalRoot = join5(configHome, "opencode", "acp-prompts");
11726
- const defaultsDir = join5(globalRoot, "defaults");
11727
- const globalOverridesDir = join5(globalRoot, "overrides");
11728
- const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR ? join5(process.env.OPENCODE_CONFIG_DIR, "acp-prompts", "overrides") : null;
11876
+ const configHome = process.env.XDG_CONFIG_HOME || join6(homedir4(), ".config");
11877
+ const globalRoot = join6(configHome, "opencode", "acp-prompts");
11878
+ const defaultsDir = join6(globalRoot, "defaults");
11879
+ const globalOverridesDir = join6(globalRoot, "overrides");
11880
+ const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR ? join6(process.env.OPENCODE_CONFIG_DIR, "acp-prompts", "overrides") : null;
11729
11881
  const opencodeDir = findOpencodeDir2(workingDirectory);
11730
- const projectOverridesDir = opencodeDir ? join5(opencodeDir, "acp-prompts", "overrides") : null;
11882
+ const projectOverridesDir = opencodeDir ? join6(opencodeDir, "acp-prompts", "overrides") : null;
11731
11883
  return {
11732
11884
  defaultsDir,
11733
11885
  globalOverridesDir,
@@ -11827,7 +11979,7 @@ function buildDefaultsReadmeContent() {
11827
11979
  `;
11828
11980
  }
11829
11981
  function readFileIfExists(filePath) {
11830
- if (!existsSync5(filePath)) {
11982
+ if (!existsSync6(filePath)) {
11831
11983
  return null;
11832
11984
  }
11833
11985
  try {
@@ -11900,23 +12052,23 @@ var PromptStore = class {
11900
12052
  const candidates = [];
11901
12053
  if (this.paths.projectOverridesDir) {
11902
12054
  candidates.push({
11903
- path: join5(this.paths.projectOverridesDir, fileName)
12055
+ path: join6(this.paths.projectOverridesDir, fileName)
11904
12056
  });
11905
12057
  }
11906
12058
  if (this.paths.configDirOverridesDir) {
11907
12059
  candidates.push({
11908
- path: join5(this.paths.configDirOverridesDir, fileName)
12060
+ path: join6(this.paths.configDirOverridesDir, fileName)
11909
12061
  });
11910
12062
  }
11911
12063
  candidates.push({
11912
- path: join5(this.paths.globalOverridesDir, fileName)
12064
+ path: join6(this.paths.globalOverridesDir, fileName)
11913
12065
  });
11914
12066
  return candidates;
11915
12067
  }
11916
12068
  ensureDefaultFiles() {
11917
12069
  try {
11918
- mkdirSync2(this.paths.defaultsDir, { recursive: true });
11919
- mkdirSync2(this.paths.globalOverridesDir, { recursive: true });
12070
+ mkdirSync3(this.paths.defaultsDir, { recursive: true });
12071
+ mkdirSync3(this.paths.globalOverridesDir, { recursive: true });
11920
12072
  } catch {
11921
12073
  this.logger.warn("Failed to initialize prompt directories", {
11922
12074
  defaultsDir: this.paths.defaultsDir,
@@ -11933,13 +12085,13 @@ var PromptStore = class {
11933
12085
  const managedContent = buildDefaultPromptFileContent(
11934
12086
  bundledEditable || bundledPrompts[definition.runtimeField]
11935
12087
  );
11936
- const filePath = join5(this.paths.defaultsDir, definition.fileName);
12088
+ const filePath = join6(this.paths.defaultsDir, definition.fileName);
11937
12089
  try {
11938
12090
  const existing = readFileIfExists(filePath);
11939
12091
  if (existing === managedContent) {
11940
12092
  continue;
11941
12093
  }
11942
- writeFileSync2(filePath, managedContent, "utf-8");
12094
+ writeFileSync3(filePath, managedContent, "utf-8");
11943
12095
  } catch {
11944
12096
  this.logger.warn("Failed to write default prompt file", {
11945
12097
  key: definition.key,
@@ -11947,12 +12099,12 @@ var PromptStore = class {
11947
12099
  });
11948
12100
  }
11949
12101
  }
11950
- const readmePath = join5(this.paths.defaultsDir, DEFAULTS_README_FILE);
12102
+ const readmePath = join6(this.paths.defaultsDir, DEFAULTS_README_FILE);
11951
12103
  const readmeContent = buildDefaultsReadmeContent();
11952
12104
  try {
11953
12105
  const existing = readFileIfExists(readmePath);
11954
12106
  if (existing !== readmeContent) {
11955
- writeFileSync2(readmePath, readmeContent, "utf-8");
12107
+ writeFileSync3(readmePath, readmeContent, "utf-8");
11956
12108
  }
11957
12109
  } catch {
11958
12110
  this.logger.warn("Failed to write defaults README", {
@@ -12420,8 +12572,8 @@ ${report}`;
12420
12572
 
12421
12573
  // lib/commands/export.ts
12422
12574
  import * as fs2 from "fs/promises";
12423
- import { existsSync as existsSync6 } from "fs";
12424
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, resolve } from "path";
12575
+ import { existsSync as existsSync7 } from "fs";
12576
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join7, resolve } from "path";
12425
12577
  var ALL_TIERS = [1, 2, 3];
12426
12578
  var TIER_NAMES = {
12427
12579
  1: "Tier 1 \u2014 Capture",
@@ -12534,7 +12686,7 @@ function parseTiers(raw) {
12534
12686
  }
12535
12687
  function resolveDefaultOutputPath(sessionId, cwd2) {
12536
12688
  const short = (sessionId || "session").slice(0, 8);
12537
- return join6(cwd2, ".opencode", `acp-export-${short}.md`);
12689
+ return join7(cwd2, ".opencode", `acp-export-${short}.md`);
12538
12690
  }
12539
12691
  function formatTokens2(n) {
12540
12692
  if (!Number.isFinite(n) || n <= 0) return "0";
@@ -12665,11 +12817,11 @@ async function handleExportCommand(ctx, args) {
12665
12817
  const targetPath = options.outputPath !== "" ? isAbsolute2(options.outputPath) ? options.outputPath : resolve(cwd2, options.outputPath) : resolveDefaultOutputPath(sessionId, cwd2);
12666
12818
  try {
12667
12819
  const dir = dirname3(targetPath);
12668
- if (!existsSync6(dir)) {
12820
+ if (!existsSync7(dir)) {
12669
12821
  await fs2.mkdir(dir, { recursive: true });
12670
12822
  }
12671
12823
  const flag = options.append ? "a" : "w";
12672
- if (options.append && existsSync6(targetPath)) {
12824
+ if (options.append && existsSync7(targetPath)) {
12673
12825
  await fs2.appendFile(targetPath, `
12674
12826
 
12675
12827
  ---
@@ -13384,6 +13536,20 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
13384
13536
  });
13385
13537
  cacheSystemPromptTokens(state, output.messages);
13386
13538
  assignMessageRefs(state, output.messages);
13539
+ const imagesConfig = config.compress.images ?? DEFAULT_COMPRESS_IMAGES;
13540
+ if (imagesConfig.enabled) {
13541
+ const archivedImages = archiveSessionImages(
13542
+ state,
13543
+ output.messages,
13544
+ imagesConfig,
13545
+ logger
13546
+ );
13547
+ if (archivedImages > 0) {
13548
+ saveSessionState(state, logger).catch(() => {
13549
+ });
13550
+ }
13551
+ pruneImageArchive(state, imagesConfig, logger);
13552
+ }
13387
13553
  const activeBlockCountBefore = state.prune.messages.activeBlockIds.size;
13388
13554
  const compressionStateChanged = syncCompressionBlocks(state, logger, output.messages);
13389
13555
  if (compressionStateChanged || state.prune.messages.activeBlockIds.size !== activeBlockCountBefore) {
@@ -13397,7 +13563,7 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
13397
13563
  saveSessionState(state, logger).catch(() => {
13398
13564
  });
13399
13565
  }
13400
- const prePruneTokens = resolveCurrentUsage(state, output.messages, logger).tokens;
13566
+ const prePruneTokens = getCurrentTokenUsage(state, output.messages);
13401
13567
  const candidateMessages = config.compress.candidates === true ? output.messages.slice() : void 0;
13402
13568
  prune(state, logger, config, output.messages);
13403
13569
  hideConsumedCompressCalls(state, output.messages);
@@ -13446,7 +13612,7 @@ ${text}`);
13446
13612
  if (state.sessionId && smartPlanEnabled === true) {
13447
13613
  recordVisibleMessages(state.sessionId, output.messages);
13448
13614
  }
13449
- const postTokens = resolveCurrentUsage(state, output.messages, logger).tokens;
13615
+ const postTokens = getCurrentTokenUsage(state, output.messages);
13450
13616
  if (postTokens !== void 0 && effectiveLimit) {
13451
13617
  const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
13452
13618
  if (postTokens > budget) {
@@ -13661,7 +13827,7 @@ function configureClientAuth(client) {
13661
13827
 
13662
13828
  // lib/update.ts
13663
13829
  import { readFile as readFile2, rm } from "fs/promises";
13664
- import { basename, dirname as dirname4, join as join7 } from "path";
13830
+ import { basename, dirname as dirname4, join as join8 } from "path";
13665
13831
  import { fileURLToPath } from "url";
13666
13832
  var PACKAGE_NAME = "opencode-acp";
13667
13833
  function startAutoUpdate(ctx, enabled, logger) {
@@ -13708,7 +13874,7 @@ async function checkAutoUpdate(signal, logger) {
13708
13874
  logger?.info("Auto-update skipped: package dir not found");
13709
13875
  return { updated: false };
13710
13876
  }
13711
- const pkg = await readPackageJson(join7(packageDir, "package.json"));
13877
+ const pkg = await readPackageJson(join8(packageDir, "package.json"));
13712
13878
  if (!pkg?.name || !pkg.version) {
13713
13879
  logger?.info("Auto-update skipped: package.json unreadable", { packageDir });
13714
13880
  return { updated: false };
@@ -13749,7 +13915,7 @@ async function checkAutoUpdate(signal, logger) {
13749
13915
  async function findPackageDir(name) {
13750
13916
  let dir = dirname4(fileURLToPath(import.meta.url));
13751
13917
  for (; ; ) {
13752
- const pkg = await readPackageJson(join7(dir, "package.json"));
13918
+ const pkg = await readPackageJson(join8(dir, "package.json"));
13753
13919
  if (pkg?.name === name) return dir;
13754
13920
  const parent = dirname4(dir);
13755
13921
  if (parent === dir) return void 0;
@@ -13761,7 +13927,7 @@ async function updateTarget(packageDir, name) {
13761
13927
  const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname4(packageParent) : packageParent;
13762
13928
  if (basename(nodeModulesDir) !== "node_modules") return void 0;
13763
13929
  const wrapperDir = dirname4(nodeModulesDir);
13764
- const wrapperPkg = await readPackageJson(join7(wrapperDir, "package.json"));
13930
+ const wrapperPkg = await readPackageJson(join8(wrapperDir, "package.json"));
13765
13931
  const spec = wrapperSpec(wrapperDir, name) ?? wrapperPkg?.dependencies?.[name];
13766
13932
  if (!spec || !isAutoUpdatableSpec(spec)) return void 0;
13767
13933
  return { removeDir: wrapperDir, spec };
@@ -13872,7 +14038,7 @@ var server = (async (ctx) => {
13872
14038
  }
13873
14039
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
13874
14040
  logger.info("ACP plugin initialized", {
13875
- version: true ? "1.18.3-pr.477.211" : "dev",
14041
+ version: true ? "1.18.3-pr.479.213" : "dev",
13876
14042
  workspace: ctx.directory,
13877
14043
  logLevel: logger.level,
13878
14044
  debug: config.debug,