opencode-acp 1.18.4 → 1.18.5-pr.416.226

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
@@ -921,6 +921,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
921
921
  "compress.images.minBytes",
922
922
  "compress.images.ttlDays",
923
923
  "compress.completionReserveTokens",
924
+ "compress.emergencyTruncation",
924
925
  "gc",
925
926
  "gc.algorithm",
926
927
  "gc.promotionThreshold",
@@ -1350,6 +1351,13 @@ function validateConfigTypes(config) {
1350
1351
  actual: typeof compress.completionReserveTokens
1351
1352
  });
1352
1353
  }
1354
+ if (compress.emergencyTruncation !== void 0 && typeof compress.emergencyTruncation !== "boolean") {
1355
+ errors.push({
1356
+ key: "compress.emergencyTruncation",
1357
+ expected: "boolean",
1358
+ actual: typeof compress.emergencyTruncation
1359
+ });
1360
+ }
1353
1361
  if (typeof compress.completionReserveTokens === "number" && compress.completionReserveTokens < 0) {
1354
1362
  errors.push({
1355
1363
  key: "compress.completionReserveTokens",
@@ -1422,6 +1430,7 @@ function validateConfigTypes(config) {
1422
1430
  const OVERRIDE_FIELD_TYPES = {
1423
1431
  showCompression: "boolean",
1424
1432
  summaryBuffer: "boolean",
1433
+ emergencyTruncation: "boolean",
1425
1434
  protectTags: "boolean",
1426
1435
  protectUserMessages: "boolean",
1427
1436
  lastSegmentSoftBlock: "boolean",
@@ -1809,6 +1818,7 @@ var defaultConfig = {
1809
1818
  permission: "allow",
1810
1819
  showCompression: true,
1811
1820
  summaryBuffer: true,
1821
+ emergencyTruncation: false,
1812
1822
  candidates: false,
1813
1823
  maxContextLimit: "80%",
1814
1824
  minContextLimit: "80%",
@@ -2017,7 +2027,8 @@ function mergeCompress(base, override) {
2017
2027
  minBytes: override.images?.minBytes ?? base.images?.minBytes ?? DEFAULT_COMPRESS_IMAGES.minBytes,
2018
2028
  ttlDays: override.images?.ttlDays ?? base.images?.ttlDays ?? DEFAULT_COMPRESS_IMAGES.ttlDays
2019
2029
  },
2020
- completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens
2030
+ completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens,
2031
+ emergencyTruncation: override.emergencyTruncation ?? base.emergencyTruncation
2021
2032
  };
2022
2033
  }
2023
2034
  function mergeCommands(base, override) {
@@ -2585,6 +2596,209 @@ import { existsSync as existsSync2 } from "fs";
2585
2596
  import { homedir as homedir2 } from "os";
2586
2597
  import { isAbsolute, join as join2 } from "path";
2587
2598
 
2599
+ // lib/message-ids.ts
2600
+ var MESSAGE_REF_REGEX = /^m(\d{4,5})$/;
2601
+ var BLOCK_REF_REGEX = /^b([1-9]\d*)$/;
2602
+ var MESSAGE_ID_TAG_NAME = "dcp-message-id";
2603
+ var MESSAGE_REF_WIDTH = 5;
2604
+ var MESSAGE_REF_MIN_INDEX = 1;
2605
+ var MESSAGE_REF_MAX_INDEX = 99999;
2606
+ function formatMessageRef(index) {
2607
+ if (!Number.isInteger(index) || index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
2608
+ throw new Error(
2609
+ `Message ID index out of bounds: ${index}. Supported range is ${MESSAGE_REF_MIN_INDEX}-${MESSAGE_REF_MAX_INDEX}.`
2610
+ );
2611
+ }
2612
+ return `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}`;
2613
+ }
2614
+ function formatBlockRef(blockId) {
2615
+ if (!Number.isInteger(blockId) || blockId < 1) {
2616
+ throw new Error(`Invalid block ID: ${blockId}`);
2617
+ }
2618
+ return `b${blockId}`;
2619
+ }
2620
+ function parseMessageRef(ref) {
2621
+ const normalized = ref.trim().toLowerCase();
2622
+ const match = normalized.match(MESSAGE_REF_REGEX);
2623
+ if (!match) {
2624
+ return null;
2625
+ }
2626
+ const index = Number.parseInt(match[1], 10);
2627
+ if (!Number.isInteger(index)) {
2628
+ return null;
2629
+ }
2630
+ if (index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
2631
+ return null;
2632
+ }
2633
+ return index;
2634
+ }
2635
+ function migrateMessageRef(ref) {
2636
+ const index = parseMessageRef(ref);
2637
+ return index === null ? ref : formatMessageRef(index);
2638
+ }
2639
+ function parseBlockRef(ref) {
2640
+ const normalized = ref.trim().toLowerCase();
2641
+ const match = normalized.match(BLOCK_REF_REGEX);
2642
+ if (!match) {
2643
+ return null;
2644
+ }
2645
+ const id = Number.parseInt(match[1], 10);
2646
+ return Number.isInteger(id) ? id : null;
2647
+ }
2648
+ function parseBoundaryId(id) {
2649
+ const normalized = id.trim().toLowerCase();
2650
+ const messageIndex = parseMessageRef(normalized);
2651
+ if (messageIndex !== null) {
2652
+ return {
2653
+ kind: "message",
2654
+ ref: formatMessageRef(messageIndex),
2655
+ index: messageIndex
2656
+ };
2657
+ }
2658
+ const blockId = parseBlockRef(normalized);
2659
+ if (blockId !== null) {
2660
+ return {
2661
+ kind: "compressed-block",
2662
+ ref: formatBlockRef(blockId),
2663
+ blockId
2664
+ };
2665
+ }
2666
+ return null;
2667
+ }
2668
+ function escapeXmlAttribute(value) {
2669
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2670
+ }
2671
+ function formatMessageIdTag(ref, attributes) {
2672
+ const serializedAttributes = Object.entries(attributes || {}).sort(([left], [right]) => left.localeCompare(right)).map(([name, value]) => {
2673
+ if (name.trim().length === 0 || typeof value !== "string" || value.length === 0) {
2674
+ return "";
2675
+ }
2676
+ return ` ${name}="${escapeXmlAttribute(value)}"`;
2677
+ }).join("");
2678
+ return `
2679
+ <${MESSAGE_ID_TAG_NAME}${serializedAttributes}>${ref}</${MESSAGE_ID_TAG_NAME}>`;
2680
+ }
2681
+ function formatTokenSize(tokens) {
2682
+ if (tokens < 1e3) return String(tokens);
2683
+ if (tokens < 1e4) return `${(tokens / 1e3).toFixed(1)}K`;
2684
+ return `${Math.round(tokens / 1e3)}K`;
2685
+ }
2686
+ function classifyMessageType(parts) {
2687
+ let hasTool = false;
2688
+ let hasText = false;
2689
+ let hasReasoning = false;
2690
+ const toolNames = [];
2691
+ for (const part of parts) {
2692
+ if (part.type === "tool") {
2693
+ hasTool = true;
2694
+ if (typeof part.tool === "string" && !toolNames.includes(part.tool)) {
2695
+ toolNames.push(part.tool);
2696
+ }
2697
+ } else if (part.type === "text") {
2698
+ hasText = true;
2699
+ } else if (part.type === "reasoning") {
2700
+ hasReasoning = true;
2701
+ }
2702
+ }
2703
+ if (hasTool) {
2704
+ return toolNames.length > 0 ? `tool:${toolNames.join(",")}` : "tool";
2705
+ }
2706
+ if (hasReasoning && !hasText) return "reasoning";
2707
+ return "text";
2708
+ }
2709
+ function assignMessageRefs(state, messages) {
2710
+ let assigned = 0;
2711
+ let skippedSubAgentPrompt = false;
2712
+ for (const message of messages) {
2713
+ if (isIgnoredUserMessage(message)) {
2714
+ continue;
2715
+ }
2716
+ if (state.isSubAgent && !skippedSubAgentPrompt && message.info.role === "user") {
2717
+ skippedSubAgentPrompt = true;
2718
+ continue;
2719
+ }
2720
+ const rawMessageId = message.info.id;
2721
+ if (typeof rawMessageId !== "string" || rawMessageId.length === 0) {
2722
+ continue;
2723
+ }
2724
+ if (rawMessageId.startsWith("msg_dcp_summary_") || rawMessageId.startsWith("msg_dcp_text_")) {
2725
+ continue;
2726
+ }
2727
+ const existingRef = state.messageIds.byRawId.get(rawMessageId);
2728
+ if (existingRef) {
2729
+ if (state.messageIds.byRef.get(existingRef) !== rawMessageId) {
2730
+ state.messageIds.byRef.set(existingRef, rawMessageId);
2731
+ }
2732
+ continue;
2733
+ }
2734
+ const ref = allocateNextMessageRef(state);
2735
+ state.messageIds.byRawId.set(rawMessageId, ref);
2736
+ state.messageIds.byRef.set(ref, rawMessageId);
2737
+ assigned++;
2738
+ }
2739
+ return assigned;
2740
+ }
2741
+ function repairNonMonotonicMessageRefs(state, messages) {
2742
+ const eligible = messages.filter((message) => {
2743
+ if (isIgnoredUserMessage(message)) return false;
2744
+ const id = message.info.id;
2745
+ return typeof id === "string" && id.length > 0 && !id.startsWith("msg_dcp_summary_") && !id.startsWith("msg_dcp_text_");
2746
+ });
2747
+ let previous = 0;
2748
+ let corrupted = false;
2749
+ for (const message of eligible) {
2750
+ const ref = state.messageIds.byRawId.get(message.info.id);
2751
+ const parsed = ref ? parseMessageRef(ref) : null;
2752
+ if (parsed === null) continue;
2753
+ if (parsed <= previous) {
2754
+ corrupted = true;
2755
+ break;
2756
+ }
2757
+ previous = parsed;
2758
+ }
2759
+ if (!corrupted) return false;
2760
+ const oldRawIds = [...state.messageIds.byRawId.keys()];
2761
+ state.messageIds.byRawId.clear();
2762
+ state.messageIds.byRef.clear();
2763
+ state.messageIds.nextRef = 1;
2764
+ for (const message of eligible) {
2765
+ const rawId = message.info.id;
2766
+ const ref = allocateNextMessageRef(state);
2767
+ state.messageIds.byRawId.set(rawId, ref);
2768
+ state.messageIds.byRef.set(ref, rawId);
2769
+ }
2770
+ for (const rawId of oldRawIds) {
2771
+ if (state.messageIds.byRawId.has(rawId)) continue;
2772
+ const ref = allocateNextMessageRef(state);
2773
+ state.messageIds.byRawId.set(rawId, ref);
2774
+ state.messageIds.byRef.set(ref, rawId);
2775
+ }
2776
+ for (const block of state.prune.messages.blocksById.values()) {
2777
+ const ids = [.../* @__PURE__ */ new Set([...block.effectiveMessageIds, ...block.directMessageIds])].filter((id) => state.messageIds.byRawId.has(id)).sort(
2778
+ (left, right) => (parseMessageRef(state.messageIds.byRawId.get(left)) ?? 0) - (parseMessageRef(state.messageIds.byRawId.get(right)) ?? 0)
2779
+ );
2780
+ if (ids.length === 0) continue;
2781
+ block.startId = state.messageIds.byRawId.get(ids[0]);
2782
+ block.endId = state.messageIds.byRawId.get(ids[ids.length - 1]);
2783
+ }
2784
+ state.prune.messages.structureVersion = (state.prune.messages.structureVersion ?? 0) + 1;
2785
+ return true;
2786
+ }
2787
+ function allocateNextMessageRef(state) {
2788
+ let candidate = Number.isInteger(state.messageIds.nextRef) ? Math.max(MESSAGE_REF_MIN_INDEX, state.messageIds.nextRef) : MESSAGE_REF_MIN_INDEX;
2789
+ while (candidate <= MESSAGE_REF_MAX_INDEX) {
2790
+ const ref = formatMessageRef(candidate);
2791
+ if (!state.messageIds.byRef.has(ref)) {
2792
+ state.messageIds.nextRef = candidate + 1;
2793
+ return ref;
2794
+ }
2795
+ candidate++;
2796
+ }
2797
+ throw new Error(
2798
+ `Message ID alias capacity exceeded. Cannot allocate more than ${formatMessageRef(MESSAGE_REF_MAX_INDEX)} aliases in this session.`
2799
+ );
2800
+ }
2801
+
2588
2802
  // lib/state/utils.ts
2589
2803
  var isMessageCompacted = (state, msg) => {
2590
2804
  if (!isMessageWithInfo(msg)) {
@@ -2736,8 +2950,10 @@ function loadPruneMessagesState(persisted) {
2736
2950
  mode: block.mode === "range" || block.mode === "message" ? block.mode : void 0,
2737
2951
  topic: typeof block.topic === "string" ? block.topic : "",
2738
2952
  batchTopic: typeof block.batchTopic === "string" ? block.batchTopic : typeof block.topic === "string" ? block.topic : "",
2739
- startId: typeof block.startId === "string" ? block.startId : "",
2740
- endId: typeof block.endId === "string" ? block.endId : "",
2953
+ // [Issue #415] Migrate legacy 4-digit boundary refs (pre-1.1.0 state)
2954
+ // to canonical 5-digit form, matching the messageIds ref migration.
2955
+ startId: typeof block.startId === "string" ? migrateMessageRef(block.startId) : "",
2956
+ endId: typeof block.endId === "string" ? migrateMessageRef(block.endId) : "",
2741
2957
  anchorMessageId: typeof block.anchorMessageId === "string" ? block.anchorMessageId : "",
2742
2958
  compressMessageId: typeof block.compressMessageId === "string" ? block.compressMessageId : "",
2743
2959
  compressCallId: typeof block.compressCallId === "string" ? block.compressCallId : void 0,
@@ -2955,7 +3171,8 @@ function saveSessionState(sessionState, logger, sessionName) {
2955
3171
  modelContextLimit: sessionState.modelContextLimit,
2956
3172
  modelProviderID: sessionState.modelProviderID,
2957
3173
  modelID: sessionState.modelID,
2958
- imageArchive: sessionState.imageArchive
3174
+ imageArchive: sessionState.imageArchive,
3175
+ stickyTruncations: sessionState.stickyTruncations
2959
3176
  };
2960
3177
  const key = saveQueueKey(sessionState.sessionId, sessionState.storageDir);
2961
3178
  let queue = saveQueues.get(key);
@@ -2963,8 +3180,8 @@ function saveSessionState(sessionState, logger, sessionName) {
2963
3180
  queue = { pending: [], waiters: [], draining: false };
2964
3181
  saveQueues.set(key, queue);
2965
3182
  }
2966
- const promise = new Promise((resolve2, reject) => {
2967
- queue.waiters.push({ resolve: resolve2, reject });
3183
+ const promise = new Promise((resolve3, reject) => {
3184
+ queue.waiters.push({ resolve: resolve3, reject });
2968
3185
  });
2969
3186
  queue.pending.push({
2970
3187
  sessionId: sessionState.sessionId,
@@ -3088,205 +3305,6 @@ import { existsSync as existsSync3 } from "fs";
3088
3305
  import { join as join3 } from "path";
3089
3306
  import { cwd } from "process";
3090
3307
 
3091
- // lib/message-ids.ts
3092
- var MESSAGE_REF_REGEX = /^m(\d{4,5})$/;
3093
- var BLOCK_REF_REGEX = /^b([1-9]\d*)$/;
3094
- var MESSAGE_ID_TAG_NAME = "dcp-message-id";
3095
- var MESSAGE_REF_WIDTH = 5;
3096
- var MESSAGE_REF_MIN_INDEX = 1;
3097
- var MESSAGE_REF_MAX_INDEX = 99999;
3098
- function formatMessageRef(index) {
3099
- if (!Number.isInteger(index) || index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
3100
- throw new Error(
3101
- `Message ID index out of bounds: ${index}. Supported range is ${MESSAGE_REF_MIN_INDEX}-${MESSAGE_REF_MAX_INDEX}.`
3102
- );
3103
- }
3104
- return `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}`;
3105
- }
3106
- function formatBlockRef(blockId) {
3107
- if (!Number.isInteger(blockId) || blockId < 1) {
3108
- throw new Error(`Invalid block ID: ${blockId}`);
3109
- }
3110
- return `b${blockId}`;
3111
- }
3112
- function parseMessageRef(ref) {
3113
- const normalized = ref.trim().toLowerCase();
3114
- const match = normalized.match(MESSAGE_REF_REGEX);
3115
- if (!match) {
3116
- return null;
3117
- }
3118
- const index = Number.parseInt(match[1], 10);
3119
- if (!Number.isInteger(index)) {
3120
- return null;
3121
- }
3122
- if (index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
3123
- return null;
3124
- }
3125
- return index;
3126
- }
3127
- function parseBlockRef(ref) {
3128
- const normalized = ref.trim().toLowerCase();
3129
- const match = normalized.match(BLOCK_REF_REGEX);
3130
- if (!match) {
3131
- return null;
3132
- }
3133
- const id = Number.parseInt(match[1], 10);
3134
- return Number.isInteger(id) ? id : null;
3135
- }
3136
- function parseBoundaryId(id) {
3137
- const normalized = id.trim().toLowerCase();
3138
- const messageIndex = parseMessageRef(normalized);
3139
- if (messageIndex !== null) {
3140
- return {
3141
- kind: "message",
3142
- ref: formatMessageRef(messageIndex),
3143
- index: messageIndex
3144
- };
3145
- }
3146
- const blockId = parseBlockRef(normalized);
3147
- if (blockId !== null) {
3148
- return {
3149
- kind: "compressed-block",
3150
- ref: formatBlockRef(blockId),
3151
- blockId
3152
- };
3153
- }
3154
- return null;
3155
- }
3156
- function escapeXmlAttribute(value) {
3157
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3158
- }
3159
- function formatMessageIdTag(ref, attributes) {
3160
- const serializedAttributes = Object.entries(attributes || {}).sort(([left], [right]) => left.localeCompare(right)).map(([name, value]) => {
3161
- if (name.trim().length === 0 || typeof value !== "string" || value.length === 0) {
3162
- return "";
3163
- }
3164
- return ` ${name}="${escapeXmlAttribute(value)}"`;
3165
- }).join("");
3166
- return `
3167
- <${MESSAGE_ID_TAG_NAME}${serializedAttributes}>${ref}</${MESSAGE_ID_TAG_NAME}>`;
3168
- }
3169
- function formatTokenSize(tokens) {
3170
- if (tokens < 1e3) return String(tokens);
3171
- if (tokens < 1e4) return `${(tokens / 1e3).toFixed(1)}K`;
3172
- return `${Math.round(tokens / 1e3)}K`;
3173
- }
3174
- function classifyMessageType(parts) {
3175
- let hasTool = false;
3176
- let hasText = false;
3177
- let hasReasoning = false;
3178
- const toolNames = [];
3179
- for (const part of parts) {
3180
- if (part.type === "tool") {
3181
- hasTool = true;
3182
- if (typeof part.tool === "string" && !toolNames.includes(part.tool)) {
3183
- toolNames.push(part.tool);
3184
- }
3185
- } else if (part.type === "text") {
3186
- hasText = true;
3187
- } else if (part.type === "reasoning") {
3188
- hasReasoning = true;
3189
- }
3190
- }
3191
- if (hasTool) {
3192
- return toolNames.length > 0 ? `tool:${toolNames.join(",")}` : "tool";
3193
- }
3194
- if (hasReasoning && !hasText) return "reasoning";
3195
- return "text";
3196
- }
3197
- function assignMessageRefs(state, messages) {
3198
- let assigned = 0;
3199
- let skippedSubAgentPrompt = false;
3200
- for (const message of messages) {
3201
- if (isIgnoredUserMessage(message)) {
3202
- continue;
3203
- }
3204
- if (state.isSubAgent && !skippedSubAgentPrompt && message.info.role === "user") {
3205
- skippedSubAgentPrompt = true;
3206
- continue;
3207
- }
3208
- const rawMessageId = message.info.id;
3209
- if (typeof rawMessageId !== "string" || rawMessageId.length === 0) {
3210
- continue;
3211
- }
3212
- if (rawMessageId.startsWith("msg_dcp_summary_") || rawMessageId.startsWith("msg_dcp_text_")) {
3213
- continue;
3214
- }
3215
- const existingRef = state.messageIds.byRawId.get(rawMessageId);
3216
- if (existingRef) {
3217
- if (state.messageIds.byRef.get(existingRef) !== rawMessageId) {
3218
- state.messageIds.byRef.set(existingRef, rawMessageId);
3219
- }
3220
- continue;
3221
- }
3222
- const ref = allocateNextMessageRef(state);
3223
- state.messageIds.byRawId.set(rawMessageId, ref);
3224
- state.messageIds.byRef.set(ref, rawMessageId);
3225
- assigned++;
3226
- }
3227
- return assigned;
3228
- }
3229
- function repairNonMonotonicMessageRefs(state, messages) {
3230
- const eligible = messages.filter((message) => {
3231
- if (isIgnoredUserMessage(message)) return false;
3232
- const id = message.info.id;
3233
- return typeof id === "string" && id.length > 0 && !id.startsWith("msg_dcp_summary_") && !id.startsWith("msg_dcp_text_");
3234
- });
3235
- let previous = 0;
3236
- let corrupted = false;
3237
- for (const message of eligible) {
3238
- const ref = state.messageIds.byRawId.get(message.info.id);
3239
- const parsed = ref ? parseMessageRef(ref) : null;
3240
- if (parsed === null) continue;
3241
- if (parsed <= previous) {
3242
- corrupted = true;
3243
- break;
3244
- }
3245
- previous = parsed;
3246
- }
3247
- if (!corrupted) return false;
3248
- const oldRawIds = [...state.messageIds.byRawId.keys()];
3249
- state.messageIds.byRawId.clear();
3250
- state.messageIds.byRef.clear();
3251
- state.messageIds.nextRef = 1;
3252
- for (const message of eligible) {
3253
- const rawId = message.info.id;
3254
- const ref = allocateNextMessageRef(state);
3255
- state.messageIds.byRawId.set(rawId, ref);
3256
- state.messageIds.byRef.set(ref, rawId);
3257
- }
3258
- for (const rawId of oldRawIds) {
3259
- if (state.messageIds.byRawId.has(rawId)) continue;
3260
- const ref = allocateNextMessageRef(state);
3261
- state.messageIds.byRawId.set(rawId, ref);
3262
- state.messageIds.byRef.set(ref, rawId);
3263
- }
3264
- for (const block of state.prune.messages.blocksById.values()) {
3265
- const ids = [.../* @__PURE__ */ new Set([...block.effectiveMessageIds, ...block.directMessageIds])].filter((id) => state.messageIds.byRawId.has(id)).sort(
3266
- (left, right) => (parseMessageRef(state.messageIds.byRawId.get(left)) ?? 0) - (parseMessageRef(state.messageIds.byRawId.get(right)) ?? 0)
3267
- );
3268
- if (ids.length === 0) continue;
3269
- block.startId = state.messageIds.byRawId.get(ids[0]);
3270
- block.endId = state.messageIds.byRawId.get(ids[ids.length - 1]);
3271
- }
3272
- state.prune.messages.structureVersion = (state.prune.messages.structureVersion ?? 0) + 1;
3273
- return true;
3274
- }
3275
- function allocateNextMessageRef(state) {
3276
- let candidate = Number.isInteger(state.messageIds.nextRef) ? Math.max(MESSAGE_REF_MIN_INDEX, state.messageIds.nextRef) : MESSAGE_REF_MIN_INDEX;
3277
- while (candidate <= MESSAGE_REF_MAX_INDEX) {
3278
- const ref = formatMessageRef(candidate);
3279
- if (!state.messageIds.byRef.has(ref)) {
3280
- state.messageIds.nextRef = candidate + 1;
3281
- return ref;
3282
- }
3283
- candidate++;
3284
- }
3285
- throw new Error(
3286
- `Message ID alias capacity exceeded. Cannot allocate more than ${formatMessageRef(MESSAGE_REF_MAX_INDEX)} aliases in this session.`
3287
- );
3288
- }
3289
-
3290
3308
  // lib/compress/state.ts
3291
3309
  var DEFAULT_PROMOTION_THRESHOLD = 5;
3292
3310
  var COMPRESSED_BLOCK_HEADER = "[Compressed conversation section]";
@@ -4758,7 +4776,11 @@ function restoreForkCompressionState(state, forkMessages, parent, parentMessages
4758
4776
  directMessageIds,
4759
4777
  directToolIds,
4760
4778
  effectiveMessageIds,
4761
- effectiveToolIds
4779
+ effectiveToolIds,
4780
+ // [Issue #415] The parent record is raw persisted JSON; legacy
4781
+ // 4-digit boundary refs must be normalized like every other ref.
4782
+ startId: typeof block.startId === "string" ? migrateMessageRef(block.startId) : block.startId,
4783
+ endId: typeof block.endId === "string" ? migrateMessageRef(block.endId) : block.endId
4762
4784
  });
4763
4785
  }
4764
4786
  const activeParentIds = new Set(parent.prune.messages.activeBlockIds);
@@ -5124,7 +5146,8 @@ function createSessionState() {
5124
5146
  qualityGateRetryPending: false,
5125
5147
  noContextLimitWarned: false,
5126
5148
  imageArchive: {},
5127
- imagePruneDone: false
5149
+ imagePruneDone: false,
5150
+ stickyTruncations: {}
5128
5151
  };
5129
5152
  }
5130
5153
  function resetSessionState(state) {
@@ -5170,6 +5193,7 @@ function resetSessionState(state) {
5170
5193
  state.noContextLimitWarned = false;
5171
5194
  state.imageArchive = {};
5172
5195
  state.imagePruneDone = false;
5196
+ state.stickyTruncations = {};
5173
5197
  }
5174
5198
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
5175
5199
  if (state.sessionId === sessionId) {
@@ -5198,6 +5222,13 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
5198
5222
  if (parentSessionId) {
5199
5223
  try {
5200
5224
  const parent = await loadSessionState(parentSessionId, logger);
5225
+ if (parent?.stickyTruncations) {
5226
+ for (const [callID, output] of Object.entries(parent.stickyTruncations)) {
5227
+ if (typeof output === "string") {
5228
+ state.stickyTruncations[callID] = output;
5229
+ }
5230
+ }
5231
+ }
5201
5232
  const response = parent ? await client.session.messages({ path: { id: parentSessionId } }) : void 0;
5202
5233
  const parentMessages = Array.isArray(response?.data) ? response.data : Array.isArray(response) ? response : [];
5203
5234
  if (parent && parentMessages.length > 0) {
@@ -5263,6 +5294,14 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
5263
5294
  }
5264
5295
  }
5265
5296
  }
5297
+ const persistedSticky = persisted.stickyTruncations;
5298
+ if (persistedSticky && typeof persistedSticky === "object") {
5299
+ for (const [callID, output] of Object.entries(persistedSticky)) {
5300
+ if (typeof output === "string") {
5301
+ state.stickyTruncations[callID] = output;
5302
+ }
5303
+ }
5304
+ }
5266
5305
  const persistedAny = persisted;
5267
5306
  if (persistedAny._persistedMessageIds) {
5268
5307
  state.messageIds = {
@@ -10254,6 +10293,102 @@ function extractTextContent(msg) {
10254
10293
  return textParts.join(" ").replace(/\s+/g, " ").trim();
10255
10294
  }
10256
10295
 
10296
+ // lib/compress/tofile-target.ts
10297
+ import * as fsp from "fs/promises";
10298
+ import { homedir as homedir3, tmpdir } from "os";
10299
+ import { dirname as dirname2, join as join5, relative, resolve } from "path";
10300
+ async function resolveSafeToFileTarget(targetPath, options) {
10301
+ const osTmp = tmpdir();
10302
+ const allowedDirs = options?.allowedDirs ?? [osTmp, join5(homedir3(), ".cache", "opencode")];
10303
+ const dirList = allowedDirs.join(" or ");
10304
+ if (typeof targetPath !== "string" || targetPath.length === 0) {
10305
+ return {
10306
+ ok: false,
10307
+ error: `Error: toFile path must be under ${dirList}. Got: ${targetPath}`
10308
+ };
10309
+ }
10310
+ const resolvedPath = resolve(targetPath);
10311
+ const lexicallyAllowed = allowedDirs.some((dir) => {
10312
+ const rel = relative(resolve(dir), resolvedPath);
10313
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !isWinAbsolute(rel);
10314
+ });
10315
+ if (!lexicallyAllowed) {
10316
+ return {
10317
+ ok: false,
10318
+ error: `Error: toFile path must be under ${dirList}. Got: ${targetPath}`
10319
+ };
10320
+ }
10321
+ let realAncestor = null;
10322
+ const tail = [];
10323
+ let current = resolvedPath;
10324
+ while (true) {
10325
+ try {
10326
+ realAncestor = await fsp.realpath(current);
10327
+ break;
10328
+ } catch (err) {
10329
+ const code = err.code;
10330
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
10331
+ return {
10332
+ ok: false,
10333
+ error: `Error: cannot validate toFile path: ${err.message}`
10334
+ };
10335
+ }
10336
+ const parent = dirname2(current);
10337
+ if (parent === current) {
10338
+ return {
10339
+ ok: false,
10340
+ error: `Error: cannot validate toFile path: no existing ancestor found`
10341
+ };
10342
+ }
10343
+ tail.unshift(basename(current));
10344
+ current = parent;
10345
+ }
10346
+ }
10347
+ const physicalPath = tail.length > 0 ? join5(realAncestor, ...tail) : realAncestor;
10348
+ const realAllowedDirs = [];
10349
+ for (const dir of allowedDirs) {
10350
+ try {
10351
+ realAllowedDirs.push(await fsp.realpath(resolve(dir)));
10352
+ } catch {
10353
+ }
10354
+ }
10355
+ const physicallyAllowed = realAllowedDirs.some((dir) => containsOrIs(dir, physicalPath));
10356
+ if (!physicallyAllowed) {
10357
+ return {
10358
+ ok: false,
10359
+ error: `Error: toFile path resolves outside the allowed directories via a symlink. Got: ${targetPath}`
10360
+ };
10361
+ }
10362
+ try {
10363
+ const stat = await fsp.lstat(resolvedPath);
10364
+ if (stat.isSymbolicLink()) {
10365
+ return {
10366
+ ok: false,
10367
+ error: `Error: toFile target exists as a symlink and will not be followed. Got: ${targetPath}`
10368
+ };
10369
+ }
10370
+ } catch (err) {
10371
+ const code = err.code;
10372
+ if (code !== "ENOENT") {
10373
+ return {
10374
+ ok: false,
10375
+ error: `Error: cannot validate toFile path: ${err.message}`
10376
+ };
10377
+ }
10378
+ }
10379
+ return { ok: true, filePath: resolvedPath };
10380
+ }
10381
+ function isWinAbsolute(p) {
10382
+ return /^[a-zA-Z]:[\\/]/.test(p);
10383
+ }
10384
+ function basename(p) {
10385
+ return p.split(/[\\/]/).filter(Boolean).pop() ?? p;
10386
+ }
10387
+ function containsOrIs(root, p) {
10388
+ const rel = relative(root, p);
10389
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !isWinAbsolute(rel);
10390
+ }
10391
+
10257
10392
  // lib/compress/decompress.ts
10258
10393
  async function prepareDecompressSession(ctx, toolCtx) {
10259
10394
  await toolCtx.ask({
@@ -10287,7 +10422,13 @@ function resolveTargets(args, state, rawMessages, logger) {
10287
10422
  if (mode.mode === "block") {
10288
10423
  return resolveSingleBlockTarget(messagesState, args.blockId);
10289
10424
  }
10290
- return resolveRangeTarget(state, rawMessages, args.startId, args.endId, logger);
10425
+ return resolveRangeTarget(
10426
+ state,
10427
+ rawMessages,
10428
+ args.startId,
10429
+ args.endId,
10430
+ logger
10431
+ );
10291
10432
  }
10292
10433
  function resolveSingleBlockTarget(messagesState, blockIdArg) {
10293
10434
  const targetBlockId = parseBlockIdArg(blockIdArg);
@@ -10399,11 +10540,21 @@ IMPORTANT:
10399
10540
  - Do NOT call this tool in parallel with compress \u2014 their state mutations may conflict.`;
10400
10541
  function buildSchema2() {
10401
10542
  return {
10402
- blockId: tool3.schema.string().optional().describe('Block reference to decompress (e.g., "b0", "b2"). Mutually exclusive with startId/endId.'),
10403
- startId: tool3.schema.string().optional().describe('Range start: message ref (e.g., "m00150") or block ref (e.g., "b2"). Used with endId.'),
10404
- endId: tool3.schema.string().optional().describe('Range end: message ref (e.g., "m00200") or block ref (e.g., "b5"). Used with startId.'),
10405
- toFile: tool3.schema.string().optional().describe("If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Path must be under /tmp or ~/.cache/opencode/. Example: '/tmp/block52.txt'"),
10406
- full: tool3.schema.boolean().optional().describe("If true, restores ALL content down to original messages (multi-level decompress). Default: false \u2014 restores one tier up (e.g., decompressing a T2 block restores T1 summaries, not raw messages). Use full:true only when you need the exact original content and have context budget for it.")
10543
+ blockId: tool3.schema.string().optional().describe(
10544
+ 'Block reference to decompress (e.g., "b0", "b2"). Mutually exclusive with startId/endId.'
10545
+ ),
10546
+ startId: tool3.schema.string().optional().describe(
10547
+ 'Range start: message ref (e.g., "m00150") or block ref (e.g., "b2"). Used with endId.'
10548
+ ),
10549
+ endId: tool3.schema.string().optional().describe(
10550
+ 'Range end: message ref (e.g., "m00200") or block ref (e.g., "b5"). Used with startId.'
10551
+ ),
10552
+ toFile: tool3.schema.string().optional().describe(
10553
+ "If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Path must be under /tmp or ~/.cache/opencode/. Example: '/tmp/block52.txt'"
10554
+ ),
10555
+ full: tool3.schema.boolean().optional().describe(
10556
+ "If true, restores ALL content down to original messages (multi-level decompress). Default: false \u2014 restores one tier up (e.g., decompressing a T2 block restores T1 summaries, not raw messages). Use full:true only when you need the exact original content and have context budget for it."
10557
+ )
10407
10558
  };
10408
10559
  }
10409
10560
  function extractMessageId(m) {
@@ -10444,7 +10595,12 @@ function createDecompressTool(factoryCtx) {
10444
10595
  const contextUsageBefore = effectiveLimitBefore ? Math.round(
10445
10596
  getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitBefore.limit * 100
10446
10597
  ) : void 0;
10447
- const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
10598
+ const resolved = resolveTargets(
10599
+ args,
10600
+ ctx.state,
10601
+ rawMessages,
10602
+ ctx.logger
10603
+ );
10448
10604
  if (!resolved.ok) {
10449
10605
  return resolved.error;
10450
10606
  }
@@ -10460,19 +10616,9 @@ function createDecompressTool(factoryCtx) {
10460
10616
  }
10461
10617
  if (args.toFile) {
10462
10618
  const targetPath = args.toFile;
10463
- const os = await import("os");
10464
- const path = await import("path");
10465
- const allowedDirs = [
10466
- os.tmpdir() + "/",
10467
- path.join(os.homedir(), ".cache", "opencode") + "/"
10468
- ];
10469
- const resolvedPath = path.resolve(targetPath);
10470
- const isAllowed = allowedDirs.some((dir) => {
10471
- const rel = path.relative(dir, resolvedPath);
10472
- return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
10473
- });
10474
- if (!isAllowed) {
10475
- return `Error: toFile path must be under ${os.tmpdir()} or ~/.cache/opencode/. Got: ${targetPath}`;
10619
+ const safe = await resolveSafeToFileTarget(targetPath);
10620
+ if (!safe.ok) {
10621
+ return safe.error;
10476
10622
  }
10477
10623
  const msgIdSet = /* @__PURE__ */ new Set();
10478
10624
  for (const block of activeBlocks) {
@@ -10482,7 +10628,8 @@ function createDecompressTool(factoryCtx) {
10482
10628
  }
10483
10629
  const blockMessages = rawMessages.filter((m) => msgIdSet.has(extractMessageId(m)));
10484
10630
  const lines2 = blockMessages.map(extractMessageText2);
10485
- const { writeFile: writeFile4 } = await import("fs/promises");
10631
+ const fsp2 = await import("fs/promises");
10632
+ const { constants } = await import("fs");
10486
10633
  let fileContent;
10487
10634
  let sourceNote;
10488
10635
  if (lines2.length > 0) {
@@ -10492,9 +10639,19 @@ function createDecompressTool(factoryCtx) {
10492
10639
  fileContent = targets[0]?.blocks[0]?.summary ?? "(no content available)";
10493
10640
  sourceNote = `no original messages found; wrote block summary (${targets.length} block${targets.length === 1 ? "" : "s"})`;
10494
10641
  }
10495
- await writeFile4(targetPath, fileContent, "utf-8");
10642
+ const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW);
10643
+ try {
10644
+ const handle = await fsp2.open(safe.filePath, flags, 384);
10645
+ try {
10646
+ await handle.write(Buffer.from(fileContent, "utf-8"));
10647
+ } finally {
10648
+ await handle.close();
10649
+ }
10650
+ } catch (err) {
10651
+ return `Error: toFile write failed: ${err.message}`;
10652
+ }
10496
10653
  const displayIds2 = targets.map((t) => `b${t.displayId}`).join(", ");
10497
- return `Block(s) ${displayIds2} content (${sourceNote}, ${fileContent.length} chars) written to ${targetPath}. Block(s) stay compressed \u2014 context unchanged. Use read tool to access specific parts.`;
10654
+ return `Block(s) ${displayIds2} content (${sourceNote}, ${fileContent.length} chars) written to ${safe.filePath}. Block(s) stay compressed \u2014 context unchanged. Use read tool to access specific parts.`;
10498
10655
  }
10499
10656
  const availability = checkDecompressSourceAvailability(
10500
10657
  messagesState,
@@ -10591,7 +10748,9 @@ function isLiveBlock(block) {
10591
10748
  return block.active && !block.deactivatedByUser && !block.deactivatedByUserDeep;
10592
10749
  }
10593
10750
  function rangeKey(startId, endId) {
10594
- return `${startId}::${endId}`;
10751
+ const s = typeof startId === "string" ? migrateMessageRef(startId) : startId;
10752
+ const e = typeof endId === "string" ? migrateMessageRef(endId) : endId;
10753
+ return `${s}::${e}`;
10595
10754
  }
10596
10755
  function rewriteCompressInput(part, liveKeys) {
10597
10756
  if (part.type !== "tool") return null;
@@ -11326,10 +11485,10 @@ function hideFailedCompressCalls(messages) {
11326
11485
 
11327
11486
  // lib/logger.ts
11328
11487
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
11329
- import { join as join5 } from "path";
11488
+ import { join as join6 } from "path";
11330
11489
  import { existsSync as existsSync5 } from "fs";
11331
- import { homedir as homedir3 } from "os";
11332
- var LOG_VERSION = true ? "1.18.4" : "dev";
11490
+ import { homedir as homedir4 } from "os";
11491
+ var LOG_VERSION = true ? "1.18.5-pr.416.226" : "dev";
11333
11492
  var LEVEL_RANK = {
11334
11493
  debug: 10,
11335
11494
  info: 20,
@@ -11349,8 +11508,8 @@ var Logger = class {
11349
11508
  constructor(enabled, level) {
11350
11509
  const resolved = level !== void 0 && LEVEL_RANK[level] !== void 0 ? level : void 0;
11351
11510
  this.level = resolved ?? (enabled ? "debug" : "warn");
11352
- const configHome = process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config");
11353
- this.logDir = join5(configHome, "opencode", "logs", "acp");
11511
+ const configHome = process.env.XDG_CONFIG_HOME || join6(homedir4(), ".config");
11512
+ this.logDir = join6(configHome, "opencode", "logs", "acp");
11354
11513
  }
11355
11514
  /** Back-compat view of the old flag: true only at full debug verbosity. */
11356
11515
  get enabled() {
@@ -11412,11 +11571,11 @@ var Logger = class {
11412
11571
  const dataStr = this.formatData(data);
11413
11572
  const logLine = `${timestamp} ${level.padEnd(5)} ${component}: ${message}${dataStr ? " | " + dataStr : ""} | v=${LOG_VERSION}
11414
11573
  `;
11415
- const dailyLogDir = join5(this.logDir, "daily");
11574
+ const dailyLogDir = join6(this.logDir, "daily");
11416
11575
  if (!existsSync5(dailyLogDir)) {
11417
11576
  await mkdir2(dailyLogDir, { recursive: true });
11418
11577
  }
11419
- const logFile = join5(dailyLogDir, `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
11578
+ const logFile = join6(dailyLogDir, `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
11420
11579
  await writeFile2(logFile, logLine, { flag: "a" });
11421
11580
  } catch (error) {
11422
11581
  }
@@ -11527,7 +11686,7 @@ var Logger = class {
11527
11686
  async saveContext(sessionId, messages) {
11528
11687
  if (this.level !== "debug") return;
11529
11688
  try {
11530
- const contextDir = join5(this.logDir, "context", sessionId);
11689
+ const contextDir = join6(this.logDir, "context", sessionId);
11531
11690
  if (!existsSync5(contextDir)) {
11532
11691
  await mkdir2(contextDir, { recursive: true });
11533
11692
  }
@@ -11535,9 +11694,9 @@ var Logger = class {
11535
11694
  (msg) => msg.parts && msg.parts.length > 0
11536
11695
  );
11537
11696
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
11538
- const contextFile = join5(contextDir, `${timestamp}.json`);
11697
+ const contextFile = join6(contextDir, `${timestamp}.json`);
11539
11698
  await writeFile2(contextFile, JSON.stringify(minimized, null, 2));
11540
- const versionFile = join5(contextDir, "_version");
11699
+ const versionFile = join6(contextDir, "_version");
11541
11700
  if (!existsSync5(versionFile)) {
11542
11701
  await writeFile2(versionFile, LOG_VERSION);
11543
11702
  }
@@ -11548,8 +11707,8 @@ var Logger = class {
11548
11707
 
11549
11708
  // lib/prompts/store.ts
11550
11709
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3, statSync as statSync3 } from "fs";
11551
- import { join as join6, dirname as dirname2 } from "path";
11552
- import { homedir as homedir4 } from "os";
11710
+ import { join as join7, dirname as dirname3 } from "path";
11711
+ import { homedir as homedir5 } from "os";
11553
11712
 
11554
11713
  // lib/prompts/system.ts
11555
11714
  var ACP_STATUS_TOOL_LINE_RANGES = `- \`acp_status\` \u2014 Context status with compressible ranges. No args = overview + ranges. \`scope:"uncompressed"\` for range view; add \`view:"messages"\` for per-message listing with \`tool\`/\`sort\` filters. \`scope:"compressed"\` for block details.`;
@@ -11856,7 +12015,7 @@ function createBundledRuntimePrompts(candidatesEnabled) {
11856
12015
  function findOpencodeDir2(startDir) {
11857
12016
  let current = startDir;
11858
12017
  while (current !== "/") {
11859
- const candidate = join6(current, ".opencode");
12018
+ const candidate = join7(current, ".opencode");
11860
12019
  if (existsSync6(candidate)) {
11861
12020
  try {
11862
12021
  if (statSync3(candidate).isDirectory()) {
@@ -11865,7 +12024,7 @@ function findOpencodeDir2(startDir) {
11865
12024
  } catch {
11866
12025
  }
11867
12026
  }
11868
- const parent = dirname2(current);
12027
+ const parent = dirname3(current);
11869
12028
  if (parent === current) {
11870
12029
  break;
11871
12030
  }
@@ -11874,13 +12033,13 @@ function findOpencodeDir2(startDir) {
11874
12033
  return null;
11875
12034
  }
11876
12035
  function resolvePromptPaths(workingDirectory) {
11877
- const configHome = process.env.XDG_CONFIG_HOME || join6(homedir4(), ".config");
11878
- const globalRoot = join6(configHome, "opencode", "acp-prompts");
11879
- const defaultsDir = join6(globalRoot, "defaults");
11880
- const globalOverridesDir = join6(globalRoot, "overrides");
11881
- const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR ? join6(process.env.OPENCODE_CONFIG_DIR, "acp-prompts", "overrides") : null;
12036
+ const configHome = process.env.XDG_CONFIG_HOME || join7(homedir5(), ".config");
12037
+ const globalRoot = join7(configHome, "opencode", "acp-prompts");
12038
+ const defaultsDir = join7(globalRoot, "defaults");
12039
+ const globalOverridesDir = join7(globalRoot, "overrides");
12040
+ const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR ? join7(process.env.OPENCODE_CONFIG_DIR, "acp-prompts", "overrides") : null;
11882
12041
  const opencodeDir = findOpencodeDir2(workingDirectory);
11883
- const projectOverridesDir = opencodeDir ? join6(opencodeDir, "acp-prompts", "overrides") : null;
12042
+ const projectOverridesDir = opencodeDir ? join7(opencodeDir, "acp-prompts", "overrides") : null;
11884
12043
  return {
11885
12044
  defaultsDir,
11886
12045
  globalOverridesDir,
@@ -12053,16 +12212,16 @@ var PromptStore = class {
12053
12212
  const candidates = [];
12054
12213
  if (this.paths.projectOverridesDir) {
12055
12214
  candidates.push({
12056
- path: join6(this.paths.projectOverridesDir, fileName)
12215
+ path: join7(this.paths.projectOverridesDir, fileName)
12057
12216
  });
12058
12217
  }
12059
12218
  if (this.paths.configDirOverridesDir) {
12060
12219
  candidates.push({
12061
- path: join6(this.paths.configDirOverridesDir, fileName)
12220
+ path: join7(this.paths.configDirOverridesDir, fileName)
12062
12221
  });
12063
12222
  }
12064
12223
  candidates.push({
12065
- path: join6(this.paths.globalOverridesDir, fileName)
12224
+ path: join7(this.paths.globalOverridesDir, fileName)
12066
12225
  });
12067
12226
  return candidates;
12068
12227
  }
@@ -12086,7 +12245,7 @@ var PromptStore = class {
12086
12245
  const managedContent = buildDefaultPromptFileContent(
12087
12246
  bundledEditable || bundledPrompts[definition.runtimeField]
12088
12247
  );
12089
- const filePath = join6(this.paths.defaultsDir, definition.fileName);
12248
+ const filePath = join7(this.paths.defaultsDir, definition.fileName);
12090
12249
  try {
12091
12250
  const existing = readFileIfExists(filePath);
12092
12251
  if (existing === managedContent) {
@@ -12100,7 +12259,7 @@ var PromptStore = class {
12100
12259
  });
12101
12260
  }
12102
12261
  }
12103
- const readmePath = join6(this.paths.defaultsDir, DEFAULTS_README_FILE);
12262
+ const readmePath = join7(this.paths.defaultsDir, DEFAULTS_README_FILE);
12104
12263
  const readmeContent = buildDefaultsReadmeContent();
12105
12264
  try {
12106
12265
  const existing = readFileIfExists(readmePath);
@@ -12184,6 +12343,7 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
12184
12343
  const part = parts[pi];
12185
12344
  if (part?.type !== "tool") continue;
12186
12345
  if (part.state?.status !== "completed") continue;
12346
+ if (part.tool === "compress") continue;
12187
12347
  const content = extractCompletedToolOutput(part);
12188
12348
  if (content === void 0) continue;
12189
12349
  if (content === "[Old tool result content cleared]") continue;
@@ -12224,6 +12384,74 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
12224
12384
  }
12225
12385
  }
12226
12386
 
12387
+ // lib/messages/sticky-truncation.ts
12388
+ function collectToolParts(messages) {
12389
+ const parts = [];
12390
+ for (const message of messages) {
12391
+ const messageParts = Array.isArray(message.parts) ? message.parts : [];
12392
+ for (const part of messageParts) {
12393
+ if (part?.type === "tool") {
12394
+ parts.push(part);
12395
+ }
12396
+ }
12397
+ }
12398
+ return parts;
12399
+ }
12400
+ function applyStickyTruncations(state, messages, logger) {
12401
+ const frozen = state.stickyTruncations;
12402
+ if (Object.keys(frozen).length === 0) return;
12403
+ let replayed = 0;
12404
+ const seen = /* @__PURE__ */ new Set();
12405
+ for (const part of collectToolParts(messages)) {
12406
+ const callID = typeof part.callID === "string" ? part.callID : void 0;
12407
+ if (!callID) continue;
12408
+ seen.add(callID);
12409
+ const recorded = frozen[callID];
12410
+ if (recorded === void 0) continue;
12411
+ if (!part.state) continue;
12412
+ if (part.state.output === recorded) continue;
12413
+ part.state.output = recorded;
12414
+ replayed++;
12415
+ }
12416
+ for (const callID of Object.keys(frozen)) {
12417
+ if (!seen.has(callID)) delete frozen[callID];
12418
+ }
12419
+ if (replayed > 0) {
12420
+ logger.debug("Sticky truncation: replayed frozen tool outputs", {
12421
+ replayed,
12422
+ frozen: Object.keys(frozen).length
12423
+ });
12424
+ }
12425
+ }
12426
+ function snapshotToolOutputs(messages) {
12427
+ const snapshot = /* @__PURE__ */ new Map();
12428
+ for (const part of collectToolParts(messages)) {
12429
+ const callID = typeof part.callID === "string" ? part.callID : void 0;
12430
+ if (!callID) continue;
12431
+ const content = extractCompletedToolOutput(part);
12432
+ if (content === void 0) continue;
12433
+ snapshot.set(callID, content);
12434
+ }
12435
+ return snapshot;
12436
+ }
12437
+ function recordStickyTruncations(state, messages, before, logger) {
12438
+ let recorded = 0;
12439
+ for (const part of collectToolParts(messages)) {
12440
+ const callID = typeof part.callID === "string" ? part.callID : void 0;
12441
+ if (!callID) continue;
12442
+ if (typeof part.state?.output !== "string") continue;
12443
+ const prior = before.get(callID);
12444
+ if (prior === void 0 || prior === part.state.output) continue;
12445
+ if (state.stickyTruncations[callID] === part.state.output) continue;
12446
+ state.stickyTruncations[callID] = part.state.output;
12447
+ recorded++;
12448
+ }
12449
+ if (recorded > 0) {
12450
+ logger.info("Sticky truncation: froze truncated tool outputs", { recorded });
12451
+ }
12452
+ return recorded;
12453
+ }
12454
+
12227
12455
  // lib/messages/enforce-budget.ts
12228
12456
  var DEFAULT_COMPLETION_RESERVE_TOKENS = 32768;
12229
12457
  var TRUNCATION_MARKER2 = "[truncated for context space";
@@ -12574,7 +12802,7 @@ ${report}`;
12574
12802
  // lib/commands/export.ts
12575
12803
  import * as fs2 from "fs/promises";
12576
12804
  import { existsSync as existsSync7 } from "fs";
12577
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join7, resolve } from "path";
12805
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join8, resolve as resolve2 } from "path";
12578
12806
  var ALL_TIERS = [1, 2, 3];
12579
12807
  var TIER_NAMES = {
12580
12808
  1: "Tier 1 \u2014 Capture",
@@ -12687,7 +12915,7 @@ function parseTiers(raw) {
12687
12915
  }
12688
12916
  function resolveDefaultOutputPath(sessionId, cwd2) {
12689
12917
  const short = (sessionId || "session").slice(0, 8);
12690
- return join7(cwd2, ".opencode", `acp-export-${short}.md`);
12918
+ return join8(cwd2, ".opencode", `acp-export-${short}.md`);
12691
12919
  }
12692
12920
  function formatTokens2(n) {
12693
12921
  if (!Number.isFinite(n) || n <= 0) return "0";
@@ -12815,9 +13043,9 @@ async function handleExportCommand(ctx, args) {
12815
13043
  return;
12816
13044
  }
12817
13045
  const cwd2 = ctx.workingDirectory || process.cwd();
12818
- const targetPath = options.outputPath !== "" ? isAbsolute2(options.outputPath) ? options.outputPath : resolve(cwd2, options.outputPath) : resolveDefaultOutputPath(sessionId, cwd2);
13046
+ const targetPath = options.outputPath !== "" ? isAbsolute2(options.outputPath) ? options.outputPath : resolve2(cwd2, options.outputPath) : resolveDefaultOutputPath(sessionId, cwd2);
12819
13047
  try {
12820
- const dir = dirname3(targetPath);
13048
+ const dir = dirname4(targetPath);
12821
13049
  if (!existsSync7(dir)) {
12822
13050
  await fs2.mkdir(dir, { recursive: true });
12823
13051
  }
@@ -13594,13 +13822,32 @@ ${text}`);
13594
13822
  prePruneTokens,
13595
13823
  candidateMessages
13596
13824
  );
13597
- truncateLargeToolOutputs(
13598
- state,
13825
+ const emergencyTruncationEnabled = applyCompressOverrides(
13599
13826
  config,
13600
- logger,
13601
- output.messages.filter((message) => !isSyntheticMessage(message))
13602
- );
13603
- enforceContextBudget(state, config, logger, output.messages);
13827
+ dropReasoningModel?.providerID ?? state.modelProviderID,
13828
+ dropReasoningModel?.modelID ?? state.modelID
13829
+ ).compress.emergencyTruncation;
13830
+ if (emergencyTruncationEnabled === true) {
13831
+ applyStickyTruncations(state, output.messages, logger);
13832
+ const toolOutputsBefore = snapshotToolOutputs(output.messages);
13833
+ truncateLargeToolOutputs(
13834
+ state,
13835
+ config,
13836
+ logger,
13837
+ output.messages.filter((message) => !isSyntheticMessage(message))
13838
+ );
13839
+ enforceContextBudget(state, config, logger, output.messages);
13840
+ const frozenCount = recordStickyTruncations(
13841
+ state,
13842
+ output.messages,
13843
+ toolOutputsBefore,
13844
+ logger
13845
+ );
13846
+ if (frozenCount > 0) {
13847
+ saveSessionState(state, logger).catch(() => {
13848
+ });
13849
+ }
13850
+ }
13604
13851
  injectMessageIds(state, config, output.messages, compressionPriorities);
13605
13852
  hideFailedCompressCalls(output.messages);
13606
13853
  stripStaleMetadata(output.messages);
@@ -13828,7 +14075,7 @@ function configureClientAuth(client) {
13828
14075
 
13829
14076
  // lib/update.ts
13830
14077
  import { readFile as readFile2, rm } from "fs/promises";
13831
- import { basename, dirname as dirname4, join as join8 } from "path";
14078
+ import { basename as basename2, dirname as dirname5, join as join9 } from "path";
13832
14079
  import { fileURLToPath } from "url";
13833
14080
  var PACKAGE_NAME = "opencode-acp";
13834
14081
  function startAutoUpdate(ctx, enabled, logger) {
@@ -13875,7 +14122,7 @@ async function checkAutoUpdate(signal, logger) {
13875
14122
  logger?.info("Auto-update skipped: package dir not found");
13876
14123
  return { updated: false };
13877
14124
  }
13878
- const pkg = await readPackageJson(join8(packageDir, "package.json"));
14125
+ const pkg = await readPackageJson(join9(packageDir, "package.json"));
13879
14126
  if (!pkg?.name || !pkg.version) {
13880
14127
  logger?.info("Auto-update skipped: package.json unreadable", { packageDir });
13881
14128
  return { updated: false };
@@ -13914,21 +14161,21 @@ async function checkAutoUpdate(signal, logger) {
13914
14161
  return { updated: true, name: pkg.name, current: pkg.version, latest };
13915
14162
  }
13916
14163
  async function findPackageDir(name) {
13917
- let dir = dirname4(fileURLToPath(import.meta.url));
14164
+ let dir = dirname5(fileURLToPath(import.meta.url));
13918
14165
  for (; ; ) {
13919
- const pkg = await readPackageJson(join8(dir, "package.json"));
14166
+ const pkg = await readPackageJson(join9(dir, "package.json"));
13920
14167
  if (pkg?.name === name) return dir;
13921
- const parent = dirname4(dir);
14168
+ const parent = dirname5(dir);
13922
14169
  if (parent === dir) return void 0;
13923
14170
  dir = parent;
13924
14171
  }
13925
14172
  }
13926
14173
  async function updateTarget(packageDir, name) {
13927
- const packageParent = dirname4(packageDir);
13928
- const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname4(packageParent) : packageParent;
13929
- if (basename(nodeModulesDir) !== "node_modules") return void 0;
13930
- const wrapperDir = dirname4(nodeModulesDir);
13931
- const wrapperPkg = await readPackageJson(join8(wrapperDir, "package.json"));
14174
+ const packageParent = dirname5(packageDir);
14175
+ const nodeModulesDir = basename2(packageParent).startsWith("@") ? dirname5(packageParent) : packageParent;
14176
+ if (basename2(nodeModulesDir) !== "node_modules") return void 0;
14177
+ const wrapperDir = dirname5(nodeModulesDir);
14178
+ const wrapperPkg = await readPackageJson(join9(wrapperDir, "package.json"));
13932
14179
  const spec = wrapperSpec(wrapperDir, name) ?? wrapperPkg?.dependencies?.[name];
13933
14180
  if (!spec || !isAutoUpdatableSpec(spec)) return void 0;
13934
14181
  return { removeDir: wrapperDir, spec };
@@ -13936,13 +14183,13 @@ async function updateTarget(packageDir, name) {
13936
14183
  function wrapperSpec(wrapperDir, name) {
13937
14184
  if (name.startsWith("@")) {
13938
14185
  const [scope, pkg] = name.split("/");
13939
- if (!scope || !pkg || basename(dirname4(wrapperDir)) !== scope) return void 0;
14186
+ if (!scope || !pkg || basename2(dirname5(wrapperDir)) !== scope) return void 0;
13940
14187
  const prefix2 = `${pkg}@`;
13941
- const base2 = basename(wrapperDir);
14188
+ const base2 = basename2(wrapperDir);
13942
14189
  return base2.startsWith(prefix2) ? base2.slice(prefix2.length) : void 0;
13943
14190
  }
13944
14191
  const prefix = `${name}@`;
13945
- const base = basename(wrapperDir);
14192
+ const base = basename2(wrapperDir);
13946
14193
  return base.startsWith(prefix) ? base.slice(prefix.length) : void 0;
13947
14194
  }
13948
14195
  function isAutoUpdatableSpec(spec) {
@@ -14039,7 +14286,7 @@ var server = (async (ctx) => {
14039
14286
  }
14040
14287
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
14041
14288
  logger.info("ACP plugin initialized", {
14042
- version: true ? "1.18.4" : "dev",
14289
+ version: true ? "1.18.5-pr.416.226" : "dev",
14043
14290
  workspace: ctx.directory,
14044
14291
  logLevel: logger.level,
14045
14292
  debug: config.debug,