opencode-acp 1.14.19 → 1.14.20-pr.316.31
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 +16 -1
- package/README.zh-CN.md +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +171 -60
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/decompress.d.ts.map +1 -1
- package/dist/lib/compress/quality-gate/index.d.ts +1 -1
- package/dist/lib/compress/quality-gate/index.d.ts.map +1 -1
- package/dist/lib/compress/quality-gate/rejection.d.ts +0 -1
- package/dist/lib/compress/quality-gate/rejection.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/compress/status.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts +2 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/logger.d.ts +2 -2
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/messages/filter/apply.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/state/index.d.ts +1 -0
- package/dist/lib/state/index.d.ts.map +1 -1
- package/dist/lib/state/model-limits.d.ts +27 -0
- package/dist/lib/state/model-limits.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +2 -0
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts +9 -0
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/state/types.d.ts +5 -1
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/ui/notification.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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,20 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
|
|
|
492
493
|
|
|
493
494
|
## Changelog
|
|
494
495
|
|
|
496
|
+
### v1.14.20 — Post-v1.14.19 fix batch (modelContextLimit lifecycle, inactive-block decompress, logging)
|
|
497
|
+
|
|
498
|
+
**Problem**: Five clusters of post-v1.14.19 issues: (1) `modelContextLimit` stayed stale after a model switch (#312) — within one LLM request the messages hook runs before the system hook refreshes the limit, and on a catalog miss (fresh instance + failed hydration) the previous model's window survived, so every percentage threshold misfired (#315); (2) `decompress` failed on standalone inactive blocks and inactive blocks were invisible in `acp_status` (#193); (3) a preemptive `acknowledgeRisk: true` (carried over from a non-quality error) hard-failed with "no quality gate rejection is pending" (#301); (4) debug nudge notifications used `sendIgnoredMessage` causing phantom turns, the debug recommendation-filter log spammed every turn, and ERROR/WARN lines only reached the daily log when debug was on (#311, #278, #279).
|
|
499
|
+
|
|
500
|
+
**Fix**:
|
|
501
|
+
- #314 + #315: reconcile `state.modelContextLimit` from the model-limit catalog on model switch; record the (provider, model) identity pair alongside the limit and, on a catalog miss, invalidate it when the request's model identity mismatches (match keeps it; legacy persisted states without an identity are treated as stale — one blind turn per upgraded session). The catalog is extracted to `lib/state/model-limits.ts`; failed hydration now logs (info with entry count on success, warn on zero entries/failure) instead of degrading silently.
|
|
502
|
+
- #193: `decompress` accepts standalone inactive blocks (nested-block redirect unchanged) and `decompress toFile` writes the real block content; `acp_status scope=compressed` now lists all blocks with an `[inactive]` marker plus active/inactive counts (overview still shows active blocks only).
|
|
503
|
+
- #303: `acknowledgeRisk` without a pending quality-gate rejection is now a no-op with a usage-teaching note instead of an error — only a real quality-gate rejection arms the bypass, so the gate's protection is unchanged.
|
|
504
|
+
- #278/#279/#311: debug notifications no longer use `sendIgnoredMessage` (phantom turn loop gone); the recommendation-filter log is gated behind `shouldInject` (no more per-turn noise); ERROR/WARN are written to the daily log even when debug is off.
|
|
505
|
+
|
|
506
|
+
Files: `lib/compress/range.ts`, `lib/state/model-limits.ts`, `lib/state/types.ts`, `index.ts`, `lib/state/registry.ts`. Tests: 1003 pass.
|
|
507
|
+
|
|
508
|
+
**Install**: `opencode plugin opencode-acp@latest --global`
|
|
509
|
+
|
|
495
510
|
### v1.14.19 — Fix release pipeline for real (npm 10 `--ignore-scripts` bug)
|
|
496
511
|
|
|
497
512
|
**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`).
|
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
|
|
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,20 @@ ACP 是 DCP 的直接替代品。迁移步骤:
|
|
|
446
447
|
|
|
447
448
|
## 更新日志
|
|
448
449
|
|
|
450
|
+
### v1.14.20 — v1.14.19 之后的修复批次(modelContextLimit 生命周期、inactive block 解压缩、日志)
|
|
451
|
+
|
|
452
|
+
**问题**:五类 v1.14.19 之后发现的问题:(1)`modelContextLimit` 在模型切换后失效(#312)——同一次 LLM 请求内,messages hook 先于 system hook 刷新窗口值,而目录未命中时(新实例 + 水合失败),上一个模型的窗口值会一直残留,导致所有按百分比计算的阈值误触发(#315);(2)`decompress` 对独立的 inactive block 报 `Block not found`,且 inactive block 在 `acp_status` 里完全不可见(#193);(3)预防式 `acknowledgeRisk: true`(从非质量门禁错误里带过来的)直接报错 `no quality gate rejection is pending`(#301);(4)debug nudge 通知用 `sendIgnoredMessage` 触发幽灵回合(#311),debug 推荐过滤日志每回合刷屏,ERROR/WARN 在 debug 关闭时不进日常日志(#311、#278、#279)。
|
|
453
|
+
|
|
454
|
+
**修复**:
|
|
455
|
+
- #314 + #315:模型切换时从模型限制目录重新对齐 `state.modelContextLimit`;目录未命中时记录 (provider, model) 身份对,请求的身份与记录不一致时使窗口值失效(一致则保留;未记录身份的旧持久化状态按失效处理,升级后每个会话盲一轮)。目录提取到 `lib/state/model-limits.ts`;水合失败现在会记日志(成功 info 带条目数,失败 warn),不再静默降级。
|
|
456
|
+
- #193:`decompress` 接受独立的 inactive block(嵌套 block 重定向不变);`acp_status scope=compressed` 现在列出所有 block,inactive 的带 `[inactive]` 标记,并显示 active/inactive 计数(overview 仍只显示 active)。
|
|
457
|
+
- #303:没有待处理的质量门禁拒绝时,`acknowledgeRisk` 变为 no-op + 用法提示,不再报错 —— 真正的质量门禁拒绝才会武装 bypass,门禁保护不受影响。
|
|
458
|
+
- #278/#279/#311:debug 通知不再用 `sendIgnoredMessage`(幽灵回合循环消失);推荐过滤日志改为 `shouldInject` 后才输出(不再每回合刷屏);ERROR/WARN 在 debug 关闭时也写入日常日志。
|
|
459
|
+
|
|
460
|
+
文件:`lib/compress/range.ts`、`lib/state/model-limits.ts`、`lib/state/types.ts`、`index.ts`、`lib/state/registry.ts`。测试:1003 通过。
|
|
461
|
+
|
|
462
|
+
**安装**:`opencode plugin opencode-acp@latest --global`
|
|
463
|
+
|
|
449
464
|
### v1.14.19 — 彻底修复发版流水线(npm 10 `--ignore-scripts` 失效)
|
|
450
465
|
|
|
451
466
|
**问题**: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`)。
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
@@ -2418,7 +2418,9 @@ async function saveSessionState(sessionState, logger, sessionName) {
|
|
|
2418
2418
|
nextRef: sessionState.messageIds.nextRef
|
|
2419
2419
|
},
|
|
2420
2420
|
lastCompaction: sessionState.lastCompaction,
|
|
2421
|
-
modelContextLimit: sessionState.modelContextLimit
|
|
2421
|
+
modelContextLimit: sessionState.modelContextLimit,
|
|
2422
|
+
modelProviderID: sessionState.modelProviderID,
|
|
2423
|
+
modelID: sessionState.modelID
|
|
2422
2424
|
};
|
|
2423
2425
|
await writePersistedSessionState(sessionState.sessionId, state, logger);
|
|
2424
2426
|
}
|
|
@@ -2949,6 +2951,51 @@ function applyPendingCompressionDurations(state) {
|
|
|
2949
2951
|
return updates;
|
|
2950
2952
|
}
|
|
2951
2953
|
|
|
2954
|
+
// lib/state/model-limits.ts
|
|
2955
|
+
function createModelLimitCatalog() {
|
|
2956
|
+
const modelLimits = /* @__PURE__ */ new Map();
|
|
2957
|
+
return {
|
|
2958
|
+
record(providerId, modelId, limit) {
|
|
2959
|
+
if (!providerId || !modelId || typeof limit !== "number" || limit <= 0) return;
|
|
2960
|
+
modelLimits.set(`${providerId}/${modelId}`, limit);
|
|
2961
|
+
},
|
|
2962
|
+
resolve(providerId, modelId) {
|
|
2963
|
+
if (!providerId || !modelId) return void 0;
|
|
2964
|
+
return modelLimits.get(`${providerId}/${modelId}`);
|
|
2965
|
+
},
|
|
2966
|
+
/**
|
|
2967
|
+
* Best-effort one-time seed from the host's provider catalog
|
|
2968
|
+
* (`client.config.providers()` → GET /config/providers). Never throws;
|
|
2969
|
+
* returns the number of model-limit entries recorded.
|
|
2970
|
+
*/
|
|
2971
|
+
async hydrateFromClient(client) {
|
|
2972
|
+
try {
|
|
2973
|
+
const config = client;
|
|
2974
|
+
const result = await config.config?.providers?.();
|
|
2975
|
+
const payload = result;
|
|
2976
|
+
const providers = payload?.data?.providers;
|
|
2977
|
+
if (!Array.isArray(providers)) return 0;
|
|
2978
|
+
let recorded = 0;
|
|
2979
|
+
for (const provider of providers) {
|
|
2980
|
+
const { id, models } = provider ?? {};
|
|
2981
|
+
if (typeof id !== "string" || !models) continue;
|
|
2982
|
+
for (const [modelId, model] of Object.entries(models)) {
|
|
2983
|
+
const limit = model?.limit;
|
|
2984
|
+
const context = limit?.context;
|
|
2985
|
+
if (typeof context === "number" && context > 0) {
|
|
2986
|
+
modelLimits.set(`${id}/${modelId}`, context);
|
|
2987
|
+
recorded++;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
return recorded;
|
|
2992
|
+
} catch {
|
|
2993
|
+
return 0;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2952
2999
|
// lib/compress/search.ts
|
|
2953
3000
|
import { tool } from "@opencode-ai/plugin";
|
|
2954
3001
|
async function fetchSessionMessages(client, sessionId) {
|
|
@@ -4140,6 +4187,26 @@ var SessionStateRegistry = class {
|
|
|
4140
4187
|
startsByCallId: /* @__PURE__ */ new Map(),
|
|
4141
4188
|
pendingByCallId: /* @__PURE__ */ new Map()
|
|
4142
4189
|
};
|
|
4190
|
+
// [FIX #312] Model-limit catalog (full rationale in ./model-limits.ts):
|
|
4191
|
+
// lets the messages hook reconcile state.modelContextLimit against the
|
|
4192
|
+
// model named on the request's user message instead of waiting one turn
|
|
4193
|
+
// for the system hook. Shared implementation — the test registry stub
|
|
4194
|
+
// composes the same factory.
|
|
4195
|
+
catalog = createModelLimitCatalog();
|
|
4196
|
+
recordModelLimit(providerId, modelId, limit) {
|
|
4197
|
+
this.catalog.record(providerId, modelId, limit);
|
|
4198
|
+
}
|
|
4199
|
+
resolveModelLimit(providerId, modelId) {
|
|
4200
|
+
return this.catalog.resolve(providerId, modelId);
|
|
4201
|
+
}
|
|
4202
|
+
/**
|
|
4203
|
+
* Best-effort one-time seed from the host's provider catalog
|
|
4204
|
+
* (`client.config.providers()` → GET /config/providers). Never throws;
|
|
4205
|
+
* returns the number of model-limit entries recorded.
|
|
4206
|
+
*/
|
|
4207
|
+
hydrateModelLimitsFromClient(client) {
|
|
4208
|
+
return this.catalog.hydrateFromClient(client);
|
|
4209
|
+
}
|
|
4143
4210
|
get(sessionId) {
|
|
4144
4211
|
return this.states.get(sessionId);
|
|
4145
4212
|
}
|
|
@@ -4228,6 +4295,8 @@ function createSessionState() {
|
|
|
4228
4295
|
lastCompaction: 0,
|
|
4229
4296
|
currentTurn: 0,
|
|
4230
4297
|
modelContextLimit: void 0,
|
|
4298
|
+
modelProviderID: void 0,
|
|
4299
|
+
modelID: void 0,
|
|
4231
4300
|
systemPromptTokens: void 0,
|
|
4232
4301
|
qualityGateRetryPending: false
|
|
4233
4302
|
};
|
|
@@ -4267,6 +4336,8 @@ function resetSessionState(state) {
|
|
|
4267
4336
|
state.lastCompaction = 0;
|
|
4268
4337
|
state.currentTurn = 0;
|
|
4269
4338
|
state.modelContextLimit = void 0;
|
|
4339
|
+
state.modelProviderID = void 0;
|
|
4340
|
+
state.modelID = void 0;
|
|
4270
4341
|
state.systemPromptTokens = void 0;
|
|
4271
4342
|
state.qualityGateRetryPending = false;
|
|
4272
4343
|
}
|
|
@@ -4341,6 +4412,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
4341
4412
|
}
|
|
4342
4413
|
if (typeof persisted.modelContextLimit === "number" && persisted.modelContextLimit > 0) {
|
|
4343
4414
|
state.modelContextLimit = persisted.modelContextLimit;
|
|
4415
|
+
state.modelProviderID = persisted.modelProviderID;
|
|
4416
|
+
state.modelID = persisted.modelID;
|
|
4344
4417
|
}
|
|
4345
4418
|
const applied = applyPendingCompressionDurations(state);
|
|
4346
4419
|
if (applied > 0) {
|
|
@@ -4688,8 +4761,8 @@ ${progressBar}`;
|
|
|
4688
4761
|
let toastMessage = message;
|
|
4689
4762
|
toastMessage = config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage);
|
|
4690
4763
|
if (config.debug) {
|
|
4691
|
-
|
|
4692
|
-
|
|
4764
|
+
logger.debug(`[ACP Debug] Compress notification:
|
|
4765
|
+
${message}`);
|
|
4693
4766
|
}
|
|
4694
4767
|
await client.tui.showToast({
|
|
4695
4768
|
body: {
|
|
@@ -5672,11 +5745,6 @@ exact values, errors). Then add "acknowledgeRisk": true to the compress tool cal
|
|
|
5672
5745
|
Without acknowledgeRisk: true, the compression will be rejected again.`;
|
|
5673
5746
|
return new Error(message);
|
|
5674
5747
|
}
|
|
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
5748
|
|
|
5681
5749
|
// lib/compress/pipeline.ts
|
|
5682
5750
|
function snapshotCompressionState(state) {
|
|
@@ -6150,13 +6218,13 @@ function createCompressRangeTool(factoryCtx) {
|
|
|
6150
6218
|
}
|
|
6151
6219
|
const acknowledgeRisk = args.acknowledgeRisk === true;
|
|
6152
6220
|
const qualityGateRetryPendingBefore = ctx.state.qualityGateRetryPending;
|
|
6153
|
-
|
|
6154
|
-
|
|
6221
|
+
const bypassQuality = acknowledgeRisk && ctx.state.qualityGateRetryPending;
|
|
6222
|
+
const ignoredAcknowledgeRisk = acknowledgeRisk && !ctx.state.qualityGateRetryPending;
|
|
6223
|
+
if (ignoredAcknowledgeRisk) {
|
|
6224
|
+
ctx.logger.warn("compress: acknowledgeRisk ignored \u2014 no quality gate rejection pending");
|
|
6155
6225
|
}
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
} else {
|
|
6159
|
-
ctx.state.qualityGateRetryPending = false;
|
|
6226
|
+
ctx.state.qualityGateRetryPending = false;
|
|
6227
|
+
if (!bypassQuality) {
|
|
6160
6228
|
for (const plan of preparedPlans) {
|
|
6161
6229
|
const result = evaluatePreCommitQuality(
|
|
6162
6230
|
rawMessages,
|
|
@@ -6232,7 +6300,10 @@ function createCompressRangeTool(factoryCtx) {
|
|
|
6232
6300
|
const skippedNote = phantomSkipNotice !== null ? `
|
|
6233
6301
|
\u26A0\uFE0F ${phantomSkipNotice}
|
|
6234
6302
|
` : "";
|
|
6235
|
-
|
|
6303
|
+
const ackNote = ignoredAcknowledgeRisk ? `
|
|
6304
|
+
\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.
|
|
6305
|
+
` : "";
|
|
6306
|
+
return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.${skippedNote}${ackNote}
|
|
6236
6307
|
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
6308
|
\u{1F4A1} Tip: Use search_context('keyword') to find compressed content when you need it later.`;
|
|
6238
6309
|
}
|
|
@@ -7566,16 +7637,6 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
7566
7637
|
{ logger }
|
|
7567
7638
|
);
|
|
7568
7639
|
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
7640
|
const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
|
|
7580
7641
|
const allInProtectedZone = protectedRefs.size > 0 && unprotectedCompressible.length === 0;
|
|
7581
7642
|
const nothingToCompress = allProtected || allInProtectedZone;
|
|
@@ -7671,6 +7732,16 @@ ${rules}`;
|
|
|
7671
7732
|
}
|
|
7672
7733
|
}
|
|
7673
7734
|
state.nudges.shouldInjectThisTurn = shouldInject;
|
|
7735
|
+
if (shouldInject && config.debug && contextRanges.compressible.length > 0) {
|
|
7736
|
+
const compressible = contextRanges.compressible;
|
|
7737
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
7738
|
+
const lines = [
|
|
7739
|
+
`[ACP Debug] Recommendation filter:`,
|
|
7740
|
+
` Input: ${compressible.length} range(s), ${fmt(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`,
|
|
7741
|
+
` Output: ${recommendedRanges.length} range(s) (last segment marked dangerous)`
|
|
7742
|
+
];
|
|
7743
|
+
logger.debug(lines.join("\n"));
|
|
7744
|
+
}
|
|
7674
7745
|
let tipsText = null;
|
|
7675
7746
|
if (shouldInject) {
|
|
7676
7747
|
if (suffixMessage && composition.total > 0) {
|
|
@@ -8125,10 +8196,6 @@ function resolveSingleBlockTarget(messagesState, blockIdArg) {
|
|
|
8125
8196
|
error: `Error: Block ${target.displayId} is nested inside active block ${activeAncestorBlockId}. Decompress block ${activeAncestorBlockId} first.`
|
|
8126
8197
|
};
|
|
8127
8198
|
}
|
|
8128
|
-
return {
|
|
8129
|
-
ok: false,
|
|
8130
|
-
error: `Error: Block ${target.displayId} is not active. It may have already been decompressed.`
|
|
8131
|
-
};
|
|
8132
8199
|
}
|
|
8133
8200
|
return { ok: true, targets: [target] };
|
|
8134
8201
|
}
|
|
@@ -8281,7 +8348,7 @@ function createDecompressTool(factoryCtx) {
|
|
|
8281
8348
|
const blockMessages = rawMessages.filter((m) => msgIdSet.has(extractMessageId(m)));
|
|
8282
8349
|
const lines2 = blockMessages.map(extractMessageText2);
|
|
8283
8350
|
const { writeFile: writeFile3 } = await import("fs/promises");
|
|
8284
|
-
const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") :
|
|
8351
|
+
const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") : targets[0]?.blocks[0]?.summary ?? "(no content available)";
|
|
8285
8352
|
await writeFile3(targetPath, fileContent, "utf-8");
|
|
8286
8353
|
const displayIds2 = targets.map((t) => `b${t.displayId}`).join(", ");
|
|
8287
8354
|
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 +8805,23 @@ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
|
|
|
8738
8805
|
lines.push(`Sorted by ${sort === "time" ? "time" : sort === "age" ? "age" : "size"}`);
|
|
8739
8806
|
lines.push("");
|
|
8740
8807
|
const shown = sorted.slice(0, limit);
|
|
8808
|
+
const activeCount = sorted.filter((b) => b.active).length;
|
|
8809
|
+
const inactiveCount = sorted.length - activeCount;
|
|
8810
|
+
if (inactiveCount > 0) {
|
|
8811
|
+
lines.push(`${activeCount} active, ${inactiveCount} inactive/consumed`);
|
|
8812
|
+
lines.push("");
|
|
8813
|
+
}
|
|
8741
8814
|
for (const b of shown) {
|
|
8742
8815
|
const survived = b.survivedCount ?? 0;
|
|
8743
8816
|
const gen = b.generation ?? "young";
|
|
8744
8817
|
const effCount = b.effectiveMessageIds?.length ?? 0;
|
|
8745
8818
|
const consumed = b.includedBlockIds && b.includedBlockIds.length > 0 ? ` nested=[${b.includedBlockIds.map((n) => `b${n}`).join(",")}]` : "";
|
|
8819
|
+
const status = b.active ? "" : " [inactive]";
|
|
8746
8820
|
const topic = b.topic || "(no topic)";
|
|
8747
8821
|
const tier = tierLabel(b);
|
|
8748
8822
|
const effTokens = getEffectiveCompressedTokens(b, blocksById);
|
|
8749
8823
|
lines.push(
|
|
8750
|
-
` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}`
|
|
8824
|
+
` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}${status}`
|
|
8751
8825
|
);
|
|
8752
8826
|
lines.push(` "${topic}"`);
|
|
8753
8827
|
}
|
|
@@ -8768,8 +8842,10 @@ function buildStatusReport(renderCtx, rawMessages, options) {
|
|
|
8768
8842
|
const sort = options?.sort ?? "size";
|
|
8769
8843
|
const limit = options?.limit ?? 30;
|
|
8770
8844
|
const msgState = renderCtx.state.prune.messages;
|
|
8771
|
-
const
|
|
8772
|
-
|
|
8845
|
+
const allBlocks = Array.from(msgState.blocksById.values()).sort(
|
|
8846
|
+
(a, b) => a.blockId - b.blockId
|
|
8847
|
+
);
|
|
8848
|
+
const activeBlocks = allBlocks.filter((b) => b.active);
|
|
8773
8849
|
const lines = [];
|
|
8774
8850
|
if (scope === "compressed") {
|
|
8775
8851
|
lines.push(...renderCompressedDrilldown(allBlocks, sort, limit, msgState.blocksById));
|
|
@@ -8791,7 +8867,7 @@ function buildStatusReport(renderCtx, rawMessages, options) {
|
|
|
8791
8867
|
visibleMsgs,
|
|
8792
8868
|
summaryTokens,
|
|
8793
8869
|
systemTokens,
|
|
8794
|
-
|
|
8870
|
+
activeBlocks,
|
|
8795
8871
|
false,
|
|
8796
8872
|
rawMessages,
|
|
8797
8873
|
renderCtx
|
|
@@ -8949,7 +9025,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
|
8949
9025
|
import { join as join3 } from "path";
|
|
8950
9026
|
import { existsSync as existsSync3 } from "fs";
|
|
8951
9027
|
import { homedir as homedir3 } from "os";
|
|
8952
|
-
var LOG_VERSION = true ? "1.14.
|
|
9028
|
+
var LOG_VERSION = true ? "1.14.20-pr.316.31" : "dev";
|
|
8953
9029
|
var Logger = class {
|
|
8954
9030
|
logDir;
|
|
8955
9031
|
enabled;
|
|
@@ -9004,7 +9080,7 @@ var Logger = class {
|
|
|
9004
9080
|
}
|
|
9005
9081
|
}
|
|
9006
9082
|
async write(level, component, message, data) {
|
|
9007
|
-
if (!this.enabled) return;
|
|
9083
|
+
if (!this.enabled && level !== "ERROR" && level !== "WARN") return;
|
|
9008
9084
|
try {
|
|
9009
9085
|
await this.ensureLogDir();
|
|
9010
9086
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9031,12 +9107,10 @@ var Logger = class {
|
|
|
9031
9107
|
return this.write("DEBUG", component, message, data);
|
|
9032
9108
|
}
|
|
9033
9109
|
warn(message, data) {
|
|
9034
|
-
if (!this.enabled) return;
|
|
9035
9110
|
const component = this.getCallerFile(2);
|
|
9036
9111
|
return this.write("WARN", component, message, data);
|
|
9037
9112
|
}
|
|
9038
9113
|
error(message, data) {
|
|
9039
|
-
if (!this.enabled) return;
|
|
9040
9114
|
const component = this.getCallerFile(2);
|
|
9041
9115
|
return this.write("ERROR", component, message, data);
|
|
9042
9116
|
}
|
|
@@ -10047,6 +10121,7 @@ function applyMessageFilters(messages, config, logger, ctx) {
|
|
|
10047
10121
|
}
|
|
10048
10122
|
const result = { partsFiltered: 0, partsDropped: 0, partsModified: 0 };
|
|
10049
10123
|
const total = messages.length;
|
|
10124
|
+
const warnedFilters = /* @__PURE__ */ new Set();
|
|
10050
10125
|
const buildCtx = (text, role, i) => ({
|
|
10051
10126
|
text,
|
|
10052
10127
|
role,
|
|
@@ -10102,11 +10177,14 @@ function applyMessageFilters(messages, config, logger, ctx) {
|
|
|
10102
10177
|
try {
|
|
10103
10178
|
decision = filter.filter(filterCtx);
|
|
10104
10179
|
} catch (err) {
|
|
10105
|
-
|
|
10106
|
-
filter
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10180
|
+
if (!warnedFilters.has(filter.name)) {
|
|
10181
|
+
warnedFilters.add(filter.name);
|
|
10182
|
+
logger.warn("Message filter threw error", {
|
|
10183
|
+
filter: filter.name,
|
|
10184
|
+
error: err instanceof Error ? err.message : String(err),
|
|
10185
|
+
messageIndex: i
|
|
10186
|
+
});
|
|
10187
|
+
}
|
|
10110
10188
|
continue;
|
|
10111
10189
|
}
|
|
10112
10190
|
if (decision.action === "keep") continue;
|
|
@@ -10514,9 +10592,16 @@ function isInternalAgentRequest(messages) {
|
|
|
10514
10592
|
}
|
|
10515
10593
|
function createSystemPromptHandler(registry4, logger, config, prompts) {
|
|
10516
10594
|
return async (input, output) => {
|
|
10595
|
+
registry4.recordModelLimit(
|
|
10596
|
+
input.model?.providerID,
|
|
10597
|
+
input.model?.id,
|
|
10598
|
+
input.model?.limit?.context
|
|
10599
|
+
);
|
|
10517
10600
|
const state = input.sessionID ? registry4.get(input.sessionID) : void 0;
|
|
10518
10601
|
if (state && input.model?.limit?.context) {
|
|
10519
10602
|
state.modelContextLimit = input.model.limit.context;
|
|
10603
|
+
state.modelProviderID = input.model?.providerID;
|
|
10604
|
+
state.modelID = input.model?.id;
|
|
10520
10605
|
}
|
|
10521
10606
|
if (!state || state.isSubAgent && !config.allowSubAgents) {
|
|
10522
10607
|
return;
|
|
@@ -10569,6 +10654,30 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
10569
10654
|
messages,
|
|
10570
10655
|
config
|
|
10571
10656
|
);
|
|
10657
|
+
const requestModel = lastUserMessage.info.model;
|
|
10658
|
+
const requestModelLimit = registry4.resolveModelLimit(
|
|
10659
|
+
requestModel?.providerID,
|
|
10660
|
+
requestModel?.modelID
|
|
10661
|
+
);
|
|
10662
|
+
if (requestModelLimit !== void 0) {
|
|
10663
|
+
state.modelContextLimit = requestModelLimit;
|
|
10664
|
+
state.modelProviderID = requestModel?.providerID;
|
|
10665
|
+
state.modelID = requestModel?.modelID;
|
|
10666
|
+
} else if (
|
|
10667
|
+
// [FIX #312 fallback] Catalog miss: we cannot CORRECT the
|
|
10668
|
+
// limit, but we can tell when it belongs to a DIFFERENT model.
|
|
10669
|
+
// Invalidate instead of letting every percentage threshold
|
|
10670
|
+
// below run against the wrong window (#312's false positive).
|
|
10671
|
+
// States persisted before this identity pair existed carry no
|
|
10672
|
+
// identity and are treated as stale for the same reason.
|
|
10673
|
+
// Consumers already tolerate undefined — fresh sessions run
|
|
10674
|
+
// with it until the first system.transform sets the pair.
|
|
10675
|
+
requestModel?.providerID && requestModel?.modelID && state.modelContextLimit !== void 0 && (state.modelProviderID !== requestModel.providerID || state.modelID !== requestModel.modelID)
|
|
10676
|
+
) {
|
|
10677
|
+
state.modelContextLimit = void 0;
|
|
10678
|
+
state.modelProviderID = requestModel.providerID;
|
|
10679
|
+
state.modelID = requestModel.modelID;
|
|
10680
|
+
}
|
|
10572
10681
|
await updatePerTurnState(state, logger, messages);
|
|
10573
10682
|
}
|
|
10574
10683
|
syncCompressPermissionState(state, config, hostPermissions, output.messages);
|
|
@@ -10614,23 +10723,6 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
10614
10723
|
config.debug ? (text) => {
|
|
10615
10724
|
logger.debug(`[ACP Debug] Nudge injected:
|
|
10616
10725
|
${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
10726
|
client.tui.showToast({
|
|
10635
10727
|
body: {
|
|
10636
10728
|
title: "ACP: Nudge Injected",
|
|
@@ -10954,6 +11046,25 @@ var server = (async (ctx) => {
|
|
|
10954
11046
|
if (isSecureMode()) {
|
|
10955
11047
|
configureClientAuth(ctx.client);
|
|
10956
11048
|
}
|
|
11049
|
+
registry4.hydrateModelLimitsFromClient(ctx.client).then(
|
|
11050
|
+
(recorded) => {
|
|
11051
|
+
if (recorded > 0) {
|
|
11052
|
+
logger.info("Model limit catalog seeded from provider config", {
|
|
11053
|
+
models: recorded
|
|
11054
|
+
});
|
|
11055
|
+
} else {
|
|
11056
|
+
logger.warn(
|
|
11057
|
+
"Model limit catalog seeding recorded no entries \u2014 falling back to per-request refresh (system.transform)"
|
|
11058
|
+
);
|
|
11059
|
+
}
|
|
11060
|
+
},
|
|
11061
|
+
(error) => {
|
|
11062
|
+
logger.warn(
|
|
11063
|
+
"Model limit catalog seeding failed \u2014 falling back to per-request refresh (system.transform)",
|
|
11064
|
+
{ error: error instanceof Error ? error.message : String(error) }
|
|
11065
|
+
);
|
|
11066
|
+
}
|
|
11067
|
+
);
|
|
10957
11068
|
logger.info("DCP initialized");
|
|
10958
11069
|
startAutoUpdate(ctx, config.autoUpdate);
|
|
10959
11070
|
const compressToolContext = {
|