opencode-acp 1.16.0-pr.350.123 → 1.16.0-pr.360.124

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,7 +890,6 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
890
890
  "compress.modelMaxLimits",
891
891
  "compress.modelMinLimits",
892
892
  "compress.providers",
893
- "compress.contextLimitFallback",
894
893
  "compress.nudgeFrequency",
895
894
  "compress.minNudgeContextPercent",
896
895
  "compress.nudgeGrowthTokens",
@@ -914,7 +913,6 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
914
913
  "compress.reasoning",
915
914
  "compress.reasoning.drop",
916
915
  "compress.reasoning.threshold",
917
- "compress.completionReserveTokens",
918
916
  "gc",
919
917
  "gc.algorithm",
920
918
  "gc.promotionThreshold",
@@ -966,11 +964,7 @@ function validateConfigTypes(config) {
966
964
  errors.push({ key: "storagePath", expected: "string", actual: typeof config.storagePath });
967
965
  }
968
966
  if (config.allowSubAgents !== void 0 && typeof config.allowSubAgents !== "boolean") {
969
- errors.push({
970
- key: "allowSubAgents",
971
- expected: "boolean",
972
- actual: typeof config.allowSubAgents
973
- });
967
+ errors.push({ key: "allowSubAgents", expected: "boolean", actual: typeof config.allowSubAgents });
974
968
  }
975
969
  if (config.pruneNotification !== void 0) {
976
970
  const validValues = ["off", "minimal", "detailed"];
@@ -1299,20 +1293,6 @@ function validateConfigTypes(config) {
1299
1293
  }
1300
1294
  }
1301
1295
  }
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
- }
1316
1296
  if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
1317
1297
  errors.push({
1318
1298
  key: "compress.iterationNudgeThreshold",
@@ -1431,20 +1411,12 @@ function validateConfigTypes(config) {
1431
1411
  break;
1432
1412
  case "nudgeForce":
1433
1413
  if (value !== "strong" && value !== "soft") {
1434
- errors.push({
1435
- key,
1436
- expected: "'strong' | 'soft'",
1437
- actual: JSON.stringify(value)
1438
- });
1414
+ errors.push({ key, expected: "'strong' | 'soft'", actual: JSON.stringify(value) });
1439
1415
  }
1440
1416
  break;
1441
1417
  case "stringArray":
1442
1418
  if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
1443
- errors.push({
1444
- key,
1445
- expected: "string[]",
1446
- actual: JSON.stringify(value)
1447
- });
1419
+ errors.push({ key, expected: "string[]", actual: JSON.stringify(value) });
1448
1420
  }
1449
1421
  break;
1450
1422
  case "reasoningConfig":
@@ -1479,11 +1451,7 @@ function validateConfigTypes(config) {
1479
1451
  return;
1480
1452
  }
1481
1453
  if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides)) {
1482
- errors.push({
1483
- key: prefix,
1484
- expected: "CompressModelOverrides",
1485
- actual: typeof overrides
1486
- });
1454
+ errors.push({ key: prefix, expected: "CompressModelOverrides", actual: typeof overrides });
1487
1455
  return;
1488
1456
  }
1489
1457
  const model = overrides;
@@ -1516,11 +1484,7 @@ function validateConfigTypes(config) {
1516
1484
  for (const [providerId, providerValue] of Object.entries(providers)) {
1517
1485
  const prefix = `compress.providers.${providerId}`;
1518
1486
  if (typeof providerValue !== "object" || providerValue === null || Array.isArray(providerValue)) {
1519
- errors.push({
1520
- key: prefix,
1521
- expected: "ProviderOverrides",
1522
- actual: typeof providerValue
1523
- });
1487
+ errors.push({ key: prefix, expected: "ProviderOverrides", actual: typeof providerValue });
1524
1488
  continue;
1525
1489
  }
1526
1490
  const provider = providerValue;
@@ -1557,20 +1521,6 @@ function validateConfigTypes(config) {
1557
1521
  }
1558
1522
  };
1559
1523
  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
- }
1574
1524
  const validValues = ["ask", "allow", "deny"];
