Harness 运行时
Harness 运行时是 Zuno 的核心:它决定一个回合如何组装、如何持久化、如何恢复,以及扩展在什么边界内运行。
本页是导读,不是完整译文
英文版 Harness Runtime 逐节覆盖运行时的每一处契约,是唯一权威来源。本页按相同章节顺序给出每一节要解决的问题和 关键结论,用于定位与建立整体认识。
不要用本页替代英文版做实现判断:精确的字段语义、状态机转换条件、重试与恢复的分类规则, 以及验收标准都只在英文版中完整表述。两者出现分歧时以英文版为准。
运行时模型
一切都是原生组件。产品行为属于某个 Component、某个类型化服务,或某个 AgentDriver,而不属于一个不断膨胀的中心循环。
一项能力只有在接口、提供方与消费方三者齐备时才算完整。当这三个角色的生命周期不同时,应保持彼此分离。
注册是一种副作用。挂载操作返回的 disposer 精确移除它注册过的东西;profile 替换是事务性的,失败时按相反顺序回滚。
组件通过 Component::stop_budget 声明运行时等待其每个 disposer 的时长:默认的 StopBudget::Runtime 沿用运行时配置的停止超时;必须终止并回收进程树、排空 socket 或 等待 flush 的组件返回 StopBudget::Bounded(时长),零值按 Runtime 处理。disposer 仍按 注册的相反顺序逐个运行,预算只限定运行时等待每一个的时长。超出预算的 disposer 记为 TimedOut 诊断但不会被取消:半途丢弃它会丢掉它正在回收的东西,比迟到的停止更糟,所以 它被脱离并在后台继续回收。
部署可以用 runtime.max_component_stop_ms 给每一次等待设上限,运行时按 min(声明值, 上限) 生效;省略或 0 表示本机不设上限,这也是默认行为。上限只缩短等待: disposer 仍被脱离而不是被取消,被夹取的进程树回收仍会在超时之后继续。
Agent 与提示词契约
Agent 具有显式的正向职责、负向委派边界、权限以及结构化输出预期。
内置 Agent 的分工:build 负责端到端交付,plan 是只读规划,deep 承担困难的跨领域实现, 并可在父级授权、深度与并发限制内委派边界明确的任务,不能递归转交整个原目标。
子级继承父级当前已生效的权限规则、资源边界与完整授权工具目录,显式只读角色进一步收窄。 已授权但尚未展示的 MCP schema 不等于不存在的权限;反过来,新发现的全局配置也不能给 child 增加父级没有的权限。旧 child 的权限快照不能覆盖本次委派的新上限。
task 的工作契约是扁平对象:agent、objective、deliverable、instructions、 success_evidence 都是顶层必填字符串,task_id 续跑时也一样。intent 只是可选 UI 标签,类型化解析前会移除,不能替代 objective,也不会被宿主自动转换为缺失参数。 工具说明内的完整 JSON 示例会经过真实擦除工具调用测试。orchestrator 与 deep 共用简短参数提醒;它不改变权限,也不覆盖用户自定义提示词。 缺少顶层属性时,schema 校验保留原错误,最多补充四条当前字段说明,每条上限 256 个 Unicode 字符;嵌套错误不借用同名顶层说明,新增提示不复制调用参数值。修正后的调用仍 须经过正常校验与授权。 字段名超过 128 字节时不附加提示,不改变原始参数校验或自动截断字段名。
运行时提示默认让 shell、task 和编排在前台执行;只有父级确实有独立工作可并行推进时 才显式后台执行。workflow/Council 内部并行不等于父级后台化,bg 只使用返回的执行句柄 或输出路径。评审保留稳定 finding ID、证据和处置,修复后复查变更与受影响路径,不设固定 评审轮数上限。这些是提示词指导,不增加工具硬门禁。
实施 Agent 自行决定任务范围内的技术实现,包括修复自己引入的问题、补齐产物流及修正 验收失败;存在多种实现不等于必须询问。明确的“只验证、不改代码”仍然有效。 子代理把跨模块取舍交给主 Agent;只有缺少用户专属信息、改变目标、增加对外影响或 改变保护/授权时,主 Agent 才向用户询问。负向安全检查通过不等于正向交付成功。
原生 task_context 工具有界保存长任务理解:目标、交付/检查意图、动作摘要、禁止事项、 真实用户消息来源、决策归属、阻塞,以及分别记录的交付/安全检查。 session.task_context.updated.1 保存带 revision 检查与幂等键的快照;更新保留省略字段, 合并约束和检查,切换任务须显式 replace。验收定义修订须显式关联用户来源,并重置检查 状态和证据。来源同时绑定消息元数据及全部持久输入部分摘要(含附件引用),变更或 丢失时投影仅作历史参考。更新后刷新 runtime.task_context,重启时重新加载。 它是 Agent 维护的理解,不是权限授予、宿主验收认证、Goal 恢复或自动唤醒。 交付完成除全部已有检查通过并附证据引用外,还必须有正向交付检查通过。 普通 final 仍结束当前周期,不因这份上下文存在未完成事项而强制续跑。
设计对照固定在 Codex 9ba1d9eb5bbbd87ba2fc528d91ad239eea975ee9: codex-rs/core/gpt_5_2_prompt.md 的端到端任务责任、 core/src/tools/handlers/request_user_input.rs 的根会话提问与 Plan 阻塞边界, 以及 tui/src/bottom_pane/request_user_input/mod.rs 的 60 秒宽限加 60 秒倒计时、 交互暂停和空答案自动结束(后两路径同样相对 codex-rs/)。 Zuno 将这些边界适配到 QuestionPort 和持久事件;task_context、来源摘要及交付检查 校验是 Zuno 的接入设计,不声称 Codex 存在同名工具,也不替代原生授权和执行门禁。
提问、批准与会话调度
QuestionPort 将发布、查询、回答、等待分离;QuestionService 在同一事务中维护请求 revision、稳定问题项 ID、部分回答、幂等命令回执、inbox 和匹配的 Goal/会话状态。 Work/Goal 的可选问题使用 deferred 投递,question_async 同样不阻碍继续执行或最终回复。 普通 Work 真正需要用户作出阻塞性选择时,用一个清楚的纯文本 final 问题结束当前回合, 在回答前不执行受影响的动作,也不制造持久暂停。Plan 保留同步澄清,必需的 Goal 输入仍 使用 goal_request_input 的类型化门禁。旧 pending 表单或未完成 Plan 本身既不授权 续跑,也不禁止新输入;等待权威是 runtime execution wait reference。 TUI 用 Ctrl+S 选择稍后;/questions 列出可回答表单和已关闭但仍待投递的状态行,只重开 可回答表单。高亮、空输入、取消都不是批准。
原本以 deferred 投递的普通澄清问题持久保存 120 秒自动延后期限;TUI、ACP、HTTP 宿主持有原生计时驱动,重连/重启后读取问题也会核对过期状态。 到期记录 question.auto_deferred,更新 QuestionView.autoDefer 的 deadlineAt 和 armed|snoozed|deferred 状态,不生成答案或 inbox 输入,之后仍可回答。 TUI 开始交互后暂停自动收起,高亮项不会因超时被选中。同步阻塞澄清、必需输入、 Plan 批准、Goal 恢复没有此期限;旧问题没有期限元数据时保持不变。 ACP 迟答仅可跨越显示 revision 的精确自动延后后继,不能覆盖已变更的草稿、答案或授权。 问题展示限定在绑定会话及其子会话;通知不能把另一个根会话加入展示范围。 TUI 和 HTTP 事件转发同时核对持久状态与本地通知,避免漏掉其他服务实例已提交的延后。 HTTP SSE 的实时和有界补发路径均包含 question.auto_deferred,保留原事件身份。
QuestionView.delivery 从关联输入回执派生,不是第二份持久 question 状态:
| 阶段 | 含义 |
|---|---|
WaitingAnswer(waiting_answer) | 表单仍为 pending,可以继续回答。 |
AnsweredPendingDelivery(answered_pending_delivery) | 表单已关闭,但关联输入尚未全部应用,或提前批准的 Plan 仍待来源回合交接。 |
Applied(applied) | 已关闭表单的关联输入已有应用证据,且没有剩余未应用条目。 |
「Keep paused」等已结束且不产生模型输入的控制,delivery 为 None,序列化时省略, 不是 WaitingAnswer,不会重开问题。提前批准的 Plan 若仍待交接,即使控制输入尚未 准入,也保持 AnsweredPendingDelivery。
pendingInputs、appliedInputs、最新输入回执状态及 appliedAt 区分回答已保存和实际 应用。已关闭表单可以作为投递状态继续列出,不会因此重开 modal。应用也不等于 provider 成功响应或任务完成。
plan_exit 只发布精确绑定 Plan、review、Agent/模型身份的批准请求。用户提前批准时, 必须等来源 Plan 回合正常交接后才排入 Work;过期或中断的批准不生效。
普通会话和 Goal 共用 session_execution_state.scheduling:可执行、等待指定人工请求、 等待指定外部来源/周期、暂停、完成。无门禁的普通 provider final 返回 Finish 并结束当前 cycle,即使 Plan/Todo 尚未完成或仍可执行,也不单凭这些状态再启动模型请求,更不把步骤 自动标成完成。前台工具等待与真实工具续跑由宿主在 final 对账之前处理。已有类型化等待和 受保护暂停优先;只有当前 cycle 拥有的 active Goal 返回 ContinueGoal,并保留连续三次 相同进度指纹触发 no_progress 的规则。 这参考 Codex 9ba1d9eb5b 的 core/src/session/turn.rs:475:续步依据模型后续工作与 待投递输入,再处理 stop hooks;core/src/tools/handlers/plan.rs:93 只发布 Plan 更新事件, Goal 另由 ext/goal/src/runtime.rs:399 决定续跑。Zuno 的持久状态与控制接口是原生实现, 不声称兼容 Codex。 暂停期间 callback 仍能入库,但不能重置无进展计数或解除暂停。 真实用户输入在提升、消费的同一事务中绑定 session_work_cycle,忙时仅排队不会改写当前回合。 普通停止记录旧输入/回合并关闭其周期,下一条消息无需 /resume。新请求不继承旧 Plan 或无进展计数;原生 Work 授权及类型化 Plan/Todo 修改才建立工作归属,读取或旧步骤仍为 in_progress 都不是承接。受保护的等待、审批、认证、预算和未知副作用门禁保留。 /resume 不能绕过待答条件、Plan 授权,也不能把未知副作用登记为已核验。
启动、重连、新输入和升级不会自动解锁旧暂停或阻塞。后续正常回复或相似错误文字不能证明 旧门禁来源;既有 failed 桥接保持 failed,证据缺失、被裁剪、改变或来源未知时继续保留 门禁。普通 /resume 仍执行全部授权和安全检查。下文的显式 session repair 仅处理 证据完整的那一条旧误阻塞输入,不是通用暂停重置或重放工具。
终态 bg output、bg wait、callback 使用同一消费回执,并保留来源工作周期。 格式 15 在一个原子前向迁移中升级受支持的 5–14 格式,保留用户原数据,最后更新格式标记。 在既有输入回执、Context 快照和 Goal 恢复选择之上,新增 session_work_cycle、 goal_turn_observation、goal_cycle_failure、goal_turn_audit; 迁移不调用模型、不批量晋升历史证据,也不恢复暂停 Goal。 本次普通 final、问题投递与有界修复行为保持格式 15 及其 schema 不变,不增加自动旧状态 解锁或迁移。
边界参考本地 Codex 9ba1d9eb5bbbd87ba2fc528d91ad239eea975ee9 的 protocol/src/turn_input.rs:178:TurnInputSubmission::Started 与 Steered 仅确认 Core 接收输入用于回合处理,不等待 hook、模型上下文更新、rollout 落盘或采样。 core/src/session/turn_input.rs 另行区分 User/Automatic/Recovery; core/src/tasks/mod.rs 取消活跃工作,TUI Goal 菜单单独控制显式 Goal 恢复。 Goal 工具和提示词同时对照 eaa8b6d91701d6cabe464141facc677e5915fbfc。持久周期、 报告转交回执及宿主阻塞计数是 Zuno 原生适配,不声称 Codex 提供相同数据库或协议接口。
runtime.execution 还会按最终工具快照为内置与自定义 Agent 生成简短降级规则:首选工具 限速、不可用或暂时失败时,不原样重复调用。tool_search 可见时可以发现另一个已经授权的 已连接工具,包括 google_search;Shell 可见时,GitHub 优先使用已安装的 gh,仓库搜索 优先使用 rg,而不是先写原始 curl 或手工遍历。不存在的能力不会出现在指导中,降级也 不能扩大权限。
根回合在权限、允许列表、父级 schema 过滤后解析 mcp_tool_exposure。默认 auto 在数量/ 字节预算内直接展示小型服务整个工具集;可全局或逐服务覆盖为 eager/deferred。 策略独立于传输配置,不改变 McpConnectionIdentity,不跨会话共享外部进程。 设计参考 Codex 9ba1d9eb 的 direct/deferred 与来源列表机制,再适配 Zuno 的持久工具快照。
延迟工具的实现仍保留在调度器中,tool_search 公布原始服务名称和有界能力摘要; 匹配项从下一次 provider step 起按单调 revision 扩展确切工具快照。 成功的 tool_search 结果同时是会话的持久暴露账本。后台报告重建宿主、进程重启或客户端 重新挂载时,会在第一次 provider 请求之前恢复这些 id,并与当前仍连接、权限仍可见的目录 取交集;已经移除的能力不会因此重新获得权限。 Agent 的确切 tools 允许列表会立即公开其中点名的 MCP schema;子级仍受父级 Attempt 中已持久化的确切 schema 上限约束,不能搜索出更大的权限面。ACP session-local mcpServers 在严格连接门禁后也立即公开,同一目录中的宿主配置 server 采用上述可见性策略; Catalog 会把这个会话边界传递到子回合与后台续跑。
发现工具本身必须可见:原生角色在用户覆盖之前授予 tool_search;若用户禁用/拒绝它, 或发生同名冲突,其他已授权 MCP schema 保持直接可见,不会藏到不可达入口之后。 Tool::source 保留原始服务归属,不反向拆解已规范化的 wire id。运行时指导 Agent 主动选择 相关已授权 MCP,区分配置、连接、缓存和延迟状态;普通服务调用不先加载 customize-zuno, 也不把扩展/资源列表当作工具发现。
每个新工具 part 还会保存准入该调用的 provider-visible schema identity。历史输入与当前可执行 工具声明是两个独立契约。普通 Agent 请求在有效 surface 为 Responses 时保留原生历史: 优先级为显式 ResolvedModel.surface > adapter 已解析的 Capabilities.default_surface > Spec.surface。没有显式模型 surface 时,仅在 adapter 仍为 Default(未知)时回退到 Spec; Spec 不能覆盖 adapter 已解析的结果,全部仍为 Default 时保持未知、不猜测。 没有模型覆盖时,compatible fixed profile 忽略冲突的 Spec:xai 即使设置 Spec.surface = Chat 仍走 Responses;openrouter 即使设置 Spec.surface = Responses 仍走 Chat。OpenAI 在 model 与 Spec 都为 Default 时仍解析为 Responses。Provider trait 仍只有三个方法, 复用已有能力声明,不新增用户配置。解析后的选项 显式设置 reasoningReplay: "encrypted" 时也采用同一保留策略。即使工具缺失、被权限隐藏、 被 hook 删除或 schema 发生结构性变化,历史调用及结果仍保留原始内容、call id、arguments 字符串、顺序和边界,既不转成惰性文本,也不按 AuthoritativeState 删除。Responses 的这条 规则在 reasoningReplay 为 off 时同样生效,但不会因此开启封装推理重放。
保留历史不会扩张当前请求的工具清单、恢复缺失实现或授予执行权限。新调用仍通过当前锁定的 工具快照、schema 校验、权限规则与风险门禁;回放已有调用及结果不会再次派发工具。这一边界 参考 Codex 9ba1d9eb5bbbd87ba2fc528d91ad239eea975ee9: core/src/client_common.rs:19/56 分离 input 与 tools, protocol/src/models.rs:1034/1068 保留封装推理和原始 arguments, core/src/client.rs:315/1335 校验增量请求一致性。这些是设计依据,不代表已完成 provider 验收。
不属于上述保留策略的普通请求继续使用现有声明 fallback:只对更早 turn 的保留历史与当前 hook 后的工具定义对账;当前 turn 刚产生的调用始终保留原生配对,以便未知或被拒绝的调用 仍能收到协议完整的 tool result。对更早历史,声明一致时保留原生 tool-use/result 协议。 新的 replay hash 只在 schema 与子 schema 位置移除 description、 title、examples、comment、default 等纯注解键,保留 required、type、enum 与其他取值约束。 参数名、定义名,以及 const、enum 或未知扩展值中的对象均保持原样,即使它们的键也叫 description、title 或其他注解名称。没有 replay hash 的旧记录 仍要求 description 与 schema hash 完全一致。工具缺失、结构性 schema 变化或持久 identity 无法读取时,只在本次请求中降级为有界惰性 JSON 文本:arguments 与 output 会在序列化 fallback 对象前按 UTF-8 边界限制单字段和整次请求大小。数据库里的原记录不会被改写,宿主也不会为了 重放而把当前不可执行的旧工具重新宣传成可调用能力。旧版本没有 identity 的记录会先按 assistant message 从不可变 provider-request Attempt 中恢复确切 hash;若这份证据也不存在, 即使当前存在同名工具也会降级,而不会把旧调用静默绑定到新 schema。无工具的内部压缩请求 使用独立于普通 Agent 回放的工具无关投影:工具调用和结果以有界的惰性 JSON 文本进入摘要 模型,绝不会在没有声明的情况下继续使用原生函数协议,也不携带封装推理。 降级到不认识新增 replaySchemaSha256 字段的旧 Zuno 时,会失败关闭为惰性历史; 持久调用本身不会损坏或被改写。
在声明 fallback 路径中,Goal、Plan 与 Todo 状态工具使用 AuthoritativeState 历史策略。 旧声明不兼容时,历史调用与结果会被省略,不转成模型可见的惰性说明;这种省略不适用于普通 Responses 请求或显式启用加密重放的请求。当前状态仍由每次请求同源生成的 runtime.work_state 提供,保留旧状态工具的结果既不覆盖当前状态,也不会恢复 Goal。上述保留策略之外的通用工具 继续采用精确声明 fallback。历史修复 notice 使用 Diagnostic audience,按会话、 context epoch、工具及新旧 identity 去重,只写结构化日志,不进入 ACP thought、TUI 对话或 HTTP 事件历史。
prepare_request hook 仍然只能缩小已锁定的工具集合;新增、替换或重复 schema 会在发送前 失败。engine 工具 dispatch 使用同一份 hook 后已收窄的快照,不回到更宽的 hook 前 registry。 若 hook 删除的是保留历史仍引用的声明,普通 Responses 请求和显式加密重放请求保留 历史调用及结果;其他协议继续使用上述角色感知 fallback,不把声明删除本身当作 hook 错误。 当前 turn 刚产生的调用不参与这项历史修复。历史 ToolUse/ToolResult 块另有一份按出现次序 与 role 锁定的快照:hook 不能新增、删除、替换、复制、重排、拆分、在原生调用与结果之间 插入另一条消息,或改变工具协议历史的 role。独立的 sealed-history guard 还保护封装推理绑定的 assistant 内容、项顺序与输入边界,复用共享 Responses 最大 assistant 组边界;同组相邻的 未密封 assistant 内容也受保护,受保护组外的未密封普通文本仍允许原先的 hook 行为。 在适用声明 fallback 的路径中,持久 identity 失败与 hook 后声明删除 会先合并,再执行一次按 occurrence 排序的投影,因此混合并行批次仍保持持久结果顺序。
仓库与用户的规则文件要么整份进入 Prompt,要么不进入。宿主无法读取的本地规则文件仍会在第一 次 provider 请求前以类型化错误停止本轮,并点名文件与修复方式。一个内容完整但超出指令预算 (64 KB 与模型 context window 四分之一取较小值)的条目则整份跳过。宿主发出 warning 级 instruction.not_in_force notice,包含来源、字节数、预算与剩余空间,然后继续 考虑后续更小的独立条目;规则文件绝不截断,超大 AGENTS.md 也不会阻止 ACP、TUI、server 或 CLI 启动。无法抓取的远程规则来源使用同一类非致命 notice,因为网络可用性不应决定 Agent 能否 运行。
扩展包与可执行插件宿主
扩展要么是显式 WASI 授权下的 WebAssembly 组件,要么是使用行分隔 JSON-RPC 的受限子进程。能力必须声明,不会被默认赋予。
详见 插件与扩展;完整 WASI guest 与原生 Rust 实现路径见 开发 Agent 与扩展。
提示词溯源
模型可见即被记录。 每一个提示词分段、外部输入、工具结果、重试通知和子 Agent 报告,只要它能改变一次模型请求,就必须能从持久会话事件中重建。
提示词组装使用稳定的分段标识、确切来源、有序内容和内容摘要。实际经过 hook 之后的提示词在 provider 请求发出之前落盘。
加密推理重放
有些 Responses 端点会把一个步骤的推理封装成不透明信封,并绑定到单一模型、账户与会话。provider 用 reasoningReplay: "encrypted" 声明这项能力:它是端点选项,绝不是从 provider id 推断出来的规则。此后 Zuno 会为该 provider 的每个 Responses 请求加上 include: ["reasoning.encrypted_content"],并在后续请求中逐字节回送每个封装项。只要请求解析到 Responses surface,这项声明就会生效:目录里的 openai provider 不需要任何声明就能到达那里,而端点来自 provider 选项的网关只有声明 transport: "openai" 搭配 surface: "responses" 才能到达。配置校验会按 provider 和按模型拒绝那些确实无法承载封装项的路由,并接受本来就会解析成 Responses 的配置。
默认值是 off:请求既不带 include,也不带任何封装项,包括同一会话在选项为 encrypted 时存下的信封。它并不表示请求字节与既有版本一致:下面的顺序修正对所有 Responses provider 生效,与该选项无关,因此先写文本再调用工具的一轮现在会先发文本项。每个重放的工具调用也会带上 provider 自己的 arguments 字节而不是重新序列化的结果,因为端点对它发出的那个字符串做指纹;而某个步骤的封装项后面没有任何输出时,这一项会被扣留而不是单独发出,并计入被扣留数而不算作一次重放。
封装信封属于持久状态,因此一个步骤会被持久化成带位置的 part 账本,而不是一段文本加上尾部堆积的工具调用。每个 part id 携带它在流中的位置 prt_{turn}_{step}_{position}_{kind},且同一步骤的所有 part 共享 assistant 消息的创建时间,于是水合出来的顺序就是 provider 的产出顺序。一个先推理、写文本、调用工具、再推理、再调用第二个工具的步骤,会按同样的次序重放,每个信封都紧挨在它所解释的输出之前。这正是封装端点会校验的内容:顺序被打乱或只回送摘要都会在链路上被拒绝。
独立的 sealed-history guard 复用共享 Responses 最大 assistant 组边界,在请求 hook 前后 保护绑定输出、项顺序与输入边界,阻止通过插入或改写同组相邻未密封 assistant 内容绕过保护; 受保护组外的未密封普通文本仍允许原先的 hook 行为。不能因工具不可用而改写绑定输出、 删除旧状态调用或移动边界并继续携带旧信封。符合重放条件的密封请求组装后,guard 还锁定 hook 前后的模型与 surface,并在 dispatch 前拒绝请求参数中的 input、messages 或 model 覆盖。该保护不新增用户配置或数据库迁移,不改写原始持久历史;provider/model、年龄、 off 与歧义组 withholding 策略保持不变。
若 text_complete 改写刚完成的 provider-sealed 输出,Zuno 会恢复原始文本并返回 Hook 错误; 失配文本不会持久化,该错误也不会自动重试。sealed-history guard 仅使用哈希快照保护历史, 不会打印 capsule 内容。
Kiro 返回的 400 reasoning_replay_context_mismatch 等端点拒绝不会因此变成可重试错误。 本策略不新增 400 盲重试,不全局删除 reasoning,也不放宽 provider 对账户、会话、模型和 输出的绑定校验。Kiro Provider v3.1.1 保留这些校验;现有有效的 Zuno Responses 配置无需 新增设置。实际运行验收与既有失败输入的恢复结果仍需分别核验。
Kiro v3.1.1 会对本轮清单未声明的历史调用返回 missing_tool_declaration。 此场景需使用 Kiro v3.1.2 的历史工具作用域修复,将历史校验与当前工具授权分离, 无需新增 Zuno 设置。缺少可证明原始别名绑定的旧 namespace/custom 历史仍返回 missing_historical_tool_binding,双方都不会猜测该身份。不要重新公开已禁用工具或 删除密封推理来绕过校验;真实失败会话仍需在实际端点上完成恢复验收。
自动 Goal continuation 即使没有新的用户消息,也属于一个新的 provider turn。Zuno 会在该 turn 的第一条 assistant 行上只保存 prompt receipt 引用,而不是再复制一份提示词;当两个 assistant 响应在 Responses input 中本来会直接相邻时,引擎会把 receipt 中 hook 后实际发送的 developer 项解析成后一个请求消息上的结构化 ResponsesInputBoundary sidecar。OpenAI、OpenAI-compatible 与 Bedrock Mantle/Runtime 共用同一 Responses cursor,把这些标准输入项投影到两个 assistant 输出之间,再重放后一轮的推理信封。通用 Message 内容、Chat Completions、Anthropic Messages 与 Bedrock Converse 不携带该 sidecar;空边界的序列化与普通消息完全一致。旧版本写入的行会沿持久化的 assistantMessageID -> promptReceiptID -> actualProviderProjection.developer 证据链恢复同一 receipt;只有没有 hook 改写时才回退到 providerProjection。恢复时会剥离稳定的 runtime policy 前缀,因此历史边界只包含原始 turn context、memory 与 request hook context。这个过程不会伪造 user 消息或工具结果。
压缩摘要和旧 session export/import 可能确实没有 receipt。此时 Zuno 只扣留歧义 assistant 输出组中的封装 capsule,保留文本、工具调用和真实工具结果,让会话以较低推理连续性继续。每条 Responses 请求构造路径仍保留最后一道本地校验;若共享修复后仍存在畸形分组,它只报告消息索引,不会渲染不透明 token。
重放是有作用域的,而且作用域在组装请求时生效,不是在写行时生效。信封只会被重放给产出它的那条 assistant 消息上记录的目录 provider 与模型,并且只在它比 reasoningReplayMaxAge 更新时重放。其他情况下它只在这一次请求的内存里被扣留,持久行保留原密文,因此换回原模型即可恢复重放。标题、摘要、压缩、反思与 Council 请求运行在其他模型上,完全不会收到信封,这也让压缩转录中不含 provider 状态。
每个前台 session.provider.request 事件都会记录 reasoningReplay、replayedReasoningCapsules、withheldReasoningCapsules、restoredReasoningReplayBoundaries 与 withheldAmbiguousReasoningCapsules。这些字段就是重放确实生效的依据:如果一个会话从第二个请求起报告的重放信封数仍然是零,那它就没有在重放,无论端点怎么声称。重放计数就是 adapter 真正放到链路上的数量,因此被配对规则丢弃的信封会计入被扣留数,绝不会算作一次重放。恢复边界数表示本次请求从 prompt receipt 重建了多少条旧 assistant 行的准确 developer 后缀;歧义扣留数是因边界无法证明而降级的子集。请求事件只记录计数,不记录信封或 developer 文本。
信封本身是不透明的 provider 密文,作为会话内容保存:它存放在推理 part 的 metadata.providerReasoning 中,会由 HTTP messages 端点返回,还会随一个完整携带密文的流事件转发:服务器 SSE 流上的类型是 provider.reasoning.item,zuno run --json 打印的是 provider_reasoning_item,两者的密文都在 encryptedContent 字段里。它是后续请求需要的持久状态,因此不会被脱敏;能读取某个会话的消息或事件流,就等于能读取它的信封。
可审计的记忆与反思
常驻记忆与用户学习默认启用。自动学习优先使用显式 learning.extractor_model,否则依次使用当前 provider 下可达的 small_model 与当前会话模型,不会打开另一个 provider 或改写配置。只有项目范围且 置信度至少为 0.9 的候选会自动写入;全局与低置信候选继续等待评审,并且所有已应用 变更都可以撤销。
自动学习的 post_turn.idle_delay_ms 默认 0。合格回合完成后保存有界来源快照并立即 入队、唤醒 worker,下一轮对话正在执行不会阻止上一轮快照提炼;显式正值保留空闲延迟。 新任务优先于历史补处理。后台仍受每次最多两个任务、项目并发、资格检查、输入/输出/时间 上限、去重与最多三次尝试约束;真实 provider 限流会保留 typed Retry-After。新会话固化新的自动默认值,已有 /memories 会话策略不会被追溯改写。
提炼与 Memory 合并仍分两阶段,并复用所选模型的参数与能力解析。400 会保存脱敏响应、 状态码和请求标识,不能靠盲重试或换模型处理。/learn reprocess <assistant-message-id> 可按新提炼版本重处理;/learn repair-history [--dry-run] 先核对原始来源,缺证内容 继续未验证。更正和遗忘复用 memory_update,后续 provider 请求刷新 Memory 提示词。
Provider 用量
ContextUsageSnapshot 是窗口占用的统一来源:最近供应商确认基线,加尚未计入内容的估算。 73,948 的粗估值不能覆盖 149,501 的确认输入。累计用量、确认值、尾部估算分开, 分批 usage 合并而非清零缺失字段或重复累加;缓存、推理子项不重复计数。 快照携带来源、请求/attempt、epoch、revision、freshness 与更新时间;未知不等于 0。 ACP、TUI、HTTP、JSON run 与恢复路径消费同一快照,子 Agent 和学习请求不覆盖主会话窗口。 估算按实际规范化发送内容计算,不按未读取文件大小计算。
assistant checkpoint 在同一事务内对账消息快照与会话投影;同一消息的重复 checkpoint 先扣除旧快照再加入新快照。会话分别保存累计非重叠 token 桶、最新完整提示词和统计口径。
Bedrock Converse 与 Anthropic Invoke 的非缓存输入、缓存读取和缓存写入互不重叠, 完整输入为三者之和。OpenAI Responses 的缓存明细已包含在输入数中。Invoke 的 message_start 与 message_delta 更新同一次请求的快照,明确报告的 thinking 是输出 的子集。内容过滤导致的空响应会保留已报告用量,并成为带类型的终态 provider 拒绝, 不会按空回答自动重试。已有历史 receipt 不会被猜测改写。
TUI 保存请求前基线及当前请求的可替换用量快照。分批事件保留未再次报告的字段, 原始输出先拆分为可见输出和推理,再计入累计非重叠桶。新请求重置快照, 重试回滚恢复基线;恢复持久会话时清除临时累计状态。
持久输入
用户提示词、steering 以及子 Agent 报告在执行前进入持久 FIFO 收件箱。reportDelivery: nextStep 必须完成子结果结算、准许父级输入并唤醒父级,且不存在轮询竞态。
恢复后的 provider 请求发送前,引擎在安全点按 admission sequence 扫描符合条件的持久 输入,即使进程内唤醒通知已丢失或被取走也一样。已保存回答、已结算报告和合法 steering 不会仅因恢复控制先唤醒宿主而被跳过;明确排到下一回合的普通输入仍留在原队列。选择时 校验 cycle/turn 归属、唤醒资格与输入 revision,不会把不支持的客户端载荷降成纯文本或删除。
InputDeliveryBatch 记录本次安全点实际消费的 session、turn、cycle,以及每条输入的 ID、准入序号和预期 revision。session.input.delivery_batch.1 的状态是 recorded, 它证明消费,不是第二套输入生命周期,也不证明应用。实际 post-hook provider dispatch 对应的输入回执才建立 applied;批次或唤醒都不证明 provider 成功。
InputAdmissionReceipt 分开记录 admitted、recorded、applied 与 completed/failed/cancelled。 写入历史不代表模型已处理;实际 post-hook 请求建立应用事实,逻辑执行结束才结算完成。 同周期恢复显式交接 owner,其他 turn 不能完成这条输入。可选客户端消息 ID 按会话幂等, 不按文本去重。
对已消费并写入历史、但从未应用的输入,原生 session-control 事务可以保存 session.input.execution_gate(存储类型为 session.input.execution_gate.1), 记录 input ID、gate 和时间;同一写事务快照读取执行资格并发布回执。 可选 InputAdmissionReceipt.executionGate 从该持久事件投影,不改变 format 15 或其 schema。回执保持 recorded,appliedAt、completedAt、turnId 缺省, 不会仅因驱动遇到执行门禁、未发起采样而改为 failed。
输入持久化事务把捕获的 cycle ID 随提交结果直接返回给驱动 owner,不依赖提交后可能 失败的再次读取。迟到的失败兜底会在更新回执的事务内重新校验回执状态、gate、turn 绑定、 输入的预期周期及当前会话周期;本地未捕获 cycle 时,也只允许仍未消费且未绑定的情况。 已被替换的 owner 不能把原生恢复已接管的输入改为失败。
executionGate 固定 reason、recovery、executionRevision、cycleId 及可选 requestId/sourceId。类型化恢复值是诊断提示,不是授权。 普通 /resume 先校验既有 Work revision、Plan 和 Goal 授权、精确人工/外部等待、 认证、预算、blocked 状态及原生未知副作用核验证据;通过后才在同一事务中把匹配的 gated 且未应用 anchor 迁入新周期,并记录 session.input.execution_recovered, 不会重新准入或重复插入原文。真实 turn 绑定前 gate 仍可观察;绑定后由正常应用、 完成路径推进。此路径不会重置 failed、cancelled、applied、completed 回执, 也不会授权旧周期 callback。
单条旧误阻塞输入的显式修复
已分类的 HTTP 400 reasoning_replay_context_mismatch 只终结独立普通请求的工作周期, 不会锁住之后的新用户输入。它仍是不可原样重试的失败:不会自动重投、更改回放密文, 也不会让迟到报告复活旧周期。属于活跃 Goal 的失败仍由该 Goal 记录阻塞;认证、 未知永久错误、协议/配置错误、预算、审批和未知副作用保护保持原有边界。 该行为参考 Codex 9ba1d9eb5bbbd87ba2fc528d91ad239eea975ee9 的 codex-rs/core/src/session/turn_input.rs 中 User/Automatic/Recovery 分离, 不是将所有 HTTP 400 当成可重试错误。
安装二进制不会自动清除旧门禁。有界修复除了旧重试耗尽缺陷,还可以核验这一确切的 请求拒绝链,范围限于此前经审计的旧误阻塞修复所建立的 recovery 周期;必须同时匹配原生失败结算和请求 诊断。只恢复后续那条 recorded、从未绑定的新输入。旧失败输入即使没有 appliedAt, 也不能重置终态回执或重投。 连续继承同一门禁时,最多逐层核验 16 条中间输入的周期归属和六事件准入链, 要求从未绑定执行、执行 revision 逐次递增,再重新核验最初被拒绝的恢复请求。 中间周期只能增加由下一次已证明准入所保存的 scheduling 快照;任何夹入的执行、控制、 未知事件版本、等待条件变化或证据缺失都会拒绝修复。只重新绑定选中的最新输入, 更早的输入原样保留,并通过 inheritedInputIds 记录在修复证据中。
命令使用当前配置指向的既有数据库和确切的 session/input ID。默认只读检查,不创建或 迁移数据库;将 N 替换为检查返回的正整数 expectedRevision:
zuno session repair SESSION --input INPUT --dry-run
zuno session repair SESSION --input INPUT --apply --expected-revision N2
--apply 与 --dry-run 互斥,--expected-revision 只能和 --apply 一起使用。 应用必须离线并独占数据库:关闭所有持有该库的 ACP、TUI、server 和其他 Zuno 进程, 包括空闲连接。CLI 除原生恢复租约外还要求 SQLite exclusive locking,并在写事务中 重新验证完整证据。
只接受格式 15 中已 consumed、回执仍为 recorded、从未绑定或应用到 provider turn 的真实用户输入。有界原生事件必须证明确切旧误阻塞及其 cycle、turn、provider failure 和输入来源链。revision 改变、证据缺失或有歧义、并发工作、真实门禁和未知工具结果都 失败关闭;已完成 Goal 或看似空闲的客户端不能代替证明。
成功应用写入 session.repair.legacy_false_blocked_applied.1 并准入一个审计过的恢复 控制。原输入保持 consumed,只转交未应用 anchor,不重排队、不再次插入原文;真实 turn 绑定之前仍保留原 gate。它不重放工具、不修改 Goal、不重置终态回执,也不迁移格式。 成功返回 control_queued,只证明审计过的控制已排队,不证明输入已应用或 provider 成功。仅当同一个控制仍为 queued,且完整证据与执行快照均未改变时,重复调用才以 already_queued 幂等返回,不重复写入或准入。控制已推进或原输入已绑定/应用后, 修复拒绝重投;后续原生执行及其输入回执才是权威。 修复命令本身不会启动 provider 请求。
恢复与客户端观察
标准 ACP prompt 等待关联结果,其中包括已保存的执行门禁:门禁返回 -32005, error data 携带 admission: "accepted"、reason: "executionGated"、 recoveryRequired: true 和 receipt,客户端不得重发已保存输入。 同一消息 ID 的重复观察者在恢复交接空隙仍可收到相同 gate。 session/steer 即时返回接收,不证明已经采样。观察者断线不等于撤回,也不能证明旧执行已结束。
ACP 把携带普通 /resume 的 session/prompt 绑定到原生恢复控制输入(ctl_…) 及其 InputAdmissionReceipt。FIFO driver 由会话持有,使用独立于 RPC 观察者的 session-scoped client connection;观察者丢失不会丢弃 drive future,也不会隐式取消 驱动发起的客户端请求;这种独立性不意味着整个 ACP 连接、运行时或进程关闭后仍继续执行。 ClientConnection::session_scoped 仍共享断线状态。连接 EOF 最多等待 25 ms 以排空 已就绪的请求,随后运行时关闭会取消正在运行的恢复控制;其数据和 cancelled 回执 继续持久保留,session/load 不会重放该控制。 控制处于 Queued 只证明已准入,不代表完成:prompt 会等待关联 原生执行完成、等待人工输入、被取消或失败。回执与发布跟踪确保响应晚于关联结果及其 待发布更新,避免提前 end_turn 清除客户端 busy 状态和 Stop 控件。
显式 $/cancel_request 仍绑定该请求贡献的控制输入;如果执行已经开始,撤回会在 实际取消边界核对选中的输入。session/cancel 保留精确回合校验及旧客户端的当前目标 捕获,两条路径都不会给后续回合预置取消。无关回合的完成不能结算恢复控制的回执。
队列立即发送在同一准入事务中比较选中行的 revision 与界面显示的 turn id;失败则回滚行和事件, 不改投新回合。空闲时先持有 run guard 再提升选中行,其他条目保持原准入顺序。消费时校验信号 revision,用户消息和 consumed 状态提交后才发出 InputConsumed。完成边界会原子关闭准入; 若输入先赢得竞争,则继续同一 engine turn。普通根/子会话 Enter 排队,显式立即发送才 steer, 不调用 Stop,也不会把软检查点报告为硬中断。 HTTP 显示投影使用 turn.input.consumed;权威 inbox 迁移仍是 session.input.consumed, 不会用相同事件类型再写一次。
TUI 的 DraftRecovery 在准入回执之前保管文本、光标、选区、粘贴块和图片;拒绝时不覆盖 更新的草稿,也不自动重发。队列文字编辑保留图片;已准入的 steering 快照在消费前可取消, 但不再由 TUI 改写。输入框上下键在整个缓冲区两个端点浏览历史,内部移动光标,不滚动对话。 粘贴聚合先于快捷键分发,块内换行不是提交。选区和复制共用最后一帧的字素坐标映射。 Windows 剪贴板异步读写,优先 pwsh.exe,其次 powershell.exe;OSC52 仅报告请求已发送。
子会话和 job 创建在同一事务中继承父会话最新持久 memory policy。 SessionMemoryPolicyDefaults 只含默认值,不携带 revision;父策略 revision ≥ 1 合法, 子会话从自己的 revision 1 开始。不会重置父版本,也不需要改变数据库格式或重建数据库。
准入不与活跃回合租约竞争。同一个准入服务先提交 session_input 行,再决定它如何到达模型,因此每个界面(TUI、ACP、HTTP 与 run 宿主)对一条已经持久的输入只报告三种结果之一:调用方拿到独占回合租约并自己驱动该行;某个正在运行的回合以软中断接纳该行,并在下一个安全点提升它;或者该行保持待决,等下一次 FIFO 提升。先抢租约、抢不到就提前返回,正是那种「提示词丢失且没有任何持久痕迹」的做法,所以会话忙碌是准入的一种结果,而不是准入的失败。若调用方自己的驱动循环本就拥有该会话的每个回合,它根本不申请租约,只会收到 steered 或待决结果。
每个界面只解码自己能驱动的载荷形态,而每一种已发布形态只有一个解码器。驱动方无法渲染的待决行(HTTP 提示驱动遇到的排队 TUI 提交、终端驱动遇到的带自己 agent 与模型覆盖的 HTTP 请求体)会按 FIFO 顺序被跨过,保持待决交给拥有它的界面,而不是先提升再结算为 failed。已结算的异步报告与已回答的人工请求都是纯文本,所以每个提示驱动都会运行它们。没有任何写入方发布的载荷,出于同一个理由也保持待决:驱动方无法区分「无法识别的形态」与「自己本就不拥有的形态」,所以该行被保留并继续显示在队列里,而不是被销毁。因此在同一个会话上混用多个界面时,一条驱动方并不拥有的行不可能废掉该驱动方。
已结算的报告按批投递,而不是逐行投递。一次唤醒在同一个事务里认领父级当前所有待决报告,并在同一个回合里驱动它们:每条报告仍然生成自己的持久用户消息、自己的 session.input.promoted 与 session.input.consumed 事件,以及自己的 message.data.taskReport,只有 provider 请求是共享的。唤醒发现会话正忙时,同样把整批待决报告交给正在运行回合的下一个安全点;那个回合没来得及取走的报告保持待决,等下一次扫描。逐行各开一个回合的做法,会让同时结算的一次 fan-out 变成一串回合,而每个回合通报的都是批次内更晚的报告早已取代的状态。HTTP 提示驱动在同一个事务里认领同一个批次,因此在该界面上,持有三条已结算报告的会话同样只产生一个助手回合;而用户输入的提示词仍作为自己的请求运行,带自己的 agent 与模型覆盖。每个输入的飞行中租约仍按 (session_id, input_id) 保留:赢下回合的那次唤醒认领整批,输掉的唤醒发现自己那一行已被认领,直接返回而不驱动任何东西。
准入永不排在投递后面。后台命令的完成先作为持久 inbox 行准入,之后才请求回合;watcher 在同一轮里把排在它后面已经就绪的结算一并准入,因此同时结束的一组后台命令构成一个批次,重启后发现多条终态命令的进程也按一个批次投递。某条命令在投递回合已经运行时才结算,则立即准入并加入下一个批次——它的报告在结算之前无法存在。
渲染批次时报告按它们描述的工作分组。当一个批次里含同一个 job 或同一次后台执行的多条报告时,只有工作完成最晚的那条被呈现为该工作的当前状态,更早的在模型读到的文本里标注为已被取代。这个投影属于引擎而不属于某一个客户端,因此唤醒新开回合驱动的批次与唤醒交给正在运行回合的批次文本完全一致:无论更新的报告到达时父级是空闲还是繁忙,被取代的状态都读作已被取代,而现在与将来的每个客户端界面都从同一批持久行得到同一结论。Plan 对账以唤醒自己开出的那个回合所驱动批次里最新的报告为种子;被并入一个已经在运行的回合的批次作为持久用户输入进入该回合,而该回合保持它启动时的 planning 来源。分组只是对持久行的投影:不合并、不重排、不丢弃、也不改变任何 inbox 状态;每个工作单元只有一条报告的投递则完全按写入方原样呈现。已提升但持久 prompt 里没有模型可见文本的报告结算为 failed 并记录原因,而不是卡住排在它后面的报告。
图像入口在写入 inbox 前统一经过 AttachmentStore:规范化方向、像素与编码,原子发布当前数据库身份下的内容寻址对象,持久 part 只保存 ImageAttachmentRef。Provider 请求组装时才校验并内联对象;缺失或 digest 不符是永久持久状态失败,不回退原始路径。
提交转录回退(revert_commit)会删除暂存边界消息 (time_created, id) 之后的投影 session_message 行与旧表 message 行,清空会话的 context epoch,并把所有 queued、steering、promoted 的收件箱输入经常规取消迁移退役,每条各记一条 session.input.cancelled;已消费(consumed)的输入是不可变历史,不受影响。回退永不删除收件箱行。随后追加一条 session.reverted 事件,字段为:sessionID(字符串)、messageID(字符串,回退后仍是转录尾部的边界消息)、marker(对象,暂存的回退 JSON 原样,如 {"messageID": "...", "files": []})、boundaryTimeCreated(i64 毫秒)、removedMessageCount(u64,删除的投影行数)、removedLegacyMessageCount(u64,删除的旧表行数)、cancelledInputIDs(字符串数组,按准入顺序)、contextEpochCleared(布尔)、timeUpdated(i64 毫秒)。所有键始终存在。
自动记忆
常驻 Memory 默认自动维护,不需要逐条 approval。memory_read 返回有上限的当前条目及 revision;memory_update 通过宿主管理的 add/replace/remove 接口写入,不接收任意路径。 它的 ToolEffect::ManagedMemory 只免除通用 strict 副作用审批,显式工具 deny/ask、 会话 generation policy 和只读角色限制仍然生效。已有的 memory.promotion: review 配置继续复核。Skill 仍须显式复核、离线评估和应用。
原始提取只保存带来源的 Experience 与记忆建议。独立的 MemoryMaintainer 使用 project_aggregation 的 purpose: memory 任务,不依赖 Skill 模式聚合的数量门槛。 无工具权限的 consolidator 根据当前记忆、有效来源和用户纠正生成最多 32 项修改; 写入事务重新核验两个 scope 的 revision、任务租约、来源实际内容与 generation policy, 一次提交候选日志、版本、来源关系、任务终态和无变化水位。语义无效最多修复一次; 相同输入不在每次轮询重复调用模型。每个前台回合读取两个 scope 的同一 SQLite 快照, 旧 Prompt receipt 不受后续改写影响。
来源失效会立即隐藏失去全部支持的派生记忆。显式遗忘来源与撤回在同一事务中完成, 不会恢复纠正前的旧内容,不会删除仍有独立支持或用户主动确认的条目。 显式遗忘/undo 也限制后续自动重新写入。关闭生成不等于遗忘。记忆只是可出错的参考数据, 不能成为修改权限、配置或执行命令的授权。数据库格式 12 对格式 5–11 执行原子前向迁移, 保留原有数据和版本,不要求用户重建数据库。
详见 Memory 与学习。
Plan 与 Work 状态迁移
持久的 Goal、Plan、Todo、收件箱和 job 状态控制续跑,而不是自然语言。「接下来我会……」 这类文字不构成进展。默认 profile 发布类型化的宿主 Planning capability;即使最终工具 过滤隐藏了 plan_update,已有 Plan 仍会持久化、投影并在重启后恢复,但模型不能创建 或修改新的战略步骤。宿主只执行模式与持久状态策略:显式 plan 协作模式为 Required,已有 active Plan 为 Maintain,普通 Work 输入为 Optional,没有 active Plan 的宿主生成输入或空输入为 Atomic,隐藏工具为 Unavailable。图片、resource、 selection、branch diff 和多文本块在 Work 模式也只是结构化上下文,不会自动强制 Plan。
宿主不再解析「修改」「修复」「全部」、问句、确认语或其他中英文关键词来猜复杂度。Work 模式由模型依据完整会话判断:只有真正多步骤且需要协调、顺序、委派或恢复价值时才使用 Plan;直接完成简单或单步任务,绝不创建单步骤 Plan。因此「OK,你修改下吧」无需专门词表, 复杂任务仍可由模型主动建立 durable Plan。模型使用 create / patch / append / push / pop 操作维护 Plan,step id 由宿主生成,已有 Plan 修改都受 expected_revision 保护。每次 mutation 之前都要立即调用 plan_get 并复制其 当前 revision;只有 plan_get 返回 null 后的首次 create 可以省略。 plan_update、notes、history 这类以操作为标签的 参数枚举以单个对象 schema 发送给 provider:action 属性枚举全部操作,也是 schema 中唯一 必填的字段;每个操作自身需要的字段由类型化反序列化器校验。同一字段在不同操作间形状不同时, 若只是某个操作把它变为可选,就发送可为空的形式;若只有描述不同,则按操作归属各自的描述; 形状确实不同时发送 anyOf。
工作状态工具还参与两项类型化引擎契约。读取会公布 ToolProgressObservation,其指纹只包含权威 Plan/Todo 状态,不包含自由文本 intent; mutation 会公布 ToolDynamicContextRefresh::WorkPlan 或 WorkItems。若确实还要发送下一次 provider 请求,引擎把已提交连接交给宿主刷新器;CLI 宿主从 SQL 重新生成 Goal/Plan/Todo/Job 上下文,Plan mutation 还会把一次性的 Required 指令切换成 Maintain。
连续三次成功、单工具、同一工作状态指纹的读取会以 StagnantToolLoop 停止。第三次结果先 持久化并投影;换工具、失败/阻塞/中断、写入路径、continuation 或状态变化都会重置序列。 恢复策略是 Pause,活跃 Goal 记录 no_progress,不会自动重复同一付费读取。
机器执行阶段单独持久化为 DriverPhase,不进入用户可见 Plan,旧 reconciling 事件仍可读取。PlanReconciliationDriver 在真正的 provider final 后对账:普通 Work 返回 Finish,保留未完成或仍可执行的 Plan/Todo 步骤;宿主此前已处理前台等待和工具 续步。真实等待与保护门禁优先,只有 active owned Goal 返回 ContinueGoal。 Goal 的 progress fingerprint 与计数跨重启保留,连续三次相同仍暂停为 no_progress; 普通 final 不因 Plan 状态触发这个重试循环,也不制造通用人工确认或持久暂停。
若宿主为运行中的观察器记录了确切外部执行等待,driver 进入 waiting_background,结束当前回合,不轮询,也不创建通用人工问题。活跃 Goal 在观察器仍运行时不会立即再次自动续跑;已有后台 完成 watcher 会在进程结算后把终态报告写入 inbox 并唤醒会话,后续回合重新查询远端权威状态, 再恢复普通对账。
ACP 通过会话级投影器订阅 TurnHost::work_state_changes(),而不是识别某个工具名。 每次唤醒都会读取权威 Plan 并发送完整的 stable-V1 更新;(plan_id, revision) 阻止 重复与旧 revision 覆盖新状态,Plan 被移除时发送空 entries 清除客户端旧面板。 实时变更、prompt 结束前 flush、load、resume、后台 continuation 与 host 重建共用同一 投影器。
根会话消息是独立的持久输入能力。只有没有 parent tool authority 的 turn 才能看到 session_message;执行时还会再次确认来源是 root。同项目 root 可以互发,root 也可发送给 自己的后代;其他 root 的 child、归档目标、自己与跨项目目标都会被拒绝。TUI、ACP 与 server 消费同一个 sessionMessage 形状;在线进程可在安全点 steer,否则输入保持 queued。
原生会话命令、压缩与硬中断
会话命令、上下文压缩与硬中断都是原生能力,不依赖模型配合。
prelude 会在第一次 provider 请求之前检查持久历史。长工具回合不会等到下一回合才再次 判断:同一回合第一次之后的每个 provider 请求,都会在发送前按同一套 compaction policy 主动检查上下文。检查优先使用上一响应由 provider 报告的上下文用量;没有可用报告时, 回退到当前刚组装完成的 prompt 估算。达到阈值会发出 info 级稳定 notice code context.compact,并在写入 assistant checkpoint、prompt receipt 或 provider-request 记录之前返回 TurnError::CompactionRequired。宿主会在同一次 drive 内消费这个内部信号, 压缩持久历史、把落盘摘要投影给 live TUI/ACP,再直接重试;成功恢复不会产生终端 turn failure、不会增加 failed_turns、不会安排 Goal retry,也不需要等待下一条用户输入、子任务 报告或 timer wake。单次 host drive 最多自动恢复五次;超过上限或压缩本身永久失败时会 类型化失败关闭,而不是循环。compaction.auto: false 不会向回合挂载主动阈值,因此也会 关闭这些请求间检查;手动压缩和 provider 明确报告的上下文上限恢复不受影响。
provider 明确报告的上下文上限错误使用 CompactionTrigger::ContextLimit 进入同一条即时宿主恢复路径,保留 provider 的 used/limit 字段,并受既有五次 context-compaction 预算约束。进程重启后的 Goal retry 只用于恢复此前 已持久化的 context-limit 失败,不再是进程内主动阈值越界的正常路径。
生成检查点和压缩后续跑使用独立的、任务中立的模板。compaction/summary.md 保存真实目标、 约束与决策、已验证进展、进行中的工作、阻塞项、下一步和引用;重复压缩时继承上一份有效 摘要,并按较新的明确指令纠正旧信息。只要请求保留了有效检查点,compaction/continuation.md 就作为原生 runtime.compaction 段加入请求,覆盖手动压缩、新用户输入和重启后的请求。 这一设计吸收 Codex 的上下文交接方式和 OpenCode 的增量摘要规则,具体任务及权限仍由 用户输入、当前模式和 Zuno 的持久状态决定,不限定为修复,也不新增用户授权。
宿主在同一次 drive 压缩后的第一个请求之前重新读取 Goal/Plan/Todo/Job。若本次驱动期间 Plan 已更新,一次性的“创建或替换 Plan”要求会转为维护当前 Plan;没有更新则保留原要求。 压缩标记不会取代真实用户 anchor、经验检索文本或已完成回合的学习窗口起点,旧版没有 mode 字段的标记也会 被正确排除。续跑通过原生运行时上下文完成,不合成另一条人类消息。
摘要请求独立接收解析后的 compaction agent 系统提示;主 agent 的初始指令和常驻 Memory 不进入摘要输入,而是单独恢复。有效检查点会切断旧窗口的用量测量,后续仅使用新主请求的 数据,避免刚压缩后又因旧用量再次触发;缓存口径和单独记录的推理 token 也会正确计入。 hook 之后的完整请求、稳定段标识、 来源、摘要散列和运行上限会先记录为 session.compaction.prompt,摘要消息保存对应 receipt。辅助请求不会覆盖用于恢复 Skill 和执行来源的前台 prompt receipt。用量记录保留 分帧报告和缓存计数口径,单独记录的推理 token 不会再次计入可见输出。
只有非空、正常结束的响应才能发布检查点。重试回滚会清除该次尝试的残缺文本及用量; 缺少结束帧、长度截断、意外工具操作、超时或超出摘要字节上限都不会提交成功摘要。 响应流遵守 compaction.timeout_seconds、compaction.max_summary_bytes 和所属回合的 中断信号。取消会停止续跑,并保留后续重新压缩的能力。
创建标记与发布成功摘要分别使用原子事务,模型等待期间不持有事务。较新的失败或悬空 尝试不会撤销先前有效的边界;模型请求仅使用当前有效摘要,旧摘要和失败记录仍保留在库中。 下一次压缩显式传入上一份有效摘要,避免它落在原文保留区时漏掉更早的目标或并行工作。
工具调用配对信息和真实用户来源在文本转换之前保存,因此工具结果不会被计作用户回合, 压缩边界也不会拆散调用与结果;重复调用 ID 按前面的对应调用配对。较长的工具参数、输出 和推理使用有界首尾片段并标明省略,签名推理只保留标明性质的历史文本,加密推理不进入 摘要请求。原始持久证据以及保留区正常的 provider 工具协议保持不变。
自动压缩只在摘要落盘之后才咨询 auto-continue hook。该 hook 失败时会话保持 Compacted:摘要保留,不合成续跑回合(没有人投票就不授予续跑),失败原因随压缩结果以 auto_continue_hook_failure 记录并输出告警;会话不会被标记为失败,因此后续压缩不再被 AlreadyFailed 拒绝。工具 after-hook 失败同理:工具已经运行,结果保持自身状态与 is_error,hook 失败作为 afterHookError 元数据随结果返回,而不是被改写成一条会让模型 以为副作用没有发生、进而重复执行的裸错误。
工具取消的确定性
硬中断到来时,正在运行的工具会得到两秒的协作式清理窗口。在该窗口内结算,产生一次带类型的 cooperative 取消并保留工具自己的最终报告;窗口耗尽则强制中止该次调用,记为 forced 加 uncertain,重试前必须检查权威状态。forced 只表示宽限窗口已耗尽。
协作式取消并不自动等于结果确定。工作尚未得出结论就被停下的工具,会在其已结算结果的 cancellation metadata 键上声明这一点,dispatcher 也会把该次调用记为 uncertain,并把要求 检查权威状态的那句话追加到模型读到的报告里;没有作出声明的工具保持原有的确定读法,文本也不 改动。
shell 同时承载两种读法,而区分它们的是服务是否把某个状态结算为命令自己的判定,而不 只是进程有没有退出:取消被处理之前就已完成并报告了自己判定的运行,报告该退出状态并拿到正常 完成的运行所应得的凭据,只标记为已取消而不是不确定。其余每一次被取消的运行都保留已捕获的 输出、携带没有 exit authority 的 unresolved 凭据,并且属于不确定——包括那些确实报告了数字的 情况:属于子进程守护器自身失败的 exit 125、在硬上限处被杀掉的运行,以及运行报告过但被结算 为并非命令自身结局的状态。报告出的退出码仍留在结果里,因为那正是终端会显示的内容,所以一次 不确定的取消也可能给出退出码;拒绝为它背书的是凭据。两种读法都绝不会被机械重放。
解析出的判定随 ToolDispatchInterrupted 运行时事件一起发布,而不只是写入持久记录,因此 SSE 的 tool.dispatch.interrupted 载荷、ACP session update 与 zuno run 发布的 uncertain 与重放会话从持久 metadata 重建出的判定一致。
原生 History 与 Notes
zuno-continuity 是通过 ProfileBundle 与 ToolContributions 挂载的原生组件,默认关闭。 history 只读取当前会话,并以成功压缩作为窗口边界;reasoning、加密值、合成内部提示 正文和二进制附件字节不会返回。notes 使用逻辑文档名,按 session_id + Agent 隔离。
Notes 每个作用域最多 100 个文档,单文档 256 KiB,总计 1 MiB。写入必须带精确 expected_revision,并用可信 call_id、请求摘要和 revision 做幂等与并发冲突保护。 读取采用 Safe + ParallelSafe + ReadOnly,写入采用 Never + Exclusive + SideEffecting;非法 action 按最严格策略失败即拒绝。
组件自有的 session_note 与 session_note_operation 是增量表。它们随 session 级联删除,并进入 session export/import、sanitize 与 prune。TUI、server、ACP 和 child turn 都消费同一套运行时工具快照,不拥有私有连续性逻辑。
持久 Goal 恢复
活跃的 Goal 会持续推进,直到它完成、被显式暂停或阻塞、达到预算上限,或遇到类型化的永久失败。
失败归属在执行前冻结为 TurnFailureScope:cycle、可选 turn 和可选 Goal ID。结算事务 重新核对这个确切组合及未停止的周期;迟到失败不能接管新用户周期或被替换的 Goal。 既有类型化等待、受保护暂停和未知工具结果优先保留。
普通周期耗尽有界 provider 重试次数或恢复窗口后,可恢复失败只关闭该周期,保留失败 回执,不阻塞无关的新输入,也不改写残留 Goal。只有失败周期确切拥有且仍 active 的 Goal 才为同类失败保存退避重试。真正的 turn_budget、认证、审批、未知副作用或永久阻塞仍 保留对应门禁;provider 重试窗口耗尽不等于可以解除这些保护。
中断保持 Goal 暂停。宿主参考 Codex 的 Goal 菜单,提供 Resume goal / Keep paused: 跳过不授权。Zuno 用 QuestionPort 保存选择,通过 GoalResumeRequest 绑定 Goal ID/revision 和已有输入 ID; 确认才在一个事务中衔接 Goal、执行门禁与输入,不重投已处理文本。 Plan、审批、预算、认证及未知副作用仍由各自控制处理;记录 callback 也不代表已进入模型。 提示词显示真实 Goal 状态、暂停原因和恢复条件。
报告入库与模型续跑是两个边界。观察者即使正常退出,完成凭据已由 callback 领取、 历史输入已为 consumed,也不等于模型已处理。空闲报告路径在保存整批报告后,在同一 写事务中重新核对周期和唤醒权限,再满足精确的外部等待;实时注入使用相同的归属规则。 只有当前工作周期绑定的 Goal 必须活跃。明确 goal_id=None 的普通周期不会被残留的 暂停、已完成或预算受限 Goal 阻止,也不会消耗其预算或将其恢复。 没有持久 scope 的旧数据不等于明确无 Goal,仍保留保守的非活跃 Goal 门禁。 停止周期、审批、预算、认证和未知副作用保护不变;旧完成结果不能擅自启动新周期。 混合批次中的规划文本、来源与完成身份必须来自同一条合法报告,被拒绝的报告只作为历史。 不能续跑时使用 report_deferred_by_execution_state,说明当前周期没有自动续跑权限, 不再一律要求普通会话恢复 Goal。
设计参考 Codex 9ba1d9eb5b 的通知与自动回合接纳分离,以及 Goal 自身的续跑条件; Zuno 保留有授权的 nextStep 唤醒,不照搬 Codex 的通知触发或 Plan 模式策略。
独立查询不会恢复旧 Goal。显式恢复通过 Goal ID/revision 校验后,才登记原 Goal 报告周期 向当前执行周期的转交;原始完成凭据和一次消费约束不变,已处理输入不重投。普通 callback 或新消息不能自行登记这项授权。
Goal continuation 是一等的回合来源。准备阶段会捕获确切 Goal id 与 revision;provider 工作开始前若 revision 已变化,这份 continuation 会失效。回合执行身份独立地从当前 host 捕获 Agent、目录 provider 与目录模型。保留的 user 历史只提供因果 transcript anchor, 不授予权限;Zuno 不会为了让重配置后的 host 看起来像历史状态而改写它,其中旧的 Agent/模型字段也不能再路由自动 Goal 回合。普通用户回合仍使用自身消息里的身份。 自动 Goal continuation 的第一次 provider 请求也会从 SQLite 注入与普通回合同源的 runtime.work_state,不会依赖旧的状态工具调用恢复当前 Plan/Todo。
Goal 完成审计与 Plan 写入方共用同一个 step status 类型。completed 与 superseded 都是终态;缺失、未知或旧的 cancelled 值会明确按持久 Plan 损坏失败关闭。模型在一次 goal_update 中结算 criteria 并完成 Goal 时,两者位于同一事务;审计拒绝会同时回滚 checklist 与 Goal revision。
用户创建且声明、台账均为空的 Goal,修改文件后仍可完成;非空声明与台账不一致则失败关闭。 已声明检查项的证据、新鲜度、所有权和未知副作用审计保持不变,模型不能从独立输入关闭 paused Goal。阻塞工具返回暂存状态、真实 Goal 状态和已完成回合计数;宿主按可信 Goal/cycle/turn 身份计数,观察、状态、历史及幂等结算回执原子提交。
Engine 在解析当前身份之后、发送 provider 请求之前写入一条 session.turn.started.1。事件记录 turnTrigger、anchorMessageID、Agent、provider 与 模型;Goal 回合还记录 goalID 与 goalRevision。provider attempt 事件重复 Goal 的触发 类型与已解析身份,使重试证据本身也完整。Agent 或模型解析失败时, session.turn.rejected.1 会在 Goal 被阻塞前记录请求身份与类型化失败,不会伪造 started 或 provider-attempt 事件。
原生 /goal <目标> 创建或编辑成功后,会把这次宿主命令标记为完整的 idle edge,并立即 交给共享 Goal continuation driver。若会话还没有 user message,driver 会先把目标本身 通过持久 inbox 准入为首个 user turn anchor;字面的斜杠控制文本不会进入 provider。 目标变化也会同步活跃 Plan:多阶段目标归档此前可见 Plan 并安装绑定当前 goal_id 的新根 Plan;原子目标不改绑已终态的历史 Plan,属于上一个 Goal 的终态 Plan 会在目标变化时、或仍 可见时在下一次宿主规划决策时归档为已完成的历史,完成审计不会再拿它对账新 Goal。
ACP load/resume 会重建运行时、按请求重放持久投影,然后通过 detached continuation observer 调度 active 根 Goal。恢复任务与普通 prompt 共用会话执行门,因此不会并发启动 第二个 Goal 回合;0.6.0 已落盘但未产生首个 user message 的 Goal 也会在此路径补齐并续跑。
可恢复的 provider、网络、流、SQLite 争用、Agent 步数上限和符合条件的工具失败,会在等待前先持久化一次指数退避重试。进程重启后从 SQLite 重建截止时间。
重试延迟是正数、有上限、带抖动,并且可被用户输入打断。有效的对端 Retry-After 会被限制到配置上限,且绝不会被更早的本地延迟替换。同请求 recovery window 从首次可重试 provider 失败返回后开始;对端要求的延迟超过剩余窗口时,回合以对端自身的类型化错误结束,Goal 级重试等待的是对端值按 max_delay_ms 截断后的结果,不再退回更短的本地退避。本地退避若超过窗口,则以 provider_retry_deadline 结束,并持久化最后一次安全诊断的状态码、错误码、请求 ID、原因,以及恢复耗时和总耗时。
重试决策使用类型化错误,而非渲染后的消息。认证失败与用户中断导致暂停;无效协议、损坏的持久状态和永久性配置失败导致阻塞。
围绕不可重放副作用的超时或响应丢失以 uncertain_side_effect 暂停:恢复必须检查权威状态, 绝不自动再次调用该工具。这份义务落在工具记录上,而不是 pause 上——dispatcher 在让结果对 模型可见的同一条语句里写入 state.outcome = "uncertain" 与 state.uncertain(工具名、 call id、该调用报告已改动的路径、类型化 cause 取 lost_outcome 或 interrupted,以及 observedAtMs)。进程若死在这次写入之后、pause 行落盘之前,Goal 仍然拒绝运行:下一次 continuation 会查询当前目标下仍待处理的记录并再次暂停。state.uncertain.reconciledAtMs 缺失的时长,恰好等于这次检查被拖欠的时长;查询范围由 Goal 自己的 created_at_ms 界定, 所以新目标不会继承上一个目标的义务。
通用 session/Goal 恢复不会写入这个标记。/inspect-outcome 是不经过模型的原生文件检查 命令。具体文件实现授权后、执行前写入 native.filesystem.intent,绑定实际实现、 canonical workspace、完整解析后的目标、schema/参数摘要及原 part/call/turn/cycle。 只有工具名、上报路径或普通成功命令回执不够。检查器在 Unix/Windows 上执行有界的 句柄锚定读取,再原子保存观察事件及确切 part 标记。真实 native session lease 持有至 结算完成,前端断线也不能提前释放;模型调用仍须匹配活跃 turn。原生检查可以读取已停周期, 但不会重新打开它。report-delivery alias 不授予工具调用权限。缺少原生记录的旧调用、 Shell 和远程操作失败关闭。检查保留原 uncertain outcome,不授权重放或自动恢复。
不可重放工具(含远程 MCP 调用)返回类型化 timeout、network-timeout 或 transient 错误时,也会形成逐调用的 lost_outcome 未决记录,而不是进入重试队列。同名工具后来 成功不能消除较早调用的检查义务;明确允许安全重放的工具仍使用退避。经预算接口返回的 未知结果停止仍保留 uncertain_side_effect,不会误标为 turn_budget 耗尽。
对于 Goal 已因未知结果暂停、却缺少结构化调用记录的旧会话,/inspect-outcome 返回 goalPause 和 legacyUncertaintyWithoutCallRecords: true;pending 为空不代表已经安全。 新生成的输入门禁诊断可根据类型化 Goal 暂停原因解释旧的笼统 blocked,但不修改执行 门禁、不恢复 Goal,也不重写已有的冻结回执。远程状态仍需权威核验,不能用重发消息、 重启或通用数据库修复来代替。
未知结果暂停下,真实的新用户消息可以进入禁用全部工具的讨论回合。原生会话服务 核验输入、门禁 revision、独立周期、Goal 和其他保护条件后,原子绑定一次回合并记录 session.discussion.started.1;不会更改原执行暂停或 Goal。引擎在请求 hook 后清除当前 工具声明,同时保留历史调用和密封回放;模型若尝试调用工具,在进入 dispatcher 前即失败。 宿主跳过 Work 前置处理、按消息加载 skill、Plan 对账和 Goal 自动续跑,仍使用现有模型与 回合预算检查。可解释和提供方案,不可编辑文件、操作浏览器、调用 MCP 或委派任务。 认证、待决审批、Plan 授权、已耗尽/未知的 Goal 预算及明确 turn-budget 门禁仍然阻塞。
ACP 重新打开会话后,也可认领最新一条已 consumed、从未绑定回合的用户咨询。不会重插入 原文或重跑 failed/applied 输入。回执 applied/completed 只表示这次讨论已进入模型并完成, 不表示原未知结果已核验。宿主输入 ID 与转录 message ID 通过持久映射关联,不按文本去重, 也不假设 ID 前缀。未决绘图等工作仍须按原生安全流程核验后,才能显式恢复。
读取或记账 Goal 预算时遇到 SQLite 争用(SQLITE_BUSY)会持久化一次 database_busy 指数退避重试,Goal 保持活跃,而不是以 turn_budget 暂停;其他数据库失败仍以 usage_unknown 停止回合并暂停 Goal;本构建无法读取的持久状态仍然阻塞。CLI 回合中的 Plan 对账驱动、human request 创建与重试上下文压缩标记路径也经同一 GoalTerminalFailure::from_db_error 规则分类:争用现在以 database_busy 重试,过去则以 host_permanent 阻塞。
timeout、headerTimeout、chunkTimeout 只有 OpenAI-compatible 传输会读取,其默认值是 330 秒响应头截止时间与 120 秒分片空闲上限。原生的 OpenAI、Anthropic、Google、Bedrock 四个 provider 不读这三个键:它们固定采用 330 秒响应头截止时间,不设整请求截止时间(一个 合理的长回合没有 provider 能事先知道的上限),分片阶段则沿用共享的 300 秒流空闲上限, 该上限由 ZUNO_STREAM_IDLE_TIMEOUT_SECS 对所有 provider 统一调整。原生请求在收到第一个 响应头之前卡住时,现在会在上限处以类型化错误失败,而不是一直等到用户中断。
Provider 条目中的类型化 retry 块不是 SDK option。max_attempts 包含首次请求, recovery_window_ms 则只在首次请求返回可重试错误后开始;rollback、退避和所有替代尝试 必须在窗口内完成。默认值是 3 次尝试、180 秒恢复窗口、2 秒初始延迟、30 秒最大延迟和 20% 抖动。策略随解析后的 provider 冻结,绝不会发送给上游。
主会话、子会话和恢复路径与内部请求保留同一个完整的模型解析结果,不能只复制 Spec 后 让重试策略、价格或协议面落回默认值。Agent 采样参数和父级推理参数只覆盖各自字段。 修改配置仍须重新解析宿主/模型,不是运行中直接变更配置。
失败尝试把有界、脱敏的 status、code、requestID、reason 保存为 providerDiagnostic。恢复超时保留最后一次已返回失败的 lastProviderFailure, 包括 transient 503 上实际捕获的错误码;取消后续请求不会抹掉它,也不能说被取消的请求 返回了同一个 HTTP 状态。终态请求事件及 ACP 处理回执的错误文字携带同一安全信息。 缺失事实保持未知,不保存原始响应体或凭据,不依据诊断文字决定重试,也不回填旧回执。
请求及重试耗时与整个 child task 的墙上时间分开统计。直接 oracle 任务可能先成功 完成九轮 provider 请求,最后一条请求及重试再消耗 510 秒;这个最终请求诊断不限制 此前各轮、工具执行或整个任务的总耗时。缺少最后的 provider code 不表示此前没有 成功的 provider 请求。
流在没有终止标记的情况下结束,属于上游流不完整,而不是一次完成的回答。每个原生解码器都会 报出携带 upstream_stream_incomplete 的 ProviderError::Stream,因此它可重试并允许替换 已产生的部分输出:引擎发出 RetryRollback,丢弃被截断的流写出的内容,然后重放原样的请求。 一个终止标记就足够,所以只发 finish_reason、或只发 [DONE] 的 Chat Completions 流都算 正常完成。
- 被自身预算策略停下的回合以
turn_budget暂停。额度属于单个回合,Goal 保留剩余的 token 预算,但不会自动续跑:下一回合只会以同样的方式花掉同样的额度。这与 Goal 整体 预算耗尽的budget_limited状态不同。 - 预算策略可以要求压缩而不是停止。这被归类为上下文上限失败并走同一条路径:压缩保留的 历史,然后重试该回合。
- 多步骤工具回合中的主动上下文阈值同样走这条类型化路径。第一次之后的每次 provider 请求都会在发送前检查:有 provider 报告时使用上一响应的上下文用量,否则使用当前 assembled prompt estimate;触顶时发出
context.compact,压缩保留历史并重试。compaction.auto: false会关闭这项主动触发。 - 每次 provider 请求前后都会咨询回合预算策略。默认 profile 发布
TurnAllowance::UNLIMITED,不会为未设预算的 Goal 猜测 token 上限,也不会默认设置工具 调用次数或墙上时间上限。goal.default_token_budget可为没有自身token_budget的 Goal 配置宿主兜底;Goal 自己的显式预算始终优先。自定义 profile 还可设置工具调用次数与墙上时间 上限;两者无论有没有 Goal 都生效,触顶时以tool_call_budget或time_budget停止。 在用户显式设定的 Goal 预算下,用量不可测时以usage_unknown停止;配置的宿主兜底额度 则继续执行,只按已计数的用量生效。上限优先于压缩请求或继续执行,但让位于 Goal 自身已产生的停止。 每次停止都是类型化的TurnError::BudgetLimited,并以notice事件 (code 为budget.<kind>)投影给客户端;压缩请求的 code 为budget.compact。
自动 Goal 续跑会检查 provider 真正保留的历史,而不是整个 message 表。若压缩边界从 assistant 消息开始,导致保留后缀里没有真实 user turn,宿主会在下一轮前把 Goal objective 重新持久化为 user anchor;合成的 compaction marker 永远不被当成用户授权。
Council 席位预算与终态
第一方 pack 拥有的 balanced-review Council 总截止时间为 600000 ms, 其中预留 60000 ms 合成;三个席位共享自 Council 开始执行起的 540000 ms 截止时间。这只替换内置 preset 原先的 180000/60000 ms 分配,自定义 preset descriptor 保留自己的预算。Descriptor 冻结在 capability snapshot 中,用户 JSON 配置和面向模型的工具参数均不能覆盖这些预算。
每次席位尝试前,host 计算实际剩余硬时间及对应的 UTC 截止时间(Unix 毫秒),并将 两者写入持久化任务提示。委派排队和重试消耗同一席位阶段预算。Host 的单调时钟截止时间执行硬限制, 提示只告知时间,不授予额外时间。范围指令要求按规定报告格式优先返回有证据的局部结论 及明确未知项,不要求扫完整个仓库。
席位执行状态与报告有效性是两份事实。取消、失败、超时或不确定的席位可能已有成功 provider 请求和已完成工具,却没有被接受的终态报告。超时结算保留已经存在的 child session 身份与已记录的 usage/progress。Quorum 只统计有效终态报告, 0/2 不等于所有 provider 请求失败。Council 成功仍要求满足 quorum 且合成成功, 已有进展不能替代这两项要求。只有原生 Completed 但结构化报告 invalid 时,才允许 在 preset 的重试次数和同一截止时间内再次尝试。原生 failed/cancelled/uncertain 结果与 host Err 直接结算而不重放;provider 的同请求恢复保持独立。终态失败的 task 或 Council 不会自动重放。
状态边界采纳自本地 Codex 9ba1d9eb5bbbd87ba2fc528d91ad239eea975ee9,不复制其超时常量。 codex-rs/core/src/guardian/review_session.rs:1573 的 run_before_review_deadline 区分 TimedOut 与外部取消的 Aborted;1591 行的 run_before_review_deadline_with_cancel 在这两种结果下都通知所拥有的 child 取消。 另一条观察路径 codex-rs/core/src/tools/handlers/multi_agents/wait.rs:191 在未观察到 终态时返回 timed_out;multi_agents_tests.rs:3196 的 wait_agent_times_out_when_status_is_not_final 验证该结果,不据此伪造远端失败。 Zuno 保留“等待结束”和“拥有执行权的截止时间取消工作”的区别;Council 预算、 持久 child 回执、quorum 与合成要求仍由 Zuno 自身定义。
文件工具的路径权威
read、write、edit、apply_patch 在授权时解析路径一次,随后通过解析过程保留下来的 目录句柄执行操作。解析从授权边界开始 —— 工作区根目录,或一个被显式授予的外部目录 —— 逐段向下,打开每一段时都不跟随符号链接并要求它是目录,且绝不第二次解析这个名字。这条 性质是精确的:调用要么到达用户批准的那个目录对象,要么失败。最后一段是符号链接是唯一 有意的例外:它在授权之前被跟随一次,因此用户批准的是目标文件,而链接本身在写入之后 保留。
两个更弱的修法被否决了。只拒绝跟随最后一段没有任何作用,因为被替换掉的对象是中间目录。 在授权之后重新 canonicalize 仍然是「检查」与「使用」分离,窗口只是被挪了个位置。
机制按平台不同,保证也不相等:
| 目标平台 | 机制 | 窗口 |
|---|---|---|
| Linux | 每一段都经 /proc/self/fd/{fd}/{segment} 打开,内核相对被固定的描述符解析 | 已关闭 |
| macOS | 用 O_NOFOLLOW_ANY 打开 root/relative,发布之前再核对文件身份 | 收窄:rename 无法携带该标志 |
| Windows | 带 FILE_FLAG_OPEN_REPARSE_POINT 逐段遍历,拒绝任何带 FILE_ATTRIBUTE_REPARSE_POINT 的分段,发布之前再核对卷序列号与文件索引 | 收窄 |
| 其他目标平台 | 同样的逐段遍历,改用 symlink_metadata 拒绝,发布前做同样的复核 | 收窄 |
只有 Linux 完全关闭了这个窗口,因为只有 Linux 提供了在不使用第一方 unsafe(本工作区 禁止)的前提下相对一个打开的描述符命名路径的办法。因此 openat2、renameat 与 GetFinalPathNameByHandleW 都没有使用。逐段拒绝 reparse point 同时覆盖 Windows 的目录 junction 与符号链接,这一点很重要,因为 junction 在那里才是更常见的攻击形态。
发布环节区分两种失败,因为它们不该拿到同一张收据。在「让新内容可见的那次 rename」之前 发生的失败,目标文件仍然保有它原本的字节,报为普通的工具失败。那次 rename 部分完成后 失败、或结果丢失,则报为 Uncertain,并带上已经生效的路径。这个结果不可重试、绝不重放: 它要求检查权威状态,与快照存储对一次半途而废的恢复所采用的规则相同。当目标是符号链接时, 替换还会拒绝长度超过 40 的链接链而不是继续跟随,因为到那个长度这已经是一个环,而不是 有意的重定向。
原生搜索与 Shell 隔离
搜索把遍历委托给 ripgrep 本身,Zuno 不维护第二个 ripgrep 兼容的遍历器。发现是惰性的,因此其他命令与工具不需要 ripgrep;只有在调用 glob 或 grep 时才要求 PATH 上有 14 或更新的主版本 rg(或由分发方随 Zuno 一起打包)。缺少或版本不受支持时是带类型的工具错误,而不是静默回退,也不会妨碍 Zuno 启动。
发现结果会被缓存,但不对称。成功的解析保留整个进程生命周期,因此 session 重新挂载与 子回合不会反复 spawn rg --version。失败只保留五秒,随后重新探测。这个不对称是有意的: ripgrep 只支撑 glob 与 grep,所以在会话进行中安装它的用户必须能不重启 Zuno 就让这 两个工具可用;而一个在没装 rg 的机器上反复调用 grep 的模型,不能每次调用都 spawn 一次探测。并发的首批调用方之间只做一次探测,探测过程 panic 也不会让 ripgrep 永久不可用。
Skill catalog 热更新
运行中的 Skill catalog 只监听精确的项目 Skill 根目录,不把整个 worktree 作为监听根。 从会话目录到 worktree 的每一层,在未禁用项目配置时都会保留 .zuno/skill 逻辑根, 在未禁用外部 Skill 时都会保留 .agents/skills 逻辑根。规范用户根目录 $XDG_CONFIG_HOME/zuno/skill 与显式配置路径也会在尚不存在时保留。
根目录尚不存在时,只非递归监听最近的已有父目录;目录出现后,消费任务会在 native watcher 回调之外安全地逐级收窄订阅,并且只在精确根目录存在时开启递归监听。ignore 过滤属于事件策略,不能替代有界的 native 注册;Zuno 不会为了发现未来的 Skill 根而 递归监听整个 worktree。
Zuno 不监听 ~/.zuno 或远端 Skill 缓存;缓存只在配置远端索引并实际下载时按需创建。 标准共享根 ~/.agents/skills 只在启动时已经存在时监听,其他共享目录需要通过 skills.paths 显式配置以支持运行中安装。
Shell 的约束取决于所选后端。没有显式后端、降级、网络拒绝或路径约束时,Windows/macOS 默认原生执行(platform_native),Linux 默认自动发现约束后端。显式 backend: auto 仍要求兑现 read-only/workspace-write,不可用时拒绝;受信 run-unconfined 仍仅适用于 具备写能力的合格不可用错误。显式 sandbox.backend: native 记录为 trusted_native。 各类原生执行均保留权限模式,但不是 OS 隔离。执行权限记录写版本 4,读取兼容版本 2、3, 未来或非法版本拒绝;executionReady 与约束 ready 分开。Agent/Plan/Work 切换先预检目标, 再改持久状态;Start Work 校验预检的 execution revision。详见 权限与沙箱。
Linux bubblewrap 后端的发现结果按进程缓存,键为规范化 workspace 加 helper 可执行文件;每次命中前重新校验可信 launcher、可信 true 与 helper 在磁盘上的身份,校验失败即逐出并重新探测;发现失败绝不缓存,缓存也不跨进程持久化。zuno debug sandbox 与部署报告绕过缓存,始终重新探测。
常驻进程约束
当唯一有用的下一步是等待结果时,主线默认保持前台,包括单条命令、子任务结果及串行 CI/发版/部署依赖。耗时长或任务数量少都不是转后台的依据。 backgroundPurpose: "remoteObserver" 只说明观察目的,前后台均可使用,不会开启 background: true。只有明确拆分并行工作,且派发前已确定主线要做的另一项独立工作, 才分离到后台;派发后实际推进该主线工作。不能把唯一任务放到后台后就结束主回合。 这是提示词与 schema 指引,不按任务数量新增硬性工具拦截。
参考 Codex 9ba1d9eb5b 的 unified exec/write_stdin 同句柄续等:观察窗口返回不等于 任务完成,也不要求新建观察者 Agent。Zuno 保留现有前台句柄、事件驱动等待与 steer/中断能力,不复制其超时常量或工具参数。前台逻辑等待不应冻结界面, 也不应靠结束主回合、反复调用模型或重启命令来代替等待。 Codex 的 core/src/tools/handlers/multi_agents_spec.rs 先区分关键路径与并行子任务, 要求委派前确定主线下一项工作、保留即时阻塞工作,并在子任务运行时推进不重叠的本地工作; 这也是 Zuno 明确并行拆分指引的源码依据。
常驻进程在声明的约束内运行,其生命周期由运行时拥有。
Unix PTY 通过前台守护进程拥有进程组与终端前台切换。Windows ConPTY 则直接启动请求的 终端程序:不能把常驻 Job Object 守护器嵌入 ConPTY,否则交互输入与自然退出都可能无法 收敛。PTY 所有者持有直接子进程 PID,先应答并移除后端的一次性继承游标查询,再向客户端 转发终端输出;关闭 writer/master 后发布退出,显式停止通过 taskkill /T 终止完整 子进程树。
Windows 上的守护器不再轮询 tasklist,而是通过绝对路径 %SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe 启动一个持有真实进程句柄、 等待父进程退出的助手,armed 之后 PID 复用无法冒充父进程。助手在 payload 启动之前武装: 无法启动时守护器以 125 关闭,payload 不会启动;助手启动后无判定地结束时只写一条诊断, payload 继续运行并仅监督其自身退出,丢失的助手绝不视为父进程已死。
启动 zuno 本身不会往这棵树里插入任何进程,CLI 自身的启动也不是被守护的进程。客户端无论 启动 zuno acp、zuno serve 还是任何其他调用,在所有受支持平台上监督的都只有一个进程: 它启动的那个进程就是执行命令的进程,结束它就结束命令,命令的 stdout 与 stderr 也随之 到达文件末尾。启动过程把全局选项与 ZUNO_* 变量解析成一个进程内的值;Unix 上还会替换一次 自身镜像并保持相同的进程 ID,好让之后启动的程序继承这些值,没有镜像替换的平台则直接在调用方 派生的那个进程里分派命令,不会派生第二个进程。任何一次 zuno 调用都不会再启动第二个 zuno 来携带这份环境,因此启动 Zuno 永远不会武装上面那个 PowerShell 父进程监视助手,Windows PowerShell 仍然只是那个守护器的后端依赖,而不是运行 CLI 的依赖。参见一次调用就是一个进程。
后台命令执行
后台命令有独立的生命周期与输出游标,父会话通过持久状态观察它们,而不是靠轮询。
前台观察超时只返回同一进程句柄,不自动切换为后台任务。现有 bg wait/output 可以 继续等待该句柄;宿主保持逻辑任务、原周期与同一预算,在下一次模型请求之前等待真实 进程/控制通知,支持 steer 和中断,不让模型反复查询无变化状态。只有显式 background: true 才进入后台交付。串行 CI 默认前台,仅在主线另有独立工作可推进的 明确并行拆分中使用后台。
前台终态输出、原工具验证凭据与消费权原子落库后才回收句柄,不再额外产生 callback。 观察超时不等于远端失败,进程丢失不等于成功;原有硬时限与预算继续有效。
bg 工具为当前会话拥有的执行提供 list、output、wait、cancel,另有 artifact 用于读回被输出上限从该会话任一工具那里扣留的输出。output、wait、artifact 都接受可选的 cursor 与 limit,并返回下一个窗口的起始游标:limit 默认 16384 字节并被夹取到 51200 (默认输出字节上限的数值,不随配置的 tool_output.max_bytes 变化),超限是夹取而不是报错, 因此一次读回绝不会比内联结果给出更多字节。早于 2 MiB 常驻环形缓冲的游标会落到执行的磁盘 .output 文件上并报告 fromDisk,让已被丢弃的前缀重新可达,而不是把请求向前夹取。 artifact 是扣留通知点名的读回方式,也是唯一一种既不重跑产生这些字节的调用、又不让窗口再次 经过扣留它的输出上限的读法:隐藏 bg 的 agent 配置会让自己被扣留的输出只能靠 accept_large_output: true 重跑那次调用才能读到。
每次执行都在子进程守护器之后运行,因此退出码 125、126、127 可能属于守护器而非命令:125 表示守护器自身失败、命令结局未知,shell 工具报告不确定结果且绝不重放;126、127 表示程序 从未启动,退出码被记录但没有 exit authority。只有捕获输出中出现守护器自身的诊断行时, 保留码才被读作守护器的判定,普通程序自行 exit 125 仍保留权威收据。信号致死时守护器在 自身重放同一信号,收据没有退出码,显示为「killed by a signal」而不是 exit 1。
只读调查的报告产物
report_write 是独立于工作区编辑的原生输出能力。宿主接收最多 1 MiB 的报告正文和 可移植的纯文件名,在 .zuno/reports/ 选择不可覆盖的路径,并复用 anchored file writer 拒绝符号链接越界。该能力不会授予 Shell 或源码写权限。
最终工具快照暴露此能力时才生成 runtime.reports 提示词分段;runtime.read_only 说明当前尝试的 Shell 约束,避免把子会话的只读拒绝误判为宿主磁盘只读。父子工具交集、 显式工具白名单及权限规则仍然生效。
子任务报告元数据 schema 3 的 artifacts 数组只从本次 Job 证据边界之后的 report_write receipt 构建,并验证子会话归属。前台结果、后台 Job 结算、父收件箱交付 和客户端重放使用同一份元数据。报告作为交付产物保留,会话清理不会自动删除。
后台子 Agent 与产品 Agent
工具执行默认是至多一次。ToolReplayPolicy::Never 是默认值;只有显式声明为只读或幂等的工具才可以声明 Safe。
副作用附近的超时或响应丢失属于结果不确定。这种情况会被持久化,要求检查权威状态,绝不机械重放调用。
subagent_model_selection 默认关闭。开启后,精确 model allowlist 会在 profile 激活时解析,并按 session 持久冻结为带 digest 的策略;task 才会出现可选 model/effort。续跑不能改变首次冻结的模型或强度。
并发网络搜索
web_search 接受一批查询,并在单查询 provider 之上拥有并发、取消、稳定排序、限流与 URL 去重。
网络出口
网络出口受沙箱的网络授权控制。deny 会创建私有网络命名空间并拒绝网络系统调用,而不是一条可被绕过的防火墙规则。
模型 provider 请求、Zuno 自管认证、目录、远程 instructions、远程 MCP 与 web tools 共用 zuno-network。AWS 凭据发现是有意的例外:zuno-aws-auth 把标准凭据链、刷新与 SigV4 签名交给 AWS Rust SDK;签名后的 Bedrock 模型请求仍通过 zuno-network,因此继续 遵循进程代理。IAM Identity Center、STS、web identity、容器凭据与 IMDS 等凭据 provider 流量由 AWS SDK 自己负责,并遵循所选 SDK 版本的 HTTP 配置。
公开网页抓取使用独立 PublicHttpClient:只接受无凭据 HTTP(S),遵循进程级 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 与 NO_PROXY,并且代理失败不会静默 改为直连。每次请求和每次重定向都重新解析、校验全部地址,通过代理连接已校验的目标 IP, 同时保留原始 Host/TLS SNI。公私混合 DNS、回环/私网/链路本地/CGNAT/保留地址,以及 IPv4-mapped IPv6 与 NAT64 中嵌套的非公开地址都会整体拒绝。
WebSearch 的带密钥 wire URL 不进入诊断。错误只保留 provider、scheme、host、path、状态与类别,reqwest cause 在进入错误链前移除 URL。
提示词工作流 V2 验收
提示词工作流 V2 的验收条件与证据记录在 提示词与工作流指南 与 设计文档。
构建一个 harness
优先通过 ProfileBundle 与 HarnessProfile 进行组合。部署选择与可调参数属于经过校验的 profile 或配置字段。
新行为使用文档化的扩展点。改变默认 Agent 循环需要在同一次变更中更新英文版 harness 运行时文档。
客户端界面
客户端界面消费持久事件、收件箱状态和投影。TUI、server、ACP 以及未来的 GUI 客户端不得获得私有的 Agent 循环行为。
原生文件修改的实时投影与历史 replay 也共用同一内容策略。edit、write 和 apply_patch 的标准 title 使用 Editing <filename>,完整参数尚未提供目标时才回退 到 Editing files。标准 locations 只包含绝对路径,不借用适配器 cwd 猜测位置。 预期 patch 目标由原生 parser 投影,完成时优先使用实际修改路径;标题最多三个文件名、 160 个 Unicode 字符,其余数量用 (+N more) 表示。成功且有类型化状态时,可见内容只展示 新增/修改/删除 diff,完整原始结果保留在 rawOutput。成功但没有 diff 时保留简短文本。 写入前失败只展示可操作错误,不伪造 diff;部分写入或其他不确定结果保持 failed, 保留已观察到的路径/diff,并发布类型化 uncertain outcome。
zuno run --show-reasoning 只把 provider 明确提供的 reasoning delta 用稳定区块写入 stderr,最终答案继续只写 stdout;signed/encrypted reasoning 永不显示,且不能与 JSON 格式组合。
zuno serve --browser-auth 是显式的纯回环模式:单次 256-bit 启动 token 换取绑定 authority 的 30 天签名 Cookie;Basic Auth 与 Cookie 任一有效即可授权,Cookie 的非安全方法还要求精确 Origin。bootstrap query 在访问日志前被脱敏。
参见
- Harness Runtime(英文完整版) —— 逐节权威契约
- 权限与沙箱 —— 沙箱与权限的两个门禁
- 编排与委派 —— 委派边界与模型路由
- Goal、Plan 与 Todo —— 持久状态如何控制续跑