From d99ad2b1d05da476e93b5eaa4a58e288d4d8b179 Mon Sep 17 00:00:00 2001
From: imccyu <276526105+imccyu@users.noreply.github.com>
Date: Sun, 9 Aug 2026 20:36:57 +0800
Subject: [PATCH] fix conversation node review follow-ups
---
docs/subsystems/compaction.i18n.yaml | 4 ++--
docs/subsystems/compaction.md | 7 +++---
docs/subsystems/compaction.zh.md | 7 +++---
.../fixtures/workspace-context-compaction.ts | 4 ++--
packages/client/runtime/package.json | 1 +
.../src/client/sessions/steering-history.ts | 4 +---
.../src/client/conversation-nodes/command.ts | 4 ++--
packages/compact/compact/README.i18n.yaml | 4 ++--
packages/compact/compact/README.md | 10 ++++----
packages/compact/compact/README.zh.md | 10 ++++----
packages/compact/compact/src/checkpoint.ts | 23 +++++++++----------
packages/compact/compact/src/index.ts | 13 ++++++-----
.../tests/session-reference.spec.ts | 15 ++++++++----
packages/llm/llm-retry/README.i18n.yaml | 4 ++--
packages/llm/llm-retry/README.md | 4 ++--
packages/llm/llm-retry/README.zh.md | 4 ++--
.../tool-cordis/src/api-catalog.ts | 2 +-
pnpm-lock.yaml | 3 +++
18 files changed, 67 insertions(+), 56 deletions(-)
diff --git a/docs/subsystems/compaction.i18n.yaml b/docs/subsystems/compaction.i18n.yaml
index 532750f069..5e7c6d92be 100644
--- a/docs/subsystems/compaction.i18n.yaml
+++ b/docs/subsystems/compaction.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/compaction.md
-compaction.md: 11ee6eae797db1c57bf2ffbb90f1f37f47b8400c
-compaction.zh.md: a560e47dbb7aa92f0757179534803bbcaaebc6ba
+compaction.md: 8325694d92aa1a3019aef8f6b0c1f99d45ad0df5
+compaction.zh.md: 9f9014b8c1c1db12abc6581fbe6ebacfac3dd6fe
diff --git a/docs/subsystems/compaction.md b/docs/subsystems/compaction.md
index 11ee6eae79..8325694d92 100644
--- a/docs/subsystems/compaction.md
+++ b/docs/subsystems/compaction.md
@@ -66,7 +66,7 @@ Automatic callers state why policy is running; implementations may treat confirm
type CompactionTrigger = 'pressure' | 'context-overflow'
```
-`CompactService` exposes `compactIfNeeded(agent, trigger, signal)` for automatic `pressure` or `context-overflow` policy, `compactNow(agent, signal)` for one useful idle-session reduction even below pressure, and `compactRegion(...)` for an explicit inclusive surface range. `compactNow()` runs as agent maintenance between turns, returns `null` without writing when no useful range exists, records a standalone `turn: null` bracket before summarization, and flushes a closed attempt before later queued prompts may derive from the new surface. Every backend marks its replacement `user/message` with `COMPACT_CHECKPOINT_SOURCE`; client and wire consumers import that value and `isCompactCheckpointSource()` from the cordis-free `@deepseek-ai/dsh-compact/checkpoint` subpath, while the package root re-exports both for host consumers. The predicate keeps checkpoint recognition independent of any one backend. Implementations must forward the supplied signal to summarization. The seam owns no pricing API: the singleton [`ctx.tokenMeter`](token-meter.md) directly owns estimation and replay, while `dsh-compact-basic` owns retention, event sequencing, routed summarization calls, and their configuration.
+`CompactService` exposes `compactIfNeeded(agent, trigger, signal)` for automatic `pressure` or `context-overflow` policy, `compactNow(agent, signal)` for one useful idle-session reduction even below pressure, and `compactRegion(...)` for an explicit inclusive surface range. `compactNow()` runs as agent maintenance between turns, returns `null` without writing when no useful range exists, records a standalone `turn: null` bracket before summarization, and flushes a closed attempt before later queued prompts may derive from the new surface. Every backend creates its replacement `user/message` source with `compactCheckpointSource(compactionId, sourceCommandId?)`; client and wire consumers import that constructor, `CompactCheckpointSource`, and `isCompactCheckpointSource()` from the cordis-free `@deepseek-ai/dsh-compact/checkpoint` subpath, while the package root re-exports them for host consumers. The required transaction identity correlates the replacement checkpoint, while the predicate keeps recognition independent of any one backend. Implementations must forward the supplied signal to summarization. The seam owns no pricing API: the singleton [`ctx.tokenMeter`](token-meter.md) directly owns estimation and replay, while `dsh-compact-basic` owns retention, event sequencing, routed summarization calls, and their configuration.
Expected manual failures use `ManualCompactionErrorCode`:
@@ -129,7 +129,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
### `ctx.compact` — `CompactService` (abstract seam)
-Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses COMPACT_CHECKPOINT_SOURCE so consumers recognize it independently of the backend. Load one implementation per context as `ctx.compact`.
+Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses compactCheckpointSource with the transaction identity so consumers recognize and correlate it independently of the backend. Load one implementation per context as `ctx.compact`.
```ts cordis-catalog
/**
@@ -175,7 +175,8 @@ abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, sour
* balanced so assistant tool calls remain paired with their results. A model-
* backed implementation forwards cancellation and rejects active, missing,
* reversed, or unbalanced ranges. The target session is `agent.session`.
- * Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}.
+ * Its replacement user message must use {@link compactCheckpointSource} with
+ * the transaction's `CompactionId`.
* Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}
* for the edge checks.
*
diff --git a/docs/subsystems/compaction.zh.md b/docs/subsystems/compaction.zh.md
index a560e47dbb..9f9014b8c1 100644
--- a/docs/subsystems/compaction.zh.md
+++ b/docs/subsystems/compaction.zh.md
@@ -66,7 +66,7 @@ interface CompactionResult {
type CompactionTrigger = 'pressure' | 'context-overflow'
```
-`CompactService` 暴露 `compactIfNeeded(agent, trigger, signal)` 以执行自动 `pressure` 或 `context-overflow` 策略,暴露 `compactNow(agent, signal)` 以便即使未达到压力也对空闲会话进行一次有效缩减,还针对显式、两端均包含的 surface 范围暴露 `compactRegion(...)`。`compactNow()` 作为轮次之间的 agent maintenance 运行;没有有效范围时返回 `null` 且不写入;在摘要前记录独立的 `turn: null` 标记对,并在后续排队提示词能够从新表层派生前 flush 已闭合尝试。每个后端都使用 `COMPACT_CHECKPOINT_SOURCE` 标记其替换用的 `user/message`;client 与 wire 消费方从无 cordis 的 `@deepseek-ai/dsh-compact/checkpoint` 子路径导入该值和 `isCompactCheckpointSource()`,包根则为 host 消费方重新导出两者。该判定函数使检查点识别不依赖任一特定后端。实现必须把传入的 signal 转发给摘要流程。该 seam 不拥有计价 API:单例 [`ctx.tokenMeter`](token-meter.md) 直接拥有估算与回放,而 `dsh-compact-basic` 拥有保留策略、事件排序、按路由执行的摘要调用及其配置。
+`CompactService` 暴露 `compactIfNeeded(agent, trigger, signal)` 以执行自动 `pressure` 或 `context-overflow` 策略,暴露 `compactNow(agent, signal)` 以便即使未达到压力也对空闲会话进行一次有效缩减,还针对显式、两端均包含的 surface 范围暴露 `compactRegion(...)`。`compactNow()` 作为轮次之间的 agent maintenance 运行;没有有效范围时返回 `null` 且不写入;在摘要前记录独立的 `turn: null` 标记对,并在后续排队提示词能够从新表层派生前 flush 已闭合尝试。每个后端都使用 `compactCheckpointSource(compactionId, sourceCommandId?)` 创建替换用 `user/message` 的源;client 与 wire 消费方从无 cordis 的 `@deepseek-ai/dsh-compact/checkpoint` 子路径导入该构造函数、`CompactCheckpointSource` 和 `isCompactCheckpointSource()`,包根则为 host 消费方重新导出它们。必填的事务身份会关联替换检查点,而该判定函数使检查点识别不依赖任一特定后端。实现必须把传入的 signal 转发给摘要流程。该 seam 不拥有计价 API:单例 [`ctx.tokenMeter`](token-meter.md) 直接拥有估算与回放,而 `dsh-compact-basic` 拥有保留策略、事件排序、按路由执行的摘要调用及其配置。
预期的手动失败使用 `ManualCompactionErrorCode`:
@@ -129,7 +129,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
### `ctx.compact` — `CompactService` (abstract seam)
-Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses COMPACT_CHECKPOINT_SOURCE so consumers recognize it independently of the backend. Load one implementation per context as `ctx.compact`.
+Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses compactCheckpointSource with the transaction identity so consumers recognize and correlate it independently of the backend. Load one implementation per context as `ctx.compact`.
```ts cordis-catalog
/**
@@ -175,7 +175,8 @@ abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, sour
* balanced so assistant tool calls remain paired with their results. A model-
* backed implementation forwards cancellation and rejects active, missing,
* reversed, or unbalanced ranges. The target session is `agent.session`.
- * Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}.
+ * Its replacement user message must use {@link compactCheckpointSource} with
+ * the transaction's `CompactionId`.
* Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}
* for the edge checks.
*
diff --git a/examples/acp-agent/tests/fixtures/workspace-context-compaction.ts b/examples/acp-agent/tests/fixtures/workspace-context-compaction.ts
index e13e3cf6b3..6c1cd46fbf 100644
--- a/examples/acp-agent/tests/fixtures/workspace-context-compaction.ts
+++ b/examples/acp-agent/tests/fixtures/workspace-context-compaction.ts
@@ -1,6 +1,6 @@
import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-agent'
-import { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact'
+import { CompactionId, compactCheckpointSource } from '@deepseek-ai/dsh-compact'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-tools'
@@ -26,7 +26,7 @@ export function apply(ctx: Context): void {
if (baseline === undefined) throw new Error('workspace baseline missing before snapshot compaction')
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'Earlier context was compacted for this snapshot.' }],
- source: COMPACT_CHECKPOINT_SOURCE,
+ source: compactCheckpointSource(CompactionId('workspace-context-fixture')),
}), {
surfaceOp: { op: 'replace', start: baseline.seq, end: baseline.seq },
sourceEventSeqs: [baseline.seq],
diff --git a/packages/client/runtime/package.json b/packages/client/runtime/package.json
index f5a9a06a60..83a62693cb 100644
--- a/packages/client/runtime/package.json
+++ b/packages/client/runtime/package.json
@@ -35,6 +35,7 @@
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
+ "@deepseek-ai/dsh-compact": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
diff --git a/packages/client/runtime/src/client/sessions/steering-history.ts b/packages/client/runtime/src/client/sessions/steering-history.ts
index 4220a769ff..a28c3376a8 100644
--- a/packages/client/runtime/src/client/sessions/steering-history.ts
+++ b/packages/client/runtime/src/client/sessions/steering-history.ts
@@ -1,9 +1,7 @@
/** Reconstruct durable steering identity from the event-sourced agent inbox. */
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
-import type {} from '@deepseek-ai/dsh-agent/types'
-
-type InboxTarget = 'next-turn' | 'next-step'
+import type { InboxTarget } from '@deepseek-ai/dsh-agent/types'
/** Minimal pending identity retained while replaying durable inbox splices. */
interface PendingIdentity {
diff --git a/packages/client/ui-conversation/src/client/conversation-nodes/command.ts b/packages/client/ui-conversation/src/client/conversation-nodes/command.ts
index 38cb85ed87..752d7e3dd2 100644
--- a/packages/client/ui-conversation/src/client/conversation-nodes/command.ts
+++ b/packages/client/ui-conversation/src/client/conversation-nodes/command.ts
@@ -4,7 +4,7 @@ import type {
ConversationNodeDefinition,
} from '@deepseek-ai/dsh-client-runtime/client'
import { isReplacementSurfaceEvent } from '@deepseek-ai/dsh-client-runtime/client'
-import type { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact/checkpoint'
+import type { CompactCheckpointSource } from '@deepseek-ai/dsh-compact/checkpoint'
import type {} from '@deepseek-ai/dsh-compact/types'
import type {} from '@deepseek-ai/dsh-commands/types'
import type { ManualCompactionChatData } from '../contract/chat-nodes.ts'
@@ -21,7 +21,7 @@ declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
type CommandId = CommandNode['commandId']
-const COMPACT_PLUGIN: typeof COMPACT_CHECKPOINT_SOURCE.plugin = 'compact'
+const COMPACT_PLUGIN: CompactCheckpointSource['plugin'] = 'compact'
interface CommandState {
readonly command: CommandNode
diff --git a/packages/compact/compact/README.i18n.yaml b/packages/compact/compact/README.i18n.yaml
index ec95ee2165..8b16ecfa4b 100644
--- a/packages/compact/compact/README.i18n.yaml
+++ b/packages/compact/compact/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/compact/compact/README.md
-README.md: 3fecc91422bcdc994bcbe4db308ee4a52cb3c53f
-README.zh.md: ea66d8bdb0684f27cf68a5431be75966041b7534
+README.md: eac4339da76352bc9468488d0828a0b90a993d48
+README.zh.md: 4aec6123169a7c88a36da647202e7462fbf94564
diff --git a/packages/compact/compact/README.md b/packages/compact/compact/README.md
index 3fecc91422..eac4339da7 100644
--- a/packages/compact/compact/README.md
+++ b/packages/compact/compact/README.md
@@ -8,7 +8,7 @@ This package owns the Service Definition role of the compaction capability, spli
| Package | Role |
|---|---|
-| `@deepseek-ai/dsh-compact` (this) | Service Definition: abstract service + `compact/*` events + `CompactionResult` + canonical checkpoint source + tool-pairing boundary helpers |
+| `@deepseek-ai/dsh-compact` (this) | Service Definition: abstract service + `compact/*` events + `CompactionResult` + correlated checkpoint-source constructor + tool-pairing boundary helpers |
| `@deepseek-ai/dsh-compact-basic` | Service provider: `ctx.tokenMeter` pressure + token-budget retention + `llm.stream()` summarization |
| `@deepseek-ai/dsh-command-compact` | Consumer: the human `/compact` command over `ctx.compact.compactNow()` |
@@ -22,7 +22,7 @@ All three operations are **abstract** — the backend owns trigger policy, reten
|---|---|
| `compactIfNeeded(agent, trigger, signal)` | Consider automatic compaction for `trigger: 'pressure' \| 'context-overflow'`. A pressure trigger may apply the backend's threshold and retained-tail policy; a confirmed overflow may force a useful balanced reduction. Returns the `CompactionResult`, or `null` when no safe range exists. A backend's summarization request is a direct `ctx.llm.stream()` call (not a loop step), so per-call interception happens at `llm/stream`. |
| `compactNow(agent, signal)` | Explicitly compact one useful balanced older span even below automatic pressure. It synchronously reserves idle turn admission before yielding, writes nothing when no useful span exists, records a standalone `compact/* { turn: null }` attempt before summarization, and awaits its durability checkpoint before release. Expected operational failures use `ManualCompactionError`; cancellation rethrows the exact abort reason. |
-| `compactRegion(start, end, agent, signal?)` | Forcibly summarize surface nodes `[start, end]` (inclusive seqs) from `agent.session` into a single replacement node whose source is `COMPACT_CHECKPOINT_SOURCE`. **Throws** if a compaction is already in progress, if `start`/`end` aren't surface nodes, or if `start` is positioned after `end` on the surface. The range is a SURFACE-POSITION span, not a numeric seq interval — after a prior replace lands a fresh high-seq summary node at the shadowed range's position, surface order no longer tracks seq order. |
+| `compactRegion(start, end, agent, signal?)` | Forcibly summarize surface nodes `[start, end]` (inclusive seqs) from `agent.session` into a single replacement node whose source comes from `compactCheckpointSource(compactionId)`. **Throws** if a compaction is already in progress, if `start`/`end` aren't surface nodes, or if `start` is positioned after `end` on the surface. The range is a SURFACE-POSITION span, not a numeric seq interval — after a prior replace lands a fresh high-seq summary node at the shadowed range's position, surface order no longer tracks seq order. |
`CompactionResult` keeps the raw summary and bookkeeping-event seqs available to callers alongside the shadowed range and token accounting; its drift-checked shape lives in the [compaction data-structure reference](../../../docs/subsystems/compaction.md#compactionresult).
@@ -43,7 +43,7 @@ The private per-session cache is keyed by `session.surface.replaceGeneration` an
1. appends `compact/start` (log-only) — acquires the lock,
2. summarizes the range,
3. appends `compact/summary` (log-only) with the summary, range, shadowed seqs, token count, and provider/model call envelope,
-4. appends a single `user/message` with `source: COMPACT_CHECKPOINT_SOURCE` and `surfaceOp: { op: 'replace', start, end }` carrying the summary — **the only surface mutation in this operation**,
+4. appends a single `user/message` with `source: compactCheckpointSource(compactionId, sourceCommandId?)` and `surfaceOp: { op: 'replace', start, end }` carrying the summary — **the only surface mutation in this operation**,
5. appends `compact/end` (log-only) — releases the lock.
The surface mutation (step 4) sits **inside** the lock bracket: `compact/end` is the last event, so the lock is never released before the mutation lands. A crash between `compact/start` and `compact/end` therefore leaves a detectable orphaned lock (a `compact/start` with no matching `compact/end`) rather than a `compact/end` that falsely claims compaction finished while the surface was never shadowed.
@@ -64,11 +64,11 @@ The `compact/*` events extend `SessionEventMap` (merge-extensible) via declarati
## Implementing a backend
-Subclass `CompactService`, implement `compactIfNeeded`, `compactNow`, and `compactRegion`, and load the subclass as a plugin — it registers as `ctx.compact`. Every successful backend uses `COMPACT_CHECKPOINT_SOURCE` on its replacement user message; `isCompactCheckpointSource()` recognizes the marker after persistence or cloning without depending on backend identity. A template- or model-backed implementation can live as a sibling package without changing callers or the shared token meter.
+Subclass `CompactService`, implement `compactIfNeeded`, `compactNow`, and `compactRegion`, and load the subclass as a plugin — it registers as `ctx.compact`. Every successful backend creates its replacement user message source with `compactCheckpointSource(compactionId, sourceCommandId?)`; the required `compactionId` correlates the checkpoint with its `compact/*` transaction, while `isCompactCheckpointSource()` recognizes the marker after persistence or cloning without depending on backend identity. A template- or model-backed implementation can live as a sibling package without changing callers or the shared token meter.
## Recognizing a checkpoint outside the host program (`./checkpoint`)
-`COMPACT_CHECKPOINT_SOURCE` and `isCompactCheckpointSource()` are declared on the `@deepseek-ai/dsh-compact/checkpoint` subpath and re-exported from the root, so host-side consumers keep reading them from the root. The leaf imports no cordis and declares no module augmentation (the [`dsh-commands/brand`](../../interaction/commands/README.md) shape), which is what lets a client or wire program name the checkpoint source: the package **root** cannot enter such a program at all, because it reaches `dsh-session`'s root and that `Context` merge declares the host `sessions` service against the client's own (`TS2717` — one program per side, per [development.md](../../../docs/development.md#typescript-project-layout)). The web client's transcript adapter pins its plugin literal to this leaf with a type-only import, so renaming the plugin id here is a compile error there.
+`compactCheckpointSource()`, `CompactCheckpointSource`, and `isCompactCheckpointSource()` are declared on the `@deepseek-ai/dsh-compact/checkpoint` subpath and re-exported from the root, so host-side consumers keep reading them from the root. The constructor requires the owning `CompactionId`, preventing backends from writing an uncorrelated marker that the package invariant must reject. The leaf imports no cordis and declares no module augmentation (the [`dsh-commands/brand`](../../interaction/commands/README.md) shape), which is what lets a client or wire program name the checkpoint source: the package **root** cannot enter such a program at all, because it reaches `dsh-session`'s root and that `Context` merge declares the host `sessions` service against the client's own (`TS2717` — one program per side, per [development.md](../../../docs/development.md#typescript-project-layout)). The web client's transcript adapter pins its plugin literal to the leaf's source type, so renaming the plugin id there is a compile error here.
## Model Experience
diff --git a/packages/compact/compact/README.zh.md b/packages/compact/compact/README.zh.md
index ea66d8bdb0..4aec612316 100644
--- a/packages/compact/compact/README.zh.md
+++ b/packages/compact/compact/README.zh.md
@@ -8,7 +8,7 @@
| 包 | 职责 |
|---|---|
-| `@deepseek-ai/dsh-compact`(本包) | Service Definition:抽象服务 + `compact/*` 事件 + `CompactionResult` + 规范检查点源 + 工具配对边界 helper |
+| `@deepseek-ai/dsh-compact`(本包) | Service Definition:抽象服务 + `compact/*` 事件 + `CompactionResult` + 关联检查点源构造函数 + 工具配对边界 helper |
| `@deepseek-ai/dsh-compact-basic` | Service provider:`ctx.tokenMeter` 压力 + token 预算保留 + `llm.stream()` 摘要 |
| `@deepseek-ai/dsh-command-compact` | Consumer:面向人类的 `/compact` 命令,基于 `ctx.compact.compactNow()` 实现 |
@@ -22,7 +22,7 @@
|---|---|
| `compactIfNeeded(agent, trigger, signal)` | 根据 `trigger: 'pressure' \| 'context-overflow'` 判断是否需要自动压缩。压力触发可应用后端的阈值与保留尾部策略;已确认溢出可强制进行有效的平衡缩减。返回 `CompactionResult`,无安全范围时则返回 `null`。后端摘要请求是直接的 `ctx.llm.stream()` 调用(不是 agent loop 步骤),因此每次调用都可在 `llm/stream` 处拦截。 |
| `compactNow(agent, signal)` | 即使未达到自动压力,也显式压缩一段有效、平衡的较早范围。该操作会在让出控制权前同步预留空闲轮次接纳;没有有效范围时不写入任何内容;在摘要前记录独立的 `compact/* { turn: null }` 尝试;释放预留前等待其持久性检查点。预期操作失败使用 `ManualCompactionError`;取消会原样重新抛出 abort 原因。 |
-| `compactRegion(start, end, agent, signal?)` | 强制将表层节点 `[start, end]`(包含两端 seq)从 `agent.session` 摘要为单个替换节点,其源为 `COMPACT_CHECKPOINT_SOURCE`。如果压缩已在进行、`start`/`end` 不是表层节点,或 `start` 在表层上位于 `end` 之后,则**抛出异常**。该范围是表层位置范围,不是数值 seq 区间:在之前的 replace 将新生成的高 seq 摘要节点放到已遮蔽范围的位置之后,表层顺序不再跟随 seq 顺序。 |
+| `compactRegion(start, end, agent, signal?)` | 强制将表层节点 `[start, end]`(包含两端 seq)从 `agent.session` 摘要为单个替换节点,其源由 `compactCheckpointSource(compactionId)` 创建。如果压缩已在进行、`start`/`end` 不是表层节点,或 `start` 在表层上位于 `end` 之后,则**抛出异常**。该范围是表层位置范围,不是数值 seq 区间:在之前的 replace 将新生成的高 seq 摘要节点放到已遮蔽范围的位置之后,表层顺序不再跟随 seq 顺序。 |
`CompactionResult` 向调用方保留原始摘要与记录操作过程的事件 seq,同时保留已遮蔽范围与 token 计量;其结构由漂移检查保障,定义见 [压缩数据结构参考](../../../docs/subsystems/compaction.md#compactionresult)。
@@ -43,7 +43,7 @@
1. 追加 `compact/start`(仅日志):获取锁;
2. 摘要该范围;
3. 追加 `compact/summary`(仅日志),其中记录摘要、范围、已遮蔽 seq、token 数与提供方/模型调用 envelope;
-4. 追加单个 `user/message`,其携带 `source: COMPACT_CHECKPOINT_SOURCE` 和包含摘要的 `surfaceOp: { op: 'replace', start, end }`:这是**本操作唯一的表层变更**;
+4. 追加单个 `user/message`,其携带 `source: compactCheckpointSource(compactionId, sourceCommandId?)` 和包含摘要的 `surfaceOp: { op: 'replace', start, end }`:这是**本操作唯一的表层变更**;
5. 追加 `compact/end`(仅日志):释放锁。
表层变更(第 4 步)位于锁的起止范围**内**:`compact/end` 是最后一个事件,因此表层变更落地前绝不会释放锁。如果在 `compact/start` 与 `compact/end` 之间崩溃,会留下可检测的遗留锁(一个 `compact/start` 没有匹配的 `compact/end`),而不是虚假声称压缩已完成、但表层从未被遮蔽的 `compact/end`。
@@ -64,11 +64,11 @@
## 实现后端
-继承 `CompactService`,实现 `compactIfNeeded`、`compactNow` 与 `compactRegion`,再将子类作为插件加载:它会注册为 `ctx.compact`。每个成功后端都在替换 user 消息上使用 `COMPACT_CHECKPOINT_SOURCE`;`isCompactCheckpointSource()` 可在持久化或克隆后识别该标记,无需依赖后端身份。基于模板或模型的实现可以放在同级包中,不需更改调用方或共享 token meter。
+继承 `CompactService`,实现 `compactIfNeeded`、`compactNow` 与 `compactRegion`,再将子类作为插件加载:它会注册为 `ctx.compact`。每个成功后端都使用 `compactCheckpointSource(compactionId, sourceCommandId?)` 创建替换 user 消息的源;必填的 `compactionId` 将检查点与对应 `compact/*` 事务关联,而 `isCompactCheckpointSource()` 可在持久化或克隆后识别该标记,无需依赖后端身份。基于模板或模型的实现可以放在同级包中,不需更改调用方或共享 token meter。
## 在 host 程序之外识别检查点(`./checkpoint`)
-`COMPACT_CHECKPOINT_SOURCE` 与 `isCompactCheckpointSource()` 声明在 `@deepseek-ai/dsh-compact/checkpoint` 子路径上,并由包根重新导出,因此 host 侧消费方仍从根读取它们。该叶子不导入 cordis、也不声明任何模块增强(即 [`dsh-commands/brand`](../../interaction/commands/README.md) 的形状),这正是客户端或 wire 程序能够命名该检查点来源的原因:包的**根**根本无法进入这类程序,因为它会到达 `dsh-session` 的根,而那处 `Context` 合并会让 host 的 `sessions` 服务与客户端自己的冲突(`TS2717`——每侧一个程序,见 [development.md](../../../docs/development.md#typescript-project-layout))。Web 客户端的对话记录适配器用仅类型导入把它的插件字面量钉在该叶子上,因此在此处改插件 id 会让那边编译失败。
+`compactCheckpointSource()`、`CompactCheckpointSource` 与 `isCompactCheckpointSource()` 声明在 `@deepseek-ai/dsh-compact/checkpoint` 子路径上,并由包根重新导出,因此 host 侧消费方仍从根读取它们。构造函数要求传入所属 `CompactionId`,防止后端写入缺少关联关系、必然被包不变量拒绝的标记。该叶子不导入 cordis、也不声明任何模块增强(即 [`dsh-commands/brand`](../../interaction/commands/README.md) 的形状),这正是客户端或 wire 程序能够命名该检查点来源的原因:包的**根**根本无法进入这类程序,因为它会到达 `dsh-session` 的根,而那处 `Context` 合并会让 host 的 `sessions` 服务与客户端自己的冲突(`TS2717`——每侧一个程序,见 [development.md](../../../docs/development.md#typescript-project-layout))。Web 客户端的对话记录适配器用仅类型导入把它的插件字面量钉在该叶子的源类型上,因此在此处改插件 id 会让那边编译失败。
## 模型体验
diff --git a/packages/compact/compact/src/checkpoint.ts b/packages/compact/compact/src/checkpoint.ts
index 9908fe4ac0..18a2c5985e 100644
--- a/packages/compact/compact/src/checkpoint.ts
+++ b/packages/compact/compact/src/checkpoint.ts
@@ -1,12 +1,12 @@
/**
- * The compaction seam's canonical checkpoint source: the plugin marker every
- * backend stamps on the replacement user message that lands a checkpoint, plus
- * the predicate that recognizes it.
+ * Compaction checkpoint provenance: the correlated source constructor and type
+ * every backend uses for its replacement user message, plus the predicate that
+ * recognizes persisted checkpoints.
*
- * The seam itself lives in `@deepseek-ai/dsh-compact`, which re-exports both of
- * these; this module is a pure value/predicate outlet (no cordis imports, no
- * module augmentation) so client and wire programs can name the checkpoint
- * source without loading the host plugin's Context merges — the
+ * The seam itself lives in `@deepseek-ai/dsh-compact`, which re-exports these
+ * contracts; this module is a pure type/value/predicate outlet (no cordis
+ * imports, no module augmentation) so client and wire programs can name the
+ * checkpoint source without loading the host plugin's Context merges — the
* `dsh-commands/brand` shape.
*
* @module @deepseek-ai/dsh-compact/checkpoint
@@ -16,11 +16,10 @@ import type { MessageSource } from '@deepseek-ai/dsh-llm/message'
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
import type { CompactionId } from './brand.ts'
-/** Canonical source for the replacement user message produced by every compaction backend. */
-export const COMPACT_CHECKPOINT_SOURCE = Object.freeze({ kind: 'plugin', plugin: 'compact' } as const)
+const COMPACT_CHECKPOINT_MARKER = Object.freeze({ kind: 'plugin', plugin: 'compact' } as const)
/** Message provenance carried by a concrete compaction checkpoint. */
-export type CompactCheckpointSource = typeof COMPACT_CHECKPOINT_SOURCE & {
+export type CompactCheckpointSource = typeof COMPACT_CHECKPOINT_MARKER & {
readonly compactionId: CompactionId
readonly sourceCommandId?: CommandId
}
@@ -36,7 +35,7 @@ export function compactCheckpointSource(
sourceCommandId?: CommandId,
): CompactCheckpointSource {
return Object.freeze({
- ...COMPACT_CHECKPOINT_SOURCE,
+ ...COMPACT_CHECKPOINT_MARKER,
compactionId,
...sourceCommandId === undefined ? {} : { sourceCommandId },
})
@@ -48,5 +47,5 @@ export function compactCheckpointSource(
* @returns whether the source carries the backend-independent checkpoint marker.
*/
export function isCompactCheckpointSource(source: MessageSource): boolean {
- return source.kind === 'plugin' && source.plugin === COMPACT_CHECKPOINT_SOURCE.plugin
+ return source.kind === 'plugin' && source.plugin === COMPACT_CHECKPOINT_MARKER.plugin
}
diff --git a/packages/compact/compact/src/index.ts b/packages/compact/compact/src/index.ts
index 0085321333..6fb9706c04 100644
--- a/packages/compact/compact/src/index.ts
+++ b/packages/compact/compact/src/index.ts
@@ -15,10 +15,10 @@ import type { CompactionResult } from './types.ts'
export type { CompactionResult } from './types.ts'
export { CompactionId } from './brand.ts'
export { toolPairingBalancedAfter, toolPairingBalancedBefore } from './tool-pairing.ts'
-// The checkpoint source and its predicate are declared on the cordis-free
+// The checkpoint source constructor and predicate are declared on the cordis-free
// `./checkpoint` leaf so client and wire programs can name them without this
// root's Context merge; the root stays the host-side entry point for both.
-export { COMPACT_CHECKPOINT_SOURCE, compactCheckpointSource, isCompactCheckpointSource } from './checkpoint.ts'
+export { compactCheckpointSource, isCompactCheckpointSource } from './checkpoint.ts'
export type { CompactCheckpointSource } from './checkpoint.ts'
/** Why automatic policy is asking a backend to consider compaction. */
@@ -89,9 +89,9 @@ declare module 'cordis' {
* and summarization, and may consume a separate measurement service. A
* successful run replaces the selected surface span with one summary node and
* prevents concurrent compaction of the same session. The replacement user
- * message uses {@link COMPACT_CHECKPOINT_SOURCE} so consumers recognize it
- * independently of the backend. Load one implementation per context as
- * `ctx.compact`.
+ * message uses {@link compactCheckpointSource} with the transaction identity
+ * so consumers recognize and correlate it independently of the backend. Load
+ * one implementation per context as `ctx.compact`.
*/
export abstract class CompactService extends Service {
constructor(ctx: Context) {
@@ -149,7 +149,8 @@ export abstract class CompactService extends Service {
* balanced so assistant tool calls remain paired with their results. A model-
* backed implementation forwards cancellation and rejects active, missing,
* reversed, or unbalanced ranges. The target session is `agent.session`.
- * Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}.
+ * Its replacement user message must use {@link compactCheckpointSource} with
+ * the transaction's `CompactionId`.
* Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}
* for the edge checks.
*
diff --git a/packages/context/session-reference/tests/session-reference.spec.ts b/packages/context/session-reference/tests/session-reference.spec.ts
index b9e5e55241..ce964af16a 100644
--- a/packages/context/session-reference/tests/session-reference.spec.ts
+++ b/packages/context/session-reference/tests/session-reference.spec.ts
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
-import { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact'
+import { CompactionId, compactCheckpointSource } from '@deepseek-ai/dsh-compact'
import { createUserMessage, CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
import SessionQueryService from '@deepseek-ai/dsh-session-query'
@@ -48,6 +48,10 @@ function expectCode(code: SessionReferenceErrorCode): Error {
return expect.objectContaining({ code }) as Error
}
+function checkpointSource(id: string) {
+ return compactCheckpointSource(CompactionId(id))
+}
+
function appendConversation(session: Session): void {
const oldUser = session.append(
'user/message',
@@ -75,7 +79,8 @@ function appendConversation(session: Session): void {
session.append(
'user/message',
createUserMessage({
- content: [{ type: 'text', text: 'checkpoint' }], source: COMPACT_CHECKPOINT_SOURCE,
+ content: [{ type: 'text', text: 'checkpoint' }],
+ source: checkpointSource('conversation'),
}),
{
surfaceOp: { op: 'replace', start: oldUser.seq, end: oldAssistant.seq },
@@ -534,7 +539,8 @@ describe('session reference discovery and preparation', () => {
source.append(
'user/message',
createUserMessage({
- content: [{ type: 'text', text: `${id}-${'界'.repeat(400)}` }], source: COMPACT_CHECKPOINT_SOURCE,
+ content: [{ type: 'text', text: `${id}-${'界'.repeat(400)}` }],
+ source: checkpointSource(id),
}),
{ surfaceOp: 'append' },
)
@@ -616,7 +622,8 @@ describe('session reference discovery and preparation', () => {
source.append(
'user/message',
createUserMessage({
- content: [{ type: 'text', text: 'later compact checkpoint' }], source: COMPACT_CHECKPOINT_SOURCE,
+ content: [{ type: 'text', text: 'later compact checkpoint' }],
+ source: checkpointSource('later-source-mutation'),
}),
{
surfaceOp: { op: 'replace', start: original.seq, end: later.seq },
diff --git a/packages/llm/llm-retry/README.i18n.yaml b/packages/llm/llm-retry/README.i18n.yaml
index 65e0c911ed..2a1f8d46a2 100644
--- a/packages/llm/llm-retry/README.i18n.yaml
+++ b/packages/llm/llm-retry/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/llm-retry/README.md
-README.md: e6e56ec44032d714393c6fcc1c42d7271017a294
-README.zh.md: b7ce8bee4acd2c4f7c88870745dff96ec5695435
+README.md: 0a907b5505650f7c2cd5e9933750be3701f8e34b
+README.zh.md: bb8c5b50efddf3def0047f411285b57c3c544f95
diff --git a/packages/llm/llm-retry/README.md b/packages/llm/llm-retry/README.md
index e6e56ec440..0a907b5505 100644
--- a/packages/llm/llm-retry/README.md
+++ b/packages/llm/llm-retry/README.md
@@ -8,9 +8,9 @@ Each provider adapter owns an optional nested `retryPolicy`, captured when its r
Both modes use bounded exponential backoff with symmetric jitter. A valid `providerRetryAfterMs` at or below `maxDelayMs` replaces local backoff without jitter. An over-cap provider delay makes normal mode delegate, while always mode uses its configured local backoff so it cannot terminate on that instruction.
-Before waiting, the plugin appends a non-surface `llm/retry` event with the provider, mode, canonical resolved-policy key, failure, and scheduled delay. Its payload is available from the browser-safe `@deepseek-ai/dsh-llm-retry/types` subpath, so remote renderers can consume the durable status without loading the policy runtime. The key includes every behavior-affecting field and sorts normal-mode codes because eligibility uses set membership. Retry numbers continue only across events with the same provider and complete policy key, so a route replacement with different limits, code membership, or backoff starts its own history. Normal events include the finite maximum; always events omit it, and UIs render `∞`. After the wait, the listener returns `{ kind: 'retry' }`, and the loop closes the failed turn and opens a retry turn over the same durable history. Cancellation and plugin disposal abort active backoff, drain active delegated recovery before applying the abort, and make a callback captured before disposal fail closed.
+Before waiting, the plugin appends a non-surface `llm/retry` event with the shared `retryId`, provider, mode, canonical resolved-policy key, failure, and scheduled delay. Its payload is available from the browser-safe `@deepseek-ai/dsh-llm-retry/types` subpath, so remote renderers can consume the durable status without loading the policy runtime. The key includes every behavior-affecting field and sorts normal-mode codes because eligibility uses set membership. Retry numbers continue only across events with the same provider and complete policy key, so a route replacement with different limits, code membership, or backoff starts its own history. Normal events include the finite maximum; always events omit it, and UIs render `∞`. When the wait completes, the plugin appends `llm/retry-started` with the same `retryId`, turn, step, and retry number immediately before returning `{ kind: 'retry' }`; cancellation during backoff writes no started event. The loop then closes the failed turn and opens a retry turn over the same durable history. Cancellation and plugin disposal abort active backoff, drain active delegated recovery before applying the abort, and make a callback captured before disposal fail closed.
-The separately published `./invariant` companion checks that every retry record names the current open turn and latest closed step, matches the failed request's durable provider, carries non-empty provider and policy identities, has mode-specific bounds, a unique step record, the correct provider-policy retry number, and a bounded timer delay. Full jitter may schedule zero milliseconds at its lower boundary.
+The separately published `./invariant` companion checks that every scheduled retry names the current open turn and latest closed step, matches the failed request's durable provider, carries non-empty provider and policy identities, has mode-specific bounds, a unique step record, the correct provider-policy retry number, and a bounded timer delay. It also requires each `llm/retry-started` event to name one prior scheduled attempt with the same `retryId`, turn, step, and retry number, and rejects repeated started events. Full jitter may schedule zero milliseconds at its lower boundary.
```yaml
- name: '@deepseek-ai/dsh-llm-deepseek'
diff --git a/packages/llm/llm-retry/README.zh.md b/packages/llm/llm-retry/README.zh.md
index b7ce8bee4a..bb8c5b50ef 100644
--- a/packages/llm/llm-retry/README.zh.md
+++ b/packages/llm/llm-retry/README.zh.md
@@ -8,9 +8,9 @@
两种 mode 都使用带对称 jitter 的有界指数退避。有效 `providerRetryAfterMs` 不超过 `maxDelayMs` 时会替换本地退避,并且不加 jitter。超出上限的提供方延迟会使 normal mode 继续委托;always mode 则改用已配置的本地退避,避免该指令终止重试。
-等待前,插件会追加一条不进入表层的 `llm/retry` 事件,其中包含提供方、mode、已解析策略的规范 key、失败和计划延迟。该载荷由可安全用于浏览器的 `@deepseek-ai/dsh-llm-retry/types` 子路径导出,因此远程渲染器无需加载策略运行时即可使用该持久状态。该 key 包含所有影响行为的字段,并对 normal mode 的 code 排序,因为合格性采用集合成员关系判断。只有提供方与完整策略 key 都相同的事件才会延续重试编号;因此,用限制、code 成员关系或退避不同的路由替换后,会开始自己的历史。normal 事件包含有限上限;always 事件省略该上限,UI 会渲染 `∞`。等待结束后,监听器返回 `{ kind: 'retry' }`,循环关闭失败轮次,并在同一持久历史上开启重试轮次。取消与插件 dispose 会中止活跃退避,在应用中止前排空活跃的委托恢复,并使 dispose 前捕获的 callback 只能以失败结束。
+等待前,插件会追加一条不进入表层的 `llm/retry` 事件,其中包含共享 `retryId`、提供方、mode、已解析策略的规范 key、失败和计划延迟。该载荷由可安全用于浏览器的 `@deepseek-ai/dsh-llm-retry/types` 子路径导出,因此远程渲染器无需加载策略运行时即可使用该持久状态。该 key 包含所有影响行为的字段,并对 normal mode 的 code 排序,因为合格性采用集合成员关系判断。只有提供方与完整策略 key 都相同的事件才会延续重试编号;因此,用限制、code 成员关系或退避不同的路由替换后,会开始自己的历史。normal 事件包含有限上限;always 事件省略该上限,UI 会渲染 `∞`。等待完成时,插件会在返回 `{ kind: 'retry' }` 前立即追加 `llm/retry-started`,其中带有相同的 `retryId`、轮次、步骤与重试编号;退避期间取消则不会写入 started 事件。随后循环关闭失败轮次,并在同一持久历史上开启重试轮次。取消与插件 dispose 会中止活跃退避,在应用中止前排空活跃的委托恢复,并使 dispose 前捕获的 callback 只能以失败结束。
-单独发布的 `./invariant` 配套模块会检查每个重试记录是否指向当前开启轮次及其最新已关闭步骤,是否与失败请求的持久提供方匹配,是否携带非空的提供方与策略标识,是否满足 mode 特定边界,是否拥有唯一步骤记录和正确的提供方策略重试编号,以及是否携带有界定时器延迟。完整 jitter 可以在下界调度为零毫秒。
+单独发布的 `./invariant` 配套模块会检查每个已调度重试是否指向当前开启轮次及其最新已关闭步骤,是否与失败请求的持久提供方匹配,是否携带非空的提供方与策略标识,是否满足 mode 特定边界,是否拥有唯一步骤记录和正确的提供方策略重试编号,以及是否携带有界定时器延迟。它还要求每个 `llm/retry-started` 事件通过相同的 `retryId`、轮次、步骤与重试编号指向一个先前调度的尝试,并拒绝重复的 started 事件。完整 jitter 可以在下界调度为零毫秒。
```yaml
- name: '@deepseek-ai/dsh-llm-deepseek'
diff --git a/packages/self-modification/tool-cordis/src/api-catalog.ts b/packages/self-modification/tool-cordis/src/api-catalog.ts
index b52000f554..c8ec4894b4 100644
--- a/packages/self-modification/tool-cordis/src/api-catalog.ts
+++ b/packages/self-modification/tool-cordis/src/api-catalog.ts
@@ -282,7 +282,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
{
signature: 'abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise',
- jsDoc: '/**\n * Forcibly compact a range of surface nodes into a single summary node.\n * `start` and `end` name an inclusive span by surface position, not numeric seq\n * order; replacements can make visible seqs non-monotonic. Both edges must be\n * balanced so assistant tool calls remain paired with their results. A model-\n * backed implementation forwards cancellation and rejects active, missing,\n * reversed, or unbalanced ranges. The target session is `agent.session`.\n * Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}.\n * Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}\n * for the edge checks.\n *\n * @param start - first surface seq, inclusive.\n * @param end - last surface seq, inclusive.\n * @param agent - context whose session is mutated and whose routing options guide summarization.\n * @param signal - optional cancellation; model-backed implementations must forward it.\n * @throws when compaction is active or the range is missing, reversed, or unbalanced.\n * @returns the appended event seqs, summary, replaced range, and token accounting.\n */',
+ jsDoc: '/**\n * Forcibly compact a range of surface nodes into a single summary node.\n * `start` and `end` name an inclusive span by surface position, not numeric seq\n * order; replacements can make visible seqs non-monotonic. Both edges must be\n * balanced so assistant tool calls remain paired with their results. A model-\n * backed implementation forwards cancellation and rejects active, missing,\n * reversed, or unbalanced ranges. The target session is `agent.session`.\n * Its replacement user message must use {@link compactCheckpointSource} with\n * the transaction\'s `CompactionId`.\n * Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}\n * for the edge checks.\n *\n * @param start - first surface seq, inclusive.\n * @param end - last surface seq, inclusive.\n * @param agent - context whose session is mutated and whose routing options guide summarization.\n * @param signal - optional cancellation; model-backed implementations must forward it.\n * @throws when compaction is active or the range is missing, reversed, or unbalanced.\n * @returns the appended event seqs, summary, replaced range, and token accounting.\n */',
},
],
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6fb20b9718..1c27b19685 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1507,6 +1507,9 @@ importers:
'@deepseek-ai/dsh-commands':
specifier: workspace:^
version: link:../../interaction/commands
+ '@deepseek-ai/dsh-compact':
+ specifier: workspace:^
+ version: link:../../compact/compact
'@deepseek-ai/dsh-host-apiproxy':
specifier: workspace:^
version: link:../../host/apiproxy