# 进程内入口：CliService 与应用层

> TUI、exec 与 ACP 共用一个进程内运行时：启动顺序与服务组装，CliService 的 86 个方法如何分派到三组应用对象，以及源码边界在代码里的落点。

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

MiniMax Code 没有常驻守护进程，运行时也不对外提供 HTTP 接口。每个 `mcode` 进程——交互式 TUI、`mcode exec`、`mcode acp`，连 `mcode provider`、`mcode plugin` 这类管理子命令——都在自己的进程里拼出一整套运行时，再用普通的方法调用去用它。`docs/architecture.md` 把这条链写成一行：

> TUI / exec / ACP → CliService → local Applications → Session / Turn / Agent services → Pi / model providers / local tools

本篇讲链条的前半段：运行时怎样组装，`CliService` 暴露什么、转给谁，以及文档强调的“不实例化 DesktopService、HTTP 前门、云端交接”在代码里靠什么守住。作为对照，OpenCode 的各个前端都经同一套 HTTP API 与内核交谈，见[HTTP 服务端](https://daiw.org/manual/opencode/server)。

## 怎么用

所有入口最后都调用 `packages/tui/src/runtime/lifecycle.ts:137` 的 `createTuiRuntime`，区别只在 `surface`：

| 入口 | 创建运行时的位置 | `surface` |
| --- | --- | --- |
| `mcode [prompt]` | `packages/tui/src/tui/launcher.ts:326` | `tui` |
| `mcode exec` | `packages/tui/src/cli/run-exec-command.ts:74` | `headless` |
| `mcode acp` | `packages/tui/src/cli/run-acp-command.ts:64` | `acp` |
| `mcode provider`、`mcode plugin` 的子命令 | `packages/tui/src/cli/provider-command.ts:164`、`packages/tui/src/cli/plugin-command.ts:177` | `headless` |

`surface` 带来几处行为差异：

- `headless` 与 `acp` 会设 `startupExecutionPolicy: 'quarantined'`（`lifecycle.ts:455`），运行时据此不恢复上一个进程遗留的后台作业与持久化执行状态（`packages/local-runtime-v2/src/runtime.ts:831`）；判断很简单，只有 `quarantined` 会关掉这一项（`packages/local-runtime/src/runtime/startup-execution-policy.ts:10`）。
- 非 `headless` 才打开 `questionnaireReply`、`permissionPrompt`、`elicitation` 三项交互能力（`lifecycle.ts:450`）。
- TUI 不等运行时就绪就先画界面：`createDeferredTuiRuntime`（`packages/tui/src/runtime/deferred.ts:11`）返回一个 Proxy，所有调用先 `await` 运行时的 Promise；输入框在就绪前 `disableSubmit`（`launcher.ts:457`），就绪后才放开（`launcher.ts:527`）。

几个可观察的开关与数字：

- 环境变量 `MAVIS_LOCAL_RUNTIME_V2_FORCE_LEGACY=1` 不会回退到旧运行时，而是直接报错“legacy fallback is disabled”（`packages/tui/src/runtime/embedded-host.ts:65`）。
- 内嵌宿主会改写几项配置上限：`memory.enabled` 恒为 `false`，`review.mode` 在用户没显式设置时取 `inline`（`embedded-host.ts:35`）。
- 每次启动写一行 `local-runtime-v2 startup timings` 日志（`local-runtime-v2/src/runtime.ts:585`），分阶段记录耗时，冷启动慢时可以对照下一节的阶段表。
- 退出时整张关闭图的外层上限是 60 秒（`DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS`，`lifecycle.ts:73`）。

## 启动：从 launchTui 到 CliService

```mermaid
flowchart TD
  A["launchTui、exec、acp 入口"] --> B["createTuiRuntime"]
  B --> C["createEmbeddedRuntimeHost"]
  C --> D["createLocalRuntimeHostV2"]
  D --> E["布局迁移、打开 SQLite、跑 schema 迁移"]
  E --> F["createRuntimeAgentComposition"]
  F --> G["第一代宿主 createLocalRuntimeHostV1"]
  G --> H["initializeOwnerRuntime"]
  H --> I["createRuntimeServices"]
  I --> J["createCliService"]
  J --> K["TuiRuntimeAdapter"]
```

`createTuiRuntime` 先处理账号（共享 OAuth 租约、账号客户端）与日志，再调用 `createEmbeddedRuntimeHost`。后者给宿主定死几项身份：`runtimeOwnerKind: 'tui'`、`capabilityProfile: 'cli'`、`capabilities.cliEmbedded: true`（`embedded-host.ts:84`）。这几项决定了能不能拿到 `CliService`，判断在 `packages/local-runtime-v2/src/runtime.ts:526`：

```ts
function isV2RuntimeOwner(options: CreateLocalRuntimeHostOptions): boolean {
  return (
    (options.runtimeOwnerKind === "electron" &&
      options.capabilities?.electronHost === true) ||
    ((options.runtimeOwnerKind === "cli" ||
      options.runtimeOwnerKind === "tui") &&
      options.capabilities?.cliEmbedded === true)
  );
}
```

不是 v2 owner 的宿主拿不到 `cliService`，内嵌宿主会直接抛出“does not expose CliService”（`embedded-host.ts:98`）。`createLocalRuntimeHostV2Internal`（`local-runtime-v2/src/runtime.ts:120`）按固定顺序启动，每段都记进前面那行耗时日志：

| 日志字段 | 做什么 | 位置 |
| --- | --- | --- |
| `layout_migration_ms` | 把数据目录布局迁到 v2，必须在打开 v2 数据库前完成 | `local-runtime-v2/src/runtime.ts:174` |
| `db_open_ms` | 打开 `<dataDir>/v2/sqlite/runtime-state.sqlite` | `local-runtime-v2/src/runtime.ts:180`；路径常量见 `packages/local-runtime-v2/src/infra/db/client.ts:48` |
| `db_initialize_ms` | 跑 schema 迁移，顺带报告修复过的历史会话与项目数据 | `local-runtime-v2/src/runtime.ts:191` |
| `agent_composition_ms` | Agent 定义与遗留 Agent 的导入 | `local-runtime-v2/src/runtime.ts:208` |
| `v1_host_ready_ms` | 创建并等待第一代宿主 | `local-runtime-v2/src/runtime.ts:254`、`262` |
| `owner_runtime_ms` | 后台设施加全部 v2 服务 | `local-runtime-v2/src/runtime.ts:269` |
| `channel_startup_ms` | IM 渠道子系统；对 v2 owner 它脱离启动关键路径 | `local-runtime-v2/src/runtime.ts:321` |

有两点值得注意。第一，v2 运行时先建了一个第一代宿主（`@mavis/local-runtime`），用来提供数据库、文件工具、权限、存储这些被复用的设施；v2 通过兼容层给它配上 `disableLegacyConversation: true`、`cronConsumerEnabled: false`（`packages/local-runtime-v2/src/compat/v1/runtime.ts:436`），旧的对话执行路径因此停用，细节见[第一代运行时与 OpenCode 遗产](https://daiw.org/manual/minimax-code/runtime-v1)。第二，启动路径上曾有一个整目录快照检查点，2026-09-03 被移除，注释给出的理由很具体（`local-runtime-v2/src/runtime.ts:163`）：

> a 17G dataDir starved the 600s bootstrap watchdog six times in a row

`initializeOwnerRuntime`（`local-runtime-v2/src/runtime.ts:806`）先建后台设施 `createBackgroundRuntime`，其中调度器只给 Electron 宿主（`enableScheduler: electronOwner`，`local-runtime-v2/src/runtime.ts:842`），再调用 `createRuntimeServices`，最后依次 `background.start()` 与 `services.ready()`（`local-runtime-v2/src/runtime.ts:887`）。一切就绪后才调用 `createCliService(ownerRuntime.services)`（`local-runtime-v2/src/runtime.ts:299`）。关闭时大致反过来：先停第一代宿主上的 cron 与渠道生产者，再停后台、关服务、关后台设施，然后是 Agent owner 与第一代宿主，最后关数据库（`local-runtime-v2/src/runtime.ts:928`）。

## createRuntimeServices 的组装顺序

`packages/local-runtime-v2/src/services.ts:395` 的 `createRuntimeServices` 是整个 v2 的组装根，按下面的顺序建各个 owner：

1. Browser Use 组合（`services.ts:403`）。`prepareRuntimeServices`（`services.ts:346`）预建的技能启用状态、IM 凭据迁移回执与 CLI 下线提示，在生产环境里早在第一代宿主之前就建好了（`local-runtime-v2/src/runtime.ts:229`），因为第一代宿主也要用。
2. 沙箱、LLM 上下文检查器（开发用，构建可隐藏）、全局事件写入器、工作区（Git 与 HTML 预览）（`services.ts:409`–`418`）。
3. 回合能力生命周期、内容安全、全局指令、提示词支持（`services.ts:419`–`425`）。
4. `createProductionSessionComposition`：Agent 产品能力、模型系统、会话系统、置顶服务一次建好（`services.ts:436`）。
5. 画布，以及绑定提示词回滚的一组 owner（`services.ts:474`–`475`）：可选的渠道系统、查询折叠键、消息投递、回合生命周期事件，再进入 `initializeRuntimeServiceOwners`，依次是 MCP（`services.ts:649`）、插件（`656`）、MiniApp（`671`）、Plan（`690`）、Goal 校验（`717`）、回合系统（`725`）、会话应用（`741`）、Cron（`747`）。
6. 用户侧应用：`ConversationApplication` 与技能应用（`services.ts:504`），再合成进程内产品应用（`services.ts:513`）。
7. 服务生命周期、插件钩子、提示词生命周期收尾（`services.ts:543`–`569`）。

Cron 与渠道只在 `ownsElectronRuntimeCapabilities` 为真时创建（`services.ts:492`、`498`），它对 `tui` 返回假（`packages/local-runtime-v2/src/application/agent/runtime-browser-use-composition.ts:39`），所以命令行进程里没有这两个 owner。互相依赖的 owner 靠闭包延迟绑定：Plan 先于回合系统创建，它的 `dispatchQueue` 写成 `requireTurnSystem(turnSystem).turns.dispatchQueue(sessionId)`（`services.ts:700`），真正调用时回合系统早已就位。

各服务目录的文件名有固定分工：

- `initialize.ts` 只建本服务的 owner，例如 `packages/local-runtime-v2/src/service/content-safety/initialize.ts:6` 的 `initializeContentSafetyService`、`packages/local-runtime-v2/src/service/turn-system/initialize.ts:39` 的 `initializeTurnSystem`。
- `composition.ts` 把几个 owner 或兼容端口拼成一个能力，例如 `packages/local-runtime-v2/src/service/agent/composition.ts:182` 的 `createRuntimeAgentComposition`、`packages/local-runtime-v2/src/service/v1-conversation-compat/composition.ts:42` 的 `composeV1Conversation`。
- `production-composition.ts` 全仓只有回合系统一份，负责把原生 AgentHost 接到产品端口上（`packages/local-runtime-v2/src/service/turn-system/production-composition.ts:338` 的 `createLocalAgentHost`），见[回合系统](https://daiw.org/manual/minimax-code/turn-system)。

## CliService：一层很薄的门面

`CliService` 在 `packages/local-runtime-v2/src/local/cli-service.ts:97` 定义，构造参数就是三组应用对象：

```ts
export interface CliServiceOptions {
  readonly applications: RuntimeApplications;
  readonly application: LocalRuntimeApplication;
  readonly conversation: ConversationApplication;
}

export type CliSendMessageReq = ConversationSendMessageRequest;
export type CliSendMessageOptions = ConversationSendOptions;

/** In-process CLI entry point: call local Application directly, without HTTP or RPC envelopes. */
export class CliService {
  constructor(private readonly options: CliServiceOptions) {}
```

`createCliService`（`packages/local-runtime-v2/src/local/app.ts:4`）直接收下整个 `RuntimeServices`，因为后者恰好有 `conversation`、`applications`、`application` 三个字段（`services.ts:242`）。类里 86 个公开方法几乎都是一行转发，按去向可分成：

| 方法 | 转给 |
| --- | --- |
| `listSessions`、`getSession`、`getSessionTree` | `applications.session.query` |
| `createSession`、`updateSession`、`archiveSession`、`deleteSession` | `applications.session.lifecycle` |
| `getMessages`、`listSessionInputSummaries`、`getSessionUsage`、`getPeekContext` | `applications.session.content` |
| `forkSession`、`rewindSession`、`editSessionMessage` 及其预览 | `applications.session.conversationMutation` |
| `listQueueMessages`、`updateQueueItem`、`reorderQueue`、`deleteQueueItem` | `applications.queue` |
| `sendMessage`、`resumeSession`、`abortSession`、`steer`、`steerSession`、`enqueueMessage`、`requestCompaction` | `conversation` |
| 问卷、权限、Goal、工作区、账号、诊断、插件、模型与提供方、技能、后台任务、MCP、事件流 | `application` |

`createSession` 有一处小分支：请求带了非空 `workspaceDir` 时走 `createExplicitWorkspaceSession`，否则走生命周期应用的 `createSession`（`cli-service.ts:121`），TUI 总会带上当前目录。可选能力统一经 `requireCapability` 取用，缺失时抛出“Runtime does not expose … Application.”（`cli-service.ts:869`）。同一个类因此可以挂在能力多寡不同的宿主上，缺哪项能力，就在调用那一刻报错。

## 应用层：application 下的四个目录

`packages/local-runtime-v2/src/application` 按用例分目录，都只依赖服务层的 owner：

- **session**：`initializeApplications`（`packages/local-runtime-v2/src/application/initialize.ts:74`）建出 `query`、`content`、`lifecycle`、`root`、`diff`、`conversationMutation` 六个会话应用和一个 `QueueApplication`，注释说它“without adding an aggregate forwarding facade”（`application/initialize.ts:73`）。它们的数据来源见[会话系统](https://daiw.org/manual/minimax-code/session-system)。
- **conversation**：`ConversationApplication`（`packages/local-runtime-v2/src/application/conversation/conversation-application.ts:125`）承接发消息、恢复流、中止、引导、入队与压缩，内部由 `DirectSendDeliveryService`、`QueueSteerWorkflow`、`TurnContinuationDeliveryService` 组成（`packages/local-runtime-v2/src/application/conversation/runtime-conversation-application.ts:29`）。入队属于这里而不属于队列应用：`enqueueMessage` 实际调用的是 `turn.submit`，并带上 `allowQueue: true`（`conversation-application.ts:400`）；队列应用只管列出、修改、排序和删除。
- **queue**：`QueueApplication`（`packages/local-runtime-v2/src/application/queue/queue-application.ts:112`）。
- **agent**：`AgentApplication` 不经 `CliService` 暴露，而是通过 `createV2AgentRuntimeManagementPort` 绑到第一代宿主上（`local-runtime-v2/src/runtime.ts:99`），让 v2 成为唯一的 Agent owner，见[Agent 服务](https://daiw.org/manual/minimax-code/agent-service)。

应用层的错误都是 `AppError` 或它的子类，字段为 `status`、`key`、`message` 与可选的 `retryable`（`packages/local-runtime-v2/src/application/errors.ts:1`）。`status` 仍是 404、409 这类数字，流式接口返回 `ProcessLocalStreamResult`，失败分支同样带 `status` 与 `body`（`packages/agent-modules/conversation-contract/src/process-local.ts:7`）。这些数字只是分类码，进程内没有任何 HTTP 往返。

## TUI 一侧：runtime、host、application

`packages/tui/src` 下与运行时打交道的是三个目录，分工不同：

- **runtime**：真正持有 `CliService` 的一层。`TuiRuntimeAdapter`（`packages/tui/src/runtime/adapter.ts:109`）在构造时把 `CliService` 拆给十来个访问对象：会话、对话、交互、事件、插件、Goal、工作区等（`adapter.ts:131`），对外实现 `TuiRuntime`。`TuiRuntime` 是十几个窄端口的交叉类型（`packages/tui/src/runtime/port.ts:613`），命令与控制器按需依赖其中一两个端口。TUI 创建会话时默认用的 Agent 名是 `mavis`（`packages/tui/src/product-context.ts:1`）。
- **application**：与终端渲染无关的产品逻辑，例如 `TuiRunCoordinator`（`packages/tui/src/application/run-coordinator.ts:92`）消费 `sendMessage` 返回的事件流，汇总成一次运行的结果，TUI、headless 与 ACP 都用它；队列处于暂停状态时，它先征求用户意见再重发（`run-coordinator.ts:128`）。
- **host**：剪贴板图片、外部编辑器、Shell 补全、按键与设置持久化等操作系统集成，与运行时无关。

## 边界在代码里的落点

`docs/architecture.md` 说应用层“does not instantiate DesktopService, an HTTP front door, or cloud handoff services”。代码里有几道闸门：

1. **上下文里没有网络身份**。所有应用用例接收的 `ProcessLocalContext` 只有一个 `signal`（`process-local.ts:1`），注释写着“accepts no network identity or HTTP headers”。会话查询的入参类型也声明“callers cannot supply tenant, account, or RPC metadata”（`packages/local-runtime-v2/src/application/session/query-contract.ts:4`）。
2. **协议包只放数据**。`packages/protocol/src/local.ts` 放 CLI 与 TUI 共用的数据结构：会话、消息、队列、分叉、问卷等视图与入参，例如 `SessionInfoView`（`local.ts:714`）、`SendMessageInput`（`local.ts:571`），开头声明不含路由、客户端、账号与 RPC 信封（`local.ts:1`）；数字枚举是为了读得懂旧会话，例如 `SessionStatusView` 从 `Idle: 0` 排到 `Abort: 3`（`local.ts:36`）。`packages/protocol/src/runtime.ts` 放 Agent 配置 `IAgentConfig`（`protocol/src/runtime.ts:315`）与运行时事件 `IRuntimeEvent`（`protocol/src/runtime.ts:398`），事件 schema 仍叫 `archon.runtime.event.v1`（`protocol/src/runtime.ts:3`），首行注释说它排除云端控制、会话交换与认证传输。
3. **退役清单挡住回流**。`scripts/lib/retired-sources.mjs:9` 列出不许出现在公开仓库里的路径，包括两个运行时包的 `src/http/`、`packages/protocol/src/generated/`、`packages/thrift-gen/`、`packages/thrift-gen-client/` 与 `packages/local-runtime-v2/src/service/session-handoff/`，由 `check:source` 检查。
4. **DesktopService 只剩名字**。公开源码里它只出现在注释与说明文字中，比如 `packages/local-runtime/src/cron/contract.ts:4` 说那段映射原本服务于 IDL 生成的 DesktopService。

<Callout type="info">
  边界并非一刀切干净。从代码看，回合层仍保留 `cloud-handoff` 这个 `clientIntent` 的抢占式提交分支（`packages/local-runtime-v2/src/service/turn-system/execution/turn-execution.service.ts:56`），应用层也还会为它构造专门的来源信息（`conversation-application.ts:817`），但发起交接的 `session-handoff` 服务已经退役。另有少数注释仍提到 HTTP，例如 `packages/local-runtime-v2/src/local/agent-runtime-port.ts:27` 与会话恢复服务的类注释（`packages/local-runtime-v2/src/service/session-system/sessions/recovery/recovery-service.ts:21`），读的时候当作内部原版的描述即可。
</Callout>

下一篇：[会话系统](https://daiw.org/manual/minimax-code/session-system)——应用层之下，会话、消息、队列与流式帧的数据模型。
