opencode-acp 1.13.6 → 1.13.7
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 -0
- package/README.zh-CN.md +16 -0
- package/dist/index.js +153 -88
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/decompress.d.ts +2 -2
- package/dist/lib/compress/decompress.d.ts.map +1 -1
- package/dist/lib/compress/message.d.ts +2 -2
- package/dist/lib/compress/message.d.ts.map +1 -1
- package/dist/lib/compress/prune-tool.d.ts +2 -2
- package/dist/lib/compress/prune-tool.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts +2 -2
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/compress/recap.d.ts +2 -2
- package/dist/lib/compress/recap.d.ts.map +1 -1
- package/dist/lib/compress/search.d.ts +2 -2
- package/dist/lib/compress/search.d.ts.map +1 -1
- package/dist/lib/compress/status.d.ts +2 -2
- package/dist/lib/compress/status.d.ts.map +1 -1
- package/dist/lib/compress/timing.d.ts +0 -1
- package/dist/lib/compress/timing.d.ts.map +1 -1
- package/dist/lib/compress/types.d.ts +9 -0
- package/dist/lib/compress/types.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts +6 -5
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts +17 -1
- package/dist/lib/state/state.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -475,6 +475,22 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
|
|
|
475
475
|
|
|
476
476
|
## Changelog
|
|
477
477
|
|
|
478
|
+
### v1.13.7 — Per-Session State + Inactive Block Fixes + Preserve First User Message (PRs #184, #193, #196)
|
|
479
|
+
|
|
480
|
+
**Problem**: Three bugs since v1.13.6. (1) **Subagent state isolation failure** (PR #184): ACP stored a single global `SessionState` per plugin instance. When subagent (child) sessions ran interleaved with the parent, the child's state overwrote the parent's `modelContextLimit` — losing the 1M context window and falling back to the 6K adaptive floor, causing over-aggressive nudging in the parent. The `compressionTiming` tracker was also shared across sessions, risking cross-session collision. (2) **Inactive block opacity** (PR #193): `decompress` rejected inactive blocks with "not active — may have already been decompressed", and `acp_status` hid consumed/inactive blocks entirely. Users could not decompress blocks that were GC'd or consumed by secondary compression, and could not see them in status output. (3) **Zero-user session freeze** (PR #196): When compression pruned all user-role messages (all fell inside compressed ranges), zhipuai-lb rejected the request with HTTP 400 code 1214 (`"messages 参数非法"`, `isRetryable: false`), freezing the session. The v1.13.2 `preserve-last-user` fix searched the messages array for a pruned user to restore — but after OpenCode compaction removes pruned messages from the array, the search finds nothing and zero-user requests slip through.
|
|
481
|
+
|
|
482
|
+
**Fix**: (1) PR #184 — Introduced `SessionStateRegistry` in `lib/state/state.ts`: a `Map<sessionID, SessionState>` with a shared `compressionTiming` tracker and soft-cap eviction (32 sessions). Each session now resolves its own state via `registry.getOrCreate(sessionID)`, isolating subagent state from the parent. The system-prompt hook gracefully handles missing state (returns early). Also reverted the over-aggressive `baseline = 0` change back to `baseline = currentTokens` (system prompt is not growth). 851 tests pass. (2) PR #193 — Removed the "not active" rejection from `lib/compress/decompress.ts` and the `/acp decompress` slash command; standalone inactive blocks (user-decompressed, GC'd, orphaned) now decompress successfully. `acp_status` compressed scope now lists ALL blocks (active + inactive) with an `[inactive]` marker and "N active, M inactive/consumed" summary line. Fixed `toFile` fallback to use `targets[0].blocks[0].summary` instead of the undefined `activeBlocks[0].summary`. 859 tests pass after 3 rounds of dual-agent review. (3) PR #196 — Replaced `preserve-last-user` with `preserve-first-user` in `lib/messages/prune.ts`: the first user message (session's original task, always present in the array) is unconditionally force-preserved (`survive[firstUserIdx] = true`) regardless of prune state. Simpler and more reliable — does not depend on pruned messages remaining in the array after OpenCode compaction. Trade-off: may produce two adjacent user messages (first user + a later surviving user), which all major providers accept. Dual-agent reviewed (Oracle + General, both APPROVE). 846 tests pass.
|
|
483
|
+
|
|
484
|
+
Files: `lib/state/state.ts`, `lib/hooks.ts`, `lib/compress/types.ts`, `lib/compress/decompress.ts`, `lib/compress/status.ts`, `lib/commands/decompress.ts`, `lib/messages/prune.ts`, `lib/messages/inject/inject.ts`. Tests: `tests/registry.test.ts`, `tests/inactive-block-decompress.test.ts`, `tests/acp-status.test.ts`, `tests/decompress-logic.test.ts`, `tests/prune.test.ts`, `tests/e2e-message-transform.test.ts`.
|
|
485
|
+
|
|
486
|
+
### v1.13.7-dev.1 — Dev Prerelease Sync (master @ v1.13.6)
|
|
487
|
+
|
|
488
|
+
**Purpose**: Sync the `dev` npm tag (stuck at `1.12.10-dev.1`) with current master. The `dev` tag had fallen far behind `latest` (1.13.6), making it impossible for early adopters to test the latest fixes via `opencode-acp@dev`.
|
|
489
|
+
|
|
490
|
+
**Content**: Identical to v1.13.6 stable (master HEAD `5d67b84`). No new code changes. This is a dev-tag-only release to bring `opencode-acp@dev` up to parity with `opencode-acp@latest`.
|
|
491
|
+
|
|
492
|
+
Files: `package.json`, `README.md`, `README.zh-CN.md`. 846 tests pass (no source changes).
|
|
493
|
+
|
|
478
494
|
### v1.13.6 — Force-Protect Compress Tool Regardless of User Config (PR #188)
|
|
479
495
|
|
|
480
496
|
**Problem**: `compress.protectedTools` uses a replace merge policy (PR #177): a user setting `protectedTools: ["skill"]` or `protectedTools: []` silently removed `"compress"` from the protected list. This made compress summaries — the sole record of compressed conversation — vulnerable to being pruned by subsequent sequential compressions, causing irreversible data loss.
|
package/README.zh-CN.md
CHANGED
|
@@ -443,6 +443,22 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将
|
|
|
443
443
|
|
|
444
444
|
## 更新日志
|
|
445
445
|
|
|
446
|
+
### v1.13.7 — 每会话状态隔离 + 失活块修复 + 保留首条用户消息(PR #184、#193、#196)
|
|
447
|
+
|
|
448
|
+
**问题**:v1.13.6 之后的三个 bug。(1)**子代理状态隔离失败**(PR #184):ACP 为每个插件实例存储单个全局 `SessionState`。当子代理(child)会话与父会话交替运行时,子会话的状态覆盖了父会话的 `modelContextLimit` —— 丢失 1M 上下文窗口,回退到 6K 自适应下限,导致父会话中过度触发压缩提醒。`compressionTiming` 追踪器也跨会话共享,存在跨会话碰撞风险。(2)**失活块不可见**(PR #193):`decompress` 拒绝失活块("not active — may have already been decompressed"),`acp_status` 完全隐藏被消费/失活的块。用户无法解压被 GC 回收或被二次压缩消费的块,也无法在状态输出中看到它们。(3)**零用户消息会话冻结**(PR #196):当压缩剪枝了所有 user 角色消息(全部落在压缩范围内)时,zhipuai-lb 拒绝请求(HTTP 400,code 1214,`"messages 参数非法"`,`isRetryable: false`),冻结会话。v1.13.2 的 `preserve-last-user` 修复在消息数组中搜索被剪枝的用户消息来恢复 —— 但 OpenCode 压缩移除被剪枝的消息后,搜索找不到任何内容,零用户请求仍然漏过。
|
|
449
|
+
|
|
450
|
+
**修复**:(1)PR #184 —— 在 `lib/state/state.ts` 引入 `SessionStateRegistry`:一个 `Map<sessionID, SessionState>`,带共享 `compressionTiming` 追踪器和软上限驱逐(32 个会话)。每个会话通过 `registry.getOrCreate(sessionID)` 解析自己的状态,将子代理状态与父会话隔离。系统提示钩子优雅处理缺失状态(提前返回)。同时将过于激进的 `baseline = 0` 改回 `baseline = currentTokens`(系统提示不算增长)。851 项测试通过。(2)PR #193 —— 移除 `lib/compress/decompress.ts` 和 `/acp decompress` 斜杠命令中的 "not active" 拒绝;独立的失活块(用户解压、GC 回收、孤立)现在可以成功解压。`acp_status` 压缩作用域现在列出所有块(活跃 + 失活),带 `[inactive]` 标记和"N active, M inactive/consumed"汇总行。修复 `toFile` 回退使用 `targets[0].blocks[0].summary` 而非未定义的 `activeBlocks[0].summary`。经 3 轮双 Agent 审查后 859 项测试通过。(3)PR #196 —— 在 `lib/messages/prune.ts` 用 `preserve-first-user` 替换 `preserve-last-user`:首条用户消息(会话的原始任务,始终存在于数组中)被无条件强制保留(`survive[firstUserIdx] = true`),无论剪枝状态如何。更简单、更可靠 —— 不依赖于被剪枝消息在 OpenCode 压缩后仍留在数组中。权衡:可能产生两个相邻的用户消息(首条用户 + 后续存活的用户),所有主流 provider 均可接受。经双 Agent 审查(Oracle + General,均 APPROVE)。846 项测试通过。
|
|
451
|
+
|
|
452
|
+
文件:`lib/state/state.ts`、`lib/hooks.ts`、`lib/compress/types.ts`、`lib/compress/decompress.ts`、`lib/compress/status.ts`、`lib/commands/decompress.ts`、`lib/messages/prune.ts`、`lib/messages/inject/inject.ts`。测试:`tests/registry.test.ts`、`tests/inactive-block-decompress.test.ts`、`tests/acp-status.test.ts`、`tests/decompress-logic.test.ts`、`tests/prune.test.ts`、`tests/e2e-message-transform.test.ts`。
|
|
453
|
+
|
|
454
|
+
### v1.13.7-dev.1 — Dev 预发布同步(master @ v1.13.6)
|
|
455
|
+
|
|
456
|
+
**目的**:将 npm `dev` 标签(卡在 `1.12.10-dev.1`)同步到当前 master。`dev` 标签已远远落后于 `latest`(1.13.6),导致早期采用者无法通过 `opencode-acp@dev` 测试最新修复。
|
|
457
|
+
|
|
458
|
+
**内容**:与 v1.13.6 稳定版完全相同(master HEAD `5d67b84`)。无新代码变更。仅为 dev 标签发布,使 `opencode-acp@dev` 与 `opencode-acp@latest` 保持一致。
|
|
459
|
+
|
|
460
|
+
文件:`package.json`、`README.md`、`README.zh-CN.md`。846 项测试通过(无源码变更)。
|
|
461
|
+
|
|
446
462
|
### v1.13.6 — 强制保护 compress 工具,无视用户配置(PR #188)
|
|
447
463
|
|
|
448
464
|
**问题**:`compress.protectedTools` 使用替换式合并策略(PR #177):用户设置 `protectedTools: ["skill"]` 或 `protectedTools: []` 会静默地从保护列表中移除 `"compress"`。这使得 compress 摘要 — 压缩对话的唯一记录 — 容易被后续的顺序压缩裁剪,导致不可恢复的数据丢失。
|
package/dist/index.js
CHANGED
|
@@ -1948,6 +1948,23 @@ Using previous/default values`
|
|
|
1948
1948
|
return config;
|
|
1949
1949
|
}
|
|
1950
1950
|
|
|
1951
|
+
// lib/compress/types.ts
|
|
1952
|
+
function resolveToolContext(factoryCtx, sessionID) {
|
|
1953
|
+
const state = factoryCtx.registry.get(sessionID);
|
|
1954
|
+
if (!state) {
|
|
1955
|
+
throw new Error(
|
|
1956
|
+
`ACP: session ${sessionID} has no initialized state. messages.transform must run before a compress tool call.`
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
return {
|
|
1960
|
+
client: factoryCtx.client,
|
|
1961
|
+
state,
|
|
1962
|
+
logger: factoryCtx.logger,
|
|
1963
|
+
config: factoryCtx.config,
|
|
1964
|
+
prompts: factoryCtx.prompts
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1951
1968
|
// lib/compress/message.ts
|
|
1952
1969
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
1953
1970
|
|
|
@@ -2648,8 +2665,8 @@ function buildSearchPreview(text, firstTerm) {
|
|
|
2648
2665
|
}
|
|
2649
2666
|
return text.substring(0, 200) + (text.length > 200 ? "..." : "");
|
|
2650
2667
|
}
|
|
2651
|
-
function createSearchContextTool(
|
|
2652
|
-
|
|
2668
|
+
function createSearchContextTool(factoryCtx) {
|
|
2669
|
+
factoryCtx.prompts.reload();
|
|
2653
2670
|
return tool({
|
|
2654
2671
|
description: SEARCH_CONTEXT_TOOL_DESCRIPTION,
|
|
2655
2672
|
args: {
|
|
@@ -2657,7 +2674,8 @@ function createSearchContextTool(ctx) {
|
|
|
2657
2674
|
limit: tool.schema.number().optional().describe("Maximum results to return (default: 10)"),
|
|
2658
2675
|
deep: tool.schema.boolean().optional().describe("If true, also search visible (uncompressed) messages. Slower but more thorough (default: false)")
|
|
2659
2676
|
},
|
|
2660
|
-
async execute(args) {
|
|
2677
|
+
async execute(args, toolCtx) {
|
|
2678
|
+
const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
|
|
2661
2679
|
const query = (args.query || "").toLowerCase().trim();
|
|
2662
2680
|
const limit = args.limit ?? 10;
|
|
2663
2681
|
if (!query) {
|
|
@@ -3976,12 +3994,6 @@ async function loadAllSessionStats(logger) {
|
|
|
3976
3994
|
function buildCompressionTimingKey(messageId, callId) {
|
|
3977
3995
|
return `${messageId}:${callId}`;
|
|
3978
3996
|
}
|
|
3979
|
-
function consumeCompressionStart(state, messageId, callId) {
|
|
3980
|
-
const key = buildCompressionTimingKey(messageId, callId);
|
|
3981
|
-
const start = state.compressionTiming.startsByCallId.get(key);
|
|
3982
|
-
state.compressionTiming.startsByCallId.delete(key);
|
|
3983
|
-
return start;
|
|
3984
|
-
}
|
|
3985
3997
|
function resolveCompressionDuration(startedAt, eventTime, partTime) {
|
|
3986
3998
|
const runningAt = typeof partTime?.start === "number" && Number.isFinite(partTime.start) ? partTime.start : eventTime;
|
|
3987
3999
|
const pendingToRunningMs = typeof startedAt === "number" && typeof runningAt === "number" ? Math.max(0, runningAt - startedAt) : void 0;
|
|
@@ -4368,28 +4380,7 @@ function rebuildCompressionState(state, messages, config, logger) {
|
|
|
4368
4380
|
}
|
|
4369
4381
|
|
|
4370
4382
|
// lib/state/state.ts
|
|
4371
|
-
|
|
4372
|
-
const lastUserMessage = getLastUserMessage(messages);
|
|
4373
|
-
if (!lastUserMessage) {
|
|
4374
|
-
return;
|
|
4375
|
-
}
|
|
4376
|
-
const lastSessionId = lastUserMessage.info.sessionID;
|
|
4377
|
-
if (state.sessionId === null || state.sessionId !== lastSessionId) {
|
|
4378
|
-
logger.info(`Session changed: ${state.sessionId} -> ${lastSessionId}`);
|
|
4379
|
-
try {
|
|
4380
|
-
await ensureSessionInitialized(
|
|
4381
|
-
client,
|
|
4382
|
-
state,
|
|
4383
|
-
lastSessionId,
|
|
4384
|
-
logger,
|
|
4385
|
-
messages,
|
|
4386
|
-
manualModeDefault,
|
|
4387
|
-
config
|
|
4388
|
-
);
|
|
4389
|
-
} catch (err) {
|
|
4390
|
-
logger.error("Failed to initialize session state", { error: err.message });
|
|
4391
|
-
}
|
|
4392
|
-
}
|
|
4383
|
+
async function updatePerTurnState(state, logger, messages) {
|
|
4393
4384
|
const lastCompactionTimestamp = findLastCompactionTimestamp(messages);
|
|
4394
4385
|
if (lastCompactionTimestamp > state.lastCompaction) {
|
|
4395
4386
|
state.lastCompaction = lastCompactionTimestamp;
|
|
@@ -4404,6 +4395,66 @@ var checkSession = async (client, state, logger, messages, manualModeDefault, co
|
|
|
4404
4395
|
});
|
|
4405
4396
|
}
|
|
4406
4397
|
state.currentTurn = countTurns(state, messages);
|
|
4398
|
+
}
|
|
4399
|
+
var REGISTRY_SOFT_CAP = 32;
|
|
4400
|
+
var SessionStateRegistry = class {
|
|
4401
|
+
constructor(logger) {
|
|
4402
|
+
this.logger = logger;
|
|
4403
|
+
}
|
|
4404
|
+
logger;
|
|
4405
|
+
states = /* @__PURE__ */ new Map();
|
|
4406
|
+
compressionTiming = {
|
|
4407
|
+
startsByCallId: /* @__PURE__ */ new Map(),
|
|
4408
|
+
pendingByCallId: /* @__PURE__ */ new Map()
|
|
4409
|
+
};
|
|
4410
|
+
get(sessionId) {
|
|
4411
|
+
return this.states.get(sessionId);
|
|
4412
|
+
}
|
|
4413
|
+
all() {
|
|
4414
|
+
return Array.from(this.states.values());
|
|
4415
|
+
}
|
|
4416
|
+
get size() {
|
|
4417
|
+
return this.states.size;
|
|
4418
|
+
}
|
|
4419
|
+
// Idempotent: ensureSessionInitialized returns immediately once
|
|
4420
|
+
// state.sessionId === sessionId (assigned synchronously before any await),
|
|
4421
|
+
// so repeat calls for the same session never re-reset.
|
|
4422
|
+
async getOrCreate(client, sessionId, messages, manualModeDefault, config) {
|
|
4423
|
+
let state = this.states.get(sessionId);
|
|
4424
|
+
if (!state) {
|
|
4425
|
+
state = createSessionState();
|
|
4426
|
+
state.compressionTiming = this.compressionTiming;
|
|
4427
|
+
this.states.set(sessionId, state);
|
|
4428
|
+
this.enforceSoftCap();
|
|
4429
|
+
}
|
|
4430
|
+
try {
|
|
4431
|
+
await ensureSessionInitialized(
|
|
4432
|
+
client,
|
|
4433
|
+
state,
|
|
4434
|
+
sessionId,
|
|
4435
|
+
this.logger,
|
|
4436
|
+
messages,
|
|
4437
|
+
manualModeDefault,
|
|
4438
|
+
config
|
|
4439
|
+
);
|
|
4440
|
+
} catch (err) {
|
|
4441
|
+
this.logger.error("Failed to initialize session state", {
|
|
4442
|
+
error: err.message
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4445
|
+
return state;
|
|
4446
|
+
}
|
|
4447
|
+
enforceSoftCap() {
|
|
4448
|
+
if (this.states.size <= REGISTRY_SOFT_CAP) return;
|
|
4449
|
+
const oldest = this.states.keys().next().value;
|
|
4450
|
+
if (oldest !== void 0) {
|
|
4451
|
+
this.states.delete(oldest);
|
|
4452
|
+
this.logger.info("SessionStateRegistry evicted session (soft cap)", {
|
|
4453
|
+
sessionId: oldest,
|
|
4454
|
+
remaining: this.states.size
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4407
4458
|
};
|
|
4408
4459
|
function createSessionState() {
|
|
4409
4460
|
return {
|
|
@@ -6407,13 +6458,14 @@ function buildSchema(maxSummaryLengthHard) {
|
|
|
6407
6458
|
acknowledgeRisk: tool2.schema.boolean().optional()
|
|
6408
6459
|
};
|
|
6409
6460
|
}
|
|
6410
|
-
function createCompressMessageTool(
|
|
6411
|
-
|
|
6412
|
-
const runtimePrompts =
|
|
6461
|
+
function createCompressMessageTool(factoryCtx) {
|
|
6462
|
+
factoryCtx.prompts.reload();
|
|
6463
|
+
const runtimePrompts = factoryCtx.prompts.getRuntimePrompts();
|
|
6413
6464
|
return tool2({
|
|
6414
6465
|
description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
|
|
6415
|
-
args: buildSchema(
|
|
6466
|
+
args: buildSchema(factoryCtx.config.compress.maxSummaryLengthHard),
|
|
6416
6467
|
async execute(args, toolCtx) {
|
|
6468
|
+
const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
|
|
6417
6469
|
const input = args;
|
|
6418
6470
|
validateArgs(input);
|
|
6419
6471
|
const maxLen = args.summaryMaxChars ?? ctx.config.compress.maxSummaryLengthHard;
|
|
@@ -6620,13 +6672,14 @@ function buildSchema2(maxSummaryLengthHard) {
|
|
|
6620
6672
|
acknowledgeRisk: tool3.schema.boolean().optional()
|
|
6621
6673
|
};
|
|
6622
6674
|
}
|
|
6623
|
-
function createCompressRangeTool(
|
|
6624
|
-
|
|
6625
|
-
const runtimePrompts =
|
|
6675
|
+
function createCompressRangeTool(factoryCtx) {
|
|
6676
|
+
factoryCtx.prompts.reload();
|
|
6677
|
+
const runtimePrompts = factoryCtx.prompts.getRuntimePrompts();
|
|
6626
6678
|
return tool3({
|
|
6627
6679
|
description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
|
|
6628
|
-
args: buildSchema2(
|
|
6680
|
+
args: buildSchema2(factoryCtx.config.compress.maxSummaryLengthHard),
|
|
6629
6681
|
async execute(args, toolCtx) {
|
|
6682
|
+
const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
|
|
6630
6683
|
const input = args;
|
|
6631
6684
|
validateArgs2(input);
|
|
6632
6685
|
const maxLen = args.summaryMaxChars ?? ctx.config.compress.maxSummaryLengthHard;
|
|
@@ -6922,16 +6975,9 @@ var filterCompressedRanges = (state, messages) => {
|
|
|
6922
6975
|
}
|
|
6923
6976
|
return false;
|
|
6924
6977
|
});
|
|
6925
|
-
const
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
if (!anyUserSurvives) {
|
|
6929
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
6930
|
-
if (messages[i].info.role === "user" && !survive[i]) {
|
|
6931
|
-
survive[i] = true;
|
|
6932
|
-
break;
|
|
6933
|
-
}
|
|
6934
|
-
}
|
|
6978
|
+
const firstUserIdx = messages.findIndex((msg) => msg.info.role === "user");
|
|
6979
|
+
if (firstUserIdx >= 0) {
|
|
6980
|
+
survive[firstUserIdx] = true;
|
|
6935
6981
|
}
|
|
6936
6982
|
const result = [];
|
|
6937
6983
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -9080,11 +9126,12 @@ function extractMessageText2(m) {
|
|
|
9080
9126
|
return `[${role}]
|
|
9081
9127
|
${content}`;
|
|
9082
9128
|
}
|
|
9083
|
-
function createDecompressTool(
|
|
9129
|
+
function createDecompressTool(factoryCtx) {
|
|
9084
9130
|
return tool4({
|
|
9085
9131
|
description: TOOL_DESCRIPTION,
|
|
9086
9132
|
args: buildSchema3(),
|
|
9087
9133
|
async execute(args, toolCtx) {
|
|
9134
|
+
const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
|
|
9088
9135
|
const { rawMessages } = await prepareDecompressSession(ctx, toolCtx);
|
|
9089
9136
|
const contextUsageBefore = ctx.state.modelContextLimit ? Math.round(
|
|
9090
9137
|
getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
|
|
@@ -9437,8 +9484,8 @@ function renderCompressedDrilldown(blocks, sort, limit) {
|
|
|
9437
9484
|
);
|
|
9438
9485
|
return lines;
|
|
9439
9486
|
}
|
|
9440
|
-
function createAcpStatusTool(
|
|
9441
|
-
|
|
9487
|
+
function createAcpStatusTool(factoryCtx) {
|
|
9488
|
+
factoryCtx.prompts.reload();
|
|
9442
9489
|
return tool5({
|
|
9443
9490
|
description: ACP_STATUS_TOOL_DESCRIPTION,
|
|
9444
9491
|
args: {
|
|
@@ -9453,6 +9500,7 @@ function createAcpStatusTool(ctx) {
|
|
|
9453
9500
|
limit: tool5.schema.number().optional().describe("Max items to list (default 30)")
|
|
9454
9501
|
},
|
|
9455
9502
|
async execute(args, toolCtx) {
|
|
9503
|
+
const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
|
|
9456
9504
|
const scope = args.scope === "compressed" || args.scope === "uncompressed" ? args.scope : void 0;
|
|
9457
9505
|
const view = args.view === "messages" ? "messages" : "ranges";
|
|
9458
9506
|
const toolFilter = typeof args.tool === "string" ? args.tool : void 0;
|
|
@@ -9514,13 +9562,14 @@ Call this tool to re-fetch a specific block's summary without decompressing the
|
|
|
9514
9562
|
|
|
9515
9563
|
Args:
|
|
9516
9564
|
- blockId: optional block number (e.g., 5). If omitted, lists all active blocks with brief info.`;
|
|
9517
|
-
function createAcpContextRecapTool(
|
|
9565
|
+
function createAcpContextRecapTool(factoryCtx) {
|
|
9518
9566
|
return tool6({
|
|
9519
9567
|
description: RECAP_TOOL_DESCRIPTION,
|
|
9520
9568
|
args: {
|
|
9521
9569
|
blockId: tool6.schema.number().optional().describe("Block number to retrieve (e.g., 5). If omitted, lists all active blocks.")
|
|
9522
9570
|
},
|
|
9523
|
-
async execute(args) {
|
|
9571
|
+
async execute(args, toolCtx) {
|
|
9572
|
+
const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
|
|
9524
9573
|
const msgState = ctx.state.prune.messages;
|
|
9525
9574
|
const activeIds = Array.from(msgState.activeBlockIds).sort((a, b) => a - b);
|
|
9526
9575
|
if (activeIds.length === 0) {
|
|
@@ -9567,8 +9616,8 @@ Unlike compress (which creates summaries), prune directly strips tool call outpu
|
|
|
9567
9616
|
Args:
|
|
9568
9617
|
- toolType: tool name to prune (e.g., "todowrite", "bash", "edit")
|
|
9569
9618
|
- keepLatest: how many recent calls to keep visible (default 3)`;
|
|
9570
|
-
function createPruneTool(
|
|
9571
|
-
|
|
9619
|
+
function createPruneTool(factoryCtx) {
|
|
9620
|
+
factoryCtx.prompts.reload();
|
|
9572
9621
|
return tool7({
|
|
9573
9622
|
description: PRUNE_TOOL_DESCRIPTION,
|
|
9574
9623
|
args: {
|
|
@@ -9576,6 +9625,7 @@ function createPruneTool(ctx) {
|
|
|
9576
9625
|
keepLatest: tool7.schema.number().optional().describe("How many recent calls to keep visible (default 3)")
|
|
9577
9626
|
},
|
|
9578
9627
|
async execute(args, toolCtx) {
|
|
9628
|
+
const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
|
|
9579
9629
|
const keepLatest = args.keepLatest ?? 3;
|
|
9580
9630
|
const { rawMessages } = await prepareSession(
|
|
9581
9631
|
ctx,
|
|
@@ -11617,12 +11667,13 @@ function isInternalAgentRequest(messages) {
|
|
|
11617
11667
|
const agent = lastUserMessage.info.agent;
|
|
11618
11668
|
return typeof agent === "string" && INTERNAL_AGENT_NAMES.has(agent);
|
|
11619
11669
|
}
|
|
11620
|
-
function createSystemPromptHandler(
|
|
11670
|
+
function createSystemPromptHandler(registry3, logger, config, prompts) {
|
|
11621
11671
|
return async (input, output) => {
|
|
11622
|
-
|
|
11672
|
+
const state = input.sessionID ? registry3.get(input.sessionID) : void 0;
|
|
11673
|
+
if (state && input.model?.limit?.context) {
|
|
11623
11674
|
state.modelContextLimit = input.model.limit.context;
|
|
11624
11675
|
}
|
|
11625
|
-
if (state.isSubAgent && !config.experimental.allowSubAgents) {
|
|
11676
|
+
if (!state || state.isSubAgent && !config.experimental.allowSubAgents) {
|
|
11626
11677
|
return;
|
|
11627
11678
|
}
|
|
11628
11679
|
const systemText = output.system.join("\n");
|
|
@@ -11630,7 +11681,7 @@ function createSystemPromptHandler(state, logger, config, prompts) {
|
|
|
11630
11681
|
logger.info("Skipping DCP system prompt injection for internal agent");
|
|
11631
11682
|
return;
|
|
11632
11683
|
}
|
|
11633
|
-
const effectivePermission =
|
|
11684
|
+
const effectivePermission = compressPermission(state, config);
|
|
11634
11685
|
if (effectivePermission === "deny") {
|
|
11635
11686
|
return;
|
|
11636
11687
|
}
|
|
@@ -11674,7 +11725,7 @@ function runMajorGC(state, config, logger, messages) {
|
|
|
11674
11725
|
});
|
|
11675
11726
|
}
|
|
11676
11727
|
}
|
|
11677
|
-
function createChatMessageTransformHandler(client,
|
|
11728
|
+
function createChatMessageTransformHandler(client, registry3, logger, config, prompts, hostPermissions) {
|
|
11678
11729
|
return async (input, output) => {
|
|
11679
11730
|
const receivedMessages = Array.isArray(output.messages) ? output.messages.length : 0;
|
|
11680
11731
|
const messages = filterMessagesInPlace(output.messages);
|
|
@@ -11688,7 +11739,20 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
11688
11739
|
logger.debug("Skipping message transform for internal agent request");
|
|
11689
11740
|
return;
|
|
11690
11741
|
}
|
|
11691
|
-
|
|
11742
|
+
const lastUserMessage = getLastUserMessage(messages);
|
|
11743
|
+
let state;
|
|
11744
|
+
if (!lastUserMessage) {
|
|
11745
|
+
state = createSessionState();
|
|
11746
|
+
} else {
|
|
11747
|
+
state = await registry3.getOrCreate(
|
|
11748
|
+
client,
|
|
11749
|
+
lastUserMessage.info.sessionID,
|
|
11750
|
+
messages,
|
|
11751
|
+
config.manualMode.enabled,
|
|
11752
|
+
config
|
|
11753
|
+
);
|
|
11754
|
+
await updatePerTurnState(state, logger, messages);
|
|
11755
|
+
}
|
|
11692
11756
|
syncCompressPermissionState(state, config, hostPermissions, output.messages);
|
|
11693
11757
|
if (state.isSubAgent && !config.experimental.allowSubAgents) {
|
|
11694
11758
|
return;
|
|
@@ -11753,7 +11817,7 @@ ${text}`);
|
|
|
11753
11817
|
}
|
|
11754
11818
|
};
|
|
11755
11819
|
}
|
|
11756
|
-
function createCommandExecuteHandler(client,
|
|
11820
|
+
function createCommandExecuteHandler(client, registry3, logger, config, workingDirectory, hostPermissions) {
|
|
11757
11821
|
return async (input, output) => {
|
|
11758
11822
|
if (!config.commands.enabled) {
|
|
11759
11823
|
return;
|
|
@@ -11763,11 +11827,9 @@ function createCommandExecuteHandler(client, state, logger, config, workingDirec
|
|
|
11763
11827
|
path: { id: input.sessionID }
|
|
11764
11828
|
});
|
|
11765
11829
|
const messages = filterMessages(messagesResponse.data || messagesResponse);
|
|
11766
|
-
await
|
|
11830
|
+
const state = await registry3.getOrCreate(
|
|
11767
11831
|
client,
|
|
11768
|
-
state,
|
|
11769
11832
|
input.sessionID,
|
|
11770
|
-
logger,
|
|
11771
11833
|
messages,
|
|
11772
11834
|
config.manualMode.enabled,
|
|
11773
11835
|
config
|
|
@@ -11851,7 +11913,7 @@ function createTextCompleteHandler() {
|
|
|
11851
11913
|
output.text = stripHallucinationsFromString(output.text);
|
|
11852
11914
|
};
|
|
11853
11915
|
}
|
|
11854
|
-
function createEventHandler(
|
|
11916
|
+
function createEventHandler(registry3, logger) {
|
|
11855
11917
|
return async (input) => {
|
|
11856
11918
|
const eventTime = typeof input.event?.time === "number" && Number.isFinite(input.event.time) ? input.event.time : typeof input.event?.properties?.time === "number" && Number.isFinite(input.event.properties.time) ? input.event.properties.time : void 0;
|
|
11857
11919
|
if (input.event.type !== "message.part.updated") {
|
|
@@ -11861,16 +11923,17 @@ function createEventHandler(state, logger) {
|
|
|
11861
11923
|
if (part?.type !== "tool" || part.tool !== "compress") {
|
|
11862
11924
|
return;
|
|
11863
11925
|
}
|
|
11926
|
+
const timing = registry3.compressionTiming;
|
|
11864
11927
|
if (part.state.status === "pending") {
|
|
11865
11928
|
if (typeof part.callID !== "string" || typeof part.messageID !== "string") {
|
|
11866
11929
|
return;
|
|
11867
11930
|
}
|
|
11868
11931
|
const startedAt = eventTime ?? Date.now();
|
|
11869
11932
|
const key = buildCompressionTimingKey(part.messageID, part.callID);
|
|
11870
|
-
if (
|
|
11933
|
+
if (timing.startsByCallId.has(key)) {
|
|
11871
11934
|
return;
|
|
11872
11935
|
}
|
|
11873
|
-
|
|
11936
|
+
timing.startsByCallId.set(key, startedAt);
|
|
11874
11937
|
logger.debug("Recorded compression start", {
|
|
11875
11938
|
messageID: part.messageID,
|
|
11876
11939
|
callID: part.callID,
|
|
@@ -11883,34 +11946,36 @@ function createEventHandler(state, logger) {
|
|
|
11883
11946
|
return;
|
|
11884
11947
|
}
|
|
11885
11948
|
const key = buildCompressionTimingKey(part.messageID, part.callID);
|
|
11886
|
-
const start =
|
|
11949
|
+
const start = timing.startsByCallId.get(key);
|
|
11950
|
+
timing.startsByCallId.delete(key);
|
|
11887
11951
|
const durationMs = resolveCompressionDuration(start, eventTime, part.state.time);
|
|
11888
11952
|
if (typeof durationMs !== "number") {
|
|
11889
11953
|
return;
|
|
11890
11954
|
}
|
|
11891
|
-
|
|
11955
|
+
timing.pendingByCallId.set(key, {
|
|
11892
11956
|
messageId: part.messageID,
|
|
11893
11957
|
callId: part.callID,
|
|
11894
11958
|
durationMs
|
|
11895
11959
|
});
|
|
11896
|
-
const
|
|
11897
|
-
|
|
11898
|
-
|
|
11960
|
+
for (const state of registry3.all()) {
|
|
11961
|
+
const updates = applyPendingCompressionDurations(state);
|
|
11962
|
+
if (updates > 0) {
|
|
11963
|
+
await saveSessionState(state, logger);
|
|
11964
|
+
logger.info("Attached compression time to blocks", {
|
|
11965
|
+
messageID: part.messageID,
|
|
11966
|
+
callID: part.callID,
|
|
11967
|
+
blocks: updates,
|
|
11968
|
+
durationMs
|
|
11969
|
+
});
|
|
11970
|
+
}
|
|
11899
11971
|
}
|
|
11900
|
-
await saveSessionState(state, logger);
|
|
11901
|
-
logger.info("Attached compression time to blocks", {
|
|
11902
|
-
messageID: part.messageID,
|
|
11903
|
-
callID: part.callID,
|
|
11904
|
-
blocks: updates,
|
|
11905
|
-
durationMs
|
|
11906
|
-
});
|
|
11907
11972
|
return;
|
|
11908
11973
|
}
|
|
11909
11974
|
if (part.state.status === "running") {
|
|
11910
11975
|
return;
|
|
11911
11976
|
}
|
|
11912
11977
|
if (typeof part.callID === "string" && typeof part.messageID === "string") {
|
|
11913
|
-
|
|
11978
|
+
timing.startsByCallId.delete(
|
|
11914
11979
|
buildCompressionTimingKey(part.messageID, part.callID)
|
|
11915
11980
|
);
|
|
11916
11981
|
}
|
|
@@ -12097,7 +12162,7 @@ var server = (async (ctx) => {
|
|
|
12097
12162
|
return {};
|
|
12098
12163
|
}
|
|
12099
12164
|
const logger = new Logger(config.debug);
|
|
12100
|
-
const
|
|
12165
|
+
const registry3 = new SessionStateRegistry(logger);
|
|
12101
12166
|
const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts);
|
|
12102
12167
|
const hostPermissions = {
|
|
12103
12168
|
global: void 0,
|
|
@@ -12112,21 +12177,21 @@ var server = (async (ctx) => {
|
|
|
12112
12177
|
startAutoUpdate(ctx, config.autoUpdate);
|
|
12113
12178
|
const compressToolContext = {
|
|
12114
12179
|
client: ctx.client,
|
|
12115
|
-
|
|
12180
|
+
registry: registry3,
|
|
12116
12181
|
logger,
|
|
12117
12182
|
config,
|
|
12118
12183
|
prompts
|
|
12119
12184
|
};
|
|
12120
12185
|
return {
|
|
12121
12186
|
"experimental.chat.system.transform": createSystemPromptHandler(
|
|
12122
|
-
|
|
12187
|
+
registry3,
|
|
12123
12188
|
logger,
|
|
12124
12189
|
config,
|
|
12125
12190
|
prompts
|
|
12126
12191
|
),
|
|
12127
12192
|
"experimental.chat.messages.transform": createChatMessageTransformHandler(
|
|
12128
12193
|
ctx.client,
|
|
12129
|
-
|
|
12194
|
+
registry3,
|
|
12130
12195
|
logger,
|
|
12131
12196
|
config,
|
|
12132
12197
|
prompts,
|
|
@@ -12135,13 +12200,13 @@ var server = (async (ctx) => {
|
|
|
12135
12200
|
"experimental.text.complete": createTextCompleteHandler(),
|
|
12136
12201
|
"command.execute.before": createCommandExecuteHandler(
|
|
12137
12202
|
ctx.client,
|
|
12138
|
-
|
|
12203
|
+
registry3,
|
|
12139
12204
|
logger,
|
|
12140
12205
|
config,
|
|
12141
12206
|
ctx.directory,
|
|
12142
12207
|
hostPermissions
|
|
12143
12208
|
),
|
|
12144
|
-
event: createEventHandler(
|
|
12209
|
+
event: createEventHandler(registry3, logger),
|
|
12145
12210
|
tool: {
|
|
12146
12211
|
...config.compress.permission !== "deny" && {
|
|
12147
12212
|
compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext),
|