opencode-acp 1.14.25-pr.349.75 → 1.14.25-pr.350.76

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",
@@ -909,6 +908,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
909
908
  "compress.preserveRecentMessages",
910
909
  "compress.preserveRecentTokens",
911
910
  "compress.preserveLastUserMessage",
911
+ "compress.completionReserveTokens",
912
912
  "gc",
913
913
  "gc.algorithm",
914
914
  "gc.promotionThreshold",
@@ -1262,6 +1262,20 @@ function validateConfigTypes(config) {
1262
1262
  actual: typeof compress.preserveLastUserMessage
1263
1263
  });
1264
1264
  }
1265
+ if (compress.completionReserveTokens !== void 0 && typeof compress.completionReserveTokens !== "number") {
1266
+ errors.push({
1267
+ key: "compress.completionReserveTokens",
1268
+ expected: "number",
1269
+ actual: typeof compress.completionReserveTokens
1270
+ });
1271
+ }
1272
+ if (typeof compress.completionReserveTokens === "number" && compress.completionReserveTokens < 0) {
1273
+ errors.push({
1274
+ key: "compress.completionReserveTokens",
1275
+ expected: "non-negative number (>= 0)",
1276
+ actual: `${compress.completionReserveTokens}`
1277
+ });
1278
+ }
1265
1279
  if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
