Skip to content

fix(dashboard): re-read only the Goal an action touched - #4559

Merged
huangruiteng merged 2 commits into
mainfrom
codex/progressive-lifecycle-refresh
Sep 16, 2026
Merged

huangruiteng merged 2 commits into
mainfrom
codex/progressive-lifecycle-refresh

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

动机

在前端暂停一个 Goal 之后,别的 Goal 会整屏重新加载一遍:卡片退回"加载中"泳道,然后逐个重新拉取。

根因在渐进加载的刷新路径。loadFromUrl 在刷新时是这样保留快照的:

const retained = options.retryOnly && ... && progress?.directory.registry_revision === directory.registry_revision
  ? progress.snapshots : {};
setProgress({ directory, snapshots: retained, errors: {} });

生命周期动作后的 reconciliation 走的是 onReconcileStatus = () => loadFromUrl(url, { background: true }) —— 没有 retryOnly,于是 retained = {}:所有 Goal 的 per-Goal 快照被丢掉,每个 peer 都得重新向状态服务完整拉一次,期间显示为"加载中"。本机 8 个活跃 Goal 时,一次暂停会让 9 个 Goal 重新加载。

改动思路

目录条目本身就是"这个 Goal 的生命周期没有动"的廉价权威信号:条目没变,它的快照就仍然描述它。于是:

  • 同源刷新时,按 每个 Goal 的目录条目是否变化 决定保留:条目相同的 Goal 保留快照,不重读;条目变化的 Goal 重读。
  • reconciliation 额外声明本次动作触碰的 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。
  • 覆盖:loader smoke 增加保留规则的 7 条断言;fixture 新增 workspace-directory 与 per-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-loading
  • 负向对照:仅回退 dashboard-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,surfaces public_boundary;advisory control-plane-maintainability-ratchet-smoke.py 为 clean origin/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

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 huangruiteng left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@huangruiteng
huangruiteng merged commit 7153f8f into main Sep 16, 2026
5 of 6 checks passed
@huangruiteng
huangruiteng deleted the codex/progressive-lifecycle-refresh branch September 16, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant