opencode-acp 1.14.22 → 1.14.23-pr.332.52

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,11 +278,15 @@ 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
- // Enable INFO/DEBUG logging + per-request snapshots to ~/.config/opencode/logs/acp/
285
- // (WARN/ERROR are always logged to daily/<date>.log)
285
+ // File log verbosity: "debug" | "info" | "warn" | "error" | "silent".
286
+ // Default "info" writes decision-level events (nudges, transforms, updates)
287
+ // to ~/.config/opencode/logs/acp/daily/<date>.log
288
+ "logLevel": "info",
289
+ // Enable full DEBUG logging + per-request snapshots (overrides logLevel)
286
290
  "debug": false,
287
291
  // Notification display: "off", "minimal", or "detailed"
288
292
  "pruneNotification": "off",
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,11 +233,14 @@ 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
- // Enable INFO/DEBUG logging + per-request snapshots to ~/.config/opencode/logs/acp/
240
- // (WARN/ERROR are always logged to daily/<date>.log)
239
+ // 文件日志级别: "debug" | "info" | "warn" | "error" | "silent"
240
+ // 默认 "info":决策级事件(压缩提示、转换摘要、更新检查)落盘到
241
+ // ~/.config/opencode/logs/acp/daily/<日期>.log
242
+ "logLevel": "info",
243
+ // 启用完整 DEBUG 日志 + 按请求快照(优先于 logLevel)
241
244
  "debug": false,
242
245
  // Notification display: "off", "minimal", or "detailed"
243
246
  "pruneNotification": "detailed",
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AA2BjD,QAAA,MAAM,MAAM,EAAE,MA6IK,CAAA;AAEnB,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AA2BjD,QAAA,MAAM,MAAM,EAAE,MAqJK,CAAA;AAEnB,eAAe,MAAM,CAAA"}
package/dist/index.js CHANGED
@@ -868,6 +868,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
868
868
  "enabled",
869
869
  "autoUpdate",
870
870
  "debug",
871
+ "logLevel",
871
872
  "showUpdateToasts",
872
873
  "allowSubAgents",
873
874
  "pruneNotification",
