bootstrap:把运行时拼起来
bootstrap 是 Agent CLI 的装配层:配置分几层、怎样合并,主配置文件有哪些键,数据目录各放什么,一次 createZCodeApp 怎样造出 AgentRuntime、注入哪些端口,模型怎样选,会话怎样新建与恢复,各扩展机制在哪里接入,以及启动日志、日志保留与资源采样的数字。
core 里的 AgentRuntime 只认端口(见AgentRuntime:端口、依赖与方法装配),不知道配置文件在哪、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,命令行本身归命令行入口、无头模式与打包。
在分层里的位置
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):
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)实际只装其中五层,按优先级从低到高:
- system:
DefaultRuntimeConfig(contracts/src/config/index.ts:290)。 - user:
~/.zcode/cli/config.json(apps/zcode-cli/packages/adapters/src/config/file-config.adapter.ts:61)。这个路径写死在 home 下,不随storage.dir移动。 - project:从 cwd 向上找到含
.git(目录或文件都算)的那一层,再按“仓库根在前、cwd 在后”排列;没有 git 就只看 cwd(packages/shared/src/workspace-hook-config.ts:335)。每层目录依次取zcode.json与.zcode/config.json(workspace-hook-config.ts:174),越深、越靠后的文件优先级越高。 - 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)。 - cli:
createConfigCliOverrides只投影四样东西:模式、工具允许清单、工具禁用清单、界面语言(apps/zcode-cli/packages/bootstrap/src/app/app-config-options.ts:10)。命令行上对应--mode、--disallowedTools、--locale,允许清单只有协议的 session/create 会传。
合并由 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 与工作区信任。- MCP 服务器单独再解析一遍,规则是用户层压过项目层(
config-factory.ts:381):
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):
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 |
plugins | 启用,其余为空(contracts/src/config/index.ts:322) | 插件发现,见插件与官方市场 |
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 |
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 规则、模型目录与选项映射。这个文件还不存在时,旧版 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),见技能与自定义命令。
数据目录
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 会话库 | 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 | 模型请求录制,见遥测、调试与提示词轨迹 | paths.ts:13 |
<cli>/agents | 子 Agent 的输出 | create-app.ts:256 |
<cli>/plugins | 插件缓存、数据与官方市场 | paths.ts:9 |
<cli>/memories/projects/<slug>-<hash>/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:一次装配
入口先要求调用方给出 Provider Registry,没有就直接抛错(create-app.ts:147);会话 id 缺省时新建一个,trace 上下文以它为根(create-app.ts:152);工作目录取 runtimeConfig.workingDirectory 或进程 cwd(create-app.ts:154)。随后的顺序与依赖关系是:
- 配置与日志:
createConfig之后先解析界面语言(app-config-options.ts:37),再建日志工厂和启动计时器(create-app.ts:167、172)。 - 扩展发现:用户与项目的子 Agent 定义(
create-app.ts:208)、插件(create-app.ts:214)、插件自带的子 Agent(create-app.ts:223)、由插件推导出的运行时特性与内置node_repl(create-app.ts:229、230)。 - 存储:未注入会话库时打开 SQLite,启动时跑完迁移(
apps/zcode-cli/packages/bootstrap/src/app/session-store.ts:74);接着读出该项目上次的权限模式(create-app.ts:241)。 - 运行时配置:
resolveAppRuntimeConfig把配置、持久化模式、插件钩子与 MCP、子 Agent 定义合成一份AgentRuntimeConfig(create-app.ts:244)。宿主注入了浏览器控制端口且 Browser Use 已启用时,再给node_repl接上浏览器 broker(create-app.ts:262)。 - 适配器:工作区钩子安全、权限服务、产物库、邮箱、MCP、执行、PDF、文件系统、HTTP(
create-app.ts:297至417)。每个都允许调用方注入替身,否则用 Node 实现。 - 模型与工作流:模型适配器与模型工厂(见下节),再用同一个工厂装三条工作流子运行时:脚本工作流桥(
create-app.ts:557)、动态工作流运行服务(只有会话库带dwf_*表时才构造,create-app.ts:585)、片段试跑服务与模型目录端口(create-app.ts:716、722)。 - 运行时与门面:
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,见模型适配层。
交给 AgentRuntime 的 modelFactory 来自 ApiProviderModelRuntime(apps/zcode-cli/packages/bootstrap/src/app/provider-registry-model-runtime.ts:43):
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 与闲时计划。
新建、继续与恢复
命令行入口先把请求翻译成一个会话 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):
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 会话库。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)。
扩展机制的装配点
| 机制 | 装配点 | 进入运行时的方式 |
|---|---|---|
| 技能 | createNodeSkillAdapter:额外根取 skills.roots 与插件技能根,禁用路径合并按路径的开关与动态工作流灰度(create-app.ts:745) | skillPort |
| 自定义命令 | 输入门面的 customCommandPromptResolver:先查内置的 /init,再展开用户与插件命令(create-app.ts:793) | 提交前改写提示词 |
| 插件 | resolveStartupPlugins 先播种官方插件再发现(apps/zcode-cli/packages/bootstrap/src/app/startup-marks.ts:11);插件引用目录在创建时冻结一次(create-app.ts:292) | 技能根、命令根、MCP、钩子、子 Agent |
| MCP | 插件 MCP、配置 MCP、内置 MCP 依次覆盖,内置的放最后以防同名劫持;项目 MCP 默认可信(runtime-config.ts:90、109) | runtimeConfig.mcp 与 mcpPort |
| 子 Agent | ~/.zcode/agents 与 .zcode/agents 下的定义加插件定义(subagents.ts:54、create-app.ts:235) | runtimeConfig.subagents,子运行时由 core 自建 |
| 钩子 | 用户层钩子随配置合并,项目层只进信任快照;插件钩子再追加(runtime-config.ts:160、245) | runtimeConfig.hooks 与工作区钩子准入 |
| node_repl | 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 服务器 |
| 动态工作流、专家工作流 | 第 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)。脱敏与字段约定见遥测、调试与提示词轨迹。
保留策略的数字在 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 不采样。
下一篇:输入受理、命令队列与引导——运行时装好之后,一条输入怎样被受理;忙碌时的新输入又怎样排队,或作为引导插进当前回合。