1575
1525
  if (compress.permission !== void 0 && !validValues.includes(compress.permission)) {
1576
1526
  errors.push({
@@ -1656,22 +1606,13 @@ function validateConfigTypes(config) {
1656
1606
  });
1657
1607
  } else {
1658
1608
  if (gc.batchCleanup.lowThreshold !== void 0) {
1659
- validateBatchThreshold(
1660
- "gc.batchCleanup.lowThreshold",
1661
- gc.batchCleanup.lowThreshold
1662
- );
1609
+ validateBatchThreshold("gc.batchCleanup.lowThreshold", gc.batchCleanup.lowThreshold);
1663
1610
  }
1664
1611
  if (gc.batchCleanup.highThreshold !== void 0) {
1665
- validateBatchThreshold(
1666
- "gc.batchCleanup.highThreshold",
1667
- gc.batchCleanup.highThreshold
1668
- );
1612
+ validateBatchThreshold("gc.batchCleanup.highThreshold", gc.batchCleanup.highThreshold);
1669
1613
  }
1670
1614
  if (gc.batchCleanup.forceThreshold !== void 0) {
1671
- validateBatchThreshold(
1672
- "gc.batchCleanup.forceThreshold",
1673
- gc.batchCleanup.forceThreshold
1674
- );
1615
+ validateBatchThreshold("gc.batchCleanup.forceThreshold", gc.batchCleanup.forceThreshold);
1675
1616
  }
1676
1617
  }
1677
1618
  }
@@ -1761,7 +1702,6 @@ var defaultConfig = {
1761
1702
  summaryBuffer: true,
1762
1703
  maxContextLimit: "80%",
1763
1704
  minContextLimit: "80%",
1764
- contextLimitFallback: 128e3,
1765
1705
  nudgeFrequency: 5,
1766
1706
  minNudgeContextPercent: 5,
1767
1707
  iterationNudgeThreshold: 15,
@@ -1932,7 +1872,6 @@ function mergeCompress(base, override) {
1932
1872
  modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
1933
1873
  modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
1934
1874
  providers: mergeProviderOverrides(base.providers, override.providers),
1935
- contextLimitFallback: override.contextLimitFallback ?? base.contextLimitFallback,
1936
1875
  nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
1937
1876
  minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent,
1938
1877
  nudgeGrowthTokens: override.nudgeGrowthTokens,
@@ -1956,8 +1895,7 @@ function mergeCompress(base, override) {
1956
1895
  reasoning: {
1957
1896
  drop: override.reasoning?.drop ?? base.reasoning?.drop ?? DEFAULT_COMPRESS_REASONING.drop,
1958
1897
  threshold: override.reasoning?.threshold ?? base.reasoning?.threshold ?? DEFAULT_COMPRESS_REASONING.threshold
1959
- },
1960
- completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens
1898
+ }
1961
1899
  };
1962
1900
  }
1963
1901
  function mergeCommands(base, override) {
@@ -1995,9 +1933,10 @@ function deepCloneConfig(config) {
1995
1933
  ...provider,
1996
1934
  ...provider.models ? {
1997
1935
  models: Object.fromEntries(
1998
- Object.entries(provider.models).map(
1999
- ([modelId, model]) => [modelId, { ...model }]
2000
- )
1936
+ Object.entries(provider.models).map(([modelId, model]) => [
1937
+ modelId,
1938
+ { ...model }
1939
+ ])
2001
1940
  )
2002
1941
  } : {}
2003
1942
  }
@@ -2071,14 +2010,8 @@ function mergeLayer(config, data) {
2071
2010
  ],
2072
2011
  compress: mergeCompress(config.compress, data.compress),
2073
2012
  gc: mergeGC(config.gc, data.gc),
2074
- qualityGate: mergeQualityGate(
2075
- config.qualityGate,
2076
- data.qualityGate
2077
- ),
2078
- messageFilters: mergeMessageFilters(
2079
- config.messageFilters,
2080
- data.messageFilters
2081
- )
2013
+ qualityGate: mergeQualityGate(config.qualityGate, data.qualityGate),
2014
+ messageFilters: mergeMessageFilters(config.messageFilters, data.messageFilters)
2082
2015
  };
2083
2016
  }
2084
2017
  function scheduleParseWarning(ctx, title, message) {
@@ -2702,16 +2635,6 @@ function resetOnCompaction(state) {
2702
2635
  nextRef: 1
2703
2636
  };
2704
2637
  }
2705
- function resolveEffectiveContextLimit(state, config) {
2706
- if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
2707
- return { limit: state.modelContextLimit, source: "model" };
2708
- }
2709
- const fallback = config.compress.contextLimitFallback;
2710
- if (typeof fallback === "number" && fallback > 0) {
2711
- return { limit: fallback, source: "fallback" };
2712
- }
2713
- return void 0;
2714
- }
2715
2638
 
