opencode-acp 1.8.0 → 1.8.1

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
@@ -895,6 +895,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
895
895
  "compress.modelMinLimits",
896
896
  "compress.nudgeFrequency",
897
897
  "compress.minNudgeContextPercent",
898
+ "compress.nudgeGrowthTokens",
898
899
  "compress.iterationNudgeThreshold",
899
900
  "compress.nudgeForce",
900
901
  "compress.protectedTools",
@@ -1645,6 +1646,7 @@ function mergeCompress(base, override) {
1645
1646
  modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
1646
1647
  nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
1647
1648
  minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent,
1649
+ nudgeGrowthTokens: override.nudgeGrowthTokens,
1648
1650
  iterationNudgeThreshold: override.iterationNudgeThreshold ?? base.iterationNudgeThreshold,
1649
1651
  nudgeForce: override.nudgeForce ?? base.nudgeForce,
1650
1652
  protectedTools: [.../* @__PURE__ */ new Set([...base.protectedTools, ...override.protectedTools ?? []])],
@@ -3185,7 +3187,8 @@ function resetOnCompaction(state) {
3185
3187
  turnNudgeAnchors: /* @__PURE__ */ new Set(),
3186
3188
  iterationNudgeAnchors: /* @__PURE__ */ new Set(),
3187
3189
  lastPerMessageNudgeTurn: 0,
3188
- lastPerMessageNudgeTokens: 0
3190
+ lastPerMessageNudgeTokens: void 0,
3191
+ shouldInjectThisTurn: void 0
3189
3192
  };
3190
3193
  state.messageIds = {
3191
3194
  byRawId: /* @__PURE__ */ new Map(),
@@ -3253,7 +3256,7 @@ async function saveSessionState(sessionState, logger, sessionName) {
3253
3256
  turnNudgeAnchors: Array.from(sessionState.nudges.turnNudgeAnchors),
3254
3257
  iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors),
3255
3258
  lastPerMessageNudgeTurn: sessionState.nudges.lastPerMessageNudgeTurn ?? 0,
3256
- lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens ?? 0
3259
+ lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens
3257
3260
  },
3258
3261
  stats: sessionState.stats,
3259
3262
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3469,7 +3472,8 @@ function createSessionState() {
3469
3472
  turnNudgeAnchors: /* @__PURE__ */ new Set(),
3470
3473
  iterationNudgeAnchors: /* @__PURE__ */ new Set(),
3471
3474
  lastPerMessageNudgeTurn: 0,
3472
- lastPerMessageNudgeTokens: 0
3475
+ lastPerMessageNudgeTokens: void 0,
3476
+ shouldInjectThisTurn: void 0
3473
3477
  },
3474
3478
  stats: {
3475
3479
  pruneTokenCounter: 0,
@@ -3508,7 +3512,8 @@ function resetSessionState(state) {
3508
3512
  turnNudgeAnchors: /* @__PURE__ */ new Set(),
3509
3513
  iterationNudgeAnchors: /* @__PURE__ */ new Set(),
3510
3514
  lastPerMessageNudgeTurn: 0,
3511
- lastPerMessageNudgeTokens: 0
3515
+ lastPerMessageNudgeTokens: void 0,
3516
+ shouldInjectThisTurn: void 0
3512
3517
  };
3513
3518
  state.stats = {
3514
3519
  pruneTokenCounter: 0,
@@ -3554,7 +3559,7 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
3554
3559
  persisted.nudges.iterationNudgeAnchors || []
3555
3560
  );
3556
3561
  state.nudges.lastPerMessageNudgeTurn = persisted.nudges.lastPerMessageNudgeTurn ?? 0;
3557
- state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens ?? 0;
3562
+ state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens;
3558
3563
  state.stats = {
3559
3564
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
3560
3565
  totalPruneTokens: persisted.stats?.totalPruneTokens || 0
@@ -3704,20 +3709,20 @@ function matchesGlob(inputPath, pattern) {
3704
3709
  regex += "$";
3705
3710
  return new RegExp(regex).test(input);
3706
3711
  }
3707
- function getFilePathsFromParameters(tool5, parameters) {
3712
+ function getFilePathsFromParameters(tool6, parameters) {
3708
3713
  if (typeof parameters !== "object" || parameters === null) {
3709
3714
  return [];
3710
3715
  }
3711
3716
  const paths = [];
3712
3717
  const params = parameters;
3713
- if (tool5 === "apply_patch" && typeof params.patchText === "string") {
3718
+ if (tool6 === "apply_patch" && typeof params.patchText === "string") {
3714
3719
  const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
3715
3720
  let match;
3716
3721
  while ((match = pathRegex.exec(params.patchText)) !== null) {
3717
3722
  paths.push(match[1].trim());
3718
3723
  }
3719
3724
  }
3720
- if (tool5 === "multiedit") {
3725
+ if (tool6 === "multiedit") {
3721
3726
  if (typeof params.filePath === "string") {
3722
3727
  paths.push(params.filePath);
3723
3728
  }
@@ -3812,13 +3817,13 @@ var deduplicate = (state, logger, config, messages) => {
3812
3817
  logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`);
3813
3818
  }
3814
3819
  };
3815
- function createToolSignature(tool5, parameters) {
3820
+ function createToolSignature(tool6, parameters) {
3816
3821
  if (!parameters) {
3817
- return tool5;
3822
+ return tool6;
3818
3823
  }
3819
3824
  const normalized = normalizeParameters(parameters);
3820
3825
  const sorted = sortObjectKeys(normalized);
3821
- return `${tool5}::${JSON.stringify(sorted)}`;
3826
+ return `${tool6}::${JSON.stringify(sorted)}`;
3822
3827
  }
3823
3828
  function normalizeParameters(params) {
3824
3829
  if (typeof params !== "object" || params === null) return params;
@@ -3893,9 +3898,9 @@ var purgeErrors = (state, logger, config, messages) => {
3893
3898
  };
3894
3899
 
3895
3900
  // lib/ui/utils.ts
3896
- function extractParameterKey(tool5, parameters) {
3901
+ function extractParameterKey(tool6, parameters) {
3897
3902
  if (!parameters) return "";
3898
- if (tool5 === "read" && parameters.filePath) {
3903
+ if (tool6 === "read" && parameters.filePath) {
3899
3904
  const offset = parameters.offset;
3900
3905
  const limit = parameters.limit;
3901
3906
  if (offset !== void 0 && limit !== void 0) {
@@ -3909,10 +3914,10 @@ function extractParameterKey(tool5, parameters) {
3909
3914
  }
3910
3915
  return parameters.filePath;
3911
3916
  }
3912
- if ((tool5 === "write" || tool5 === "edit" || tool5 === "multiedit") && parameters.filePath) {
3917
+ if ((tool6 === "write" || tool6 === "edit" || tool6 === "multiedit") && parameters.filePath) {
3913
3918
  return parameters.filePath;
3914
3919
  }
3915
- if (tool5 === "apply_patch" && typeof parameters.patchText === "string") {
3920
+ if (tool6 === "apply_patch" && typeof parameters.patchText === "string") {
3916
3921
  const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
3917
3922
  const paths = [];
3918
3923
  let match;
@@ -3929,51 +3934,51 @@ function extractParameterKey(tool5, parameters) {
3929
3934
  }
3930
3935
  return "patch";
3931
3936
  }
3932
- if (tool5 === "list") {
3937
+ if (tool6 === "list") {
3933
3938
  return parameters.path || "(current directory)";
3934
3939
  }
3935
- if (tool5 === "glob") {
3940
+ if (tool6 === "glob") {
3936
3941
  if (parameters.pattern) {
3937
3942
  const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
3938
3943
  return `"${parameters.pattern}"${pathInfo}`;
3939
3944
  }
3940
3945
  return "(unknown pattern)";
3941
3946
  }
3942
- if (tool5 === "grep") {
3947
+ if (tool6 === "grep") {
3943
3948
  if (parameters.pattern) {
3944
3949
  const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
3945
3950
  return `"${parameters.pattern}"${pathInfo}`;
3946
3951
  }
3947
3952
  return "(unknown pattern)";
3948
3953
  }
3949
- if (tool5 === "bash") {
3954
+ if (tool6 === "bash") {
3950
3955
  if (parameters.description) return parameters.description;
3951
3956
  if (parameters.command) {
3952
3957
  return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command;
3953
3958
  }
3954
3959
  }
3955
- if (tool5 === "webfetch" && parameters.url) {
3960
+ if (tool6 === "webfetch" && parameters.url) {
3956
3961
  return parameters.url;
3957
3962
  }
3958
- if (tool5 === "websearch" && parameters.query) {
3963
+ if (tool6 === "websearch" && parameters.query) {
3959
3964
  return `"${parameters.query}"`;
3960
3965
  }
3961
- if (tool5 === "codesearch" && parameters.query) {
3966
+ if (tool6 === "codesearch" && parameters.query) {
3962
3967
  return `"${parameters.query}"`;
3963
3968
  }
3964
- if (tool5 === "todowrite") {
3969
+ if (tool6 === "todowrite") {
3965
3970
  return `${parameters.todos?.length || 0} todos`;
3966
3971
  }
3967
- if (tool5 === "todoread") {
3972
+ if (tool6 === "todoread") {
3968
3973
  return "read todo list";
3969
3974
  }
3970
- if (tool5 === "task" && parameters.description) {
3975
+ if (tool6 === "task" && parameters.description) {
3971
3976
  return parameters.description;
3972
3977
  }
3973
- if (tool5 === "skill" && parameters.name) {
3978
+ if (tool6 === "skill" && parameters.name) {
3974
3979
  return parameters.name;
3975
3980
  }
3976
- if (tool5 === "lsp") {
3981
+ if (tool6 === "lsp") {
3977
3982
  const op = parameters.operation || "lsp";
3978
3983
  const path = parameters.filePath || "";
3979
3984
  const line = parameters.line;
@@ -3986,7 +3991,7 @@ function extractParameterKey(tool5, parameters) {
3986
3991
  }
3987
3992
  return op;
3988
3993
  }
3989
- if (tool5 === "question") {
3994
+ if (tool6 === "question") {
3990
3995
  const questions = parameters.questions;
3991
3996
  if (Array.isArray(questions) && questions.length > 0) {
3992
3997
  const headers = questions.map((q) => q.header || "").filter(Boolean).slice(0, 3);
@@ -4006,6 +4011,13 @@ function extractParameterKey(tool5, parameters) {
4006
4011
  }
4007
4012
  return paramStr.substring(0, 50);
4008
4013
  }
4014
+ function formatAge(createdAt) {
4015
+ const elapsed = Date.now() - createdAt;
4016
+ if (elapsed < 6e4) return "just now";
4017
+ if (elapsed < 36e5) return `${Math.floor(elapsed / 6e4)}m ago`;
4018
+ if (elapsed < 864e5) return `${Math.floor(elapsed / 36e5)}h ago`;
4019
+ return `${Math.floor(elapsed / 864e5)}d ago`;
4020
+ }
4009
4021
  function formatTokenCount(tokens, compact) {
4010
4022
  const suffix = compact ? "" : " tokens";
4011
4023
  if (tokens >= 1e3) {
@@ -4172,7 +4184,12 @@ function formatCompressionMetrics(removedTokens, summaryTokens) {
4172
4184
  }
4173
4185
  return metrics.join(", ");
4174
4186
  }
4175
- async function sendCompressNotification(client, logger, config, state, sessionId, entries, batchTopic, sessionMessageIds, params) {
4187
+ function formatContextTransition(tokensBefore, tokensAfter) {
4188
+ const beforeStr = formatTokenCount(tokensBefore, true);
4189
+ const afterStr = formatTokenCount(tokensAfter, true);
4190
+ return `Context ${beforeStr}\u2192${afterStr}`;
4191
+ }
4192
+ async function sendCompressNotification(client, logger, config, state, sessionId, entries, batchTopic, sessionMessageIds, params, contextTokensBefore) {
4176
4193
  if (config.pruneNotification === "off") {
4177
4194
  return false;
4178
4195
  }
@@ -4220,9 +4237,14 @@ async function sendCompressNotification(client, logger, config, state, sessionId
4220
4237
  }
4221
4238
  }
4222
4239
  const topic = batchTopic ?? (entries.length === 1 ? state.prune.messages.blocksById.get(entries[0]?.blockId ?? -1)?.topic ?? "(unknown topic)" : "(unknown topic)");
4223
- const totalActiveSummaryTkns = getActiveSummaryTokenUsage(state);
4224
- const totalGross = state.stats.totalPruneTokens + state.stats.pruneTokenCounter;
4225
- const notificationHeader = `\u25A3 ACP | ${formatCompressionMetrics(totalGross, totalActiveSummaryTkns)}`;
4240
+ const contextTokensAfter = Math.max(
4241
+ 0,
4242
+ contextTokensBefore - compressedTokens + summaryTokens
4243
+ );
4244
+ const notificationHeader = `\u25A3 ACP | ${formatContextTransition(
4245
+ contextTokensBefore,
4246
+ contextTokensAfter
4247
+ )}`;
4226
4248
  if (config.pruneNotification === "minimal") {
4227
4249
  message = `${notificationHeader} \u2014 ${compressionLabel}`;
4228
4250
  } else {
@@ -4354,6 +4376,7 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
4354
4376
  await saveSessionState(ctx.state, ctx.logger);
4355
4377
  const params = getCurrentParams(ctx.state, rawMessages, ctx.logger);
4356
4378
  const sessionMessageIds = rawMessages.filter((msg) => !isIgnoredUserMessage(msg)).map((msg) => msg.info.id);
4379
+ const contextTokensBefore = getCurrentTokenUsage(ctx.state, rawMessages);
4357
4380
  await sendCompressNotification(
4358
4381
  ctx.client,
4359
4382
  ctx.logger,
@@ -4363,7 +4386,8 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
4363
4386
  entries,
4364
4387
  batchTopic,
4365
4388
  sessionMessageIds,
4366
- params
4389
+ params,
4390
+ contextTokensBefore
4367
4391
  );
4368
4392
  }
4369
4393
 
@@ -5485,8 +5509,8 @@ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agent
5485
5509
  agentName ? hostPermissions.agents[agentName] : void 0
5486
5510
  ) ? "deny" : basePermission;
5487
5511
  };
5488
- var hasExplicitToolPermission = (permissionConfig, tool5) => {
5489
- return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool5) : false;
5512
+ var hasExplicitToolPermission = (permissionConfig, tool6) => {
5513
+ return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool6) : false;
5490
5514
  };
5491
5515
 
5492
5516
  // lib/compress-permission.ts
@@ -5505,26 +5529,15 @@ var syncCompressPermissionState = (state, config, hostPermissions, messages) =>
5505
5529
  // lib/prompts/extensions/nudge.ts
5506
5530
  function buildCompressedBlockGuidance(state, gcConfig, context) {
5507
5531
  const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
5508
- const refs = activeBlockIds.map((id) => {
5509
- const block = state.prune.messages.blocksById.get(id);
5510
- const tokens = block?.summaryTokens ?? 0;
5511
- return `b${id}${tokens > 0 ? ` (${tokens}t)` : ""}`;
5512
- });
5513
- const blockCount = refs.length;
5514
- let blockList;
5515
- if (blockCount <= 20) {
5516
- blockList = blockCount > 0 ? refs.join(", ") : "none";
5517
- } else {
5518
- const recent = refs.slice(-20).join(", ");
5519
- blockList = `${recent} (+${blockCount - 20} older, use decompress to access by ID)`;
5520
- }
5521
- const includeHint = context?.includeHint ?? true;
5532
+ const blockCount = activeBlockIds.length;
5533
+ const blocksForStats = activeBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
5534
+ const totalSummaryTokens = blocksForStats.reduce((s, b) => s + (b.summaryTokens ?? 0), 0);
5535
+ const totalSummaryDisplay = totalSummaryTokens >= 1e3 ? `${(totalSummaryTokens / 1e3).toFixed(1)}K` : String(totalSummaryTokens);
5536
+ const lastBlock = blocksForStats.length > 0 ? blocksForStats.reduce((latest, b) => b.createdAt > latest.createdAt ? b : latest) : null;
5537
+ const ageStr = lastBlock ? formatAge(lastBlock.createdAt) : "never";
5522
5538
  const lines = [
5523
- `- Compressed blocks: ${blockCount} (${blockList})`
5539
+ `- Compressed blocks: ${blockCount} (${totalSummaryDisplay} summary, last ${ageStr}). Use acp_status for details.`
5524
5540
  ];
5525
- if (includeHint) {
5526
- lines.push("- \u{1F4A1} Tools: compress, decompress, search_context.");
5527
- }
5528
5541
  if (blockCount > 50) {
5529
5542
  const oldBlockIds = activeBlockIds.slice(0, Math.max(0, blockCount - 20));
5530
5543
  const allOldBlocks = oldBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0);
@@ -5713,8 +5726,8 @@ function getModelInfo(messages) {
5713
5726
  }
5714
5727
  const userInfo = lastUserMessage.info;
5715
5728
  return {
5716
- providerId: userInfo.model.providerID,
5717
- modelId: userInfo.model.modelID
5729
+ providerId: userInfo.model?.providerID,
5730
+ modelId: userInfo.model?.modelID
5718
5731
  };
5719
5732
  }
5720
5733
  function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
@@ -5783,6 +5796,29 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
5783
5796
  modelContextLimit: state.modelContextLimit
5784
5797
  };
5785
5798
  }
5799
+ function computeShouldNudge(params) {
5800
+ const { currentTokens, modelContextLimit, overMinLimit, overMaxLimit } = params;
5801
+ const contextPct = modelContextLimit && currentTokens ? currentTokens / modelContextLimit * 100 : 0;
5802
+ const lastNudgeTokens = params.lastNudgeTokens;
5803
+ const growthSinceLastNudge = (currentTokens ?? 0) - (lastNudgeTokens ?? 0);
5804
+ const frequencyTriggered = lastNudgeTokens === void 0 || growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
5805
+ const shouldNudge = contextPct >= params.minNudgeContextPercent && frequencyTriggered;
5806
+ if (!shouldNudge) {
5807
+ return { shouldNudge: false, tipsVariant: null };
5808
+ }
5809
+ const tipsVariant = overMaxLimit ? "maxLimit" : overMinLimit ? "minLimit" : "normal";
5810
+ return { shouldNudge: true, tipsVariant };
5811
+ }
5812
+ var NUDGE_GROWTH_FLOOR = 6e3;
5813
+ var NUDGE_GROWTH_CAP = 5e4;
5814
+ var NUDGE_GROWTH_RATIO = 0.05;
5815
+ function resolveAdaptiveNudgeGrowth(modelContextLimit) {
5816
+ if (!modelContextLimit || modelContextLimit <= 0) return NUDGE_GROWTH_FLOOR;
5817
+ return Math.min(
5818
+ NUDGE_GROWTH_CAP,
5819
+ Math.max(NUDGE_GROWTH_FLOOR, Math.round(modelContextLimit * NUDGE_GROWTH_RATIO))
5820
+ );
5821
+ }
5786
5822
  function addAnchor(anchorMessageIds, anchorMessageId, anchorMessageIndex, messages, interval) {
5787
5823
  if (anchorMessageIndex < 0) {
5788
5824
  return false;
@@ -5899,8 +5935,6 @@ Context: ${formatK(currentTokens)} tokens.
5899
5935
  All compression serves the primary task, but be frugal. Context capacity is precious \u2014 compress waste promptly. Save context by compressing consumed outputs, not by avoiding tools. Compress by need, not by percentage.`;
5900
5936
  }
5901
5937
  function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
5902
- const contextUsageInfo = buildContextUsageGuidance(config, currentTokens, modelContextLimit);
5903
- const contextLimitNudgeWithUsage = prompts.contextLimitNudge + contextUsageInfo;
5904
5938
  const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
5905
5939
  if (suffixMessage) {
5906
5940
  const nudgeParts = [];
@@ -5908,7 +5942,7 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
5908
5942
  if (state.nudges.contextLimitAnchors.size > 0) {
5909
5943
  for (const { index } of collectAnchoredMessages(state.nudges.contextLimitAnchors, messages)) {
5910
5944
  const guidance = buildMessagePriorityGuidance(messages, compressionPriorities, index, MESSAGE_MODE_NUDGE_PRIORITY);
5911
- nudgeParts.push(appendGuidanceToDcpTag(contextLimitNudgeWithUsage, guidance));
5945
+ nudgeParts.push(appendGuidanceToDcpTag(prompts.contextLimitNudge, guidance));
5912
5946
  }
5913
5947
  }
5914
5948
  if (turnNudgeAnchors.size > 0) {
@@ -5925,7 +5959,7 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
5925
5959
  }
5926
5960
  } else {
5927
5961
  if (state.nudges.contextLimitAnchors.size > 0) {
5928
- nudgeParts.push(contextLimitNudgeWithUsage);
5962
+ nudgeParts.push(prompts.contextLimitNudge);
5929
5963
  }
5930
5964
  if (turnNudgeAnchors.size > 0) {
5931
5965
  nudgeParts.push(prompts.turnNudge);
@@ -5944,7 +5978,7 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
5944
5978
  applyMessageModeAnchoredNudge(
5945
5979
  state.nudges.contextLimitAnchors,
5946
5980
  messages,
5947
- contextLimitNudgeWithUsage,
5981
+ prompts.contextLimitNudge,
5948
5982
  compressionPriorities
5949
5983
  );
5950
5984
  applyMessageModeAnchoredNudge(
@@ -5964,7 +5998,7 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
5964
5998
  applyRangeModeAnchoredNudge(
5965
5999
  state.nudges.contextLimitAnchors,
5966
6000
  messages,
5967
- contextLimitNudgeWithUsage,
6001
+ prompts.contextLimitNudge,
5968
6002
  ""
5969
6003
  );
5970
6004
  applyRangeModeAnchoredNudge(
@@ -5999,16 +6033,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
5999
6033
  }
6000
6034
  const lastMessage = findLastNonIgnoredMessage(messages);
6001
6035
  const lastAssistantMessage = messages.findLast((message) => message.info.role === "assistant");
6002
- if (lastAssistantMessage && messageHasCompress(lastAssistantMessage)) {
6003
- state.nudges.contextLimitAnchors.clear();
6004
- state.nudges.turnNudgeAnchors.clear();
6005
- state.nudges.iterationNudgeAnchors.clear();
6006
- state.nudges.lastPerMessageNudgeTokens = 0;
6007
- void saveSessionState(state, logger);
6008
- return;
6009
- }
6010
6036
  const { providerId, modelId } = getModelInfo(messages);
6011
- let anchorsChanged = false;
6012
6037
  const { overMaxLimit, overMinLimit, currentTokens, modelContextLimit } = isContextOverLimits(
6013
6038
  config,
6014
6039
  state,
@@ -6016,6 +6041,15 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6016
6041
  modelId,
6017
6042
  messages
6018
6043
  );
6044
+ if (lastAssistantMessage && messageHasCompress(lastAssistantMessage)) {
6045
+ state.nudges.contextLimitAnchors.clear();
6046
+ state.nudges.turnNudgeAnchors.clear();
6047
+ state.nudges.iterationNudgeAnchors.clear();
6048
+ state.nudges.lastPerMessageNudgeTokens = currentTokens;
6049
+ void saveSessionState(state, logger);
6050
+ return;
6051
+ }
6052
+ let anchorsChanged = false;
6019
6053
  if (!overMinLimit) {
6020
6054
  const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0;
6021
6055
  const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0;
@@ -6075,44 +6109,49 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6075
6109
  }
6076
6110
  const suffixMessage = createSuffixMessage(messages);
6077
6111
  applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
6078
- const contextPct = modelContextLimit && currentTokens ? currentTokens / modelContextLimit * 100 : 0;
6079
- const minPercent = config.compress?.minNudgeContextPercent ?? 15;
6080
- injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
6112
+ const decision = computeShouldNudge({
6113
+ currentTokens,
6114
+ modelContextLimit,
6115
+ overMinLimit,
6116
+ overMaxLimit,
6117
+ lastNudgeTokens: state.nudges.lastPerMessageNudgeTokens,
6118
+ minNudgeContextPercent: config.compress?.minNudgeContextPercent ?? 15,
6119
+ nudgeGrowthTokens: config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth(modelContextLimit)
6120
+ });
6121
+ state.nudges.shouldInjectThisTurn = decision.shouldNudge;
6081
6122
  let tipsText = null;
6082
- if (overMaxLimit || overMinLimit) {
6083
- const lastWarnPct = state.nudges.lastPerMessageNudgeTokens && modelContextLimit ? state.nudges.lastPerMessageNudgeTokens / modelContextLimit * 100 : 0;
6084
- const growthSinceWarn = contextPct - lastWarnPct;
6085
- if (lastWarnPct === 0 || growthSinceWarn >= 10) {
6086
- tipsText = overMaxLimit ? '\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.' : "\n\n\u26A0\uFE0F Context is growing \u2014 consider compressing older work. Tools: compress, decompress, search_context.";
6087
- state.nudges.lastPerMessageNudgeTokens = currentTokens ?? 0;
6088
- state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
6123
+ if (decision.shouldNudge) {
6124
+ injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
6125
+ if (decision.tipsVariant === "maxLimit") {
6126
+ tipsText = '\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
6127
+ } else if (decision.tipsVariant === "minLimit") {
6128
+ tipsText = "\n\n\u26A0\uFE0F Context is growing \u2014 consider compressing older work. Tools: compress, decompress, search_context.";
6129
+ } else {
6130
+ tipsText = "\n\n\u{1F4A1} Tools: compress, decompress, search_context.";
6089
6131
  }
6090
- } else if (contextPct >= minPercent) {
6091
- tipsText = "\n\n\u{1F4A1} Tools: compress, decompress, search_context.";
6092
- if (state.nudges.lastPerMessageNudgeTokens) {
6093
- state.nudges.lastPerMessageNudgeTokens = 0;
6132
+ state.nudges.lastPerMessageNudgeTokens = currentTokens;
6133
+ state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
6134
+ if (config.compress.mode !== "message") {
6135
+ const visibleMessageIds = new Set(
6136
+ messages.map((message) => message.info.id)
6137
+ );
6138
+ const blockGuidance = buildCompressedBlockGuidance(state, config.gc, {
6139
+ currentTokens,
6140
+ modelContextLimit,
6141
+ includeHint: tipsText !== null,
6142
+ visibleMessageIds
6143
+ });
6144
+ if (blockGuidance.trim() && suffixMessage) {
6145
+ appendToLastTextPart(suffixMessage, "\n\n" + blockGuidance);
6146
+ }
6094
6147
  }
6095
- }
6096
- if (config.compress.mode !== "message") {
6097
- const visibleMessageIds = new Set(
6098
- messages.map((message) => message.info.id)
6099
- );
6100
- const blockGuidance = buildCompressedBlockGuidance(state, config.gc, {
6101
- currentTokens,
6102
- modelContextLimit,
6103
- includeHint: tipsText !== null,
6104
- visibleMessageIds
6105
- });
6106
- if (blockGuidance.trim() && suffixMessage) {
6107
- appendToLastTextPart(suffixMessage, "\n\n" + blockGuidance);
6148
+ if (tipsText && suffixMessage) {
6149
+ appendToLastTextPart(suffixMessage, tipsText);
6108
6150
  }
6151
+ injectVisibleIdRange(state, messages, suffixMessage);
6109
6152
  }
6110
- if (tipsText && suffixMessage) {
6111
- appendToLastTextPart(suffixMessage, tipsText);
6112
- }
6113
- injectVisibleIdRange(state, messages, suffixMessage);
6114
6153
  if (suffixMessage) {
6115
- appendToLastTextPart(suffixMessage, "\n</acp-context>");
6154
+ appendToLastTextPart(suffixMessage, "\n");
6116
6155
  }
6117
6156
  if (anchorsChanged) {
6118
6157
  void saveSessionState(state, logger);
@@ -6687,6 +6726,52 @@ ${content}`;
6687
6726
  });
6688
6727
  }
6689
6728
 
6729
+ // lib/compress/status.ts
6730
+ import { tool as tool5 } from "@opencode-ai/plugin";
6731
+ var ACP_STATUS_TOOL_DESCRIPTION = `Show detailed status of all active compressed context blocks. Returns a table of block IDs, summary sizes, ages, and topics \u2014 use this to decide which blocks to decompress or search. No arguments needed.
6732
+
6733
+ Use this tool when:
6734
+ - You need to know what content has been compressed away
6735
+ - You want to see block sizes before deciding to decompress
6736
+ - You need a quick overview of context compression state`;
6737
+ function formatTokens(n) {
6738
+ return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
6739
+ }
6740
+ function createAcpStatusTool(ctx) {
6741
+ ctx.prompts.reload();
6742
+ return tool5({
6743
+ description: ACP_STATUS_TOOL_DESCRIPTION,
6744
+ args: {},
6745
+ async execute() {
6746
+ const messages = ctx.state.prune.messages;
6747
+ const activeIds = Array.from(messages.activeBlockIds).sort((a, b) => a - b);
6748
+ if (activeIds.length === 0) {
6749
+ return "No compressed blocks. Context is fully visible.";
6750
+ }
6751
+ const blocks = activeIds.map((id) => messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
6752
+ const totalSummary = blocks.reduce((sum, b) => sum + b.summaryTokens, 0);
6753
+ const totalCompressed = blocks.reduce((sum, b) => sum + b.compressedTokens, 0);
6754
+ const lines = [
6755
+ `ACP Status \u2014 ${blocks.length} active compressed block${blocks.length === 1 ? "" : "s"} (${formatTokens(totalSummary)} summary tokens, ${formatTokens(totalCompressed)} original content compressed)`,
6756
+ ""
6757
+ ];
6758
+ const idWidth = String(Math.max(...blocks.map((b) => b.blockId))).length;
6759
+ for (const b of blocks) {
6760
+ const idStr = `b${b.blockId}`.padEnd(idWidth + 1);
6761
+ const tokStr = `${formatTokens(b.summaryTokens)}t`.padStart(7);
6762
+ const ageStr = formatAge(b.createdAt).padStart(10);
6763
+ const topic = b.topic || "(no topic)";
6764
+ lines.push(` ${idStr} ${tokStr} ${ageStr} "${topic}"`);
6765
+ }
6766
+ lines.push("");
6767
+ lines.push(
6768
+ "Use decompress to restore a block's full content, or search_context to search within compressed blocks."
6769
+ );
6770
+ return lines.join("\n");
6771
+ }
6772
+ });
6773
+ }
6774
+
6690
6775
  // lib/logger.ts
6691
6776
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
6692
6777
  import { join as join3 } from "path";
@@ -7861,7 +7946,7 @@ var COMPRESS_TRIGGER_PROMPT = [
7861
7946
  "Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
7862
7947
  "Return after compress with a brief explanation of what content was compressed."
7863
7948
  ].join("\n\n");
7864
- function getTriggerPrompt(tool5, state, config, userFocus) {
7949
+ function getTriggerPrompt(tool6, state, config, userFocus) {
7865
7950
  const base = COMPRESS_TRIGGER_PROMPT;
7866
7951
  const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
7867
7952
  const sections = [base, compressedBlockGuidance];
@@ -7890,8 +7975,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
7890
7975
  );
7891
7976
  logger.info("Manual mode toggled", { manualMode: state.manualMode });
7892
7977
  }
7893
- async function handleManualTriggerCommand(ctx, tool5, userFocus) {
7894
- return getTriggerPrompt(tool5, ctx.state, ctx.config, userFocus);
7978
+ async function handleManualTriggerCommand(ctx, tool6, userFocus) {
7979
+ return getTriggerPrompt(tool6, ctx.state, ctx.config, userFocus);
7895
7980
  }
7896
7981
  function applyPendingManualTrigger(state, messages, logger) {
7897
7982
  const pending = state.pendingManualTrigger;
@@ -8624,6 +8709,9 @@ function createSystemPromptHandler(state, logger, config, prompts) {
8624
8709
  if (effectivePermission === "deny") {
8625
8710
  return;
8626
8711
  }
8712
+ if (state.nudges.shouldInjectThisTurn === false && !state.manualMode) {
8713
+ return;
8714
+ }
8627
8715
  prompts.reload();
8628
8716
  const runtimePrompts = prompts.getRuntimePrompts();
8629
8717
  const newPrompt = renderSystemPrompt(
@@ -9152,7 +9240,8 @@ var server = (async (ctx) => {
9152
9240
  ...config.compress.permission !== "deny" && {
9153
9241
  compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext),
9154
9242
  decompress: createDecompressTool(compressToolContext),
9155
- search_context: createSearchContextTool(compressToolContext)
9243
+ search_context: createSearchContextTool(compressToolContext),
9244
+ acp_status: createAcpStatusTool(compressToolContext)
9156
9245
  }
9157
9246
  },
9158
9247
  config: async (opencodeConfig) => {
@@ -9168,7 +9257,7 @@ var server = (async (ctx) => {
9168
9257
  }
9169
9258
  const toolsToAdd = [];
9170
9259
  if (config.compress.permission !== "deny" && !config.experimental.allowSubAgents) {
9171
- toolsToAdd.push("compress", "decompress", "search_context");
9260
+ toolsToAdd.push("compress", "decompress", "search_context", "acp_status");
9172
9261
  }
9173
9262
  if (toolsToAdd.length > 0) {
9174
9263
  const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [];
@@ -9181,7 +9270,8 @@ var server = (async (ctx) => {
9181
9270
  const permission = opencodeConfig.permission ?? {};
9182
9271
  opencodeConfig.permission = {
9183
9272
  ...permission,
9184
- compress: config.compress.permission
9273
+ compress: config.compress.permission,
9274
+ acp_status: "allow"
9185
9275
  };
9186
9276
  }
9187
9277
  hostPermissions.global = opencodeConfig.permission;