opencode-acp 1.16.0 → 1.17.0-pr.383.130

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
@@ -890,6 +890,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
890
890
  "compress.modelMaxLimits",
891
891
  "compress.modelMinLimits",
892
892
  "compress.providers",
893
+ "compress.contextLimitFallback",
893
894
  "compress.nudgeFrequency",
894
895
  "compress.minNudgeContextPercent",
895
896
  "compress.nudgeGrowthTokens",
@@ -913,6 +914,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
913
914
  "compress.reasoning",
914
915
  "compress.reasoning.drop",
915
916
  "compress.reasoning.threshold",
917
+ "compress.completionReserveTokens",
916
918
  "gc",
917
919
  "gc.algorithm",
918
920
  "gc.promotionThreshold",
@@ -964,7 +966,11 @@ function validateConfigTypes(config) {
964
966
  errors.push({ key: "storagePath", expected: "string", actual: typeof config.storagePath });
965
967
  }
966
968
  if (config.allowSubAgents !== void 0 && typeof config.allowSubAgents !== "boolean") {
967
- errors.push({ key: "allowSubAgents", expected: "boolean", actual: typeof config.allowSubAgents });
969
+ errors.push({
970
+ key: "allowSubAgents",
971
+ expected: "boolean",
972
+ actual: typeof config.allowSubAgents
973
+ });
968
974
  }
969
975
  if (config.pruneNotification !== void 0) {
970
976
  const validValues = ["off", "minimal", "detailed"];
@@ -1293,6 +1299,20 @@ function validateConfigTypes(config) {
1293
1299
  }
1294
1300
  }
1295
1301
  }
1302
+ if (compress.completionReserveTokens !== void 0 && typeof compress.completionReserveTokens !== "number") {
1303
+ errors.push({
1304
+ key: "compress.completionReserveTokens",
1305
+ expected: "number",
1306
+ actual: typeof compress.completionReserveTokens
1307
+ });
1308
+ }
1309
+ if (typeof compress.completionReserveTokens === "number" && compress.completionReserveTokens < 0) {
1310
+ errors.push({
1311
+ key: "compress.completionReserveTokens",
1312
+ expected: "non-negative number (>= 0)",
1313
+ actual: `${compress.completionReserveTokens}`
1314
+ });
1315
+ }
1296
1316
  if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
1297
1317
  errors.push({
1298
1318
  key: "compress.iterationNudgeThreshold",
@@ -1411,12 +1431,20 @@ function validateConfigTypes(config) {
1411
1431
  break;
1412
1432
  case "nudgeForce":
1413
1433
  if (value !== "strong" && value !== "soft") {
1414
- errors.push({ key, expected: "'strong' | 'soft'", actual: JSON.stringify(value) });
1434
+ errors.push({
1435
+ key,
1436
+ expected: "'strong' | 'soft'",
1437
+ actual: JSON.stringify(value)
1438
+ });
1415
1439
  }
1416
1440
  break;
1417
1441
  case "stringArray":
1418
1442
  if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
1419
- errors.push({ key, expected: "string[]", actual: JSON.stringify(value) });
1443
+ errors.push({
1444
+ key,
1445
+ expected: "string[]",
1446
+ actual: JSON.stringify(value)
1447
+ });
1420
1448
  }
1421
1449
  break;
1422
1450
  case "reasoningConfig":
@@ -1451,7 +1479,11 @@ function validateConfigTypes(config) {
1451
1479
  return;
1452
1480
  }
1453
1481
  if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides)) {
1454
- errors.push({ key: prefix, expected: "CompressModelOverrides", actual: typeof overrides });
1482
+ errors.push({
1483
+ key: prefix,
1484
+ expected: "CompressModelOverrides",
1485
+ actual: typeof overrides
1486
+ });
1455
1487
  return;
1456
1488
  }
1457
1489
  const model = overrides;
