# Makelore Code OpenCode → Pi 硬切换实施规范 ## 0. 文档信息 | 字段 | 值 | | --- | --- | | Spec ID | `ML-CODE-PI-001` | | 状态 | 已获用户实施授权;实施中 | | 日期 | 2026-08-22 | | Task | `20260822-pi-runtime-spec-b6e2c9a4` | | 基线 | `fba68e86d93c66d2c50f53d215de76a7c69c97a7` | | 来源设计 | Task `20260821-pi-runtime-replacement-doc-8e31c4a7` 的 OpenCode → Pi 硬切换改造设计 | | 关联票据 | `20260822-pi-runtime-spec-b6e2c9a4__pi-runtime-cutover-tickets.md` | | Pi 基线 | `@earendil-works/pi-coding-agent@0.84.2`;上游 tag `v0.84.2` / commit `914cf1472e715297caa30db4b9535d534a9eb718`,已由 `PI-000` 精确固化 | | `PI-000` 实施基线 | Commit `2bc423ebc58589307442ebdaf1c81d83ec9dc2d5`(`feat: qualify Pi runtime cutover foundation`);实施任务 `20260822-pi-runtime-qualification-c52e8a14` 已 `ready_for_integration` | | `PI-010` 交接 | 实施任务 `20260822-pi-conversation-contracts-a61d9c42` 已从 `2bc423e` 建立,当前状态 `planning` | | 发布单元 | 一次完整硬切换;不允许部分能力以双 runtime 形式发布 | | Phase-0 范围修订 | 用户于 2026-08-22 明确将 macOS x64/arm64 资格验证延期到 `PI-150`;该平台未通过、也不得记为 Pass | | Provider 风险决策 | 用户于 2026-08-22 明确豁免真实外部 Provider Account 与真实 provider 双 worker 验证并接受对应风险;`QG-004`/`QG-005` 不是 Pass | | `PI-000` 审计状态 | Done with explicit waivers;`QG-001/002/003/006` Pass,`QG-004/005` Accepted Risk,`QG-007` Pending / not triggered | 本文把详细改造设计收敛为可验证的实施合同。它规定“必须交付什么、边界在哪里、如何判定完成”,不规定每个函数的内部写法。 本文中的关键词含义如下: - **MUST / 必须**:发布前不可缺少;不满足即 Spec 未完成。 - **MUST NOT / 禁止**:任何实现均不得出现。 - **SHOULD / 应当**:默认实现方式;偏离时必须在实施记录中给出可验证理由。 - **MAY / 可以**:不影响合规的实现选择。 ## 1. 目标结果 ### 1.1 产品结果 完成后,Pi 是 Makelore Code 唯一 coding-agent runtime。用户仍使用 Makelore 的项目、伙伴和 Conversation 产品模型,不直接感知 Pi CLI、RPC、session 文件或 provider 配置格式。 系统必须同时达成以下结果: 1. 删除 OpenCode runtime、依赖、插件、Host API、Renderer store、运行资源与打包产物。 2. 项目、伙伴和空 Conversation 的创建仅写本地产品元数据,不启动 Pi,也不阻塞输入框。 3. 每个运行中或温热 Conversation 使用一个独立 Pi worker;多个 Conversation 可以真正并行。 4. Renderer 只消费 Makelore-owned Snapshot/Patch 协议;Pi wire 只存在于 Electron Main 的 Adapter 内。 5. Pi subagent 由 Makelore 自有 extension 编排,具有全局并发上限、父子 abort 和同项目写租约。 6. 保留有产品价值的文本、图片、thinking、tool、retry、compaction、queue、interaction、模型、thinking level、文件、changes、browser 和 skill 能力。 7. 旧 OpenCode Conversation 不在 Pi 中继续或展示;迁移前备份,发布说明明确告知。 8. packaged runtime、四协议 loopback/provider-shaped 双 worker、资源占用和性能预算都有实际证据;真实外部 Provider 兼容与并发风险以明确 waiver 记录,不伪装成 Pass。 ### 1.2 成功定义 只有第 20 节 Definition of Done 全部通过,才可宣称 OpenCode 已被 Pi 替换。仅能启动 Pi、仅能完成单轮聊天、或仅删除 package dependency 都不构成完成。 ## 2. 范围 ### 2.1 包含 - Code 模块的项目/伙伴/Conversation 产品持久化升级。 - Electron Main 的 Pi runtime、worker pool、session registry、provider/resource loader、event projector、extensions 和 Host API。 - Renderer 的 vendor-neutral store、Chat UI、执行图、工具卡、交互、queue、compaction、subagent、changes 和诊断投影。 - 一次性 OpenCode 数据备份与 schema 迁移。 - Pi runtime staging、artifact verifier、真实 smoke、三平台打包验证。 - 流式渲染、首 Conversation、并发和资源预算的遥测与验收。 - OpenCode 专属能力的删除或产品语义替换。 ### 2.2 不包含 - OpenCode 与 Pi 双 runtime、fallback、feature flag、compatibility Adapter 或旧 Host API facade。 - 旧 OpenCode Session 到 Pi Session 的续聊转换。 - 通用 runtime 插件平台或任意第三方 MCP 市场。 - 同一项目多个写入 Agent 的 git worktree 产品;本次只实现项目级 mutation lease。 - Makelore app id、protocol、`niancode` 存储根、通用用户数据或其他产品模块迁移。 - Works gallery、asset gallery、publish/upload 或 cloud-deploy workbench 的恢复。 - Pi TUI 所有功能的 GUI 镜像。 - 自动删除 `/opencode` 或用户不确定所有权的 `.opencode` 文件。 ## 3. 不可变架构决定 ### 3.1 硬切换 - `CUT-001`:发布产物 MUST 只包含 Pi runtime,不得包含 OpenCode binary、package、plugin 或可执行 fallback。 - `CUT-002`:代码 MUST NOT 定义 `OpenCodeRuntime | PiRuntime`、runtime capability negotiation、runtime selector 或 OpenCode Adapter。 - `CUT-003`:所有 `/api/opencode/*` 路由 MUST 删除;Renderer MUST NOT 继续调用旧路由。 - `CUT-004`:源码中不得用 `Opencode*` 名称包装 Pi 数据。仅历史文档、迁移提示和旧数据目录说明可以保留 OpenCode 字样。 - `CUT-005`:硬切换以一个完整应用版本为回退单位,不在同一版本内做 runtime 开关。 ### 3.2 产品权威数据 - `BND-001`:`.niancode/project.json` MUST 继续是项目、伙伴、默认模型和 skill 选择的权威来源。 - `BND-002`:稳定 Agent ID、`app.niancode.desktop`、现有 URL protocol、`niancode` 存储标识 MUST 保持不变。 - `BND-003`:不得生成项目内 `.pi/agents/*` 作为伙伴配置的第二权威来源。 - `BND-004`:项目中任意 `.pi/settings.json`、自动发现的 `.pi/extensions`、`.pi/skills` 或第三方 package MUST NOT 被生产 runtime 隐式加载。 ### 3.3 Main / Renderer 边界 - `BND-005`:Electron Main MUST 拥有 runtime、worker、provider、credential、session 文件、项目文件、browser 和系统集成。 - `BND-006`:Renderer 只能通过 `src/lib/host-api.ts` 或 `src/lib/api-client.ts` 的 typed facade 访问上述能力;禁止页面/组件直接 IPC 或直连 Pi localhost/stdin。 - `BND-007`:Renderer MUST NOT 导入 Pi 类型、解析 Pi JSONL、读取 Pi session、接收绝对 session path 或接收 credential。 - `BND-008`:Pi 实现 MUST 位于一个深的 Main-owned `PiConversationRuntime` Module 后;公共 `CodingConversationRuntime` Interface 只表达 Makelore 产品语义。 ## 4. 用户行为规范 ### 4.1 项目、伙伴与 Conversation 创建 - `UX-001`:创建项目 MUST 只完成项目目录和产品 metadata 初始化;成功响应不得等待 Pi spawn、provider network、session list 或 runtime diagnostics。 - `UX-002`:创建/保存伙伴 MUST 只写 `.niancode/project.json` 及受管本地资源;不得启动或重启所有 worker。 - `UX-003`:创建空 Conversation MUST 生成稳定产品 `conversationId` 并写入 schema v2;此时 `piSessionId` 和 `sessionKey` 可以为空。 - `UX-004`:选择 Conversation 后 Main MAY 后台 prewarm worker,但 prewarm 失败不得阻止用户编辑 draft。 - `UX-005`:项目、伙伴和 Conversation metadata 写入失败必须明确失败,不得返回成功后再依赖 runtime 补齐产品记录。 ### 4.2 Composer - `UX-010`:Textarea 可编辑条件只能依赖 active project 存在、有效伙伴存在、当前项目不处于销毁/迁移临界状态。 - `UX-011`:Textarea 可编辑性 MUST NOT 依赖 worker state、session/status 请求、全局 loading、其他 Conversation busy、provider request 或 diagnostics。 - `UX-012`:附件编码、模型不可用或当前 submit 本地校验 MAY 暂时禁用 Send,但不得禁用 Textarea。 - `UX-013`:cold worker 尚未 ready 时,UI 显示 Conversation 级“正在准备本地 Agent”,不得显示覆盖整个 Chat 的无界 loading。 - `UX-014`:任何本地准备超过 10 秒 MUST 进入明确、可恢复错误;不得让 Composer 分钟级 disabled。 ### 4.3 Prompt、queue 与完成 - `UX-020`:发送时 Renderer MUST 立即创建带 `clientRequestId` 的 optimistic user node。 - `UX-021`:HTTP 成功只表示 prompt/steer/follow-up 已接受,不能表示 run 完成。 - `UX-022`:prompt 接受后,Provider/tool 错误通过 patch stream 投影;不得删除已接受的 user node。 - `UX-023`:一个 Conversation 的 prompt、abort、retry、queue、model、thinking、error MUST 与其他 Conversation 隔离。 - `UX-024`:`agent_settled` 是权威 idle;`agent_end`、`turn_end`、`message_end`、`compaction_end` 单独出现时均不得释放 top-level permit 或清除 busy。 - `UX-025`:steer 与 follow-up 必须是两个明确的 queue mode;UI 不得继续使用含义不明的单一 OpenCode queue。 - `UX-026`:abort 只影响目标 Conversation 及其 child subagents,不得终止其他 worker。 ### 4.4 Conversation 设置 - `UX-030`:伙伴模型只是新 Conversation 默认值;已有 Conversation 保持自己的 `accountId/modelId/thinkingLevel`。 - `UX-031`:模型切换只调用目标 worker 的 Pi `set_model`,并原子更新目标 Conversation metadata;不得修改伙伴默认、重启其他 worker或全局 provider 配置。 - `UX-032`:thinking level 是 Conversation setting,行为与模型切换相同。 - `UX-033`:目标模型不在当前受管 catalog 时切换必须失败,旧模型保持不变。 ### 4.5 并发与 subagent - `UX-040`:不同 Conversation 必须使用两个独立 worker,且最终 packaged provider-shaped loopback turn 必须能在时间上重叠;不能通过一个 worker 反复 `switch_session` 冒充并发。真实外部 Provider 可能并发、串行、限流或拒绝,本 Spec 不再以外部 turn 重叠作为 release hard gate。 - `UX-041`:多个 subagent 支持 `single`、`parallel`、`chain` 三种模式。 - `UX-042`:subagent 作为父 tool 下的嵌套执行节点展示,不得创建虚构的 `role: subagent` chat message。 - `UX-043`:只读 subagent 可以并行;同一项目 mutation 必须遵守项目写租约。 - `UX-044`:abort parent 必须取消所有未完成 child;parallel 中一个 child 失败不得抹掉其他已完成结果;chain 在首个失败处停止。 ### 4.6 功能删除与改名 | OpenCode 旧能力 | Pi 版本要求 | | --- | --- | | share / unshare | UI、route、type、test 全部删除;未来 sharing 另立 Makelore 合同。 | | revert / unrevert | 删除;用“从这里创建新对话分支”表达 Pi fork,不承诺文件回滚。 | | todos endpoint | 删除;若需要结构化进度,使用版本化 `task_state` extension details。 | | global runtime start/stop/restart | 删除;worker 由 Conversation lazy lifecycle 管理。 | | Session diff | 改为 Main-owned `ConversationChangeTracker`,不向 Pi 查询。 | | questions / permissions 双模型 | 合并为 Makelore `interactions`;具体高影响 tool 自己发 confirm。 | | Playwright MCP | 删除;`agent_browser` extension 直接调用 Main browser seam。 | | OpenCode Agent materialization | 删除;伙伴 prompt/manifest 从 `.niancode/project.json` 生成到受管 runtime cache。 | ## 5. 目标架构与模块责任 ```mermaid flowchart LR UI[Renderer Coding UI] API[Main Host API /api/coding] CS[Conversation Service] PS[Project & Conversation Store] FS[Project File / Change Service] RT[CodingConversationRuntime] POOL[Pi Worker Pool] W1[Pi RPC Worker A] W2[Pi RPC Worker B] EXT[Makelore Pi Extension] PR[Provider Service / Secure Storage] UI -->|typed command| API API --> CS API --> PS API --> FS CS --> RT RT --> POOL POOL --> W1 POOL --> W2 W1 --> EXT W2 --> EXT PR --> POOL CS -->|Snapshot + ordered Patch| API API -->|SSE| UI ``` ### 5.1 产品模块 | 模块 | 必须拥有 | 禁止拥有 | | --- | --- | --- | | `electron/coding-projects` | project config/store、conversation metadata、migration、file service、skills registry | Pi RPC、provider turn、Renderer state | | `electron/coding-runtime/contracts.ts` | vendor-neutral runtime Interface 和产品 DTO | Pi event/type import | | `electron/coding-runtime/conversation-service.ts` | product command orchestration、snapshot/patch subscription、acceptance | child process 细节 | | `electron/coding-runtime/pi` | process、RPC、worker pool、session registry、event/session projector、provider/resource input、extensions | project CRUD、Renderer component | | `electron/api/routes/coding-*` | request validation、typed response、SSE、安全错误投影 | vendor wire passthrough | | Renderer `coding-conversations` | per-Conversation snapshot/patch reducer、draft 和 selection | Pi JSONL、绝对路径、credential | ### 5.2 `CodingConversationRuntime` Interface 实现 MUST 提供等价于下列产品能力的最小接口: ```ts interface CodingConversationRuntime { prepare(input: PrepareConversationInput): Promise; getSnapshot(conversationId: string): Promise; prompt(input: PromptConversationInput): Promise; steer(input: QueueMessageInput): Promise; followUp(input: QueueMessageInput): Promise; abort(conversationId: string): Promise; setModel(input: SetConversationModelInput): Promise; setThinking(input: SetThinkingLevelInput): Promise; compact(conversationId: string): Promise; fork(input: ForkConversationInput): Promise; recover(conversationId: string): Promise; dispose(conversationId: string): Promise; subscribe(listener: (patch: ConversationPatchEnvelope) => void): () => void; } ``` - `ARC-001`:生产 `PiConversationRuntime` 与测试 `InMemoryConversationRuntime` 是该 Interface 的两个 Implementation。 - `ARC-002`:Interface MUST NOT 包含 vendor enum、Pi CLI option、OpenCode capability 或绝对路径。 - `ARC-003`:项目 CRUD 和文件读取不属于 runtime Interface,应由对应 Main service 直接提供。 - `ARC-004`:实现内部 MAY 进一步拆分,但不得把 process/RPC/event/session/provider 生命周期重新堆回单个 Host route 文件。 ### 5.3 建议目录 ```text electron/ coding-projects/ project-config.ts project-store.ts conversation-store.ts project-files.ts conversation-change-tracker.ts skill-registry.ts coding-runtime/ contracts.ts conversation-service.ts runtime-errors.ts pi/ runtime.ts worker-pool.ts worker-process.ts rpc-framer.ts rpc-client.ts event-projector.ts session-projector.ts provider-config.ts resource-loader.ts extensions/ makelore-runtime.ts interaction.ts write-lease.ts subagent.ts agent-browser.ts game-assets.ts api/routes/ coding-projects.ts coding-conversations.ts coding-files.ts ``` 目录名称可以按仓库约定小幅调整,但责任边界和依赖方向是规范要求。 ## 6. 产品数据模型 ### 6.1 产品 ID 与 Pi ID - `DAT-001`:`conversationId` MUST 是 Makelore 生成的稳定 UUID;Renderer、URL、archive、unread、selection、Zustand key 只使用该 ID。 - `DAT-002`:Pi `sessionId`、session file、entry ID、leaf ID 是 Main 私有实现字段。 - `DAT-003`:worker 重启、session reopen 或 Pi 升级不得改变产品 `conversationId`。 - `DAT-004`:任何绝对 session path MUST NOT 出现在 Host API、SSE、Renderer log 或 UI。 ### 6.2 `.niancode/project.json` schema v2 每个 Agent 的模型选择 MUST 迁移为: ```ts interface ProductModelRef { accountId: string; modelId: string; thinkingLevel: "off" | "minimal" | "low" | "medium" | "high"; } interface ProductModelSelection { model: ProductModelRef | null; modelResolution: "resolved" | "required"; } ``` - `accountId` 指向 Main Provider Service 的稳定账号。 - `modelId` 是 provider 原生模型 ID,不是 Pi 派生 provider key。 - runtime provider ID 只在 Main 运行时派生。 - 旧 string model 只有在 Provider Service 能唯一映射时才自动转换。 - 不能唯一映射时保留 Agent 其他字段,写入 `{ model: null, modelResolution: "required" }`;UI 要求用户选择,不得静默使用默认模型。 - Agent id、name、prompt、skills、archive 状态 MUST 保持。 ### 6.3 `.niancode/conversations.json` schema v2 ```ts interface CodingConversationFileV2 { schemaVersion: 2; conversations: Array<{ id: string; agentId: string; title: string; model: ProductModelRef | null; modelResolution: "resolved" | "required"; piSessionId?: string; sessionKey?: string; archivedAt: string | null; unread: boolean; createdAt: string; updatedAt: string; }>; } ``` - `DAT-010`:`sessionKey` 必须是 Main session root 下的 opaque relative key;禁止 `..`、绝对路径或 Renderer 提供路径。 - `DAT-011`:Conversation 可以在 Pi session 不存在时持久化。 - `DAT-012`:第一次 prompt 的 session create/open 与 Conversation 绑定 MUST 使用 per-Conversation single-flight;并发首发不得创建两个 session。 - `DAT-013`:title、archive、unread 和 model metadata 不依赖 worker 存活。 - `DAT-014`:删除 Conversation 时先 dispose worker,再把 session 移入 Main-owned trash;Renderer 不直接删除文件。 - `DAT-015`:产品 metadata 写入 MUST 使用原子替换;session 文件由 Pi 持久化合同负责。 ### 6.4 受管 Pi 目录 ```text /coding-runtime/pi/ config/ sessions//.jsonl prompts//.md extensions/ logs/ trash/ ``` `PI_CODING_AGENT_DIR` MUST 指向受管 config。项目 cwd 仍是实际项目目录,但 Pi 全局配置、session、prompt 和 extension 不写入任意用户 `~/.pi`。 ## 7. Provider、模型与受管资源 ### 7.1 Provider 投影 Main 内部 MUST 用 vendor-neutral account 生成等价于下列描述: ```ts interface PiProviderDescriptor { accountId: string; runtimeProviderId: string; api: | "openai-completions" | "openai-responses" | "anthropic-messages" | "google-generative-ai"; baseUrl?: string; headers: Record; apiKeyEnv?: string; models: Array<{ id: string; name: string; input: Array<"text" | "image">; contextWindow?: number; maxOutputTokens?: number; }>; } ``` 具体 `api` union 在 `PI-000` 中按锁定 Pi 版本固化。 - `PRV-001`:只有认证、base URL 和协议完全匹配时才使用 Pi built-in provider。 - `PRV-002`:自定义 base URL/header、Works gateway/proxy 和导入模型必须由 Main 生成受管 catalog。 - `PRV-003`:runtime provider ID 由稳定 account ID 派生,两个同 vendor 账号不得冲突。 - `PRV-004`:当前 Works gateway `/v1` 归一化、proxy token 与 refresh 语义必须迁移并有 fixture。 - `PRV-005`:credential 只在 spawn 前从 secure storage 读取,放入单 worker environment 或受管 credential provider;不得进入 command line、snapshot、SSE、Renderer 或日志。 - `PRV-006`:同 account refresh 使用 Provider Service single-flight;认证恢复最多执行一次 refresh + worker reopen,不得无限循环。 - `PRV-007`:Provider/config 维护 Main-owned revision。idle stale worker 在下次 prompt 前重建;running worker在 settled 后重建,不得中断当前 run。 ### 7.2 Prompt、skills 与 context - `RES-001`:伙伴 prompt 从 `.niancode/project.json` 生成到受管 cache;command line 只出现受管路径,不出现 prompt 内容。 - `RES-002`:内建 coding skills 从 `.opencode/skills` 移到 `resources/coding-skills`,由 skill ID 映射到显式打包路径。 - `RES-003`:child 只加载当前伙伴选择的 skill;禁止自动发现项目或用户目录的 Pi resources。 - `RES-004`:正在运行的 worker 保持本 run 的 resource snapshot;新 run/新 worker使用最新 revision。 - `RES-005`:`AGENTS.md` 等 context file 的加载策略必须由 `PI-000` 在锁定版本上显式决定并测试,不能依赖 Pi 默认值漂移。 ## 8. Pi 进程、RPC 与 Worker Pool ### 8.1 Phase-0 资格门 - `QG-001`:在任何产品迁移代码开始前,必须精确 pin 一个 Pi 版本并记录 package、CLI entry、Node engine 和上游 tag/commit。 - `QG-002`:Phase-0 平台范围内的 Windows x64、Linux x64 packaged qualification artifact 必须证明 Electron Node 能 spawn、RPC ready、open session、accept prompt、abort、settle、reopen、clean exit。 - `QG-003`:必须证明 Phase-0 平台范围内的 Windows x64、Linux x64 完整 production dependency closure、WASM/native/optional 资源能从 packaged qualification artifact resolve;最终产品 artifact 的同项验证属于 `PI-150`。 - `QG-004`:真实外部 Provider Account 的四协议验证由用户于 2026-08-22 标记为 **Explicitly Waived / Accepted Risk**,不得记为 Pass。Phase-0 仍必须保留 Windows/Linux packaged loopback 四协议矩阵,验证 request/SSE、base URL/path、header、environment credential plumbing、model 和 image serialization;报告中的 `realTurnVerified` 必须保持 `false`。 - `QG-005`:真实外部 provider 的双 worker turn overlap/isolation 由用户于 2026-08-22 标记为 **Explicitly Waived / Accepted Risk**,不得记为 Pass。Phase-0 仍必须以两个独立 worker 通过 packaged provider-shaped loopback 证明时间窗口重叠、单侧 abort 后另一侧 settled,以及受控 seam 内的 event/session/model/credential 路由不串线;不得把它表述为真实外部 Provider 并发证据。 - `QG-006`:必须报告 Phase-0 平台范围内 Windows x64、Linux x64 的 cold/warm latency、RSS、退出清理 p50/p95/max 和样本数。 - `QG-007`:若 RPC 无法通过未豁免的打包、provider-shaped contract/concurrency/abort 或预算门,必须在业务迁移前停止并修订本 Spec,选择“Pi SDK in Electron utility process”;禁止继续实现双 runtime 或同时维护 RPC/SDK 两条生产路径。缺少已明确豁免的真实外部 Provider 样本不是已确认失败,不触发本条。 **Phase-0 macOS waiver(用户范围决策,2026-08-22)**:macOS x64 与 macOS arm64 在 `PI-000` 中标记为 **Explicitly Waived / Deferred by user**, 不得标记为 Pass。原 Phase-0 中的 macOS workspace、staged production closure、 controlled packaged、packaged loopback 和 metrics 全部移交 `PI-150` 实际验证。 该 waiver 只改变 `QG-002`、`QG-003`、`QG-006` 的 Phase-0 平台范围; `QG-004`、`QG-005` 后续由下面独立的 Provider waiver 处理。这会把 macOS packaging、native、resource 和 performance defect 的暴露时间推迟到 release stage,可能导致更晚返工;这是用户接受的时序风险,不代表风险已经消除。 **真实 Provider waiver(用户风险决策,2026-08-22)**:用户明确豁免真实 外部 Provider Account/credential 与真实 provider 双 worker 验证,并接受以下 尚未验证的风险: - 真实认证、endpoint、proxy、rate-limit 和 provider-specific response variation; - base URL、header、environment credential、model、image 等字段在真实协议端点 的兼容性; - 真实 provider 可能并发、串行、限流或拒绝两个 worker turn; - 跨 worker abort、event、session、model、credential 的真实外部隔离。 该决定把 `QG-004`、`QG-005` 置为 **Explicitly Waived / Accepted Risk**, 不是 Pass,也不是已确认失败。Windows/Linux packaged loopback 与 provider-shaped concurrency/abort smoke 仍是必须通过的替代证据;任何报告均须 保留 `realTurnVerified: false`。真实外部 Provider Account、凭证或 turn 不再是 `PI-000`、`PI-150` 或 release 的阻断条件,缺失它们不触发 `QG-007`。 ### 8.2 启动合同 生产实现 SHOULD 使用 Electron executable + `ELECTRON_RUN_AS_NODE=1` 启动 staged Pi CLI RPC entry。锁定版本的实际参数必须由 smoke 确认,语义要求如下: - RPC mode; - offline/禁用 Pi 自更新类启动网络,但不阻断 provider; - `--no-approve` 或锁定版本等价选项; - 禁止自动 extension/skill/prompt-template 发现; - 只显式加载 Makelore extension、selected skills、受管 session/provider/model; - stdout 只能输出 RPC JSONL,日志只能走 stderr; - secret 只进入 child environment; - cwd 是当前项目目录;Pi config/session root 是 Main-owned 目录。 ### 8.3 RPC - `RPC-001`:使用自有严格 LF JSONL framer;不得用 Node `readline`,JSON string 内 U+2028/U+2029 不能分帧。 - `RPC-002`:command 必须带相关 ID;response 可乱序并通过 ID resolve;event 不得误配到 pending command。 - `RPC-003`:半行跨 chunk、单 chunk 多行、CRLF 尾部、malformed JSON、oversized line、stdout 非协议文字、stderr、backpressure、exit 和 timeout 都必须覆盖。 - `RPC-004`:stdout protocol violation 必须 kill 当前 worker、generation +1,并只保留有界且脱敏的诊断片段。 - `RPC-005`:只读 command 可在 generation 不变时重试一次;`prompt/steer/follow_up/fork/compact` timeout 后禁止自动重发。 ### 8.4 Worker 生命周期 ```text absent -> queued -> spawning -> ready -> running ^ | | | v v crashed <- idle <- settling | evicted ``` - `RUN-001`:`prepare` 必须是 per-Conversation 幂等 single-flight。 - `RUN-002`:一个 worker 同一时间只拥有一个产品 Conversation/Pi AgentSession;不得在两个 streaming Conversation 间 `switch_session`。 - `RUN-003`:running worker 禁止 LRU eviction;idle worker 超预算按最久未使用关闭。 - `RUN-004`:worker crash/recover 后 generation 加一;所有旧 generation event、response、interaction 和 lease 必须丢弃/取消。 - `RUN-005`:app quit 先拒绝新 prompt,再给 worker 3 秒 graceful shutdown,超时 kill;退出不得无限等待。 - `RUN-006`:Session 已持久化时,idle eviction 后可以重新 spawn 和 hydrate。 - `RUN-007`:worker unexpected exit 必须取消 pending RPC、interaction、write lease 和 child subagent,但保留 session 文件。 ### 8.5 初始调度预算 | 资源 | 初始产品上限 | 必须行为 | | --- | ---: | --- | | 同时 running top-level Conversation | 4 | 第 5 个进入自己的队列,UI 显示等待。 | | warm idle worker | 4 | 超出后 LRU eviction。 | | 单次 subagent dispatch task | 8 | schema validation 拒绝第 9 个。 | | 全应用 running subagent child | 4 | 所有 parent 共用 semaphore。 | | Pi 总进程软上限 | 8 | 4 parent + 4 child;running 不被强杀。 | 这些值是 Makelore 初始常量,不是 Pi 引擎保证,也不做用户 feature flag。只有 packaged RSS/latency 证据支持时才可在后续独立变更中调整。 ### 8.6 项目写租约 - `LEASE-001`:同一项目同时最多一个 mutation tool 持有 write lease。 - `LEASE-002`:`read/grep/find/ls` 和纯推理无需 lease,可以并行。 - `LEASE-003`:受管 `write`、`edit` 及可执行任意命令的 `bash` 在执行前必须向 Main 获取 lease。 - `LEASE-004`:等待时 tool card 显示“等待项目写入”,且用户可 abort。 - `LEASE-005`:complete、error、abort、timeout、worker crash、child exit 都必须释放 lease。 - `LEASE-006`:不同项目 mutation 可以并行;同 parent 的 coding subagent 也必须遵守租约。 ## 9. Makelore Pi Extension 合同 ### 9.1 显式 bundle 生产包只显式加载一个版本化 Makelore extension entry。它至少注册: | 能力 | 责任 | | --- | --- | | `ask_user` | select/confirm/input/editor 与 pending interaction | | `project_write_lease` | mutation 前后申请/释放项目租约 | | `subagent` | stable Agent manifest、single/parallel/chain、nested progress | | `agent_browser` | Main-owned browser seam | | `game_asset_browser/review` | 迁移现有游戏资源产品能力 | | `task_state` | 版本化任务步骤/进度;不是 Pi core todo | | `changed_file` | 上报受管 tool touched path | | `runtime_context` | 项目、Conversation、Agent、knowledge、skills;不含 secret | ### 9.2 Extension → Main seam - `EXT-001`:extension 不得直连 Renderer。 - `EXT-002`:Main 提供 loopback internal endpoint 或 child IPC bridge,并为每个 worker生成短期随机 bearer token;worker dispose 后 token 失效。 - `EXT-003`:该 endpoint 只暴露 interaction、browser、write lease、subagent permit 和受管资源读取;不得复用 Works credential。 - `EXT-004`:request 必须带 `conversationId/workerGeneration/runId`,Main 必须核对 worker registry。 - `EXT-005`:同机受管子进程边界只要求随机 bearer token + loopback;不得额外引入签名、哈希或通用远程授权框架。 - `EXT-006`:extension 误写 stdout 属于 RPC protocol violation。 ### 9.3 Interaction ```ts interface ConversationInteraction { id: string; conversationId: string; runId: string; kind: "select" | "confirm" | "input" | "editor"; title: string; message?: string; options?: Array<{ id: string; label: string; description?: string }>; status: "pending" | "answered" | "rejected" | "cancelled"; } ``` - 常规 read/write/edit/bash 沿用本地协作者默认授权,不建设泛化 permission policy engine。 - 高影响操作只有在具体产品 tool 明确要求时调用 confirm。 - worker crash/abort 必须取消其 pending interactions。 - notify 投影为 toast;status/widget 只接受已注册类型;unknown details 只进入 bounded diagnostics。 ### 9.4 Subagent details Makelore MUST 自有稳定 schema,不能直接把 Pi 官方示例 details 传给 GUI: ```ts interface SubagentDetailsV1 { schema: "subagent.v1"; dispatchId: string; mode: "single" | "parallel" | "chain"; tasks: Array<{ taskId: string; agentId: string; toolProfile: "read-only" | "coding"; status: "queued" | "running" | "complete" | "error" | "aborted" | "skipped"; summary?: string; errorCode?: string; usage?: PublicUsage; }>; } ``` - child 使用独立 Pi process/context,默认不成为可恢复的用户 Conversation。 - parent UI 只消费 `subagent.v1` 及普通嵌套 tool/message progress。 - unknown schema version 不得 dump raw JSON;显示通用不可用状态并记录诊断。 ## 10. Snapshot / Patch 领域协议 ### 10.1 Snapshot ```ts interface ConversationSnapshot { schemaVersion: 1; conversation: { id: string; projectId: string; agentId: string; title: string; model: ConversationModelState; }; nodes: ConversationNode[]; run: ConversationRunState; queue: ConversationQueueState; context: ConversationContextState; pendingInteractions: ConversationInteraction[]; worker: PublicWorkerState; cursor: { workerGeneration: number; seq: number; leafEntryId?: string; }; } ``` ### 10.2 Node ```ts type ConversationNode = | ConversationMessageNode | ConversationToolNode | ConversationCompactionNode | ConversationBoundaryNode | ConversationSubagentNode | ConversationNoticeNode; interface ConversationMessageNode { kind: "message"; id: string; sourceEntryId?: string; clientRequestId?: string; role: "user" | "assistant"; status: "optimistic" | "streaming" | "complete" | "error" | "aborted"; blocks: ConversationContentBlock[]; usage?: PublicUsage; stopReason?: "stop" | "length" | "tool-use" | "error" | "aborted"; } type ConversationContentBlock = | { kind: "text"; id: string; text: string; status: "streaming" | "complete" } | { kind: "thinking"; id: string; text: string; status: "streaming" | "complete" } | { kind: "image"; id: string; attachmentId: string; mime: string }; interface ConversationToolNode { kind: "tool"; id: string; toolCallId: string; toolName: string; title: string; inputText: string; status: "declared" | "waiting" | "running" | "complete" | "error" | "aborted"; output: ConversationContentBlock[]; details?: KnownToolDetails; } ``` Pi `toolResult` MUST attach to `ConversationToolNode`;它不是公开 message role,也不得生成独立 bubble。 ### 10.3 Patch ```ts type ConversationPatch = | { op: "worker.state"; state: PublicWorkerState } | { op: "run.state"; run: ConversationRunState } | { op: "message.upsert"; node: ConversationMessageNode } | { op: "message.block-delta"; messageId: string; blockId: string; delta: string } | { op: "tool.upsert"; node: ConversationToolNode } | { op: "compaction.upsert"; node: ConversationCompactionNode } | { op: "subagent.upsert"; node: ConversationSubagentNode } | { op: "queue.replace"; queue: ConversationQueueState } | { op: "interaction.upsert"; interaction: ConversationInteraction } | { op: "interaction.remove"; interactionId: string } | { op: "context.replace"; context: ConversationContextState } | { op: "snapshot.invalidated"; reason: string }; interface ConversationPatchEnvelope { conversationId: string; workerGeneration: number; runId?: string; seq: number; at: number; patch: ConversationPatch; } ``` - `EVT-001`:`seq` 在单 Conversation + generation 内严格递增。 - `EVT-002`:Renderer 丢弃旧 generation;发现 gap 不猜测缺失内容,GET 目标 Conversation snapshot。 - `EVT-003`:初次订阅和重连先取 snapshot,再接 live stream;不建设永久 event log。 - `EVT-004`:live UI ID 在 `message_start` 生成,durable reconcile 只绑定 `sourceEntryId`,不得替换 UI ID。 - `EVT-005`:禁止使用 timestamp 或 array index 作为唯一 UI identity。 - `EVT-006`:live assembly 与 hydrated snapshot 必须使用同一 normalization reducer 和 fixture。 - `EVT-007`:unknown Pi event/content 默认不进入 timeline,只进入有界 diagnostics。 ### 10.4 Active branch hydration `get_entries` 不能直接按 append order 渲染。Main MUST: 1. 读取 `entries` 与当前 `leafId`。 2. 建立 `id → entry` map。 3. 从 `leafId` 沿 `parentId` 回溯到 root。 4. 反转为 active path。 5. 应用 compaction/retained-tail 规则。 6. 通过统一 reducer 生成 Snapshot。 7. 在 `agent_settled` 后用 durable entries/leaf reconcile。 废弃 branch 不进入当前 transcript;只有明确的分支历史查询可以读取。 ## 11. Pi 事件映射 | Pi 输入 | 产品投影 | 规范要求 | | --- | --- | --- | | prompt response success | `PromptAcceptance` | 只表示接受/排队,不是完成。 | | prompt response failure | optimistic node rejected/error | 保留 draft/附件恢复能力;不得创建 assistant。 | | `agent_start` | run `running` | 验证/绑定预分配 `runId`。 | | `agent_end` | low-level checkpoint | 禁止标 idle。 | | `agent_settled` | run `idle` + durable reconcile | 唯一权威 idle。 | | `turn_start/end` | boundary node | 不重复插入 message/tool result。 | | user `message_start` | reconcile optimistic user | 通过 request/run 关联。 | | assistant `message_start` | streaming assistant node | 生成稳定 UI ID。 | | `text_start/delta/end` | text block | 按 `contentIndex` 组装;end 为权威值。 | | `thinking_start/delta/end` | thinking block | 按 `contentIndex`,遵守展示偏好。 | | `toolcall_start/delta/end` | declared tool + args buffer | delta 阶段不 parse JSON,end 才解析。 | | `tool_execution_start` | tool running | 只更新 `toolCallId` 对应节点。 | | `tool_execution_update` | replace cumulative output/details | 必须 replace,禁止 append `partialResult`。 | | `tool_execution_end` | complete/error/aborted tool | final result 权威。 | | assistant `message_end` | authoritative message | 替换 draft blocks/usage/stop reason。 | | `toolResult` `message_end` | attach final result | 不生成独立 bubble。 | | `queue_update` | queue replace | UI badge/composer 状态,不进 transcript。 | | `compaction_start/end` | compaction node/context | `willRetry` 时 run 保持 active;summary 默认隐藏。 | | retry events | retry state/trace | 脱敏 error,显示 attempt/delay。 | | extension interaction | pending interaction | request ID 关联 response。 | | notify/status/widget/title/editor text | toast/status/known widget/title/draft suggestion | draft revision 已变化时不得覆盖用户输入。 | | `bashExecution` | direct command node | 仅产品暴露 direct command 时显示。 | | `custom display:false` | hidden | 不显示。 | | registered custom | typed product node | 只渲染已注册 schema。 | | unknown custom | diagnostic | 不 dump raw details。 | | branch/compaction summary | metadata/timeline marker | 不当 user/assistant bubble。 | ### 11.1 Usage 与 context - `message_update.usage` 可流式显示,但最终值以 `message_end` 与 `get_session_stats` reconcile。 - session total 必须包含 compaction/branch 等有效 usage,不能只相加 assistant message。 - 刚压缩后 context 尚未可算时显示“重新计算中”,不得显示虚假的 0%。 ## 12. Host API ### 12.1 路由 所有新路由使用 `/api/coding/*`: | 路由 | 方法 | 语义 | | --- | --- | --- | | `/api/coding/projects` | GET | 列出项目。 | | `/api/coding/projects/open` | POST | 打开已有目录。 | | `/api/coding/projects/create` | POST | 创建本地项目;不启动 Pi。 | | `/api/coding/projects/remove` | POST | 从 catalog 移除,不删除项目文件。 | | `/api/coding/projects/active` | GET/POST | 查询/选择 active project。 | | `/api/coding/projects/config` | GET/PUT | `.niancode/project.json`。 | | `/api/coding/projects/knowledge` | POST | 更新项目 knowledge。 | | `/api/coding/projects/conversations` | GET/POST | metadata;POST 不启动 Pi。 | | `/api/coding/conversations/:id` | GET/PATCH/DELETE | metadata 与受管 session lifecycle。 | | `/api/coding/conversations/:id/snapshot` | GET | active branch 完整 Snapshot。 | | `/api/coding/events` | GET/SSE | 全局有序 envelope stream;seq 按 Conversation。 | | `/api/coding/conversations/:id/prompt` | POST | prompt/steer/follow-up;只等待 acceptance。 | | `/api/coding/conversations/:id/abort` | POST | abort 目标 Conversation。 | | `/api/coding/conversations/:id/model` | POST | 目标 Conversation model。 | | `/api/coding/conversations/:id/thinking` | POST | 目标 Conversation thinking level。 | | `/api/coding/conversations/:id/compact` | POST | 手动压缩。 | | `/api/coding/conversations/:id/fork` | POST | 从 user entry 创建新产品 Conversation。 | | `/api/coding/conversations/:id/recover` | POST | 重新创建 worker 并 hydrate。 | | `/api/coding/conversations/:id/commands` | GET | Pi command + Makelore command 安全投影。 | | `/api/coding/interactions` | GET | 当前 pending interactions。 | | `/api/coding/interactions/:id/respond` | POST | 回答/拒绝 interaction。 | | `/api/coding/skills` | GET | 产品 skill registry。 | | `/api/coding/files/status` | GET | 项目文件状态。 | | `/api/coding/files/find` | GET | 文件查找。 | | `/api/coding/files/content` | GET | 文件内容预览。 | | `/api/coding/search` | GET | 文本搜索。 | | `/api/coding/runtime/diagnostics` | GET | 脱敏 worker pool/resource summary。 | ### 12.2 Prompt request/acceptance ```ts interface PromptConversationInput { clientRequestId: string; conversationId: string; mode: "prompt" | "steer" | "follow-up"; text: string; attachments: Array<{ attachmentId: string }>; } interface PromptAcceptance { accepted: true; conversationId: string; clientRequestId: string; runId: string; mode: "prompt" | "steer" | "follow-up"; queuePosition?: number; } ``` - `API-001`:接受时返回 HTTP `202`;产品校验未通过时返回 typed 4xx,且 `accepted` 不得伪造为 true。 - `API-002`:HTTP 不等待 `agent_settled`、provider first token 或完整 assistant message。 - `API-003`:同一进程内重复 `clientRequestId` 若已明确 accepted,应返回相同 acceptance;状态不确定时不得自动发送第二次 prompt。 - `API-004`:请求 attachment 只引用 Main-owned attachment ID;Renderer 不提交任意本地路径。 - `API-005`:SSE/event stream 断线不得导致 accepted prompt 自动重发。 ### 12.3 错误合同 Renderer 只接收稳定 code、可操作状态和固定中文提示。至少覆盖: | code | 场景 | 可恢复性 | | --- | --- | --- | | `CODING_RUNTIME_START_FAILED` | child spawn 失败 | 手动 recover | | `CODING_RUNTIME_READY_TIMEOUT` | 10 秒未 ready | kill 后 recover | | `CODING_RUNTIME_PROTOCOL_ERROR` | stdout/RPC 违规 | kill 后 recover | | `CODING_PROVIDER_AUTH_REQUIRED` | credential 不可用/刷新一次仍失败 | 修复账号后 recover | | `CODING_MODEL_UNAVAILABLE` | model 不在 catalog | 选择模型 | | `CODING_SESSION_UNREADABLE` | session JSONL 无法读取 | 保留文件,新建/诊断 | | `CODING_STORAGE_WRITE_FAILED` | product/session persistence 失败 | 停止新 prompt,修复存储 | | `CODING_REQUEST_UNCERTAIN` | mutating RPC timeout 后无法确认 | 用户显式 reconcile/retry | | `CODING_MIGRATION_MODEL_REQUIRED` | 旧 model 无唯一映射 | 用户选择模型 | 原始 stderr、provider body、credential、账号 ID、完整路径、stack 和 extension path 不得穿透 Renderer。 ## 13. Project File、Changes 与 Browser ### 13.1 Project File Service - 文件 status/find/content/search 从 OpenCode route 移入 Main-owned product service。 - 所有路径必须相对 active project 解析;Renderer 不获得绝对项目根。 - 行为保持现有受支持功能,不因 runtime 切换扩展为任意文件系统代理。 ### 13.2 `ConversationChangeTracker` - `CHG-001`:run start 记录项目基线状态和当前 git head(若存在)。 - `CHG-002`:受管 write/edit tool 通过 `changed_file` 上报 touched relative path。 - `CHG-003`:bash 可能修改任意文件,因此 settled 后执行一次项目级 git status/diff refresh。 - `CHG-004`:只读取 changed path 的 diff;untracked 只提供受限 preview。 - `CHG-005`:结果进入 Conversation changes panel,不写入 Pi message/session。 - `CHG-006`:不得为每个文件增加哈希/checkpoint 框架;若将来需要文件回滚,另立 git checkpoint 设计。 ### 13.3 Browser ```text Pi tool agent_browser -> Makelore extension -> Main browser service -> bounded structured result / screenshot attachmentId ``` - 不启动 Playwright MCP server,不维护 MCP discovery/stdio/second protocol。 - screenshot 经 Main attachment service 投影,不把大 base64 反复传给 Renderer。 - browser 继续遵守现有 Main-owned browser seam 的边界。 ## 14. 渲染与内存性能 - `REN-001`:Main 按 16–33 ms 窗口合并 text/thinking delta。 - `REN-002`:Renderer 只更新当前 Conversation 的目标 node/block;禁止每 token 替换整个 transcript。 - `REN-003`:tool cumulative partial output 必须 replace;禁止重复拼接。 - `REN-004`:tool-call args 在 end 前只作为字符串 buffer。 - `REN-005`:大图片/base64 转成 Main-owned attachment ID/preview URL;SSE/Zustand 不反复复制 base64。 - `REN-006`:隐藏 Conversation 只更新轻量 run/unread summary,不做 Markdown 全量重渲染。 - `REN-007`:Markdown、thinking 和大 tool output 使用 memoized block;超长输出默认折叠或虚拟化。 - `REN-008`:压力 fixture 必须记录 Main patch 数、Renderer commit 数和 IPC/SSE bytes,证明 batching 生效。 ## 15. 一次性迁移与旧数据政策 ### 15.1 首次迁移 打开 schema v1 项目时,Main MUST 按顺序执行: 1. 在 `.niancode/migration-backups/opencode-cutover-/` 复制原 `project.json` 与 `conversations.json`。 2. 通过 Provider Service 把旧 Agent model string 唯一映射为结构化 model ref。 3. 不能唯一映射的 Agent 标记 model required,保留其他字段。 4. 原子写 project schema v2,保持 Agent identity、prompt、skills、archive。 5. 写空的 Conversation schema v2;旧 OpenCode Session 不进入新 sidebar。 6. 显示一次性说明:“旧版 OpenCode 对话不能在 Pi 中继续,原数据已保留用于回退或独立导出。” 7. 成功后写明确 schema version;失败时不覆盖 backup 或原始可恢复数据。 - `MIG-001`:迁移是一次性、显式版本分支,不建设通用 migration framework。 - `MIG-002`:迁移前必须备份;无备份时不得覆写 schema v1。 - `MIG-003`:旧 Conversation 不显示、不继续、不被 Pi release 读取。 - `MIG-004`:如果产品需要旧聊天可读,必须在仍含 OpenCode 的旧版本中另做 read-only exporter;Pi release 不保留 OpenCode client/daemon。 ### 15.2 `.opencode` 项目内容 - 只删除 materialization metadata 能证明是 Makelore 生成且未修改的 `.opencode/agent` 文件。 - 用户修改过或所有权不确定的生成文件移到 migration backup。 - 项目中其他 `.opencode` 内容不删除,但新应用永不读取。 - 不创建 `.pi/agents` 替代。 ### 15.3 userData `/opencode` 在首个 Pi release 保持原地、惰性且不读取。不得自动递归删除。它只支持完整应用版本回退或人工导出,不代表兼容层。 ### 15.4 版本回退 1. 发布 Pi 版本前保留上一版安装包和 release tag。 2. schema migration backup 是回退旧 metadata 的唯一输入。 3. 阻断问题发生时安装上一版,并按 runbook 恢复 backup。 4. Pi 新建 Conversation 不能在 OpenCode 版本继续,release note 必须明确。 5. 普通项目文件不自动回滚;用户自行使用 VCS。 ## 16. 打包和运行时交付 ### 16.1 依赖 - `PKG-001`:删除精确版本 `opencode-ai`、`@opencode-ai/plugin` 和只为 OpenCode MCP 存在的 `@playwright/mcp`。 - `PKG-002`:`@earendil-works/pi-coding-agent` 必须精确 pin;禁止 `^`/`~`。 - `PKG-003`:lockfile、许可清单、runtime manifest 和 diagnostics 记录同一精确版本。 - `PKG-004`:目标 Pi Node engine 必须与每个平台 bundled Electron Node 实测兼容。 ### 16.2 Staging `scripts/bundle-pi-runtime.mjs` 或等价脚本 MUST 从 frozen lockfile/已安装依赖图生成 `build/pi-runtime`,包含: - Pi 运行所需 dist/resource; - 全部 production dependencies; - 当前平台 required optional dependencies; - WASM/native 资源; - Makelore extension bundle; - 内建 coding skills; - runtime manifest:package、version、entry、Node engine、resource paths。 构建阶段禁止在线安装或自动更新 Pi。不得只复制顶层 package。 ### 16.3 electron-builder 与 artifact verifier - 删除 `build/opencode-ai` 与 `.opencode/skills` 打包映射。 - 增加 `build/pi-runtime → pi-runtime`。 - native/WASM 使用正确 `extraResources` 或 `asarUnpack`。 - 保持 appId、productName、protocol 和现有产品路径。 最终 artifact verifier 必须证明: 1. OpenCode package/binary/plugin 不存在。 2. Pi manifest、package.json 与 lockfile 版本一致。 3. CLI/RPC entry 可由 Electron Node 加载。 4. production closure 全部 resolve。 5. Node engine 相容。 6. extension/skills paths 存在。 7. 临时空项目 `get_state` 成功。 8. 无 credential protocol smoke 可启动/退出;四协议受控 loopback/provider-shaped endpoint 使用环境凭据引用完成 prompt、settled、双 worker overlap 与 abort isolation。真实外部 Provider Account 不属于 release hard gate,`realTurnVerified` 保持 `false`。 9. Windows、macOS x64/arm64、Linux artifact 无开发机绝对路径。 `PI-000` 的 macOS waiver 到 `PI-150` 即失效。`PI-150` 必须分别在 macOS x64 与 arm64 上以独立 checkout/frozen install 运行 workspace 5+5、staged 5+5、controlled packaged 5+5、packaged loopback、closure/native/resource 检查和 metrics,再对最终目标 artifact 运行 verifier/smoke。静态 darwin path test、其他平台回归或 Phase-0 waiver 均不能替代 macOS 实跑证据。 ### 16.4 Real runtime smoke `smoke:pi:real` 中的 `real` 指最终 artifact 内实际 Pi runtime/process seam,不指真实外部 Provider Account。它必须通过受控 loopback/provider-shaped endpoint 覆盖:spawn、`get_state`、persistent session create/open、prompt accepted、text/tool projection、abort、settled、restart/reopen hydration、两个 worker 同时 run、一个 subagent child、clean shutdown。 ## 17. 可观测性与性能预算 ### 17.1 Privacy-safe milestones 每次 cold/warm 首发记录: | milestone | 含义 | | --- | --- | | `conversation.local_create` | 产品 Conversation metadata 写入 | | `composer.interactive` | 选择到 Textarea 可编辑 | | `worker.queue_wait` | worker permit 等待 | | `worker.spawn` | child spawn | | `rpc.ready` | 首次 `get_state` 成功 | | `session.open` | Pi session create/open | | `resources.ready` | provider/prompt/skills/extensions ready | | `prompt.accepted` | RPC prompt response success | | `agent.start` | accepted 到 `agent_start` | | `provider.first_event` | agent start 到首 provider event | | `renderer.first_commit` | Main 首 patch 到 Renderer commit | | `agent.settled` | accepted 到权威 idle | - `OBS-001`:记录匿名 Conversation 局部短 ID、workerGeneration、runId、cold/warm 和 duration。 - `OBS-002`:不得记录 prompt、文件内容、credential、完整路径、账号 ID、完整 request ID 或原始 provider body。 - `OBS-003`:本地 runtime overhead 与 provider first-event 延迟必须分开。 - `OBS-004`:报告 p50/p95/max、样本数和平台/packaged 状态,不只报单次体验。 ### 17.2 发布候选预算 | 指标 | p95 目标 | | --- | ---: | | 创建项目本地 metadata | ≤ 1,000 ms | | 创建伙伴本地 metadata | ≤ 500 ms | | 创建空 Conversation metadata | ≤ 500 ms | | 选择新 Conversation → Composer 可编辑 | ≤ 500 ms | | warm worker `rpc.ready` | ≤ 1,500 ms | | cold worker `rpc.ready` | ≤ 3,000 ms | | warm prompt accepted | ≤ 250 ms | | cold prompt accepted | ≤ 3,000 ms | | Main delta → Renderer commit | ≤ 50 ms | Operational limits:metadata Host API 5 秒;spawn/RPC ready 10 秒;graceful shutdown 3 秒;stream batching 16–33 ms。 这些预算只衡量本地 runtime/GUI overhead。受控 provider-shaped first event 单独报告,不得用它掩盖本地超时,也不得把它计入本地 p95。若未来自愿收集真实外部 Provider first token 数据,只能作为非阻断诊断样本,不改变本次 Accepted Risk。 ### 17.3 必测场景 1. 全新 userData:创建项目、首伙伴、首 Conversation,不发送,测 Composer。 2. 首次 prompt:拆分 spawn/RPC/session/resources/accepted/provider/first commit。 3. 重启同一 Conversation,测 warm restore。 4. 两个不同项目 Conversation 同时 prompt,通过最终 packaged provider-shaped loopback 验证窗口重叠、事件路由和单侧 abort 隔离;不要求真实外部 Provider turn。 5. 同项目两个 Conversation 同时只读 tool,验证并行。 6. 同项目两个 mutation tool,验证 lease 串行和可取消。 7. parent 并行四个只读 subagent,同时另一个 Conversation 运行,验证全局 cap。 8. 100 KB cumulative tool output、长 thinking、混合 blocks,测 patch/commit/bytes。 9. 大图片输入,确认 SSE/Renderer 不重复搬运 base64。 10. worker crash、SSE reconnect、app quit,确认恢复与进程清理。 ## 18. 故障与恢复 | 故障 | 用户状态 | 自动动作 | 禁止行为 | | --- | --- | --- | --- | | spawn 失败 | 本地 Agent 启动失败;Composer 可编辑 | 手动 recover | 无限重试/禁用整个 Chat | | RPC ready timeout | worker failed | kill、generation +1 | 复用未知 child | | malformed stdout | worker crashed | 有界诊断、kill/recover | 当 assistant 文本 | | mutating RPC timeout | request uncertain | reconcile session/queue | 静默重发 prompt | | provider 429/5xx | retry banner | Pi 有界 retry | Main 叠加无界 retry | | credential expired | auth error | refresh once + reopen | 输出 credential | | extension error | 对应 tool/run error | 隔离目标 Conversation | 杀所有 worker | | worker exit | recoverable | 取消 RPC/interaction/lease/child | 接收旧 generation event | | SSE gap | reconnect | GET 目标 snapshot | 清空所有 Conversation | | unreadable session | recoverable | 保留文件、允许诊断/新建 | 自动截断/覆盖 | | disk write failure | persistence error | 停止目标新 prompt | 继续生成并假装持久化 | | write lease wait | tool waiting | 可 abort | 并发 mutation | `recover` MUST:关闭旧 generation → cancel pending 资源 → 3 秒有界退出/kill → 从 registry 读取 sessionKey → 用最新 provider/resource revision spawn → `get_state/get_entries` hydrate → 发布新 generation Snapshot。不得自动重放未确认 prompt。 ## 19. 验证合同 ### 19.1 Unit - RPC framing、correlation、protocol violation、timeout、generation invalidation。 - event projector 的 text/thinking/toolcall、cumulative tool、`toolResult`、`agent_settled`、retry、compaction、queue、interaction、unknown custom。 - active leaf/parent hydration、abandoned branch、compaction retained tail、live/durable ID reconcile。 - worker single-flight、caps、LRU、crash cleanup、quit、provider revision。 - extension explicit resources、interaction cancel、subagent modes/caps/abort、write lease、known schema versions。 - schema v1 backup/migration、unresolved model、stable Agent ID、atomic failure。 ### 19.2 Main integration 使用可编程 fake Pi child 输出锁定版本 fixture,覆盖 Host acceptance → SSE patches → snapshot reconcile、gap/reconnect、provider redaction/model、persistence/reopen/fork/delete、files/changes、crash/recover。另有少量 real Pi smoke 防止 fixture 漂移。 ### 19.3 Renderer - metadata 请求 pending 时 selected Agent 仍可输入。 - 首发 optimistic/reconcile。 - per-Conversation run/error/queue/model/draft 隔离。 - message/tool/thinking/compaction/interaction/subagent 映射。 - seq gap 只刷新目标 Conversation。 - cumulative tool output 不重复。 - hidden Conversation 不重渲染全文。 ### 19.4 Electron E2E 至少覆盖: 1. 首项目/伙伴/Conversation Composer readiness; 2. 首 prompt cold path; 3. 两个 Conversation 同时 streaming; 4. 模型切换只影响一个 Conversation; 5. steer/follow-up queue; 6. compaction retry + settled; 7. interaction select/confirm/input; 8. parallel subagent nested UI; 9. worker crash/recover; 10. image attachment; 11. packaged skill/browser tool; 12. old schema migration notice。 ### 19.5 必跑命令 实现完成后必须运行仓库 pin 的 pnpm: ```text pnpm run typecheck pnpm run lint:check pnpm test pnpm run build:vite pnpm run test:e2e ``` 另外必须运行 Pi artifact verifier、real runtime smoke、Windows/macOS x64/arm64/Linux packaged smoke 和第 17 节性能场景。任何未运行项必须阻止 release,不得写成“本地测试通过即可替代”。这里的 real runtime smoke 按第 16.4 节使用实际 packaged Pi + 受控 provider-shaped endpoint,不要求真实外部 Provider Account。`PI-000` 的 macOS waiver 不延续到此发布门;macOS x64 或 arm64 任一缺失时均不得宣称 cross-platform release-ready。 ## 20. Definition of Done ### 20.1 架构 - `CUT-*`、`BND-*`、`ARC-*` 全部满足。 - Main 有 vendor-neutral `CodingConversationRuntime` 和唯一生产 `PiConversationRuntime`。 - Renderer 不导入或解析任何 Pi wire 类型。 - `.niancode/project.json`、稳定 Agent ID 和通用 app/storage 标识保持。 ### 20.2 能力 - 文本、图片、thinking、tool、retry、compaction、queue、interaction、模型/thinking、file/changes、browser、skills 全部通过测试与 E2E。 - 两个独立 Conversation worker 通过最终 packaged provider-shaped loopback turn 重叠且受控状态不串线;真实外部 Provider 隔离仍是明确 Accepted Risk,不得宣称已验证。 - subagent single/parallel/chain、child abort、失败隔离和全局 cap 正常。 - 同项目 mutation 受写租约控制。 - share、revert/unrevert、todos、global runtime controls 已从 UI、route、type、test、文档删除。 ### 20.3 性能 - 第 17 节 p95 预算全部达标。 - 不存在分钟级无界 Composer disabled/loading。 - 100 KB tool/长 thinking/大图片压力场景证明 batching、局部更新和 attachment ref 生效。 - 4 parent + 4 child 场景下主 UI 保持可交互,RSS/CPU 数据进入发布报告。 ### 20.4 迁移与交付 - schema v1 backup、schema v2、unresolved model、一次性 notice、完整版本回退 runbook 通过。 - package/lock/artifact 只含精确 Pi runtime,OpenCode 为零。 - typecheck、lint、unit、build、E2E、实际 packaged Pi + provider-shaped loopback、Windows x64/Linux x64/macOS x64/macOS arm64 packaged 与性能验证全部通过;`PI-000` 的 macOS waiver 不构成此处证据,真实外部 Provider waiver 也不得被写成 Pass。 - README、accepted ADR、canonical architecture/current state/business rules/success criteria 和 superseded OpenCode commitments 由 Integration Gate 更新。 - release note 明确旧 Conversation 不继续、已删除能力和回退限制。 ### 20.5 零残留 下列搜索只允许命中历史 task/proposal、migration backup/notice 文案: ```text rg -i "opencode|@opencode-ai|/api/opencode|\.opencode" . ``` `package.json`、lockfile、生产源码、`dist-electron`、`build/pi-runtime`、ASAR/extraResources 和最终安装产物不得命中 OpenCode package、binary、plugin、route 或 runtime resource。 ## 21. 需求到票据追踪 | 需求组 | 主交付票据 | | --- | --- | | `QG-*`(Phase-0 Windows x64/Linux x64;`QG-004`/`QG-005` explicit waivers) | `PI-000` | | Phase-0 延期的 macOS x64/arm64 workspace/staged/controlled packaged/loopback/metrics | `PI-150` | | `BND-*` | `PI-010`, `PI-020`, `PI-100`, `PI-110`, `PI-140` | | `ARC-*`, `EVT-*` contracts | `PI-010`, `PI-060` | | `DAT-*`, `MIG-*` schema/migration | `PI-020`, `PI-140` | | `RPC-*`, `RUN-*` process foundation | `PI-030`, `PI-050` | | `PRV-*`, `RES-*` | `PI-040` | | event mapping/hydration/recovery | `PI-060` | | `EXT-*`, `LEASE-*`, interaction | `PI-070` | | `UX-*` overall | `PI-020`, `PI-050`, `PI-080`, `PI-100`, `PI-120`, `PI-130`, `PI-150` | | `UX-040` true Conversation concurrency | `PI-050`, `PI-150` | | `UX-041`~`UX-044`, `subagent.v1` | `PI-080`, `PI-130` | | `CHG-*`, browser/game/skills/changes | `PI-090`, `PI-105`, `PI-130` | | `API-*` and `/api/coding` | `PI-100`, `PI-105` | | Snapshot/Patch Renderer store | `PI-110` | | `UX-001`~`UX-005` local-only creation | `PI-020`, `PI-100`, `PI-120` | | `UX-010`~`UX-033` Conversation UX | `PI-100`, `PI-120`, `PI-130` | | `REN-*` | `PI-120`, `PI-150` | | model/queue/compaction/interaction/subagent/changes UI | `PI-130` | | `CUT-*`, OpenCode deletion, release migration notice | `PI-140`, `PI-160` | | `PKG-*`, `OBS-*`, performance/cross-platform verification | `PI-150` | | 全部 DoD、独立审阅、canonical promotion | `PI-160` | 依赖、ready frontier、文件所有权和每张票据验收条件见关联票据文档。