opencode-acp 1.16.0 → 1.17.0-pr.385.132

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.
Files changed (39) hide show
  1. package/README.md +28 -0
  2. package/README.zh-CN.md +27 -0
  3. package/dist/index.js +565 -108
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/compress/decompress-logic.d.ts.map +1 -1
  6. package/dist/lib/compress/decompress.d.ts.map +1 -1
  7. package/dist/lib/compress/hide-consumed.d.ts.map +1 -1
  8. package/dist/lib/compress/search.d.ts.map +1 -1
  9. package/dist/lib/compress/state.d.ts.map +1 -1
  10. package/dist/lib/compress/status.d.ts.map +1 -1
  11. package/dist/lib/compress/types.d.ts +6 -0
  12. package/dist/lib/compress/types.d.ts.map +1 -1
  13. package/dist/lib/config-validation.d.ts.map +1 -1
  14. package/dist/lib/config.d.ts +16 -1
  15. package/dist/lib/config.d.ts.map +1 -1
  16. package/dist/lib/gc/merge.d.ts.map +1 -1
  17. package/dist/lib/hooks.d.ts.map +1 -1
  18. package/dist/lib/messages/enforce-budget.d.ts +65 -0
  19. package/dist/lib/messages/enforce-budget.d.ts.map +1 -0
  20. package/dist/lib/messages/inject/inject.d.ts.map +1 -1
  21. package/dist/lib/messages/inject/utils.d.ts +1 -0
  22. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  23. package/dist/lib/messages/query.d.ts +16 -0
  24. package/dist/lib/messages/query.d.ts.map +1 -1
  25. package/dist/lib/messages/sync.d.ts.map +1 -1
  26. package/dist/lib/messages/truncate-tools.d.ts +1 -0
  27. package/dist/lib/messages/truncate-tools.d.ts.map +1 -1
  28. package/dist/lib/prompts/system.d.ts +1 -1
  29. package/dist/lib/prompts/system.d.ts.map +1 -1
  30. package/dist/lib/state/persistence.d.ts.map +1 -1
  31. package/dist/lib/state/state.d.ts +2 -0
  32. package/dist/lib/state/state.d.ts.map +1 -1
  33. package/dist/lib/state/types.d.ts +29 -0
  34. package/dist/lib/state/types.d.ts.map +1 -1
  35. package/dist/lib/state/utils.d.ts +23 -0
  36. package/dist/lib/state/utils.d.ts.map +1 -1
  37. package/dist/lib/token-utils.d.ts +8 -0
  38. package/dist/lib/token-utils.d.ts.map +1 -1
  39. package/package.json +1 -1
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;
@@ -2325,6 +2442,9 @@ function countMessageCharacters(msg) {
2325
2442
  }
2326
2443
  return total;
2327
2444
  }
2445
+ function estimateAllMessageTokensFast(msg) {
2446
+ return Math.round(countMessageCharacters(msg) / 4);
2447
+ }
2328
2448
 
2329
2449
  // lib/prompts/extensions/tool.ts
