opencode-acp 1.18.1 → 1.18.2

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
@@ -903,6 +903,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
903
903
  "compress.protectUserMessages",
904
904
  "compress.maxSummaryLengthHard",
905
905
  "compress.minCompressRange",
906
+ "compress.smartPlanRequired",
906
907
  "compress.minNudgeGrowthRatio",
907
908
  "compress.minNudgeGrowthFloor",
908
909
  "compress.emergencyThresholdPercent",
@@ -1151,6 +1152,13 @@ function validateConfigTypes(config) {
1151
1152
  actual: `${compress.minCompressRange}`
1152
1153
  });
1153
1154
  }
1155
+ if (compress.smartPlanRequired !== void 0 && typeof compress.smartPlanRequired !== "boolean") {
1156
+ errors.push({
1157
+ key: "compress.smartPlanRequired",
1158
+ expected: "boolean",
1159
+ actual: typeof compress.smartPlanRequired
1160
+ });
1161
+ }
1154
1162
  if (compress.minNudgeGrowthRatio !== void 0 && typeof compress.minNudgeGrowthRatio !== "number") {
1155
1163
  errors.push({
1156
1164
  key: "compress.minNudgeGrowthRatio",
@@ -1396,6 +1404,7 @@ function validateConfigTypes(config) {
1396
1404
  protectedTools: "stringArray",
1397
1405
  maxSummaryLengthHard: "positiveNumber",
1398
1406
  minCompressRange: "nonNegativeNumber",
1407
+ smartPlanRequired: "boolean",
1399
1408
  maxVisibleSegments: "positiveNumber",
1400
1409
  keepEmbedMaxChars: "nonNegativeNumber",
1401
1410
  preserveRecentMessages: "nonNegativeNumber",
@@ -1773,6 +1782,7 @@ var defaultConfig = {
1773
1782
  protectUserMessages: false,
1774
1783
  maxSummaryLengthHard: 2e4,
1775
1784
  minCompressRange: 5e3,
1785
+ smartPlanRequired: false,
1776
1786
  minNudgeGrowthRatio: 0.45,
1777
1787
  minNudgeGrowthFloor: 5e3,
1778
1788
  nudgeGrowthTokens: 5e4,
@@ -1947,6 +1957,7 @@ function mergeCompress(base, override) {
1947
1957
  protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
1948
1958
  maxSummaryLengthHard: override.maxSummaryLengthHard ?? base.maxSummaryLengthHard,
1949
1959
  minCompressRange: override.minCompressRange ?? base.minCompressRange,
1960
+ smartPlanRequired: override.smartPlanRequired ?? base.smartPlanRequired,
1950
1961
  minNudgeGrowthRatio: override.minNudgeGrowthRatio ?? base.minNudgeGrowthRatio,
1951
1962
  minNudgeGrowthFloor: override.minNudgeGrowthFloor ?? base.minNudgeGrowthFloor,
1952
1963
  emergencyThresholdPercent: override.emergencyThresholdPercent ?? base.emergencyThresholdPercent,
@@ -2756,11 +2767,6 @@ function resetOnCompaction(state) {
2756
2767
  compressBaselineSet: false,
2757
2768
  lastProcessedCompressMessageId: void 0
2758
2769
  };
2759
- state.messageIds = {
2760
- byRawId: /* @__PURE__ */ new Map(),
2761
- byRef: /* @__PURE__ */ new Map(),
2762
- nextRef: 1
2763
- };
2764
2770
  }
2765
2771
  function resolveEffectiveContextLimit(state, config) {
2766
2772
  if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
@@ -3124,6 +3130,52 @@ function assignMessageRefs(state, messages) {
3124
3130
  }
3125
3131
  return assigned;
3126
3132
  }
3133
+ function repairNonMonotonicMessageRefs(state, messages) {
3134
+ const eligible = messages.filter((message) => {
3135
+ if (isIgnoredUserMessage(message)) return false;
3136
+ const id = message.info.id;
3137
+ return typeof id === "string" && id.length > 0 && !id.startsWith("msg_dcp_summary_") && !id.startsWith("msg_dcp_text_");
3138
+ });
3139
+ let previous = 0;
3140
+ let corrupted = false;
3141
+ for (const message of eligible) {
3142
+ const ref = state.messageIds.byRawId.get(message.info.id);
3143
+ const parsed = ref ? parseMessageRef(ref) : null;
3144
+ if (parsed === null) continue;
3145
+ if (parsed <= previous) {
3146
+ corrupted = true;
3147
+ break;
3148
+ }
3149
+ previous = parsed;
3150
+ }
3151
+ if (!corrupted) return false;
3152
+ const oldRawIds = [...state.messageIds.byRawId.keys()];
3153
+ state.messageIds.byRawId.clear();
3154
+ state.messageIds.byRef.clear();
3155
+ state.messageIds.nextRef = 1;
3156
+ for (const message of eligible) {
3157
+ const rawId = message.info.id;
3158
+ const ref = allocateNextMessageRef(state);
3159
+ state.messageIds.byRawId.set(rawId, ref);
3160
+ state.messageIds.byRef.set(ref, rawId);
3161
+ }
3162
+ for (const rawId of oldRawIds) {
3163
+ if (state.messageIds.byRawId.has(rawId)) continue;
3164
+ const ref = allocateNextMessageRef(state);
3165
+ state.messageIds.byRawId.set(rawId, ref);
3166
+ state.messageIds.byRef.set(ref, rawId);
3167
+ }
3168
+ for (const block of state.prune.messages.blocksById.values()) {
3169
+ const ids = [.../* @__PURE__ */ new Set([...block.effectiveMessageIds, ...block.directMessageIds])].filter((id) => state.messageIds.byRawId.has(id)).sort(
3170
+ (left, right) => (parseMessageRef(state.messageIds.byRawId.get(left)) ?? 0) - (parseMessageRef(state.messageIds.byRawId.get(right)) ?? 0)
3171
+ );
3172
+ if (ids.length === 0) continue;
3173
+ block.startId = state.messageIds.byRawId.get(ids[0]);
3174
+ block.endId = state.messageIds.byRawId.get(ids[ids.length - 1]);
3175
+ }
3176
+ state.prune.messages.structureVersion = (state.prune.messages.structureVersion ?? 0) + 1;
3177
+ return true;
3178
+ }
3127
3179
  function allocateNextMessageRef(state) {
3128
3180
  let candidate = Number.isInteger(state.messageIds.nextRef) ? Math.max(MESSAGE_REF_MIN_INDEX, state.messageIds.nextRef) : MESSAGE_REF_MIN_INDEX;
3129
3181
  while (candidate <= MESSAGE_REF_MAX_INDEX) {
@@ -3949,7 +4001,7 @@ function createSearchContextTool(factoryCtx) {
3949
4001
 
3950
4002
  // lib/protected-patterns.ts
3951
4003
  function normalizePath(input) {
3952
- return input.replaceAll("\\\\", "/");
4004
+ return input.replaceAll("\\", "/");
3953
4005
  }
3954
4006
  function escapeRegExpChar(ch) {
3955
4007
  return /[\\.^$+{}()|\[\]]/.test(ch) ? `\\${ch}` : ch;
@@ -4661,21 +4713,21 @@ function rebuildRangeInvocation(state, input, searchContext, invocation, protect
4661
4713
  const runId = allocateRunId(state);
4662
4714
  let created = 0;
4663
4715
  for (const plan of plans) {
4664
- const filteredSelection = filterProtectedToolMessages(
4716
+ const filteredSelection2 = filterProtectedToolMessages(
4665
4717
  plan.selection,
4666
4718
  searchContext,
4667
4719
  protectedTools,
4668
4720
  protectedFilePatterns
4669
4721
  );
4670
- if (filteredSelection.messageIds.length === 0) {
4722
+ if (filteredSelection2.messageIds.length === 0) {
4671
4723
  continue;
4672
4724
  }
4673
4725
  const boundaryConsumed = extractBoundaryConsumedBlocks(
4674
- filteredSelection.startReference,
4675
- filteredSelection.endReference
4726
+ filteredSelection2.startReference,
4727
+ filteredSelection2.endReference
4676
4728
  );
4677
4729
  const consumedBlockIds = dedupeBlockIds([
4678
- ...filteredSelection.requiredBlockIds,
4730
+ ...filteredSelection2.requiredBlockIds,
4679
4731
  ...boundaryConsumed
4680
4732
  ]);
4681
4733
  const blockId = allocateBlockId(state);
@@ -4694,7 +4746,7 @@ function rebuildRangeInvocation(state, input, searchContext, invocation, protect
4694
4746
  compressCallId: invocation.callId,
4695
4747
  summaryTokens
4696
4748
  },
4697
- filteredSelection,
4749
+ filteredSelection2,
4698
4750
  plan.anchorMessageId,
4699
4751
  blockId,
4700
4752
  storedSummary,
@@ -6822,6 +6874,33 @@ function buildToolIdList(state, messages) {
6822
6874
  var stripHallucinationsFromString = (text) => {
6823
6875
  return text.replace(DCP_PAIRED_TAG_REGEX, "").replace(DCP_UNPAIRED_TAG_REGEX, "");
6824
6876
  };
6877
+ var LEAKED_TRAILING_REF_REGEX = /(?:^|\n)[ \t]*(?:m(\d{4,5})|b([1-9]\d*))\b/g;
6878
+ var stripLeakedTrailingRefs = (text, bounds) => {
6879
+ if (typeof text !== "string" || text.length === 0) {
6880
+ return text;
6881
+ }
6882
+ const rawMessageBound = bounds.nextMessageRef;
6883
+ const rawBlockBound = bounds.nextBlockRef;
6884
+ const messageBound = typeof rawMessageBound === "number" && Number.isInteger(rawMessageBound) ? rawMessageBound : null;
6885
+ const blockBound = typeof rawBlockBound === "number" && Number.isInteger(rawBlockBound) ? rawBlockBound : null;
6886
+ if (messageBound === null && blockBound === null) {
6887
+ return text;
6888
+ }
6889
+ const pattern = new RegExp(LEAKED_TRAILING_REF_REGEX.source, "g");
6890
+ let match;
6891
+ while ((match = pattern.exec(text)) !== null) {
6892
+ if (match[1] !== void 0) {
6893
+ if (messageBound !== null && Number(match[1]) >= messageBound) {
6894
+ return text.slice(0, match.index).replace(/[ \t\r\n]*$/, "");
6895
+ }
6896
+ } else if (match[2] !== void 0) {
6897
+ if (blockBound !== null && Number(match[2]) >= blockBound) {
6898
+ return text.slice(0, match.index).replace(/[ \t\r\n]*$/, "");
6899
+ }
6900
+ }
6901
+ }
6902
+ return text;
6903
+ };
6825
6904
  var stripHallucinations = (messages) => {
6826
6905
  for (const message of messages) {
6827
6906
  for (const part of message.parts) {
@@ -6988,10 +7067,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6988
7067
  return parseLimitValue(globalLimit);
6989
7068
  }
6990
7069
  function isContextOverLimits(config, state, providerId, modelId, messages) {
6991
- const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(
6992
- state,
6993
- new Set(messages.map((m) => m.info.id))
6994
- ) : 0;
7070
+ const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(state, new Set(messages.map((m) => m.info.id))) : 0;
6995
7071
  const resolvedMaxContextLimit = resolveContextTokenLimit(
6996
7072
  config,
6997
7073
  state,
@@ -7375,7 +7451,15 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7375
7451
  lastUserRefIdx.length = 0;
7376
7452
  lastUserRefIdx.push(msgInfo.length);
7377
7453
  }
7378
- msgInfo.push({ ref, refNum: rn, tokens, effectiveTokens: 0, meaningful: hasMeaningfulPart, isTool, isUser: msg.info.role === "user" });
7454
+ msgInfo.push({
7455
+ ref,
7456
+ refNum: rn,
7457
+ tokens,
7458
+ effectiveTokens: 0,
7459
+ meaningful: hasMeaningfulPart,
7460
+ isTool,
7461
+ isUser: msg.info.role === "user"
7462
+ });
7379
7463
  }
7380
7464
  const lastUserIdx = lastUserRefIdx.length > 0 ? lastUserRefIdx[0] : -1;
7381
7465
  for (let i = 0; i < msgInfo.length; i++) {
@@ -7394,7 +7478,7 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7394
7478
  prevRefNum = info.refNum;
7395
7479
  continue;
7396
7480
  }
7397
- const hasGap = info.refNum > prevRefNum + 1;
7481
+ const hasGap = info.refNum !== prevRefNum + 1;
7398
7482
  if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
7399
7483
  groups.push(cur);
7400
7484
  cur = null;
@@ -7474,9 +7558,7 @@ function filterRecommendedRanges(compressible, _protectedRanges, options) {
7474
7558
  const effective = r.effectiveTokens ?? r.tokens;
7475
7559
  return effective > 0 && effective >= floor;
7476
7560
  });
7477
- const result = kept.map(
7478
- (r, i) => i === kept.length - 1 ? { ...r, dangerous: true } : r
7479
- );
7561
+ const result = kept.map((r, i) => i === kept.length - 1 ? { ...r, dangerous: true } : r);
7480
7562
  log?.("filterRecommendedRanges: effective-token floor applied", {
7481
7563
  inputRanges: compressible.length,
7482
7564
  outputRanges: result.length,
@@ -7610,12 +7692,160 @@ function computeProtectedRefs(messages, state, compress) {
7610
7692
  }
7611
7693
  function excludeProtectedRanges(ranges, protectedRefs) {
7612
7694
  if (protectedRefs.size === 0) return ranges;
7613
- return ranges.filter(
7614
- (r) => !protectedRefs.has(r.startRef) && !protectedRefs.has(r.endRef)
7695
+ return ranges.filter((r) => !protectedRefs.has(r.startRef) && !protectedRefs.has(r.endRef));
7696
+ }
7697
+
7698
+ // lib/compress/smart-plan.ts
7699
+ var SMART_PLAN_TTL_MS = 10 * 60 * 1e3;
7700
+ var armedPlans = /* @__PURE__ */ new Map();
7701
+ var visibleMessageIdsBySession = /* @__PURE__ */ new Map();
7702
+ function refNumber(ref) {
7703
+ const value = Number.parseInt(ref.slice(1), 10);
7704
+ return Number.isFinite(value) ? value : -1;
7705
+ }
7706
+ function mergeAdjacentSafeRanges(ranges) {
7707
+ const merged = [];
7708
+ for (const range of ranges.filter((candidate) => !candidate.dangerous)) {
7709
+ const previous = merged.at(-1);
7710
+ if (previous && refNumber(range.startRef) === refNumber(previous.endRef) + 1) {
7711
+ const previousEffective = previous.effectiveTokens ?? previous.tokens;
7712
+ previous.endRef = range.endRef;
7713
+ previous.count += range.count;
7714
+ previous.tokens += range.tokens;
7715
+ previous.effectiveTokens = previousEffective + (range.effectiveTokens ?? range.tokens);
7716
+ continue;
7717
+ }
7718
+ merged.push({ ...range });
7719
+ }
7720
+ return merged;
7721
+ }
7722
+ function recordVisibleMessages(sessionID, messages) {
7723
+ visibleMessageIdsBySession.set(sessionID, new Set(messages.map((message) => message.info.id)));
7724
+ }
7725
+ function getVisibleMessageIds(sessionID) {
7726
+ return visibleMessageIdsBySession.get(sessionID);
7727
+ }
7728
+ function clearSmartPlan(sessionID) {
7729
+ armedPlans.delete(sessionID);
7730
+ }
7731
+ function clearSmartPlanSession(sessionID) {
7732
+ armedPlans.delete(sessionID);
7733
+ visibleMessageIdsBySession.delete(sessionID);
7734
+ }
7735
+ function consumeSmartPlan(sessionID) {
7736
+ armedPlans.delete(sessionID);
7737
+ }
7738
+ function filteredSelection(startId, endId, searchContext, ctx) {
7739
+ const [resolved] = resolveRanges(
7740
+ {
7741
+ topic: "ACP smart plan",
7742
+ content: [{ startId, endId, summary: "Pending model-authored summary." }]
7743
+ },
7744
+ searchContext,
7745
+ ctx.state,
7746
+ ctx.logger
7615
7747
  );
7748
+ if (!resolved) return void 0;
7749
+ let selection = filterProtectedToolMessages(
7750
+ resolved.selection,
7751
+ searchContext,
7752
+ ctx.config.compress.protectedTools,
7753
+ ctx.config.protectedFilePatterns
7754
+ );
7755
+ selection = filterLastUserMessage(selection, searchContext, ctx.state, ctx.config.compress);
7756
+ selection = filterProtectedRecentMessages(
7757
+ selection,
7758
+ searchContext,
7759
+ ctx.state,
7760
+ ctx.config.compress
7761
+ );
7762
+ return selection;
7763
+ }
7764
+ function armBestSmartPlan(sessionID, ranges, searchContext, ctx, visibleMessageIds, now = Date.now(), targetChars = ctx.config.compress.minCompressRange) {
7765
+ const candidates = [];
7766
+ for (const range of mergeAdjacentSafeRanges(ranges)) {
7767
+ const selection = filteredSelection(range.startRef, range.endRef, searchContext, ctx);
7768
+ if (!selection || selection.messageIds.length === 0) continue;
7769
+ if (visibleMessageIds && !selection.messageIds.every((id) => visibleMessageIds.has(id)))
7770
+ continue;
7771
+ let exactChars = 0;
7772
+ for (const messageId of selection.messageIds) {
7773
+ const message = searchContext.rawMessagesById.get(messageId);
7774
+ if (message) exactChars += countMessageCharacters(message);
7775
+ }
7776
+ if (ctx.config.compress.minCompressRange > 0 && exactChars < ctx.config.compress.minCompressRange) {
7777
+ continue;
7778
+ }
7779
+ candidates.push({
7780
+ sessionID,
7781
+ createdAt: now,
7782
+ structureVersion: ctx.state.prune.messages.structureVersion ?? 0,
7783
+ startId: range.startRef,
7784
+ endId: range.endRef,
7785
+ exactChars,
7786
+ messageIds: [...selection.messageIds],
7787
+ rawStart: selection.startReference.rawIndex
7788
+ });
7789
+ }
7790
+ const sufficient = candidates.filter((candidate) => candidate.exactChars >= targetChars);
7791
+ const pool = sufficient.length > 0 ? sufficient : candidates;
7792
+ pool.sort(
7793
+ (a, b) => sufficient.length > 0 ? a.rawStart - b.rawStart || b.exactChars - a.exactChars : b.exactChars - a.exactChars || a.rawStart - b.rawStart
7794
+ );
7795
+ const best = pool[0];
7796
+ if (!best) {
7797
+ clearSmartPlan(sessionID);
7798
+ return void 0;
7799
+ }
7800
+ const { rawStart: _rawStart, ...plan } = best;
7801
+ armedPlans.set(sessionID, plan);
7802
+ return plan;
7803
+ }
7804
+ function validateSmartPlan(sessionID, startId, endId, messageIds, exactChars, structureVersion, now = Date.now()) {
7805
+ const plan = armedPlans.get(sessionID);
7806
+ const preflight = 'Use the exact SMART PLAN range from the latest ACP nudge, or call acp_status({scope:"uncompressed"}) to refresh it.';
7807
+ if (!plan) throw new Error(`No smart compression plan is armed. ${preflight}`);
7808
+ if (now - plan.createdAt > SMART_PLAN_TTL_MS) {
7809
+ clearSmartPlan(sessionID);
7810
+ throw new Error(`Smart compression plan expired. ${preflight}`);
7811
+ }
7812
+ if (plan.structureVersion !== structureVersion) {
7813
+ clearSmartPlan(sessionID);
7814
+ throw new Error(
7815
+ `Smart compression plan is stale because compression state changed. ${preflight}`
7816
+ );
7817
+ }
7818
+ if (plan.startId !== startId || plan.endId !== endId) {
7819
+ throw new Error(
7820
+ `Range does not match the armed smart plan (${plan.startId}-${plan.endId}). Do not sort, merge, or batch ranges.`
7821
+ );
7822
+ }
7823
+ const idsMatch = plan.messageIds.length === messageIds.length && plan.messageIds.every((id, i) => id === messageIds[i]);
7824
+ const charsDiff = Math.abs(plan.exactChars - exactChars);
7825
+ const charsMatch = charsDiff === 0 || charsDiff <= 300 || charsDiff / Math.max(plan.exactChars, 1) < 0.02;
7826
+ if (!idsMatch || !charsMatch) {
7827
+ clearSmartPlan(sessionID);
7828
+ throw new Error(
7829
+ `Smart compression plan no longer resolves to the armed messages. ${preflight}`
7830
+ );
7831
+ }
7832
+ const visibleMessageIds = getVisibleMessageIds(sessionID);
7833
+ if (visibleMessageIds && !plan.messageIds.every((id) => visibleMessageIds.has(id))) {
7834
+ clearSmartPlan(sessionID);
7835
+ throw new Error(
7836
+ `Smart compression plan moved outside the model's visible context. ${preflight}`
7837
+ );
7838
+ }
7839
+ return plan;
7616
7840
  }
7617
7841
 
7618
7842
  // lib/compress/range.ts
7843
+ function requiresSmartPlan(smartPlanRequired, content) {
7844
+ const isBlockCompression = content.every(
7845
+ (entry) => entry.startId.startsWith("b") && entry.endId.startsWith("b")
7846
+ );
7847
+ return smartPlanRequired && !isBlockCompression;
7848
+ }
7619
7849
  function buildSchema(maxSummaryLengthHard) {
7620
7850
  return {
7621
7851
  topic: tool2.schema.string().optional().describe(
@@ -7678,13 +7908,32 @@ function createCompressRangeTool(factoryCtx) {
7678
7908
  ...ctx0,
7679
7909
  config: applyCompressOverrides(ctx0.config, providerId, modelId)
7680
7910
  };
7681
- const { plans: filteredPlans } = prepareExecutableRangePlans(
7911
+ const needsSmartPlan = requiresSmartPlan(
7912
+ ctx.config.compress.smartPlanRequired === true,
7913
+ input.content
7914
+ );
7915
+ if (needsSmartPlan && input.content.length !== 1) {
7916
+ throw new Error(
7917
+ 'Raw-message compression accepts exactly one canonical SMART PLAN range per call. Use the latest ACP nudge, or call acp_status({scope:"uncompressed"}) to refresh it.'
7918
+ );
7919
+ }
7920
+ const { plans: filteredPlans, totalChars } = prepareExecutableRangePlans(
7682
7921
  input,
7683
7922
  searchContext,
7684
7923
  ctx.state,
7685
7924
  ctx.config,
7686
7925
  ctx.logger
7687
7926
  );
7927
+ if (needsSmartPlan) {
7928
+ validateSmartPlan(
7929
+ toolCtx.sessionID,
7930
+ filteredPlans[0].entry.startId,
7931
+ filteredPlans[0].entry.endId,
7932
+ filteredPlans[0].selection.messageIds,
7933
+ totalChars,
7934
+ ctx.state.prune.messages.structureVersion ?? 0
7935
+ );
7936
+ }
7688
7937
  const notifications = [];
7689
7938
  let preparedPlans = [];
7690
7939
  let totalCompressedMessages = 0;
@@ -7862,6 +8111,7 @@ function createCompressRangeTool(factoryCtx) {
7862
8111
  });
7863
8112
  }
7864
8113
  await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
8114
+ if (needsSmartPlan) consumeSmartPlan(toolCtx.sessionID);
7865
8115
  } catch (error) {
7866
8116
  restoreCompressionState(ctx.state, snapshot);
7867
8117
  ctx.state.qualityGateRetryPending = qualityGateRetryPendingBefore;
@@ -8958,9 +9208,40 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8958
9208
  const nothingToCompress = candidatesEnabled && candidateMessages !== void 0 ? noExecutableCandidates : allProtected || allInProtectedZone || allBelowMin;
8959
9209
  const emergencyNoTargets = emergencyOverride && nothingToCompress;
8960
9210
  const noticeCadenceMet = state.nudges.lastNudgeShownTokens === void 0 || growthSinceBaseline !== void 0 && growthSinceBaseline >= growthFloor;
8961
- const shouldInjectNudge = nudgeAllowed && !nothingToCompress;
8962
- const shouldInjectNotice = emergencyNoTargets && noticeCadenceMet;
9211
+ let shouldInjectNudge = nudgeAllowed && !nothingToCompress;
9212
+ let shouldInjectNotice = emergencyNoTargets && noticeCadenceMet;
8963
9213
  let shouldInject = shouldInjectNudge || shouldInjectNotice;
9214
+ let armedSmartPlan;
9215
+ if (shouldInjectNudge && config.compress.smartPlanRequired === true && state.sessionId) {
9216
+ const planningRanges = candidatesEnabled ? candidatePlan.candidates.map((candidate) => ({
9217
+ startRef: candidate.startRef,
9218
+ endRef: candidate.endRef,
9219
+ count: candidate.messageCount,
9220
+ tokens: candidate.estimatedTokens,
9221
+ effectiveTokens: candidate.estimatedTokens,
9222
+ toolPct: candidate.toolPct,
9223
+ textPct: candidate.textPct
9224
+ })) : recommendedRanges;
9225
+ const maxLimit = resolveContextTokenLimit(config, state, providerId, modelId, "max");
9226
+ const reclaimTargetChars = Math.max(
9227
+ config.compress.minCompressRange,
9228
+ maxLimit === void 0 || currentTokens === void 0 ? 0 : Math.max(0, currentTokens - maxLimit + 8192) * 4
9229
+ );
9230
+ armedSmartPlan = armBestSmartPlan(
9231
+ state.sessionId,
9232
+ planningRanges,
9233
+ buildSearchContext(state, messages),
9234
+ { state, config, logger },
9235
+ new Set(messages.map((message) => message.info.id)),
9236
+ Date.now(),
9237
+ reclaimTargetChars
9238
+ );
9239
+ if (!armedSmartPlan) {
9240
+ shouldInjectNudge = false;
9241
+ shouldInjectNotice = emergencyOverride && noticeCadenceMet;
9242
+ shouldInject = shouldInjectNotice;
9243
+ }
9244
+ }
8964
9245
  if (shouldInjectNudge) {
8965
9246
  const effectivePrompts = candidatesEnabled ? {
8966
9247
  ...prompts,
@@ -9168,13 +9449,22 @@ Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.too
9168
9449
  ${HOW_TO_COMPRESS_RULES}
9169
9450
 
9170
9451
  ${candidateText}`;
9171
- breakdown += candidatesEnabled ? "\n\u{1F4A1} You may batch selected independent candidates in one call (pass multiple content entries: `content: [{...}, {...}]`); omit any candidate whose content is still needed." : "\n\u{1F4A1} Compress all ranges in one call (pass multiple content entries: `content: [{...}, {...}]`).";
9452
+ if (armedSmartPlan) {
9453
+ breakdown += `
9454
+
9455
+ SMART PLAN ARMED: ${armedSmartPlan.startId}-${armedSmartPlan.endId} | ${armedSmartPlan.exactChars} effective chars`;
9456
+ breakdown += `
9457
+ Next: compress exactly this one range now. acp_status is optional; use it only to inspect or refresh a stale plan.`;
9458
+ } else {
9459
+ breakdown += candidatesEnabled ? "\n\u{1F4A1} You may batch selected independent candidates in one call (pass multiple content entries: `content: [{...}, {...}]`); omit any candidate whose content is still needed." : "\n\u{1F4A1} Compress all ranges in one call (pass multiple content entries: `content: [{...}, {...}]`).";
9460
+ }
9172
9461
  }
9173
- breakdown += `
9462
+ breakdown += armedSmartPlan ? `
9463
+ Use \`acp_status({scope:"uncompressed"})\` only to inspect or refresh a stale plan, or \`acp_status\` for compressed block details.` : `
9174
9464
  Use \`acp_status({scope:"uncompressed"})\` to re-fetch compressible ranges after compressing, or \`acp_status\` for compressed block details.`;
9175
9465
  appendToLastTextPart(suffixMessage, breakdown);
9176
9466
  }
9177
- if (effectiveTipsVariant === "maxLimit" && !emergencyNoTargets) {
9467
+ if (effectiveTipsVariant === "maxLimit" && shouldInjectNudge) {
9178
9468
  tipsText = candidatesEnabled ? "\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Select at least one listed candidate whose content is no longer needed and call the `compress` tool in your next reply. Do not merely recommend compression. Candidates are advisory about selection: preserve current intent and active work, and never compress content still needed for the current task.\n\n" + HOW_TO_COMPRESS_RULES + '\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. If several selected candidates are stale, older work may be preferable; never override semantic judgment or compress content still needed for the current task.' : "\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n" + HOW_TO_COMPRESS_RULES + '\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
9179
9469
  } else if (shouldInjectNotice) {
9180
9470
  const emergencyPct = currentTokens !== void 0 && modelContextLimit !== void 0 && modelContextLimit > 0 ? Math.round(currentTokens / modelContextLimit * 100) : void 0;
@@ -10430,13 +10720,85 @@ function createAcpStatusTool(factoryCtx) {
10430
10720
  rawMessages = [];
10431
10721
  }
10432
10722
  hideConsumedCompressCalls(ctx.state, rawMessages);
10433
- return buildStatusReport({ state: ctx.state, config: ctx.config }, rawMessages, {
10434
- scope,
10435
- view,
10436
- tool: toolFilter,
10437
- sort,
10438
- limit
10439
- });
10723
+ assignMessageRefs(ctx.state, rawMessages);
10724
+ if (repairNonMonotonicMessageRefs(ctx.state, rawMessages)) {
10725
+ ctx.logger.warn("Repaired non-monotonic message aliases after native compaction", {
10726
+ session: toolCtx.sessionID,
10727
+ messages: rawMessages.length
10728
+ });
10729
+ await saveSessionState(ctx.state, ctx.logger);
10730
+ }
10731
+ const { providerId, modelId } = getModelInfo(rawMessages);
10732
+ const effectiveCtx = {
10733
+ ...ctx,
10734
+ config: applyCompressOverrides(ctx.config, providerId, modelId)
10735
+ };
10736
+ let smartPlanSuffix = "";
10737
+ if (effectiveCtx.config.compress?.smartPlanRequired === true && scope === "uncompressed" && view !== "messages") {
10738
+ const lastModelView = getVisibleMessageIds(toolCtx.sessionID);
10739
+ const visibleRaw = rawMessages.filter((message) => {
10740
+ const entry = effectiveCtx.state.prune.messages.byMessageId.get(message.info.id);
10741
+ return (!entry || entry.activeBlockIds.length === 0) && (!lastModelView || lastModelView.has(message.info.id));
10742
+ });
10743
+ const protectedRefs = computeProtectedRefs(
10744
+ visibleRaw,
10745
+ effectiveCtx.state,
10746
+ effectiveCtx.config.compress
10747
+ );
10748
+ const contextRanges = buildCompressibleRanges(
10749
+ visibleRaw,
10750
+ effectiveCtx.state,
10751
+ effectiveCtx.config.compress.protectedTools,
10752
+ effectiveCtx.config.protectedFilePatterns,
10753
+ protectedRefs
10754
+ );
10755
+ const ranges = filterRecommendedRanges(
10756
+ contextRanges.compressible,
10757
+ contextRanges.protected,
10758
+ {
10759
+ logger: effectiveCtx.logger,
10760
+ minEffectiveTokens: resolveEffectiveFloor(effectiveCtx.config)
10761
+ }
10762
+ );
10763
+ const composition = estimateContextComposition(
10764
+ visibleRaw,
10765
+ effectiveCtx.state,
10766
+ effectiveCtx.config.compress.protectedTools,
10767
+ effectiveCtx.config.protectedFilePatterns
10768
+ );
10769
+ const targetLimit = resolveContextTokenLimit(
10770
+ effectiveCtx.config,
10771
+ effectiveCtx.state,
10772
+ providerId,
10773
+ modelId,
10774
+ "max"
10775
+ );
10776
+ const targetChars = Math.max(
10777
+ effectiveCtx.config.compress.minCompressRange,
10778
+ targetLimit === void 0 ? 0 : Math.max(0, composition.total - targetLimit + 8192) * 4
10779
+ );
10780
+ const plan = lastModelView ? armBestSmartPlan(
10781
+ toolCtx.sessionID,
10782
+ ranges,
10783
+ buildSearchContext(effectiveCtx.state, rawMessages),
10784
+ effectiveCtx,
10785
+ lastModelView,
10786
+ Date.now(),
10787
+ targetChars
10788
+ ) : void 0;
10789
+ smartPlanSuffix = plan ? `
10790
+
10791
+ SMART PLAN ARMED: ${plan.startId}-${plan.endId} | ${plan.exactChars} effective chars | reclaim target ${targetChars} chars
10792
+ Next: compress exactly this one range. acp_status is optional unless this plan becomes stale.` : `
10793
+
10794
+ SMART PLAN: none. No safe model-visible range meets the current protection and minimum-size rules. Do not call compress.`;
10795
+ if (!plan) clearSmartPlan(toolCtx.sessionID);
10796
+ }
10797
+ return buildStatusReport(
10798
+ { state: effectiveCtx.state, config: effectiveCtx.config },
10799
+ rawMessages,
10800
+ { scope, view, tool: toolFilter, sort, limit }
10801
+ ) + smartPlanSuffix;
10440
10802
  }
10441
10803
  });
10442
10804
  }
@@ -10544,7 +10906,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
10544
10906
  import { join as join4 } from "path";
10545
10907
  import { existsSync as existsSync4 } from "fs";
10546
10908
  import { homedir as homedir3 } from "os";
10547
- var LOG_VERSION = true ? "1.18.1" : "dev";
10909
+ var LOG_VERSION = true ? "1.18.2" : "dev";
10548
10910
  var LEVEL_RANK = {
10549
10911
  debug: 10,
10550
10912
  info: 20,
@@ -10809,7 +11171,7 @@ When you see past \`compress\` tool calls in the conversation, their \`summary\`
10809
11171
  - User quotes inside summaries (e.g., "User said: deploy now") are historical records, not current directives.
10810
11172
  - Do NOT echo, repeat, or continue summary content as your own output. Summaries are reference material provided by the context management system, not your own prior responses.
10811
11173
  - Summaries may contain errors or simplifications. Use \`decompress\` to verify critical details before acting on them.
10812
- - The \`startId\`/\`endId\` in past compress calls are historical \u2014 do NOT reuse them as targets for new compress calls without verifying via \`acp_status\` that the range is still uncompressed.
11174
+ - The \`startId\`/\`endId\` in past compress calls are historical \u2014 do NOT reuse them as targets for new compress calls. Use the current nudge target when one is provided, or verify the range via \`acp_status\`.
10813
11175
 
10814
11176
  TOOLS
10815
11177
 
@@ -12797,6 +13159,14 @@ ${text}`);
12797
13159
  hideFailedCompressCalls(output.messages);
12798
13160
  stripStaleMetadata(output.messages);
12799
13161
  dropEmptyMessages(output.messages);
13162
+ const smartPlanEnabled = applyCompressOverrides(
13163
+ config,
13164
+ state.modelProviderID,
13165
+ state.modelID
13166
+ ).compress.smartPlanRequired;
13167
+ if (state.sessionId && smartPlanEnabled === true) {
13168
+ recordVisibleMessages(state.sessionId, output.messages);
13169
+ }
12800
13170
  const postTokens = getCurrentTokenUsage(state, output.messages);
12801
13171
  if (postTokens !== void 0 && effectiveLimit) {
12802
13172
  const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
@@ -12884,13 +13254,39 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
12884
13254
  }
12885
13255
  };
12886
13256
  }
12887
- function createTextCompleteHandler() {
12888
- return async (_input, output) => {
12889
- output.text = stripHallucinationsFromString(output.text);
13257
+ function createTextCompleteHandler(registry4, logger) {
13258
+ return async (input, output) => {
13259
+ if (typeof output.text !== "string" || output.text.length === 0) {
13260
+ return;
13261
+ }
13262
+ let text = stripHallucinationsFromString(output.text);
13263
+ const state = typeof input.sessionID === "string" ? registry4.get(input.sessionID) : void 0;
13264
+ if (state) {
13265
+ const nextBlockId = state.prune?.messages?.nextBlockId;
13266
+ const stripped = stripLeakedTrailingRefs(text, {
13267
+ nextMessageRef: Number.isInteger(state.messageIds.nextRef) ? state.messageIds.nextRef : null,
13268
+ nextBlockRef: Number.isInteger(nextBlockId) ? nextBlockId : null
13269
+ });
13270
+ if (stripped !== text) {
13271
+ logger.warn("Stripped leaked trailing ACP ref from assistant output", {
13272
+ sessionID: input.sessionID,
13273
+ messageID: input.messageID,
13274
+ removedChars: text.length - stripped.length,
13275
+ removedTailPreview: text.slice(stripped.length, stripped.length + 200)
13276
+ });
13277
+ text = stripped;
13278
+ }
13279
+ }
13280
+ output.text = text;
12890
13281
  };
12891
13282
  }
12892
13283
  function createEventHandler(registry4, logger) {
12893
13284
  return async (input) => {
13285
+ if (input.event?.type === "session.idle" || input.event?.type === "session.deleted") {
13286
+ const sessionID = input.event?.properties?.sessionID ?? input.event?.properties?.info?.id;
13287
+ if (typeof sessionID === "string") clearSmartPlanSession(sessionID);
13288
+ return;
13289
+ }
12894
13290
  const eventTime = typeof input.event?.time === "number" && Number.isFinite(input.event.time) ? input.event.time : typeof input.event?.properties?.time === "number" && Number.isFinite(input.event.properties.time) ? input.event.properties.time : void 0;
12895
13291
  if (input.event.type !== "message.part.updated") {
12896
13292
  return;
@@ -13228,7 +13624,7 @@ var server = (async (ctx) => {
13228
13624
  }
13229
13625
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
13230
13626
  logger.info("ACP plugin initialized", {
13231
- version: true ? "1.18.1" : "dev",
13627
+ version: true ? "1.18.2" : "dev",
13232
13628
  workspace: ctx.directory,
13233
13629
  logLevel: logger.level,
13234
13630
  debug: config.debug,
@@ -13293,7 +13689,7 @@ var server = (async (ctx) => {
13293
13689
  hostPermissions
13294
13690
  )
13295
13691
  ),
13296
- "experimental.text.complete": guard(createTextCompleteHandler()),
13692
+ "experimental.text.complete": guard(createTextCompleteHandler(registry4, logger)),
13297
13693
  "command.execute.before": guard(
13298
13694
  createCommandExecuteHandler(
13299
13695
  ctx.client,