fix conversation node review follow-ups

This commit is contained in:
imccyu
2026-08-09 20:36:57 +08:00
parent 81822f4076
commit d99ad2b1d0
18 changed files with 67 additions and 56 deletions
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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
+5 -5
View File
@@ -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 会让那边编译失败。
## 模型体验
+11 -12
View File
@@ -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
}
+7 -6
View File
@@ -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.
*