2330
2450
  var RANGE_FORMAT_EXTENSION = `
@@ -2374,6 +2494,9 @@ var isMessageCompacted = (state, msg) => {
2374
2494
  }
2375
2495
  return false;
2376
2496
  };
2497
+ function bumpPruneStructureVersion(messagesState) {
2498
+ messagesState.structureVersion = (messagesState.structureVersion ?? 0) + 1;
2499
+ }
2377
2500
  function serializePruneMessagesState(messagesState) {
2378
2501
  return {
2379
2502
  byMessageId: Object.fromEntries(messagesState.byMessageId),
@@ -2635,6 +2758,16 @@ function resetOnCompaction(state) {
2635
2758
  nextRef: 1
2636
2759
  };
2637
2760
  }
2761
+ function resolveEffectiveContextLimit(state, config) {
2762
+ if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
2763
+ return { limit: state.modelContextLimit, source: "model" };
2764
+ }
2765
+ const fallback = config.compress.contextLimitFallback;
2766
+ if (typeof fallback === "number" && fallback > 0) {
2767
+ return { limit: fallback, source: "fallback" };
2768
+ }
2769
+ return void 0;
2770
+ }
2638
2771
 
2639
2772
  // lib/state/persistence.ts
2640
2773
  function getDefaultStorageDir() {
@@ -2681,9 +2814,13 @@ async function writePersistedSessionState(sessionId, state, logger, storageDir)
2681
2814
  totalTokensSaved: state.stats.totalPruneTokens
2682
2815
  });
2683
2816
  }
2684
- async function saveSessionState(sessionState, logger, sessionName) {
2817
+ var saveQueues = /* @__PURE__ */ new Map();
2818
+ function saveQueueKey(sessionId, storageDir) {
2819
+ return `${sessionId}\0${storageDir ?? ""}`;
2820
+ }
2821
+ function saveSessionState(sessionState, logger, sessionName) {
2685
2822
  if (!sessionState.sessionId) {
2686
- return;
2823
+ return Promise.resolve();
2687
2824
  }
2688
2825
  const state = {
2689
2826
  sessionName,
@@ -2714,7 +2851,53 @@ async function saveSessionState(sessionState, logger, sessionName) {
2714
2851
  modelProviderID: sessionState.modelProviderID,
2715
2852
  modelID: sessionState.modelID
2716
2853
  };
2717
- await writePersistedSessionState(sessionState.sessionId, state, logger, sessionState.storageDir);
2854
+ const key = saveQueueKey(sessionState.sessionId, sessionState.storageDir);
2855
+ let queue = saveQueues.get(key);
2856
+ if (!queue) {
2857
+ queue = { pending: [], waiters: [], draining: false };
2858
+ saveQueues.set(key, queue);
2859
+ }
2860
+ const promise = new Promise((resolve2, reject) => {
2861
+ queue.waiters.push({ resolve: resolve2, reject });
2862
+ });
2863
+ queue.pending.push({
2864
+ sessionId: sessionState.sessionId,
2865
+ state,
2866
+ storageDir: sessionState.storageDir,
2867
+ logger
2868
+ });
2869
+ if (!queue.draining) {
2870
+ queue.draining = true;
2871
+ setImmediate(() => drainSaveQueue(key));
2872
+ }
2873
+ return promise;
2874
+ }
2875
+ function drainSaveQueue(key) {
2876
+ const queue = saveQueues.get(key);
2877
+ if (!queue || queue.pending.length === 0) {
2878
+ if (queue) {
2879
+ queue.draining = false;
2880
+ if (queue.waiters.length === 0) saveQueues.delete(key);
2881
+ }
2882
+ return;
2883
+ }
2884
+ const batch = queue.pending.splice(0, queue.pending.length);
2885
+ const waiters = queue.waiters.splice(0, batch.length);
2886
+ const latest = batch[batch.length - 1];
2887
+ writePersistedSessionState(latest.sessionId, latest.state, latest.logger, latest.storageDir).then(() => {
2888
+ for (const waiter of waiters) waiter.resolve();
2889
+ }).catch((err) => {
2890
+ for (const waiter of waiters) waiter.reject(err);
2891
+ }).finally(() => {
2892
+ const q = saveQueues.get(key);
2893
+ if (!q) return;
2894
+ if (q.pending.length > 0) {
2895
+ drainSaveQueue(key);
2896
+ } else {
2897
+ q.draining = false;
2898
+ if (q.waiters.length === 0) saveQueues.delete(key);
2899
+ }
2900
+ });
2718
2901
  }
2719
2902
  async function loadSessionState(sessionId, logger, storageDir) {
2720
2903
  try {
@@ -3208,6 +3391,7 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3208
3391
  state.stats.pruneTokenCounter += compressedTokens;
3209
3392
  state.stats.totalPruneTokens += state.stats.pruneTokenCounter;
3210
3393
  state.stats.pruneTokenCounter = 0;
3394
+ bumpPruneStructureVersion(messagesState);
3211
3395
  return {
3212
3396
  compressedTokens,
3213
3397
  messageIds: selection.messageIds,
@@ -3321,15 +3505,17 @@ function buildSearchContext(state, rawMessages) {
3321
3505
  }
3322
3506
  summaryByBlockId.set(blockId, block);
3323
3507
  }
3324
- return {
3508
+ const context = {
3325
3509
  rawMessages,
3326
3510
  rawMessagesById,
3327
3511
  rawIndexById,
3328
3512
  summaryByBlockId
3329
3513
  };
3514
+ context.boundaryLookup = buildBoundaryLookup(context, state);
3515
+ return context;
3330
3516
  }
3331
3517
  function resolveBoundaryIds(context, state, startId, endId, logger) {
3332
- const lookup = buildBoundaryLookup(context, state);
3518
+ const lookup = context.boundaryLookup ?? (context.boundaryLookup = buildBoundaryLookup(context, state));
3333
3519
  const issues = [];
3334
3520
  const parsedStartId = parseBoundaryId(startId);
3335
3521
  const parsedEndId = parseBoundaryId(endId);
@@ -3529,7 +3715,7 @@ function resolveSelection(context, startReference, endReference) {
3529
3715
  messageIds.push(messageId);
3530
3716
  }
3531
3717
  if (!messageTokenById.has(messageId)) {
3532
- messageTokenById.set(messageId, countAllMessageTokens(rawMessage));
3718
+ messageTokenById.set(messageId, estimateAllMessageTokensFast(rawMessage));
3533
3719
  }
3534
3720
  const parts = Array.isArray(rawMessage.parts) ? rawMessage.parts : [];
3535
3721
  for (const part of parts) {
@@ -4506,6 +4692,24 @@ var SessionStateRegistry = class {
4506
4692
  hydrateModelLimitsFromClient(client) {
4507
4693
  return this.catalog.hydrateFromClient(client);
4508
4694
  }
4695
+ // [FIX #346] The init-time seed (above) is fire-and-forget and races
4696
+ // server readiness: in headless spawn+resume mode the provider-config
4697
+ // call can fail before the server is up, leaving the catalog empty for
4698
+ // the process's lifetime. During a request the server is guaranteed up
4699
+ // (we are inside its pipeline), so on a catalog miss we retry hydration
4700
+ // once per process before giving up (the fallback limit then applies).
4701
+ // The in-flight promise (not a boolean) lets concurrent callers await the
4702
+ // same hydration instead of skipping it.
4703
+ lazyHydration;
4704
+ async hydrateAndResolve(client, providerId, modelId) {
4705
+ const existing = this.catalog.resolve(providerId, modelId);
4706
+ if (existing !== void 0) {
4707
+ return existing;
4708
+ }
4709
+ this.lazyHydration ??= this.catalog.hydrateFromClient(client);
4710
+ await this.lazyHydration;
4711
+ return this.catalog.resolve(providerId, modelId);
4712
+ }
4509
4713
  get(sessionId) {
4510
4714
  return this.states.get(sessionId);
4511
4715
  }
@@ -4599,7 +4803,8 @@ function createSessionState() {
4599
4803
  modelID: void 0,
4600
4804
  systemPromptTokens: void 0,
4601
4805
  storageDir: void 0,
4602
- qualityGateRetryPending: false
4806
+ qualityGateRetryPending: false,
4807
+ noContextLimitWarned: false
4603
4808
  };
4604
4809
  }
4605
4810
  function resetSessionState(state) {
@@ -4642,6 +4847,7 @@ function resetSessionState(state) {
4642
4847
  state.systemPromptTokens = void 0;
4643
4848
  state.storageDir = void 0;
4644
4849
  state.qualityGateRetryPending = false;
4850
+ state.noContextLimitWarned = false;
4645
4851
  }
4646
4852
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
4647
4853
  if (state.sessionId === sessionId) {
@@ -6657,6 +6863,7 @@ function getModelInfo(messages) {
6657
6863
  };
6658
6864
  }
6659
6865
  function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
6866
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
6660
6867
  const parseLimitValue = (limit) => {
6661
6868
  if (limit === void 0) {
6662
6869
  return void 0;
@@ -6664,7 +6871,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6664
6871
  if (typeof limit === "number") {
6665
6872
  return limit;
6666
6873
  }
6667
- if (!limit.endsWith("%") || state.modelContextLimit === void 0) {
6874
+ if (!limit.endsWith("%") || effectiveLimit === void 0) {
6668
6875
  return void 0;
6669
6876
  }
6670
6877
  const parsedPercent = parseFloat(limit.slice(0, -1));
@@ -6673,7 +6880,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6673
6880
  }
6674
6881
  const roundedPercent = Math.round(parsedPercent);
6675
6882
  const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
6676
- return Math.round(clampedPercent / 100 * state.modelContextLimit);
6883
+ return Math.round(clampedPercent / 100 * effectiveLimit.limit);
6677
6884
  };
6678
6885
  if (threshold === "max") {
6679
6886
  const nestedLimit = resolveCompressOverrides(config, providerId, modelId).maxContextLimit;
@@ -6724,11 +6931,12 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
6724
6931
  if (!overMaxLimit) break;
6725
6932
  }
6726
6933
  }
6934
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
6727
6935
  return {
6728
6936
  overMaxLimit,
6729
6937
  overMinLimit,
6730
6938
  currentTokens,
6731
- modelContextLimit: state.modelContextLimit
6939
+ modelContextLimit: effectiveLimit?.limit
6732
6940
  };
6733
6941
  }
6734
6942
  ensureBuiltinTriggerPolicyRegistered();
@@ -6931,6 +7139,7 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
6931
7139
  let summaryTokens = 0;
6932
7140
  let messageTokens = 0;
6933
7141
  let protectedTokens = 0;
7142
+ let reasoningTokens = 0;
6934
7143
  const perMessage = [];
6935
7144
  const perTool = [];
6936
7145
  const perCode = [];
@@ -6984,6 +7193,10 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
6984
7193
  summaryTokens += summaryPartTokens;
6985
7194
  toolTypeMap.set(toolName, (toolTypeMap.get(toolName) || 0) + toolPartTokens);
6986
7195
  if (!msgToolName) msgToolName = toolName;
7196
+ } else if (part.type === "reasoning" && typeof part.text === "string") {
7197
+ const tokens = Math.round(part.text.length / 4);
7198
+ msgTotal += tokens;
7199
+ reasoningTokens += tokens;
6987
7200
  }
6988
7201
  }
6989
7202
  if (isProtected && !isSummary) {
@@ -7011,7 +7224,8 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
7011
7224
  textTokens: Math.max(0, messageTokens - codeTokens),
7012
7225
  systemTokens,
7013
7226
  protectedTokens,
7014
- total: systemTokens + toolTokens + summaryTokens + messageTokens,
7227
+ reasoningTokens,
7228
+ total: systemTokens + toolTokens + summaryTokens + messageTokens + reasoningTokens,
7015
7229
  largestRanges: perMessage.slice(0, 15),
7016
7230
  largestToolRanges: perTool.slice(0, 15),
7017
7231
  largestCodeRanges: perCode.slice(0, 5),
@@ -7034,13 +7248,10 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7034
7248
  if (!ref) continue;
7035
7249
  const rn = parseInt(ref.slice(1), 10);
7036
7250
  if ((protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns)) {
7037
- let tokens2 = 0;
7251
+ const tokens2 = Math.round(countMessageCharacters(msg) / 4);
7038
7252
  const tools = /* @__PURE__ */ new Set();
7039
7253
  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);
7254
+ if (part.type !== "text" && part.type !== "reasoning") {
7044
7255
  const toolName = part?.tool;
7045
7256
  const callID = part?.callID;
7046
7257
  if (toolName && callID) {
@@ -7061,15 +7272,13 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7061
7272
  protectedMsgInfo.push({ ref, refNum: rn, tokens: tokens2, tools: [...tools] });
7062
7273
  continue;
7063
7274
  }
7064
- let tokens = 0;
7275
+ const tokens = Math.round(countMessageCharacters(msg) / 4);
7065
7276
  let isTool = false;
7066
7277
  let hasMeaningfulPart = false;
7067
7278
  for (const part of msg.parts || []) {
7068
7279
  if (part.type === "text" && typeof part.text === "string") {
7069
- tokens += Math.round(part.text.length / 4);
7070
7280
  if (part.text.trim().length > 0) hasMeaningfulPart = true;
7071
7281
  } else if (part.type !== "text" && part.type !== "reasoning") {
7072
- tokens += Math.round(JSON.stringify(part).length / 4);
7073
7282
  isTool = true;
7074
7283
  hasMeaningfulPart = true;
7075
7284
  }
@@ -7713,6 +7922,23 @@ var syncCompressionBlocks = (state, logger, messages) => {
7713
7922
  return;
7714
7923
  }
7715
7924
  const messageIds = new Set(messages.map((msg) => msg.info.id));
7925
+ const structureVersion = messagesState.structureVersion ?? 0;
7926
+ if (messagesState.lastSyncedStructureVersion === structureVersion) {
7927
+ const activeBlocks = [...messagesState.activeBlockIds].map((id) => messagesState.blocksById.get(id)).filter((b) => b !== void 0).sort(sortBlocksByCreation);
7928
+ const nextAnchorMap = /* @__PURE__ */ new Map();
7929
+ for (const block of activeBlocks) {
7930
+ if (!messageIds.has(block.anchorMessageId)) continue;
7931
+ nextAnchorMap.set(block.anchorMessageId, block.blockId);
7932
+ }
7933
+ const previous = messagesState.activeByAnchorMessageId;
7934
+ if (previous.size !== nextAnchorMap.size || ![...nextAnchorMap.entries()].every(([anchor, id]) => previous.get(anchor) === id)) {
7935
+ previous.clear();
7936
+ for (const [anchor, id] of nextAnchorMap) {
7937
+ previous.set(anchor, id);
7938
+ }
7939
+ }
7940
+ return;
7941
+ }
7716
7942
  const previousActiveBlockIds = new Set(
7717
7943
  Array.from(messagesState.blocksById.values()).filter((block) => block.active).map((block) => block.blockId)
7718
7944
  );
@@ -7779,6 +8005,7 @@ var syncCompressionBlocks = (state, logger, messages) => {
7779
8005
  reactivatedCount
7780
8006
  });
7781
8007
  }
8008
+ messagesState.lastSyncedStructureVersion = structureVersion;
7782
8009
  };
7783
8010
 
7784
8011
  // lib/host-permissions.ts
@@ -7893,8 +8120,11 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7893
8120
  state.nudges.iterationNudgeAnchors.clear();
7894
8121
  state.nudges.lastNudgeShownTokens = void 0;
7895
8122
  state.nudges.lastToolOutputNudgeTokens = void 0;
7896
- state.nudges.lastTier2NudgeTokens = currentTokens;
7897
- state.nudges.lastTier3NudgeTokens = currentTokens;
8123
+ const captureOnly = isCaptureOnlyCompress(lastCompressMsg);
8124
+ if (!captureOnly) {
8125
+ state.nudges.lastTier2NudgeTokens = currentTokens;
8126
+ state.nudges.lastTier3NudgeTokens = currentTokens;
8127
+ }
7898
8128
  const currentTurnHasSuccessfulCompress = messages.slice(currentTurnStart).some((m) => m.info.role === "assistant" && messageHasCompress(m));
7899
8129
  if (currentTurnHasSuccessfulCompress && wasNudgeTriggered && !state.nudges.compressBaselineSet) {
7900
8130
  const baseline = state.nudges.lastPerMessageNudgeTokens;
@@ -8029,20 +8259,23 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8029
8259
  state.nudges.lastPerMessageNudgeTokens = currentTokens;
8030
8260
  baselineReEstablished = true;
8031
8261
  }
8032
- const composition = estimateContextComposition(
8262
+ const tierUsageEarly = getTierTokenUsage(state);
8263
+ const tierTriggerPossible = !!suffixMessage && (tierUsageEarly.tier1Tokens >= nudgeGrowthTokens || tierUsageEarly.tier2Tokens >= nudgeGrowthTokens);
8264
+ const needsNudgeAnalysis = nudgeAllowed || emergencyOverride || tierTriggerPossible;
8265
+ const composition = needsNudgeAnalysis ? estimateContextComposition(
8033
8266
  messages,
8034
8267
  state,
8035
8268
  config.compress.protectedTools,
8036
8269
  config.protectedFilePatterns
8037
- );
8038
- const protectedRefs = computeProtectedRefs(messages, state, config.compress);
8039
- const contextRanges = buildCompressibleRanges(
8270
+ ) : null;
8271
+ const protectedRefs = needsNudgeAnalysis ? computeProtectedRefs(messages, state, config.compress) : /* @__PURE__ */ new Set();
8272
+ const contextRanges = needsNudgeAnalysis ? buildCompressibleRanges(
8040
8273
  messages,
8041
8274
  state,
8042
8275
  config.compress.protectedTools,
8043
8276
  config.protectedFilePatterns,
8044
8277
  protectedRefs
8045
- );
8278
+ ) : { compressible: [], protected: [] };
8046
8279
  const unprotectedCompressible = excludeProtectedRanges(contextRanges.compressible, protectedRefs);
8047
8280
  const recommendedRanges = filterRecommendedRanges(
8048
8281
  unprotectedCompressible,
@@ -8067,7 +8300,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8067
8300
  baselineReEstablished = true;
8068
8301
  }
8069
8302
  if (suffixMessage && !shouldInject) {
8070
- const tierUsage = getTierTokenUsage(state);
8303
+ const tierUsage = tierUsageEarly;
8071
8304
  const tierChecks = [
8072
8305
  { triggerTier: 2, targetTier: 1, tokens: tierUsage.tier1Tokens, lastNudge: state.nudges.lastTier2NudgeTokens },
8073
8306
  { triggerTier: 3, targetTier: 2, tokens: tierUsage.tier2Tokens, lastNudge: state.nudges.lastTier3NudgeTokens }
@@ -8194,7 +8427,7 @@ ${rules}`;
8194
8427
  }
