AGC 项目状态一次抖动后被永久钉成「检查失败」,且不会自愈(首页最近项目同时变空) #490

Open
opened 2026-09-23 12:13:36 +08:00 by suzmii · 1 comment
Member

现象

AGC「卡住」一次之后,项目列表的全部行显示「检查失败 + 待识别」,首页「最近项目」变成「暂无最近项目」。后端完全恢复也不会自愈,只有重启客户端才恢复。

根因

前端把一次性失败当成了终态,且没有任何重试入口:

  • useRecentProjects.ts:19 每个最近项目的目录检查只有 5s 预算;失败在 inspectRecentWorkspace 里被 catch 吞成 null。
  • useRecentProjects.ts:83-97 增量投影 next[workspace] = current[workspace] ?? null 原样保留上一次的 null,失败结果跨刷新存活。
  • model.ts:270-272 directoryStatus === null → 「检查失败」;projectKind 落成 unknown → 「待识别」。
  • useRecentProjects.ts:208-210 首页只收 canOpen 为真的行,canOpen 依赖 Boolean(directoryStatus) → 同一批 null 让首页变空态。
  • effect 依赖只有 [recentWorkspaces, recentWorkspaceRefreshKey](:115);refreshKey 只有 rememberRecentWorkspace 会 +1,refreshRecentWorkspace 只在重命名项目时调用。没有自动重试,也没有窗口 focus / visibility 重查。

⇒ 一次 5s 量级的抖动,就能让整个会话的列表与首页保持错误状态,与后端真实状态彻底脱钩。

现场证据(2026-09-23 本机 dev 客户端)

  • 11:19:41 客户端启动;11:24:12 两个平台接口 15s 超时(/api/runtime/frontend-config、/api/llm/models,错误池里 source=api);11:24:26 最后一条 Rust 日志 project_snapshot.sync.completed … trigger=project-close(正好是关闭项目、回启动页那一刻),此后 application.log 静默 32 分钟。
  • 11:56 在同一进程里逐条复跑 inspect_local_project_directory(8 个最近项目):全部 OK、0ms,返回完整 status 对象;而界面仍是全「检查失败」。
  • 结论:本地命令与 IPC 是好的,是界面状态被钉住了。
  • 停顿发生在哪一层(远端网络 / IPC 回传 / WebView 主线程)尚未定位;已用控制台探针(invoke 拦截 + IPC 心跳 + 界面状态计数 + 错误池捕获)继续采证。

打算怎么改

  1. 失败不落终态:增量投影只保留成功结果,失败项回到「检查中」并重新进入本轮检查。
  2. 行级有限次退避重试:单次 5s 预算不变,失败后做有限次(含退避)重试,避免把瞬时抖动固化成状态。
  3. 窗口重获焦点时重查:focus / visibilitychange 时,若存在失败或缺失项则重跑检查(复用 ErrorReportNotice 已有的监听模式)。
  4. 不动 5s 超时口径、不动 Rust 侧命令、不动任何公开契约与持久化格式。

验证

  • 定向:tests/recentProjectsHook.test.tsx,新增「一次失败后能自愈」「失败结果不跨刷新保留」用例。
  • 门禁:npm run agc:typecheck、npm run check:encoding、git diff --check。
  • 真机:制造一次目录检查失败(invoke 超时),确认列表能从「检查失败」回到正常态,且首页最近项目恢复。
