# bootstrap：把运行时拼起来

> bootstrap 是 Agent CLI 的装配层：配置分几层、怎样合并，主配置文件有哪些键，数据目录各放什么，一次 createZCodeApp 怎样造出 AgentRuntime、注入哪些端口，模型怎样选，会话怎样新建与恢复，各扩展机制在哪里接入，以及启动日志、日志保留与资源采样的数字。

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.net/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.net/manual/zcode/bootstrap-assembly
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

core 里的 `AgentRuntime` 只认端口（见[AgentRuntime：端口、依赖与方法装配](https://daiw.net/manual/zcode/agent-runtime)），不知道配置文件在哪、SQLite 库在哪、MCP 进程怎么起。把这些 Node 世界的事实装进一个运行时的，是 `apps/zcode-cli/packages/bootstrap`。不算 `zcode-protocol`、`zcode-protocol-v4` 两个协议目录，它有 122 个文件、约 2.7 万行，装配根只有一个函数 `createZCodeApp`（`apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:146`）。命令行的 `-p`、TUI、给桌面与 Web 用的 `app-server`，最后都调用它（`apps/zcode-cli/packages/cli/src/prompt-command.ts:212`、`apps/zcode-cli/packages/cli/src/tui-prompt-handler.ts:147`、`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-entrypoint.ts:251`）；调用一次得到一个会话的 `ZCodeApp`，里面有且只有一个主 `AgentRuntime`。

本篇按装配顺序走：先看 bootstrap 在包依赖里的位置，再看配置分层、主配置文件与数据目录，然后拆 `createZCodeApp`，最后是模型工厂、会话入口、各扩展的装配点和日志。协议层归[ZCode Protocol V4](https://daiw.net/manual/zcode/zcode-protocol)，命令行本身归[命令行入口、无头模式与打包](https://daiw.net/manual/zcode/cli-surface)。

## 在分层里的位置

```mermaid
flowchart TD
  CLI["@zcode/cli 入口与命令路由"] --> BOOT["@zcode/bootstrap 装配"]
  CLI --> TUI["@zcode/tui 终端界面"]
  BOOT --> CORE["@zcode/core AgentRuntime"]
  BOOT --> ADP["@zcode/adapters Node 实现"]
  BOOT --> PROV["@zcode/provider 与 provider-node"]
  BOOT --> TEL["@zcode/telemetry"]
  CORE --> CON["@zcode/contracts 端口与类型"]
  ADP --> CON
  TUI --> CON
```

core 与 adapters 是平级的两个包，都只依赖 contracts，互不引用（`apps/zcode-cli/packages/core/package.json:33`、`apps/zcode-cli/packages/adapters/package.json:101`）；bootstrap 是第一个同时看见两者的包（`apps/zcode-cli/packages/bootstrap/package.json:25`）。TUI 在工作区包里只依赖 contracts、i18n 与 shared（`apps/zcode-cli/packages/tui/package.json:21`），会话、模式、模型这些业务能力都由 cli 以回调形式注入给它（`apps/zcode-cli/packages/cli/src/tui-command.ts:59`）。这个方向是约定而非强制：cli 也直接从 core、adapters 取一些小工具，例如 `createNodeLoggerFactory`、`getRuntimeInfo`（`apps/zcode-cli/packages/cli/src/run.ts:2`），架构策略文件把整个 `apps/zcode-cli` 标成未托管的存量模块（`architecture-policy.yaml:55`）。

bootstrap 的公开面在 `apps/zcode-cli/packages/bootstrap/src/index.ts:3`：`createZCodeApp`、协议入口 `runZCodeProtocolAgent`、进程级 Provider Registry 启动函数、插件、技能、自定义命令、会话列表与钩子信任的命令行辅助函数，以及供 `--output-format stream-json` 复用的 `mapSessionEvent`（`bootstrap/src/index.ts:71`）；另有一个 `./v4-replay` 子路径导出（`bootstrap/package.json:13`）。adapters 的根入口重新导出 18 个模块（`apps/zcode-cli/packages/adapters/src/index.ts:2`），装配根 `create-app.ts` 则全部按子路径导入，例如 `@zcode/adapters/storage`、`@zcode/adapters/mcp`（`create-app.ts:2`）。`adapters/package.json:89` 还声明了一个 `./tools` 导出，`src` 下却没有这个目录，也找不到引用方。

## 配置：五层合并，外加一个会话层

作用域与优先级定义在 contracts（`apps/zcode-cli/packages/contracts/src/config/index.ts:168`）：

```ts
export const ConfigScopePriority: Record<ConfigScope, number> = {
  [ConfigScope.System]: 0,
  [ConfigScope.User]: 10,
  [ConfigScope.Project]: 20,
  [ConfigScope.Session]: 30,
  [ConfigScope.Env]: 40,
  [ConfigScope.Cli]: 50,
};
```

`createConfig`（`apps/zcode-cli/packages/adapters/src/config/config-factory.ts:129`）实际只装其中五层，按优先级从低到高：

1. **system**：`DefaultRuntimeConfig`（`contracts/src/config/index.ts:290`）。
2. **user**：`~/.zcode/cli/config.json`（`apps/zcode-cli/packages/adapters/src/config/file-config.adapter.ts:61`）。这个路径写死在 home 下，不随 `storage.dir` 移动。
3. **project**：从 cwd 向上找到含 `.git`（目录或文件都算）的那一层，再按“仓库根在前、cwd 在后”排列；没有 git 就只看 cwd（`packages/shared/src/workspace-hook-config.ts:335`）。每层目录依次取 `zcode.json` 与 `.zcode/config.json`（`workspace-hook-config.ts:174`），越深、越靠后的文件优先级越高。
4. **env**：只认一小撮 `ZCODE_*` 变量：`STORAGE_DIR`、`SESSION_DB_PATH`（或 `SESSION_DB`）、`HTTP_PROXY`、`NO_PROXY`、`AGENT_CA_CERT`、`HTTP_TIMEOUT`（或 `TIMEOUT`）、`LOG_FORMAT`、`MAX_TOOL_CONCURRENCY`（`apps/zcode-cli/packages/adapters/src/config/env-config.adapter.ts:26`）。
5. **cli**：`createConfigCliOverrides` 只投影四样东西：模式、工具允许清单、工具禁用清单、界面语言（`apps/zcode-cli/packages/bootstrap/src/app/app-config-options.ts:10`）。命令行上对应 `--mode`、`--disallowedTools`、`--locale`，允许清单只有协议的 session/create 会传。

```mermaid
flowchart LR
  S["system 默认值"] --> M["mergeConfigs 按优先级"]
  U["user：~/.zcode/cli/config.json"] --> M
  P["project：git 根到 cwd 的 zcode.json 与 .zcode/config.json"] --> M
  E["env：少量 ZCODE_* 变量"] --> M
  C["cli：模式、禁用工具、语言"] --> M
  M --> R["ConfigResult.config"]
  P -.->|"hooks 字段"| H["工作区钩子快照，等待信任"]
  R --> RT["resolveAppRuntimeConfig 叠加持久化模式、插件与内置 MCP"]
```

合并由 `mergeConfigs` 完成（`apps/zcode-cli/packages/adapters/src/config/config-merger.ts:26`），各段做浅层的对象合并，有几处特例：

- `plugins` 的启用表、市场与选项按键逐项合并，`dirs` 取并集；项目层写的 `extraKnownMarketplaces` 会被丢掉，市场只能配在用户层（`config-merger.ts:33`、`76`）。
- `hooks` 不整体覆盖：只有来源没写 `enabled: false` 时才把它的事件追加进来，合并后的 `enabled` 是“任一来源启用”（`config-merger.ts:170`）。项目文件里的 `hooks` 更是根本不进可执行配置，只留在一份不可变的快照里等待工作区信任（`apps/zcode-cli/packages/adapters/src/config/project-config.adapter.ts:122`），细节见[生命周期 Hooks 与工作区信任](https://daiw.net/manual/zcode/hooks)。
- MCP 服务器单独再解析一遍，规则是用户层压过项目层（`config-factory.ts:381`）：

```ts
  const servers: Record<string, McpServerConfig> = {};
  const sources: Record<string, McpServerConfigSource> = {};
  const apply = (source: McpServerConfigSource, patch: RuntimeConfigPatch | undefined) => {
    for (const [name, server] of Object.entries(patch?.mcp?.servers ?? {})) {
      servers[name] = server;
      sources[name] = source;
    }
  };

  apply("system", DefaultRuntimeConfig);
  // MCP server discovery has an extension-specific rule: user config shadows project config.
  // This does not change the global config precedence for model/permission/UI fields.
  apply("project", input.projectConfig);
  apply("user", input.userConfig);
  apply("env", input.envConfig);
  apply("cli", input.cliOverrides);
  return { servers, sources };
```

**session 层**在类型里有位置，运行时却不走 `ConfigPort`：`ConfigPortImpl.set()` 会把写入记为 `session` 作用域（`apps/zcode-cli/packages/adapters/src/config/index.ts:343`），但非测试代码里没有调用方；`createConfig` 返回的 `ConfigPort` 只用来算出 `config`（`config-factory.ts:255`），而且它把合并后的整份配置按 `system` 作用域写入（`adapters/src/config/index.ts:35`），`getSources()` 因此查不出真实来源。来源信息改由 `ConfigResult.sources` 携带：用户与项目文件是否加载、诊断、每个 MCP 服务器来自哪一层（`config-factory.ts:256`）。真正的会话级状态落在 SQLite：项目维度的权限模式（`setMode` 写入，`apps/zcode-cli/packages/bootstrap/src/app/session-facade.ts:423`）与会话维度的模型选择（`session-facade.ts:649`）。

以权限模式为例，最终取值的优先级是命令行、该项目上次持久化的模式、配置文件、默认值 `build`（`apps/zcode-cli/packages/bootstrap/src/app/runtime-config.ts:118`）：

```ts
  const runtimeConfig: AgentRuntimeConfig = {
    ...options.runtimeConfig,
    bashTimeoutPolicy:
      options.runtimeConfig?.bashTimeoutPolicy ??
      resolveBashTimeoutPolicy(options.env ?? process.env),
    mode: options.runtimeConfig?.mode ?? persistedMode ?? configResult.config.permission.mode,
    modelSelection: initialModelSelection,
    // 仅接受显式传入的会话级工具面（ZCode Protocol session/create 或 CLI
    // --allowed-tools/--disallowed-tools）。不要从 config.permission.allowedTools
    // 回落：那个键的既有语义是“免审批清单”，把它投影到注册面会让老配置里
    // 只写了几个 allowedTools 的用户突然丢失其余全部工具。
    toolAllowlist: options.runtimeConfig?.toolAllowlist,
    toolDisallowlist: options.runtimeConfig?.toolDisallowlist,
```

注释提到的 `--allowed-tools` 在命令行里并不存在，全局参数表只有 `--disallowed-tools` 与 `--disallowedTools`（`apps/zcode-cli/packages/cli/src/arguments.ts:125`）。坏配置的处理偏宽容：整份文件解析失败时这一层当作未加载，并记一条 `config_file_invalid` 诊断（`file-config.adapter.ts:110`）；单个 MCP 服务器不合法只跳过它自己（`apps/zcode-cli/packages/adapters/src/config/schema.ts:486`）；所有诊断在汇总处写成 warn 日志（`config-factory.ts:170`）。

## 主配置文件 config.json

文件结构由 `ZCodeConfigFileSchema` 定义（`schema.ts:286`），未知键原样放行（`schema.ts:306`）。顶层键、默认值与读取方：

| 键 | 默认值 | 谁读它 |
| --- | --- | --- |
| `permission` | `mode: "build"`，两份工具清单为空，两个风险开关为 `false` | `PermissionService`（`create-app.ts:342`）；`mode` 另见上文。schema 还接受 `auto`（`schema.ts:14`） |
| `modelStream.idleTimeoutMs` | 600000（`contracts/src/config/index.ts:284`） | 模型适配器的流空闲超时（`create-app.ts:537`） |
| `storage` | `dir: "~/.zcode"`，`sessionDbPath: "~/.zcode/cli/db/db.sqlite"`（`contracts/src/config/index.ts:302`） | 数据目录与会话库 |
| `network` | `timeout: 180000`（`contracts/src/config/index.ts:306`），代理与 CA 证书为空 | MCP、执行、WebFetch 与模型适配器 |
| `features` | 六个开关全为 `true`（`contracts/src/config/index.ts:308`） | `skill`、`mcp`、`subagent`、`memory` 分别决定对应端口或配置是否生效（`create-app.ts:746`、`runtime-config.ts:156`、`168`、`178`） |
| `memory.use` | `true` | 与 `features.memory` 一起决定项目记忆是否启用（`runtime-config.ts:183`、`apps/zcode-cli/packages/core/src/runtime/helpers/project-memory.ts:9`） |
| `mcp.servers` | 空 | 见[MCP](https://daiw.net/manual/zcode/mcp) |
| `plugins` | 启用，其余为空（`contracts/src/config/index.ts:322`） | 插件发现，见[插件与官方市场](https://daiw.net/manual/zcode/plugins) |
| `skills`、`skill`、`command` | `metadataBudget: 20000`（`contracts/src/config/index.ts:333`）；后两者按 `.md` 绝对路径开关单个技能或命令 | 技能与命令发现 |
| `hooks` | `enabled: false`，`timeoutMs: 60000`，`maxOutputBytes: 32768`（`contracts/src/config/index.ts:349`） | 七个事件（`schema.ts:273`），见[生命周期 Hooks](https://daiw.net/manual/zcode/hooks) |
| `ui` | `locale: "en-US"`，`theme: "auto"`（`contracts/src/config/index.ts:355`） | 界面语言与主题 |
| `toolConcurrency.maxConcurrency` | 10（`contracts/src/config/index.ts:343`） | 工具调度并发上限（`runtime-config.ts:146`） |
| `modelAnomalyGuard` | 两项默认值均为 3（`contracts/src/config/index.ts:345`） | 回合里的异常提醒 |
| `logging` | `level: "info"`，`format: "text"` | 只被解析，未找到读取方 |

`features.compact`、`features.rewind` 与 `skills.includeInstructions` 同样只在配置层解析与合并，非测试代码里找不到消费它们的地方；日志级别实际由运行环境决定（见下文）。模型与 Provider 不在这个文件里，而在 `~/.zcode/v2/provider_config.json`（`apps/zcode-cli/packages/cli/src/provider-runtime-env.ts:76`），由 Provider Registry 管理，见[Provider 规则、模型目录与选项映射](https://daiw.net/manual/zcode/provider-config)。这个文件还不存在时，旧版 CLI 写在 `config.json` 里的 `provider`、`model` 键会被导入并写成新文件（`apps/zcode-cli/packages/bootstrap/src/app/legacy-cli-personal-provider-config-importer.ts:129`、`packages/provider-node/src/personal-provider-config-repository.ts:128`）。

`.agents` 协议兼容写在 CLI 的 AGENTS.md 里（`apps/zcode-cli/AGENTS.md:80`），在代码里主要落在发现根上：技能与自定义命令在用户目录和项目各层都同时扫 `.zcode/` 与 `.agents/` 两套目录，同层 `.zcode` 优先（`apps/zcode-cli/packages/adapters/src/skills/roots.ts:94`、`apps/zcode-cli/packages/adapters/src/commands/roots.ts:99`），见[技能与自定义命令](https://daiw.net/manual/zcode/skills-commands)。

## 数据目录

`storage.dir` 默认 `~/.zcode`，bootstrap 在它下面取 `cli` 子目录作为 CLI 的存储根（`apps/zcode-cli/packages/bootstrap/src/app/paths.ts:5`，下表记作 `<cli>`）：

| 路径 | 用途 | 出处 |
| --- | --- | --- |
| `~/.zcode/cli/config.json` | 用户主配置 | `file-config.adapter.ts:61` |
| `~/.zcode/cli/db/db.sqlite` | 会话库，见[SQLite 会话库](https://daiw.net/manual/zcode/session-store) | `contracts/src/config/index.ts:303` |
| `~/.zcode/cli/log/` | 日志，可用 `ZCODE_LOG_DIR` 改 | `apps/zcode-cli/packages/adapters/src/logging/index.ts:222` |
| `<cli>/artifacts` 与 `image-cache`、`pdf-cache`、`video-cache` | 工具大结果、提示词附件与派生媒体 | `create-app.ts:349` |
| `<cli>/exec` | 命令执行的输出 | `create-app.ts:401` |
| `<cli>/rollout`，开发态为 `<cli>/debug` | 模型请求录制，见[遥测、调试与提示词轨迹](https://daiw.net/manual/zcode/telemetry-debug) | `paths.ts:13` |
| `<cli>/agents` | 子 Agent 的输出 | `create-app.ts:256` |
| `<cli>/plugins` | 插件缓存、数据与官方市场 | `paths.ts:9` |
| `<cli>/memories/projects/<slug>-<hash>/memory` | 项目记忆，见[项目记忆](https://daiw.net/manual/zcode/memory) | `apps/zcode-cli/packages/core/src/memory/project-root.ts:23` |
| `~/.zcode/agents/` | 用户自定义子 Agent | `apps/zcode-cli/packages/bootstrap/src/subagents.ts:55` |
| `~/.zcode/v2/` | 个人 Provider 配置、账号凭据 `credentials.json`、子 Agent 状态 | `provider-runtime-env.ts:76`、`apps/zcode-cli/packages/adapters/src/auth/shared-credentials.ts:289`、`subagents.ts:53` |
| `~/.zcode/security/workspace-hook-trust-v1.json` | 工作区钩子信任记录 | `apps/zcode-cli/packages/adapters/src/storage/workspace-hook-trust-store.ts:20` |
| `~/.zcode/mailbox` | 会话邮箱，只在 `ZCODE_MESSAGE_ENABLED` 为 `1` 或 `true` 时启用 | `create-app.ts:358` |
| `~/.zcode/cache/runtime_tools/` | SEA 单文件版解出的原生搜索工具 | `apps/zcode-cli/packages/cli/src/sea-runtime-tools.ts:96` |

几个位置并不跟着 `storage.dir` 走：配置文件、日志目录与会话库默认值都是写死的 `~/.zcode/cli/...`，`v2` 下的 Provider 配置与凭据取 `ZCODE_DATA_BASE_DIR` 或 home。beta 版会把 `ZCODE_STORAGE_DIR` 设为 `~/.zcode-beta`（`apps/zcode-cli/packages/cli/src/env.ts:133`），从代码看，它与正式版仍共用同一份配置文件、日志目录和会话库。

## createZCodeApp：一次装配

```mermaid
flowchart TD
  A["createConfig 五层配置"] --> B["日志工厂与 StartupTimer"]
  B --> C["子 Agent 定义与插件发现"]
  C --> D["内置 node_repl MCP"]
  D --> E["打开 SQLite 并迁移"]
  E --> F["resolveAppRuntimeConfig"]
  F --> G["钩子信任、权限、存储与各 I/O 适配器"]
  G --> H["模型适配器与 ApiProviderModelRuntime"]
  H --> I["脚本工作流、动态工作流与模型目录端口"]
  I --> J["new AgentRuntime"]
  J --> K["输入、工作流、会话三组门面"]
  K --> L["返回 ZCodeApp"]
```

入口先要求调用方给出 Provider Registry，没有就直接抛错（`create-app.ts:147`）；会话 id 缺省时新建一个，trace 上下文以它为根（`create-app.ts:152`）；工作目录取 `runtimeConfig.workingDirectory` 或进程 cwd（`create-app.ts:154`）。随后的顺序与依赖关系是：

1. 配置与日志：`createConfig` 之后先解析界面语言（`app-config-options.ts:37`），再建日志工厂和启动计时器（`create-app.ts:167`、`172`）。
2. 扩展发现：用户与项目的子 Agent 定义（`create-app.ts:208`）、插件（`create-app.ts:214`）、插件自带的子 Agent（`create-app.ts:223`）、由插件推导出的运行时特性与内置 `node_repl`（`create-app.ts:229`、`230`）。
3. 存储：未注入会话库时打开 SQLite，启动时跑完迁移（`apps/zcode-cli/packages/bootstrap/src/app/session-store.ts:74`）；接着读出该项目上次的权限模式（`create-app.ts:241`）。
4. 运行时配置：`resolveAppRuntimeConfig` 把配置、持久化模式、插件钩子与 MCP、子 Agent 定义合成一份 `AgentRuntimeConfig`（`create-app.ts:244`）。宿主注入了浏览器控制端口且 Browser Use 已启用时，再给 `node_repl` 接上浏览器 broker（`create-app.ts:262`）。
5. 适配器：工作区钩子安全、权限服务、产物库、邮箱、MCP、执行、PDF、文件系统、HTTP（`create-app.ts:297` 至 `417`）。每个都允许调用方注入替身，否则用 Node 实现。
6. 模型与工作流：模型适配器与模型工厂（见下节），再用同一个工厂装三条工作流子运行时：脚本工作流桥（`create-app.ts:557`）、动态工作流运行服务（只有会话库带 `dwf_*` 表时才构造，`create-app.ts:585`）、片段试跑服务与模型目录端口（`create-app.ts:716`、`722`）。
7. 运行时与门面：`new AgentRuntime(sessionId, runtimeConfig, deps)`（`create-app.ts:726`），然后是输入、工作流、会话三组门面（`create-app.ts:791`、`826`、`849`），最后把它们与附件读写、动态工作流读面平铺成一个 `ZCodeApp` 返回（`create-app.ts:924`）。`ZCodeApp` 接口有 101 个成员（`apps/zcode-cli/packages/bootstrap/src/app/types.ts:291`），调用方主要和它打交道，只在订阅事件这类场合才伸手去拿 `app.runtime`。

注入 `AgentRuntime` 的依赖：

| 依赖 | 来源 |
| --- | --- |
| `eventStore`、`sessionStore` | 内存事件库（`create-app.ts:730`）；SQLite 会话库 |
| `executionPort`、`fileSystemPort`、`httpClientPort` | Node 执行适配器，输出目录 `<cli>/exec`；文件系统；WebFetch 客户端，超时取 `network.timeout`（`create-app.ts:392`、`408`、`409`） |
| `imageProcessorPort`、`pdfDocumentPort`、`artifactStore` | Jimp、Poppler、工具产物库（`create-app.ts:357`、`405`、`349`） |
| `contextSourcePort`、`skillPort`、`mcpPort` | 上下文源；技能端口仅在 `features.skill` 与 `skills.enabled` 同时为真时存在；MCP 关闭时为空（`create-app.ts:743`、`745`、`375`） |
| `modelFactory`、`modelIoDir`、`modelRequestAdmission` | 见下节；录制目录；进程级并发治理器的 observer（`create-app.ts:729`） |
| `permissionService`、`permissionBroker` | 规则来自 `permission.*`；审批代理由调用方给出，缺省时 core 退回一律拒绝的实现（`apps/zcode-cli/packages/core/src/runtime/agent-runtime.ts:249`） |
| `workflowPort`、`dynamicWorkflowRunPort`、`dynamicWorkflowSnippetPort`、`modelCatalogPort` | 第 6 步的四个端口（`create-app.ts:771`） |
| `workspaceHookAdmission`、`workspaceHookSnapshot`、`sessionMailboxPort`、`browserControlPort` 等 | 钩子信任、邮箱与宿主能力 |

工具注册表、执行器、调度器、钩子运行器、子 Agent 端口与任务注册表都不由 bootstrap 注入，由 `AgentRuntime` 构造时自建；构造函数最后一行就启动 MCP 连接（`agent-runtime.ts:250`、`307`）。全仓非测试代码里 `new AgentRuntime` 只有四处：这里、专家工作流的子运行时（`apps/zcode-cli/packages/bootstrap/src/app/workflow-facade.ts:283`）、脚本工作流与动态工作流 actor 的子运行时（`apps/zcode-cli/packages/bootstrap/src/app/script-workflow-child-runtime.ts:111`），以及 core 里的子 Agent（`apps/zcode-cli/packages/core/src/runtime/methods/subagent.ts:239`）。装配中途任何一步抛错，已创建的模型运行时、遥测和 broker 都会被释放，并记一条启动失败日志后重新抛出（`create-app.ts:1281`）。

## 模型工厂

`createZCodeApp` 需要的 Provider Registry 由调用方先用 `startProcessProviderRegistryRuntime` 起好，一个进程一份（`apps/zcode-cli/packages/bootstrap/src/app/process-provider-registry-runtime.ts:45`）：`-p` 与 TUI 以独立模式启动，自己持有账号凭据并负责旧配置导入（`process-provider-registry-runtime.ts:35`）；协议模式下账号配置由宿主推送（`process-provider-registry-runtime.ts:150`）。`createModelAdapter` 只是 `AiSdkModelAdapter` 的构造包装（`apps/zcode-cli/packages/bootstrap/src/model-factory.ts:23`），参数里带流空闲超时、录制目录与状态汇；默认请求头由 `createRuntimeAiSdkModelExecutionConfig` 生成，包括 `User-Agent: ZCode/<版本>` 和标明来源的 `X-Title`，协议模式下来源记为 `electron`，其余为 `cli`（`apps/zcode-cli/packages/bootstrap/src/model-config.ts:47`、`75`）。适配器内部怎样对接各家 API，见[模型适配层](https://daiw.net/manual/zcode/model-adapters)。

交给 `AgentRuntime` 的 `modelFactory` 来自 `ApiProviderModelRuntime`（`apps/zcode-cli/packages/bootstrap/src/app/provider-registry-model-runtime.ts:43`）：

```ts
  readonly modelFactory: RuntimeModelFactory = (target): Model => {
    if (!this.#started) throw new Error("ApiProviderModelRuntime 必须先 start() 再创建 Model");
    const validation = this.#registry.validateSelection(target.selection);
    if (!validation.ok) throw createRegistrySelectionProtocolError(validation);
    const providerId = target.selection.providerId;
    const modelId = target.selection.modelId;
    const provider = this.#registry.getProvider(providerId);
    if (!provider) throw new Error("Registry Selection 校验与 Provider 索引结果不一致");
    const registryModel = this.#registry.getModel(providerId, modelId);
    if (!registryModel) throw new Error("Registry Selection 校验与 Model 索引结果不一致");
    return this.#createRegistryModel(provider, registryModel, target);
  };
```

它不做任何“缺省修复”：选择必须在 Registry 里完整有效，推理强度直接取自选择本身（`provider-registry-model-runtime.ts:73`），闲时账号额外带上请求鉴权来源（`provider-registry-model-runtime.ts:79`）。每次创建模型都现查 Registry 的当前视图，所以主回合、脚本工作流、动态工作流 actor 与专家工作流共用这一个工厂，Registry 刷新后新建的模型立即可见（`create-app.ts:553`）。

“用哪个模型”在进工厂之前就定好了。新会话依次取调用方显式给的选择、仍然可选的环境默认选择、按 Registry 顺序找到的第一个可见且能补全选项的模型，后者的推理档取最高一档（`runtime-config.ts:212`、`packages/provider/src/model-selection-config.ts:28`）；恢复会话不套用任何默认值，失效的选择宁可让运行时保持未绑定（`runtime-config.ts:64`）。账号与 Coding Plan 怎样影响 Registry，见[账号、Coding Plan 与闲时计划](https://daiw.net/manual/zcode/accounts-plans)。

## 新建、继续与恢复

命令行入口先把请求翻译成一个会话 id（`apps/zcode-cli/packages/cli/src/resume.ts:6`）：`--resume <id>` 原样使用；`-c` 调用 `resolveLatestSession`，在会话库里按工作目录取最近一个根会话（`apps/zcode-cli/packages/bootstrap/src/sessions.ts:7`），找不到就报 `No resumable session found`（`resume.ts:27`）；都没有则留空，由 `createZCodeApp` 新建。有 id 时调用方同时传 `resume: true`（`apps/zcode-cli/packages/cli/src/prompt-command.ts:227`）。

恢复是惰性的，发生在“第一次真实用户执行”的边界上（`create-app.ts:502`）：

```ts
    const prepareResume = async (
      submitTraceContext?: TraceContext,
      abortSignal?: AbortSignal,
    ): Promise<void> => {
      if (!options.resume || resumePrepared) return;
      await resumeFromStore({
        ...(abortSignal ? { abortSignal } : {}),
        traceContext: submitTraceContext ?? traceContext,
      });
      resumePrepared = true;
    };

    const prepareUserExecutionBoundary: PrepareUserExecutionBoundary = async (boundaryOptions) => {
      // Bash shell 快照属于“首次真实用户执行”边界，而不是 chat
      // input 独有状态。普通 prompt、expert workflow、script workflow 都可能
      // 作为新 session 的第一个模型/子 agent 入口，必须统一在 resume/context
      // 初始化前落定一次，避免模型看到的 Shell 与 Bash 执行 shell 分叉。
      await initializeSessionShellEnvironment();
      await prepareResume(boundaryOptions?.traceContext, boundaryOptions?.abortSignal);
    };
```

`resumeFromStore`（`create-app.ts:476`）先从会话条目里读回该会话最后一次显式的模型选择，只有通过 Registry 校验才绑定（`create-app.ts:450`），再初始化 Shell 环境，调用 `runtime.resumeFromStore` 重建内存历史；只有调用方显式给了模式才作为覆盖传入，否则恢复会话自己持久化的模式（`create-app.ts:488`）；最后重新激活暂停的目标（`create-app.ts:494`）。库里怎样重建历史，见[SQLite 会话库](https://daiw.net/manual/zcode/session-store)。TUI 的 `/resume` 在换上新 App 后会立即显式调用 `app.resume`，不等第一条输入（`apps/zcode-cli/packages/cli/src/command-center/create.ts:346`）。

TUI 的 `/new`、`/resume`、`/fork` 都是换一个 `ZCodeApp`：新建一个，关掉旧的，重挂事件订阅（`apps/zcode-cli/packages/cli/src/tui-prompt-handler.ts:118`）；Provider Registry 与遥测是进程级句柄，跨 App 复用（`apps/zcode-cli/packages/cli/src/tui-prompt-handler-runtime.ts:15`）。协议服务端则把 `createZCodeApp` 包进 `ProtocolRuntimeResources`，按会话创建与托管（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server.ts:234`）。关闭一个 App 时，先停止新调度并等待记忆抽取最多 60 秒，再停下本会话的动态工作流，然后并行关闭浏览器会话、执行、MCP 与 broker，每项最多 6 秒，最后关会话库（`session-facade.ts:264`、`82`、`573`）。

## 扩展机制的装配点

| 机制 | 装配点 | 进入运行时的方式 |
| --- | --- | --- |
| [技能](https://daiw.net/manual/zcode/skills-commands) | `createNodeSkillAdapter`：额外根取 `skills.roots` 与插件技能根，禁用路径合并按路径的开关与动态工作流灰度（`create-app.ts:745`） | `skillPort` |
| [自定义命令](https://daiw.net/manual/zcode/skills-commands) | 输入门面的 `customCommandPromptResolver`：先查内置的 `/init`，再展开用户与插件命令（`create-app.ts:793`） | 提交前改写提示词 |
| [插件](https://daiw.net/manual/zcode/plugins) | `resolveStartupPlugins` 先播种官方插件再发现（`apps/zcode-cli/packages/bootstrap/src/app/startup-marks.ts:11`）；插件引用目录在创建时冻结一次（`create-app.ts:292`） | 技能根、命令根、MCP、钩子、子 Agent |
| [MCP](https://daiw.net/manual/zcode/mcp) | 插件 MCP、配置 MCP、内置 MCP 依次覆盖，内置的放最后以防同名劫持；项目 MCP 默认可信（`runtime-config.ts:90`、`109`） | `runtimeConfig.mcp` 与 `mcpPort` |
| [子 Agent](https://daiw.net/manual/zcode/subagents) | `~/.zcode/agents` 与 `.zcode/agents` 下的定义加插件定义（`subagents.ts:54`、`create-app.ts:235`） | `runtimeConfig.subagents`，子运行时由 core 自建 |
| [钩子](https://daiw.net/manual/zcode/hooks) | 用户层钩子随配置合并，项目层只进信任快照；插件钩子再追加（`runtime-config.ts:160`、`245`） | `runtimeConfig.hooks` 与工作区钩子准入 |
| [node_repl](https://daiw.net/manual/zcode/node-repl-browser) | Browser Use 或 Computer Use 插件启用、且 `node-repl-host` 在场时注册名为 `node_repl` 的 stdio MCP，超时 600000 毫秒（`apps/zcode-cli/packages/bootstrap/src/app/built-in-node-repl.ts:16`、`42`） | 作为内置 MCP 服务器 |
| [动态工作流](https://daiw.net/manual/zcode/dwf-tools)、[专家工作流](https://daiw.net/manual/zcode/expert-workflow) | 第 6 步的端口与 `createWorkflowFacade`（`create-app.ts:826`） | 四个工作流端口与 `ZCodeApp` 上的工作流方法 |

钩子这一行有个值得留意的细节：只要有启用的插件带了钩子，`mergeRuntimeHooks` 就把 `enabled` 直接置为 `true`（`runtime-config.ts:264`）。从代码看，用户配置里写了事件、却没写 `hooks.enabled: true` 的钩子，会因此跟着生效，因为合并阶段已经把它们的事件收了进来（`config-merger.ts:181`）。

## 日志、启动日志与保留

日志工厂在 `apps/zcode-cli/packages/adapters/src/logging/index.ts:170`：

- 每行一条 JSON，同步追加到 `zcode-YYYY-MM-DD.jsonl`（按本地日期，`logging/index.ts:247`），写失败一律忽略，日志永不打断 Agent（`logging/index.ts:129`）。
- 级别由运行环境决定：`ZCODE_RUNTIME_ENV` 为 `development`，或入口是 `packages/cli/src` 下的 `.ts` 源码时为 debug，否则为 info（`logging/index.ts:226`）；配置里的 `logging.level` 不参与。
- `ZCODE_LOG_CONSOLE=1` 时同时镜像到 stderr（`logging/index.ts:177`）。脱敏与字段约定见[遥测、调试与提示词轨迹](https://daiw.net/manual/zcode/telemetry-debug)。

保留策略的数字在 `apps/zcode-cli/packages/adapters/src/logging/retention.ts:6`：保留 7 天，启动后 60 秒才清理一次；截止日期是“今天减 6 天”，于是今天加前 6 天共 7 个文件留下（`retention.ts:132`），只删匹配文件名格式的文件（`retention.ts:9`）。清理由 `createZCodeApp` 收尾时和协议入口启动完成时安排（`create-app.ts:790`、`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-entrypoint.ts:345`），同一个工厂只安排一次（`logging/index.ts:206`）。定时器是 unref 的（`retention.ts:120`），一次不到 60 秒就结束的 `-p` 通常等不到它，实际的清理多发生在 TUI 与 `app-server` 这类长进程里。

启动日志由 `StartupTimer` 写（`apps/zcode-cli/packages/bootstrap/src/startup-logging.ts:10`），每条带距上一条的 `durationMs` 与累计的 `totalDurationMs`：

| 事件 | 阶段 |
| --- | --- |
| `bootstrap.app.startup.started` | `start`（`startup-marks.ts:65`） |
| `bootstrap.app.startup.config.completed` | `load_config`（`startup-marks.ts:79`） |
| `bootstrap.app.startup.plugins.completed` | `resolve_plugins`，带插件、技能根、钩子、MCP 计数（`startup-marks.ts:39`） |
| `bootstrap.app.startup.sqlite_migration.started` 与 `.completed` | `migrate_session_db`（`session-store.ts:79`） |
| `bootstrap.app.startup.runtime_config.completed` | `resolve_runtime_config`（`create-app.ts:287`） |
| `bootstrap.app.startup.storage.completed`、`.mcp.completed` | `initialize_storage`、`initialize_mcp`（`startup-marks.ts:97`、`116`） |
| `bootstrap.app.startup.runtime.completed`、`.completed` | `create_runtime`、`total`（`startup-marks.ts:133`、`148`） |

失败时记 `bootstrap.app.startup.failed`（`create-app.ts:1285`）。协议入口另有一套 `zcode_protocol.startup.*`（`zcode-protocol-entrypoint.ts:118`）。冷启动慢时，按 `durationMs` 找出最大的一段即可。

## 进程资源采样

`createZCodeProcessResourceSampler`（`apps/zcode-cli/packages/bootstrap/src/process-resource-sampler.ts:100`）每 60 秒采一次（`packages/shared/src/processResourceTelemetry.ts:29`），第一次只记基线（`process-resource-sampler.ts:133`）。样本含平台、架构、逻辑 CPU 数（截在 1 到 4096，`process-resource-sampler.ts:105`）、区间内的 CPU 核数与百分比、RSS、已用堆、运行分钟数与物理内存，CPU 与内存读数保留 4 位小数；进程实例标识是首次采样时生成的 8 字节随机串，刻意不用 pid（`process-resource-sampler.ts:20`）。读数或上报出错只丢掉当前样本（`process-resource-sampler.ts:167`）。

只有协议模式会启动它（`zcode-protocol-entrypoint.ts:336`）：每个样本作为通知发给宿主，同一节拍顺带重平衡常驻会话、修剪事件缓存，并按变化门控写一条本地内存诊断日志（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/resource-sampler.ts:25`）。`-p` 与 TUI 不采样。

下一篇：[输入受理、命令队列与引导](https://daiw.net/manual/zcode/prompt-admission)——运行时装好之后，一条输入怎样被受理；忙碌时的新输入又怎样排队，或作为引导插进当前回合。
