# 会话与存储

> Session 与 Agent 两级 scope 的分工、磁盘布局与会话索引、fork、prompt 生命周期，以及 v2 的 DI 与 Feature 接缝。

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

## Session ≠ Agent

v1 用 `Session` 与 `Agent` 两个类区分职责；v2 把这条边界做成了 DI 的**生命周期 scope**。`src/app/scopes.ts` 声明了 `LifecycleScope.App`、`Session`、`Agent`，每个服务注册时写明自己活在哪一级：

| 归属 | 目录 | 代表服务 |
| --- | --- | --- |
| **Agent** | `src/agent/` | `IAgentLoopService`、`IAgentContextMemoryService`、`IAgentLLMRequesterService`、`IAgentProfileService`、`IAgentToolExecutorService`、`IAgentFullCompactionService` |
| **Session** | `src/session/` | `IAgentLifecycleService`（本会话里的 agent，主 agent 的 ID 是 `main`）、`ISessionSubagentService`、`ISessionMetadata`、终端服务 |
| **App** | `src/app/` | `ISessionIndex`、配置、模型目录、遥测 |

工作区这一级在 `src/workspace/`：`WorkspaceInstanceManager` 为每个工作区创建一个 `SessionLifecycleService`，负责会话的 `create`、`resume`、`fork`、`archive`、`restore`、`delete`。打开会话时，它用 `createScopedChildHandle()` 造出 Session scope；会话里的每个 agent 再各自拥有一个 Agent scope，主 agent 与子代理的服务实例互不共享。

## 磁盘布局

