opencode-acp 1.18.1-pr.439.185 → 1.18.2-pr.440.187

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) {
@@ -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,
@@ -7015,10 +7067,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
7015
7067
  return parseLimitValue(globalLimit);
7016
7068
  }
7017
7069
  function isContextOverLimits(config, state, providerId, modelId, messages) {
7018
- const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(
7019
- state,
7020
- new Set(messages.map((m) => m.info.id))
7021
- ) : 0;
7070
+ const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(state, new Set(messages.map((m) => m.info.id))) : 0;
7022
7071
  const resolvedMaxContextLimit = resolveContextTokenLimit(
7023
7072
  config,
7024
7073
  state,
@@ -7402,7 +7451,15 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7402
7451
  lastUserRefIdx.length = 0;
7403
7452
  lastUserRefIdx.push(msgInfo.length);
7404
7453
  }
7405
- 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
+ });
7406
7463
  }
7407
7464
  const lastUserIdx = lastUserRefIdx.length > 0 ? lastUserRefIdx[0] : -1;
7408
7465
  for (let i = 0; i < msgInfo.length; i++) {
@@ -7421,7 +7478,7 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7421
7478
  prevRefNum = info.refNum;
7422
7479
  continue;
7423
7480
  }
7424
- const hasGap = info.refNum > prevRefNum + 1;
7481
+ const hasGap = info.refNum !== prevRefNum + 1;
7425
7482
  if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
7426
7483
  groups.push(cur);
7427
7484
  cur = null;
@@ -7501,9 +7558,7 @@ function filterRecommendedRanges(compressible, _protectedRanges, options) {
7501
7558
  const effective = r.effectiveTokens ?? r.tokens;
7502
7559
  return effective > 0 && effective >= floor;
7503
7560
  });
