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

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
@@ -914,6 +914,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
914
914
  "compress.reasoning",
915
915
  "compress.reasoning.drop",
916
916
  "compress.reasoning.threshold",
917
+ "compress.completionReserveTokens",
917
918
  "gc",
918
919
  "gc.algorithm",
919
920
  "gc.promotionThreshold",
@@ -965,7 +966,11 @@ function validateConfigTypes(config) {
965
966
  errors.push({ key: "storagePath", expected: "string", actual: typeof config.storagePath });
966
967
  }
967
968
  if (config.allowSubAgents !== void 0 && typeof config.allowSubAgents !== "boolean") {
968
- 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
+ });
969
974
  }
970
975
  if (config.pruneNotification !== void 0) {
971
976
  const validValues = ["off", "minimal", "detailed"];
@@ -1294,6 +1299,20 @@ function validateConfigTypes(config) {
1294
1299
  }
1295
1300
  }
1296
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
+ }
1297
1316
  if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
1298
1317
  errors.push({
1299
1318
  key: "compress.iterationNudgeThreshold",
@@ -1412,12 +1431,20 @@ function validateConfigTypes(config) {
1412
1431
  break;
1413
1432
  case "nudgeForce":
1414
1433
  if (value !== "strong" && value !== "soft") {
1415
- 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
+ });
1416
1439
  }
1417
1440
  break;
1418
1441
  case "stringArray":
1419
1442
  if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
1420
- errors.push({ key, expected: "string[]", actual: JSON.stringify(value) });
1443
+ errors.push({
1444
+ key,
1445
+ expected: "string[]",
1446
+ actual: JSON.stringify(value)
1447
+ });
1421
1448
  }
1422
1449
  break;
1423
1450
  case "reasoningConfig":
@@ -1452,7 +1479,11 @@ function validateConfigTypes(config) {
1452
1479
  return;
1453
1480
  }
1454
1481
  if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides)) {
1455
- errors.push({ key: prefix, expected: "CompressModelOverrides", actual: typeof overrides });
1482
+ errors.push({
1483
+ key: prefix,
1484
+ expected: "CompressModelOverrides",
1485
+ actual: typeof overrides
1486
+ });
1456
1487
  return;
1457
1488
  }
1458
1489
  const model = overrides;
@@ -1485,7 +1516,11 @@ function validateConfigTypes(config) {
1485
1516
  for (const [providerId, providerValue] of Object.entries(providers)) {
1486
1517
  const prefix = `compress.providers.${providerId}`;
1487
1518
  if (typeof providerValue !== "object" || providerValue === null || Array.isArray(providerValue)) {
1488
- errors.push({ key: prefix, expected: "ProviderOverrides", actual: typeof providerValue });
1519
+ errors.push({
1520
+ key: prefix,
1521
+ expected: "ProviderOverrides",
1522
+ actual: typeof providerValue
1523
+ });
1489
1524
  continue;
1490
1525
  }
1491
1526
  const provider = providerValue;
@@ -1621,13 +1656,22 @@ function validateConfigTypes(config) {
1621
1656
  });
1622
1657
  } else {
1623
1658
  if (gc.batchCleanup.lowThreshold !== void 0) {
1624
- validateBatchThreshold("gc.batchCleanup.lowThreshold", gc.batchCleanup.lowThreshold);
1659
+ validateBatchThreshold(
1660
+ "gc.batchCleanup.lowThreshold",
1661
+ gc.batchCleanup.lowThreshold
1662
+ );
1625
1663
  }
1626
1664
  if (gc.batchCleanup.highThreshold !== void 0) {
1627
- validateBatchThreshold("gc.batchCleanup.highThreshold", gc.batchCleanup.highThreshold);
1665
+ validateBatchThreshold(
1666
+ "gc.batchCleanup.highThreshold",
1667
+ gc.batchCleanup.highThreshold
1668
+ );
1628
1669
  }
1629
1670
  if (gc.batchCleanup.forceThreshold !== void 0) {
1630
- validateBatchThreshold("gc.batchCleanup.forceThreshold", gc.batchCleanup.forceThreshold);
1671
+ validateBatchThreshold(
1672
+ "gc.batchCleanup.forceThreshold",
1673
+ gc.batchCleanup.forceThreshold
1674
+ );
1631
1675
  }
1632
1676
  }
1633
1677
  }
@@ -1912,7 +1956,8 @@ function mergeCompress(base, override) {
1912
1956
  reasoning: {
1913
1957
  drop: override.reasoning?.drop ?? base.reasoning?.drop ?? DEFAULT_COMPRESS_REASONING.drop,
1914
1958
  threshold: override.reasoning?.threshold ?? base.reasoning?.threshold ?? DEFAULT_COMPRESS_REASONING.threshold
1915
- }
1959
+ },
1960
+ completionReserveTokens: override.completionReserveTokens ?? base.completionReserveTokens
1916
1961
  };
1917
1962
  }
