opencode-acp 1.14.18-pr.307.19 → 1.14.19-pr.297.30

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
@@ -284,7 +284,8 @@ Each level overrides the previous, so project settings take priority over global
284
284
  // Automatically update npm-installed ACP when a newer npm latest is available.
285
285
  // Version-locked plugin specs are not updated.
286
286
  "autoUpdate": true,
287
- // Enable debug logging to ~/.config/opencode/logs/acp/
287
+ // Enable INFO/DEBUG logging + per-request snapshots to ~/.config/opencode/logs/acp/
288
+ // (WARN/ERROR are always logged to daily/<date>.log)
288
289
  "debug": false,
289
290
  // Notification display: "off", "minimal", or "detailed"
290
291
  "pruneNotification": "off",
@@ -492,6 +493,16 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
492
493
 
493
494
  ## Changelog
494
495
 
496
+ ### v1.14.19 — Fix release pipeline for real (npm 10 `--ignore-scripts` bug)
497
+
498
+ **Problem**: v1.14.18 (#306) was merged but still never published — `release.yml` failed at the exact same step. Root cause of the failed fix: the CI runner uses Node 22 (npm 10.9.x), and **npm 10 runs the `prepare` lifecycle hook during `npm pack` even when `--ignore-scripts` is passed**. v1.14.18's fix (`npm pack --ignore-scripts`) works on npm 11 but is a no-op on npm 10. When prepare runs, tsup writes `CLI Building entry: index.ts` to **stdout**, breaking `JSON.parse` in `verify-package.mjs` (`SyntaxError: Unexpected token`).
499
+
500
+ **Fix**: Redirect the `prepare` build's stdout to stderr: `"prepare": "npm run build 1>&2"` in `package.json`. Build output (informational) goes to stderr; stdout stays pure JSON regardless of npm version or whether `--ignore-scripts` works. Verified empirically with npm 10.9.9 locally: `npm pack --dry-run --json` now returns valid JSON (173 tarball entries) both with and without `--ignore-scripts`. The `--ignore-scripts` flag added in v1.14.18 is kept (harmless on npm 10, skips the redundant rebuild on npm 11). The `npm install github:...#branch` path still works — prepare still runs and still builds `dist/`.
501
+
502
+ Files: `package.json`. Tests: 976 pass; `check:package` green on npm 10.9.9 and npm 11.12.1.
503
+
504
+ **Install**: `opencode plugin opencode-acp@latest --global`
505
+
495
506
  ### v1.14.18 — Supersedes unpublished v1.14.17; fix release pipeline
496
507
 
497
508
  **Problem**: v1.14.17 was merged (#305) but never published — `release.yml` failed at `npm run check:package` because the `prepare` hook added in #298 (`npm run build`) runs during `npm pack --json` inside `verify-package.mjs`, and tsup's `CLI Building entry: index.ts` stdout output breaks `JSON.parse` (`SyntaxError: Unexpected token 'C'`).
package/README.zh-CN.md CHANGED
@@ -239,7 +239,8 @@ ACP 使用自己的配置文件,按以下顺序搜索:
239
239
  // Automatically update npm-installed ACP when a newer npm latest is available.
240
240
  // Version-locked plugin specs are not updated.
241
241
  "autoUpdate": true,
242
- // Enable debug logging to ~/.config/opencode/logs/acp/
242
+ // Enable INFO/DEBUG logging + per-request snapshots to ~/.config/opencode/logs/acp/
243
+ // (WARN/ERROR are always logged to daily/<date>.log)
243
244
  "debug": false,
244
245
  // Notification display: "off", "minimal", or "detailed"
245
246
  "pruneNotification": "detailed",
@@ -446,6 +447,16 @@ ACP 是 DCP 的直接替代品。迁移步骤:
446
447
 
447
448
  ## 更新日志
448
449
 
450
+ ### v1.14.19 — 彻底修复发版流水线(npm 10 `--ignore-scripts` 失效)
451
+
452
+ **问题**:v1.14.18(#306)已合并但依然没有发布 —— `release.yml` 又在同一步骤失败。v1.14.18 修复失败的根本原因:CI 运行器是 Node 22(npm 10.9.x),而 **npm 10 在 `npm pack` 时即使传了 `--ignore-scripts` 也照样执行 `prepare` 生命周期钩子**。v1.14.18 的修复(`npm pack --ignore-scripts`)在 npm 11 上有效,在 npm 10 上是空操作。prepare 一旦执行,tsup 会把 `CLI Building entry: index.ts` 写到 **stdout**,破坏 `verify-package.mjs` 里的 `JSON.parse`(`SyntaxError: Unexpected token`)。
453
+
454
+ **修复**:把 `prepare` 构建的 stdout 重定向到 stderr:`package.json` 中改为 `"prepare": "npm run build 1>&2"`。构建输出(信息性)走 stderr,stdout 无论 npm 版本、无论 `--ignore-scripts` 是否生效都保持纯 JSON。已在本地用 npm 10.9.9 实证验证:`npm pack --dry-run --json` 现在返回合法 JSON(173 个 tarball 条目),带与不带 `--ignore-scripts` 均通过。v1.14.18 加的 `--ignore-scripts` 标志保留(npm 10 上无害,npm 11 上跳过多余重建)。`npm install github:...#branch` 路径不受影响 —— prepare 照常执行并构建 `dist/`。
455
+
456
+ 文件:`package.json`。测试:976 通过;`check:package` 在 npm 10.9.9 和 npm 11.12.1 上均绿。
457
+
458
+ **安装**:`opencode plugin opencode-acp@latest --global`
459
+
449
460
  ### v1.14.18 — 取代未能发布的 v1.14.17;修复发版流水线
450
461
 
451
462
  **问题**:v1.14.17 已合并(#305)但从未发布 —— \`release.yml\` 在 \`npm run check:package\` 一步失败:#298 新增的 \`prepare\` 钩子(\`npm run build\`)会在 \`verify-package.mjs\` 的 \`npm pack --json\` 期间运行,tsup 的 \`CLI Building entry: index.ts\` 输出污染 stdout,导致 \`JSON.parse\` 报错(\`SyntaxError: Unexpected token 'C'\`)。
@@ -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,MAiHK,CAAA;AAEnB,eAAe,MAAM,CAAA"}
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"}
package/dist/index.js CHANGED
@@ -2949,6 +2949,51 @@ function applyPendingCompressionDurations(state) {
2949
2949
  return updates;
2950
2950
  }
2951
2951
 
2952
+ // lib/state/model-limits.ts
2953
+ function createModelLimitCatalog() {
2954
+ const modelLimits = /* @__PURE__ */ new Map();
2955
+ return {
2956
+ record(providerId, modelId, limit) {
2957
+ if (!providerId || !modelId || typeof limit !== "number" || limit <= 0) return;
2958
+ modelLimits.set(`${providerId}/${modelId}`, limit);
2959
+ },
2960
+ resolve(providerId, modelId) {
2961
+ if (!providerId || !modelId) return void 0;
2962
+ return modelLimits.get(`${providerId}/${modelId}`);
2963
+ },
2964
+ /**
2965
+ * Best-effort one-time seed from the host's provider catalog
2966
+ * (`client.config.providers()` → GET /config/providers). Never throws;
2967
+ * returns the number of model-limit entries recorded.
2968
+ */
2969
+ async hydrateFromClient(client) {
2970
+ try {
2971
+ const config = client;
2972
+ const result = await config.config?.providers?.();
2973
+ const payload = result;
2974
+ const providers = payload?.data?.providers;
2975
+ if (!Array.isArray(providers)) return 0;
2976
+ let recorded = 0;
2977
+ for (const provider of providers) {
2978
+ const { id, models } = provider ?? {};
2979
+ if (typeof id !== "string" || !models) continue;
2980
+ for (const [modelId, model] of Object.entries(models)) {
2981
+ const limit = model?.limit;
2982
+ const context = limit?.context;
2983
+ if (typeof context === "number" && context > 0) {
2984
+ modelLimits.set(`${id}/${modelId}`, context);
2985
+ recorded++;
2986
+ }
2987
+ }
2988
+ }
2989
+ return recorded;
2990
+ } catch {
2991
+ return 0;
2992
+ }
2993
+ }
2994
+ };
2995
+ }
2996
+
2952
2997
  // lib/compress/search.ts
2953
2998
  import { tool } from "@opencode-ai/plugin";
2954
2999
  async function fetchSessionMessages(client, sessionId) {
@@ -4140,6 +4185,26 @@ var SessionStateRegistry = class {
4140
4185
  startsByCallId: /* @__PURE__ */ new Map(),
4141
4186
  pendingByCallId: /* @__PURE__ */ new Map()
4142
4187
  };
4188
+ // [FIX #312] Model-limit catalog (full rationale in ./model-limits.ts):
4189
+ // lets the messages hook reconcile state.modelContextLimit against the
4190
+ // model named on the request's user message instead of waiting one turn
4191
+ // for the system hook. Shared implementation — the test registry stub
4192
+ // composes the same factory.
4193
+ catalog = createModelLimitCatalog();
4194
+ recordModelLimit(providerId, modelId, limit) {
4195
+ this.catalog.record(providerId, modelId, limit);
4196
+ }
4197
+ resolveModelLimit(providerId, modelId) {
4198
+ return this.catalog.resolve(providerId, modelId);
4199
+ }
4200
+ /**
4201
+ * Best-effort one-time seed from the host's provider catalog
4202
+ * (`client.config.providers()` → GET /config/providers). Never throws;
4203
+ * returns the number of model-limit entries recorded.
4204
+ */
4205
+ hydrateModelLimitsFromClient(client) {
4206
+ return this.catalog.hydrateFromClient(client);
4207
+ }
4143
4208
  get(sessionId) {
4144
4209
  return this.states.get(sessionId);
4145
4210
  }
@@ -4688,8 +4753,8 @@ ${progressBar}`;
4688
4753
  let toastMessage = message;
4689
4754
  toastMessage = config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage);
4690
4755
  if (config.debug) {
4691
- const chatMessage = config.pruneNotification === "minimal" ? message : truncateToastBody(message);
4692
- await sendIgnoredMessage(client, sessionId, chatMessage, params, logger);
4756
+ logger.debug(`[ACP Debug] Compress notification:
4757
+ ${message}`);
4693
4758
  }
4694
4759
  await client.tui.showToast({
4695
4760
  body: {
@@ -5672,11 +5737,6 @@ exact values, errors). Then add "acknowledgeRisk": true to the compress tool cal
5672
5737
  Without acknowledgeRisk: true, the compression will be rejected again.`;
5673
5738
  return new Error(message);
5674
5739
  }
5675
- function buildPreemptiveAcknowledgeError() {
5676
- return new Error(
5677
- 'Parameter "acknowledgeRisk": true was provided, but no quality gate rejection is pending. This parameter is only valid immediately after a compression was rejected by the quality gate. Remove it and try again.'
5678
- );
5679
- }
5680
5740
 
5681
5741
  // lib/compress/pipeline.ts
5682
5742
  function snapshotCompressionState(state) {
@@ -6150,13 +6210,13 @@ function createCompressRangeTool(factoryCtx) {
6150
6210
  }
6151
6211
  const acknowledgeRisk = args.acknowledgeRisk === true;
6152
6212
  const qualityGateRetryPendingBefore = ctx.state.qualityGateRetryPending;
6153
- if (acknowledgeRisk && !ctx.state.qualityGateRetryPending) {
6154
- throw buildPreemptiveAcknowledgeError();
6213
+ const bypassQuality = acknowledgeRisk && ctx.state.qualityGateRetryPending;
6214
+ const ignoredAcknowledgeRisk = acknowledgeRisk && !ctx.state.qualityGateRetryPending;
6215
+ if (ignoredAcknowledgeRisk) {
6216
+ ctx.logger.warn("compress: acknowledgeRisk ignored \u2014 no quality gate rejection pending");
6155
6217
  }
6156
- if (acknowledgeRisk) {
6157
- ctx.state.qualityGateRetryPending = false;
6158
- } else {
6159
- ctx.state.qualityGateRetryPending = false;
6218
+ ctx.state.qualityGateRetryPending = false;
6219
+ if (!bypassQuality) {
6160
6220
  for (const plan of preparedPlans) {
6161
6221
  const result = evaluatePreCommitQuality(
6162
6222
  rawMessages,
@@ -6232,7 +6292,10 @@ function createCompressRangeTool(factoryCtx) {
6232
6292
  const skippedNote = phantomSkipNotice !== null ? `
6233
6293
  \u26A0\uFE0F ${phantomSkipNotice}
6234
6294
  ` : "";
6235
- return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.${skippedNote}
6295
+ const ackNote = ignoredAcknowledgeRisk ? `
6296
+ \u26A0\uFE0F acknowledgeRisk was ignored: no quality gate rejection was pending, so quality checks ran normally. Only pass it when retrying immediately after a quality gate rejection.
6297
+ ` : "";
6298
+ return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.${skippedNote}${ackNote}
6236
6299
  IMPORTANT: This was an automatic context compression. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next.
6237
6300
  \u{1F4A1} Tip: Use search_context('keyword') to find compressed content when you need it later.`;
6238
6301
  }
@@ -7566,16 +7629,6 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7566
7629
  { logger }
7567
7630
  );
7568
7631
  const hasRecommendations = recommendedRanges.length > 0;
7569
- if (config.debug && contextRanges.compressible.length > 0) {
7570
- const compressible = contextRanges.compressible;
7571
- const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
7572
- const lines = [
7573
- `[ACP Debug] Recommendation filter:`,
7574
- ` Input: ${compressible.length} range(s), ${fmt(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`,
7575
- ` Output: ${recommendedRanges.length} range(s) (last segment marked dangerous)`
7576
- ];
7577
- logger.debug(lines.join("\n"));
7578
- }
7579
7632
  const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
7580
7633
  const allInProtectedZone = protectedRefs.size > 0 && unprotectedCompressible.length === 0;
7581
7634
  const nothingToCompress = allProtected || allInProtectedZone;
@@ -7671,6 +7724,16 @@ ${rules}`;
7671
7724
  }
7672
7725
  }
7673
7726
  state.nudges.shouldInjectThisTurn = shouldInject;
7727
+ if (shouldInject && config.debug && contextRanges.compressible.length > 0) {
7728
+ const compressible = contextRanges.compressible;
7729
+ const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
7730
+ const lines = [
7731
+ `[ACP Debug] Recommendation filter:`,
7732
+ ` Input: ${compressible.length} range(s), ${fmt(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`,
7733
+ ` Output: ${recommendedRanges.length} range(s) (last segment marked dangerous)`
7734
+ ];
7735
+ logger.debug(lines.join("\n"));
7736
+ }
7674
7737
  let tipsText = null;
7675
7738
  if (shouldInject) {
7676
7739
  if (suffixMessage && composition.total > 0) {
@@ -8125,10 +8188,6 @@ function resolveSingleBlockTarget(messagesState, blockIdArg) {
8125
8188
  error: `Error: Block ${target.displayId} is nested inside active block ${activeAncestorBlockId}. Decompress block ${activeAncestorBlockId} first.`
8126
8189
  };
8127
8190
  }
8128
- return {
8129
- ok: false,
8130
- error: `Error: Block ${target.displayId} is not active. It may have already been decompressed.`
8131
- };
8132
8191
  }
8133
8192
  return { ok: true, targets: [target] };
8134
8193
  }
@@ -8281,7 +8340,7 @@ function createDecompressTool(factoryCtx) {
8281
8340
  const blockMessages = rawMessages.filter((m) => msgIdSet.has(extractMessageId(m)));
8282
8341
  const lines2 = blockMessages.map(extractMessageText2);
8283
8342
  const { writeFile: writeFile3 } = await import("fs/promises");
8284
- const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") : activeBlocks[0]?.summary ?? "(no content available)";
8343
+ const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") : targets[0]?.blocks[0]?.summary ?? "(no content available)";
8285
8344
  await writeFile3(targetPath, fileContent, "utf-8");
8286
8345
  const displayIds2 = targets.map((t) => `b${t.displayId}`).join(", ");
8287
8346
  return `Block(s) ${displayIds2} content (${blockMessages.length} messages, ${fileContent.length} chars) written to ${targetPath}. Block(s) stay compressed \u2014 context unchanged. Use read tool to access specific parts.`;
@@ -8738,16 +8797,23 @@ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
8738
8797
  lines.push(`Sorted by ${sort === "time" ? "time" : sort === "age" ? "age" : "size"}`);
8739
8798
  lines.push("");
8740
8799
  const shown = sorted.slice(0, limit);
8800
+ const activeCount = sorted.filter((b) => b.active).length;
8801
+ const inactiveCount = sorted.length - activeCount;
8802
+ if (inactiveCount > 0) {
8803
+ lines.push(`${activeCount} active, ${inactiveCount} inactive/consumed`);
8804
+ lines.push("");
8805
+ }
8741
8806
  for (const b of shown) {
8742
8807
  const survived = b.survivedCount ?? 0;
8743
8808
  const gen = b.generation ?? "young";
8744
8809
  const effCount = b.effectiveMessageIds?.length ?? 0;
8745
8810
  const consumed = b.includedBlockIds && b.includedBlockIds.length > 0 ? ` nested=[${b.includedBlockIds.map((n) => `b${n}`).join(",")}]` : "";
8811
+ const status = b.active ? "" : " [inactive]";
8746
8812
  const topic = b.topic || "(no topic)";
8747
8813
  const tier = tierLabel(b);
8748
8814
  const effTokens = getEffectiveCompressedTokens(b, blocksById);
8749
8815
  lines.push(
8750
- ` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}`
8816
+ ` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}${status}`
8751
8817
  );
8752
8818
  lines.push(` "${topic}"`);
8753
8819
  }
@@ -8768,8 +8834,10 @@ function buildStatusReport(renderCtx, rawMessages, options) {
8768
8834
  const sort = options?.sort ?? "size";
8769
8835
  const limit = options?.limit ?? 30;
8770
8836
  const msgState = renderCtx.state.prune.messages;
8771
- const activeIds = Array.from(msgState.activeBlockIds).sort((a, b) => a - b);
8772
- const allBlocks = activeIds.map((id) => msgState.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
8837
+ const allBlocks = Array.from(msgState.blocksById.values()).sort(
8838
+ (a, b) => a.blockId - b.blockId
8839
+ );
8840
+ const activeBlocks = allBlocks.filter((b) => b.active);
8773
8841
  const lines = [];
8774
8842
  if (scope === "compressed") {
8775
8843
  lines.push(...renderCompressedDrilldown(allBlocks, sort, limit, msgState.blocksById));
@@ -8791,7 +8859,7 @@ function buildStatusReport(renderCtx, rawMessages, options) {
8791
8859
  visibleMsgs,
8792
8860
  summaryTokens,
8793
8861
  systemTokens,
8794
- allBlocks,
8862
+ activeBlocks,
8795
8863
  false,
8796
8864
  rawMessages,
8797
8865
  renderCtx
@@ -8949,7 +9017,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
8949
9017
  import { join as join3 } from "path";
8950
9018
  import { existsSync as existsSync3 } from "fs";
8951
9019
  import { homedir as homedir3 } from "os";
8952
- var LOG_VERSION = true ? "1.14.18-pr.307.19" : "dev";
9020
+ var LOG_VERSION = true ? "1.14.19-pr.297.30" : "dev";
8953
9021
  var Logger = class {
8954
9022
  logDir;
8955
9023
  enabled;
@@ -9004,7 +9072,7 @@ var Logger = class {
9004
9072
  }
9005
9073
  }
9006
9074
  async write(level, component, message, data) {
9007
- if (!this.enabled) return;
9075
+ if (!this.enabled && level !== "ERROR" && level !== "WARN") return;
9008
9076
  try {
9009
9077
  await this.ensureLogDir();
9010
9078
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -9031,12 +9099,10 @@ var Logger = class {
9031
9099
  return this.write("DEBUG", component, message, data);
9032
9100
  }
9033
9101
  warn(message, data) {
9034
- if (!this.enabled) return;
9035
9102
  const component = this.getCallerFile(2);
9036
9103
  return this.write("WARN", component, message, data);
9037
9104
  }
9038
9105
  error(message, data) {
9039
- if (!this.enabled) return;
9040
9106
  const component = this.getCallerFile(2);
9041
9107
  return this.write("ERROR", component, message, data);
9042
9108
  }
@@ -10047,6 +10113,7 @@ function applyMessageFilters(messages, config, logger, ctx) {
10047
10113
  }
10048
10114
  const result = { partsFiltered: 0, partsDropped: 0, partsModified: 0 };
10049
10115
  const total = messages.length;
10116
+ const warnedFilters = /* @__PURE__ */ new Set();
10050
10117
  const buildCtx = (text, role, i) => ({
10051
10118
  text,
10052
10119
  role,
@@ -10102,11 +10169,14 @@ function applyMessageFilters(messages, config, logger, ctx) {
10102
10169
  try {
10103
10170
  decision = filter.filter(filterCtx);
10104
10171
  } catch (err) {
10105
- logger.warn("Message filter threw error", {
10106
- filter: filter.name,
10107
- error: err instanceof Error ? err.message : String(err),
10108
- messageIndex: i
10109
- });
10172
+ if (!warnedFilters.has(filter.name)) {
10173
+ warnedFilters.add(filter.name);
10174
+ logger.warn("Message filter threw error", {
10175
+ filter: filter.name,
10176
+ error: err instanceof Error ? err.message : String(err),
10177
+ messageIndex: i
10178
+ });
10179
+ }
10110
10180
  continue;
10111
10181
  }
10112
10182
  if (decision.action === "keep") continue;
@@ -10514,6 +10584,11 @@ function isInternalAgentRequest(messages) {
10514
10584
  }
10515
10585
  function createSystemPromptHandler(registry4, logger, config, prompts) {
10516
10586
  return async (input, output) => {
10587
+ registry4.recordModelLimit(
10588
+ input.model?.providerID,
10589
+ input.model?.id,
10590
+ input.model?.limit?.context
10591
+ );
10517
10592
  const state = input.sessionID ? registry4.get(input.sessionID) : void 0;
10518
10593
  if (state && input.model?.limit?.context) {
10519
10594
  state.modelContextLimit = input.model.limit.context;
@@ -10569,6 +10644,14 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
10569
10644
  messages,
10570
10645
  config
10571
10646
  );
10647
+ const requestModel = lastUserMessage.info.model;
10648
+ const requestModelLimit = registry4.resolveModelLimit(
10649
+ requestModel?.providerID,
10650
+ requestModel?.modelID
10651
+ );
10652
+ if (requestModelLimit !== void 0) {
10653
+ state.modelContextLimit = requestModelLimit;
10654
+ }
10572
10655
  await updatePerTurnState(state, logger, messages);
10573
10656
  }
10574
10657
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
@@ -10614,23 +10697,6 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
10614
10697
  config.debug ? (text) => {
10615
10698
  logger.debug(`[ACP Debug] Nudge injected:
10616
10699
  ${text}`);
10617
- if (state.sessionId && lastUserMessage) {
10618
- const userInfo = lastUserMessage.info;
10619
- sendIgnoredMessage(
10620
- client,
10621
- state.sessionId,
10622
- `[ACP Debug Nudge]
10623
- ${text}`,
10624
- {
10625
- providerId: userInfo.model?.providerID,
10626
- modelId: userInfo.model?.modelID,
10627
- agent: userInfo.agent,
10628
- variant: userInfo.variant
10629
- },
10630
- logger
10631
- ).catch(() => {
10632
- });
10633
- }
10634
10700
  client.tui.showToast({
10635
10701
  body: {
10636
10702
  title: "ACP: Nudge Injected",
@@ -10684,10 +10750,9 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
10684
10750
  const sub = input.arguments?.trim().toLowerCase();
10685
10751
  if (sub === "stats" || sub === "status") {
10686
10752
  await handleStatsCommand(commandCtx);
10687
- throw new Error("__DCP_CONTEXT_HANDLED__");
10753
+ return;
10688
10754
  }
10689
10755
  await handleContextCommand(commandCtx);
10690
- throw new Error("__DCP_CONTEXT_HANDLED__");
10691
10756
  }
10692
10757
  };
10693
10758
  }
@@ -10954,6 +11019,25 @@ var server = (async (ctx) => {
10954
11019
  if (isSecureMode()) {
10955
11020
  configureClientAuth(ctx.client);
10956
11021
  }
11022
+ registry4.hydrateModelLimitsFromClient(ctx.client).then(
11023
+ (recorded) => {
11024
+ if (recorded > 0) {
11025
+ logger.info("Model limit catalog seeded from provider config", {
11026
+ models: recorded
11027
+ });
11028
+ } else {
11029
+ logger.warn(
11030
+ "Model limit catalog seeding recorded no entries \u2014 falling back to per-request refresh (system.transform)"
11031
+ );
11032
+ }
11033
+ },
11034
+ (error) => {
11035
+ logger.warn(
11036
+ "Model limit catalog seeding failed \u2014 falling back to per-request refresh (system.transform)",
11037
+ { error: error instanceof Error ? error.message : String(error) }
11038
+ );
11039
+ }
11040
+ );
10957
11041
  logger.info("DCP initialized");
10958
11042
  startAutoUpdate(ctx, config.autoUpdate);
10959
11043
  const compressToolContext = {