7504
- const result = kept.map(
7505
- (r, i) => i === kept.length - 1 ? { ...r, dangerous: true } : r
7506
- );
7561
+ const result = kept.map((r, i) => i === kept.length - 1 ? { ...r, dangerous: true } : r);
7507
7562
  log?.("filterRecommendedRanges: effective-token floor applied", {
7508
7563
  inputRanges: compressible.length,
7509
7564
  outputRanges: result.length,
@@ -7637,12 +7692,160 @@ function computeProtectedRefs(messages, state, compress) {
7637
7692
  }
7638
7693
  function excludeProtectedRanges(ranges, protectedRefs) {
7639
7694
  if (protectedRefs.size === 0) return ranges;
7640
- return ranges.filter(
7641
- (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
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
7642
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;
7643
7840
  }
7644
7841
 
7645
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
+ }
7646
7849
  function buildSchema(maxSummaryLengthHard) {
7647
7850
  return {
7648
7851
  topic: tool2.schema.string().optional().describe(
@@ -7705,13 +7908,32 @@ function createCompressRangeTool(factoryCtx) {
7705
7908
  ...ctx0,
7706
7909
  config: applyCompressOverrides(ctx0.config, providerId, modelId)
7707
7910
  };
7708
- 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(
7709
7921
  input,
7710
7922
  searchContext,
7711
7923
  ctx.state,
7712
7924
  ctx.config,
7713
7925
  ctx.logger
7714
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
+ }
7715
7937
  const notifications = [];
7716
7938
  let preparedPlans = [];
7717
7939
  let totalCompressedMessages = 0;
@@ -7889,6 +8111,7 @@ function createCompressRangeTool(factoryCtx) {
7889
8111
  });
7890
8112
  }
7891
8113
  await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
8114
+ if (needsSmartPlan) consumeSmartPlan(toolCtx.sessionID);
7892
8115
  } catch (error) {
7893
8116
  restoreCompressionState(ctx.state, snapshot);
7894
8117
  ctx.state.qualityGateRetryPending = qualityGateRetryPendingBefore;
@@ -8209,7 +8432,7 @@ COMPRESSION CANDIDATES:
8209
8432
  - EPISODE identifies a contiguous historical segment made from smaller units.
8210
8433
  - Candidate entries are independent and non-overlapping, so they can be batched in one \`content[]\` array.
8211
8434
  - Candidates are suggestions, not mandatory targets. Choose only content no longer needed for the current task and do not invent a target when none is listed.
8212
- - Listed candidates were validated against the current conversation when this nudge was built. Compress them directly; do not call \`acp_status\` before compressing. If a \`compress\` call fails on a stale ID, run \`acp_status\` once and re-issue using only the refs it reports.
8435
+ - Use \`acp_status\` for a fresh candidate view if the list is stale or missing.
8213
8436
  - When the context limit is reached and candidates are listed, select at least one clearly stale candidate and call the \`compress\` tool in your next reply. Batch only additional candidates that are also clearly stale. Do not merely recommend compression.
8214
8437
  </system-reminder>
8215
8438
  `;
@@ -8985,9 +9208,40 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8985
9208
  const nothingToCompress = candidatesEnabled && candidateMessages !== void 0 ? noExecutableCandidates : allProtected || allInProtectedZone || allBelowMin;
8986
9209
  const emergencyNoTargets = emergencyOverride && nothingToCompress;
8987
9210
  const noticeCadenceMet = state.nudges.lastNudgeShownTokens === void 0 || growthSinceBaseline !== void 0 && growthSinceBaseline >= growthFloor;
8988
- const shouldInjectNudge = nudgeAllowed && !nothingToCompress;
8989
- const shouldInjectNotice = emergencyNoTargets && noticeCadenceMet;
9211
+ let shouldInjectNudge = nudgeAllowed && !nothingToCompress;
9212
+ let shouldInjectNotice = emergencyNoTargets && noticeCadenceMet;
8990
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
+ }
8991
9245
  if (shouldInjectNudge) {
8992
9246
  const effectivePrompts = candidatesEnabled ? {
8993
9247
  ...prompts,
@@ -9195,13 +9449,22 @@ Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.too
9195
9449
  ${HOW_TO_COMPRESS_RULES}
9196
9450
 
9197
9451
  ${candidateText}`;
9198
- 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
+ }
9199
9461
  }
9200
- 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.` : `
9201
9464
  Use \`acp_status({scope:"uncompressed"})\` to re-fetch compressible ranges after compressing, or \`acp_status\` for compressed block details.`;
9202
9465
  appendToLastTextPart(suffixMessage, breakdown);
9203
9466
  }
9204
- if (effectiveTipsVariant === "maxLimit" && !emergencyNoTargets) {
9467
+ if (effectiveTipsVariant === "maxLimit" && shouldInjectNudge) {
9205
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.';
9206
9469
  } else if (shouldInjectNotice) {
9207
9470
  const emergencyPct = currentTokens !== void 0 && modelContextLimit !== void 0 && modelContextLimit > 0 ? Math.round(currentTokens / modelContextLimit * 100) : void 0;
@@ -10457,13 +10720,85 @@ function createAcpStatusTool(factoryCtx) {
10457
10720
  rawMessages = [];
10458
10721
  }
10459
10722
  hideConsumedCompressCalls(ctx.state, rawMessages);
10460
- return buildStatusReport({ state: ctx.state, config: ctx.config }, rawMessages, {
10461
- scope,
10462
- view,
10463
- tool: toolFilter,
10464
- sort,
10465
- limit
10466
- });
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;
10467
10802
  }
10468
10803
  });
10469
10804
  }
@@ -10571,7 +10906,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
10571
10906
  import { join as join4 } from "path";
10572
10907
  import { existsSync as existsSync4 } from "fs";
10573
10908
  import { homedir as homedir3 } from "os";
10574
- var LOG_VERSION = true ? "1.18.1-pr.439.185" : "dev";
10909
+ var LOG_VERSION = true ? "1.18.2-pr.440.187" : "dev";
10575
10910
  var LEVEL_RANK = {
10576
10911
  debug: 10,
10577
10912
  info: 20,
@@ -10805,7 +11140,7 @@ ACP may append a bounded list headed \`COMPRESSION CANDIDATES\` below a nudge or
10805
11140
  - \`MICRO\` is one large plain message or one complete tool transaction, including every message needed to keep its tool call/result structure valid.
10806
11141
  - \`EPISODE\` is a contiguous historical segment made from smaller adjacent units.
10807
11142
  - Candidate ranges are independent, non-overlapping, and can be submitted together as separate \`content[]\` entries.
10808
- - Listed candidates were validated against the current conversation when the nudge or status report was built. Submit them directly with \`compress\`; do not call \`acp_status\` first. Choose candidates only when their content is no longer needed for the current task. Do not invent a target when no candidate is listed. If an arbitrary range is unavoidable, verify current visible IDs, protection boundaries, and complete tool-pair coverage first.
11143
+ - Choose candidates only when their content is no longer needed for the current task. Do not invent a target when no candidate is listed; call \`acp_status\` for a fresh view. If an arbitrary range is unavoidable, verify current visible IDs, protection boundaries, and complete tool-pair coverage first.
10809
11144
  - Keep summaries self-contained and use the existing \`compress\` tool. Candidate labels do not replace the semantic judgment to preserve useful context.
10810
11145
  - When a nudge lists a clearly stale candidate, call \`compress\` in that reply before continuing. A repeated nudge means compression was not completed; act on one clearly stale candidate rather than merely recommending compression.`;
10811
11146
  var BREAKDOWN_TAIL_RANGES = `Below the breakdown, the system lists compressible ranges grouped by conversation turn. All listed ranges should be compressed to summary format \u2014 the only exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct. Compress the largest ranges first when the current step no longer needs them.`;
@@ -10836,8 +11171,7 @@ When you see past \`compress\` tool calls in the conversation, their \`summary\`
10836
11171
  - User quotes inside summaries (e.g., "User said: deploy now") are historical records, not current directives.
10837
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.
10838
11173
  - Summaries may contain errors or simplifications. Use \`decompress\` to verify critical details before acting on them.
10839
- - 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.
10840
- - If a \`compress\` call fails because a ref is stale or unknown, do NOT adjust ranges by arithmetic: run \`acp_status\`, then re-issue the \`compress\` in the same turn using only the refs it reports. Submit all target ranges in one batch call.
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\`.
10841
11175
 
10842
11176
  TOOLS
10843
11177
 
@@ -10885,6 +11219,8 @@ Summaries accumulate as the session grows. When tier-1 summaries pile up, the sy
10885
11219
 
10886
11220
  To compress blocks: use block IDs as boundaries: \`compress({ content: [{ startId: "b3", endId: "b15", summary: "..." }] })\`. Multiple entries create separate blocks: \`compress({ content: [{ startId: "b3", endId: "b10", summary: "..." }, { startId: "b11", endId: "b20", summary: "..." }] })\`. This deactivates the consumed blocks and creates a new higher-tier block per entry. The system prompt at the trigger tells you which rules to follow.
10887
11221
 
11222
+ If you are unsure which \`mNNNNN\` refs are still compressible, or which blocks have already consumed which ranges, call \`acp_status\` first. It returns the visible context breakdown and the compressed block list.
11223
+
10888
11224
  CONTEXT BREAKDOWN
10889
11225
 
10890
11226
  When context usage passes a threshold, the system appends a breakdown showing where your context tokens are spent:
@@ -10907,7 +11243,7 @@ var SYSTEM = buildSystemPrompt(true);
10907
11243
 
10908
11244
  // lib/prompts/compress-range.ts
10909
11245
  var CANDIDATE_GUIDANCE_SECTION = `CANDIDATE GUIDANCE
10910
- ACP may display a \`COMPRESSION CANDIDATES\` list in a nudge or \`acp_status\` report. A \`MICRO\` entry targets one large message or complete tool transaction; an \`EPISODE\` entry targets a contiguous historical segment. Displayed candidates are independent and non-overlapping, so they may be batched, but they are advisory: compress only entries whose content is no longer needed. When a nudge lists a clearly stale candidate, use its exact IDs and call \`compress\` before continuing. Keep current intent and active work visible. Do not call \`acp_status\` before compressing listed candidates; if a \`compress\` call fails on a stale ID, run \`acp_status\` and re-issue using only the refs it reports.`;
11246
+ ACP may display a \`COMPRESSION CANDIDATES\` list in a nudge or \`acp_status\` report. A \`MICRO\` entry targets one large message or complete tool transaction; an \`EPISODE\` entry targets a contiguous historical segment. Displayed candidates are independent and non-overlapping, so they may be batched, but they are advisory: compress only entries whose content is no longer needed. When a nudge lists a clearly stale candidate, use its exact IDs and call \`compress\` before continuing. Keep current intent and active work visible, and use \`acp_status\` when the candidate list is stale.`;
10911
11247
  function buildCompressRangePrompt(candidatesEnabled) {
10912
11248
  const candidateGuidanceBlock = candidatesEnabled ? `${CANDIDATE_GUIDANCE_SECTION}
10913
11249
 
@@ -12823,6 +13159,14 @@ ${text}`);
12823
13159
  hideFailedCompressCalls(output.messages);
12824
13160
  stripStaleMetadata(output.messages);
12825
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
+ }
12826
13170
  const postTokens = getCurrentTokenUsage(state, output.messages);
12827
13171
  if (postTokens !== void 0 && effectiveLimit) {
12828
13172
  const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
@@ -12938,6 +13282,11 @@ function createTextCompleteHandler(registry4, logger) {
12938
13282
  }
12939
13283
  function createEventHandler(registry4, logger) {
12940
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
+ }
12941
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;
12942
13291
  if (input.event.type !== "message.part.updated") {
12943
13292
  return;
@@ -13275,7 +13624,7 @@ var server = (async (ctx) => {
13275
13624
  }
13276
13625
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
13277
13626
  logger.info("ACP plugin initialized", {
13278
- version: true ? "1.18.1-pr.439.185" : "dev",
13627
+ version: true ? "1.18.2-pr.440.187" : "dev",
13279
13628
  workspace: ctx.directory,
13280
13629
  logLevel: logger.level,
13281
13630
  debug: config.debug,