@@ -1484,7 +1516,11 @@ function validateConfigTypes(config) {
1484
1516
  for (const [providerId, providerValue] of Object.entries(providers)) {
1485
1517
  const prefix = `compress.providers.${providerId}`;
1486
1518
  if (typeof providerValue !== "object" || providerValue === null || Array.isArray(providerValue)) {
1487
- errors.push({ key: prefix, expected: "ProviderOverrides", actual: typeof providerValue });
1519
+ errors.push({
1520
+ key: prefix,
1521
+ expected: "ProviderOverrides",
1522
+ actual: typeof providerValue
1523
+ });
1488
1524
  continue;
1489
1525
  }
1490
1526
  const provider = providerValue;
@@ -1521,6 +1557,20 @@ function validateConfigTypes(config) {
1521
1557
  }
1522
1558
  };
1523
1559
  validateProviderOverrides(compress.providers);
1560
+ if (compress.contextLimitFallback !== void 0 && typeof compress.contextLimitFallback !== "number") {
1561
+ errors.push({
1562
+ key: "compress.contextLimitFallback",
1563
+ expected: "number",
1564
+ actual: typeof compress.contextLimitFallback
1565
+ });
1566
+ }
1567
+ if (typeof compress.contextLimitFallback === "number" && compress.contextLimitFallback < 0) {
1568
+ errors.push({
1569
+ key: "compress.contextLimitFallback",
1570
+ expected: "non-negative number (0 disables the fallback)",
1571
+ actual: `${compress.contextLimitFallback}`
1572
+ });
1573
+ }
1524
1574
  const validValues = ["ask", "allow", "deny"];
1525
1575
  if (compress.permission !== void 0 && !validValues.includes(compress.permission)) {
1526
1576
  errors.push({
@@ -1606,13 +1656,22 @@ function validateConfigTypes(config) {
1606
1656
  });
1607
1657
  } else {
1608
1658
  if (gc.batchCleanup.lowThreshold !== void 0) {
1609
- validateBatchThreshold("gc.batchCleanup.lowThreshold", gc.batchCleanup.lowThreshold);
1659
+ validateBatchThreshold(
1660
+ "gc.batchCleanup.lowThreshold",
1661
+ gc.batchCleanup.lowThreshold
1662
+ );
1610
1663
  }
1611
1664
  if (gc.batchCleanup.highThreshold !== void 0) {
1612
- validateBatchThreshold("gc.batchCleanup.highThreshold", gc.batchCleanup.highThreshold);
1665
+ validateBatchThreshold(
1666
+ "gc.batchCleanup.highThreshold",
1667
+ gc.batchCleanup.highThreshold
1668
+ );
1613
1669
  }
1614
1670
  if (gc.batchCleanup.forceThreshold !== void 0) {
1615
- validateBatchThreshold("gc.batchCleanup.forceThreshold", gc.batchCleanup.forceThreshold);
1671
+ validateBatchThreshold(
1672
+ "gc.batchCleanup.forceThreshold",
1673
+ gc.batchCleanup.forceThreshold
1674
+ );
1616
1675
  }
1617
1676
  }
1618
1677
  }
@@ -1702,6 +1761,7 @@ var defaultConfig = {
1702
1761
  summaryBuffer: true,
1703
1762
  maxContextLimit: "80%",
1704
1763
  minContextLimit: "80%",
1764
+ contextLimitFallback: 128e3,
1705
1765
  nudgeFrequency: 5,
1706
1766
  minNudgeContextPercent: 5,
1707
1767
  iterationNudgeThreshold: 15,
@@ -1872,6 +1932,7 @@ function mergeCompress(base, override) {
1872
1932
  modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
1873
1933
  modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
1874
1934
  providers: mergeProviderOverrides(base.providers, override.providers),
1935
+ contextLimitFallback: override.contextLimitFallback ?? base.contextLimitFallback,
1875
1936
  nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
1876
1937
  minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent,
1877
1938
  nudgeGrowthTokens: override.nudgeGrowthTokens,
@@ -1895,7 +1956,8 @@ function mergeCompress(base, override) {
1895
1956
  reasoning: {
1896
1957
  drop: override.reasoning?.drop ?? base.reasoning?.drop ?? DEFAULT_COMPRESS_REASONING.drop,
1897
1958
  threshold: override.reasoning?.threshold ?? base.reasoning?.threshold ?? DEFAULT_COMPRESS_REASONING.threshold
1898
- }
1959
+ },
1960
+ completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens
1899
1961
  };
1900
1962
  }
1901
1963
  function mergeCommands(base, override) {
@@ -1933,10 +1995,9 @@ function deepCloneConfig(config) {
1933
1995
  ...provider,
1934
1996
  ...provider.models ? {
1935
1997
  models: Object.fromEntries(
1936
- Object.entries(provider.models).map(([modelId, model]) => [
1937
- modelId,
1938
- { ...model }
1939
- ])
1998
+ Object.entries(provider.models).map(
1999
+ ([modelId, model]) => [modelId, { ...model }]
2000
+ )
1940
2001
  )
1941
2002
  } : {}
1942
2003
  }
@@ -2010,8 +2071,14 @@ function mergeLayer(config, data) {
2010
2071
  ],
2011
2072
  compress: mergeCompress(config.compress, data.compress),
2012
2073
  gc: mergeGC(config.gc, data.gc),
2013
- qualityGate: mergeQualityGate(config.qualityGate, data.qualityGate),
2014
- messageFilters: mergeMessageFilters(config.messageFilters, data.messageFilters)
2074
+ qualityGate: mergeQualityGate(
2075
+ config.qualityGate,
2076
+ data.qualityGate
2077
+ ),
2078
+ messageFilters: mergeMessageFilters(
2079
+ config.messageFilters,
2080
+ data.messageFilters
2081
+ )
2015
2082
  };
2016
2083
  }
