Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/sync-upstream.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,13 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
echo "::warning title=REQ-012 anchor tripwire::upstream sync broke reskin anchor contract — visual re-verify required before any release"
fi

# S39 复盘病灶 2(2026-07-12):ADR-020 预警过「冻结前端 ↔ 新引擎的运行时契约漂移,
# typecheck 测不全,真机冒烟兜底」——此前冒烟从未机械化,sync 后引擎行为变化只能等真机撞见
# (REQ-083 复验实锤:同一验收脚本 7-10 绿、合 sync 后 stage5 僵死)。本步无头启动合并后的
# 引擎 → 打关键端点 → 硬杀 → 同端口重启 → 再打(respawn 族覆盖)。失败 = 本 run 红:
# 引擎运行时冒烟破,发布/继续开发前必须人工排查(merge 已推送——红的意义是高声报警)。
- name: Engine runtime smoke (S39 — boot/probe/kill/reboot)
run: |
bun install
bash scripts/engine-smoke.sh
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

## [Unreleased]

### Fixed
- **模型选择框不再「已配置的供应商凭空消失 + 代理模型全灰」**(REQ-083,需下个签名版本生效)——引擎重启的间隙(存 Key、登录、激活代理都会触发引擎重启)打开模型选择框,此前会静默渲染成一个假象:已配置 Key 的 DeepSeek/智谱整体不可见、代理模型明明是会员也全部灰锁,而且点灰行还会再触发一次引擎重启,越点越坏。现在:①连不上引擎时如实显示「正在连接引擎…稍后自动恢复」,已配置的供应商保留占位行(「已配置 · 模型加载中…」),绝不消失;②弹窗自动重试,引擎一恢复列表就地补全,无需重开;③引擎不可达时点灰行不再触发重启(只有「引擎在线且代理确实未激活」才会激活代理)。真机断言:两类断网场景(拦截取数/真杀引擎)点灰行全程零重启。

### Added
- **新增出厂技能「cloud-dispatch」:会话里就能把任务派到云上跑**(REQ-082,需下个签名版本生效)——登录(代付)状态下,让 agent「云端调研一下 X」即可正确派发:技能教会模型云任务的完整契约(调研/代码审查/文档三类管线与沙箱档、预算上限、结果取回),并内置数据边界纪律(代码只送 diff、绝不带密钥/.env)。未登录或 BYOK 时如实告知云派发不可用并指路登录,不会假装派发。
- **新增出厂技能「office-docs」:做表格/写文档/生成 PPT 有了正确指路**(REQ-080,需下个签名版本生效)——问「帮我做个 Excel」时模型知道该用哪个连接器(读文档=markitdown、写 xlsx/docx/pptx=对应办公连接器)、没装时指路扩展市场一键安装(或征得同意后用本地 Python 脚本兜底)、内置表格制作惯例(真实数字/日期类型、表头冻结、公式而非写死数值)与 PDF 生成代码片段(生态没有可信的 PDF 写连接器,如实补位不硬装)。
Expand Down
Binary file added docs/audits/2026-07-10-s39-req083/01-baseline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions docs/audits/2026-07-10-s39-req083/verify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# REQ-083 真机验证 — 模型选择框 respawn 竞态修复(2026-07-10,S39)

> 环境:dev 构建(vite 5173 非陈旧 bundle,`location.href` 已核)+ dev 渠道 userData(真实登录态 PRO + 已配置 deepseek/zhipuai 双 Key)。驱动 = CDP 9222(dev 默认开),脚本断言 + 截图双证。

## 根因复盘(报障当日日志定案)

用户报障(v0.1.2 打包真机,10:37–10:42):已配置 Key 的 DeepSeek/智谱在选择框整体消失;登录 PRO 后代理模型全灰。日志证据链:

1. 5 分钟内 5 次 sidecar respawn(存 Key ×2 / 登出 / 登录 / 点灰行 ×N —— `enableProxy` 无条件 respawn),每次对应 renderer 一波 `Failed to fetch`(main.log + renderer.log 时间戳逐一对齐);
2. ModelPickPop 弹窗打开只拉一次 `config.providers` 且 `.catch(()=>{})` 静默吞 → engineModels 空 → ① configured BYOK 两头落空(不进「需 KEY」区、又无模型行)= 整体不可见;② `proxyLive()` 假 → 代理 13 行全 locked;账户横幅走 main IPC 不受影响,仍显示「PRO 会员」= 矛盾画面;
3. member/balance 态点 locked 行无条件 `window.api.auth.enableProxy()` → 又一次 respawn + renderer reload = **自续循环**(10:42:38.441 上一次 respawn 完成的同一毫秒排入下一次,实锤)。

