进程内入口:CliService 与应用层

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

作者 David更新于 第 9 篇(共 33 篇)

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:326tui
mcode execpackages/tui/src/cli/run-exec-command.ts:74headless
mcode acppackages/tui/src/cli/run-acp-command.ts:64acp
mcode provider、mcode plugin 的子命令packages/tui/src/cli/provider-command.ts:164、packages/tui/src/cli/plugin-command.ts:177headless

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.sqlitelocal-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_msAgent 定义与遗留 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_msIM 渠道子系统;对 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:

  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),见回合系统。

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、getSessionTreeapplications.session.query
createSession、updateSession、archiveSession、deleteSessionapplications.session.lifecycle
getMessages、listSessionInputSummaries、getSessionUsage、getPeekContextapplications.session.content
forkSession、rewindSession、editSessionMessage 及其预览applications.session.conversationMutation
listQueueMessages、updateQueueItem、reorderQueue、deleteQueueItemapplications.queue
sendMessage、resumeSession、abortSession、steer、steerSession、enqueueMessage、requestCompactionconversation
问卷、权限、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”。代码里有几道闸门:

  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。

边界并非一刀切干净。从代码看,回合层仍保留 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),读的时候当作内部原版的描述即可。

下一篇:会话系统——应用层之下,会话、消息、队列与流式帧的数据模型。

本页目录