2017
2084
  function scheduleParseWarning(ctx, title, message) {
@@ -2159,6 +2226,56 @@ var messageHasCompressAttempt = (message) => {
2159
2226
  const parts = Array.isArray(message.parts) ? message.parts : [];
2160
2227
  return parts.some((part) => part.type === "tool" && part.tool === "compress");
2161
2228
  };
2229
+ var isCaptureOnlyCompress = (message) => {
2230
+ if (!isMessageWithInfo(message)) {
2231
+ return false;
2232
+ }
2233
+ if (message.info.role !== "assistant") {
2234
+ return false;
2235
+ }
2236
+ const parts = Array.isArray(message.parts) ? message.parts : [];
2237
+ let sawBoundary = false;
2238
+ for (const part of parts) {
2239
+ if (!(part.type === "tool" && part.tool === "compress")) {
2240
+ continue;
2241
+ }
2242
+ for (const startId of extractCompressBoundaryIds(part.state?.input)) {
2243
+ sawBoundary = true;
2244
+ if (/^b\d+$/i.test(startId)) {
2245
+ return false;
2246
+ }
2247
+ }
2248
+ }
2249
+ return sawBoundary;
2250
+ };
2251
+ function extractCompressBoundaryIds(rawInput) {
2252
+ let content = [];
2253
+ if (typeof rawInput === "string") {
2254
+ try {
2255
+ const parsed = JSON.parse(rawInput);
2256
+ const c = parsed?.content;
2257
+ content = Array.isArray(c) ? c : [];
2258
+ } catch {
2259
+ return [];
2260
+ }
2261
+ } else if (rawInput && typeof rawInput === "object") {
2262
+ const c = rawInput.content;
2263
+ content = Array.isArray(c) ? c : [];
2264
+ }
2265
+ const ids = [];
2266
+ for (const entry of content) {
2267
+ if (!entry || typeof entry !== "object") {
2268
+ continue;
2269
+ }
2270
+ const { startId, endId } = entry;
2271
+ for (const sid of [startId, endId]) {
2272
+ if (typeof sid === "string" && sid.trim() !== "") {
2273
+ ids.push(sid.trim());
2274
+ }
2275
+ }
2276
+ }
2277
+ return ids;
2278
+ }
2162
2279
  var isIgnoredUserMessage = (message) => {
2163
2280
  if (!isMessageWithInfo(message)) {
2164
2281
  return false;
@@ -2635,6 +2752,16 @@ function resetOnCompaction(state) {
2635
2752
  nextRef: 1
2636
2753
  };
2637
2754
  }
2755
+ function resolveEffectiveContextLimit(state, config) {
2756
+ if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
2757
+ return { limit: state.modelContextLimit, source: "model" };
2758
+ }
2759
+ const fallback = config.compress.contextLimitFallback;
2760
+ if (typeof fallback === "number" && fallback > 0) {
2761
+ return { limit: fallback, source: "fallback" };
2762
+ }
2763
+ return void 0;
2764
+ }
2638
2765
 
2639
2766
  // lib/state/persistence.ts
2640
2767
  function getDefaultStorageDir() {
@@ -4506,6 +4633,24 @@ var SessionStateRegistry = class {
4506
4633
  hydrateModelLimitsFromClient(client) {
4507
4634
  return this.catalog.hydrateFromClient(client);
4508
4635
  }
4636
+ // [FIX #346] The init-time seed (above) is fire-and-forget and races
4637
+ // server readiness: in headless spawn+resume mode the provider-config
4638
+ // call can fail before the server is up, leaving the catalog empty for
4639
+ // the process's lifetime. During a request the server is guaranteed up
4640
+ // (we are inside its pipeline), so on a catalog miss we retry hydration
4641
+ // once per process before giving up (the fallback limit then applies).
4642
+ // The in-flight promise (not a boolean) lets concurrent callers await the
4643
+ // same hydration instead of skipping it.
4644
+ lazyHydration;
4645
+ async hydrateAndResolve(client, providerId, modelId) {
4646
+ const existing = this.catalog.resolve(providerId, modelId);
4647
+ if (existing !== void 0) {
4648
+ return existing;
4649
+ }
4650
+ this.lazyHydration ??= this.catalog.hydrateFromClient(client);
4651
+ await this.lazyHydration;
4652
+ return this.catalog.resolve(providerId, modelId);
4653
+ }
4509
4654
  get(sessionId) {
4510
4655
  return this.states.get(sessionId);
4511
4656
  }
@@ -4599,7 +4744,8 @@ function createSessionState() {
4599
4744
  modelID: void 0,
4600
4745
  systemPromptTokens: void 0,
4601
4746
  storageDir: void 0,
4602
- qualityGateRetryPending: false
4747
+ qualityGateRetryPending: false,
4748
+ noContextLimitWarned: false
4603
4749
  };
4604
4750
  }
4605
4751
  function resetSessionState(state) {
@@ -4642,6 +4788,7 @@ function resetSessionState(state) {
4642
4788
  state.systemPromptTokens = void 0;
4643
4789
  state.storageDir = void 0;
4644
4790
  state.qualityGateRetryPending = false;
4791
+ state.noContextLimitWarned = false;
4645
4792
  }
4646
4793
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
4647
4794
  if (state.sessionId === sessionId) {
@@ -6657,6 +6804,7 @@ function getModelInfo(messages) {
6657
6804
  };
6658
6805
  }
6659
6806
  function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
6807
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
6660
6808
  const parseLimitValue = (limit) => {
6661
6809
  if (limit === void 0) {
6662
6810
  return void 0;
@@ -6664,7 +6812,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6664
6812
  if (typeof limit === "number") {
6665
6813
  return limit;
6666
6814
  }
6667
- if (!limit.endsWith("%") || state.modelContextLimit === void 0) {
6815
+ if (!limit.endsWith("%") || effectiveLimit === void 0) {
6668
6816
  return void 0;
6669
6817
  }
6670
6818
  const parsedPercent = parseFloat(limit.slice(0, -1));
@@ -6673,7 +6821,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6673
6821
  }
6674
6822
  const roundedPercent = Math.round(parsedPercent);
6675
6823
  const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
6676
- return Math.round(clampedPercent / 100 * state.modelContextLimit);
6824
+ return Math.round(clampedPercent / 100 * effectiveLimit.limit);
6677
6825
  };
6678
6826
  if (threshold === "max") {
6679
6827
  const nestedLimit = resolveCompressOverrides(config, providerId, modelId).maxContextLimit;
@@ -6724,11 +6872,12 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
6724
6872
  if (!overMaxLimit) break;
6725
6873
  }
6726
6874
  }
6875
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
6727
6876
  return {
6728
6877
  overMaxLimit,
6729
6878
  overMinLimit,
6730
6879
  currentTokens,
6731
- modelContextLimit: state.modelContextLimit
6880
+ modelContextLimit: effectiveLimit?.limit
6732
6881
  };
6733
6882
  }
6734
6883
  ensureBuiltinTriggerPolicyRegistered();
@@ -6931,6 +7080,7 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
6931
7080
  let summaryTokens = 0;
6932
7081
  let messageTokens = 0;
6933
7082
  let protectedTokens = 0;
7083
+ let reasoningTokens = 0;
6934
7084
  const perMessage = [];
6935
7085
  const perTool = [];
6936
7086
  const perCode = [];
@@ -6984,6 +7134,10 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
6984
7134
  summaryTokens += summaryPartTokens;
6985
7135
  toolTypeMap.set(toolName, (toolTypeMap.get(toolName) || 0) + toolPartTokens);
6986
7136
  if (!msgToolName) msgToolName = toolName;
7137
+ } else if (part.type === "reasoning" && typeof part.text === "string") {
7138
+ const tokens = Math.round(part.text.length / 4);
7139
+ msgTotal += tokens;
7140
+ reasoningTokens += tokens;
6987
7141
  }
6988
7142
  }