**排除项(实证)**:非端点域名切换(`alpha-gateway.tidelabs.click` HTTP 200、`ALPHA_BASE_URL` 正确、白名单同步 `byok: unrestricted`);非数据丢失(干净重启后 CDP 直查引擎:deepseek 4 模型 / zhipuai 13 模型 / alpha 13 模型全在,弹窗渲染全对)。

## 修复(三点)

1. **取数诚实 + 自愈**:load 状态机(`engineReady`/`engineStalled`)+ 退避重试(1s/2s/4s/8s 封顶,弹窗存活期间持续;sdk 未就绪同样可重试);非 ok 态顶置「正在连接引擎…」note + configured BYOK 占位行(`已配置 · 模型加载中…`,disabled)。健康路径首拉 ~ms 级不闪占位(stalled 门)。
2. **点灰行不再火上浇油**:`lockedPickAction` 纯函数(login/recharge/activate/none)——仅「引擎在线(engineReady)且代理节点确实缺席(!proxyLive)」才 activate;引擎不可达一律 none。`enableProxy` 唯一调用点即此,main 侧无需改动。
3. **respawn 后自动恢复**:重试循环覆盖(弹窗不重开就地补全);key 保存后 3s 补拉保留(失败进同一重试循环)。

## 断言结果(scripts: scratchpad/verify-req083.ts,全 PASS)

| # | 场景 | 断言 | 结果 |
|---|---|---|---|
| 0 | 基线(引擎健康) | 弹窗 30 行可选 0 locked,BYOK 模型行 19(deepseek+glm),无 note | PASS(01-baseline.png) |
| 1 | 拦截 `/config/providers`(staging 复现 Failed to fetch) | note 出现 + DeepSeek/智谱占位行 ×2 + 代理 13 行 locked | PASS(02-outage-honest-state.png) |
| 2 | 故障态点 locked 代理行 ×1 | 页面未 reload(marker 存活)、弹窗仍开、**零 respawn** | PASS |
| 3 | 解除拦截 | **同一弹窗不重开**自动恢复:note 消失、BYOK 19 行回来、0 locked | PASS(03-self-healed-same-popup.png) |
| 4 | 真死服务器(`killSidecar`)重复 1+2 | 同 1/2 全过 | PASS(04-real-outage-honest-state.png) |
| 5 | `retrySidecar` 恢复 | respawn+reload(预期行为)后基线复核:0 locked / 19 BYOK / 无 note | PASS(05-restored-baseline.png) |

**日志复核**:整场验证 main.log 仅 1 次 `respawning sidecar (proxy activation)` = stage 5 主动恢复;stage 2/4 两次故障态点灰行 **零 respawn**(修复前每点一次触发一次)。

## Gates

- alpha-check 全绿(北极星守卫 + typecheck + 单测 673/673,新增 model-picker-logic 6 例)
- 零改上游文件(全部改动落 `packages/ui-mac` 自有文件)
- UI PR 用户亲验门:dev 已留可用态(登录 PRO + 双 BYOK),待用户 GO 后合并

---

## 2026-07-12 复验(用户委托真机验收)—— FAIL → 补丁 → 全绿

**复跑结果**:stage 0-4 全 PASS;**stage 5(retrySidecar 恢复)FAIL** —— 暴露第四种故障形态「悬挂」:
respawned 引擎无鉴权请求秒回 401(中间件活着),带鉴权 `/config/providers` **永不返回**(捕获请求挂起数分钟
无 resolve/reject;新发 6s 超时请求 TimeoutError)。原修复只覆盖 reject,未覆盖 hang → fetch 既不成功也不
失败 → 状态机进不了 stalled → 无 note 无占位、13 行全灰、BYOK 消失、不自愈(用户当场撞见并截图)。

**补丁(同日)**:
1. `model-picker-logic.ts` 新增 `ENGINE_FETCH_TIMEOUT_MS = 10_000` + 单测 2 例(超时 > 一切退避间隔;取值窗口);
2. `alpha-composer-model.tsx` 取数带 `AbortSignal.timeout` —— 悬挂转成可重试失败进既有重试循环,abort 同时
关闭底层连接(防 Chromium 连接池复用死套接字);
3. 验收 harness 固化进仓 `packages/ui-mac/scripts/verify-picker-respawn.ts`(S39 复盘病灶 1):新增 **hang 注入
阶段**(尊重 abort signal 的永不返回 fetch)+ 恢复阶段改**收敛轮询**(修 7-10 脚本定时快照把引擎暖机误判
为回归的缺陷)+ 断言硬失败退出非零;
4. `scripts/engine-smoke.sh` + sync-upstream.yml 新步(S39 复盘病灶 2):每日 sync 后无头启动引擎 → 打
`/config/providers` → 硬杀 → 同端口重启 → 再打,红 = 引擎运行时冒烟破(本地实跑绿)。