1266
1280
  errors.push({
1267
1281
  key: "compress.iterationNudgeThreshold",
@@ -1312,20 +1326,6 @@ function validateConfigTypes(config) {
1312
1326
  }
1313
1327
  validateModelLimits("compress.modelMaxLimits", compress.modelMaxLimits);
1314
1328
  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
1329
  const validValues = ["ask", "allow", "deny"];
1330
1330
  if (compress.permission !== void 0 && !validValues.includes(compress.permission)) {
1331
1331
  errors.push({
@@ -1503,7 +1503,6 @@ var defaultConfig = {
1503
1503
  summaryBuffer: true,
1504
1504
  maxContextLimit: "80%",
1505
1505
  minContextLimit: "80%",
1506
- contextLimitFallback: 128e3,
1507
1506
  nudgeFrequency: 5,
1508
1507
  minNudgeContextPercent: 15,
1509
1508
  iterationNudgeThreshold: 15,
@@ -1639,7 +1638,6 @@ function mergeCompress(base, override) {
1639
1638
  minContextLimit: override.minContextLimit ?? base.minContextLimit,
1640
1639
  modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
1641
1640
  modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
1642
- contextLimitFallback: override.contextLimitFallback ?? base.contextLimitFallback,
1643
1641
  nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
1644
1642
  minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent,
1645
1643
  nudgeGrowthTokens: override.nudgeGrowthTokens,
@@ -1659,7 +1657,8 @@ function mergeCompress(base, override) {
1659
1657
  lastSegmentSoftBlock: override.lastSegmentSoftBlock ?? base.lastSegmentSoftBlock,
1660
1658
  preserveRecentMessages: override.preserveRecentMessages ?? base.preserveRecentMessages,
1661
1659
  preserveRecentTokens: override.preserveRecentTokens ?? base.preserveRecentTokens,
1662
- preserveLastUserMessage: override.preserveLastUserMessage ?? base.preserveLastUserMessage
1660
+ preserveLastUserMessage: override.preserveLastUserMessage ?? base.preserveLastUserMessage,
1661
+ completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens
1663
1662
  };
1664
1663
  }
1665
1664
  function mergeCommands(base, override) {
@@ -2382,16 +2381,6 @@ function resetOnCompaction(state) {
2382
2381
  nextRef: 1
2383
2382
  };
2384
2383
  }
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
2384
 
2396
2385
  // lib/state/persistence.ts
2397
2386
  function getStorageDir() {
@@ -4237,24 +4226,6 @@ var SessionStateRegistry = class {
4237
4226
  hydrateModelLimitsFromClient(client) {
4238
4227
  return this.catalog.hydrateFromClient(client);
4239
4228
  }
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
- // The in-flight promise (not a boolean) lets concurrent callers await the
4247
- // same hydration instead of skipping it.
4248
- lazyHydration;
4249
- async hydrateAndResolve(client, providerId, modelId) {
4250
- const existing = this.catalog.resolve(providerId, modelId);
4251
- if (existing !== void 0) {
4252
- return existing;
4253
- }
4254
- this.lazyHydration ??= this.catalog.hydrateFromClient(client);
4255
- await this.lazyHydration;
4256
- return this.catalog.resolve(providerId, modelId);
4257
- }
4258
4229
  get(sessionId) {
4259
4230
  return this.states.get(sessionId);
4260
4231
  }
@@ -4346,7 +4317,8 @@ function createSessionState() {
4346
4317
  modelProviderID: void 0,
4347
4318
  modelID: void 0,
4348
4319
  systemPromptTokens: void 0,
4349
- qualityGateRetryPending: false
4320
+ qualityGateRetryPending: false,
4321
+ noContextLimitWarned: false
4350
4322
  };
4351
4323
  }
4352
4324
  function resetSessionState(state) {
@@ -4388,6 +4360,7 @@ function resetSessionState(state) {
4388
4360
  state.modelID = void 0;
4389
4361
  state.systemPromptTokens = void 0;
4390
4362
  state.qualityGateRetryPending = false;
4363
+ state.noContextLimitWarned = false;
4391
4364
  }
4392
4365
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, config) {
4393
4366
  if (state.sessionId === sessionId) {
@@ -6925,7 +6898,6 @@ function getModelInfo(messages) {
6925
6898
  };
6926
6899
  }
6927
6900
  function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
6928
- const effectiveLimit = resolveEffectiveContextLimit(state, config);
6929
6901
  const parseLimitValue = (limit) => {
6930
6902
  if (limit === void 0) {
6931
6903
  return void 0;
@@ -6933,7 +6905,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6933
6905
  if (typeof limit === "number") {
6934
6906
  return limit;
6935
6907
  }
6936
- if (!limit.endsWith("%") || effectiveLimit === void 0) {
6908
+ if (!limit.endsWith("%") || state.modelContextLimit === void 0) {
6937
6909
  return void 0;
6938
6910
  }
6939
6911
  const parsedPercent = parseFloat(limit.slice(0, -1));
@@ -6942,7 +6914,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6942
6914
  }
6943
6915
  const roundedPercent = Math.round(parsedPercent);
6944
6916
  const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
6945
- return Math.round(clampedPercent / 100 * effectiveLimit.limit);
6917
+ return Math.round(clampedPercent / 100 * state.modelContextLimit);
6946
6918
  };
6947
6919
  const modelLimits = threshold === "max" ? config.compress.modelMaxLimits : config.compress.modelMinLimits;
6948
6920
  if (modelLimits && providerId !== void 0 && modelId !== void 0) {
@@ -6987,12 +6959,11 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
6987
6959
  if (!overMaxLimit) break;
6988
6960
  }
6989
6961
  }
6990
- const effectiveLimit = resolveEffectiveContextLimit(state, config);
6991
6962
  return {
6992
6963
  overMaxLimit,
6993
6964
  overMinLimit,
6994
6965
  currentTokens,
6995
- modelContextLimit: effectiveLimit?.limit
6966
+ modelContextLimit: state.modelContextLimit
6996
6967
  };
6997
6968
  }
6998
6969
  ensureBuiltinTriggerPolicyRegistered();
@@ -8434,9 +8405,8 @@ function createDecompressTool(factoryCtx) {
8434
8405
  async execute(args, toolCtx) {
8435
8406
  const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
8436
8407
  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
8408
+ const contextUsageBefore = ctx.state.modelContextLimit ? Math.round(
8409
+ getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
8440
8410
  ) : void 0;
8441
8411
  const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
8442
8412
  if (!resolved.ok) {
@@ -8500,9 +8470,8 @@ function createDecompressTool(factoryCtx) {
8500
8470
  0,
8501
8471
  ctx.state.stats.totalPruneTokens - restoredTokens
8502
8472
  );
8503
- const effectiveLimitAfter = resolveEffectiveContextLimit(ctx.state, ctx.config);
8504
- const contextUsageAfter = effectiveLimitAfter ? Math.round(
8505
- getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitAfter.limit * 100
8473
+ const contextUsageAfter = ctx.state.modelContextLimit ? Math.round(
8474
+ getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
8506
8475
  ) : void 0;
8507
8476
  await finalizeDecompressSession(ctx);
8508
8477
  const restoredContentPreview = buildRestoredContentPreview(
@@ -9159,7 +9128,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9159
9128
  import { join as join3 } from "path";
9160
9129
  import { existsSync as existsSync3 } from "fs";
9161
9130
  import { homedir as homedir3 } from "os";
9162
- var LOG_VERSION = true ? "1.14.25-pr.349.75" : "dev";
9131
+ var LOG_VERSION = true ? "1.14.25-pr.350.76" : "dev";
9163
9132
  var LEVEL_RANK = {
9164
9133
  debug: 10,
9165
9134
  info: 20,
@@ -9983,8 +9952,6 @@ var MIN_OUTPUT_TOKENS = 1e3;
9983
9952
  var KEEP_PREFIX_CHARS = 2e3;
9984
9953
  var KEEP_SUFFIX_CHARS = 2e3;
9985
9954
  var PROTECT_RECENT_MESSAGES = 3;
9986
- var OUTPUT_RESERVE_TOKENS = 16384;
9987
- var overheadErrorLogged = /* @__PURE__ */ new Set();
9988
9955
  function parseGcThreshold(threshold, modelContextLimit) {
9989
9956
  if (typeof threshold === "number") return threshold;
9990
9957
  const str = threshold ?? "100%";
@@ -9993,26 +9960,10 @@ function parseGcThreshold(threshold, modelContextLimit) {
9993
9960
  return modelContextLimit;
9994
9961
  }
9995
9962
  function truncateLargeToolOutputs(state, config, logger, messages) {
9996
- const effective = resolveEffectiveContextLimit(state, config);
9997
- if (!effective) return;
9963
+ if (!state.modelContextLimit) return;
9998
9964
  const currentTokens = getCurrentTokenUsage(state, messages);
9999
9965
  if (currentTokens === 0) return;
10000
- const configuredThreshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, effective.limit);
10001
- const overhead = (state.systemPromptTokens ?? 0) + OUTPUT_RESERVE_TOKENS;
10002
- const threshold = Math.min(configuredThreshold, effective.limit - overhead);
10003
- if (threshold <= 0) {
10004
- const sessionKey = state.sessionId ?? "unknown";
10005
- if (!overheadErrorLogged.has(sessionKey)) {
10006
- overheadErrorLogged.add(sessionKey);
10007
- logger.error("ACP: model context window too small to fit overhead", {
10008
- session: state.sessionId,
10009
- limit: effective.limit,
10010
- contextLimitSource: effective.source,
10011
- overhead
10012
- });
10013
- }
10014
- return;
10015
- }
9966
+ const threshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, state.modelContextLimit);
10016
9967
  if (currentTokens < threshold) return;
10017
9968
  const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES;
10018
9969
  const candidates = [];
@@ -10057,13 +10008,158 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
10057
10008
  truncatedCount,
10058
10009
  estimatedSavedTokens: Math.round(savedTokens),
10059
10010
  currentTokens,
10060
- threshold,
10061
- contextLimit: effective.limit,
10062
- contextLimitSource: effective.source
10011
+ threshold
10063
10012
  });
10064
10013
  }
10065
10014
  }
10066
10015
 
10016
+ // lib/messages/enforce-budget.ts
10017
+ var DEFAULT_COMPLETION_RESERVE_TOKENS = 32768;
10018
+ var TRUNCATION_MARKER2 = "[truncated for context space";
10019
+ var KEEP_PREFIX_CHARS2 = 2e3;
10020
+ var KEEP_SUFFIX_CHARS2 = 2e3;
10021
+ var PROTECT_RECENT_MESSAGES2 = 3;
10022
+ var MIN_CLEAR_TOKENS = 200;
10023
+ function resolveContextWindow(state) {
10024
+ if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
10025
+ return state.modelContextLimit;
10026
+ }
10027
+ return void 0;
10028
+ }
10029
+ function estimateWireTokens(state, messages) {
10030
+ const base = getCurrentTokenUsage(state, messages);
10031
+ if (base > 0) {
10032
+ let baseAssistant = -1;
10033
+ for (let i = messages.length - 1; i >= 0; i--) {
10034
+ if (messages[i].info.role !== "assistant") continue;
10035
+ const tokens = messages[i].info.tokens;
10036
+ if ((tokens?.input || 0) <= 0 && (tokens?.output || 0) <= 0) continue;
10037
+ baseAssistant = i;
10038
+ break;
10039
+ }
10040
+ if (baseAssistant >= 0) {
10041
+ let additions = 0;
10042
+ for (let i = baseAssistant + 1; i < messages.length; i++) {
10043
+ additions += countAllMessageTokens(messages[i]);
10044
+ }
10045
+ return base + additions;
10046
+ }
10047
+ }
10048
+ let total = 0;
10049
+ for (const m of messages) total += countAllMessageTokens(m);
10050
+ return total + (state.systemPromptTokens ?? 0);
10051
+ }
10052
+ function enforceContextBudget(state, config, logger, messages) {
10053
+ const window = resolveContextWindow(state);
10054
+ if (window === void 0) return void 0;
10055
+ const configuredReserve = config.compress?.completionReserveTokens;
10056
+ const reserve = typeof configuredReserve === "number" && Number.isFinite(configuredReserve) && configuredReserve >= 0 ? configuredReserve : DEFAULT_COMPLETION_RESERVE_TOKENS;
10057
+ const budget = window - reserve;
10058
+ if (budget <= 0) return void 0;
10059
+ const estimatedTokens = estimateWireTokens(state, messages);
10060
+ if (estimatedTokens <= budget) {
10061
+ return {
10062
+ applied: false,
10063
+ window,
10064
+ reserve,
10065
+ budget,
10066
+ estimatedTokens,
10067
+ finalEstimate: estimatedTokens,
10068
+ truncatedCount: 0,
10069
+ clearedCount: 0
10070
+ };
10071
+ }
10072
+ const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES2;
10073
+ const protectedTools = new Set(config.compress?.protectedTools ?? []);
10074
+ const candidates = [];
10075
+ for (let mi = 0; mi < protectedIndex; mi++) {
10076
+ if (mi === 0 && messages[mi].info.role === "user") continue;
10077
+ const msg = messages[mi];
10078
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
10079
+ for (const part of parts) {
10080
+ if (part?.type !== "tool") continue;
10081
+ if (part.state?.status !== "completed") continue;
10082
+ if (part.tool === "compress") continue;
10083
+ if (protectedTools.has(part.tool)) continue;
10084
+ const content = extractCompletedToolOutput(part);
10085
+ if (content === void 0) continue;
10086
+ if (content === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10087
+ const tokens = countTokens2(content);
10088
+ if (tokens <= 0) continue;
10089
+ candidates.push({ part, content, tokens, index: mi });
10090
+ }
10091
+ }
10092
+ let saved = 0;
10093
+ let truncatedCount = 0;
10094
+ let clearedCount = 0;
10095
+ const truncatable = candidates.filter(
10096
+ (c) => !c.content.includes(TRUNCATION_MARKER2) && c.content.length > KEEP_PREFIX_CHARS2 + KEEP_SUFFIX_CHARS2
10097
+ ).sort((a, b) => b.tokens - a.tokens);
10098
+ for (const c of truncatable) {
10099
+ if (estimatedTokens - saved <= budget) break;
10100
+ const prefix = c.content.slice(0, KEEP_PREFIX_CHARS2);
10101
+ const suffix = c.content.slice(-KEEP_SUFFIX_CHARS2);
10102
+ const truncated = prefix + `
10103
+
10104
+ ...${TRUNCATION_MARKER2} \u2014 original ~${c.tokens} tokens]...
10105
+
10106
+ ` + suffix;
10107
+ if (truncated.length >= c.content.length) continue;
10108
+ c.part.state.output = truncated;
10109
+ saved += c.tokens - countTokens2(truncated);
10110
+ truncatedCount++;
10111
+ }
10112
+ if (estimatedTokens - saved > budget) {
10113
+ const clearable = candidates.filter((c) => {
10114
+ const out = extractCompletedToolOutput(c.part);
10115
+ return out !== void 0 && out !== COMPACTED_TOOL_OUTPUT_PLACEHOLDER && countTokens2(out) > MIN_CLEAR_TOKENS;
10116
+ }).sort((a, b) => a.index - b.index);
10117
+ for (const c of clearable) {
10118
+ if (estimatedTokens - saved <= budget) break;
10119
+ const current = extractCompletedToolOutput(c.part);
10120
+ if (current === void 0 || current === COMPACTED_TOOL_OUTPUT_PLACEHOLDER) continue;
10121
+ c.part.state.output = COMPACTED_TOOL_OUTPUT_PLACEHOLDER;
10122
+ saved += countTokens2(current) - countTokens2(COMPACTED_TOOL_OUTPUT_PLACEHOLDER);
10123
+ clearedCount++;
10124
+ }
10125
+ }
10126
+ const finalEstimate = Math.max(0, estimatedTokens - saved);
10127
+ if (truncatedCount > 0 || clearedCount > 0) {
10128
+ logger.warn("Context budget guard: pruned tool outputs to fit the request", {
10129
+ session: state.sessionId,
10130
+ estimatedTokens: Math.round(estimatedTokens),
10131
+ budget,
10132
+ window,
10133
+ reserve,
10134
+ truncatedCount,
10135
+ clearedCount,
10136
+ estimatedSavedTokens: Math.round(saved),
10137
+ finalEstimate: Math.round(finalEstimate)
10138
+ });
10139
+ }
10140
+ if (finalEstimate > budget) {
10141
+ logger.warn(
10142
+ "Context budget guard: still over budget after pruning all compressible tool outputs; the request may be rejected by the model",
10143
+ {
10144
+ session: state.sessionId,
10145
+ finalEstimate: Math.round(finalEstimate),
10146
+ budget,
10147
+ window
10148
+ }
10149
+ );
10150
+ }
10151
+ return {
10152
+ applied: truncatedCount > 0 || clearedCount > 0,
10153
+ window,
10154
+ reserve,
10155
+ budget,
10156
+ estimatedTokens,
10157
+ finalEstimate,
10158
+ truncatedCount,
10159
+ clearedCount
10160
+ };
10161
+ }
10162
+
10067
10163
  // lib/commands/context.ts
10068
10164
  function analyzeTokens(state, messages) {
10069
10165
  const breakdown = {
@@ -11020,12 +11116,11 @@ function runBatchCleanup(state, config, logger, messages) {
11020
11116
  mergedCount: 0,
11021
11117
  savedTokens: 0
11022
11118
  };
11023
- const effective = resolveEffectiveContextLimit(state, config);
11024
- if (!effective) {
11119
+ if (!state.modelContextLimit || state.modelContextLimit <= 0) {
11025
11120
  return noop;
11026
11121
  }
11027
11122
  const currentTokens = getCurrentTokenUsage(state, messages);
11028
- if (currentTokens < effective.limit) {
11123
+ if (currentTokens < state.modelContextLimit) {
11029
11124
  return noop;
11030
11125
  }
11031
11126
  const maxMergedLength = config.gc.maxOldGenSummaryLength;
@@ -11042,8 +11137,7 @@ function runBatchCleanup(state, config, logger, messages) {
11042
11137
  mergedCount: result.mergedCount,
11043
11138
  savedTokens: result.savedTokens,
11044
11139
  currentTokens,
11045
- contextLimit: effective.limit,
11046
- contextLimitSource: effective.source
11140
+ contextLimit: state.modelContextLimit
11047
11141
  });
11048
11142
  return {
11049
11143
  tier: 3,
@@ -11077,6 +11171,11 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11077
11171
  input.model?.limit?.context
11078
11172
  );
11079
11173
  const state = input.sessionID ? registry4.get(input.sessionID) : void 0;
11174
+ if (state && input.model?.limit?.context) {
11175
+ state.modelContextLimit = input.model.limit.context;
11176
+ state.modelProviderID = input.model?.providerID;
11177
+ state.modelID = input.model?.id;
11178
+ }
11080
11179
  if (!state || state.isSubAgent && !config.allowSubAgents) {
11081
11180
  return;
11082
11181
  }
@@ -11085,23 +11184,6 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11085
11184
  logger.info("Skipping DCP system prompt injection for internal agent");
11086
11185
  return;
11087
11186
  }
11088
- if (input.model?.limit?.context) {
11089
- const limit = input.model.limit.context;
11090
- const providerID = input.model?.providerID;
11091
- const modelID = input.model?.id;
11092
- const changed = state.modelContextLimit !== limit || providerID !== void 0 && state.modelProviderID !== providerID || modelID !== void 0 && state.modelID !== modelID;
11093
- state.modelContextLimit = limit;
11094
- if (providerID !== void 0) {
11095
- state.modelProviderID = providerID;
11096
- }
11097
- if (modelID !== void 0) {
11098
- state.modelID = modelID;
11099
- }
11100
- if (changed) {
11101
- saveSessionState(state, logger).catch(() => {
11102
- });
11103
- }
11104
- }
11105
11187
  const effectivePermission = compressPermission(state, config);
11106
11188
  if (effectivePermission === "deny") {
11107
11189
  return;
@@ -11146,17 +11228,10 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11146
11228
  config
11147
11229
  );
11148
11230
  const requestModel = lastUserMessage.info.model;
11149
- let requestModelLimit = registry4.resolveModelLimit(
11231
+ const requestModelLimit = registry4.resolveModelLimit(
11150
11232
  requestModel?.providerID,
11151
11233
  requestModel?.modelID
11152
11234
  );
11153
- if (requestModelLimit === void 0 && requestModel?.providerID && requestModel?.modelID) {
11154
- requestModelLimit = await registry4.hydrateAndResolve(
11155
- client,
11156
- requestModel.providerID,
11157
- requestModel.modelID
11158
- );
11159
- }
11160
11235
  const prevModelID = state.modelID;
11161
11236
  if (requestModelLimit !== void 0) {
11162
11237
  state.modelContextLimit = requestModelLimit;
@@ -11186,6 +11261,16 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11186
11261
  });
11187
11262
  }
11188
11263
  await updatePerTurnState(state, logger, messages);
11264
+ if (state.modelContextLimit === void 0 && !state.noContextLimitWarned && requestModel?.providerID && requestModel?.modelID && registry4.resolveModelLimit(requestModel.providerID, requestModel.modelID) === void 0) {
11265
+ state.noContextLimitWarned = true;
11266
+ logger.warn(
11267
+ '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.',
11268
+ {
11269
+ session: state.sessionId,
11270
+ model: `${requestModel.providerID}/${requestModel.modelID}`
11271
+ }
11272
+ );
11273
+ }
11189
11274
  }
11190
11275
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
11191
11276
  if (state.isSubAgent && !config.allowSubAgents) {
@@ -11193,11 +11278,10 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11193
11278
  }
11194
11279
  stripHallucinations(output.messages);
11195
11280
  ensureBuiltinFiltersRegistered();
11196
- const effectiveLimit = resolveEffectiveContextLimit(state, config);
11197
11281
  applyMessageFilters(output.messages, config.messageFilters, logger, {
11198
11282
  sessionId: state.sessionId ?? "",
11199
11283
  isSubAgent: state.isSubAgent,
11200
- modelContextLimit: effectiveLimit?.limit
11284
+ modelContextLimit: state.modelContextLimit
11201
11285
  });
11202
11286
  cacheSystemPromptTokens(state, output.messages);
11203
11287
  assignMessageRefs(state, output.messages);
@@ -11217,6 +11301,7 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11217
11301
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
11218
11302
  prune(state, logger, config, output.messages);
11219
11303
  truncateLargeToolOutputs(state, config, logger, output.messages);
11304
+ enforceContextBudget(state, config, logger, output.messages);
11220
11305
  hideConsumedCompressCalls(state, output.messages);
11221
11306
  assignMessageRefs(state, output.messages);
11222
11307
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
@@ -11248,31 +11333,14 @@ ${text}`);
11248
11333
  stripStaleMetadata(output.messages);
11249
11334
  dropEmptyMessages(output.messages);
11250
11335
  const postTokens = getCurrentTokenUsage(state, output.messages);
11251
- if (postTokens !== void 0 && effectiveLimit) {
11252
- const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
11253
- if (postTokens > budget) {
11254
- logger.error(
11255
- "ACP hard guard: context exceeds model budget after in-flight reduction",
11256
- {
11257
- session: state.sessionId,
11258
- postTokens,
11259
- budget,
11260
- contextLimit: effectiveLimit.limit,
11261
- contextLimitSource: effectiveLimit.source,
11262
- hint: "request will likely be rejected; run /compact or start a new session"
11263
- }
11264
- );
11265
- }
11266
- }
11267
11336
  logger.info("Chat transform complete", {
11268
11337
  session: state.sessionId,
11269
11338
  model: state.modelID,
11270
11339
  messages: output.messages.length,
11271
11340
  prePruneTokens,
11272
11341
  postTokens,
11273
- contextLimit: effectiveLimit?.limit,
11274
- contextLimitSource: effectiveLimit?.source,
11275
- usagePct: postTokens !== void 0 && effectiveLimit ? `${(postTokens / effectiveLimit.limit * 100).toFixed(1)}%` : void 0,
11342
+ contextLimit: state.modelContextLimit,
11343
+ usagePct: postTokens !== void 0 && state.modelContextLimit ? `${(postTokens / state.modelContextLimit * 100).toFixed(1)}%` : void 0,
11276
11344
  nudged: state.nudges.shouldInjectThisTurn
11277
11345
  });
11278
11346
  if (state.sessionId) {
@@ -11659,7 +11727,7 @@ var server = (async (ctx) => {
11659
11727
  }
11660
11728
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
11661
11729
  logger.info("ACP plugin initialized", {
11662
- version: true ? "1.14.25-pr.349.75" : "dev",
11730
+ version: true ? "1.14.25-pr.350.76" : "dev",
11663
11731
  workspace: ctx.directory,
11664
11732
  logLevel: logger.level,
11665
11733
  debug: config.debug,