opencode-acp 1.15.0 → 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
@@ -869,6 +869,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
869
869
  "autoUpdate",
870
870
  "debug",
871
871
  "logLevel",
872
+ "storagePath",
872
873
  "showUpdateToasts",
873
874
  "allowSubAgents",
874
875
  "pruneNotification",
@@ -889,6 +890,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
889
890
  "compress.modelMaxLimits",
890
891
  "compress.modelMinLimits",
891
892
  "compress.providers",
893
+ "compress.contextLimitFallback",
892
894
  "compress.nudgeFrequency",
893
895
  "compress.minNudgeContextPercent",
894
896
  "compress.nudgeGrowthTokens",
@@ -959,6 +961,9 @@ function validateConfigTypes(config) {
959
961
  if (config.debug !== void 0 && typeof config.debug !== "boolean") {
960
962
  errors.push({ key: "debug", expected: "boolean", actual: typeof config.debug });
961
963
  }
964
+ if (config.storagePath !== void 0 && typeof config.storagePath !== "string") {
965
+ errors.push({ key: "storagePath", expected: "string", actual: typeof config.storagePath });
966
+ }
962
967
  if (config.allowSubAgents !== void 0 && typeof config.allowSubAgents !== "boolean") {
963
968
  errors.push({ key: "allowSubAgents", expected: "boolean", actual: typeof config.allowSubAgents });
964
969
  }
@@ -1517,6 +1522,20 @@ function validateConfigTypes(config) {
1517
1522
  }
1518
1523
  };
1519
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
+ }
1520
1539
  const validValues = ["ask", "allow", "deny"];
