# transcript 四层模型

> L1 存储 / L2 幂等操作 / L3 订阅粒度 / L4 视图注册，序列号契约，以及从 wire.jsonl 冷重建的两级折叠。

- 作者：David（道雾轩）
- 专栏：Kimi Code 源码解读（https://daiw.org/manual/kimi-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/kimi-code/transcript
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

`packages/transcript`（约 3696 行）是**转录渲染数据层**。包描述：

> Isomorphic transcript rendering data layer: agent-granular L1 store, idempotent L2 operations, granularity-gated L3 subscriptions, framework-free L4 view registry.

根 `AGENTS.md` 对它的定位没变：纯 TypeScript、浏览器安全、不 import 引擎，并且是 transcript 契约类型（`src/contract/`）与 op 批次序列号契约的**唯一所有者**。运行时依赖只有 `zod`。它和 agent-core-v2、kap-server 一样属于「零注释区」，源码里的注释已全部清掉。

## 为什么要独立成包

同一份对话历史，服务端要投影、浏览器要渲染：

- 浏览器端**不能** import 引擎（Node 依赖、体积）。
- 两边若各写一套数据模型与协议类型，必然分叉。

所以把「转录的数据模型 + 归约逻辑 + 线协议 schema」抽成纯 TS 包。仓库里的运行时消费者是 kap-server；Web UI 已移到另一个仓库，以预构建产物 `apps/kimi-code/dist-web` 随 CLI 发布，产物里能搜到 `subscribe_v2`、`transcript_since`、`transcript.ops` 这些协议字面量。`apps/kimi-inspect` 则已改用 kap-server 的 message protocol v3，不再依赖本包（见 [kap-server](https://daiw.org/manual/kimi-code/kap-server)）。

## 四层

```text
packages/transcript/src/
├── model/        L1 数据模型
├── store/        L1 存储（agentTranscript / transcriptStore）
├── ops/          L2 幂等操作（operation / apply）
├── granularity/  L3 订阅粒度（grade / filterOps）
├── view/         L4 视图注册（registry）
├── pagination/   分页
├── history/      冷重建（groupTurns / foldFacts）
└── contract/     协议契约（schema / events / mediaRef / origin）
```

### L1：存储

`model/` 下的实体：`ids`、`turn`、`frame`、`interaction`、`attachment`、`todo`、`item`、`task`、`meta`、`prompt`。

**agent 粒度**——主代理与各子代理各有一条转录流，`store/agentTranscript.ts` 管单个，`store/transcriptStore.ts` 管全体（按需 `ensureAgent`，并广播 agent 名册变化）。

### L2：幂等操作

```ts
export { EMPTY_AGENT_STATE, applyOperation, appendAtOffset } from './ops/apply';
export type { AgentState, ApplyResult } from './ops/apply';
```

**幂等**是关键：WS 可能重连、重放、乱序，同一个 op 应用两次结果必须不变。`AgentState` 不可变，`applyOperation` 是纯归约函数——标准的 event sourcing 形态。

### L3：订阅粒度

四档 `TranscriptGrade`：**`off` / `turn` / `block` / `delta`**。`granularity/filterOps.ts` 的判定很短：

| 档 | 放行什么 |
| --- | --- |
| `off` | 什么都不发 |
| `turn` | 除下面两档专属 op 以外的全部 op；快照里 turn 的 `steps` 置空 |
| `block` | 再加 `step.upsert` / `frame.upsert`（整状态帧） |
| `delta` | 再加逐 token 的 `append` |

同一份数据，不同消费者按需订阅：只关心结构的订 `turn`，要完整帧但不要逐 token 动画的订 `block`，打字机效果才需要 `delta`。`gradeFor()` 支持按 agent 指定、以 `'*'` 兜底；`needsResetOnTransition()` 规定**升档必须重发基线**——低档时没发的帧，增量补不回来。

### L4：视图注册

`view/registry.ts` 的 `ViewRegistry<C>` 不依赖任何 UI 框架，只按键分派：工具帧按 `frame.view ?? frame.name`（小写），输入按 `origin.kind`，时间线标记按 marker 名，另有 `fallbackTool` 兜底。`C` 是宿主框架自己的组件类型。

## 序列号契约

`contract/schema.ts` 第 434 行：

```ts
export const transcriptSeqSchema = z.number().int().nonnegative();
```

- 每个 **(session, agent)** 有单调递增的批次 `seq`。
- 订阅帧 `subscribe_v2` 的载荷里，`transcript_since` 是按 agent 的续传游标。
- 追赶响应 `transcriptOpsCatchupResponseSchema`：`batches`（每批 `seq` + `ops`）、`latest_seq`、`complete`。

订阅游标 `transcript_since` 和 REST 转录响应里的 `seq` 都是 `.optional()`——不认识 seq 的老对端照样能用，只是没有增量追赶，只能全量刷新。服务端 `GET /sessions/{session_id}/transcript/ops?since_seq=` 做点对点追赶；`complete: false` 表示会话不在内存或 journal 已回溯不到，调用方必须回退全量刷新。

## 冷重建：两级折叠

会话不在内存里（冷会话）时，要从每个 agent 的 `wire.jsonl` 重建转录。kap-server 的 `services/transcript/transcriptService.ts` 读出记录后分两步：

```text
wire.jsonl（唯一真相）
   ├── groupMessagesIntoSnapshot()  history/groupTurns.ts：上下文消息 → 回合树
   └── foldWireRecordFacts()        history/foldFacts.ts：非上下文记录 → 任务 / 交互 / 待办 / 标记……
```

文件名没变，两个入口的函数名比 v0.29.1 时更直白。`foldFacts.ts` 第 579 行起：

```ts
  for (const [id, entity] of interactions) {
    if (entity.state === 'pending') {
      interactions.set(id, { ...entity, state: 'cancelled' });
    }
  }
```

**关机时仍 pending 的交互，折叠成 `cancelled`**——否则重建出来会有永远转圈的「等待审批」。折叠也处理 `context.undo` 记录：被撤销的回合在冷重建里同样隐藏（v2 撤销时会顺带写一条兼容的 `context.undo`，见[记录与回放](https://daiw.org/manual/kimi-code/records-replay)）。

旧版根 `AGENTS.md` 曾注明「实时投影才有的字段冷重建不回填」，这句已从现行文档删去。对照代码：`model/` 里 step 的 `usage` / `finishReason` / `timing` / `retry`、task 的 `resultSummary` 仍在，`history/` 下却找不到它们的回填逻辑——这些字段依旧只来自实时投影。

## 计划的引用式存储

[Plan 章节](https://daiw.org/manual/kimi-code/goal-plan)提到的机制换了落点。引擎侧 `features/plan/planService.ts` 的 `recordRevision()`：

```ts
    const scope = this.agentCtx.scope();
    const key = `plan/${id}/v${version}.md`;
    await this.blobs.put(scope, key, bytes);
```

文档写进 agent 作用域的 blob 存储，`plan.revision` 记录只带 `{agentId, id, version, key, sha256, bytes}`。`foldFacts.ts` 优先用 `key`（可由调用方传入的 `resolvePlanRevisionKey` 解析成路径），老记录里的 `path` 仍兼容。**大文档卸载，记录里只存引用 + 校验和**——事件流保持轻量，同时可校验完整性。

## 与 kap-server 的分工

`transcript` 是纯数据层，kap-server 负责：

- 引擎事件 → transcript ops（`services/transcript/coreEventMap.ts`、`coreBinding.ts`）
- REST 路由：`/sessions/{session_id}/transcript` 及其 `/ops`、`/user-messages`、`/plan`
- 批次 seq 分配与有界 journal（`TRANSCRIPT_OPS_JOURNAL_CAPACITY = 2000`）
- 事件抑制：订阅了 transcript 的连接，不再重复收到已被投影的旧式 `session_event`（`suppressedByTranscript()`）

下一篇：[记录与回放](https://daiw.org/manual/kimi-code/records-replay)。