## 现象 AGC「卡住」一次之后,项目列表的全部行显示「检查失败 + 待识别」,首页「最近项目」变成「暂无最近项目」。**后端完全恢复也不会自愈**,只有重启客户端才恢复。 ## 根因 前端把一次性失败当成了终态,且没有任何重试入口: - `useRecentProjects.ts:19` 每个最近项目的目录检查只有 5s 预算;失败在 `inspectRecentWorkspace` 里被 `catch` 吞成 `null`。 - `useRecentProjects.ts:83-97` 增量投影 `next[workspace] = current[workspace] ?? null` **原样保留上一次的 `null`**,失败结果跨刷新存活。 - `model.ts:270-272` `directoryStatus === null` → 「检查失败」;`projectKind` 落成 `unknown` → 「待识别」。 - `useRecentProjects.ts:208-210` 首页只收 `canOpen` 为真的行,`canOpen` 依赖 `Boolean(directoryStatus)` → 同一批 `null` 让首页变空态。 - effect 依赖只有 `[recentWorkspaces, recentWorkspaceRefreshKey]`(`:115`);`refreshKey` 只有 `rememberRecentWorkspace` 会 +1,`refreshRecentWorkspace` 只在重命名项目时调用。**没有自动重试,也没有窗口 focus / visibility 重查。** ⇒ 一次 5s 量级的抖动,就能让整个会话的列表与首页保持错误状态,与后端真实状态彻底脱钩。 ## 现场证据(2026-09-23 本机 dev 客户端) - `11:19:41` 客户端启动;`11:24:12` 两个平台接口 15s 超时(`/api/runtime/frontend-config`、`/api/llm/models`,错误池里 `source=api`);`11:24:26` 最后一条 Rust 日志 `project_snapshot.sync.completed … trigger=project-close`(正好是关闭项目、回启动页那一刻),此后 `application.log` 静默 32 分钟。 - `11:56` 在同一进程里逐条复跑 `inspect_local_project_directory`(8 个最近项目):**全部 OK、0ms**,返回完整 status 对象;而界面仍是全「检查失败」。 - 结论:本地命令与 IPC 是好的,是界面状态被钉住了。 - 停顿发生在哪一层(远端网络 / IPC 回传 / WebView 主线程)尚未定位;已用控制台探针(invoke 拦截 + IPC 心跳 + 界面状态计数 + 错误池捕获)继续采证。 ## 打算怎么改 1. **失败不落终态**:增量投影只保留成功结果,失败项回到「检查中」并重新进入本轮检查。 2. **行级有限次退避重试**:单次 5s 预算不变,失败后做有限次(含退避)重试,避免把瞬时抖动固化成状态。 3. **窗口重获焦点时重查**:`focus` / `visibilitychange` 时,若存在失败或缺失项则重跑检查(复用 `ErrorReportNotice` 已有的监听模式)。 4. 不动 5s 超时口径、不动 Rust 侧命令、不动任何公开契约与持久化格式。 ## 验证 - 定向:`tests/recentProjectsHook.test.tsx`,新增「一次失败后能自愈」「失败结果不跨刷新保留」用例。 - 门禁:`npm run agc:typecheck`、`npm run check:encoding`、`git diff --check`。 - 真机:制造一次目录检查失败(invoke 超时),确认列表能从「检查失败」回到正常态,且首页最近项目恢复。
suzmii added the Kind/Bug
Reviewed
Confirmed
1
Priority
High
2
labels 2026-09-23 12:13:36 +08:00
Author
Member

已提 PR:#492(分支 fix/agc-recent-project-status-retry)。

改动口径(前后对照):

场景 改前 改后
单次目录检查失败 立刻落成「检查失败」,本会话不再重查 先就地重试一次(300ms),成功则整行直接正常
持续失败 后续刷新把上一轮 null 原样搬过去 失败项不进新投影,刷新时回到「检查中」并重新检查
一轮结束仍有失败行 无任何后续动作 15s / 45s / 120s 退避重跑整张列表(最多 3 次),连续无失败即清零

验证:

  • 新增「单次失败会重试」「刷新时重查失败项」两个用例,在改前代码上必挂(stash 掉 hook 实现后跑 → 2 failed | 1 passed)。
  • 退避重查路径用一次性脚本证明持续失败后 15.0s 自动恢复(脚本已删除)。
  • appSurface 221 tests / 13 skipped 全通过、agc:typecheck、check:encoding、eslint、prettier、git diff --check 全绿。

仍未定位:「卡住」本身的停顿层(远端网络 / Rust IPC 回传 / WebView 主线程)。本 PR 只保证一次抖动不再永久污染界面;采证探针(invoke 拦截 + IPC 心跳 + 界面状态计数 + 错误池捕获)继续用在本 issue 里。

已提 PR:#492(分支 `fix/agc-recent-project-status-retry`)。 改动口径(前后对照): | 场景 | 改前 | 改后 | | --- | --- | --- | | 单次目录检查失败 | 立刻落成「检查失败」,本会话不再重查 | 先就地重试一次(300ms),成功则整行直接正常 | | 持续失败 | 后续刷新把上一轮 null 原样搬过去 | 失败项不进新投影,刷新时回到「检查中」并重新检查 | | 一轮结束仍有失败行 | 无任何后续动作 | 15s / 45s / 120s 退避重跑整张列表(最多 3 次),连续无失败即清零 | 验证: - 新增「单次失败会重试」「刷新时重查失败项」两个用例,在改前代码上必挂(stash 掉 hook 实现后跑 → 2 failed | 1 passed)。 - 退避重查路径用一次性脚本证明持续失败后 15.0s 自动恢复(脚本已删除)。 - appSurface 221 tests / 13 skipped 全通过、agc:typecheck、check:encoding、eslint、prettier、git diff --check 全绿。 仍未定位:「卡住」本身的停顿层(远端网络 / Rust IPC 回传 / WebView 主线程)。本 PR 只保证一次抖动不再永久污染界面;采证探针(invoke 拦截 + IPC 心跳 + 界面状态计数 + 错误池捕获)继续用在本 issue 里。
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GenarrativeAI/Genarrative#490