opencode-acp 1.14.22 → 1.14.23-pr.331.51

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/README.md CHANGED
@@ -123,8 +123,8 @@ stateDiagram-v2
123
123
  - **T1** fires when raw context exceeds the configured limit. The model sees
124
124
  compressible ranges and writes a detailed summary preserving file paths,
125
125
  signatures, decisions, and rationale.
126
- - **T2** fires when T1 summary tokens reach `nudgeGrowthTokens` (default 5% of
127
- context window). The model distills old T1 blocks — keeping decisions and
126
+ - **T2** fires when T1 summary tokens reach `nudgeGrowthTokens` (fixed default
127
+ 50000). The model distills old T1 blocks — keeping decisions and
128
128
  outcomes, dropping verbose process details.
129
129
  - **T3** fires when T2 summary tokens reach the same threshold. The model
130
130
  condenses to bare facts (shipped releases, key bugs, architecture decisions).
@@ -278,7 +278,8 @@ Each level overrides the previous, so project settings take priority over global
278
278
  "$schema": "https://raw.githubusercontent.com/ranxianglei/opencode-acp/master/dcp.schema.json",
279
279
  // Enable or disable the plugin
280
280
  "enabled": true,
281
- // Automatically update npm-installed ACP when a newer npm latest is available.
281
+ // Automatically update npm-installed ACP when a newer version is available
282
+ // on the installed dist-tag/spec (@stable follows stable, @latest follows latest).
282
283
  // Version-locked plugin specs are not updated.
283
284
  "autoUpdate": true,
284
285
  // Enable INFO/DEBUG logging + per-request snapshots to ~/.config/opencode/logs/acp/
package/README.zh-CN.md CHANGED
@@ -100,7 +100,7 @@ stateDiagram-v2
100
100
  **触发机制:**
101
101
 
102
102
  - **T1** 在原始上下文超过配置限制时触发。模型看到可压缩范围,编写详细摘要,保留文件路径、函数签名、决策和理由。
103
- - **T2** 在 T1 摘要 token 达到 `nudgeGrowthTokens`(默认上下文窗口的 5%)时触发。模型蒸馏旧的 T1 块 — 保留决策和结果,丢弃冗长的过程细节。
103
+ - **T2** 在 T1 摘要 token 达到 `nudgeGrowthTokens`(固定默认 50000)时触发。模型蒸馏旧的 T1 块 — 保留决策和结果,丢弃冗长的过程细节。
104
104
  - **T3** 在 T2 摘要 token 达到同样阈值时触发。模型浓缩为纯事实(已发布的版本、关键 bug、架构决策)。
105
105
 
106
106
  每层有**独立的节奏计数器** — T2 触发不阻塞 T3。T1 通过 `!shouldInject` 守卫获得优先级:如果 T1 触发了,T2/T3 等到下一轮。这确保原始上下文压缩优先发生(影响最大)。
@@ -233,8 +233,8 @@ ACP 使用自己的配置文件,按以下顺序搜索:
233
233
  "$schema": "https://raw.githubusercontent.com/ranxianglei/opencode-acp/master/dcp.schema.json",
234
234
  // Enable or disable the plugin
235
235
  "enabled": true,
236
- // Automatically update npm-installed ACP when a newer npm latest is available.
237
- // Version-locked plugin specs are not updated.
236
+ // 自动更新 npm 安装的 ACP:跟踪安装所用 dist-tag/规范(@stable 跟随 stable,@latest 跟随 latest)。
237
+ // 版本锁定的规范不会被更新。
238
238
  "autoUpdate": true,
239
239
  // Enable INFO/DEBUG logging + per-request snapshots to ~/.config/opencode/logs/acp/
240
240
  // (WARN/ERROR are always logged to daily/<date>.log)
package/dist/index.js CHANGED
@@ -6953,13 +6953,7 @@ function computeShouldNudge2(params) {
6953
6953
  }
6954
6954
  return policy.computeShouldNudge(params);