1521
1540
  if (compress.permission !== void 0 && !validValues.includes(compress.permission)) {
1522
1541
  errors.push({
@@ -1698,6 +1717,7 @@ var defaultConfig = {
1698
1717
  summaryBuffer: true,
1699
1718
  maxContextLimit: "80%",
1700
1719
  minContextLimit: "80%",
1720
+ contextLimitFallback: 128e3,
1701
1721
  nudgeFrequency: 5,
1702
1722
  minNudgeContextPercent: 5,
1703
1723
  iterationNudgeThreshold: 15,
@@ -1868,6 +1888,7 @@ function mergeCompress(base, override) {
1868
1888
  modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
1869
1889
  modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
1870
1890
  providers: mergeProviderOverrides(base.providers, override.providers),
1891
+ contextLimitFallback: override.contextLimitFallback ?? base.contextLimitFallback,
1871
1892
  nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
1872
1893
  minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent,
1873
1894
  nudgeGrowthTokens: override.nudgeGrowthTokens,
@@ -1995,6 +2016,7 @@ function mergeLayer(config, data) {
1995
2016
  autoUpdate: data.autoUpdate ?? config.autoUpdate,
1996
2017
  debug: data.debug ?? config.debug,
1997
2018
  logLevel: data.logLevel ?? config.logLevel,
2019
+ storagePath: data.storagePath ?? config.storagePath,
1998
2020
  allowSubAgents: data.allowSubAgents ?? data.experimental?.allowSubAgents ?? config.allowSubAgents,
1999
2021
  pruneNotification: data.pruneNotification ?? config.pruneNotification,
2000
2022
  pruneNotificationType: data.pruneNotificationType ?? config.pruneNotificationType,
@@ -2349,7 +2371,7 @@ Each entry needs a topic \u2014 either its own or the top-level fallback.`;
2349
2371
  import * as fs from "fs/promises";
2350
2372
  import { existsSync as existsSync2 } from "fs";
2351
2373
  import { homedir as homedir2 } from "os";
2352
- import { join as join2 } from "path";
2374
+ import { isAbsolute, join as join2 } from "path";
2353
2375
 
2354
2376
  // lib/state/utils.ts
2355
2377
  var isMessageCompacted = (state, msg) => {
@@ -2630,9 +2652,19 @@ function resetOnCompaction(state) {
2630
2652
  nextRef: 1
2631
2653
  };
2632
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
+ }
2633
2665
 
2634
2666
  // lib/state/persistence.ts
2635
- function getStorageDir() {
2667
+ function getDefaultStorageDir() {
2636
2668
  return join2(
2637
2669
  process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
2638
2670
  "opencode",
@@ -2641,14 +2673,33 @@ function getStorageDir() {
2641
2673
  "acp"
2642
2674
  );
2643
2675
  }
2644
- function getSessionFilePath(sessionId) {
2645
- return join2(getStorageDir(), `${sessionId}.json`);
2676
+ function resolveStorageDir(configured, projectDir) {
2677
+ const trimmed = configured?.trim();
2678
+ if (!trimmed) {
2679
+ return getDefaultStorageDir();
2680
+ }
2681
+ if (trimmed === "~") {
2682
+ return homedir2();
2683
+ }
2684
+ if (trimmed.startsWith("~/")) {
2685
+ return join2(homedir2(), trimmed.slice(2));
2686
+ }
2687
+ if (isAbsolute(trimmed)) {
2688
+ return trimmed;
2689
+ }
2690
+ return join2(projectDir, trimmed);
2691
+ }
2692
+ function getStorageDir(override) {
2693
+ return override || getDefaultStorageDir();
2694
+ }
2695
+ function getSessionFilePath(sessionId, storageDir) {
2696
+ return join2(getStorageDir(storageDir), `${sessionId}.json`);
2646
2697
  }
2647
- async function writePersistedSessionState(sessionId, state, logger) {
2648
- const filePath = getSessionFilePath(sessionId);
2649
- const storageDir = getStorageDir();
2650
- if (!existsSync2(storageDir)) {
2651
- await fs.mkdir(storageDir, { recursive: true });
2698
+ async function writePersistedSessionState(sessionId, state, logger, storageDir) {
2699
+ const filePath = getSessionFilePath(sessionId, storageDir);
2700
+ const dir = getStorageDir(storageDir);
2701
+ if (!existsSync2(dir)) {
2702
+ await fs.mkdir(dir, { recursive: true });
2652
2703
  }
2653
2704
  const content = JSON.stringify(state, null, 2);
2654
2705
  await fs.writeFile(filePath, content, "utf-8");
@@ -2690,11 +2741,11 @@ async function saveSessionState(sessionState, logger, sessionName) {
2690
2741
  modelProviderID: sessionState.modelProviderID,
2691
2742
  modelID: sessionState.modelID
2692
2743
  };
2693
- await writePersistedSessionState(sessionState.sessionId, state, logger);
2744
+ await writePersistedSessionState(sessionState.sessionId, state, logger, sessionState.storageDir);
2694
2745
  }
2695
- async function loadSessionState(sessionId, logger) {
2746
+ async function loadSessionState(sessionId, logger, storageDir) {
2696
2747
  try {
2697
- const filePath = getSessionFilePath(sessionId);
2748
+ const filePath = getSessionFilePath(sessionId, storageDir);
2698
2749
  if (!existsSync2(filePath)) {
2699
2750
  return null;
2700
2751
  }
@@ -2770,6 +2821,11 @@ async function loadSessionState(sessionId, logger) {
2770
2821
  }
2771
2822
  }
2772
2823
 
2824
+ // lib/state/state.ts
2825
+ import { existsSync as existsSync3 } from "fs";
2826
+ import { join as join3 } from "path";
2827
+ import { cwd } from "process";
2828
+
2773
2829
  // lib/message-ids.ts
2774
2830
  var MESSAGE_REF_REGEX = /^m(\d{4,5})$/;
2775
2831
  var BLOCK_REF_REGEX = /^b([1-9]\d*)$/;
@@ -4446,10 +4502,12 @@ async function updatePerTurnState(state, logger, messages) {
4446
4502
  }
4447
4503
  var REGISTRY_SOFT_CAP = 32;
4448
4504
  var SessionStateRegistry = class {
4449
- constructor(logger) {
4505
+ constructor(logger, projectDir) {
4450
4506
  this.logger = logger;
4507
+ this.projectDir = projectDir;
4451
4508
  }
4452
4509
  logger;
4510
+ projectDir;
4453
4511
  states = /* @__PURE__ */ new Map();
4454
4512
  compressionTiming = {
4455
4513
  startsByCallId: /* @__PURE__ */ new Map(),
@@ -4475,6 +4533,24 @@ var SessionStateRegistry = class {
4475
4533
  hydrateModelLimitsFromClient(client) {
4476
4534
  return this.catalog.hydrateFromClient(client);
4477
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
+ }
4478
4554
  get(sessionId) {
4479
4555
  return this.states.get(sessionId);
4480
4556
  }
@@ -4502,7 +4578,8 @@ var SessionStateRegistry = class {
4502
4578
  sessionId,
4503
4579
  this.logger,
4504
4580
  messages,
4505
- config
4581
+ config,
4582
+ this.projectDir
4506
4583
  );
4507
4584
  } catch (err) {
4508
4585
  this.logger.error("Failed to initialize session state", {
@@ -4566,6 +4643,7 @@ function createSessionState() {
4566
4643
  modelProviderID: void 0,
4567
4644
  modelID: void 0,
4568
4645
  systemPromptTokens: void 0,
4646
+ storageDir: void 0,
4569
4647
  qualityGateRetryPending: false
4570
4648
  };
4571
4649
  }
@@ -4607,21 +4685,30 @@ function resetSessionState(state) {
4607
4685
  state.modelProviderID = void 0;
4608
4686
  state.modelID = void 0;
4609
4687
  state.systemPromptTokens = void 0;
4688
+ state.storageDir = void 0;
4610
4689
  state.qualityGateRetryPending = false;
4611
4690
  }
4612
- async function ensureSessionInitialized(client, state, sessionId, logger, messages, config) {
4691
+ async function ensureSessionInitialized(client, state, sessionId, logger, messages, config, projectDir) {
4613
4692
  if (state.sessionId === sessionId) {
4614
4693
  return;
4615
4694
  }
4616
4695
  resetSessionState(state);
4617
4696
  state.sessionId = sessionId;
4697
+ state.storageDir = config?.storagePath ? resolveStorageDir(config.storagePath, projectDir ?? cwd()) : void 0;
4618
4698
  const isSubAgent = await isSubAgentSession(client, sessionId);
4619
4699
  state.isSubAgent = isSubAgent;
4620
4700
  state.lastCompaction = findLastCompactionTimestamp(messages);
4621
4701
  state.currentTurn = countTurns(state, messages);
4622
4702
  state.nudges.turnNudgeAnchors = collectTurnNudgeAnchors(messages);
4623
- const persisted = await loadSessionState(sessionId, logger);
4703
+ const persisted = await loadSessionState(sessionId, logger, state.storageDir);
4624
4704
  if (persisted === null) {
4705
+ const defaultPath = join3(getDefaultStorageDir(), `${sessionId}.json`);
4706
+ if (state.storageDir && existsSync3(defaultPath)) {
4707
+ logger.warn(
4708
+ "storagePath is set but no valid state was found there; a state file exists at the default location \u2014 move it manually to keep history",
4709
+ { sessionId, storageDir: state.storageDir, defaultPath }
4710
+ );
4711
+ }
4625
4712
  if (config) {
4626
4713
  const rebuilt = rebuildCompressionState(state, messages, config, logger);
4627
4714
  if (rebuilt > 0) {
@@ -6615,6 +6702,7 @@ function getModelInfo(messages) {
6615
6702
  };
6616
6703
  }
6617
6704
  function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
6705
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
6618
6706
  const parseLimitValue = (limit) => {
6619
6707
  if (limit === void 0) {
6620
6708
  return void 0;
@@ -6622,7 +6710,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6622
6710
  if (typeof limit === "number") {
6623
6711
  return limit;
6624
6712
  }
6625
- if (!limit.endsWith("%") || state.modelContextLimit === void 0) {
6713
+ if (!limit.endsWith("%") || effectiveLimit === void 0) {
6626
6714
  return void 0;
6627
6715
  }
6628
6716
  const parsedPercent = parseFloat(limit.slice(0, -1));
@@ -6631,7 +6719,7 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
6631
6719
  }
6632
6720
  const roundedPercent = Math.round(parsedPercent);
6633
6721
  const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
6634
- return Math.round(clampedPercent / 100 * state.modelContextLimit);
6722
+ return Math.round(clampedPercent / 100 * effectiveLimit.limit);
6635
6723
  };
6636
6724
  if (threshold === "max") {
6637
6725
  const nestedLimit = resolveCompressOverrides(config, providerId, modelId).maxContextLimit;
@@ -6682,11 +6770,12 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
6682
6770
  if (!overMaxLimit) break;
6683
6771
  }
6684
6772
  }
6773
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
6685
6774
  return {
6686
6775
  overMaxLimit,
6687
6776
  overMinLimit,
6688
6777
  currentTokens,
6689
- modelContextLimit: state.modelContextLimit
6778
+ modelContextLimit: effectiveLimit?.limit
6690
6779
  };
6691
6780
  }
6692
6781
  ensureBuiltinTriggerPolicyRegistered();
@@ -8765,8 +8854,9 @@ function createDecompressTool(factoryCtx) {
8765
8854
  async execute(args, toolCtx) {
8766
8855
  const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
8767
8856
  const { rawMessages } = await prepareDecompressSession(ctx, toolCtx);
8768
- const contextUsageBefore = ctx.state.modelContextLimit ? Math.round(
8769
- 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
8770
8860
  ) : void 0;
8771
8861
  const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
8772
8862
  if (!resolved.ok) {
@@ -8830,8 +8920,9 @@ function createDecompressTool(factoryCtx) {
8830
8920
  0,
8831
8921
  ctx.state.stats.totalPruneTokens - restoredTokens
8832
8922
  );
8833
- const contextUsageAfter = ctx.state.modelContextLimit ? Math.round(
8834
- 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
8835
8926
  ) : void 0;
8836
8927
  await finalizeDecompressSession(ctx);
8837
8928
  const restoredContentPreview = buildRestoredContentPreview(
@@ -9485,10 +9576,10 @@ function hideFailedCompressCalls(messages) {
9485
9576
 
9486
9577
  // lib/logger.ts
9487
9578
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9488
- import { join as join3 } from "path";
9489
- import { existsSync as existsSync3 } from "fs";
9579
+ import { join as join4 } from "path";
9580
+ import { existsSync as existsSync4 } from "fs";
9490
9581
  import { homedir as homedir3 } from "os";
9491
- var LOG_VERSION = true ? "1.15.0" : "dev";
9582
+ var LOG_VERSION = true ? "1.16.0-pr.349.122" : "dev";
9492
9583
  var LEVEL_RANK = {
9493
9584
  debug: 10,
9494
9585
  info: 20,
@@ -9508,8 +9599,8 @@ var Logger = class {
9508
9599
  constructor(enabled, level) {
9509
9600
  const resolved = level !== void 0 && LEVEL_RANK[level] !== void 0 ? level : void 0;
9510
9601
  this.level = resolved ?? (enabled ? "debug" : "warn");
9511
- const configHome = process.env.XDG_CONFIG_HOME || join3(homedir3(), ".config");
9512
- this.logDir = join3(configHome, "opencode", "logs", "acp");
9602
+ const configHome = process.env.XDG_CONFIG_HOME || join4(homedir3(), ".config");
9603
+ this.logDir = join4(configHome, "opencode", "logs", "acp");
9513
9604
  }
9514
9605
  /** Back-compat view of the old flag: true only at full debug verbosity. */
9515
9606
  get enabled() {
@@ -9519,7 +9610,7 @@ var Logger = class {
9519
9610
  return LEVEL_RANK[level] >= LEVEL_RANK[this.level];
9520
9611
  }
9521
9612
  async ensureLogDir() {
9522
- if (!existsSync3(this.logDir)) {
9613
+ if (!existsSync4(this.logDir)) {
9523
9614
  await mkdir2(this.logDir, { recursive: true });
9524
9615
  }
9525
9616
  }
@@ -9571,11 +9662,11 @@ var Logger = class {
9571
9662
  const dataStr = this.formatData(data);
9572
9663
  const logLine = `${timestamp} ${level.padEnd(5)} ${component}: ${message}${dataStr ? " | " + dataStr : ""} | v=${LOG_VERSION}
9573
9664
  `;
9574
- const dailyLogDir = join3(this.logDir, "daily");
9575
- if (!existsSync3(dailyLogDir)) {
9665
+ const dailyLogDir = join4(this.logDir, "daily");
9666
+ if (!existsSync4(dailyLogDir)) {
9576
9667
  await mkdir2(dailyLogDir, { recursive: true });
9577
9668
  }
9578
- const logFile = join3(dailyLogDir, `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
9669
+ const logFile = join4(dailyLogDir, `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
9579
9670
  await writeFile2(logFile, logLine, { flag: "a" });
9580
9671
  } catch (error) {
9581
9672
  }
@@ -9686,18 +9777,18 @@ var Logger = class {
9686
9777
  async saveContext(sessionId, messages) {
9687
9778
  if (this.level !== "debug") return;
9688
9779
  try {
9689
- const contextDir = join3(this.logDir, "context", sessionId);
9690
- if (!existsSync3(contextDir)) {
9780
+ const contextDir = join4(this.logDir, "context", sessionId);
9781
+ if (!existsSync4(contextDir)) {
9691
9782
  await mkdir2(contextDir, { recursive: true });
9692
9783
  }
9693
9784
  const minimized = this.minimizeForDebug(messages).filter(
9694
9785
  (msg) => msg.parts && msg.parts.length > 0
9695
9786
  );
9696
9787
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9697
- const contextFile = join3(contextDir, `${timestamp}.json`);
9788
+ const contextFile = join4(contextDir, `${timestamp}.json`);
9698
9789
  await writeFile2(contextFile, JSON.stringify(minimized, null, 2));
9699
- const versionFile = join3(contextDir, "_version");
9700
- if (!existsSync3(versionFile)) {
9790
+ const versionFile = join4(contextDir, "_version");
9791
+ if (!existsSync4(versionFile)) {
9701
9792
  await writeFile2(versionFile, LOG_VERSION);
9702
9793
  }
9703
9794
  } catch (error) {
@@ -9706,8 +9797,8 @@ var Logger = class {
9706
9797
  };
9707
9798
 
9708
9799
  // lib/prompts/store.ts
9709
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync as statSync2 } from "fs";
9710
- import { join as join4, dirname as dirname2 } from "path";
9800
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync as statSync2 } from "fs";
9801
+ import { join as join5, dirname as dirname2 } from "path";
9711
9802
  import { homedir as homedir4 } from "os";
9712
9803
 
9713
9804
  // lib/prompts/system.ts
@@ -10037,8 +10128,8 @@ function createBundledRuntimePrompts() {
10037
10128
  function findOpencodeDir2(startDir) {
10038
10129
  let current = startDir;
10039
10130
  while (current !== "/") {
10040
- const candidate = join4(current, ".opencode");
10041
- if (existsSync4(candidate)) {
10131
+ const candidate = join5(current, ".opencode");
10132
+ if (existsSync5(candidate)) {
10042
10133
  try {
10043
10134
  if (statSync2(candidate).isDirectory()) {
10044
10135
  return candidate;
@@ -10055,13 +10146,13 @@ function findOpencodeDir2(startDir) {
10055
10146
  return null;
10056
10147
  }
10057
10148
  function resolvePromptPaths(workingDirectory) {
10058
- const configHome = process.env.XDG_CONFIG_HOME || join4(homedir4(), ".config");
10059
- const globalRoot = join4(configHome, "opencode", "acp-prompts");
10060
- const defaultsDir = join4(globalRoot, "defaults");
10061
- const globalOverridesDir = join4(globalRoot, "overrides");
10062
- const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR ? join4(process.env.OPENCODE_CONFIG_DIR, "acp-prompts", "overrides") : null;
10149
+ const configHome = process.env.XDG_CONFIG_HOME || join5(homedir4(), ".config");
10150
+ const globalRoot = join5(configHome, "opencode", "acp-prompts");
10151
+ const defaultsDir = join5(globalRoot, "defaults");
10152
+ const globalOverridesDir = join5(globalRoot, "overrides");
10153
+ const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR ? join5(process.env.OPENCODE_CONFIG_DIR, "acp-prompts", "overrides") : null;
10063
10154
  const opencodeDir = findOpencodeDir2(workingDirectory);
10064
- const projectOverridesDir = opencodeDir ? join4(opencodeDir, "acp-prompts", "overrides") : null;
10155
+ const projectOverridesDir = opencodeDir ? join5(opencodeDir, "acp-prompts", "overrides") : null;
10065
10156
  return {
10066
10157
  defaultsDir,
10067
10158
  globalOverridesDir,
@@ -10161,7 +10252,7 @@ function buildDefaultsReadmeContent() {
10161
10252
  `;
10162
10253
  }
10163
10254
  function readFileIfExists(filePath) {
10164
- if (!existsSync4(filePath)) {
10255
+ if (!existsSync5(filePath)) {
10165
10256
  return null;
10166
10257
  }
10167
10258
  try {
@@ -10232,16 +10323,16 @@ var PromptStore = class {
10232
10323
  const candidates = [];
10233
10324
  if (this.paths.projectOverridesDir) {
10234
10325
  candidates.push({
10235
- path: join4(this.paths.projectOverridesDir, fileName)
10326
+ path: join5(this.paths.projectOverridesDir, fileName)
10236
10327
  });
10237
10328
  }
10238
10329
  if (this.paths.configDirOverridesDir) {
10239
10330
  candidates.push({
10240
- path: join4(this.paths.configDirOverridesDir, fileName)
10331
+ path: join5(this.paths.configDirOverridesDir, fileName)
10241
10332
  });
10242
10333
  }
10243
10334
  candidates.push({
10244
- path: join4(this.paths.globalOverridesDir, fileName)
10335
+ path: join5(this.paths.globalOverridesDir, fileName)
10245
10336
  });
10246
10337
  return candidates;
10247
10338
  }
@@ -10264,7 +10355,7 @@ var PromptStore = class {
10264
10355
  const managedContent = buildDefaultPromptFileContent(
10265
10356
  bundledEditable || BUNDLED_EDITABLE_PROMPTS[definition.runtimeField]
10266
10357
  );
10267
- const filePath = join4(this.paths.defaultsDir, definition.fileName);
10358
+ const filePath = join5(this.paths.defaultsDir, definition.fileName);
10268
10359
  try {
10269
10360
  const existing = readFileIfExists(filePath);
10270
10361
  if (existing === managedContent) {
@@ -10278,7 +10369,7 @@ var PromptStore = class {
10278
10369
  });
10279
10370
  }
10280
10371
  }
10281
- const readmePath = join4(this.paths.defaultsDir, DEFAULTS_README_FILE);
10372
+ const readmePath = join5(this.paths.defaultsDir, DEFAULTS_README_FILE);
10282
10373
  const readmeContent = buildDefaultsReadmeContent();
10283
10374
  try {
10284
10375
  const existing = readFileIfExists(readmePath);
@@ -10312,6 +10403,8 @@ var MIN_OUTPUT_TOKENS = 1e3;
10312
10403
  var KEEP_PREFIX_CHARS = 2e3;
10313
10404
  var KEEP_SUFFIX_CHARS = 2e3;
10314
10405
  var PROTECT_RECENT_MESSAGES = 3;
10406
+ var OUTPUT_RESERVE_TOKENS = 16384;
10407
+ var overheadErrorLogged = /* @__PURE__ */ new Set();
10315
10408
  function parseGcThreshold(threshold, modelContextLimit) {
10316
10409
  if (typeof threshold === "number") return threshold;
10317
10410
  const str = threshold ?? "100%";
@@ -10320,10 +10413,26 @@ function parseGcThreshold(threshold, modelContextLimit) {
10320
10413
  return modelContextLimit;
10321
10414
  }
10322
10415
  function truncateLargeToolOutputs(state, config, logger, messages) {
10323
- if (!state.modelContextLimit) return;
10416
+ const effective = resolveEffectiveContextLimit(state, config);
10417
+ if (!effective) return;
10324
10418
  const currentTokens = getCurrentTokenUsage(state, messages);
10325
10419
  if (currentTokens === 0) return;
10326
- 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
+ }
10327
10436
  if (currentTokens < threshold) return;
10328
10437
  const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES;
10329
10438
  const candidates = [];
@@ -10368,7 +10477,9 @@ function truncateLargeToolOutputs(state, config, logger, messages) {
10368
10477
  truncatedCount,
10369
10478
  estimatedSavedTokens: Math.round(savedTokens),
10370
10479
  currentTokens,
10371
- threshold
10480
+ threshold,
10481
+ contextLimit: effective.limit,
10482
+ contextLimitSource: effective.source
10372
10483
  });
10373
10484
  }
10374
10485
  }
@@ -10575,8 +10686,8 @@ ${report}`;
10575
10686
 
10576
10687
  // lib/commands/export.ts
10577
10688
  import * as fs2 from "fs/promises";
10578
- import { existsSync as existsSync5 } from "fs";
10579
- import { dirname as dirname3, isAbsolute, join as join5, resolve } from "path";
10689
+ import { existsSync as existsSync6 } from "fs";
10690
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, resolve } from "path";
10580
10691
  var ALL_TIERS = [1, 2, 3];
10581
10692
  var TIER_NAMES = {
10582
10693
  1: "Tier 1 \u2014 Capture",
@@ -10687,9 +10798,9 @@ function parseTiers(raw) {
10687
10798
  }
10688
10799
  return result;
10689
10800
  }
10690
- function resolveDefaultOutputPath(sessionId, cwd) {
10801
+ function resolveDefaultOutputPath(sessionId, cwd2) {
10691
10802
  const short = (sessionId || "session").slice(0, 8);
10692
- return join5(cwd, ".opencode", `acp-export-${short}.md`);
10803
+ return join6(cwd2, ".opencode", `acp-export-${short}.md`);
10693
10804
  }
10694
10805
  function formatTokens2(n) {
10695
10806
  if (!Number.isFinite(n) || n <= 0) return "0";
@@ -10816,15 +10927,15 @@ async function handleExportCommand(ctx, args) {
10816
10927
  await sendExportNotice(client, sessionId, ctx, markdown);
10817
10928
  return;
10818
10929
  }
10819
- const cwd = ctx.workingDirectory || process.cwd();
10820
- const targetPath = options.outputPath !== "" ? isAbsolute(options.outputPath) ? options.outputPath : resolve(cwd, options.outputPath) : resolveDefaultOutputPath(sessionId, cwd);
10930
+ const cwd2 = ctx.workingDirectory || process.cwd();
10931
+ const targetPath = options.outputPath !== "" ? isAbsolute2(options.outputPath) ? options.outputPath : resolve(cwd2, options.outputPath) : resolveDefaultOutputPath(sessionId, cwd2);
10821
10932
  try {
10822
10933
  const dir = dirname3(targetPath);
10823
- if (!existsSync5(dir)) {
10934
+ if (!existsSync6(dir)) {
10824
10935
  await fs2.mkdir(dir, { recursive: true });
10825
10936
  }
10826
10937
  const flag = options.append ? "a" : "w";
10827
- if (options.append && existsSync5(targetPath)) {
10938
+ if (options.append && existsSync6(targetPath)) {
10828
10939
  await fs2.appendFile(targetPath, `
10829
10940
 
10830
10941
  ---
@@ -11329,11 +11440,12 @@ function runBatchCleanup(state, config, logger, messages) {
11329
11440
  mergedCount: 0,
11330
11441
  savedTokens: 0
11331
11442
  };
11332
- if (!state.modelContextLimit || state.modelContextLimit <= 0) {
11443
+ const effective = resolveEffectiveContextLimit(state, config);
11444
+ if (!effective) {
11333
11445
  return noop;
11334
11446
  }
11335
11447
  const currentTokens = getCurrentTokenUsage(state, messages);
11336
- if (currentTokens < state.modelContextLimit) {
11448
+ if (currentTokens < effective.limit) {
11337
11449
  return noop;
11338
11450
  }
11339
11451
  const maxMergedLength = config.gc.maxOldGenSummaryLength;
@@ -11350,7 +11462,8 @@ function runBatchCleanup(state, config, logger, messages) {
11350
11462
  mergedCount: result.mergedCount,
11351
11463
  savedTokens: result.savedTokens,
11352
11464
  currentTokens,
11353
- contextLimit: state.modelContextLimit
11465
+ contextLimit: effective.limit,
11466
+ contextLimitSource: effective.source
11354
11467
  });
11355
11468
  return {
11356
11469
  tier: 3,
@@ -11384,11 +11497,6 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11384
11497
  input.model?.limit?.context
11385
11498
  );
11386
11499
  const state = input.sessionID ? registry4.get(input.sessionID) : void 0;
11387
- if (state && input.model?.limit?.context) {
11388
- state.modelContextLimit = input.model.limit.context;
11389
- state.modelProviderID = input.model?.providerID;
11390
- state.modelID = input.model?.id;
11391
- }
11392
11500
  if (!state || state.isSubAgent && !config.allowSubAgents) {
11393
11501
  return;
11394
11502
  }
@@ -11397,6 +11505,23 @@ function createSystemPromptHandler(registry4, logger, config, prompts) {
11397
11505
  logger.info("Skipping DCP system prompt injection for internal agent");
11398
11506
  return;
11399
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
+ }
11400
11525
  const effectivePermission = compressPermission(state, config);
11401
11526
  if (effectivePermission === "deny") {
11402
11527
  return;
@@ -11441,10 +11566,17 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11441
11566
  config
11442
11567
  );
11443
11568
  const requestModel = lastUserMessage.info.model;
11444
- const requestModelLimit = registry4.resolveModelLimit(
11569
+ let requestModelLimit = registry4.resolveModelLimit(
11445
11570
  requestModel?.providerID,
11446
11571
  requestModel?.modelID
11447
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
+ }
11448
11580
  const prevModelID = state.modelID;
11449
11581
  if (requestModelLimit !== void 0) {
11450
11582
  state.modelContextLimit = requestModelLimit;
@@ -11499,10 +11631,11 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
11499
11631
  }
11500
11632
  }
11501
11633
  ensureBuiltinFiltersRegistered();
11634
+ const effectiveLimit = resolveEffectiveContextLimit(state, config);
11502
11635
  applyMessageFilters(output.messages, config.messageFilters, logger, {
11503
11636
  sessionId: state.sessionId ?? "",
11504
11637
  isSubAgent: state.isSubAgent,
11505
- modelContextLimit: state.modelContextLimit
11638
+ modelContextLimit: effectiveLimit?.limit
11506
11639
  });
11507
11640
  cacheSystemPromptTokens(state, output.messages);
11508
11641
  assignMessageRefs(state, output.messages);
@@ -11553,14 +11686,31 @@ ${text}`);
11553
11686
  stripStaleMetadata(output.messages);
11554
11687
  dropEmptyMessages(output.messages);
11555
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
+ }
11556
11705
  logger.info("Chat transform complete", {
11557
11706
  session: state.sessionId,
11558
11707
  model: state.modelID,
11559
11708
  messages: output.messages.length,
11560
11709
  prePruneTokens,
11561
11710
  postTokens,
11562
- contextLimit: state.modelContextLimit,
11563
- 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,
11564
11714
  nudged: state.nudges.shouldInjectThisTurn
11565
11715
  });
11566
11716
  if (state.sessionId) {
@@ -11767,7 +11917,7 @@ function findBiliProxyProviders(provider) {
11767
11917
 
11768
11918
  // lib/update.ts
11769
11919
  import { readFile as readFile2, rm } from "fs/promises";
11770
- import { basename, dirname as dirname4, join as join6 } from "path";
11920
+ import { basename, dirname as dirname4, join as join7 } from "path";
11771
11921
  import { fileURLToPath } from "url";
11772
11922
  var PACKAGE_NAME = "opencode-acp";
11773
11923
  function startAutoUpdate(ctx, enabled, logger) {
@@ -11814,7 +11964,7 @@ async function checkAutoUpdate(signal, logger) {
11814
11964
  logger?.info("Auto-update skipped: package dir not found");
11815
11965
  return { updated: false };
11816
11966
  }
11817
- const pkg = await readPackageJson(join6(packageDir, "package.json"));
11967
+ const pkg = await readPackageJson(join7(packageDir, "package.json"));
11818
11968
  if (!pkg?.name || !pkg.version) {
11819
11969
  logger?.info("Auto-update skipped: package.json unreadable", { packageDir });
11820
11970
  return { updated: false };
@@ -11855,7 +12005,7 @@ async function checkAutoUpdate(signal, logger) {
11855
12005
  async function findPackageDir(name) {
11856
12006
  let dir = dirname4(fileURLToPath(import.meta.url));
11857
12007
  for (; ; ) {
11858
- const pkg = await readPackageJson(join6(dir, "package.json"));
12008
+ const pkg = await readPackageJson(join7(dir, "package.json"));
11859
12009
  if (pkg?.name === name) return dir;
11860
12010
  const parent = dirname4(dir);
11861
12011
  if (parent === dir) return void 0;
@@ -11867,7 +12017,7 @@ async function updateTarget(packageDir, name) {
11867
12017
  const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname4(packageParent) : packageParent;
11868
12018
  if (basename(nodeModulesDir) !== "node_modules") return void 0;
11869
12019
  const wrapperDir = dirname4(nodeModulesDir);
11870
- const wrapperPkg = await readPackageJson(join6(wrapperDir, "package.json"));
12020
+ const wrapperPkg = await readPackageJson(join7(wrapperDir, "package.json"));
11871
12021
  const spec = wrapperSpec(wrapperDir, name) ?? wrapperPkg?.dependencies?.[name];
11872
12022
  if (!spec || !isAutoUpdatableSpec(spec)) return void 0;
11873
12023
  return { removeDir: wrapperDir, spec };
@@ -11979,14 +12129,14 @@ var server = (async (ctx) => {
11979
12129
  }
11980
12130
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
11981
12131
  logger.info("ACP plugin initialized", {
11982
- version: true ? "1.15.0" : "dev",
12132
+ version: true ? "1.16.0-pr.349.122" : "dev",
11983
12133
  workspace: ctx.directory,
11984
12134
  logLevel: logger.level,
11985
12135
  debug: config.debug,
11986
12136
  autoUpdate: config.autoUpdate,
11987
12137
  secureMode: isSecureMode()
11988
12138
  });
11989
- const registry4 = new SessionStateRegistry(logger);
12139
+ const registry4 = new SessionStateRegistry(logger, ctx.directory);
11990
12140
  const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts);
11991
12141
  const hostPermissions = {
11992
12142
  global: void 0,