Files
makelore/.project-docs/10-decisions/proposals/20260822-pi-runtime-spec-b6e2c9a4__pi-runtime-cutover-spec.md
T

1126 lines
59 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 镜像。
- 自动删除 `<userData>/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<ConversationRuntimeState>;
getSnapshot(conversationId: string): Promise<ConversationSnapshot>;
prompt(input: PromptConversationInput): Promise<PromptAcceptance>;
steer(input: QueueMessageInput): Promise<QueueAcceptance>;
followUp(input: QueueMessageInput): Promise<QueueAcceptance>;
abort(conversationId: string): Promise<void>;
setModel(input: SetConversationModelInput): Promise<ConversationModelState>;
setThinking(input: SetThinkingLevelInput): Promise<ConversationModelState>;
compact(conversationId: string): Promise<void>;
fork(input: ForkConversationInput): Promise<ForkResult>;
recover(conversationId: string): Promise<ConversationRuntimeState>;
dispose(conversationId: string): Promise<void>;
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
<userData>/coding-runtime/pi/
config/
sessions/<project-id>/<opaque-session-key>.jsonl
prompts/<project-id>/<agent-id>.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<string, string>;
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-<timestamp>/` 复制原 `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
`<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、文件所有权和每张票据验收条件见关联票据文档。