6955
6955
  }
6956
- function resolveAdaptiveNudgeGrowth2(modelContextLimit) {
6957
- const policy = getDefaultTriggerPolicy();
6958
- if (!policy) {
6959
- return 6e3;
6960
- }
6961
- return policy.resolveAdaptiveNudgeGrowth(modelContextLimit);
6962
- }
6956
+ var DEFAULT_NUDGE_GROWTH_TOKENS = 5e4;
6963
6957
  function addAnchor(anchorMessageIds, anchorMessageId, anchorMessageIndex, messages, interval) {
6964
6958
  if (anchorMessageIndex < 0) {
6965
6959
  return false;
@@ -7192,7 +7186,9 @@ function refNum(ref) {
7192
7186
  function buildCompressibleRanges(messages, state, protectedTools = [], protectedFilePatterns = [], protectedZoneRefs) {
7193
7187
  const msgInfo = [];
7194
7188
  const protectedMsgInfo = [];
7195
- for (const msg of messages) {
7189
+ const lastUserRefIdx = [];
7190
+ for (let mi = 0; mi < messages.length; mi++) {
7191
+ const msg = messages[mi];
7196
7192
  if (isSyntheticMessage(msg)) continue;
7197
7193
  const ref = state.messageIds.byRawId.get(msg.info.id);
7198
7194
  if (!ref) continue;
@@ -7227,15 +7223,27 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7227
7223
  }
7228
7224
  let tokens = 0;
7229
7225
  let isTool = false;
7226
+ let hasMeaningfulPart = false;
7230
7227
  for (const part of msg.parts || []) {
7231
7228
  if (part.type === "text" && typeof part.text === "string") {
7232
7229
  tokens += Math.round(part.text.length / 4);
7230
+ if (part.text.trim().length > 0) hasMeaningfulPart = true;
7233
7231
  } else if (part.type !== "text" && part.type !== "reasoning") {
7234
7232
  tokens += Math.round(JSON.stringify(part).length / 4);
7235
7233
  isTool = true;
7234
+ hasMeaningfulPart = true;
7236
7235
  }
7237
7236
  }
7238
- msgInfo.push({ ref, refNum: rn, tokens, isTool, isUser: msg.info.role === "user" });
7237
+ if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
7238
+ lastUserRefIdx.length = 0;
7239
+ lastUserRefIdx.push(msgInfo.length);
7240
+ }
7241
+ msgInfo.push({ ref, refNum: rn, tokens, effectiveTokens: 0, meaningful: hasMeaningfulPart, isTool, isUser: msg.info.role === "user" });
7242
+ }
7243
+ const lastUserIdx = lastUserRefIdx.length > 0 ? lastUserRefIdx[0] : -1;
7244
+ for (let i = 0; i < msgInfo.length; i++) {
7245
+ const info = msgInfo[i];
7246
+ info.effectiveTokens = i !== lastUserIdx && info.meaningful ? info.tokens : 0;
7239
7247
  }
7240
7248
  const groups = [];
7241
7249
  let cur = null;
@@ -7261,6 +7269,7 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7261
7269
  endRef: info.ref,
7262
7270
  count: 1,
7263
7271
  tokens: info.tokens,
7272
+ effectiveTokens: info.effectiveTokens,
7264
7273
  toolPct: info.isTool ? 100 : 0,
7265
7274
  textPct: info.isTool ? 0 : 100
7266
7275
  };
@@ -7268,6 +7277,7 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7268
7277
  cur.endRef = info.ref;
7269
7278
  cur.count++;
7270
7279
  cur.tokens += info.tokens;
7280
+ cur.effectiveTokens += info.effectiveTokens;
7271
7281
  if (info.isTool) {
7272
7282
  cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
7273
7283
  } else {
@@ -7310,6 +7320,11 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7310
7320
  protected: protectedGroups
7311
7321
  };
7312
7322
  }
7323
+ var EFFECTIVE_MIN_COMPRESSIBLE_TOKENS = 1250;
7324
+ function resolveEffectiveFloor(config) {
7325
+ const minChars = config.compress?.minCompressRange ?? 5e3;
7326
+ return minChars > 0 ? Math.floor(minChars / 4) : 0;
7327
+ }
7313
7328
  function filterRecommendedRanges(compressible, _protectedRanges, options) {
7314
7329
  const { logger } = options;
7315
7330
  const log = logger?.debug.bind(logger);
@@ -7317,12 +7332,22 @@ function filterRecommendedRanges(compressible, _protectedRanges, options) {
7317
7332
  log?.("filterRecommendedRanges: no compressible ranges, returning empty");
7318
7333
  return [];
7319
7334
  }
7320
- const result = compressible.map(
7321
- (r, i) => i === compressible.length - 1 ? { ...r, dangerous: true } : r
7335
+ const floor = options.minEffectiveTokens ?? EFFECTIVE_MIN_COMPRESSIBLE_TOKENS;
7336
+ const kept = compressible.filter((r) => {
7337
+ const effective = r.effectiveTokens ?? r.tokens;
7338
+ return effective > 0 && effective >= floor;
7339
+ });
7340
+ const result = kept.map(
7341
+ (r, i) => i === kept.length - 1 ? { ...r, dangerous: true } : r
7322
7342
  );
7323
- log?.("filterRecommendedRanges: passthrough (last segment marked dangerous)", {
7343
+ log?.("filterRecommendedRanges: effective-token floor applied", {
7324
7344
  inputRanges: compressible.length,
7325
- outputRanges: result.length
7345
+ outputRanges: result.length,
7346
+ floor,
7347
+ dropped: compressible.filter((r) => {
7348
+ const effective = r.effectiveTokens ?? r.tokens;
7349
+ return effective <= 0 || effective < floor;
7350
+ }).map((r) => `${r.startRef}\u2013${r.endRef} (${r.effectiveTokens ?? r.tokens} eff tokens)`)
7326
7351
  });
7327
7352
  return result;
7328
7353
  }
@@ -7331,8 +7356,10 @@ function formatCompressibleRanges(ranges, protectedRanges) {
7331
7356
  if (!protectedRanges || protectedRanges.length === 0) {
7332
7357
  if (ranges.length === 0) return "";
7333
7358
  const lines2 = ranges.map((r) => {
7359
+ const eff = r.effectiveTokens ?? r.tokens;
7360
+ const size = eff < r.tokens ? `${fmt(eff)} effective of ${fmt(r.tokens)}` : fmt(r.tokens);
7334
7361
  const suffix = r.dangerous ? " \u26A0\uFE0F NOT recommended unless you are certain. If you MUST compress this, pass `dangerous: true`." : "";
7335
- return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${fmt(r.tokens)} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
7362
+ return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${size} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
7336
7363
  });
7337
7364
  return `Compressible ranges (oldest first):
7338
7365
  ${lines2.join("\n")}`;
@@ -7348,7 +7375,7 @@ ${lines2.join("\n")}`;
7348
7375
  tokens: r.tokens,
7349
7376
  toolPct: r.toolPct,
7350
7377
  textPct: r.textPct,
7351
- compressibleTokens: r.tokens,
7378
+ compressibleTokens: r.effectiveTokens ?? r.tokens,
7352
7379
  compressibleCount: r.count,
7353
7380
  protectedTokens: 0,
7354
7381
  protectedCount: 0,
@@ -7588,7 +7615,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7588
7615
  }
7589
7616
  }
7590
7617
  const suffixMessage = createSuffixMessage(messages);
7591
- const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth2(modelContextLimit);
7618
+ const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? DEFAULT_NUDGE_GROWTH_TOKENS;
7592
7619
  const growthFloor = Math.max(
7593
7620
  config.compress?.minNudgeGrowthFloor ?? 5e3,
7594
7621
  (config.compress?.minNudgeGrowthRatio ?? 0.45) * nudgeGrowthTokens
@@ -7637,14 +7664,18 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7637
7664
  const recommendedRanges = filterRecommendedRanges(
7638
7665
  unprotectedCompressible,
7639
7666
  contextRanges.protected,
7640
- { logger }
7667
+ { logger, minEffectiveTokens: resolveEffectiveFloor(config) }
7641
7668
  );
7642
7669
  const hasRecommendations = recommendedRanges.length > 0;
7643
7670
  const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
7644
7671
  const allInProtectedZone = protectedRefs.size > 0 && unprotectedCompressible.length === 0;
7645
- const nothingToCompress = allProtected || allInProtectedZone;
7646
- const shouldInjectNudge = nudgeAllowed && (!nothingToCompress || emergencyOverride);
7647
- let shouldInject = shouldInjectNudge;
7672
+ const allBelowMin = contextRanges.compressible.length > 0 && recommendedRanges.length === 0;
7673
+ const nothingToCompress = allProtected || allInProtectedZone || allBelowMin;
7674
+ const emergencyNoTargets = emergencyOverride && nothingToCompress;
7675
+ const noticeCadenceMet = state.nudges.lastNudgeShownTokens === void 0 || growthSinceBaseline !== void 0 && growthSinceBaseline >= growthFloor;
7676
+ const shouldInjectNudge = nudgeAllowed && !nothingToCompress;
7677
+ const shouldInjectNotice = emergencyNoTargets && noticeCadenceMet;
7678
+ let shouldInject = shouldInjectNudge || shouldInjectNotice;
7648
7679
  if (shouldInjectNudge) {
7649
7680
  applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
7650
7681
  }
@@ -7778,8 +7809,19 @@ ${formatCompressibleRanges(recommendedRanges, contextRanges.protected)}`;
7778
7809
  Use \`acp_status({scope:"uncompressed"})\` to re-fetch compressible ranges after compressing, or \`acp_status\` for compressed block details.`;
7779
7810
  appendToLastTextPart(suffixMessage, breakdown);
7780
7811
  }
7781
- if (effectiveTipsVariant === "maxLimit") {
7812
+ if (effectiveTipsVariant === "maxLimit" && !emergencyNoTargets) {
7782
7813
  tipsText = "\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n" + HOW_TO_COMPRESS_RULES + '\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
7814
+ } else if (shouldInjectNotice) {
7815
+ const emergencyPct = currentTokens !== void 0 && modelContextLimit !== void 0 && modelContextLimit > 0 ? Math.round(currentTokens / modelContextLimit * 100) : void 0;
7816
+ tipsText = `
7817
+
7818
+ \u{1F6A8} Context is critically full${emergencyPct !== void 0 ? ` (${emergencyPct}% of limit)` : ""} and there is nothing left that can be safely compressed.
7819
+ Do NOT retry compress on the same ranges \u2014 they will keep failing.
7820
+ You cannot execute user commands yourself. Act now via your reply/message tool:
7821
+ - Inform the user that context is full and compression is exhausted
7822
+ - Recommend they run /acp export (archives compression summaries to a file), then /compact or start a new session
7823
+ - Alternatively, ask them to relax protected-tool / preserve-recent settings so compression becomes possible
7824
+ Then stop retrying and await the user's response.`;
7783
7825
  }
7784
7826
  state.nudges.lastNudgeShownTokens = currentTokens;
7785
7827
  {
@@ -9032,7 +9074,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9032
9074
  import { join as join3 } from "path";
9033
9075
  import { existsSync as existsSync3 } from "fs";
9034
9076
  import { homedir as homedir3 } from "os";
9035
- var LOG_VERSION = true ? "1.14.22" : "dev";
9077
+ var LOG_VERSION = true ? "1.14.23-pr.331.51" : "dev";
9036
9078
  var Logger = class {
9037
9079
  logDir;
9038
9080
  enabled;
@@ -11249,12 +11291,14 @@ async function checkAutoUpdate(signal) {
11249
11291
  if (!packageDir) return { updated: false };
11250
11292
  const pkg = await readPackageJson(join6(packageDir, "package.json"));
11251
11293
  if (!pkg?.name || !pkg.version) return { updated: false };
11252
- const latest = await fetchLatestVersion(pkg.name, signal);
11294
+ const target = await updateTarget(packageDir, pkg.name);
11295
+ if (!target) return { updated: false };
11296
+ const tag = specUpdateTag(target.spec);
11297
+ if (!tag) return { updated: false };
11298
+ const latest = await fetchLatestVersion(pkg.name, tag, signal);
11253
11299
  if (!latest || !isVersionNewer(latest, pkg.version)) return { updated: false };
11254
- const removeDir = await updateRemoveDir(packageDir, pkg.name);
11255
- if (!removeDir) return { updated: false };
11256
11300
  try {
11257
- await rm(removeDir, { recursive: true, force: true });
11301
+ await rm(target.removeDir, { recursive: true, force: true });
11258
11302
  } catch {
11259
11303
  return {
11260
11304
  updated: false,
@@ -11276,7 +11320,7 @@ async function findPackageDir(name) {
11276
11320
  dir = parent;
11277
11321
  }
11278
11322
  }
11279
- async function updateRemoveDir(packageDir, name) {
11323
+ async function updateTarget(packageDir, name) {
11280
11324
  const packageParent = dirname4(packageDir);
11281
11325
  const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname4(packageParent) : packageParent;
11282
11326
  if (basename(nodeModulesDir) !== "node_modules") return void 0;
@@ -11284,7 +11328,7 @@ async function updateRemoveDir(packageDir, name) {
11284
11328
  const wrapperPkg = await readPackageJson(join6(wrapperDir, "package.json"));
11285
11329
  const spec = wrapperSpec(wrapperDir, name) ?? wrapperPkg?.dependencies?.[name];
11286
11330
  if (!spec || !isAutoUpdatableSpec(spec)) return void 0;
11287
- return wrapperDir;
11331
+ return { removeDir: wrapperDir, spec };
11288
11332
  }
11289
11333
  function wrapperSpec(wrapperDir, name) {
11290
11334
  if (name.startsWith("@")) {
@@ -11305,8 +11349,22 @@ function isAutoUpdatableSpec(spec) {
11305
11349
  if (/^[~^]/.test(value)) return true;
11306
11350
  if (/^(?:>=|>|<=|<)/.test(value)) return true;
11307
11351
  if (/\s+(?:\|\||-|[<>=])\s+/.test(value)) return true;
11352
+ if (isDistTag(value)) return true;
11308
11353
  return false;
11309
11354
  }
11355
+ function specUpdateTag(spec) {
11356
+ const value = spec.trim();
11357
+ if (!isAutoUpdatableSpec(value)) return void 0;
11358
+ if (value === "*") return "latest";
11359
+ if (isDistTag(value)) return value;
11360
+ return "latest";
11361
+ }
11362
+ function isDistTag(value) {
11363
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) return false;
11364
+ if (parseVersion(value)) return false;
11365
+ if (/\.x(\.|$)/i.test(value)) return false;
11366
+ return true;
11367
+ }
11310
11368
  async function readPackageJson(path) {
11311
11369
  try {
11312
11370
  const data = JSON.parse(await readFile2(path, "utf-8"));
@@ -11315,10 +11373,10 @@ async function readPackageJson(path) {
11315
11373
  return void 0;
11316
11374
  }
11317
11375
  }
11318
- async function fetchLatestVersion(name, signal) {
11376
+ async function fetchLatestVersion(name, tag, signal) {
11319
11377
  try {
11320
11378
  const response = await fetch(
11321
- `https://registry.npmjs.org/${encodeURIComponent(name)}/latest`,
11379
+ `https://registry.npmjs.org/${encodeURIComponent(name)}/${encodeURIComponent(tag)}`,
11322
11380
  {
11323
11381
  signal
11324
11382
  }