1918
1963
  function mergeCommands(base, override) {
@@ -1950,10 +1995,9 @@ function deepCloneConfig(config) {
1950
1995
  ...provider,
1951
1996
  ...provider.models ? {
1952
1997
  models: Object.fromEntries(
1953
- Object.entries(provider.models).map(([modelId, model]) => [
1954
- modelId,
1955
- { ...model }
1956
- ])
1998
+ Object.entries(provider.models).map(
1999
+ ([modelId, model]) => [modelId, { ...model }]
2000
+ )
1957
2001
  )
1958
2002
  } : {}
1959
2003
  }
@@ -2027,8 +2071,14 @@ function mergeLayer(config, data) {
2027
2071
  ],
2028
2072
  compress: mergeCompress(config.compress, data.compress),
2029
2073
  gc: mergeGC(config.gc, data.gc),
2030
- qualityGate: mergeQualityGate(config.qualityGate, data.qualityGate),
2031
- 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
+ )
2032
2082
  };
2033
2083
  }
2034
2084
  function scheduleParseWarning(ctx, title, message) {
@@ -4644,7 +4694,8 @@ function createSessionState() {
4644
4694
  modelID: void 0,
4645
4695
  systemPromptTokens: void 0,
4646
4696
  storageDir: void 0,
4647
- qualityGateRetryPending: false
4697
+ qualityGateRetryPending: false,
4698
+ noContextLimitWarned: false
4648
4699
  };
4649
4700
  }
4650
4701
  function resetSessionState(state) {
@@ -4687,6 +4738,7 @@ function resetSessionState(state) {
4687
4738
  state.systemPromptTokens = void 0;
4688
4739
  state.storageDir = void 0;
4689
4740
  state.qualityGateRetryPending = false;
4741
+ state.noContextLimitWarned = false;
4690
4742
  }
4691
4743
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
4692
4744
  if (state.sessionId === sessionId) {
@@ -9579,7 +9631,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9579
9631
  import { join as join4 } from "path";
9580
9632
  import { existsSync as existsSync4 } from "fs";
9581
9633
  import { homedir as homedir3 } from "os";
9582
- var LOG_VERSION = true ? "1.16.0-pr.349.122" : "dev";
9634
+ var LOG_VERSION = true ? "1.16.0-pr.350.123" : "dev";
9583
9635
  var LEVEL_RANK = {
9584
9636
  debug: 10,
9585
9637
  info: 20,
@@ -10484,6 +10536,153 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
10484
10536
  }
10485
10537
  }
10486
10538
 
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
+
10487
10686
  // lib/commands/context.ts
10488
10687
  function analyzeTokens(state, messages) {
10489
10688
  const breakdown = {
@@ -11606,6 +11805,16 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11606
11805
  });
11607
11806
  }
11608
11807
  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
+ }
11609
11818
  }
11610
11819
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
11611
11820
  if (state.isSubAgent && !config.allowSubAgents) {
@@ -11655,6 +11864,7 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11655
11864
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
11656
11865
  prune(state, logger, config, output.messages);
11657
11866
  truncateLargeToolOutputs(state, config, logger, output.messages);
11867
+ enforceContextBudget(state, config, logger, output.messages);
11658
11868
  hideConsumedCompressCalls(state, output.messages);
11659
11869
  assignMessageRefs(state, output.messages);
11660
11870
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
@@ -11742,12 +11952,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11742
11952
  path: { id: input.sessionID }
11743
11953
  });
11744
11954
  const messages = filterMessages(messagesResponse.data || messagesResponse);
11745
- const state = await registry4.getOrCreate(
11746
- client,
11747
- input.sessionID,
11748
- messages,
11749
- config
11750
- );
11955
+ const state = await registry4.getOrCreate(client, input.sessionID, messages, config);
11751
11956
  syncCompressPermissionState(state, config, hostPermissions, messages);
11752
11957
  const commandCtx = {
11753
11958
  client,
@@ -11769,13 +11974,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11769
11974
  throw new Error("__DCP_CONTEXT_HANDLED__");
11770
11975
  }
11771
11976
  if (sub === "help") {
11772
- await sendIgnoredMessage(
11773
- client,
11774
- input.sessionID,
11775
- buildHelpText(),
11776
- {},
11777
- logger
11778
- );
11977
+ await sendIgnoredMessage(client, input.sessionID, buildHelpText(), {}, logger);
11779
11978
  throw new Error("__DCP_CONTEXT_HANDLED__");
11780
11979
  }
11781
11980
  await handleContextCommand(commandCtx);
@@ -11850,9 +12049,7 @@ function createEventHandler(registry4, logger) {
11850
12049
  return;
11851
12050
  }
11852
12051
  if (typeof part.callID === "string" && typeof part.messageID === "string") {
11853
- timing.startsByCallId.delete(
11854
- buildCompressionTimingKey(part.messageID, part.callID)
11855
- );
12052
+ timing.startsByCallId.delete(buildCompressionTimingKey(part.messageID, part.callID));
11856
12053
  }
11857
12054
  };
11858
12055
  }
@@ -12129,7 +12326,7 @@ var server = (async (ctx) => {
12129
12326
  }
12130
12327
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
12131
12328
  logger.info("ACP plugin initialized", {
12132
- version: true ? "1.16.0-pr.349.122" : "dev",
12329
+ version: true ? "1.16.0-pr.350.123" : "dev",
12133
12330
  workspace: ctx.directory,
12134
12331
  logLevel: logger.level,
12135
12332
  debug: config.debug,