2716
2639
  // lib/state/persistence.ts
2717
2640
  function getDefaultStorageDir() {
@@ -4583,24 +4506,6 @@ var SessionStateRegistry = class {
4583
4506
  hydrateModelLimitsFromClient(client) {
4584
4507
  return this.catalog.hydrateFromClient(client);
4585
4508
  }
4586
- // [FIX #346] The init-time seed (above) is fire-and-forget and races
4587
- // server readiness: in headless spawn+resume mode the provider-config
4588
- // call can fail before the server is up, leaving the catalog empty for
4589
- // the process's lifetime. During a request the server is guaranteed up
4590
- // (we are inside its pipeline), so on a catalog miss we retry hydration
4591
- // once per process before giving up (the fallback limit then applies).
4592
- // The in-flight promise (not a boolean) lets concurrent callers await the
4593
- // same hydration instead of skipping it.
4594
- lazyHydration;
4595
- async hydrateAndResolve(client, providerId, modelId) {
4596
- const existing = this.catalog.resolve(providerId, modelId);
4597
- if (existing !== void 0) {
4598
- return existing;
4599
- }
4600
- this.lazyHydration ??= this.catalog.hydrateFromClient(client);
4601
- await this.lazyHydration;
4602
- return this.catalog.resolve(providerId, modelId);
4603
- }
4604
4509
  get(sessionId) {
4605
4510
  return this.states.get(sessionId);
4606
4511
  }
@@ -4694,8 +4599,7 @@ function createSessionState() {
4694
4599
  modelID: void 0,
4695
4600
  systemPromptTokens: void 0,
4696
4601
  storageDir: void 0,
4697
- qualityGateRetryPending: false,
4698
- noContextLimitWarned: false
4602
+ qualityGateRetryPending: false
4699
4603
  };
4700
4604
  }
4701
4605
  function resetSessionState(state) {
@@ -4738,7 +4642,6 @@ function resetSessionState(state) {
4738
4642
  state.systemPromptTokens = void 0;
4739
4643
  state.storageDir = void 0;
4740
4644
  state.qualityGateRetryPending = false;
4741
- state.noContextLimitWarned = false;
4742
4645
  }
4743
4646
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
4744
4647
  if (state.sessionId === sessionId) {
@@ -6754,7 +6657,6 @@ function getModelInfo(messages) {
6754
6657
  };
6755
6658
  }
6756
6659
  function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
6757
- const effectiveLimit = resolveEffectiveContextLimit(state, config);
6758
6660
  const parseLimitValue = (limit) => {
6759
6661
  if (limit === void 0) {
6760
6662
  return void 0;
@@ -6762,7 +6664,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6762
6664
  if (typeof limit === "number") {
6763
6665
  return limit;
6764
6666
  }
6765
- if (!limit.endsWith("%") || effectiveLimit === void 0) {
6667
+ if (!limit.endsWith("%") || state.modelContextLimit === void 0) {
6766
6668
  return void 0;
6767
6669
  }
6768
6670
  const parsedPercent = parseFloat(limit.slice(0, -1));
@@ -6771,7 +6673,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6771
6673
  }
6772
6674
  const roundedPercent = Math.round(parsedPercent);
6773
6675
  const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
6774
- return Math.round(clampedPercent / 100 * effectiveLimit.limit);
6676
+ return Math.round(clampedPercent / 100 * state.modelContextLimit);
6775
6677
  };
6776
6678
  if (threshold === "max") {
6777
6679
  const nestedLimit = resolveCompressOverrides(config, providerId, modelId).maxContextLimit;
@@ -6822,12 +6724,11 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
6822
6724
  if (!overMaxLimit) break;
6823
6725
  }
6824
6726
  }
6825
- const effectiveLimit = resolveEffectiveContextLimit(state, config);
6826
6727
  return {
6827
6728
  overMaxLimit,
6828
6729
  overMinLimit,
6829
6730
  currentTokens,
6830
- modelContextLimit: effectiveLimit?.limit
6731
+ modelContextLimit: state.modelContextLimit
6831
6732
  };
6832
6733
  }
6833
6734
  ensureBuiltinTriggerPolicyRegistered();
@@ -7133,13 +7034,10 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7133
7034
  if (!ref) continue;
7134
7035
  const rn = parseInt(ref.slice(1), 10);
7135
7036
  if ((protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns)) {
7136
- let tokens2 = 0;
7037
+ const tokens2 = Math.round(countMessageCharacters(msg) / 4);
7137
7038
  const tools = /* @__PURE__ */ new Set();
7138
7039
  for (const part of msg.parts || []) {
7139
- if (part.type === "text" && typeof part.text === "string") {
7140
- tokens2 += Math.round(part.text.length / 4);
7141
- } else if (part.type !== "text" && part.type !== "reasoning") {
7142
- tokens2 += Math.round(JSON.stringify(part).length / 4);
7040
+ if (part.type !== "text" && part.type !== "reasoning") {
7143
7041
  const toolName = part?.tool;
7144
7042
  const callID = part?.callID;
7145
7043
  if (toolName && callID) {
@@ -7160,15 +7058,13 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7160
7058
  protectedMsgInfo.push({ ref, refNum: rn, tokens: tokens2, tools: [...tools] });
7161
7059
  continue;
7162
7060
  }
7163
- let tokens = 0;
7061
+ const tokens = Math.round(countMessageCharacters(msg) / 4);
7164
7062
  let isTool = false;
7165
7063
  let hasMeaningfulPart = false;
7166
7064
  for (const part of msg.parts || []) {
7167
7065
  if (part.type === "text" && typeof part.text === "string") {
7168
- tokens += Math.round(part.text.length / 4);
7169
7066
  if (part.text.trim().length > 0) hasMeaningfulPart = true;
7170
7067
  } else if (part.type !== "text" && part.type !== "reasoning") {
7171
- tokens += Math.round(JSON.stringify(part).length / 4);
7172
7068
  isTool = true;
7173
7069
  hasMeaningfulPart = true;
7174
7070
  }
@@ -8906,9 +8802,8 @@ function createDecompressTool(factoryCtx) {
8906
8802
  async execute(args, toolCtx) {
8907
8803
  const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
8908
8804
  const { rawMessages } = await prepareDecompressSession(ctx, toolCtx);
8909
- const effectiveLimitBefore = resolveEffectiveContextLimit(ctx.state, ctx.config);
8910
- const contextUsageBefore = effectiveLimitBefore ? Math.round(
8911
- getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitBefore.limit * 100
8805
+ const contextUsageBefore = ctx.state.modelContextLimit ? Math.round(
8806
+ getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
8912
8807
  ) : void 0;
8913
8808
  const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
8914
8809
  if (!resolved.ok) {
@@ -8972,9 +8867,8 @@ function createDecompressTool(factoryCtx) {
8972
8867
  0,
8973
8868
  ctx.state.stats.totalPruneTokens - restoredTokens
8974
8869
  );
8975
- const effectiveLimitAfter = resolveEffectiveContextLimit(ctx.state, ctx.config);
8976
- const contextUsageAfter = effectiveLimitAfter ? Math.round(
8977
- getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitAfter.limit * 100
8870
+ const contextUsageAfter = ctx.state.modelContextLimit ? Math.round(
8871
+ getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
8978
8872
  ) : void 0;
8979
8873
  await finalizeDecompressSession(ctx);
8980
8874
  const restoredContentPreview = buildRestoredContentPreview(
@@ -9631,7 +9525,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9631
9525
  import { join as join4 } from "path";
9632
9526
  import { existsSync as existsSync4 } from "fs";
9633
9527
  import { homedir as homedir3 } from "os";
9634
- var LOG_VERSION = true ? "1.16.0-pr.350.123" : "dev";
9528
+ var LOG_VERSION = true ? "1.16.0-pr.360.124" : "dev";
9635
9529
  var LEVEL_RANK = {
9636
9530
  debug: 10,
9637
9531
  info: 20,
@@ -10455,8 +10349,6 @@ var MIN_OUTPUT_TOKENS = 1e3;
10455
10349
  var KEEP_PREFIX_CHARS = 2e3;
10456
10350
  var KEEP_SUFFIX_CHARS = 2e3;
10457
10351
  var PROTECT_RECENT_MESSAGES = 3;
10458
- var OUTPUT_RESERVE_TOKENS = 16384;
10459
- var overheadErrorLogged = /* @__PURE__ */ new Set();
10460
10352
  function parseGcThreshold(threshold, modelContextLimit) {
10461
10353
  if (typeof threshold === "number") return threshold;
10462
10354
  const str = threshold ?? "100%";
@@ -10465,26 +10357,10 @@ function parseGcThreshold(threshold, modelContextLimit) {
10465
10357
  return modelContextLimit;
10466
10358
  }
10467
10359
  function truncateLargeToolOutputs(state, config, logger, messages) {
10468
- const effective = resolveEffectiveContextLimit(state, config);
10469
- if (!effective) return;
10360
+ if (!state.modelContextLimit) return;
10470
10361
  const currentTokens = getCurrentTokenUsage(state, messages);
10471
10362
  if (currentTokens === 0) return;
10472
- const configuredThreshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, effective.limit);
10473
- const overhead = (state.systemPromptTokens ?? 0) + OUTPUT_RESERVE_TOKENS;
10474
- const threshold = Math.min(configuredThreshold, effective.limit - overhead);
10475
- if (threshold <= 0) {
10476
- const sessionKey = state.sessionId ?? "unknown";
10477
- if (!overheadErrorLogged.has(sessionKey)) {
10478
- overheadErrorLogged.add(sessionKey);
10479
- logger.error("ACP: model context window too small to fit overhead", {
10480
- session: state.sessionId,
10481
- limit: effective.limit,
10482
- contextLimitSource: effective.source,
10483
- overhead
10484
- });
10485
- }
10486
- return;
10487
- }
10363
+ const threshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, state.modelContextLimit);
10488
10364
  if (currentTokens < threshold) return;
10489
10365
  const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES;
10490
10366
  const candidates = [];
@@ -10529,160 +10405,11 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
10529
10405
  truncatedCount,
10530
10406
  estimatedSavedTokens: Math.round(savedTokens),
10531
10407
  currentTokens,
10532
- threshold,
10533
- contextLimit: effective.limit,
10534
- contextLimitSource: effective.source
10408
+ threshold
10535
10409
  });
10536
10410
  }
10537
10411
  }
10538
10412
 
10539
- // lib/messages/enforce-budget.ts
10540
- var DEFAULT_COMPLETION_RESERVE_TOKENS = 32768;
10541
- var TRUNCATION_MARKER2 = "[truncated for context space";
10542
- var KEEP_PREFIX_CHARS2 = 2e3;
10543
- var KEEP_SUFFIX_CHARS2 = 2e3;
10544
- var PROTECT_RECENT_MESSAGES2 = 3;
10545
- var MIN_CLEAR_TOKENS = 200;
10546
- function resolveContextWindow(state) {
10547
- if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
10548
- return state.modelContextLimit;
10549
- }
10550
- return void 0;
10551
- }
10552
- function estimateWireTokens(state, messages) {
10553
- const base = getCurrentTokenUsage(state, messages);
10554
- if (base > 0) {
10555
- let baseAssistant = -1;
10556
- for (let i = messages.length - 1; i >= 0; i--) {
10557
- if (messages[i].info.role !== "assistant") continue;
10558
- const tokens = messages[i].info.tokens;
10559
- if ((tokens?.input || 0) <= 0 && (tokens?.output || 0) <= 0) continue;
10560
- baseAssistant = i;
10561
- break;
10562
- }
10563
- if (baseAssistant >= 0) {
10564
- let additions = 0;
10565
- for (let i = baseAssistant + 1; i < messages.length; i++) {
10566
- additions += countAllMessageTokens(messages[i]);
10567
- }
10568
- return base + additions;
10569
- }
10570
- }
10571
- let total = 0;
10572
- for (const m of messages) total += countAllMessageTokens(m);
10573
- return total + (state.systemPromptTokens ?? 0);
10574
- }
10575
- function enforceContextBudget(state, config, logger, messages) {
10576
- const window = resolveContextWindow(state);
10577
- if (window === void 0) return void 0;
10578
- const configuredReserve = config.compress?.completionReserveTokens;
10579
- const reserve = typeof configuredReserve === "number" && Number.isFinite(configuredReserve) && configuredReserve >= 0 ? configuredReserve : DEFAULT_COMPLETION_RESERVE_TOKENS;
10580
- const budget = window - reserve;
10581
- if (budget <= 0) return void 0;
10582
- const estimatedTokens = estimateWireTokens(state, messages);
10583
- if (estimatedTokens <= budget) {
10584
- return {
10585
- applied: false,
10586
- window,
10587
- reserve,
10588
- budget,
10589
- estimatedTokens,
10590
- finalEstimate: estimatedTokens,
10591
- truncatedCount: 0,
10592
- clearedCount: 0
10593
- };
10594
- }
10595
- const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES2;
10596
- const protectedTools = new Set(config.compress?.protectedTools ?? []);
10597
- const candidates = [];
10598
- for (let mi = 0; mi < protectedIndex; mi++) {
10599
- if (mi === 0 && messages[mi].info.role === "user") continue;
10600
- const msg = messages[mi];
10601
- const parts = Array.isArray(msg.parts) ? msg.parts : [];
10602
- for (const part of parts) {
10603
- if (part?.type !== "tool") continue;
10604
- if (part.state?.status !== "completed") continue;
10605
- if (part.tool === "compress") continue;
10606
- if (protectedTools.has(part.tool)) continue;
10607
- const content = extractCompletedToolOutput(part);
10608
- if (content === void 0) continue;
10609
- if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10610
- const tokens = countTokens2(content);
10611
- if (tokens <= 0) continue;
10612
- candidates.push({ part, content, tokens, index: mi });
10613
- }
10614
- }
10615
- let saved = 0;
10616
- let truncatedCount = 0;
10617
- let clearedCount = 0;
10618
- const truncatable = candidates.filter(
10619
- (c) => !c.content.includes(TRUNCATION_MARKER2) && c.content.length > KEEP_PREFIX_CHARS2 + KEEP_SUFFIX_CHARS2
10620
- ).sort((a, b) => b.tokens - a.tokens);
10621
- for (const c of truncatable) {
10622
- if (estimatedTokens - saved <= budget) break;
10623
- const prefix = c.content.slice(0, KEEP_PREFIX_CHARS2);
10624
- const suffix = c.content.slice(-KEEP_SUFFIX_CHARS2);
10625
- const truncated = prefix + `
10626
-
10627
- ...${TRUNCATION_MARKER2} \u2014 original ~${c.tokens} tokens]...
10628
-
10629
- ` + suffix;
10630
- if (truncated.length >= c.content.length) continue;
10631
- c.part.state.output = truncated;
10632
- saved += c.tokens - countTokens2(truncated);
10633
- truncatedCount++;
10634
- }
10635
- if (estimatedTokens - saved > budget) {
10636
- const clearable = candidates.filter((c) => {
10637
- const out = extractCompletedToolOutput(c.part);
10638
- return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens2(out) > MIN_CLEAR_TOKENS;
10639
- }).sort((a, b) => a.index - b.index);
10640
- for (const c of clearable) {
10641
- if (estimatedTokens - saved <= budget) break;
10642
- const current = extractCompletedToolOutput(c.part);
10643
- if (current === void 0 || current === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10644
- c.part.state.output = COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
10645
- saved += countTokens2(current) - countTokens2(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
10646
- clearedCount++;
10647
- }
10648
- }
10649
- const finalEstimate = Math.max(0, estimatedTokens - saved);
10650
- if (truncatedCount > 0 || clearedCount > 0) {
10651
- logger.warn("Context budget guard: pruned tool outputs to fit the request", {
10652
- session: state.sessionId,
10653
- estimatedTokens: Math.round(estimatedTokens),
10654
- budget,
10655
- window,
10656
- reserve,
10657
- truncatedCount,
10658
- clearedCount,
10659
- estimatedSavedTokens: Math.round(saved),
10660
- finalEstimate: Math.round(finalEstimate)
10661
- });
10662
- }
10663
- if (finalEstimate > budget) {
10664
- logger.warn(
10665
- "Context budget guard: still over budget after pruning all compressible tool outputs; the request may be rejected by the model",
10666
- {
10667
- session: state.sessionId,
10668
- finalEstimate: Math.round(finalEstimate),
10669
- budget,
10670
- window
10671
- }
10672
- );
10673
- }
10674
- return {
10675
- applied: truncatedCount > 0 || clearedCount > 0,
10676
- window,
10677
- reserve,
10678
- budget,
10679
- estimatedTokens,
10680
- finalEstimate,
10681
- truncatedCount,
10682
- clearedCount
10683
- };
10684
- }
10685
-
10686
10413
  // lib/commands/context.ts
10687
10414
  function analyzeTokens(state, messages) {
10688
10415
  const breakdown = {
@@ -11639,12 +11366,11 @@ function runBatchCleanup(state, config, logger, messages) {
11639
11366
  mergedCount: 0,
11640
11367
  savedTokens: 0
11641
11368
  };
11642
- const effective = resolveEffectiveContextLimit(state, config);
11643
- if (!effective) {
11369
+ if (!state.modelContextLimit || state.modelContextLimit <= 0) {
11644
11370
  return noop;
11645
11371
  }
11646
11372
  const currentTokens = getCurrentTokenUsage(state, messages);
11647
- if (currentTokens < effective.limit) {
11373
+ if (currentTokens < state.modelContextLimit) {
11648
11374
  return noop;
11649
11375
  }
11650
11376
  const maxMergedLength = config.gc.maxOldGenSummaryLength;
@@ -11661,8 +11387,7 @@ function runBatchCleanup(state, config, logger, messages) {
11661
11387
  mergedCount: result.mergedCount,
11662
11388
  savedTokens: result.savedTokens,
11663
11389
  currentTokens,
11664
- contextLimit: effective.limit,
11665
- contextLimitSource: effective.source
11390
+ contextLimit: state.modelContextLimit
11666
11391
  });
11667
11392
  return {
11668
11393
  tier: 3,
@@ -11696,6 +11421,11 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11696
11421
  input.model?.limit?.context
11697
11422
  );
11698
11423
  const state = input.sessionID ? registry4.get(input.sessionID) : void 0;
11424
+ if (state && input.model?.limit?.context) {
11425
+ state.modelContextLimit = input.model.limit.context;
11426
+ state.modelProviderID = input.model?.providerID;
11427
+ state.modelID = input.model?.id;
11428
+ }
11699
11429
  if (!state || state.isSubAgent && !config.allowSubAgents) {
11700
11430
  return;
11701
11431
  }
@@ -11704,23 +11434,6 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11704
11434
  logger.info("Skipping DCP system prompt injection for internal agent");
11705
11435
  return;
11706
11436
  }
11707
- if (input.model?.limit?.context) {
11708
- const limit = input.model.limit.context;
11709
- const providerID = input.model?.providerID;
11710
- const modelID = input.model?.id;
11711
- const changed = state.modelContextLimit !== limit || providerID !== void 0 && state.modelProviderID !== providerID || modelID !== void 0 && state.modelID !== modelID;
11712
- state.modelContextLimit = limit;
11713
- if (providerID !== void 0) {
11714
- state.modelProviderID = providerID;
11715
- }
11716
- if (modelID !== void 0) {
11717
- state.modelID = modelID;
11718
- }
11719
- if (changed) {
11720
- saveSessionState(state, logger).catch(() => {
11721
- });
11722
- }
11723
- }
11724
11437
  const effectivePermission = compressPermission(state, config);
11725
11438
  if (effectivePermission === "deny") {
11726
11439
  return;
@@ -11765,17 +11478,10 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11765
11478
  config
11766
11479
  );
11767
11480
  const requestModel = lastUserMessage.info.model;
11768
- let requestModelLimit = registry4.resolveModelLimit(
11481
+ const requestModelLimit = registry4.resolveModelLimit(
11769
11482
  requestModel?.providerID,
11770
11483
  requestModel?.modelID
11771
11484
  );
11772
- if (requestModelLimit === void 0 && requestModel?.providerID && requestModel?.modelID) {
11773
- requestModelLimit = await registry4.hydrateAndResolve(
11774
- client,
11775
- requestModel.providerID,
11776
- requestModel.modelID
11777
- );
11778
- }
11779
11485
  const prevModelID = state.modelID;
11780
11486
  if (requestModelLimit !== void 0) {
11781
11487
  state.modelContextLimit = requestModelLimit;
@@ -11805,16 +11511,6 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11805
11511
  });
11806
11512
  }
11807
11513
  await updatePerTurnState(state, logger, messages);
11808
- if (state.modelContextLimit === void 0 && !state.noContextLimitWarned && requestModel?.providerID && requestModel?.modelID && registry4.resolveModelLimit(requestModel.providerID, requestModel.modelID) === void 0) {
11809
- state.noContextLimitWarned = true;
11810
- logger.warn(
11811
- '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.',
11812
- {
11813
- session: state.sessionId,
11814
- model: `${requestModel.providerID}/${requestModel.modelID}`
11815
- }
11816
- );
11817
- }
11818
11514
  }
11819
11515
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
11820
11516
  if (state.isSubAgent && !config.allowSubAgents) {
@@ -11840,11 +11536,10 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11840
11536
  }
11841
11537
  }
11842
11538
  ensureBuiltinFiltersRegistered();
11843
- const effectiveLimit = resolveEffectiveContextLimit(state, config);
11844
11539
  applyMessageFilters(output.messages, config.messageFilters, logger, {
11845
11540
  sessionId: state.sessionId ?? "",
11846
11541
  isSubAgent: state.isSubAgent,
11847
- modelContextLimit: effectiveLimit?.limit
11542
+ modelContextLimit: state.modelContextLimit
11848
11543
  });
11849
11544
  cacheSystemPromptTokens(state, output.messages);
11850
11545
  assignMessageRefs(state, output.messages);
@@ -11864,7 +11559,6 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11864
11559
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
11865
11560
  prune(state, logger, config, output.messages);
11866
11561
  truncateLargeToolOutputs(state, config, logger, output.messages);
11867
- enforceContextBudget(state, config, logger, output.messages);
11868
11562
  hideConsumedCompressCalls(state, output.messages);
11869
11563
  assignMessageRefs(state, output.messages);
11870
11564
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
@@ -11896,31 +11590,14 @@ ${text}`);
11896
11590
  stripStaleMetadata(output.messages);
11897
11591
  dropEmptyMessages(output.messages);
11898
11592
  const postTokens = getCurrentTokenUsage(state, output.messages);
11899
- if (postTokens !== void 0 && effectiveLimit) {
11900
- const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
11901
- if (postTokens > budget) {
11902
- logger.error(
11903
- "ACP hard guard: context exceeds model budget after in-flight reduction",
11904
- {
11905
- session: state.sessionId,
11906
- postTokens,
11907
- budget,
11908
- contextLimit: effectiveLimit.limit,
11909
- contextLimitSource: effectiveLimit.source,
11910
- hint: "request will likely be rejected; run /compact or start a new session"
11911
- }
11912
- );
11913
- }
11914
- }
11915
11593
  logger.info("Chat transform complete", {
11916
11594
  session: state.sessionId,
11917
11595
  model: state.modelID,
11918
11596
  messages: output.messages.length,
11919
11597
  prePruneTokens,
11920
11598
  postTokens,
11921
- contextLimit: effectiveLimit?.limit,
11922
- contextLimitSource: effectiveLimit?.source,
11923
- usagePct: postTokens !== void 0 && effectiveLimit ? `${(postTokens / effectiveLimit.limit * 100).toFixed(1)}%` : void 0,
11599
+ contextLimit: state.modelContextLimit,
11600
+ usagePct: postTokens !== void 0 && state.modelContextLimit ? `${(postTokens / state.modelContextLimit * 100).toFixed(1)}%` : void 0,
11924
11601
  nudged: state.nudges.shouldInjectThisTurn
11925
11602
  });
11926
11603
  if (state.sessionId) {
@@ -11952,7 +11629,12 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11952
11629
  path: { id: input.sessionID }
11953
11630
  });
11954
11631
  const messages = filterMessages(messagesResponse.data || messagesResponse);
11955
- const state = await registry4.getOrCreate(client, input.sessionID, messages, config);
11632
+ const state = await registry4.getOrCreate(
11633
+ client,
11634
+ input.sessionID,
11635
+ messages,
11636
+ config
11637
+ );
11956
11638
  syncCompressPermissionState(state, config, hostPermissions, messages);
11957
11639
  const commandCtx = {
11958
11640
  client,
@@ -11974,7 +11656,13 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11974
11656
  throw new Error("__DCP_CONTEXT_HANDLED__");
11975
11657
  }
11976
11658
  if (sub === "help") {
11977
- await sendIgnoredMessage(client, input.sessionID, buildHelpText(), {}, logger);
11659
+ await sendIgnoredMessage(
11660
+ client,
11661
+ input.sessionID,
11662
+ buildHelpText(),
11663
+ {},
11664
+ logger
11665
+ );
11978
11666
  throw new Error("__DCP_CONTEXT_HANDLED__");
11979
11667
  }
11980
11668
  await handleContextCommand(commandCtx);
@@ -12049,7 +11737,9 @@ function createEventHandler(registry4, logger) {
12049
11737
  return;
12050
11738
  }
12051
11739
  if (typeof part.callID === "string" && typeof part.messageID === "string") {
12052
- timing.startsByCallId.delete(buildCompressionTimingKey(part.messageID, part.callID));
11740
+ timing.startsByCallId.delete(
11741
+ buildCompressionTimingKey(part.messageID, part.callID)
11742
+ );
12053
11743
  }
12054
11744
  };
12055
11745
  }
@@ -12326,7 +12016,7 @@ var server = (async (ctx) => {
12326
12016
  }
12327
12017
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
12328
12018
  logger.info("ACP plugin initialized", {
12329
- version: true ? "1.16.0-pr.350.123" : "dev",
12019
+ version: true ? "1.16.0-pr.360.124" : "dev",
12330
12020
  workspace: ctx.directory,
12331
12021
  logLevel: logger.level,
12332
12022
  debug: config.debug,