进程内入口:CliService 与应用层
TUI、exec 与 ACP 共用一个进程内运行时:启动顺序与服务组装,CliService 的 86 个方法如何分派到三组应用对象,以及源码边界在代码里的落点。
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 服务端。
怎么用
所有入口最后都调用 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
createTuiRuntime 先处理账号(共享 OAuth 租约、账号客户端)与日志,再调用 createEmbeddedRuntimeHost。后者给宿主定死几项身份:runtimeOwnerKind: 'tui'、capabilityProfile: 'cli'、capabilities.cliEmbedded: true(embedded-host.ts:84)。这几项决定了能不能拿到 CliService,判断在 packages/local-runtime-v2/src/runtime.ts:526:
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 遗产。第二,启动路径上曾有一个整目录快照检查点,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:
- Browser Use 组合(
services.ts:403)。prepareRuntimeServices(services.ts:346)预建的技能启用状态、IM 凭据迁移回执与 CLI 下线提示,在生产环境里早在第一代宿主之前就建好了(local-runtime-v2/src/runtime.ts:229),因为第一代宿主也要用。 - 沙箱、LLM 上下文检查器(开发用,构建可隐藏)、全局事件写入器、工作区(Git 与 HTML 预览)(
services.ts:409–418)。 - 回合能力生命周期、内容安全、全局指令、提示词支持(
services.ts:419–425)。 createProductionSessionComposition:Agent 产品能力、模型系统、会话系统、置顶服务一次建好(services.ts:436)。- 画布,以及绑定提示词回滚的一组 owner(
services.ts:474–475):可选的渠道系统、查询折叠键、消息投递、回合生命周期事件,再进入initializeRuntimeServiceOwners,依次是 MCP(services.ts:649)、插件(656)、MiniApp(671)、Plan(690)、Goal 校验(717)、回合系统(725)、会话应用(741)、Cron(747)。 - 用户侧应用:
ConversationApplication与技能应用(services.ts:504),再合成进程内产品应用(services.ts:513)。 - 服务生命周期、插件钩子、提示词生命周期收尾(
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),见回合系统。
CliService:一层很薄的门面
CliService 在 packages/local-runtime-v2/src/local/cli-service.ts:97 定义,构造参数就是三组应用对象:
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)。它们的数据来源见会话系统。 - 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 服务。
应用层的错误都是 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”。代码里有几道闸门:
- 上下文里没有网络身份。所有应用用例接收的
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)。 - 协议包只放数据。
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),首行注释说它排除云端控制、会话交换与认证传输。 - 退役清单挡住回流。
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检查。 - DesktopService 只剩名字。公开源码里它只出现在注释与说明文字中,比如
packages/local-runtime/src/cron/contract.ts:4说那段映射原本服务于 IDL 生成的 DesktopService。
边界并非一刀切干净。从代码看,回合层仍保留 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),读的时候当作内部原版的描述即可。
下一篇:会话系统——应用层之下,会话、消息、队列与流式帧的数据模型。