opencode-acp 1.14.25-pr.349.71 → 1.14.25-pr.350.73

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
@@ -888,7 +888,6 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
888
888
  "compress.minContextLimit",
889
889
  "compress.modelMaxLimits",
890
890
  "compress.modelMinLimits",
891
- "compress.contextLimitFallback",
892
891
  "compress.nudgeFrequency",
893
892
  "compress.minNudgeContextPercent",
894
893
  "compress.nudgeGrowthTokens",
@@ -1312,20 +1311,6 @@ function validateConfigTypes(config) {
1312
1311
  }
1313
1312
  validateModelLimits("compress.modelMaxLimits", compress.modelMaxLimits);
1314
1313
  validateModelLimits("compress.modelMinLimits", compress.modelMinLimits);
1315
- if (compress.contextLimitFallback !== void 0 && typeof compress.contextLimitFallback !== "number") {
1316
- errors.push({
1317
- key: "compress.contextLimitFallback",
1318
- expected: "number",
1319
- actual: typeof compress.contextLimitFallback
1320
- });
1321
- }
1322
- if (typeof compress.contextLimitFallback === "number" && compress.contextLimitFallback < 0) {
1323
- errors.push({
1324
- key: "compress.contextLimitFallback",
1325
- expected: "non-negative number (0 disables the fallback)",
1326
- actual: `${compress.contextLimitFallback}`
1327
- });
1328
- }
1329
1314
  const validValues = ["ask", "allow", "deny"];
