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

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,6 +890,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
890
890
  "compress.modelMaxLimits",
891
891
  "compress.modelMinLimits",
892
892
  "compress.providers",
893
+ "compress.contextLimitFallback",
893
894
  "compress.nudgeFrequency",
894
895
  "compress.minNudgeContextPercent",
895
896
  "compress.nudgeGrowthTokens",
@@ -1521,6 +1522,20 @@ function validateConfigTypes(config) {
1521
1522
  }
1522
1523
  };
1523
1524
  validateProviderOverrides(compress.providers);
1525
+ if (compress.contextLimitFallback !== void 0 && typeof compress.contextLimitFallback !== "number") {
1526
+ errors.push({
1527
+ key: "compress.contextLimitFallback",
1528
+ expected: "number",
1529
+ actual: typeof compress.contextLimitFallback
1530
+ });
1531
+ }
1532
+ if (typeof compress.contextLimitFallback === "number" && compress.contextLimitFallback < 0) {
1533
+ errors.push({
1534
+ key: "compress.contextLimitFallback",
1535
+ expected: "non-negative number (0 disables the fallback)",
1536
+ actual: `${compress.contextLimitFallback}`
1537
+ });
1538
+ }
1524
1539
  const validValues = ["ask", "allow", "deny"];
1525
1540
  if (compress.permission !== void 0 && !validValues.includes(compress.permission)) {
1526
1541
  errors.push({
@@ -1702,6 +1717,7 @@ var defaultConfig = {
1702
1717
  summaryBuffer: true,
1703
1718
  maxContextLimit: "80%",
1704
1719
  minContextLimit: "80%",
1720
+ contextLimitFallback: 128e3,
1705
1721
  nudgeFrequency: 5,
1706
1722
  minNudgeContextPercent: 5,
1707
1723
  iterationNudgeThreshold: 15,
@@ -1872,6 +1888,7 @@ function mergeCompress(base, override) {
1872
1888
  modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
1873
1889
  modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
1874
1890
  providers: mergeProviderOverrides(base.providers, override.providers),
1891
+ contextLimitFallback: override.contextLimitFallback ?? base.contextLimitFallback,
1875
1892
  nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
1876
1893
  minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent,
1877
1894
  nudgeGrowthTokens: override.nudgeGrowthTokens,
@@ -2635,6 +2652,16 @@ function resetOnCompaction(state) {
2635
2652
  nextRef: 1
2636
2653
  };
2637
2654
  }
2655
+ function resolveEffectiveContextLimit(state, config) {
2656
+ if (typeof state.modelContextLimit === "number" && state.modelContextLimit > 0) {
2657
+ return { limit: state.modelContextLimit, source: "model" };
2658
+ }
2659
+ const fallback = config.compress.contextLimitFallback;
2660
+ if (typeof fallback === "number" && fallback > 0) {
2661
+ return { limit: fallback, source: "fallback" };
2662
+ }
2663
+ return void 0;
2664
+ }
2638
2665
 
2639
2666
  // lib/state/persistence.ts
2640
2667
  function getDefaultStorageDir() {
@@ -4506,6 +4533,24 @@ var SessionStateRegistry = class {
4506
4533
  hydrateModelLimitsFromClient(client) {
4507
4534
  return this.catalog.hydrateFromClient(client);
4508
4535
  }
4536
+ // [FIX #346] The init-time seed (above) is fire-and-forget and races
4537
+ // server readiness: in headless spawn+resume mode the provider-config
4538
+ // call can fail before the server is up, leaving the catalog empty for
4539
+ // the process's lifetime. During a request the server is guaranteed up
4540
+ // (we are inside its pipeline), so on a catalog miss we retry hydration
4541
+ // once per process before giving up (the fallback limit then applies).
4542
+ // The in-flight promise (not a boolean) lets concurrent callers await the
4543
+ // same hydration instead of skipping it.
4544
+ lazyHydration;
4545
+ async hydrateAndResolve(client, providerId, modelId) {
4546
+ const existing = this.catalog.resolve(providerId, modelId);
4547
+ if (existing !== void 0) {
4548
+ return existing;
4549
+ }
4550
+ this.lazyHydration ??= this.catalog.hydrateFromClient(client);
4551
+ await this.lazyHydration;
4552
+ return this.catalog.resolve(providerId, modelId);
4553
+ }
4509
4554
  get(sessionId) {
4510
4555
  return this.states.get(sessionId);
4511
4556
  }
@@ -6657,6 +6702,7 @@ function getModelInfo(messages) {
6657
6702
  };
6658
6703
  }
6659
6704
  function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
6705
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
6660
6706
  const parseLimitValue = (limit) => {
6661
6707
  if (limit === void 0) {
6662
6708
  return void 0;
@@ -6664,7 +6710,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6664
6710
  if (typeof limit === "number") {
6665
6711
  return limit;
6666
6712
  }
6667
- if (!limit.endsWith("%") || state.modelContextLimit === void 0) {
6713
+ if (!limit.endsWith("%") || effectiveLimit === void 0) {
6668
6714
  return void 0;
6669
6715
  }
6670
6716
  const parsedPercent = parseFloat(limit.slice(0, -1));
@@ -6673,7 +6719,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6673
6719
  }
6674
6720
  const roundedPercent = Math.round(parsedPercent);
6675
6721
  const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
6676
- return Math.round(clampedPercent / 100 * state.modelContextLimit);
6722
+ return Math.round(clampedPercent / 100 * effectiveLimit.limit);
6677
6723
  };
6678
6724
  if (threshold === "max") {
6679
6725
  const nestedLimit = resolveCompressOverrides(config, providerId, modelId).maxContextLimit;
@@ -6724,11 +6770,12 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
6724
6770
  if (!overMaxLimit) break;
6725
6771
  }
6726
6772
  }
6773
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
6727
6774
  return {
6728
6775
  overMaxLimit,
6729
6776
  overMinLimit,
6730
6777
  currentTokens,
6731
- modelContextLimit: state.modelContextLimit
6778
+ modelContextLimit: effectiveLimit?.limit
6732
6779
  };
6733
6780
  }
6734
6781
  ensureBuiltinTriggerPolicyRegistered();
@@ -8807,8 +8854,9 @@ function createDecompressTool(factoryCtx) {
8807
8854
  async execute(args, toolCtx) {
8808
8855
  const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
8809
8856
  const { rawMessages } = await prepareDecompressSession(ctx, toolCtx);
8810
- const contextUsageBefore = ctx.state.modelContextLimit ? Math.round(
8811
- getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
8857
+ const effectiveLimitBefore = resolveEffectiveContextLimit(ctx.state, ctx.config);
8858
+ const contextUsageBefore = effectiveLimitBefore ? Math.round(
8859
+ getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitBefore.limit * 100
8812
8860
  ) : void 0;
8813
8861
  const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
8814
8862
  if (!resolved.ok) {
@@ -8872,8 +8920,9 @@ function createDecompressTool(factoryCtx) {
8872
8920
  0,
8873
8921
  ctx.state.stats.totalPruneTokens - restoredTokens
8874
8922
  );
8875
- const contextUsageAfter = ctx.state.modelContextLimit ? Math.round(
8876
- getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
8923
+ const effectiveLimitAfter = resolveEffectiveContextLimit(ctx.state, ctx.config);
8924
+ const contextUsageAfter = effectiveLimitAfter ? Math.round(
8925
+ getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitAfter.limit * 100
8877
8926
  ) : void 0;
8878
8927
  await finalizeDecompressSession(ctx);
8879
8928
  const restoredContentPreview = buildRestoredContentPreview(
@@ -9530,7 +9579,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9530
9579
  import { join as join4 } from "path";
9531
9580
  import { existsSync as existsSync4 } from "fs";
9532
9581
  import { homedir as homedir3 } from "os";
9533
- var LOG_VERSION = true ? "1.16.0-pr.297.127" : "dev";
9582
+ var LOG_VERSION = true ? "1.16.0-pr.349.122" : "dev";
9534
9583
  var LEVEL_RANK = {
9535
9584
  debug: 10,
9536
9585
  info: 20,
@@ -10354,6 +10403,8 @@ var MIN_OUTPUT_TOKENS = 1e3;
10354
10403
  var KEEP_PREFIX_CHARS = 2e3;
10355
10404
  var KEEP_SUFFIX_CHARS = 2e3;
10356
10405
  var PROTECT_RECENT_MESSAGES = 3;
10406
+ var OUTPUT_RESERVE_TOKENS = 16384;
10407
+ var overheadErrorLogged = /* @__PURE__ */ new Set();
10357
10408
  function parseGcThreshold(threshold, modelContextLimit) {
10358
10409
  if (typeof threshold === "number") return threshold;
10359
10410
  const str = threshold ?? "100%";
@@ -10362,10 +10413,26 @@ function parseGcThreshold(threshold, modelContextLimit) {
10362
10413
  return modelContextLimit;
10363
10414
  }
10364
10415
  function truncateLargeToolOutputs(state, config, logger, messages) {
10365
- if (!state.modelContextLimit) return;
10416
+ const effective = resolveEffectiveContextLimit(state, config);
10417
+ if (!effective) return;
10366
10418
  const currentTokens = getCurrentTokenUsage(state, messages);
10367
10419
  if (currentTokens === 0) return;
10368
- const threshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, state.modelContextLimit);
10420
+ const configuredThreshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, effective.limit);
10421
+ const overhead = (state.systemPromptTokens ?? 0) + OUTPUT_RESERVE_TOKENS;
10422
+ const threshold = Math.min(configuredThreshold, effective.limit - overhead);
10423
+ if (threshold <= 0) {
10424
+ const sessionKey = state.sessionId ?? "unknown";
10425
+ if (!overheadErrorLogged.has(sessionKey)) {
10426
+ overheadErrorLogged.add(sessionKey);
10427
+ logger.error("ACP: model context window too small to fit overhead", {
10428
+ session: state.sessionId,
10429
+ limit: effective.limit,
10430
+ contextLimitSource: effective.source,
10431
+ overhead
10432
+ });
10433
+ }
10434
+ return;
10435
+ }
10369
10436
  if (currentTokens < threshold) return;
10370
10437
  const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES;
10371
10438
  const candidates = [];
@@ -10410,7 +10477,9 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
10410
10477
  truncatedCount,
10411
10478
  estimatedSavedTokens: Math.round(savedTokens),
10412
10479
  currentTokens,
10413
- threshold
10480
+ threshold,
10481
+ contextLimit: effective.limit,
10482
+ contextLimitSource: effective.source
10414
10483
  });
10415
10484
  }
10416
10485
  }
@@ -11371,11 +11440,12 @@ function runBatchCleanup(state, config, logger, messages) {
11371
11440
  mergedCount: 0,
11372
11441
  savedTokens: 0
11373
11442
  };
11374
- if (!state.modelContextLimit || state.modelContextLimit <= 0) {
11443
+ const effective = resolveEffectiveContextLimit(state, config);
11444
+ if (!effective) {
11375
11445
  return noop;
11376
11446
  }
11377
11447
  const currentTokens = getCurrentTokenUsage(state, messages);
11378
- if (currentTokens < state.modelContextLimit) {
11448
+ if (currentTokens < effective.limit) {
11379
11449
  return noop;
11380
11450
  }
11381
11451
  const maxMergedLength = config.gc.maxOldGenSummaryLength;
@@ -11392,7 +11462,8 @@ function runBatchCleanup(state, config, logger, messages) {
11392
11462
  mergedCount: result.mergedCount,
11393
11463
  savedTokens: result.savedTokens,
11394
11464
  currentTokens,
11395
- contextLimit: state.modelContextLimit
11465
+ contextLimit: effective.limit,
11466
+ contextLimitSource: effective.source
11396
11467
  });
11397
11468
  return {
11398
11469
  tier: 3,
@@ -11426,11 +11497,6 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11426
11497
  input.model?.limit?.context
11427
11498
  );
11428
11499
  const state = input.sessionID ? registry4.get(input.sessionID) : void 0;
11429
- if (state && input.model?.limit?.context) {
11430
- state.modelContextLimit = input.model.limit.context;
11431
- state.modelProviderID = input.model?.providerID;
11432
- state.modelID = input.model?.id;
11433
- }
11434
11500
  if (!state || state.isSubAgent && !config.allowSubAgents) {
11435
11501
  return;
11436
11502
  }
@@ -11439,6 +11505,23 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11439
11505
  logger.info("Skipping DCP system prompt injection for internal agent");
11440
11506
  return;
11441
11507
  }
11508
+ if (input.model?.limit?.context) {
11509
+ const limit = input.model.limit.context;
11510
+ const providerID = input.model?.providerID;
11511
+ const modelID = input.model?.id;
11512
+ const changed = state.modelContextLimit !== limit || providerID !== void 0 && state.modelProviderID !== providerID || modelID !== void 0 && state.modelID !== modelID;
11513
+ state.modelContextLimit = limit;
11514
+ if (providerID !== void 0) {
11515
+ state.modelProviderID = providerID;
11516
+ }
11517
+ if (modelID !== void 0) {
11518
+ state.modelID = modelID;
11519
+ }
11520
+ if (changed) {
11521
+ saveSessionState(state, logger).catch(() => {
11522
+ });
11523
+ }
11524
+ }
11442
11525
  const effectivePermission = compressPermission(state, config);
11443
11526
  if (effectivePermission === "deny") {
11444
11527
  return;
@@ -11483,10 +11566,17 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11483
11566
  config
11484
11567
  );
11485
11568
  const requestModel = lastUserMessage.info.model;
11486
- const requestModelLimit = registry4.resolveModelLimit(
11569
+ let requestModelLimit = registry4.resolveModelLimit(
11487
11570
  requestModel?.providerID,
11488
11571
  requestModel?.modelID
11489
11572
  );
11573
+ if (requestModelLimit === void 0 && requestModel?.providerID && requestModel?.modelID) {
11574
+ requestModelLimit = await registry4.hydrateAndResolve(
11575
+ client,
11576
+ requestModel.providerID,
11577
+ requestModel.modelID
11578
+ );
11579
+ }
11490
11580
  const prevModelID = state.modelID;
11491
11581
  if (requestModelLimit !== void 0) {
11492
11582
  state.modelContextLimit = requestModelLimit;
@@ -11541,10 +11631,11 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11541
11631
  }
11542
11632
  }
11543
11633
  ensureBuiltinFiltersRegistered();
11634
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
11544
11635
  applyMessageFilters(output.messages, config.messageFilters, logger, {
11545
11636
  sessionId: state.sessionId ?? "",
11546
11637
  isSubAgent: state.isSubAgent,
11547
- modelContextLimit: state.modelContextLimit
11638
+ modelContextLimit: effectiveLimit?.limit
11548
11639
  });
11549
11640
  cacheSystemPromptTokens(state, output.messages);
11550
11641
  assignMessageRefs(state, output.messages);
@@ -11595,14 +11686,31 @@ ${text}`);
11595
11686
  stripStaleMetadata(output.messages);
11596
11687
  dropEmptyMessages(output.messages);
11597
11688
  const postTokens = getCurrentTokenUsage(state, output.messages);
11689
+ if (postTokens !== void 0 && effectiveLimit) {
11690
+ const budget = effectiveLimit.limit - (state.systemPromptTokens ?? 0) - OUTPUT_RESERVE_TOKENS;
11691
+ if (postTokens > budget) {
11692
+ logger.error(
11693
+ "ACP hard guard: context exceeds model budget after in-flight reduction",
11694
+ {
11695
+ session: state.sessionId,
11696
+ postTokens,
11697
+ budget,
11698
+ contextLimit: effectiveLimit.limit,
11699
+ contextLimitSource: effectiveLimit.source,
11700
+ hint: "request will likely be rejected; run /compact or start a new session"
11701
+ }
11702
+ );
11703
+ }
11704
+ }
11598
11705
  logger.info("Chat transform complete", {
11599
11706
  session: state.sessionId,
11600
11707
  model: state.modelID,
11601
11708
  messages: output.messages.length,
11602
11709
  prePruneTokens,
11603
11710
  postTokens,
11604
- contextLimit: state.modelContextLimit,
11605
- usagePct: postTokens !== void 0 && state.modelContextLimit ? `${(postTokens / state.modelContextLimit * 100).toFixed(1)}%` : void 0,
11711
+ contextLimit: effectiveLimit?.limit,
11712
+ contextLimitSource: effectiveLimit?.source,
11713
+ usagePct: postTokens !== void 0 && effectiveLimit ? `${(postTokens / effectiveLimit.limit * 100).toFixed(1)}%` : void 0,
11606
11714
  nudged: state.nudges.shouldInjectThisTurn
11607
11715
  });
11608
11716
  if (state.sessionId) {
@@ -11653,7 +11761,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11653
11761
  const sub = input.arguments?.trim().toLowerCase();
11654
11762
  if (sub === "stats" || sub === "status" || sub === "") {
11655
11763
  await handleStatsCommand(commandCtx);
11656
- return;
11764
+ throw new Error("__DCP_CONTEXT_HANDLED__");
11657
11765
  }
11658
11766
  if (sub === "export" || sub.startsWith("export ")) {
11659
11767
  const exportArgs = input.arguments?.trim().slice("export".length).trim() || "";
@@ -11671,6 +11779,7 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
11671
11779
  throw new Error("__DCP_CONTEXT_HANDLED__");
11672
11780
  }
11673
11781
  await handleContextCommand(commandCtx);
11782
+ throw new Error("__DCP_CONTEXT_HANDLED__");
11674
11783
  }
11675
11784
  };
11676
11785
  }
@@ -12020,7 +12129,7 @@ var server = (async (ctx) => {
12020
12129
  }
12021
12130
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
12022
12131
  logger.info("ACP plugin initialized", {
12023
- version: true ? "1.16.0-pr.297.127" : "dev",
12132
+ version: true ? "1.16.0-pr.349.122" : "dev",
12024
12133
  workspace: ctx.directory,
12025
12134
  logLevel: logger.level,
12026
12135
  debug: config.debug,