fix(dashboard): re-read only the Goal an action touched - #4559
Conversation
Pausing one Goal sent every other Goal back to the loading lane. The reconciliation that follows a lifecycle action loaded the workspace directory and then threw away all per-Goal snapshots, so each peer had to be read again from the status service before its card returned. The directory entry is the cheap authoritative signal for "this Goal's lifecycle did not move here". A same-source refresh now keeps the snapshot of every Goal whose entry held, and re-reads the Goals the action touched or whose entry changed. The reconcile also names the touched Goal, so a peer is never re-read for another Goal's pause, resume or open, while the shipped full re-read on the manual refresh is unchanged. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
…tion The browser harness never served the workspace directory, so the progressive path had no end-to-end coverage at all and the peer re-read could not be caught. The fixture can now answer the directory view and one Goal at a time, behind an explicit option so every existing scenario keeps its current path, and the new scenario pauses and resumes one Goal while asserting that no peer is re-read, no card re-enters the loading state and every peer card stays on the board. The loader smoke adds the retention rule itself, including the touched Goal, a renamed Goal and a Goal that left the directory. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Exact head reviewed: 9ad5fcc
动机
在前端暂停一个 Goal 之后,别的 Goal 会整屏重新加载:卡片退回"加载中"泳道,再逐个重新拉取。根因是渐进加载刷新时丢弃了所有 per-Goal 快照 —— reconciliation 走 background 路径且没有 retryOnly,于是 retained 为空,每个 peer 都要重新向状态服务完整读一次。本机 8 个活跃 Goal 时,一次暂停触发 9 次 Goal 读取。
改动思路
目录条目是"这个 Goal 的生命周期没有动"的廉价权威信号。同源刷新改为按每个 Goal 的目录条目是否变化决定保留:条目未变的 Goal 保留快照不重读,条目变化的 Goal 重读;reconciliation 额外声明本次动作触碰的 Goal,它一律重读。手动刷新保持原有全量重读语义。
具体改动
- workspace-progressive-status.ts:新增纯函数 reusableGoalSnapshots(previous, directory, { invalidateGoalIds })。
- dashboard-page.tsx:loadFromUrl 接受 reuseSnapshots / invalidateGoalIds;onReconcileStatus 传入被触碰的 Goal。
- personal-workspace-page.tsx:新增 reconcileStatus(invalidateGoalIds),四处 reconciliation 都声明被触碰的 Goal。
- 覆盖:loader smoke 增加 7 条保留规则断言;fixture 支持 workspace-directory 与 per-Goal 读取(progressiveWorkspace 选项,默认关闭);新增 progressive-loading 场景做暂停/恢复的端到端断言。
对主干的风险
中低。被触碰的 Goal 仍重读,未被触碰的 Goal 展示上一次读取的快照(目录条目未变即未动)。手动刷新、来源切换、首屏加载、跨 registry 变更语义不变。fixture 的目录支持默认关闭,既有五个场景路径逐字不变。
我的整体评价
正向且 proportional:一个纯函数 + 一处刷新语义 + 一个端到端场景,把"一次动作重读整个工作区"收敛为"重读被触碰的那一个",并第一次给渐进加载补上浏览器级覆盖。
Evidence:
- exact head 9ad5fcc
- personal-workspace-browser-smoke 全量通过(6 个场景)
- 负向对照:回退该修复后新场景失败,错误为 "Pausing one Goal re-read its peers: ["research-monitor"]",与业主报告一致
- smoke:workspace-progressive-loader → {"ok":true,"checks":32}
- tsc --noEmit 与 goal-order / team-plan-proposal / chat-route / agent-family / pwa-bundle 全通过
- loopx canary premerge --from-git-diff → merge_gate_passed true,surfaces public_boundary;advisory 为已知基线
备注:apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs 未被任何 script/CI 引用且在 origin/main 上已有多条过期失败断言,本 PR 不改它,另行记录。
English verdict: APPROVE
动机
在前端暂停一个 Goal 之后,别的 Goal 会整屏重新加载一遍:卡片退回"加载中"泳道,然后逐个重新拉取。
根因在渐进加载的刷新路径。
loadFromUrl在刷新时是这样保留快照的:生命周期动作后的 reconciliation 走的是
onReconcileStatus = () => loadFromUrl(url, { background: true })—— 没有retryOnly,于是retained = {}:所有 Goal 的 per-Goal 快照被丢掉,每个 peer 都得重新向状态服务完整拉一次,期间显示为"加载中"。本机 8 个活跃 Goal 时,一次暂停会让 9 个 Goal 重新加载。改动思路
目录条目本身就是"这个 Goal 的生命周期没有动"的廉价权威信号:条目没变,它的快照就仍然描述它。于是:
具体改动
src/data/workspace-progressive-status.ts:新增纯函数reusableGoalSnapshots(previous, directory, { invalidateGoalIds }),规则是"条目未变 + 未被触碰 + 仍在目录中"才复用。dashboard-page.tsx:loadFromUrl支持reuseSnapshots/invalidateGoalIds;onReconcileStatus传入被触碰的 Goal。personal-workspace-page.tsx:新增reconcileStatus(invalidateGoalIds),四处 reconciliation(生命周期直接应用、proposal 应用、typed apply、打开 Goal)都声明被触碰的 Goal。progressiveWorkspace选项,默认关闭,既有场景路径不变);新增progressive-loading场景。对主干的风险
中低。影响面是"一次动作之后要重读哪些 Goal":没有动作触碰的 Goal 不再被重读,因此它们展示的是上一次读取的快照(这正是产品语义:目录条目没动说明它没动)。手动刷新、来源切换、首次加载、跨 registry 变更的行为都不变;被触碰的 Goal 仍然重读。
我的整体评价
正向且 proportional:一个纯函数 + 一处调用点语义 + 一个端到端场景,把"一次动作重读整个工作区"收敛成"重读被触碰的那一个",并且第一次给渐进加载路径补上了浏览器级覆盖。
Validation:
personal-workspace-browser-smoke全量通过:navigation-sorting, chat-recovery, typed-actions, team-plan, execution-chip, progressive-loadingdashboard-page.tsx+personal-workspace-page.tsx+ loader 改动后,新场景失败并精确复现症状:Pausing one Goal re-read its peers: ["research-monitor"]npm run smoke:workspace-progressive-loader→{"ok":true,"checks":32}(含保留规则 7 条)tsc --noEmit、smoke:goal-order、smoke:team-plan-proposal、smoke:chat-route、smoke:agent-family、smoke:pwa-bundle全通过loopx canary premerge --from-git-diff→merge_gate_passed: true,surfacespublic_boundary;advisorycontrol-plane-maintainability-ratchet-smoke.py为 cleanorigin/main已知基线顺带发现(不在本 PR 范围,已单独记录):
apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs没有被任何 npm script / CI 引用,且在origin/main上就已经有多条断言过期失败(例如 #4170 移除isManagerProjectionQuestion后留下的两条)。本 PR 刻意不改它。Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
English verdict: APPROVE