1330
1315
  if (compress.permission !== void 0 && !validValues.includes(compress.permission)) {
1331
1316
  errors.push({
@@ -1503,7 +1488,6 @@ var defaultConfig = {
1503
1488
  summaryBuffer: true,
1504
1489
  maxContextLimit: "80%",
1505
1490
  minContextLimit: "80%",
1506
- contextLimitFallback: 128e3,
1507
1491
  nudgeFrequency: 5,
1508
1492
  minNudgeContextPercent: 15,
1509
1493
  iterationNudgeThreshold: 15,
@@ -1639,7 +1623,6 @@ function mergeCompress(base, override) {
1639
1623
  minContextLimit: override.minContextLimit ?? base.minContextLimit,
1640
1624
  modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
1641
1625
  modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
1642
- contextLimitFallback: override.contextLimitFallback ?? base.contextLimitFallback,
1643
1626
  nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
1644
1627
  minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent,
1645
1628
  nudgeGrowthTokens: override.nudgeGrowthTokens,
@@ -1659,7 +1642,8 @@ function mergeCompress(base, override) {
1659
1642
  lastSegmentSoftBlock: override.lastSegmentSoftBlock ?? base.lastSegmentSoftBlock,
1660
1643
  preserveRecentMessages: override.preserveRecentMessages ?? base.preserveRecentMessages,
1661
1644
  preserveRecentTokens: override.preserveRecentTokens ?? base.preserveRecentTokens,
1662
- preserveLastUserMessage: override.preserveLastUserMessage ?? base.preserveLastUserMessage
1645
+ preserveLastUserMessage: override.preserveLastUserMessage ?? base.preserveLastUserMessage,
1646
+ completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens
1663
1647
  };
1664
1648
  }
1665
1649
  function mergeCommands(base, override) {
@@ -2382,16 +2366,6 @@ function resetOnCompaction(state) {
2382
2366
  nextRef: 1
2383
2367
  };
2384
2368
  }
2385
- function resolveEffectiveContextLimit(state, config) {
2386
- if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
2387
- return { limit: state.modelContextLimit, source: "model" };
2388
- }
2389
- const fallback = config.compress.contextLimitFallback;
2390
- if (typeof fallback === "number" && fallback > 0) {
2391
- return { limit: fallback, source: "fallback" };
2392
- }
2393
- return void 0;
2394
- }
2395
2369
 
2396
2370
  // lib/state/persistence.ts
2397
2371
  function getStorageDir() {
@@ -4237,24 +4211,6 @@ var SessionStateRegistry = class {
4237
4211
  hydrateModelLimitsFromClient(client) {
4238
4212
  return this.catalog.hydrateFromClient(client);
4239
4213
  }
4240
- // [FIX #346] The init-time seed (above) is fire-and-forget and races
4241
- // server readiness: in headless spawn+resume mode the provider-config
4242
- // call can fail before the server is up, leaving the catalog empty for
4243
- // the process's lifetime. During a request the server is guaranteed up
4244
- // (we are inside its pipeline), so on a catalog miss we retry hydration
4245
- // once per process before giving up (the fallback limit then applies).
4246
- lazyHydrated = false;
4247
- async hydrateAndResolve(client, providerId, modelId) {
4248
- const existing = this.catalog.resolve(providerId, modelId);
4249
- if (existing !== void 0) {
4250
- return existing;
4251
- }
4252
- if (!this.lazyHydrated) {
4253
- this.lazyHydrated = true;
4254
- await this.catalog.hydrateFromClient(client);
4255
- }
4256
- return this.catalog.resolve(providerId, modelId);
4257
- }
4258
4214
  get(sessionId) {
4259
4215
  return this.states.get(sessionId);
4260
4216
  }
@@ -4346,7 +4302,8 @@ function createSessionState() {
4346
4302
  modelProviderID: void 0,
4347
4303
  modelID: void 0,
4348
4304
  systemPromptTokens: void 0,
4349
- qualityGateRetryPending: false
4305
+ qualityGateRetryPending: false,
4306
+ noContextLimitWarned: false
4350
4307
  };
4351
4308
  }
4352
4309
  function resetSessionState(state) {
@@ -4388,6 +4345,7 @@ function resetSessionState(state) {
4388
4345
  state.modelID = void 0;
4389
4346
  state.systemPromptTokens = void 0;
4390
4347
  state.qualityGateRetryPending = false;
4348
+ state.noContextLimitWarned = false;
4391
4349
  }
4392
4350
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, config) {
4393
4351
  if (state.sessionId === sessionId) {
@@ -6925,7 +6883,6 @@ function getModelInfo(messages) {
6925
6883
  };
6926
6884
  }
6927
6885
  function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
6928
- const effectiveLimit = resolveEffectiveContextLimit(state, config);
6929
6886
  const parseLimitValue = (limit) => {
6930
6887
  if (limit === void 0) {
6931
6888
  return void 0;
@@ -6933,7 +6890,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6933
6890
  if (typeof limit === "number") {
6934
6891
  return limit;
6935
6892
  }
6936
- if (!limit.endsWith("%") || effectiveLimit === void 0) {
6893
+ if (!limit.endsWith("%") || state.modelContextLimit === void 0) {
6937
6894
  return void 0;
6938
6895
  }
6939
6896
  const parsedPercent = parseFloat(limit.slice(0, -1));
@@ -6942,7 +6899,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6942
6899
  }
6943
6900
  const roundedPercent = Math.round(parsedPercent);
6944
6901
  const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
6945
- return Math.round(clampedPercent / 100 * effectiveLimit.limit);
6902
+ return Math.round(clampedPercent / 100 * state.modelContextLimit);
6946
6903
  };
6947
6904
  const modelLimits = threshold === "max" ? config.compress.modelMaxLimits : config.compress.modelMinLimits;
6948
6905
  if (modelLimits && providerId !== void 0 && modelId !== void 0) {
@@ -6987,12 +6944,11 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
6987
6944
  if (!overMaxLimit) break;
6988
6945
  }
6989
6946
  }
6990
- const effectiveLimit = resolveEffectiveContextLimit(state, config);
6991
6947
  return {
6992
6948
  overMaxLimit,
6993
6949
  overMinLimit,
6994
6950
  currentTokens,
6995
- modelContextLimit: effectiveLimit?.limit
6951
+ modelContextLimit: state.modelContextLimit
6996
6952
  };
6997
6953
  }
6998
6954
  ensureBuiltinTriggerPolicyRegistered();
@@ -8434,9 +8390,8 @@ function createDecompressTool(factoryCtx) {
8434
8390
  async execute(args, toolCtx) {
8435
8391
  const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
8436
8392
  const { rawMessages } = await prepareDecompressSession(ctx, toolCtx);
8437
- const effectiveLimitBefore = resolveEffectiveContextLimit(ctx.state, ctx.config);
8438
- const contextUsageBefore = effectiveLimitBefore ? Math.round(
8439
- getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitBefore.limit * 100
8393
+ const contextUsageBefore = ctx.state.modelContextLimit ? Math.round(
8394
+ getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
8440
8395
  ) : void 0;
8441
8396
  const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
8442
8397
  if (!resolved.ok) {
@@ -8500,9 +8455,8 @@ function createDecompressTool(factoryCtx) {
8500
8455
  0,
8501
8456
  ctx.state.stats.totalPruneTokens - restoredTokens
8502
8457
  );
8503
- const effectiveLimitAfter = resolveEffectiveContextLimit(ctx.state, ctx.config);
8504
- const contextUsageAfter = effectiveLimitAfter ? Math.round(
8505
- getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitAfter.limit * 100
8458
+ const contextUsageAfter = ctx.state.modelContextLimit ? Math.round(
8459
+ getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
8506
8460
  ) : void 0;
8507
8461
  await finalizeDecompressSession(ctx);
8508
8462
  const restoredContentPreview = buildRestoredContentPreview(
@@ -9159,7 +9113,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9159
9113
  import { join as join3 } from "path";
9160
9114
  import { existsSync as existsSync3 } from "fs";
9161
9115
  import { homedir as homedir3 } from "os";
9162
- var LOG_VERSION = true ? "1.14.25-pr.349.71" : "dev";
9116
+ var LOG_VERSION = true ? "1.14.25-pr.350.73" : "dev";
9163
9117
  var LEVEL_RANK = {
9164
9118
  debug: 10,
9165
9119
  info: 20,
@@ -9983,7 +9937,6 @@ var MIN_OUTPUT_TOKENS = 1e3;
9983
9937
  var KEEP_PREFIX_CHARS = 2e3;
9984
9938
  var KEEP_SUFFIX_CHARS = 2e3;
9985
9939
  var PROTECT_RECENT_MESSAGES = 3;
9986
- var OUTPUT_RESERVE_TOKENS = 16384;
9987
9940
  function parseGcThreshold(threshold, modelContextLimit) {
9988
9941
  if (typeof threshold === "number") return threshold;
9989
9942
  const str = threshold ?? "100%";
@@ -9992,22 +9945,10 @@ function parseGcThreshold(threshold, modelContextLimit) {
9992
9945
  return modelContextLimit;
9993
9946
  }
9994
9947
  function truncateLargeToolOutputs(state, config, logger, messages) {
9995
- const effective = resolveEffectiveContextLimit(state, config);
9996
- if (!effective) return;
9948
+ if (!state.modelContextLimit) return;
9997
9949
  const currentTokens = getCurrentTokenUsage(state, messages);
9998
9950
  if (currentTokens === 0) return;
9999
- const configuredThreshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, effective.limit);
10000
- const overhead = (state.systemPromptTokens ?? 0) + OUTPUT_RESERVE_TOKENS;
10001
- const threshold = Math.min(configuredThreshold, effective.limit - overhead);
10002
- if (threshold <= 0) {
10003
- logger.error("ACP: model context window too small to fit overhead", {
10004
- session: state.sessionId,
10005
- limit: effective.limit,
10006
- contextLimitSource: effective.source,
10007
- overhead
10008
- });
10009
- return;
10010
- }
9951
+ const threshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, state.modelContextLimit);
10011
9952
  if (currentTokens < threshold) return;
10012
9953
  const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES;
10013
9954
  const candidates = [];
@@ -10052,13 +9993,153 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
10052
9993
  truncatedCount,
10053
9994
  estimatedSavedTokens: Math.round(savedTokens),
10054
9995
  currentTokens,
10055
- threshold,
10056
- contextLimit: effective.limit,
10057
- contextLimitSource: effective.source
9996
+ threshold
10058
9997
  });
10059
9998
  }
10060
9999
  }
10061
10000
 
10001
+ // lib/messages/enforce-budget.ts
10002
+ var DEFAULT_COMPLETION_RESERVE_TOKENS = 32768;
10003
+ var TRUNCATION_MARKER2 = "[truncated for context space";
10004
+ var KEEP_PREFIX_CHARS2 = 2e3;
10005
+ var KEEP_SUFFIX_CHARS2 = 2e3;
10006
+ var PROTECT_RECENT_MESSAGES2 = 3;
10007
+ var MIN_CLEAR_TOKENS = 200;
10008
+ function resolveContextWindow(state) {
10009
+ if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
10010
+ return state.modelContextLimit;
10011
+ }
10012
+ return void 0;
10013
+ }
10014
+ function estimateWireTokens(state, messages) {
10015
+ const base = getCurrentTokenUsage(state, messages);
10016
+ if (base > 0) {
10017
+ let lastAssistant = -1;
10018
+ for (let i = messages.length - 1; i >= 0; i--) {
10019
+ if (messages[i].info.role === "assistant") {
10020
+ lastAssistant = i;
10021
+ break;
10022
+ }
10023
+ }
10024
+ let additions = 0;
10025
+ for (let i = lastAssistant + 1; i < messages.length; i++) {
10026
+ additions += countAllMessageTokens(messages[i]);
10027
+ }
10028
+ return base + additions;
10029
+ }
10030
+ let total = 0;
10031
+ for (const m of messages) total += countAllMessageTokens(m);
10032
+ return total + (state.systemPromptTokens ?? 0);
10033
+ }
10034
+ function enforceContextBudget(state, config, logger, messages) {
10035
+ const window = resolveContextWindow(state);
10036
+ if (window === void 0) return void 0;
10037
+ const reserve = config.compress?.completionReserveTokens ?? DEFAULT_COMPLETION_RESERVE_TOKENS;
10038
+ const budget = window - reserve;
10039
+ if (budget <= 0) return void 0;
10040
+ const estimatedTokens = estimateWireTokens(state, messages);
10041
+ if (estimatedTokens <= budget) {
10042
+ return {
10043
+ applied: false,
10044
+ window,
10045
+ reserve,
10046
+ budget,
10047
+ estimatedTokens,
10048
+ finalEstimate: estimatedTokens,
10049
+ truncatedCount: 0,
10050
+ clearedCount: 0
10051
+ };
10052
+ }
10053
+ const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES2;
10054
+ const protectedTools = new Set(config.compress?.protectedTools ?? []);
10055
+ const candidates = [];
10056
+ for (let mi = 0; mi < protectedIndex; mi++) {
10057
+ if (mi === 0 && messages[mi].info.role === "user") continue;
10058
+ const msg = messages[mi];
10059
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
10060
+ for (const part of parts) {
10061
+ if (part?.type !== "tool") continue;
10062
+ if (part.state?.status !== "completed") continue;
10063
+ if (part.tool === "compress") continue;
10064
+ if (protectedTools.has(part.tool)) continue;
10065
+ const content = extractCompletedToolOutput(part);
10066
+ if (content === void 0) continue;
10067
+ if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10068
+ const tokens = countTokens2(content);
10069
+ if (tokens <= 0) continue;
10070
+ candidates.push({ part, content, tokens, index: mi });
10071
+ }
10072
+ }
10073
+ let saved = 0;
10074
+ let truncatedCount = 0;
10075
+ let clearedCount = 0;
10076
+ const truncatable = candidates.filter(
10077
+ (c) => !c.content.includes(TRUNCATION_MARKER2) && c.content.length > KEEP_PREFIX_CHARS2 + KEEP_SUFFIX_CHARS2
10078
+ ).sort((a, b) => b.tokens - a.tokens);
10079
+ for (const c of truncatable) {
10080
+ if (estimatedTokens - saved <= budget) break;
10081
+ const prefix = c.content.slice(0, KEEP_PREFIX_CHARS2);
10082
+ const suffix = c.content.slice(-KEEP_SUFFIX_CHARS2);
10083
+ const truncated = prefix + `
10084
+
10085
+ ...${TRUNCATION_MARKER2} \u2014 original ~${c.tokens} tokens]...
10086
+
10087
+ ` + suffix;
10088
+ c.part.state.output = truncated;
10089
+ saved += c.tokens - countTokens2(truncated);
10090
+ truncatedCount++;
10091
+ }
10092
+ if (estimatedTokens - saved > budget) {
10093
+ const clearable = candidates.filter((c) => {
10094
+ const out = extractCompletedToolOutput(c.part);
10095
+ return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens2(out) > MIN_CLEAR_TOKENS;
10096
+ }).sort((a, b) => a.index - b.index);
10097
+ for (const c of clearable) {
10098
+ if (estimatedTokens - saved <= budget) break;
10099
+ const current = extractCompletedToolOutput(c.part);
10100
+ if (current === void 0 || current === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10101
+ c.part.state.output = COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
10102
+ saved += countTokens2(current) - countTokens2(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
10103
+ clearedCount++;
10104
+ }
10105
+ }
10106
+ const finalEstimate = Math.max(0, estimatedTokens - saved);
10107
+ if (truncatedCount > 0 || clearedCount > 0) {
10108
+ logger.warn("Context budget guard: pruned tool outputs to fit the request", {
10109
+ session: state.sessionId,
10110
+ estimatedTokens: Math.round(estimatedTokens),
10111
+ budget,
10112
+ window,
10113
+ reserve,
10114
+ truncatedCount,
10115
+ clearedCount,
10116
+ estimatedSavedTokens: Math.round(saved),
10117
+ finalEstimate: Math.round(finalEstimate)
10118
+ });
10119
+ }
10120
+ if (finalEstimate > budget) {
10121
+ logger.warn(
10122
+ "Context budget guard: still over budget after pruning all compressible tool outputs; the request may be rejected by the model",
10123
+ {
10124
+ session: state.sessionId,
10125
+ finalEstimate: Math.round(finalEstimate),
10126
+ budget,
10127
+ window
10128
+ }
10129
+ );
10130
+ }
10131
+ return {
10132
+ applied: truncatedCount > 0 || clearedCount > 0,
10133
+ window,
10134
+ reserve,
10135
+ budget,
10136
+ estimatedTokens,
10137
+ finalEstimate,
10138
+ truncatedCount,
10139
+ clearedCount
10140
+ };
10141
+ }
10142
+
10062
10143
  // lib/commands/context.ts
10063
10144
  function analyzeTokens(state, messages) {
10064
10145
  const breakdown = {
@@ -11015,12 +11096,11 @@ function runBatchCleanup(state, config, logger, messages) {
11015
11096
  mergedCount: 0,
11016
11097
  savedTokens: 0
11017
11098
  };
11018
- const effective = resolveEffectiveContextLimit(state, config);
11019
- if (!effective) {
11099
+ if (!state.modelContextLimit || state.modelContextLimit <= 0) {
11020
11100
  return noop;
11021
11101
  }
11022
11102
  const currentTokens = getCurrentTokenUsage(state, messages);
11023
- if (currentTokens < effective.limit) {
11103
+ if (currentTokens < state.modelContextLimit) {
11024
11104
  return noop;
11025
11105
  }
11026
11106
  const maxMergedLength = config.gc.maxOldGenSummaryLength;
@@ -11037,8 +11117,7 @@ function runBatchCleanup(state, config, logger, messages) {
11037
11117
  mergedCount: result.mergedCount,
11038
11118
  savedTokens: result.savedTokens,
11039
11119
  currentTokens,
11040
- contextLimit: effective.limit,
11041
- contextLimitSource: effective.source
11120
+ contextLimit: state.modelContextLimit
11042
11121
  });
11043
11122
  return {
11044
11123
  tier: 3,
@@ -11072,6 +11151,11 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11072
11151
  input.model?.limit?.context
11073
11152
  );
11074
11153
  const state = input.sessionID ? registry4.get(input.sessionID) : void 0;
11154
+ if (state && input.model?.limit?.context) {
11155
+ state.modelContextLimit = input.model.limit.context;
11156
+ state.modelProviderID = input.model?.providerID;
11157
+ state.modelID = input.model?.id;
11158
+ }
11075
11159
  if (!state || state.isSubAgent && !config.allowSubAgents) {
11076
11160
  return;
11077
11161
  }
@@ -11080,19 +11164,6 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11080
11164
  logger.info("Skipping DCP system prompt injection for internal agent");
11081
11165
  return;
11082
11166
  }
11083
- if (input.model?.limit?.context) {
11084
- const limit = input.model.limit.context;
11085
- const providerID = input.model?.providerID;
11086
- const modelID = input.model?.id;
11087
- const changed = state.modelContextLimit !== limit || state.modelProviderID !== providerID || state.modelID !== modelID;
11088
- state.modelContextLimit = limit;
11089
- state.modelProviderID = providerID;
11090
- state.modelID = modelID;
11091
- if (changed) {
11092
- saveSessionState(state, logger).catch(() => {
11093
- });
11094
- }
11095
- }
11096
11167
  const effectivePermission = compressPermission(state, config);
11097
11168
  if (effectivePermission === "deny") {
11098
11169
  return;
@@ -11137,17 +11208,10 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11137
11208
  config
11138
11209
  );
11139
11210
  const requestModel = lastUserMessage.info.model;
11140
- let requestModelLimit = registry4.resolveModelLimit(
11211
+ const requestModelLimit = registry4.resolveModelLimit(
11141
11212
  requestModel?.providerID,
11142
11213
  requestModel?.modelID
11143
11214
  );
11144
- if (requestModelLimit === void 0 && requestModel?.providerID && requestModel?.modelID) {
11145
- requestModelLimit = await registry4.hydrateAndResolve(
11146
- client,
11147
- requestModel.providerID,
11148
- requestModel.modelID
11149
- );
11150
- }
11151
11215
  const prevModelID = state.modelID;
11152
11216
  if (requestModelLimit !== void 0) {
11153
11217
  state.modelContextLimit = requestModelLimit;
@@ -11177,6 +11241,16 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11177
11241
  });
11178
11242
  }
11179
11243
  await updatePerTurnState(state, logger, messages);
11244
+ if (state.modelContextLimit === void 0 && !state.noContextLimitWarned && requestModel?.providerID && requestModel?.modelID && registry4.resolveModelLimit(requestModel.providerID, requestModel.modelID) === void 0) {
11245
+ state.noContextLimitWarned = true;
11246
+ logger.warn(
11247
+ '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}) or set an absolute compress.maxContextLimit in acp.jsonc.',
11248
+ {
11249
+ session: state.sessionId,
11250
+ model: `${requestModel.providerID}/${requestModel.modelID}`
11251
+ }
11252
+ );
11253
+ }
11180
11254
  }
11181
11255
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
11182
11256
  if (state.isSubAgent && !config.allowSubAgents) {
@@ -11184,11 +11258,10 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11184
11258
  }
11185
11259
  stripHallucinations(output.messages);
11186
11260
  ensureBuiltinFiltersRegistered();
11187
- const effectiveLimit = resolveEffectiveContextLimit(state, config);
11188
11261
  applyMessageFilters(output.messages, config.messageFilters, logger, {
11189
11262
  sessionId: state.sessionId ?? "",
11190
11263
  isSubAgent: state.isSubAgent,
11191
- modelContextLimit: effectiveLimit?.limit
11264
+ modelContextLimit: state.modelContextLimit
11192
11265
  });
11193
11266
  cacheSystemPromptTokens(state, output.messages);
11194
11267
  assignMessageRefs(state, output.messages);
@@ -11208,6 +11281,7 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11208
11281
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
11209
11282
  prune(state, logger, config, output.messages);
11210
11283
  truncateLargeToolOutputs(state, config, logger, output.messages);
11284
+ enforceContextBudget(state, config, logger, output.messages);
11211
11285
  hideConsumedCompressCalls(state, output.messages);
11212
11286
  assignMessageRefs(state, output.messages);
11213
11287
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
@@ -11239,31 +11313,14 @@ ${text}`);
11239
11313
  stripStaleMetadata(output.messages);
11240
11314
  dropEmptyMessages(output.messages);
11241
11315
  const postTokens = getCurrentTokenUsage(state, output.messages);
11242
- if (postTokens !== void 0 && effectiveLimit) {
11243
- const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
11244
- if (postTokens > budget) {
11245
- logger.error(
11246
- "ACP hard guard: context exceeds model budget after in-flight reduction",
11247
- {
11248
- session: state.sessionId,
11249
- postTokens,
11250
- budget,
11251
- contextLimit: effectiveLimit.limit,
11252
- contextLimitSource: effectiveLimit.source,
11253
- hint: "request will likely be rejected; run /compact or start a new session"
11254
- }
11255
- );
11256
- }
11257
- }
11258
11316
  logger.info("Chat transform complete", {
11259
11317
  session: state.sessionId,
11260
11318
  model: state.modelID,
11261
11319
  messages: output.messages.length,
11262
11320
  prePruneTokens,
11263
11321
  postTokens,
11264
- contextLimit: effectiveLimit?.limit,
11265
- contextLimitSource: effectiveLimit?.source,
11266
- usagePct: postTokens !== void 0 && effectiveLimit ? `${(postTokens / effectiveLimit.limit * 100).toFixed(1)}%` : void 0,
11322
+ contextLimit: state.modelContextLimit,
11323
+ usagePct: postTokens !== void 0 && state.modelContextLimit ? `${(postTokens / state.modelContextLimit * 100).toFixed(1)}%` : void 0,
11267
11324
  nudged: state.nudges.shouldInjectThisTurn
11268
11325
  });
11269
11326
  if (state.sessionId) {
@@ -11650,7 +11707,7 @@ var server = (async (ctx) => {
11650
11707
  }
11651
11708
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
11652
11709
  logger.info("ACP plugin initialized", {
11653
- version: true ? "1.14.25-pr.349.71" : "dev",
11710
+ version: true ? "1.14.25-pr.350.73" : "dev",
11654
11711
  workspace: ctx.directory,
11655
11712
  logLevel: logger.level,
11656
11713
  debug: config.debug,