8195
8428
  let tipsText = null;
8196
8429
  if (shouldInject) {
8197
- if (suffixMessage && composition.total > 0) {
8430
+ if (suffixMessage && composition !== null && composition.total > 0) {
8198
8431
  const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
8199
8432
  const pct2 = (n) => n > 0 ? Math.max(1, Math.round(n / composition.total * 100)) : 0;
8200
8433
  const growth = currentTokens !== void 0 && (state.nudges.lastNudgeShownTokens ?? state.nudges.lastPerMessageNudgeTokens) !== void 0 ? currentTokens - (state.nudges.lastNudgeShownTokens ?? state.nudges.lastPerMessageNudgeTokens) : 0;
@@ -8206,7 +8439,7 @@ This is an efficiency nudge to compress early and keep context lean \u2014 not a
8206
8439
  ${COMPRESS_PHILOSOPHY}` : "";
8207
8440
  const sysPart = composition.systemTokens > 0 ? `${fmt(composition.systemTokens)} system (${pct2(composition.systemTokens)}%) | ` : "";
8208
8441
  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}`;
8442
+ 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
8443
  const compressibleTokens = composition.total - composition.systemTokens - composition.protectedTokens - composition.summaryTokens;
8211
8444
  if (composition.protectedTokens > 0) {
8212
8445
  breakdown += `
@@ -8569,6 +8802,9 @@ function deactivateCompressionTarget(messagesState, target, options) {
8569
8802
  }
8570
8803
  }
8571
8804
  }
8805
+ if (target.blocks.length > 0) {
8806
+ bumpPruneStructureVersion(messagesState);
8807
+ }
8572
8808
  }
8573
8809
  function computeRestoredMessages(messagesState, activeMessagesBefore) {
8574
8810
  let restoredMessageCount = 0;
@@ -8807,8 +9043,9 @@ function createDecompressTool(factoryCtx) {
8807
9043
  async execute(args, toolCtx) {
8808
9044
  const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
8809
9045
  const { rawMessages } = await prepareDecompressSession(ctx, toolCtx);
8810
- const contextUsageBefore = ctx.state.modelContextLimit ? Math.round(
8811
- getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
9046
+ const effectiveLimitBefore = resolveEffectiveContextLimit(ctx.state, ctx.config);
9047
+ const contextUsageBefore = effectiveLimitBefore ? Math.round(
9048
+ getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitBefore.limit * 100
8812
9049
  ) : void 0;
8813
9050
  const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
8814
9051
  if (!resolved.ok) {
@@ -8872,8 +9109,9 @@ function createDecompressTool(factoryCtx) {
8872
9109
  0,
8873
9110
  ctx.state.stats.totalPruneTokens - restoredTokens
8874
9111
  );
8875
- const contextUsageAfter = ctx.state.modelContextLimit ? Math.round(
8876
- getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
9112
+ const effectiveLimitAfter = resolveEffectiveContextLimit(ctx.state, ctx.config);
9113
+ const contextUsageAfter = effectiveLimitAfter ? Math.round(
9114
+ getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitAfter.limit * 100
8877
9115
  ) : void 0;
8878
9116
  await finalizeDecompressSession(ctx);
8879
9117
  const restoredContentPreview = buildRestoredContentPreview(
@@ -8948,21 +9186,29 @@ function rewriteCompressInput(part, liveKeys) {
8948
9186
  };
8949
9187
  }
8950
9188
  function hideConsumedCompressCalls(state, messages) {
8951
- const allBlockCallIds = /* @__PURE__ */ new Set();
8952
- const liveRangeKeysByCallId = /* @__PURE__ */ new Map();
8953
- const activeCallIds = /* @__PURE__ */ new Set();
8954
- for (const block of state.prune.messages.blocksById.values()) {
8955
- if (!block.compressCallId) continue;
8956
- allBlockCallIds.add(block.compressCallId);
8957
- if (!isLiveBlock(block)) continue;
8958
- activeCallIds.add(block.compressCallId);
8959
- let keys = liveRangeKeysByCallId.get(block.compressCallId);
8960
- if (!keys) {
8961
- keys = /* @__PURE__ */ new Set();
8962
- liveRangeKeysByCallId.set(block.compressCallId, keys);
8963
- }
8964
- keys.add(rangeKey(block.startId, block.endId));
8965
- }
9189
+ const messagesState = state.prune.messages;
9190
+ const version = messagesState.structureVersion ?? 0;
9191
+ let cached = messagesState.hideConsumedIndex;
9192
+ if (!cached || cached.version !== version) {
9193
+ const allBlockCallIds2 = /* @__PURE__ */ new Set();
9194
+ const liveRangeKeysByCallId2 = /* @__PURE__ */ new Map();
9195
+ const activeCallIds2 = /* @__PURE__ */ new Set();
9196
+ for (const block of messagesState.blocksById.values()) {
9197
+ if (!block.compressCallId) continue;
9198
+ allBlockCallIds2.add(block.compressCallId);
9199
+ if (!isLiveBlock(block)) continue;
9200
+ activeCallIds2.add(block.compressCallId);
9201
+ let keys = liveRangeKeysByCallId2.get(block.compressCallId);
9202
+ if (!keys) {
9203
+ keys = /* @__PURE__ */ new Set();
9204
+ liveRangeKeysByCallId2.set(block.compressCallId, keys);
9205
+ }
9206
+ keys.add(rangeKey(block.startId, block.endId));
9207
+ }
9208
+ cached = { version, allBlockCallIds: allBlockCallIds2, liveRangeKeysByCallId: liveRangeKeysByCallId2, activeCallIds: activeCallIds2 };
9209
+ messagesState.hideConsumedIndex = cached;
9210
+ }
9211
+ const { allBlockCallIds, liveRangeKeysByCallId, activeCallIds } = cached;
8966
9212
  const lastOrphanedCallIds = [];
8967
9213
  for (let i = messages.length - 1; i >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; i--) {
8968
9214
  const parts = Array.isArray(messages[i]?.parts) ? messages[i].parts : [];
@@ -9094,6 +9340,7 @@ function collectVisibleMessages(rawMessages, ctx) {
9094
9340
  const ref = byRawId.get(msgId);
9095
9341
  if (!ref) return;
9096
9342
  let tokens = 0;
9343
+ let reasoning = 0;
9097
9344
  let toolName = "";
9098
9345
  for (const part of msg.parts || []) {
9099
9346
  if (part.type === "text" && typeof part.text === "string") {
@@ -9104,10 +9351,12 @@ function collectVisibleMessages(rawMessages, ctx) {
9104
9351
  if (!toolName) {
9105
9352
  toolName = part?.tool || "unknown";
9106
9353
  }
9354
+ } else if (part.type === "reasoning" && typeof part.text === "string") {
9355
+ reasoning += Math.round(part.text.length / 4);
9107
9356
  }
9108
9357
  }
9109
- if (tokens > 0) {
9110
- result.push({ ref, tokens, tool: toolName || "text", index: idx });
9358
+ if (tokens > 0 || reasoning > 0) {
9359
+ result.push({ ref, tokens, tool: toolName || "text", index: idx, reasoning });
9111
9360
  }
9112
9361
  });
9113
9362
  return {
@@ -9129,14 +9378,16 @@ function renderOverview(visibleMessages, summaryTokens, systemTokens, blocks, fe
9129
9378
  } else {
9130
9379
  const totalTool = visibleMessages.filter((m) => m.tool !== "text" && m.tool !== "step-finish").reduce((s, m) => s + m.tokens, 0);
9131
9380
  const totalText = visibleMessages.filter((m) => m.tool === "text").reduce((s, m) => s + m.tokens, 0);
9132
- const total = systemTokens + totalTool + totalText + summaryTokens;
9381
+ const totalReasoning = visibleMessages.reduce((s, m) => s + m.reasoning, 0);
9382
+ const total = systemTokens + totalTool + totalText + summaryTokens + totalReasoning;
9133
9383
  const sysPct = pct(systemTokens, total);
9134
9384
  const toolPct = pct(totalTool, total);
9135
9385
  const textPct = pct(totalText, total);
9136
9386
  const summaryPct = pct(summaryTokens, total);
9387
+ const reasoningPct = pct(totalReasoning, total);
9137
9388
  lines.push("CONTEXT BREAKDOWN");
9138
9389
  lines.push(
9139
- ` ${formatTokens(systemTokens)} system (${sysPct}%) | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%)`
9390
+ ` ${formatTokens(systemTokens)} system (${sysPct}%) | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%) | ${formatTokens(totalReasoning)} reasoning (${reasoningPct}%)`
9140
9391
  );
9141
9392
  const topTypes = Array.from(toolTypeMap.entries()).map(([tool6, tokens]) => ({ tool: tool6, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
9142
9393
  if (topTypes.length > 0) {
@@ -9247,22 +9498,23 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
9247
9498
  if (toolFilter) {
9248
9499
  filtered = filtered.filter((m) => m.tool === toolFilter);
9249
9500
  }
9501
+ const sizeOf = (m) => m.tokens + m.reasoning;
9250
9502
  if (sort === "time") {
9251
9503
  filtered.sort((a, b) => a.index - b.index);
9252
9504
  } else if (sort === "tool") {
9253
- filtered.sort((a, b) => a.tool.localeCompare(b.tool) || b.tokens - a.tokens);
9505
+ filtered.sort((a, b) => a.tool.localeCompare(b.tool) || sizeOf(b) - sizeOf(a));
9254
9506
  } else {
9255
- filtered.sort((a, b) => b.tokens - a.tokens);
9507
+ filtered.sort((a, b) => sizeOf(b) - sizeOf(a));
9256
9508
  }
9257
- const totalTokens = filtered.reduce((s, m) => s + m.tokens, 0);
9258
- const allTokens = visibleMessages.reduce((s, m) => s + m.tokens, 0);
9509
+ const totalTokens = filtered.reduce((s, m) => s + sizeOf(m), 0);
9510
+ const allTokens = visibleMessages.reduce((s, m) => s + sizeOf(m), 0);
9259
9511
  const header = toolFilter ? `UNCOMPRESSED \u2014 ${toolFilter}: ${formatTokens(totalTokens)} | ${filtered.length} msgs | ${pct(totalTokens, allTokens)}% of visible` : `UNCOMPRESSED \u2014 ${formatTokens(totalTokens)} | ${filtered.length} msgs`;
9260
9512
  lines.push(header);
9261
9513
  lines.push(`Sorted by ${sort}`);
9262
9514
  lines.push("");
9263
9515
  const shown = filtered.slice(0, limit);
9264
9516
  for (const m of shown) {
9265
- lines.push(` ${m.ref} (${formatTokens(m.tokens)}) ${m.tool}`);
9517
+ lines.push(` ${m.ref} (${formatTokens(sizeOf(m))}) ${m.tool}`);
9266
9518
  }
9267
9519
  if (filtered.length > shown.length) {
9268
9520
  lines.push("");
@@ -9530,7 +9782,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9530
9782
  import { join as join4 } from "path";
9531
9783
  import { existsSync as existsSync4 } from "fs";
9532
9784
  import { homedir as homedir3 } from "os";
9533
- var LOG_VERSION = true ? "1.16.0" : "dev";
9785
+ var LOG_VERSION = true ? "1.17.0-pr.385.132" : "dev";
9534
9786
  var LEVEL_RANK = {
9535
9787
  debug: 10,
9536
9788
  info: 20,
@@ -9826,13 +10078,14 @@ CONTEXT BREAKDOWN
9826
10078
 
9827
10079
  When context usage passes a threshold, the system appends a breakdown showing where your context tokens are spent:
9828
10080
 
9829
- Breakdown: 5.2K system (21%) | 12.3K tool (40%) | 3.1K summaries (10%) | 8.5K code (28%) | 6.5K text (22%)
10081
+ 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
10082
 
9831
10083
  - "system" = system prompt tokens (AGENTS.md, tool definitions \u2014 not compressible)
9832
10084
  - "tool" = tool call outputs (largest category \u2014 compress first when consumed)
9833
10085
  - "summaries" = existing compression block summaries (already compressed; do not re-compress standalone)
9834
10086
  - "code" = messages containing code blocks
9835
10087
  - "text" = plain text messages
10088
+ - "reasoning" = model thinking blocks (counted to match real API usage; freed when their message is compressed)
9836
10089
 
9837
10090
  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
10091
 
@@ -10354,6 +10607,8 @@ var MIN_OUTPUT_TOKENS = 1e3;
10354
10607
  var KEEP_PREFIX_CHARS = 2e3;
10355
10608
  var KEEP_SUFFIX_CHARS = 2e3;
10356
10609
  var PROTECT_RECENT_MESSAGES = 3;
10610
+ var OUTPUT_RESERVE_TOKENS = 16384;
10611
+ var overheadErrorLogged = /* @__PURE__ */ new Set();
10357
10612
  function parseGcThreshold(threshold, modelContextLimit) {
10358
10613
  if (typeof threshold === "number") return threshold;
10359
10614
  const str = threshold ?? "100%";
@@ -10362,10 +10617,26 @@ function parseGcThreshold(threshold, modelContextLimit) {
10362
10617
  return modelContextLimit;
10363
10618
  }
10364
10619
  function truncateLargeToolOutputs(state, config, logger, messages) {
10365
- if (!state.modelContextLimit) return;
10620
+ const effective = resolveEffectiveContextLimit(state, config);
10621
+ if (!effective) return;
10366
10622
  const currentTokens = getCurrentTokenUsage(state, messages);
10367
10623
  if (currentTokens === 0) return;
10368
- const threshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, state.modelContextLimit);
10624
+ const configuredThreshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, effective.limit);
10625
+ const overhead = (state.systemPromptTokens ?? 0) + OUTPUT_RESERVE_TOKENS;
10626
+ const threshold = Math.min(configuredThreshold, effective.limit - overhead);
10627
+ if (threshold <= 0) {
10628
+ const sessionKey = state.sessionId ?? "unknown";
10629
+ if (!overheadErrorLogged.has(sessionKey)) {
10630
+ overheadErrorLogged.add(sessionKey);
10631
+ logger.error("ACP: model context window too small to fit overhead", {
10632
+ session: state.sessionId,
10633
+ limit: effective.limit,
10634
+ contextLimitSource: effective.source,
10635
+ overhead
10636
+ });
10637
+ }
10638
+ return;
10639
+ }
10369
10640
  if (currentTokens < threshold) return;
10370
10641
  const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES;
10371
10642
  const candidates = [];
@@ -10410,11 +10681,160 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
10410
10681
  truncatedCount,
10411
10682
  estimatedSavedTokens: Math.round(savedTokens),
10412
10683
  currentTokens,
10413
- threshold
10684
+ threshold,
10685
+ contextLimit: effective.limit,
10686
+ contextLimitSource: effective.source
10414
10687
  });
10415
10688
  }
10416
10689
  }
10417
10690
 
10691
+ // lib/messages/enforce-budget.ts
10692
+ var DEFAULT_COMPLETION_RESERVE_TOKENS = 32768;
10693
+ var TRUNCATION_MARKER2 = "[truncated for context space";
10694
+ var KEEP_PREFIX_CHARS2 = 2e3;
10695
+ var KEEP_SUFFIX_CHARS2 = 2e3;
10696
+ var PROTECT_RECENT_MESSAGES2 = 3;
10697
+ var MIN_CLEAR_TOKENS = 200;
10698
+ function resolveContextWindow(state) {
10699
+ if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
10700
+ return state.modelContextLimit;
10701
+ }
10702
+ return void 0;
10703
+ }
10704
+ function estimateWireTokens(state, messages) {
10705
+ const base = getCurrentTokenUsage(state, messages);
10706
+ if (base > 0) {
10707
+ let baseAssistant = -1;
10708
+ for (let i = messages.length - 1; i >= 0; i--) {
10709
+ if (messages[i].info.role !== "assistant") continue;
10710
+ const tokens = messages[i].info.tokens;
10711
+ if ((tokens?.input || 0) <= 0 && (tokens?.output || 0) <= 0) continue;
10712
+ baseAssistant = i;
10713
+ break;
10714
+ }
10715
+ if (baseAssistant >= 0) {
10716
+ let additions = 0;
10717
+ for (let i = baseAssistant + 1; i < messages.length; i++) {
10718
+ additions += countAllMessageTokens(messages[i]);
10719
+ }
10720
+ return base + additions;
10721
+ }
10722
+ }
10723
+ let total = 0;
10724
+ for (const m of messages) total += countAllMessageTokens(m);
10725
+ return total + (state.systemPromptTokens ?? 0);
10726
+ }
10727
+ function enforceContextBudget(state, config, logger, messages) {
10728
+ const window = resolveContextWindow(state);
10729
+ if (window === void 0) return void 0;
10730
+ const configuredReserve = config.compress?.completionReserveTokens;
10731
+ const reserve = typeof configuredReserve === "number" && Number.isFinite(configuredReserve) && configuredReserve >= 0 ? configuredReserve : DEFAULT_COMPLETION_RESERVE_TOKENS;
10732
+ const budget = window - reserve;
10733
+ if (budget <= 0) return void 0;
10734
+ const estimatedTokens = estimateWireTokens(state, messages);
10735
+ if (estimatedTokens <= budget) {
10736
+ return {
10737
+ applied: false,
10738
+ window,
10739
+ reserve,
10740
+ budget,
10741
+ estimatedTokens,
10742
+ finalEstimate: estimatedTokens,
10743
+ truncatedCount: 0,
10744
+ clearedCount: 0
10745
+ };
10746
+ }
10747
+ const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES2;
10748
+ const protectedTools = new Set(config.compress?.protectedTools ?? []);
10749
+ const candidates = [];
10750
+ for (let mi = 0; mi < protectedIndex; mi++) {
10751
+ if (mi === 0 && messages[mi].info.role === "user") continue;
10752
+ const msg = messages[mi];
10753
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
10754
+ for (const part of parts) {
10755
+ if (part?.type !== "tool") continue;
10756
+ if (part.state?.status !== "completed") continue;
10757
+ if (part.tool === "compress") continue;
10758
+ if (protectedTools.has(part.tool)) continue;
10759
+ const content = extractCompletedToolOutput(part);
10760
+ if (content === void 0) continue;
10761
+ if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10762
+ const tokens = countTokens2(content);
10763
+ if (tokens <= 0) continue;
10764
+ candidates.push({ part, content, tokens, index: mi });
10765
+ }
10766
+ }
10767
+ let saved = 0;
10768
+ let truncatedCount = 0;
10769
+ let clearedCount = 0;
10770
+ const truncatable = candidates.filter(
10771
+ (c) => !c.content.includes(TRUNCATION_MARKER2) && c.content.length > KEEP_PREFIX_CHARS2 + KEEP_SUFFIX_CHARS2
10772
+ ).sort((a, b) => b.tokens - a.tokens);
10773
+ for (const c of truncatable) {
10774
+ if (estimatedTokens - saved <= budget) break;
10775
+ const prefix = c.content.slice(0, KEEP_PREFIX_CHARS2);
10776
+ const suffix = c.content.slice(-KEEP_SUFFIX_CHARS2);
10777
+ const truncated = prefix + `
10778
+
10779
+ ...${TRUNCATION_MARKER2} \u2014 original ~${c.tokens} tokens]...
10780
+
10781
+ ` + suffix;
10782
+ if (truncated.length >= c.content.length) continue;
10783
+ c.part.state.output = truncated;
10784
+ saved += c.tokens - countTokens2(truncated);
10785
+ truncatedCount++;
10786
+ }
10787
+ if (estimatedTokens - saved > budget) {
10788
+ const clearable = candidates.filter((c) => {
10789
+ const out = extractCompletedToolOutput(c.part);
10790
+ return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens2(out) > MIN_CLEAR_TOKENS;
10791
+ }).sort((a, b) => a.index - b.index);
10792
+ for (const c of clearable) {
10793
+ if (estimatedTokens - saved <= budget) break;
10794
+ const current = extractCompletedToolOutput(c.part);
10795
+ if (current === void 0 || current === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10796
+ c.part.state.output = COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
10797
+ saved += countTokens2(current) - countTokens2(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
10798
+ clearedCount++;
10799
+ }
10800
+ }
10801
+ const finalEstimate = Math.max(0, estimatedTokens - saved);
10802
+ if (truncatedCount > 0 || clearedCount > 0) {
10803
+ logger.warn("Context budget guard: pruned tool outputs to fit the request", {
10804
+ session: state.sessionId,
10805
+ estimatedTokens: Math.round(estimatedTokens),
10806
+ budget,
10807
+ window,
10808
+ reserve,
10809
+ truncatedCount,
10810
+ clearedCount,
10811
+ estimatedSavedTokens: Math.round(saved),
10812
+ finalEstimate: Math.round(finalEstimate)
10813
+ });
10814
+ }
10815
+ if (finalEstimate > budget) {
10816
+ logger.warn(
10817
+ "Context budget guard: still over budget after pruning all compressible tool outputs; the request may be rejected by the model",
10818
+ {
10819
+ session: state.sessionId,
10820
+ finalEstimate: Math.round(finalEstimate),
10821
+ budget,
10822
+ window
10823
+ }
10824
+ );
10825
+ }
10826
+ return {
10827
+ applied: truncatedCount > 0 || clearedCount > 0,
10828
+ window,
10829
+ reserve,
10830
+ budget,
10831
+ estimatedTokens,
10832
+ finalEstimate,
10833
+ truncatedCount,
10834
+ clearedCount
10835
+ };
10836
+ }
10837
+
10418
10838
  // lib/commands/context.ts
10419
10839
  function analyzeTokens(state, messages) {
10420
10840
  const breakdown = {
@@ -11362,6 +11782,7 @@ function mergeMarkedBlocks(state, markedIds, maxMergedLength) {
11362
11782
  0
11363
11783
  );
11364
11784
  const savedTokens = Math.max(0, sourceTokens - newSummaryTokens);
11785
+ bumpPruneStructureVersion(messagesState);
11365
11786
  return { mergedCount: sourceBlocks.length, savedTokens };
11366
11787
  }
11367
11788
  function runBatchCleanup(state, config, logger, messages) {
@@ -11371,11 +11792,12 @@ function runBatchCleanup(state, config, logger, messages) {
11371
11792
  mergedCount: 0,
11372
11793
  savedTokens: 0
11373
11794
  };
11374
- if (!state.modelContextLimit || state.modelContextLimit <= 0) {
11795
+ const effective = resolveEffectiveContextLimit(state, config);
11796
+ if (!effective) {
11375
11797
  return noop;
11376
11798
  }
11377
11799
  const currentTokens = getCurrentTokenUsage(state, messages);
11378
- if (currentTokens < state.modelContextLimit) {
11800
+ if (currentTokens < effective.limit) {
11379
11801
  return noop;
11380
11802
  }
11381
11803
  const maxMergedLength = config.gc.maxOldGenSummaryLength;
@@ -11392,7 +11814,8 @@ function runBatchCleanup(state, config, logger, messages) {
11392
11814
  mergedCount: result.mergedCount,
11393
11815
  savedTokens: result.savedTokens,
11394
11816
  currentTokens,
11395
- contextLimit: state.modelContextLimit
11817
+ contextLimit: effective.limit,
11818
+ contextLimitSource: effective.source
11396
11819
  });
11397
11820
  return {
11398
11821
  tier: 3,
@@ -11426,11 +11849,6 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11426
11849
  input.model?.limit?.context
11427
11850
  );
11428
11851
  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
11852
  if (!state || state.isSubAgent && !config.allowSubAgents) {
11435
11853
  return;
11436
11854
  }
@@ -11439,6 +11857,23 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11439
11857
  logger.info("Skipping DCP system prompt injection for internal agent");
11440
11858
  return;
11441
11859
  }
11860
+ if (input.model?.limit?.context) {
11861
+ const limit = input.model.limit.context;
11862
+ const providerID = input.model?.providerID;
11863
+ const modelID = input.model?.id;
11864
+ const changed = state.modelContextLimit !== limit || providerID !== void 0 && state.modelProviderID !== providerID || modelID !== void 0 && state.modelID !== modelID;
11865
+ state.modelContextLimit = limit;
11866
+ if (providerID !== void 0) {
11867
+ state.modelProviderID = providerID;
11868
+ }
11869
+ if (modelID !== void 0) {
11870
+ state.modelID = modelID;
11871
+ }
11872
+ if (changed) {
11873
+ saveSessionState(state, logger).catch(() => {
11874
+ });
11875
+ }
11876
+ }
11442
11877
  const effectivePermission = compressPermission(state, config);
11443
11878
  if (effectivePermission === "deny") {
11444
11879
  return;
@@ -11483,10 +11918,17 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11483
11918
  config
11484
11919
  );
11485
11920
  const requestModel = lastUserMessage.info.model;
11486
- const requestModelLimit = registry4.resolveModelLimit(
11921
+ let requestModelLimit = registry4.resolveModelLimit(
11487
11922
  requestModel?.providerID,
11488
11923
  requestModel?.modelID
11489
11924
  );
11925
+ if (requestModelLimit === void 0 && requestModel?.providerID && requestModel?.modelID) {
11926
+ requestModelLimit = await registry4.hydrateAndResolve(
11927
+ client,
11928
+ requestModel.providerID,
11929
+ requestModel.modelID
11930
+ );
11931
+ }
11490
11932
  const prevModelID = state.modelID;
11491
11933
  if (requestModelLimit !== void 0) {
11492
11934
  state.modelContextLimit = requestModelLimit;
@@ -11516,6 +11958,16 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11516
11958
  });
11517
11959
  }
11518
11960
  await updatePerTurnState(state, logger, messages);
11961
+ if (state.modelContextLimit === void 0 && !state.noContextLimitWarned && requestModel?.providerID && requestModel?.modelID && registry4.resolveModelLimit(requestModel.providerID, requestModel.modelID) === void 0) {
11962
+ state.noContextLimitWarned = true;
11963
+ logger.warn(
11964
+ '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.',
11965
+ {
11966
+ session: state.sessionId,
11967
+ model: `${requestModel.providerID}/${requestModel.modelID}`
11968
+ }
11969
+ );
11970
+ }
11519
11971
  }
11520
11972
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
11521
11973
  if (state.isSubAgent && !config.allowSubAgents) {
@@ -11541,10 +11993,11 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11541
11993
  }
11542
11994
  }
11543
11995
  ensureBuiltinFiltersRegistered();
11996
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
11544
11997
  applyMessageFilters(output.messages, config.messageFilters, logger, {
11545
11998
  sessionId: state.sessionId ?? "",
11546
11999
  isSubAgent: state.isSubAgent,
11547
- modelContextLimit: state.modelContextLimit
12000
+ modelContextLimit: effectiveLimit?.limit
11548
12001
  });
11549
12002
  cacheSystemPromptTokens(state, output.messages);
11550
12003
  assignMessageRefs(state, output.messages);
@@ -11564,6 +12017,7 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11564
12017
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
11565
12018
  prune(state, logger, config, output.messages);
11566
12019
  truncateLargeToolOutputs(state, config, logger, output.messages);
12020
+ enforceContextBudget(state, config, logger, output.messages);
11567
12021
  hideConsumedCompressCalls(state, output.messages);
11568
12022
  assignMessageRefs(state, output.messages);
11569
12023
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
@@ -11595,14 +12049,31 @@ ${text}`);
11595
12049
  stripStaleMetadata(output.messages);