**复验(补丁后,harness 全量)**:14/14 断言 PASS —— 基线 30 可选/19 BYOK;reject 故障态诚实 + 点灰行零
respawn/reload + 同弹窗自愈;**hang 故障态超时后出诚实 note + 占位(修复盲区闭合)+ 解除后自愈**;真杀
sidecar 同全过;retrySidecar 恢复后收敛回满基线。证据:`docs/audits/2026-07-12-s39-req083-reverify/`(7 png)。

**残余(单独立项追踪,不阻断本 REQ)**:引擎 respawn 后悬挂为**非确定性竞态**(6 个 kill→retry 循环 1 现),
判决实验(僵死瞬间带鉴权外部 curl 分辨引擎僵死 vs renderer 连接池僵死)未能复现完成;UI 层超时补丁使其
用户面退化为「诚实提示 + 自动恢复」,不再是静默全灰。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions docs/sprints/2026-07-10-s39-req083-picker-respawn-race/sprint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# S39 — REQ-083 模型选择框 respawn 竞态修复(2026-07-10)

> 契约(ADR-018):目标 / 抽取 IDs / task 表 / gates / 结果 / 回写清单。

## 目标

修掉模型选择框在 sidecar respawn 窗口内的三个叠加缺陷:取数静默失败(BYOK 消失 + 代理全灰)、点灰行无条件触发 respawn(自续循环)、respawn 后无自愈。根因复盘见 BACKLOG REQ-083 行内(用户报障 2026-07-10,v0.1.2 真机日志定案)。

## 抽取

| ID | 状态入 | 状态出 |
|---|---|---|
| REQ-083 | ready | shipped(PR #173) |

## Tasks

- [x] T1 取数诚实 + 自愈:ModelPickPop 引擎模型表改 load 状态机(`engineReady`/`engineStalled`)+ 退避重试(sdk 未就绪/fetch 失败均可重试,弹窗存活期间自动恢复);stalled 态顶置「正在连接引擎」note,已配置 BYOK 供应商渲染占位行(不再整体消失,反 C28 placebo;健康路径 ~ms 级首拉不闪占位)
- [x] T2 点灰行不再火上浇油:locked 行点击动作抽纯函数 `lockedPickAction`(login/recharge/activate/none)——member/balance 态仅当**引擎在线且代理节点确实缺席**才调 `enableProxy()`;引擎不可达一律 no-op,杜绝「点灰行 → respawn → reload → 再点」循环(`enableProxy` 唯一调用点在此,main 侧零改动)
- [x] T3 respawn 后自动恢复:T1 重试循环覆盖(弹窗不需重开,实测同一弹窗就地补全);key 保存后 3s 补拉保留
- [x] 单测:`model-picker-logic.test.ts` 6 例(lockedPickAction 全分支 + 退避节奏);全量 673 全绿
- [x] 真机验证(dev + CDP,证据 [audits/s39](../../audits/2026-07-10-s39-req083/verify.md)):① 拦截取数 + 真杀 sidecar 两场景 = note + BYOK 占位 ×2 + 点灰行**零 respawn**(main.log 复核全场仅 1 次主动恢复 respawn);② 解除故障 → 同一弹窗不重开自动恢复(19 BYOK 行 + 0 locked);③ 基线不回归(30 行可选)
- [x] **T4(2026-07-12 委托验收发现的盲区)悬挂转可重试失败**:`ENGINE_FETCH_TIMEOUT_MS` + 取数带 `AbortSignal.timeout`(hang → 超时 → stalled note + 重试;abort 关闭死连接)+ 单测 2 例
- [x] **T5 验收 harness 固化进仓**(复盘病灶 1):`packages/ui-mac/scripts/verify-picker-respawn.ts`,新增 hang 注入阶段 + 恢复收敛轮询 + 断言硬失败;复验 14/14 PASS(证据 [audits/2026-07-12-reverify](../../audits/2026-07-12-s39-req083-reverify/))
- [x] **T6 引擎冒烟机械化**(复盘病灶 2):`scripts/engine-smoke.sh` + sync-upstream.yml 新步(boot → probe → kill → reboot → probe,本地实跑绿)

## Gates

- [x] alpha-check(北极星守卫 + typecheck + 单测)全绿
- [x] 零改上游文件
- [x] UI PR 用户亲验门(训示 2026-07-09):用户 2026-07-12 **委托验收** —— 复验发现 stage5 FAIL(悬挂盲区,用户当场撞见)→ 同日补丁 → harness 全量 14/14 PASS → 用户指示「将 S39 处理掉」= GO 收口。残余(引擎 respawn 悬挂竞态,非确定性 1/6)单独立项追踪,UI 面已由超时补丁兜底

## 回写清单

- [x] BACKLOG REQ-083 → shipped(PR #173)
- [x] CHANGELOG [Unreleased] 用户可见条目
- [x] 证据:audits/2026-07-10-s39-req083/(5 png + verify.md)
Loading
Loading