数据根目录是 `~/.kimi-code`（可用 `KIMI_CODE_HOME` 覆盖；旧版 `~/.kimi/` 由 [`migration-legacy`](https://daiw.org/manual/kimi-code/monorepo-map) 迁移）。一个会话的目录：

```text
~/.kimi-code/sessions/<workspaceId>/<sessionId>/
├── state.json                  会话元数据（标题、fork 来源、agent 列表等）
└── agents/
    ├── main/wire.jsonl         主 agent 的事件日志（只追加）
    └── <agentId>/wire.jsonl    每个子代理一份
```

`workspaceId` 由工作目录算出（`src/_base/utils/workdir-slug.ts:17`）：

```ts
export function encodeWorkDirKey(workDir: string): string {
  const normalized = workDir.replace(/\\/g, '/').replace(/\/+$/, '');
  const base = normalized.split('/').pop() ?? normalized;
  const slug = slugifyWorkDirName(base);
  const hash = createHash('sha256').update(normalized).digest('hex').slice(0, HASH_LENGTH);
  return `${WORKDIR_KEY_PREFIX}${slug}_${hash}`;
}
```

结果形如 `wd_<目录名>_<12 位哈希>`：目录名方便人认，哈希保证唯一。同一项目被不同写法的路径打开时，`app/workspace/workspaceAlias.ts` 用 `workspaceRootKey()`（对 Windows 形态的路径做小写折叠）识别出同一根目录，把这些 key 归并起来。

## 会话索引

列出、检索会话不去扫目录，而是查 App 级的 `ISessionIndex`（`app/sessionIndex/`）：`listRecent`、`count`、`get`、`remove`，状态在 `uninitialized`、`preparing`、`ready`、`degraded` 之间切换。它的读模型放在 [minidb](https://daiw.org/manual/kimi-code/minidb) 里，v0.42 起常开。

## fork

`ISessionLifecycleService.fork()` 接收 `sourceSessionId`，以及可选的 `newSessionId`、`title`、`metadata`、`turnIndex`：

- 源会话有回合在跑时拒绝 fork，并对所有 agent 取得 quiescence，防止复制途中又有写入。
- 不带 `turnIndex`：整目录复制，每个 agent 的日志追加一条 forked 标记。
- 带 `turnIndex`：主 agent 的日志截到该回合，子代理的日志按对应时间截断，截空的子代理不带过去。
- 新会话标题默认是 `Fork: <原标题>`（原会话没有标题时用它的 ID），`state.json` 里记下 `forkedFrom`。

agent 级也能 fork（`IAgentLifecycleService.fork()`）。继承的历史若以未完成的工具调用结尾，会用 `closeTrailingOpenToolExchange()` 补上「这个调用在快照时仍在执行，结果未知」的结果，比[投影器](https://daiw.org/manual/kimi-code/context)那条通用的合成结果说得更准。

## 子代理、git 上下文与 hooks

- **子代理**：v1 的 `subagent-host.ts`、`subagent-batch.ts` 变成 Session 级的 `ISessionSubagentService`（`run`、`planSpawn`、`spawn`，外加 `onWillStartAgentTask` 钩子），并行派发在 `features/swarm/`。详见[子代理与 swarm](https://daiw.org/manual/kimi-code/subagents)。
- **git 上下文**：`session/agentLifecycle/profile/gitContext.ts` 的 `collectGitContext()` 采集 `origin` 地址、当前分支、`git status --porcelain` 与最近 3 条提交（5 秒超时，最多列 20 个改动文件），输出 `<git-context>` 块。它现在只作为 explore 子代理 profile 的 `promptPrefix`，不再由会话统一注入。
- **Hooks**：用户配置的 hooks 做成了一个 Feature（`features/externalHooks/`），在 App、Session、Agent 三级各贡献一个服务。详见 [Hooks](https://daiw.org/manual/kimi-code/hooks)。

## 切模型

`/model` 改的是 `IAgentProfileService` 里的模型别名（`setModel()`）。请求服务在每个回合第一次请求时，给模型配置拍一张快照（`llmRequesterService.ts:698`）：

```ts
  private getOrCreateTurnConfig(turnId: number): TurnRequestConfig {
    for (const id of this.turnConfigs.keys()) {
      if (id < turnId) this.turnConfigs.delete(id);
    }
    let snapshot = this.turnConfigs.get(turnId);
    if (snapshot === undefined) {
      snapshot = {
        resolved: this.profile.resolveModelContext(),
        params: this.profile.resolveRequestParams(),
        systemPrompt: this.profile.getSystemPrompt(),
      };
      this.turnConfigs.set(turnId, snapshot);
    }
    return snapshot;
  }
```

所以回合中途切模型，不会让同一回合的 step 前后用两个模型；新模型从下一个回合生效。

## prompt 的生命周期

v1 的 `PromptService` 依赖 `@moonshot-ai/protocol`；这个包已删除，prompt 的生命周期并入 `IAgentLoopService`：`submit()`（可带 `steerIfActive`）、`steer()`、`cancel()`、`promptHandle()`。状态（`loop.ts:144`）：

```ts
export type PromptState =
  | 'pending'
  | 'running'
  | 'steered'
  | 'completed'
  | 'failed'
  | 'cancelled'
  | 'blocked';
```

`steered` 表示这条排队的输入已并入正在跑的回合；`blocked` 来自 prompt gate，比如 hook 拦下了一次提交。ID 仍用 ULID：`newMessageId()` 返回 `msg_` 加一个 `ulid()`，按时间单调、天然可排序。状态变化以 `PromptSubmitted`、`PromptQueued`、`PromptSteered`、`PromptCompleted` 等事件经 `IEventDispatcher` 发出（`agent/prompt/promptEvents.ts`）；各级 scope 的状态键由脚本汇总在 `packages/agent-core-v2/docs/state-manifest.d.ts`。

## DI 与 Feature

v1 在 `services/` 下移植了 VS Code 的依赖注入；v2 把它扩展成 DI × Scope（`src/_base/di/`）。服务注册长这样（`llmRequesterService.ts:945`）：

```ts
registerScopedService(
  LifecycleScope.Agent,
  IAgentLLMRequesterService,
  AgentLLMRequesterService,
  ScopeActivation.OnScopeCreated,
  'llmRequester',
);
```

`createDecorator()` 造出服务标识，构造函数参数上的 `@IFoo` 声明依赖，`ScopeActivation` 决定 scope 一创建就实例化还是按需实例化。跨多级 scope 的功能写成 `Feature`，在一个类里把各级服务一起贡献出去（`features/externalHooks/externalHooksFeature.ts:16`）：

```ts
  constructor() {
    super();
    this.contributeService(
      LifecycleScope.App,
      IExternalHooksRunnerService,
      ExternalHooksRunnerService,
    );
    this.contributeService(
      LifecycleScope.Session,
      ISessionExternalHooksService,
      SessionExternalHooksService,
    );
    this.contributeAgentService(IAgentExternalHooksService, AgentExternalHooksService);
  }
```

Scope、Fiber 与贡献点的完整机制见 [agent-core-v2](https://daiw.org/manual/kimi-code/agent-core-v2)。

下一篇：[system prompt 组装](https://daiw.org/manual/kimi-code/prompt-system)。