11596
12050
  dropEmptyMessages(output.messages);
11597
12051
  const postTokens = getCurrentTokenUsage(state, output.messages);
12052
+ if (postTokens !== void 0 && effectiveLimit) {
12053
+ const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
12054
+ if (postTokens > budget) {
12055
+ logger.error(
12056
+ "ACP hard guard: context exceeds model budget after in-flight reduction",
12057
+ {
12058
+ session: state.sessionId,
12059
+ postTokens,
12060
+ budget,
12061
+ contextLimit: effectiveLimit.limit,
12062
+ contextLimitSource: effectiveLimit.source,
12063
+ hint: "request will likely be rejected; run /compact or start a new session"
12064
+ }
12065
+ );
12066
+ }
12067
+ }
11598
12068
  logger.info("Chat transform complete", {
11599
12069
  session: state.sessionId,
11600
12070
  model: state.modelID,
11601
12071
  messages: output.messages.length,
11602
12072
  prePruneTokens,
11603
12073
  postTokens,
11604
- contextLimit: state.modelContextLimit,
11605
- usagePct: postTokens !== void 0 && state.modelContextLimit ? `${(postTokens / state.modelContextLimit * 100).toFixed(1)}%` : void 0,
12074
+ contextLimit: effectiveLimit?.limit,
12075
+ contextLimitSource: effectiveLimit?.source,
12076
+ usagePct: postTokens !== void 0 && effectiveLimit ? `${(postTokens / effectiveLimit.limit * 100).toFixed(1)}%` : void 0,
11606
12077
  nudged: state.nudges.shouldInjectThisTurn
11607
12078
  });