@@ -1466,6 +1467,7 @@ var defaultConfig = {
1466
1467
  enabled: true,
1467
1468
  autoUpdate: true,
1468
1469
  debug: false,
1470
+ logLevel: "info",
1469
1471
  allowSubAgents: true,
1470
1472
  pruneNotification: "off",
1471
1473
  // [FIX #20] Default to toast — chat-mode notifications inject an empty
@@ -1727,6 +1729,7 @@ function mergeLayer(config, data) {
1727
1729
  enabled: data.enabled ?? config.enabled,
1728
1730
  autoUpdate: data.autoUpdate ?? config.autoUpdate,
1729
1731
  debug: data.debug ?? config.debug,
1732
+ logLevel: data.logLevel ?? config.logLevel,
1730
1733
  allowSubAgents: data.allowSubAgents ?? data.experimental?.allowSubAgents ?? config.allowSubAgents,
1731
1734
  pruneNotification: data.pruneNotification ?? config.pruneNotification,
1732
1735
  pruneNotificationType: data.pruneNotificationType ?? config.pruneNotificationType,
@@ -6953,13 +6956,7 @@ function computeShouldNudge2(params) {
6953
6956
  }
6954
6957
  return policy.computeShouldNudge(params);
6955
6958
  }
6956
- function resolveAdaptiveNudgeGrowth2(modelContextLimit) {
6957
- const policy = getDefaultTriggerPolicy();
6958
- if (!policy) {
6959
- return 6e3;
6960
- }
6961
- return policy.resolveAdaptiveNudgeGrowth(modelContextLimit);
6962
- }
6959
+ var DEFAULT_NUDGE_GROWTH_TOKENS = 5e4;
6963
6960
  function addAnchor(anchorMessageIds, anchorMessageId, anchorMessageIndex, messages, interval) {
6964
6961
  if (anchorMessageIndex < 0) {
6965
6962
  return false;
@@ -7192,7 +7189,9 @@ function refNum(ref) {
7192
7189
  function buildCompressibleRanges(messages, state, protectedTools = [], protectedFilePatterns = [], protectedZoneRefs) {
7193
7190
  const msgInfo = [];
7194
7191
  const protectedMsgInfo = [];
7195
- for (const msg of messages) {
7192
+ const lastUserRefIdx = [];
7193
+ for (let mi = 0; mi < messages.length; mi++) {
7194
+ const msg = messages[mi];
7196
7195
  if (isSyntheticMessage(msg)) continue;
7197
7196
  const ref = state.messageIds.byRawId.get(msg.info.id);
7198
7197
  if (!ref) continue;
@@ -7227,15 +7226,27 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7227
7226
  }
7228
7227
  let tokens = 0;
7229
7228
  let isTool = false;
7229
+ let hasMeaningfulPart = false;
7230
7230
  for (const part of msg.parts || []) {
7231
7231
  if (part.type === "text" && typeof part.text === "string") {
7232
7232
  tokens += Math.round(part.text.length / 4);
7233
+ if (part.text.trim().length > 0) hasMeaningfulPart = true;
7233
7234
  } else if (part.type !== "text" && part.type !== "reasoning") {
7234
7235
  tokens += Math.round(JSON.stringify(part).length / 4);
7235
7236
  isTool = true;
7237
+ hasMeaningfulPart = true;
7236
7238
  }
7237
7239
  }
7238
- msgInfo.push({ ref, refNum: rn, tokens, isTool, isUser: msg.info.role === "user" });
7240
+ if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
7241
+ lastUserRefIdx.length = 0;
7242
+ lastUserRefIdx.push(msgInfo.length);
7243
+ }
7244
+ msgInfo.push({ ref, refNum: rn, tokens, effectiveTokens: 0, meaningful: hasMeaningfulPart, isTool, isUser: msg.info.role === "user" });
7245
+ }
7246
+ const lastUserIdx = lastUserRefIdx.length > 0 ? lastUserRefIdx[0] : -1;
7247
+ for (let i = 0; i < msgInfo.length; i++) {
7248
+ const info = msgInfo[i];
7249
+ info.effectiveTokens = i !== lastUserIdx && info.meaningful ? info.tokens : 0;
7239
7250
  }
7240
7251
  const groups = [];
7241
7252
  let cur = null;
@@ -7261,6 +7272,7 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7261
7272
  endRef: info.ref,
7262
7273
  count: 1,
7263
7274
  tokens: info.tokens,
7275
+ effectiveTokens: info.effectiveTokens,
7264
7276
  toolPct: info.isTool ? 100 : 0,
7265
7277
  textPct: info.isTool ? 0 : 100
7266
7278
  };
@@ -7268,6 +7280,7 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7268
7280
  cur.endRef = info.ref;
7269
7281
  cur.count++;
7270
7282
  cur.tokens += info.tokens;
7283
+ cur.effectiveTokens += info.effectiveTokens;
7271
7284
  if (info.isTool) {
7272
7285
  cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
7273
7286
  } else {
@@ -7310,6 +7323,11 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
7310
7323
  protected: protectedGroups
7311
7324
  };
7312
7325
  }
7326
+ var EFFECTIVE_MIN_COMPRESSIBLE_TOKENS = 1250;
7327
+ function resolveEffectiveFloor(config) {
7328
+ const minChars = config.compress?.minCompressRange ?? 5e3;
7329
+ return minChars > 0 ? Math.floor(minChars / 4) : 0;
7330
+ }
7313
7331
  function filterRecommendedRanges(compressible, _protectedRanges, options) {
7314
7332
  const { logger } = options;
7315
7333
  const log = logger?.debug.bind(logger);
@@ -7317,12 +7335,22 @@ function filterRecommendedRanges(compressible, _protectedRanges, options) {
7317
7335
  log?.("filterRecommendedRanges: no compressible ranges, returning empty");
7318
7336
  return [];
7319
7337
  }
7320
- const result = compressible.map(
7321
- (r, i) => i === compressible.length - 1 ? { ...r, dangerous: true } : r
7338
+ const floor = options.minEffectiveTokens ?? EFFECTIVE_MIN_COMPRESSIBLE_TOKENS;
7339
+ const kept = compressible.filter((r) => {
7340
+ const effective = r.effectiveTokens ?? r.tokens;
7341
+ return effective > 0 && effective >= floor;
7342
+ });
7343
+ const result = kept.map(
7344
+ (r, i) => i === kept.length - 1 ? { ...r, dangerous: true } : r
7322
7345
  );
7323
- log?.("filterRecommendedRanges: passthrough (last segment marked dangerous)", {
7346
+ log?.("filterRecommendedRanges: effective-token floor applied", {
7324
7347
  inputRanges: compressible.length,
7325
- outputRanges: result.length
7348
+ outputRanges: result.length,
7349
+ floor,
7350
+ dropped: compressible.filter((r) => {
7351
+ const effective = r.effectiveTokens ?? r.tokens;
7352
+ return effective <= 0 || effective < floor;
7353
+ }).map((r) => `${r.startRef}\u2013${r.endRef} (${r.effectiveTokens ?? r.tokens} eff tokens)`)
7326
7354
  });
7327
7355
  return result;
7328
7356
  }
@@ -7331,8 +7359,10 @@ function formatCompressibleRanges(ranges, protectedRanges) {
7331
7359
  if (!protectedRanges || protectedRanges.length === 0) {
7332
7360
  if (ranges.length === 0) return "";
7333
7361
  const lines2 = ranges.map((r) => {
7362
+ const eff = r.effectiveTokens ?? r.tokens;
7363
+ const size = eff < r.tokens ? `${fmt(eff)} effective of ${fmt(r.tokens)}` : fmt(r.tokens);
7334
7364
  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}`;
7365
+ return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${size} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
7336
7366
  });
7337
7367
  return `Compressible ranges (oldest first):
7338
7368
  ${lines2.join("\n")}`;
@@ -7348,7 +7378,7 @@ ${lines2.join("\n")}`;
7348
7378
  tokens: r.tokens,
7349
7379
  toolPct: r.toolPct,
7350
7380
  textPct: r.textPct,
7351
- compressibleTokens: r.tokens,
7381
+ compressibleTokens: r.effectiveTokens ?? r.tokens,
7352
7382
  compressibleCount: r.count,
7353
7383
  protectedTokens: 0,
7354
7384
  protectedCount: 0,
@@ -7588,7 +7618,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7588
7618
  }
7589
7619
  }
7590
7620
  const suffixMessage = createSuffixMessage(messages);
7591
- const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth2(modelContextLimit);
7621
+ const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? DEFAULT_NUDGE_GROWTH_TOKENS;
7592
7622
  const growthFloor = Math.max(
7593
7623
  config.compress?.minNudgeGrowthFloor ?? 5e3,
7594
7624
  (config.compress?.minNudgeGrowthRatio ?? 0.45) * nudgeGrowthTokens
@@ -7637,14 +7667,18 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7637
7667
  const recommendedRanges = filterRecommendedRanges(
7638
7668
  unprotectedCompressible,
7639
7669
  contextRanges.protected,
7640
- { logger }
7670
+ { logger, minEffectiveTokens: resolveEffectiveFloor(config) }
7641
7671
  );
7642
7672
  const hasRecommendations = recommendedRanges.length > 0;
7643
7673
  const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
7644
7674
  const allInProtectedZone = protectedRefs.size > 0 && unprotectedCompressible.length === 0;
7645
- const nothingToCompress = allProtected || allInProtectedZone;
7646
- const shouldInjectNudge = nudgeAllowed && (!nothingToCompress || emergencyOverride);
7647
- let shouldInject = shouldInjectNudge;
7675
+ const allBelowMin = contextRanges.compressible.length > 0 && recommendedRanges.length === 0;
7676
+ const nothingToCompress = allProtected || allInProtectedZone || allBelowMin;
7677
+ const emergencyNoTargets = emergencyOverride && nothingToCompress;
7678
+ const noticeCadenceMet = state.nudges.lastNudgeShownTokens === void 0 || growthSinceBaseline !== void 0 && growthSinceBaseline >= growthFloor;
7679
+ const shouldInjectNudge = nudgeAllowed && !nothingToCompress;
7680
+ const shouldInjectNotice = emergencyNoTargets && noticeCadenceMet;
7681
+ let shouldInject = shouldInjectNudge || shouldInjectNotice;
7648
7682
  if (shouldInjectNudge) {
7649
7683
  applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
7650
7684
  }
@@ -7726,6 +7760,13 @@ Multiple entries create separate blocks: \`content: [{ startId: "b${firstBlock.b
7726
7760
  ${rules}`;
7727
7761
  appendToLastTextPart(suffixMessage, tierText);
7728
7762
  shouldInject = true;
7763
+ logger.info(`Tier ${tc.triggerTier} trigger nudge injected`, {
7764
+ session: state.sessionId,
7765
+ action,
7766
+ targetTier: tc.targetTier,
7767
+ blocks: candidates.length,
7768
+ candidateTokens
7769
+ });
7729
7770
  if (tc.triggerTier === 2) {
7730
7771
  state.nudges.lastTier2NudgeTokens = currentTokens;
7731
7772
  } else {
@@ -7735,6 +7776,32 @@ ${rules}`;
7735
7776
  }
7736
7777
  }
7737
7778
  state.nudges.shouldInjectThisTurn = shouldInject;
7779
+ const usagePct = currentTokens !== void 0 && modelContextLimit ? `${(currentTokens / modelContextLimit * 100).toFixed(1)}%` : void 0;
7780
+ if (shouldInjectNudge) {
7781
+ logger.info("Compression nudge injected", {
7782
+ session: state.sessionId,
7783
+ trigger: emergencyOverride ? "emergency" : "growth",
7784
+ currentTokens,
7785
+ usagePct,
7786
+ growthSinceBaseline,
7787
+ growthFloor,
7788
+ recommendedRanges: recommendedRanges.length
7789
+ });
7790
+ } else if (shouldInjectNotice) {
7791
+ logger.info("Emergency /compact notice injected (no compressible targets)", {
7792
+ session: state.sessionId,
7793
+ currentTokens,
7794
+ usagePct
7795
+ });
7796
+ } else if (nudgeAllowed && nothingToCompress) {
7797
+ logger.info("Nudge suppressed: nothing to compress", {
7798
+ session: state.sessionId,
7799
+ reason: allProtected ? "all_protected" : allInProtectedZone ? "in_protected_zone" : "below_effective_floor",
7800
+ currentTokens,
7801
+ usagePct,
7802
+ compressibleRanges: contextRanges.compressible.length
7803
+ });
7804
+ }
7738
7805
  if (shouldInject && config.debug && contextRanges.compressible.length > 0) {
7739
7806
  const compressible = contextRanges.compressible;
7740
7807
  const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
@@ -7778,8 +7845,19 @@ ${formatCompressibleRanges(recommendedRanges, contextRanges.protected)}`;
7778
7845
  Use \`acp_status({scope:"uncompressed"})\` to re-fetch compressible ranges after compressing, or \`acp_status\` for compressed block details.`;
7779
7846
  appendToLastTextPart(suffixMessage, breakdown);
7780
7847
  }
7781
- if (effectiveTipsVariant === "maxLimit") {
7848
+ if (effectiveTipsVariant === "maxLimit" && !emergencyNoTargets) {
7782
7849
  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.';
7850
+ } else if (shouldInjectNotice) {
7851
+ const emergencyPct = currentTokens !== void 0 && modelContextLimit !== void 0 && modelContextLimit > 0 ? Math.round(currentTokens / modelContextLimit * 100) : void 0;
7852
+ tipsText = `
7853
+
7854
+ \u{1F6A8} Context is critically full${emergencyPct !== void 0 ? ` (${emergencyPct}% of limit)` : ""} and there is nothing left that can be safely compressed.
7855
+ Do NOT retry compress on the same ranges \u2014 they will keep failing.
7856
+ You cannot execute user commands yourself. Act now via your reply/message tool:
7857
+ - Inform the user that context is full and compression is exhausted
7858
+ - Recommend they run /acp export (archives compression summaries to a file), then /compact or start a new session
7859
+ - Alternatively, ask them to relax protected-tool / preserve-recent settings so compression becomes possible
7860
+ Then stop retrying and await the user's response.`;
7783
7861
  }
7784
7862
  state.nudges.lastNudgeShownTokens = currentTokens;
7785
7863
  {
@@ -9032,15 +9110,35 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9032
9110
  import { join as join3 } from "path";
9033
9111
  import { existsSync as existsSync3 } from "fs";
9034
9112
  import { homedir as homedir3 } from "os";
9035
- var LOG_VERSION = true ? "1.14.22" : "dev";
9113
+ var LOG_VERSION = true ? "1.14.23-pr.332.52" : "dev";
9114
+ var LEVEL_RANK = {
9115
+ debug: 10,
9116
+ info: 20,
9117
+ warn: 30,
9118
+ error: 40,
9119
+ silent: 99
9120
+ };
9036
9121
  var Logger = class {
9037
9122
  logDir;
9038
- enabled;
9039
- constructor(enabled) {
9040
- this.enabled = enabled;
9123
+ /** Resolved verbosity; replaces the old boolean `enabled` flag. */
9124
+ level;
9125
+ /**
9126
+ * @param enabled legacy boolean toggle — `true` maps to `debug`,
9127
+ * `false` maps to `warn` (errors + warnings only).
9128
+ * @param level explicit verbosity; when given it wins over `enabled`.
9129
+ */
9130
+ constructor(enabled, level) {
9131
+ this.level = level ?? (enabled ? "debug" : "warn");
9041
9132
  const configHome = process.env.XDG_CONFIG_HOME || join3(homedir3(), ".config");
9042
9133
  this.logDir = join3(configHome, "opencode", "logs", "acp");
9043
9134
  }
9135
+ /** Back-compat view of the old flag: true only at full debug verbosity. */
9136
+ get enabled() {
9137
+ return this.level === "debug";
9138
+ }
9139
+ shouldWrite(level) {
9140
+ return LEVEL_RANK[level] >= LEVEL_RANK[this.level];
9141
+ }
9044
9142
  async ensureLogDir() {
9045
9143
  if (!existsSync3(this.logDir)) {
9046
9144
  await mkdir2(this.logDir, { recursive: true });
@@ -9087,7 +9185,7 @@ var Logger = class {
9087
9185
  }
9088
9186
  }
9089
9187
  async write(level, component, message, data) {
9090
- if (!this.enabled && level !== "ERROR" && level !== "WARN") return;
9188
+ if (!this.shouldWrite(level.toLowerCase())) return;
9091
9189
  try {
9092
9190
  await this.ensureLogDir();
9093
9191
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -9104,12 +9202,10 @@ var Logger = class {
9104
9202
  }
9105
9203
  }
9106
9204
  info(message, data) {
9107
- if (!this.enabled) return;
9108
9205
  const component = this.getCallerFile(2);
9109
9206
  return this.write("INFO", component, message, data);
9110
9207
  }
9111
9208
  debug(message, data) {
9112
- if (!this.enabled) return;
9113
9209
  const component = this.getCallerFile(2);
9114
9210
  return this.write("DEBUG", component, message, data);
9115
9211
  }
@@ -9207,7 +9303,7 @@ var Logger = class {
9207
9303
  });
9208
9304
  }
9209
9305
  async saveContext(sessionId, messages) {
9210
- if (!this.enabled) return;
9306
+ if (this.level !== "debug") return;
9211
9307
  try {
9212
9308
  const contextDir = join3(this.logDir, "context", sessionId);
9213
9309
  if (!existsSync3(contextDir)) {
@@ -10968,6 +11064,7 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
10968
11064
  requestModel?.providerID,
10969
11065
  requestModel?.modelID
10970
11066
  );
11067
+ const prevModelID = state.modelID;
10971
11068
  if (requestModelLimit !== void 0) {
10972
11069
  state.modelContextLimit = requestModelLimit;
10973
11070
  state.modelProviderID = requestModel?.providerID;
@@ -10987,6 +11084,14 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
10987
11084
  state.modelProviderID = requestModel.providerID;
10988
11085
  state.modelID = requestModel.modelID;
10989
11086
  }
11087
+ if (requestModel?.modelID && requestModel.modelID !== prevModelID) {
11088
+ logger.info("Model switched mid-session", {
11089
+ session: state.sessionId,
11090
+ from: prevModelID,
11091
+ to: requestModel.modelID,
11092
+ contextLimit: state.modelContextLimit
11093
+ });
11094
+ }
10990
11095
  await updatePerTurnState(state, logger, messages);
10991
11096
  }
10992
11097
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
@@ -11048,6 +11153,17 @@ ${text}`);
11048
11153
  hideFailedCompressCalls(output.messages);
11049
11154
  stripStaleMetadata(output.messages);
11050
11155
  dropEmptyMessages(output.messages);
11156
+ const postTokens = getCurrentTokenUsage(state, output.messages);
11157
+ logger.info("Chat transform complete", {
11158
+ session: state.sessionId,
11159
+ model: state.modelID,
11160
+ messages: output.messages.length,
11161
+ prePruneTokens,
11162
+ postTokens,
11163
+ contextLimit: state.modelContextLimit,
11164
+ usagePct: postTokens !== void 0 && state.modelContextLimit ? `${(postTokens / state.modelContextLimit * 100).toFixed(1)}%` : void 0,
11165
+ nudged: state.nudges.shouldInjectThisTurn
11166
+ });
11051
11167
  if (state.sessionId) {
11052
11168
  await logger.saveContext(state.sessionId, output.messages);
11053
11169
  }
@@ -11225,12 +11341,31 @@ import { readFile as readFile2, rm } from "fs/promises";
11225
11341
  import { basename, dirname as dirname4, join as join6 } from "path";
11226
11342
  import { fileURLToPath } from "url";
11227
11343
  var PACKAGE_NAME = "opencode-acp";
11228
- function startAutoUpdate(ctx, enabled) {
11229
- if (!enabled) return;
11344
+ function startAutoUpdate(ctx, enabled, logger) {
11345
+ if (!enabled) {
11346
+ logger?.info("Auto-update disabled by config");
11347
+ return;
11348
+ }
11349
+ logger?.info("Auto-update check starting");
11230
11350
  const controller = new AbortController();
11231
11351
  const timeout = setTimeout(() => controller.abort(), 1e4);
11232
- void checkAutoUpdate(controller.signal).then((result) => {
11233
- if (!result.updated) return;
11352
+ void checkAutoUpdate(controller.signal, logger).then((result) => {
11353
+ if (!result.updated) {
11354
+ if ("error" in result) {
11355
+ logger?.warn("Auto-update failed", {
11356
+ error: result.error,
11357
+ name: result.name,
11358
+ current: result.current,
11359
+ latest: result.latest
11360
+ });
11361
+ }
11362
+ return;
11363
+ }
11364
+ logger?.info("Auto-update applied update", {
11365
+ name: result.name,
11366
+ from: result.current,
11367
+ to: result.latest
11368
+ });
11234
11369
  setTimeout(() => {
11235
11370
  ctx.client.tui.showToast({
11236
11371
  body: {
@@ -11244,17 +11379,39 @@ function startAutoUpdate(ctx, enabled) {
11244
11379
  }).catch(() => {
11245
11380
  }).finally(() => clearTimeout(timeout));
11246
11381
  }
11247
- async function checkAutoUpdate(signal) {
11382
+ async function checkAutoUpdate(signal, logger) {
11248
11383
  const packageDir = await findPackageDir(PACKAGE_NAME);
11249
- if (!packageDir) return { updated: false };
11384
+ if (!packageDir) {
11385
+ logger?.info("Auto-update skipped: package dir not found");
11386
+ return { updated: false };
11387
+ }
11250
11388
  const pkg = await readPackageJson(join6(packageDir, "package.json"));
11251
- if (!pkg?.name || !pkg.version) return { updated: false };
11252
- const latest = await fetchLatestVersion(pkg.name, signal);
11253
- if (!latest || !isVersionNewer(latest, pkg.version)) return { updated: false };
11254
- const removeDir = await updateRemoveDir(packageDir, pkg.name);
11255
- if (!removeDir) return { updated: false };
11389
+ if (!pkg?.name || !pkg.version) {
11390
+ logger?.info("Auto-update skipped: package.json unreadable", { packageDir });
11391
+ return { updated: false };
11392
+ }
11393
+ const target = await updateTarget(packageDir, pkg.name);
11394
+ if (!target) {
11395
+ logger?.info("Auto-update skipped: no update target", { name: pkg.name, version: pkg.version });
11396
+ return { updated: false };
11397
+ }
11398
+ const tag = specUpdateTag(target.spec);
11399
+ if (!tag) {
11400
+ logger?.info("Auto-update skipped: spec not auto-updatable", { spec: target.spec });
11401
+ return { updated: false };
11402
+ }
11403
+ const latest = await fetchLatestVersion(pkg.name, tag, signal);
11404
+ if (!latest) {
11405
+ logger?.info("Auto-update skipped: no version published for tag", { name: pkg.name, tag });
11406
+ return { updated: false };
11407
+ }
11408
+ if (!isVersionNewer(latest, pkg.version)) {
11409
+ logger?.info("Auto-update: already up to date", { name: pkg.name, tag, current: pkg.version, latest });
11410
+ return { updated: false };
11411
+ }
11412
+ logger?.info("Auto-update: newer version available", { name: pkg.name, tag, current: pkg.version, latest });
11256
11413
  try {
11257
- await rm(removeDir, { recursive: true, force: true });
11414
+ await rm(target.removeDir, { recursive: true, force: true });
11258
11415
  } catch {
11259
11416
  return {
11260
11417
  updated: false,
@@ -11276,7 +11433,7 @@ async function findPackageDir(name) {
11276
11433
  dir = parent;
11277
11434
  }
11278
11435
  }
11279
- async function updateRemoveDir(packageDir, name) {
11436
+ async function updateTarget(packageDir, name) {
11280
11437
  const packageParent = dirname4(packageDir);
11281
11438
  const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname4(packageParent) : packageParent;
11282
11439
  if (basename(nodeModulesDir) !== "node_modules") return void 0;
@@ -11284,7 +11441,7 @@ async function updateRemoveDir(packageDir, name) {
11284
11441
  const wrapperPkg = await readPackageJson(join6(wrapperDir, "package.json"));
11285
11442
  const spec = wrapperSpec(wrapperDir, name) ?? wrapperPkg?.dependencies?.[name];
11286
11443
  if (!spec || !isAutoUpdatableSpec(spec)) return void 0;
11287
- return wrapperDir;
11444
+ return { removeDir: wrapperDir, spec };
11288
11445
  }
11289
11446
  function wrapperSpec(wrapperDir, name) {
11290
11447
  if (name.startsWith("@")) {
@@ -11305,8 +11462,22 @@ function isAutoUpdatableSpec(spec) {
11305
11462
  if (/^[~^]/.test(value)) return true;
11306
11463
  if (/^(?:>=|>|<=|<)/.test(value)) return true;
11307
11464
  if (/\s+(?:\|\||-|[<>=])\s+/.test(value)) return true;
11465
+ if (isDistTag(value)) return true;
11308
11466
  return false;
11309
11467
  }
11468
+ function specUpdateTag(spec) {
11469
+ const value = spec.trim();
11470
+ if (!isAutoUpdatableSpec(value)) return void 0;
11471
+ if (value === "*") return "latest";
11472
+ if (isDistTag(value)) return value;
11473
+ return "latest";
11474
+ }
11475
+ function isDistTag(value) {
11476
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) return false;
11477
+ if (parseVersion(value)) return false;
11478
+ if (/\.x(\.|$)/i.test(value)) return false;
11479
+ return true;
11480
+ }
11310
11481
  async function readPackageJson(path) {
11311
11482
  try {
11312
11483
  const data = JSON.parse(await readFile2(path, "utf-8"));
@@ -11315,10 +11486,10 @@ async function readPackageJson(path) {
11315
11486
  return void 0;
11316
11487
  }
11317
11488
  }
11318
- async function fetchLatestVersion(name, signal) {
11489
+ async function fetchLatestVersion(name, tag, signal) {
11319
11490
  try {
11320
11491
  const response = await fetch(
11321
- `https://registry.npmjs.org/${encodeURIComponent(name)}/latest`,
11492
+ `https://registry.npmjs.org/${encodeURIComponent(name)}/${encodeURIComponent(tag)}`,
11322
11493
  {
11323
11494
  signal
11324
11495
  }
@@ -11371,7 +11542,15 @@ var server = (async (ctx) => {
11371
11542
  if (!config.enabled) {
11372
11543
  return {};
11373
11544
  }
11374
- const logger = new Logger(config.debug);
11545
+ const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
11546
+ logger.info("ACP plugin initialized", {
11547
+ version: true ? "1.14.23-pr.332.52" : "dev",
11548
+ workspace: ctx.directory,
11549
+ logLevel: logger.level,
11550
+ debug: config.debug,
11551
+ autoUpdate: config.autoUpdate,
11552
+ secureMode: isSecureMode()
11553
+ });
11375
11554
  const registry4 = new SessionStateRegistry(logger);
11376
11555
  const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts);
11377
11556
  const hostPermissions = {
@@ -11401,7 +11580,7 @@ var server = (async (ctx) => {
11401
11580
  }
11402
11581
  );
11403
11582
  logger.info("DCP initialized");
11404
- startAutoUpdate(ctx, config.autoUpdate);
11583
+ startAutoUpdate(ctx, config.autoUpdate, logger);
11405
11584
  const compressToolContext = {
11406
11585
  client: ctx.client,
11407
11586
  registry: registry4,