6989
7143
  if (isProtected && !isSummary) {
@@ -7011,7 +7165,8 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
7011
7165
  textTokens: Math.max(0, messageTokens - codeTokens),
7012
7166
  systemTokens,
7013
7167
  protectedTokens,
7014
- total: systemTokens + toolTokens + summaryTokens + messageTokens,
7168
+ reasoningTokens,
7169
+ total: systemTokens + toolTokens + summaryTokens + messageTokens + reasoningTokens,
7015
7170
  largestRanges: perMessage.slice(0, 15),
7016
7171
  largestToolRanges: perTool.slice(0, 15),
7017
7172
  largestCodeRanges: perCode.slice(0, 5),
@@ -7034,13 +7189,10 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7034
7189
  if (!ref) continue;
7035
7190
  const rn = parseInt(ref.slice(1), 10);
7036
7191
  if ((protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns)) {
7037
- let tokens2 = 0;
7192
+ const tokens2 = Math.round(countMessageCharacters(msg) / 4);
7038
7193
  const tools = /* @__PURE__ */ new Set();
7039
7194
  for (const part of msg.parts || []) {
7040
- if (part.type === "text" && typeof part.text === "string") {
7041
- tokens2 += Math.round(part.text.length / 4);
7042
- } else if (part.type !== "text" && part.type !== "reasoning") {
7043
- tokens2 += Math.round(JSON.stringify(part).length / 4);
7195
+ if (part.type !== "text" && part.type !== "reasoning") {
7044
7196
  const toolName = part?.tool;
7045
7197
  const callID = part?.callID;
7046
7198
  if (toolName && callID) {
@@ -7061,15 +7213,13 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7061
7213
  protectedMsgInfo.push({ ref, refNum: rn, tokens: tokens2, tools: [...tools] });
7062
7214
  continue;
7063
7215
  }
7064
- let tokens = 0;
7216
+ const tokens = Math.round(countMessageCharacters(msg) / 4);
7065
7217
  let isTool = false;
7066
7218
  let hasMeaningfulPart = false;
7067
7219
  for (const part of msg.parts || []) {
7068
7220
  if (part.type === "text" && typeof part.text === "string") {
7069
- tokens += Math.round(part.text.length / 4);
7070
7221
  if (part.text.trim().length > 0) hasMeaningfulPart = true;
7071
7222
  } else if (part.type !== "text" && part.type !== "reasoning") {
7072
- tokens += Math.round(JSON.stringify(part).length / 4);
7073
7223
  isTool = true;
7074
7224
  hasMeaningfulPart = true;
7075
7225
  }
@@ -7893,8 +8043,11 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7893
8043
  state.nudges.iterationNudgeAnchors.clear();
7894
8044
  state.nudges.lastNudgeShownTokens = void 0;
7895
8045
  state.nudges.lastToolOutputNudgeTokens = void 0;
7896
- state.nudges.lastTier2NudgeTokens = currentTokens;
7897
- state.nudges.lastTier3NudgeTokens = currentTokens;
8046
+ const captureOnly = isCaptureOnlyCompress(lastCompressMsg);
8047
+ if (!captureOnly) {
8048
+ state.nudges.lastTier2NudgeTokens = currentTokens;
8049
+ state.nudges.lastTier3NudgeTokens = currentTokens;
8050
+ }
7898
8051
  const currentTurnHasSuccessfulCompress = messages.slice(currentTurnStart).some((m) => m.info.role === "assistant" && messageHasCompress(m));
7899
8052
  if (currentTurnHasSuccessfulCompress && wasNudgeTriggered && !state.nudges.compressBaselineSet) {
7900
8053
  const baseline = state.nudges.lastPerMessageNudgeTokens;
@@ -8206,7 +8359,7 @@ This is an efficiency nudge to compress early and keep context lean \u2014 not a
8206
8359
  ${COMPRESS_PHILOSOPHY}` : "";
8207
8360
  const sysPart = composition.systemTokens > 0 ? `${fmt(composition.systemTokens)} system (${pct2(composition.systemTokens)}%) | ` : "";
8208
8361
  let breakdown = `${efficiencyNote}
8209
- Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct2(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct2(plainTextTokens)}%)${growthStr}`;
8362
+ Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct2(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct2(plainTextTokens)}%) | ${fmt(composition.reasoningTokens)} reasoning (${pct2(composition.reasoningTokens)}%)${growthStr}`;
8210
8363
  const compressibleTokens = composition.total - composition.systemTokens - composition.protectedTokens - composition.summaryTokens;
8211
8364
  if (composition.protectedTokens > 0) {
8212
8365
  breakdown += `
@@ -8807,8 +8960,9 @@ function createDecompressTool(factoryCtx) {
8807
8960
  async execute(args, toolCtx) {
8808
8961
  const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
8809
8962
  const { rawMessages } = await prepareDecompressSession(ctx, toolCtx);
8810
- const contextUsageBefore = ctx.state.modelContextLimit ? Math.round(
8811
- getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
8963
+ const effectiveLimitBefore = resolveEffectiveContextLimit(ctx.state, ctx.config);
8964
+ const contextUsageBefore = effectiveLimitBefore ? Math.round(
8965
+ getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitBefore.limit * 100
8812
8966
  ) : void 0;
8813
8967
  const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
8814
8968
  if (!resolved.ok) {
@@ -8872,8 +9026,9 @@ function createDecompressTool(factoryCtx) {
8872
9026
  0,
8873
9027
  ctx.state.stats.totalPruneTokens - restoredTokens
8874
9028
  );
8875
- const contextUsageAfter = ctx.state.modelContextLimit ? Math.round(
8876
- getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
9029
+ const effectiveLimitAfter = resolveEffectiveContextLimit(ctx.state, ctx.config);
9030
+ const contextUsageAfter = effectiveLimitAfter ? Math.round(
9031
+ getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitAfter.limit * 100
8877
9032
  ) : void 0;
8878
9033
  await finalizeDecompressSession(ctx);
8879
9034
  const restoredContentPreview = buildRestoredContentPreview(
@@ -9094,6 +9249,7 @@ function collectVisibleMessages(rawMessages, ctx) {
9094
9249
  const ref = byRawId.get(msgId);
9095
9250
  if (!ref) return;
9096
9251
  let tokens = 0;
9252
+ let reasoning = 0;
9097
9253
  let toolName = "";
9098
9254
  for (const part of msg.parts || []) {
9099
9255
  if (part.type === "text" && typeof part.text === "string") {
@@ -9104,10 +9260,12 @@ function collectVisibleMessages(rawMessages, ctx) {
9104
9260
  if (!toolName) {
9105
9261
  toolName = part?.tool || "unknown";
9106
9262
  }
9263
+ } else if (part.type === "reasoning" && typeof part.text === "string") {
9264
+ reasoning += Math.round(part.text.length / 4);
9107
9265
  }
9108
9266
  }
9109
- if (tokens > 0) {
9110
- result.push({ ref, tokens, tool: toolName || "text", index: idx });
9267
+ if (tokens > 0 || reasoning > 0) {
9268
+ result.push({ ref, tokens, tool: toolName || "text", index: idx, reasoning });
9111
9269
  }
9112
9270
  });
9113
9271
  return {
@@ -9129,14 +9287,16 @@ function renderOverview(visibleMessages, summaryTokens, systemTokens, blocks, fe
9129
9287
  } else {
9130
9288
  const totalTool = visibleMessages.filter((m) => m.tool !== "text" && m.tool !== "step-finish").reduce((s, m) => s + m.tokens, 0);
9131
9289
  const totalText = visibleMessages.filter((m) => m.tool === "text").reduce((s, m) => s + m.tokens, 0);
9132
- const total = systemTokens + totalTool + totalText + summaryTokens;
9290
+ const totalReasoning = visibleMessages.reduce((s, m) => s + m.reasoning, 0);
9291
+ const total = systemTokens + totalTool + totalText + summaryTokens + totalReasoning;
9133
9292
  const sysPct = pct(systemTokens, total);
9134
9293
  const toolPct = pct(totalTool, total);
9135
9294
  const textPct = pct(totalText, total);
9136
9295
  const summaryPct = pct(summaryTokens, total);
9296
+ const reasoningPct = pct(totalReasoning, total);
9137
9297
  lines.push("CONTEXT BREAKDOWN");
9138
9298
  lines.push(
9139
- ` ${formatTokens(systemTokens)} system (${sysPct}%) | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%)`
9299
+ ` ${formatTokens(systemTokens)} system (${sysPct}%) | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%) | ${formatTokens(totalReasoning)} reasoning (${reasoningPct}%)`
9140
9300
  );
9141
9301
  const topTypes = Array.from(toolTypeMap.entries()).map(([tool6, tokens]) => ({ tool: tool6, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
9142
9302
  if (topTypes.length > 0) {
@@ -9247,22 +9407,23 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
9247
9407
  if (toolFilter) {
9248
9408
  filtered = filtered.filter((m) => m.tool === toolFilter);
9249
9409
  }
9410
+ const sizeOf = (m) => m.tokens + m.reasoning;
9250
9411
  if (sort === "time") {
9251
9412
  filtered.sort((a, b) => a.index - b.index);
9252
9413
  } else if (sort === "tool") {
9253
- filtered.sort((a, b) => a.tool.localeCompare(b.tool) || b.tokens - a.tokens);
9414
+ filtered.sort((a, b) => a.tool.localeCompare(b.tool) || sizeOf(b) - sizeOf(a));
9254
9415
  } else {
9255
- filtered.sort((a, b) => b.tokens - a.tokens);
9416
+ filtered.sort((a, b) => sizeOf(b) - sizeOf(a));
9256
9417
  }
9257
- const totalTokens = filtered.reduce((s, m) => s + m.tokens, 0);
9258
- const allTokens = visibleMessages.reduce((s, m) => s + m.tokens, 0);
9418
+ const totalTokens = filtered.reduce((s, m) => s + sizeOf(m), 0);
9419
+ const allTokens = visibleMessages.reduce((s, m) => s + sizeOf(m), 0);
9259
9420
  const header = toolFilter ? `UNCOMPRESSED \u2014 ${toolFilter}: ${formatTokens(totalTokens)} | ${filtered.length} msgs | ${pct(totalTokens, allTokens)}% of visible` : `UNCOMPRESSED \u2014 ${formatTokens(totalTokens)} | ${filtered.length} msgs`;
9260
9421
  lines.push(header);
9261
9422
  lines.push(`Sorted by ${sort}`);
9262
9423
  lines.push("");
9263
9424
  const shown = filtered.slice(0, limit);
9264
9425
  for (const m of shown) {
9265
- lines.push(` ${m.ref} (${formatTokens(m.tokens)}) ${m.tool}`);
9426
+ lines.push(` ${m.ref} (${formatTokens(sizeOf(m))}) ${m.tool}`);
9266
9427
  }
9267
9428
  if (filtered.length > shown.length) {
9268
9429
  lines.push("");
@@ -9530,7 +9691,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9530
9691
  import { join as join4 } from "path";
9531
9692
  import { existsSync as existsSync4 } from "fs";
9532
9693
  import { homedir as homedir3 } from "os";
9533
- var LOG_VERSION = true ? "1.16.0" : "dev";
9694
+ var LOG_VERSION = true ? "1.17.0-pr.383.130" : "dev";
9534
9695
  var LEVEL_RANK = {
9535
9696
  debug: 10,
9536
9697
  info: 20,
@@ -9826,13 +9987,14 @@ CONTEXT BREAKDOWN
9826
9987
 
9827
9988
  When context usage passes a threshold, the system appends a breakdown showing where your context tokens are spent:
9828
9989
 
9829
- Breakdown: 5.2K system (21%) | 12.3K tool (40%) | 3.1K summaries (10%) | 8.5K code (28%) | 6.5K text (22%)
9990
+ Breakdown: 4.2K system (21%) | 8.0K tool (40%) | 2.0K summaries (10%) | 2.6K code (13%) | 2.2K text (11%) | 1.0K reasoning (5%)
9830
9991
 
9831
9992
  - "system" = system prompt tokens (AGENTS.md, tool definitions \u2014 not compressible)
9832
9993
  - "tool" = tool call outputs (largest category \u2014 compress first when consumed)
9833
9994
  - "summaries" = existing compression block summaries (already compressed; do not re-compress standalone)
9834
9995
  - "code" = messages containing code blocks
9835
9996
  - "text" = plain text messages
9997
+ - "reasoning" = model thinking blocks (counted to match real API usage; freed when their message is compressed)
9836
9998
 
9837
9999
  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.
9838
10000
 
@@ -10354,6 +10516,8 @@ var MIN_OUTPUT_TOKENS = 1e3;
10354
10516
  var KEEP_PREFIX_CHARS = 2e3;
10355
10517
  var KEEP_SUFFIX_CHARS = 2e3;
10356
10518
  var PROTECT_RECENT_MESSAGES = 3;
10519
+ var OUTPUT_RESERVE_TOKENS = 16384;
10520
+ var overheadErrorLogged = /* @__PURE__ */ new Set();
10357
10521
  function parseGcThreshold(threshold, modelContextLimit) {
10358
10522
  if (typeof threshold === "number") return threshold;
10359
10523
  const str = threshold ?? "100%";
@@ -10362,10 +10526,26 @@ function parseGcThreshold(threshold, modelContextLimit) {
10362
10526
  return modelContextLimit;
10363
10527
  }
10364
10528
  function truncateLargeToolOutputs(state, config, logger, messages) {
10365
- if (!state.modelContextLimit) return;
10529
+ const effective = resolveEffectiveContextLimit(state, config);
10530
+ if (!effective) return;
10366
10531
  const currentTokens = getCurrentTokenUsage(state, messages);
10367
10532
  if (currentTokens === 0) return;
10368
- const threshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, state.modelContextLimit);
10533
+ const configuredThreshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, effective.limit);
10534
+ const overhead = (state.systemPromptTokens ?? 0) + OUTPUT_RESERVE_TOKENS;
10535
+ const threshold = Math.min(configuredThreshold, effective.limit - overhead);
10536
+ if (threshold <= 0) {
10537
+ const sessionKey = state.sessionId ?? "unknown";
10538
+ if (!overheadErrorLogged.has(sessionKey)) {
10539
+ overheadErrorLogged.add(sessionKey);
10540
+ logger.error("ACP: model context window too small to fit overhead", {
10541
+ session: state.sessionId,
10542
+ limit: effective.limit,
10543
+ contextLimitSource: effective.source,
10544
+ overhead
10545
+ });
10546
+ }
10547
+ return;
10548
+ }
10369
10549
  if (currentTokens < threshold) return;
10370
10550
  const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES;
10371
10551
  const candidates = [];
@@ -10410,9 +10590,158 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
10410
10590
  truncatedCount,
10411
10591
  estimatedSavedTokens: Math.round(savedTokens),
10412
10592
  currentTokens,
10413
- threshold
10593
+ threshold,
10594
+ contextLimit: effective.limit,
10595
+ contextLimitSource: effective.source
10596
+ });
10597
+ }
10598
+ }
10599
+
10600
+ // lib/messages/enforce-budget.ts
10601
+ var DEFAULT_COMPLETION_RESERVE_TOKENS = 32768;
10602
+ var TRUNCATION_MARKER2 = "[truncated for context space";
10603
+ var KEEP_PREFIX_CHARS2 = 2e3;
10604
+ var KEEP_SUFFIX_CHARS2 = 2e3;
10605
+ var PROTECT_RECENT_MESSAGES2 = 3;
10606
+ var MIN_CLEAR_TOKENS = 200;
10607
+ function resolveContextWindow(state) {
10608
+ if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
10609
+ return state.modelContextLimit;
10610
+ }
10611
+ return void 0;
10612
+ }
10613
+ function estimateWireTokens(state, messages) {
10614
+ const base = getCurrentTokenUsage(state, messages);
10615
+ if (base > 0) {
10616
+ let baseAssistant = -1;
10617
+ for (let i = messages.length - 1; i >= 0; i--) {
10618
+ if (messages[i].info.role !== "assistant") continue;
10619
+ const tokens = messages[i].info.tokens;
10620
+ if ((tokens?.input || 0) <= 0 && (tokens?.output || 0) <= 0) continue;
10621
+ baseAssistant = i;
10622
+ break;
10623
+ }
10624
+ if (baseAssistant >= 0) {
10625
+ let additions = 0;
10626
+ for (let i = baseAssistant + 1; i < messages.length; i++) {
10627
+ additions += countAllMessageTokens(messages[i]);
10628
+ }
10629
+ return base + additions;
10630
+ }
10631
+ }
10632
+ let total = 0;
10633
+ for (const m of messages) total += countAllMessageTokens(m);
10634
+ return total + (state.systemPromptTokens ?? 0);
10635
+ }
10636
+ function enforceContextBudget(state, config, logger, messages) {
10637
+ const window = resolveContextWindow(state);
10638
+ if (window === void 0) return void 0;
10639
+ const configuredReserve = config.compress?.completionReserveTokens;
10640
+ const reserve = typeof configuredReserve === "number" && Number.isFinite(configuredReserve) && configuredReserve >= 0 ? configuredReserve : DEFAULT_COMPLETION_RESERVE_TOKENS;
10641
+ const budget = window - reserve;
10642
+ if (budget <= 0) return void 0;
10643
+ const estimatedTokens = estimateWireTokens(state, messages);
10644
+ if (estimatedTokens <= budget) {
10645
+ return {
10646
+ applied: false,
10647
+ window,
10648
+ reserve,
10649
+ budget,
10650
+ estimatedTokens,
10651
+ finalEstimate: estimatedTokens,
10652
+ truncatedCount: 0,
10653
+ clearedCount: 0
10654
+ };
10655
+ }
10656
+ const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES2;
10657
+ const protectedTools = new Set(config.compress?.protectedTools ?? []);
10658
+ const candidates = [];
10659
+ for (let mi = 0; mi < protectedIndex; mi++) {
10660
+ if (mi === 0 && messages[mi].info.role === "user") continue;
10661
+ const msg = messages[mi];
10662
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
10663
+ for (const part of parts) {
10664
+ if (part?.type !== "tool") continue;
10665
+ if (part.state?.status !== "completed") continue;
10666
+ if (part.tool === "compress") continue;
10667
+ if (protectedTools.has(part.tool)) continue;
10668
+ const content = extractCompletedToolOutput(part);
10669
+ if (content === void 0) continue;
10670
+ if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10671
+ const tokens = countTokens2(content);
10672
+ if (tokens <= 0) continue;
10673
+ candidates.push({ part, content, tokens, index: mi });
10674
+ }
10675
+ }
10676
+ let saved = 0;
10677
+ let truncatedCount = 0;
10678
+ let clearedCount = 0;
10679
+ const truncatable = candidates.filter(
10680
+ (c) => !c.content.includes(TRUNCATION_MARKER2) && c.content.length > KEEP_PREFIX_CHARS2 + KEEP_SUFFIX_CHARS2
10681
+ ).sort((a, b) => b.tokens - a.tokens);
10682
+ for (const c of truncatable) {
10683
+ if (estimatedTokens - saved <= budget) break;
10684
+ const prefix = c.content.slice(0, KEEP_PREFIX_CHARS2);
10685
+ const suffix = c.content.slice(-KEEP_SUFFIX_CHARS2);
10686
+ const truncated = prefix + `
10687
+
10688
+ ...${TRUNCATION_MARKER2} \u2014 original ~${c.tokens} tokens]...
10689
+
10690
+ ` + suffix;
10691
+ if (truncated.length >= c.content.length) continue;
10692
+ c.part.state.output = truncated;
10693
+ saved += c.tokens - countTokens2(truncated);
10694
+ truncatedCount++;
10695
+ }
10696
+ if (estimatedTokens - saved > budget) {
10697
+ const clearable = candidates.filter((c) => {
10698
+ const out = extractCompletedToolOutput(c.part);
10699
+ return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens2(out) > MIN_CLEAR_TOKENS;
10700
+ }).sort((a, b) => a.index - b.index);
10701
+ for (const c of clearable) {
10702
+ if (estimatedTokens - saved <= budget) break;
10703
+ const current = extractCompletedToolOutput(c.part);
10704
+ if (current === void 0 || current === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10705
+ c.part.state.output = COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
10706
+ saved += countTokens2(current) - countTokens2(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
10707
+ clearedCount++;
10708
+ }
10709
+ }
10710
+ const finalEstimate = Math.max(0, estimatedTokens - saved);
10711
+ if (truncatedCount > 0 || clearedCount > 0) {
10712
+ logger.warn("Context budget guard: pruned tool outputs to fit the request", {
10713
+ session: state.sessionId,
10714
+ estimatedTokens: Math.round(estimatedTokens),
10715
+ budget,
10716
+ window,
10717
+ reserve,
10718
+ truncatedCount,
10719
+ clearedCount,
10720
+ estimatedSavedTokens: Math.round(saved),
10721
+ finalEstimate: Math.round(finalEstimate)
10414
10722
  });
10415
10723
  }
10724
+ if (finalEstimate > budget) {
10725
+ logger.warn(
10726
+ "Context budget guard: still over budget after pruning all compressible tool outputs; the request may be rejected by the model",
10727
+ {
10728
+ session: state.sessionId,
10729
+ finalEstimate: Math.round(finalEstimate),
10730
+ budget,
10731
+ window
10732
+ }
10733
+ );
10734
+ }
10735
+ return {
10736
+ applied: truncatedCount > 0 || clearedCount > 0,
10737
+ window,
10738
+ reserve,
10739
+ budget,
10740
+ estimatedTokens,
10741
+ finalEstimate,
10742
+ truncatedCount,
10743
+ clearedCount
10744
+ };
10416
10745
  }
10417
10746
 
10418
10747
  // lib/commands/context.ts
@@ -11371,11 +11700,12 @@ function runBatchCleanup(state, config, logger, messages) {
11371
11700
  mergedCount: 0,
11372
11701
  savedTokens: 0
11373
11702
  };
11374
- if (!state.modelContextLimit || state.modelContextLimit <= 0) {
11703
+ const effective = resolveEffectiveContextLimit(state, config);
11704
+ if (!effective) {
11375
11705
  return noop;
11376
11706
  }
11377
11707
  const currentTokens = getCurrentTokenUsage(state, messages);
11378
- if (currentTokens < state.modelContextLimit) {
11708
+ if (currentTokens < effective.limit) {
11379
11709
  return noop;
11380
11710
  }
11381
11711
  const maxMergedLength = config.gc.maxOldGenSummaryLength;
@@ -11392,7 +11722,8 @@ function runBatchCleanup(state, config, logger, messages) {
11392
11722
  mergedCount: result.mergedCount,
11393
11723
  savedTokens: result.savedTokens,
11394
11724
  currentTokens,
11395
- contextLimit: state.modelContextLimit
11725
+ contextLimit: effective.limit,
11726
+ contextLimitSource: effective.source
11396
11727
  });
11397
11728
  return {
11398
11729
  tier: 3,
@@ -11426,11 +11757,6 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11426
11757
  input.model?.limit?.context
11427
11758
  );
11428
11759
  const state = input.sessionID ? registry4.get(input.sessionID) : void 0;
11429
- if (state && input.model?.limit?.context) {
11430
- state.modelContextLimit = input.model.limit.context;
11431
- state.modelProviderID = input.model?.providerID;
11432
- state.modelID = input.model?.id;
11433
- }
11434
11760
  if (!state || state.isSubAgent && !config.allowSubAgents) {
11435
11761
  return;
11436
11762
  }
@@ -11439,6 +11765,23 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11439
11765
  logger.info("Skipping DCP system prompt injection for internal agent");
11440
11766
  return;
11441
11767
  }
11768
+ if (input.model?.limit?.context) {
11769
+ const limit = input.model.limit.context;
11770
+ const providerID = input.model?.providerID;
11771
+ const modelID = input.model?.id;
11772
+ const changed = state.modelContextLimit !== limit || providerID !== void 0 && state.modelProviderID !== providerID || modelID !== void 0 && state.modelID !== modelID;
11773
+ state.modelContextLimit = limit;
11774
+ if (providerID !== void 0) {
11775
+ state.modelProviderID = providerID;
11776
+ }
11777
+ if (modelID !== void 0) {
11778
+ state.modelID = modelID;
11779
+ }
11780
+ if (changed) {
11781
+ saveSessionState(state, logger).catch(() => {
11782
+ });
11783
+ }
11784
+ }
11442
11785
  const effectivePermission = compressPermission(state, config);
11443
11786
  if (effectivePermission === "deny") {
11444
11787
  return;
@@ -11483,10 +11826,17 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11483
11826
  config
11484
11827
  );
11485
11828
  const requestModel = lastUserMessage.info.model;
11486
- const requestModelLimit = registry4.resolveModelLimit(
11829
+ let requestModelLimit = registry4.resolveModelLimit(
11487
11830
  requestModel?.providerID,
11488
11831
  requestModel?.modelID
11489
11832
  );
11833
+ if (requestModelLimit === void 0 && requestModel?.providerID && requestModel?.modelID) {
11834
+ requestModelLimit = await registry4.hydrateAndResolve(
11835
+ client,
11836
+ requestModel.providerID,
11837
+ requestModel.modelID
11838
+ );
11839
+ }
11490
11840
  const prevModelID = state.modelID;
11491
11841
  if (requestModelLimit !== void 0) {
11492
11842
  state.modelContextLimit = requestModelLimit;
@@ -11516,6 +11866,16 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11516
11866
  });
11517
11867
  }
11518
11868
  await updatePerTurnState(state, logger, messages);
11869
+ if (state.modelContextLimit === void 0 && !state.noContextLimitWarned && requestModel?.providerID && requestModel?.modelID && registry4.resolveModelLimit(requestModel.providerID, requestModel.modelID) === void 0) {
11870
+ state.noContextLimitWarned = true;
11871
+ logger.warn(
11872
+ 'Model reports no context window and the catalog has no entry for it; all percentage thresholds (min/max/emergency, GC) and the context-budget guard are disabled. Set the model limit in opencode.json (e.g. "limit": {"context": 262144, "output": 16384}) to enable them (also fixes the 32000 max_tokens fallback); an absolute compress.maxContextLimit in acp.jsonc only enables proactive nudges, not the guard.',
11873
+ {
11874
+ session: state.sessionId,
11875
+ model: `${requestModel.providerID}/${requestModel.modelID}`
11876
+ }
11877
+ );
11878
+ }
11519
11879
  }
11520
11880
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
11521
11881
  if (state.isSubAgent && !config.allowSubAgents) {
@@ -11541,10 +11901,11 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11541
11901
  }
11542
11902
  }
11543
11903
  ensureBuiltinFiltersRegistered();
11904
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
11544
11905
  applyMessageFilters(output.messages, config.messageFilters, logger, {
11545
11906
  sessionId: state.sessionId ?? "",
11546
11907
  isSubAgent: state.isSubAgent,
11547
- modelContextLimit: state.modelContextLimit
11908
+ modelContextLimit: effectiveLimit?.limit
11548
11909
  });
11549
11910
  cacheSystemPromptTokens(state, output.messages);
11550
11911
  assignMessageRefs(state, output.messages);
@@ -11564,6 +11925,7 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11564
11925
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
11565
11926
  prune(state, logger, config, output.messages);
11566
11927
  truncateLargeToolOutputs(state, config, logger, output.messages);
11928
+ enforceContextBudget(state, config, logger, output.messages);
11567
11929
  hideConsumedCompressCalls(state, output.messages);
11568
11930
  assignMessageRefs(state, output.messages);
11569
11931
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
@@ -11595,14 +11957,31 @@ ${text}`);
11595
11957
  stripStaleMetadata(output.messages);
11596
11958
  dropEmptyMessages(output.messages);
11597
11959
  const postTokens = getCurrentTokenUsage(state, output.messages);
11960
+ if (postTokens !== void 0 && effectiveLimit) {
11961
+ const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
11962
+ if (postTokens > budget) {
11963
+ logger.error(
11964
+ "ACP hard guard: context exceeds model budget after in-flight reduction",
11965
+ {
11966
+ session: state.sessionId,
11967
+ postTokens,
11968
+ budget,
11969
+ contextLimit: effectiveLimit.limit,
11970
+ contextLimitSource: effectiveLimit.source,
11971
+ hint: "request will likely be rejected; run /compact or start a new session"
11972
+ }
11973
+ );
11974
+ }
11975
+ }
11598
11976
  logger.info("Chat transform complete", {
11599
11977
  session: state.sessionId,
11600
11978
  model: state.modelID,
11601
11979
  messages: output.messages.length,
11602
11980
  prePruneTokens,
11603
11981
  postTokens,
11604
- contextLimit: state.modelContextLimit,
11605
- usagePct: postTokens !== void 0 && state.modelContextLimit ? `${(postTokens / state.modelContextLimit * 100).toFixed(1)}%` : void 0,
11982
+ contextLimit: effectiveLimit?.limit,
11983
+ contextLimitSource: effectiveLimit?.source,
11984
+ usagePct: postTokens !== void 0 && effectiveLimit ? `${(postTokens / effectiveLimit.limit * 100).toFixed(1)}%` : void 0,
11606
11985
  nudged: state.nudges.shouldInjectThisTurn
11607
11986
  });
11608
11987
  if (state.sessionId) {
@@ -11634,12 +12013,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11634
12013
  path: { id: input.sessionID }
11635
12014
  });
11636
12015
  const messages = filterMessages(messagesResponse.data || messagesResponse);
11637
- const state = await registry4.getOrCreate(
11638
- client,
11639
- input.sessionID,
11640
- messages,
11641
- config
11642
- );
12016
+ const state = await registry4.getOrCreate(client, input.sessionID, messages, config);
11643
12017
  syncCompressPermissionState(state, config, hostPermissions, messages);
11644
12018
  const commandCtx = {
11645
12019
  client,
@@ -11653,7 +12027,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11653
12027
  const sub = input.arguments?.trim().toLowerCase();
11654
12028
  if (sub === "stats" || sub === "status" || sub === "") {
11655
12029
  await handleStatsCommand(commandCtx);
11656
- throw new Error("__DCP_CONTEXT_HANDLED__");
12030
+ return;
11657
12031
  }
11658
12032
  if (sub === "export" || sub.startsWith("export ")) {
11659
12033
  const exportArgs = input.arguments?.trim().slice("export".length).trim() || "";
@@ -11661,17 +12035,10 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11661
12035
  throw new Error("__DCP_CONTEXT_HANDLED__");
11662
12036
  }
11663
12037
  if (sub === "help") {
11664
- await sendIgnoredMessage(
11665
- client,
11666
- input.sessionID,
11667
- buildHelpText(),
11668
- {},
11669
- logger
11670
- );
12038
+ await sendIgnoredMessage(client, input.sessionID, buildHelpText(), {}, logger);
11671
12039
  throw new Error("__DCP_CONTEXT_HANDLED__");
11672
12040
  }
11673
12041
  await handleContextCommand(commandCtx);
11674
- throw new Error("__DCP_CONTEXT_HANDLED__");
11675
12042
  }
11676
12043
  };
11677
12044
  }
@@ -11742,9 +12109,7 @@ function createEventHandler(registry4, logger) {
11742
12109
  return;
11743
12110
  }
11744
12111
  if (typeof part.callID === "string" && typeof part.messageID === "string") {
11745
- timing.startsByCallId.delete(
11746
- buildCompressionTimingKey(part.messageID, part.callID)
11747
- );
12112
+ timing.startsByCallId.delete(buildCompressionTimingKey(part.messageID, part.callID));
11748
12113
  }
11749
12114
  };
11750
12115
  }
@@ -12021,7 +12386,7 @@ var server = (async (ctx) => {
12021
12386
  }
12022
12387
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
12023
12388
  logger.info("ACP plugin initialized", {
12024
- version: true ? "1.16.0" : "dev",
12389
+ version: true ? "1.17.0-pr.383.130" : "dev",
12025
12390
  workspace: ctx.directory,
12026
12391
  logLevel: logger.level,
12027
12392
  debug: config.debug,