11608
12079
  if (state.sessionId) {
@@ -11634,12 +12105,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11634
12105
  path: { id: input.sessionID }
11635
12106
  });
11636
12107
  const messages = filterMessages(messagesResponse.data || messagesResponse);
11637
- const state = await registry4.getOrCreate(
11638
- client,
11639
- input.sessionID,
11640
- messages,
11641
- config
11642
- );
12108
+ const state = await registry4.getOrCreate(client, input.sessionID, messages, config);
11643
12109
  syncCompressPermissionState(state, config, hostPermissions, messages);
11644
12110
  const commandCtx = {
11645
12111
  client,
@@ -11653,7 +12119,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11653
12119
  const sub = input.arguments?.trim().toLowerCase();
11654
12120
  if (sub === "stats" || sub === "status" || sub === "") {
11655
12121
  await handleStatsCommand(commandCtx);
11656
- throw new Error("__DCP_CONTEXT_HANDLED__");
12122
+ return;
11657
12123
  }
11658
12124
  if (sub === "export" || sub.startsWith("export ")) {
11659
12125
  const exportArgs = input.arguments?.trim().slice("export".length).trim() || "";
@@ -11661,17 +12127,10 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11661
12127
  throw new Error("__DCP_CONTEXT_HANDLED__");
11662
12128
  }
11663
12129
  if (sub === "help") {
11664
- await sendIgnoredMessage(
11665
- client,
11666
- input.sessionID,
11667
- buildHelpText(),
11668
- {},
11669
- logger
11670
- );
12130
+ await sendIgnoredMessage(client, input.sessionID, buildHelpText(), {}, logger);
11671
12131
  throw new Error("__DCP_CONTEXT_HANDLED__");
11672
12132
  }
11673
12133
  await handleContextCommand(commandCtx);
11674
- throw new Error("__DCP_CONTEXT_HANDLED__");
11675
12134
  }
11676
12135
  };
11677
12136
  }
@@ -11742,9 +12201,7 @@ function createEventHandler(registry4, logger) {
11742
12201
  return;
11743
12202
  }
11744
12203
  if (typeof part.callID === "string" && typeof part.messageID === "string") {
11745
- timing.startsByCallId.delete(
11746
- buildCompressionTimingKey(part.messageID, part.callID)
11747
- );
12204
+ timing.startsByCallId.delete(buildCompressionTimingKey(part.messageID, part.callID));
11748
12205
  }
11749
12206
  };
11750
12207
  }
@@ -12021,7 +12478,7 @@ var server = (async (ctx) => {
12021
12478
  }
12022
12479
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
12023
12480
  logger.info("ACP plugin initialized", {
12024
- version: true ? "1.16.0" : "dev",
12481
+ version: true ? "1.17.0-pr.385.132" : "dev",
12025
12482
  workspace: ctx.directory,
12026
12483
  logLevel: logger.level,
12027
12484
  debug: config.debug,