Repository navigation
feat(workflows): 让运行详情与历史随运行状态更新(有界轮询 + 单调合并) - #82
Merged
EricWvi merged 7 commits intoOct 20, 2026
Merged
Conversation
Zpc001
approved these changes
Oct 17, 2026
Zpc001
left a comment
Contributor
There was a problem hiding this comment.
复审 f7bb828,通过。
核对项:
- 范围:7 个文件都是交付物(
api.ts、两个测试、src/test的轮询测试脚手架与 README),没有进度记录或无关改动;基于main10fd6dc,mergeable/CLEAN,frontend CI 绿。 - 轮询边界:非终态 5 秒;终态停;不认识的枚举继续轮询(避免冻住服务端仍在推进的运行);
undefined状态不轮询。标签页不可见/组件卸载由 TanStack 默认行为停止。 - 合并规则:终态不被非终态读拉回;同类读
updatedAt新的胜出;nodeStates按节点只补不删;历史页以服务端返回的成员为准,并复用同一合并规则,列表与详情不会互相矛盾。 - 身份保持:合并接在
structuralSharing,之后走replaceEqualDeep;未变读保持对象身份,避免每 5 秒让冻结画布重排。 - 反例有效:测试覆盖终态回退、陈旧读丢弃、历史行回退、未变读身份;PR 描述里的「去掉接线即失败」与用例对得上。
- 本地验证:
npx vitest run src/features/workflows(55 文件 / 591 用例)通过,npm run typecheck通过。
ADR 状态正确:实现的是 D7 的读取一半,真实执行与结果回报属 #70,ADR 保持 approved。
非阻塞建议(不要求本 PR 修改):
- 初次读取失败时
query.state.data仍是undefined,refetchInterval返回false,查询自身重试耗尽后页面不会自行恢复;如果希望错误态也自愈,可以在 error 状态保持轮询。 updatedAt用字符串比较依赖服务端时间戳格式一致。当前 Postgres ISO 输出可用,但若以后出现Z与+00:00混用或精度变化,建议改成比较解析后的时间。
真实 Run 的页内证据、断线重连与租户/轮次边界按 preflight 约定等 #70 接通后补。
zhangshilong123
force-pushed
the
feat/203-run-live-status
branch
from
October 20, 2026 09:14
f7bb828 to
1f1f574
Compare
Zpc001
approved these changes
Oct 20, 2026
Zpc001
left a comment
Contributor
There was a problem hiding this comment.
复审 head 1f1f574(基于 upstream/main f405823e),代码通过。
- 两条非阻塞建议都已正确落实:
keep polling after a failed run read(failed参数让初读失败继续轮询、错误态自愈,已持有的终态 run 不会被重新拉起来);order run reads by parsed timestamp(Date.parse,解析失败按更旧处理)。 - 合并规则正确:终态不可回退、同类读按
updatedAt取新、nodeStates按节点只补不删、structuralSharing用replaceEqualDeep保持未变读引用稳定。 - rebase 干净、CI 绿、mergeable CLEAN。
原生 Approve。运行 issue ora-space/circus#203 的 review:lgtm 会等真实 Run 页内证据(第 3 条)路径定后再按最终 head 打。
zhangshilong123
force-pushed
the
feat/203-run-live-status
branch
from
October 20, 2026 11:27
2f64b07 to
263e183
Compare
Zpc001
approved these changes
Oct 20, 2026
Zpc001
left a comment
Contributor
There was a problem hiding this comment.
复审(head 263e183058):Approve。
在 rebase 后的新 head 上复核了全部 6 个 commit:
- 原始 4 个(有界轮询 + 合并、页面跟随测试、失败读后继续轮询、按解析时间戳排序)逻辑不变,仍成立。
ce6c3f3「空时间戳按未发生」:readTimestamp把""当 absent 丢弃,run 级显示同时排除null与"",未跑到的节点不再渲染空时间行。正确。263e183「容忍无 executor 绑定的 agent 节点」:executor缺失时配置区留空不崩溃,覆盖 executor-slot 谱系({"schemaVersion":1,"executorSlot":…})的真实 payload,run inspector 与编辑器卡片共用的同一函数都受益。正确。- rebase 到 main
5953506干净,frontend CI 绿(head263e183058)。
无阻塞。
An open run detail page showed whatever one read returned and never moved again: the history re-read only when its popover opened, and the detail page not at all. Nothing publishes a workflow run — `SpaceEventType` carries no run event, and naming one here would fix a backend contract this change does not own — so the read follows the run instead: `pending`, `running` and `awaiting_input` re-read every five seconds, a terminal status stops, and a status this build does not recognise keeps polling rather than freezing a run the server is still advancing. A poll can be answered late, or by a replica that has not caught up, so a read no longer overwrites the cache: it merges into the copy already held. A settled run is never reopened by an unsettled read, the newer `updatedAt` wins between reads of the same kind, and node states merge node by node so a read that raced a node's write cannot blank what the page already shows. The history and the detail page merge by the same rule, so one run cannot read two ways. `structuralSharing` is where that merge belongs: it is the only point that sees the cache's current value as a read is written, so a concurrent read cannot interleave between the comparison and the write. A custom function replaces TanStack's default rather than adding to it, so the merge is followed by `replaceEqualDeep` to keep the identity of an unchanged run — without it every poll would hand the page a new object and re-lay out the frozen graph for a run that has not moved. Co-Authored-By: Claude Code <noreply@anthropic.com>
The page is what the issue is about, so its test mounts a run that is still running, advances the poll's interval, and expects the node card, the run status and the node's output to arrive without a reload — with a node the run never reached still reading as unrun, because the statuses are overlaid on the frozen snapshot rather than on a re-read definition. Only the query's interval is faked, so the waits flush real timers. Co-Authored-By: Claude Code <noreply@anthropic.com>
A read is the only channel the page has to a run, so a failed read says nothing about whether the run settled. Before this change the poll interval stopped at the first error: `runPollInterval` returns `false` for an undefined status, and a failed read leaves no status, so an error on the first read froze the page forever. The poll now also fires while the last read failed. A run the page already holds is unaffected: a settled run stays settled, and an unsettled one keeps polling, whatever the last attempt returned.
The merge compared `updatedAt` strings, which leans on the server keeping one timestamp format forever. Compare the parsed epoch instead, so the comparison holds however the string is written. A read whose timestamp does not parse is treated as older than any that does, keeping a known-good read over an unorderable one.
The backend writes startedAt/finishedAt as "" for a node that never ran (and, before cloud#105, for the run row too). The viewer checked only for null, so the blank rendered as an empty time row: an inactive node showed "开始 —". Treat "" as absent at the boundary — drop it when parsing node states and hide the run header rows while the value is blank.
The executor-slot schema (ora-space#154) stores executorSlot where this build still summarizes the legacy executor binding. Reading executor.agentCli from such a node crashes the card summary; leave the config section empty instead.
cloud#109 added `cancelRequestedAt: string | null` to the generated WorkflowRun type; the fixture this PR adds must supply it or main's typecheck fails after merge.
zhangshilong123
force-pushed
the
feat/203-run-live-status
branch
from
October 20, 2026 12:43
263e183 to
a492316
Compare
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
运行详情页与历史列表此前只读一次 REST 快照:历史列表只在 popover 打开时
refetch(),详情页完全不刷新——一个运行从pending走到终态,打开着的页面永远停在第一次读到的状态,节点输出也不会出现。这个 PR 让读取跟着运行走,按 ora-space/circus#203 正文允许的有界轮询 + 单调合并:
pending/running/awaiting_input每 5 秒重读一次;进入终态即停;本版本不认识的状态(后端新增枚举)继续轮询,避免把服务端还在推进的运行静默冻住。标签页不可见、组件卸载时由 TanStack 既有默认行为停轮询。updatedAt新的胜出;nodeStates按节点合并,只补不删,避免与节点写入重叠的读把页面已经显示的输出抹掉。历史列表与详情页用同一条规则,同一个 Run 在两处不会互相矛盾。structuralSharing:这是唯一能看到缓存当前值、且与写入同一时刻的位置,放在读取返回之后再合并会和并发读交错。自定义函数会替换 TanStack 的默认结构共享,所以合并后再走一次replaceEqualDeep,让内容未变的读保持对象身份——否则每 5 秒都会给页面一个新对象,冻结的图画布会跟着重排。为什么不用事件:
SpaceEventType里没有工作流运行的事件类型,事件名要有后端发布者才有意义,而发布者属于 #70 / #75 的实现,在这里先立一套就是另立契约(#203 正文与 @Zpc001 的分工都禁止)。SSE 重连时的全租户失效、refetchOnReconnect/refetchOnWindowFocus的既有底座继续生效,重连后运行查询会被顺带刷新。改动文件(都是交付物):
frontend/src/features/workflows/api.tsrunPollInterval/runsPollInterval、mergeRunRead/mergeRunPage及其structuralSharing适配,接到useWorkflowRun/useWorkflowRunsfrontend/src/features/workflows/api.test.tsxupdatedAt定序、节点只增)、陈旧读被丢弃、未变读保持身份、历史行不回退frontend/src/features/workflows/run/run-detail-page.test.tsxfrontend/src/features/workflows/run/run-detail-page.tsxstartedAt/finishedAt同时排除空串(#105 合并前仍返回"")frontend/src/features/workflows/run/run-node-state.ts+.test.tsnodeStates时空时间戳按未发生,不存进RunNodeEntryfrontend/src/features/workflows/run/run-inspector.test.tsxfrontend/src/features/workflows/editor/node-parameter-summary.tsx+.test.tsxexecutor缺失)不崩溃,配置区留空frontend/src/test/polling.ts+polling.test.ts+ 两份 READMEsettleUntil:setInterval被伪造时 Testing Library 的waitFor自己也在轮询、无法工作,轮询测试用它重试断言提交按单一目的拆分:轮询与合并(含其测试)、页面级测试、读序用解析后的时间戳、读失败后继续轮询、空时间戳按未发生、executor-slot 兜底、
cancelRequestedAtfixture 对齐。本 PR 不做的
--network host)。真实栈的 Start→Agent→Output 成功与节点状态回写已在 feat(workflows): start a run from the selected version's own form #70(现已关闭)和 @Kyber5323 的 [v2] 演示视频工具链:真实录屏、自动运镜、4K 合成与片头片尾 #340 里落地并录到页内证据;[v2] 演示视频工具链:真实录屏、自动运镜、4K 合成与片头片尾 #340 里卡住「历史/详情一致」「跨运行」两项的 agent 节点 inspector 崩溃,正是本 PR 的 executor-slot 兜底修掉的——修完这两项证据可以重录。开发 simulator 只用于 UI 负例。WorkflowRun.status/nodeStates/definitionSnapshot/ 时间戳在 main 上已存在,未改 OpenAPI、未改生成的客户端。ADR 状态:实现
ora-space/specs的decisions/cloud/workflow/0-cloud-side-scheduler-owns-workflow-execution.md(status:approved)中 D7 的读取一半——「节点产出在首期落在运行的节点状态里,供运行详情展示」。本 PR 不带 specs 改动:执行侧(真实执行与结果回报)是 ora-space/circus#70(已关闭),ADR 由承接它的实现改动统一改为implemented;本 PR 只读展示,不翻转 ADR 状态。Evidence
npm run check通过(在 rebase 到 upstream/main5953506之后):format:check、lint、typecheck、test(140 文件 / 1165 用例,覆盖率 lines 91.47% / branches 82.6% / functions 89.33% / statements 90.4%,阈值 80%)、check:modules、check:docs、check:dead、check:dup、build。npm run check:modules -- --base origin/main:Module hygiene OK(4 modules)。structuralSharing接线 → 陈旧读与历史行回退两个用例失败;去掉合并后的replaceEqualDeep→ 未变读保持身份的用例失败。三条规则各自有用例兜住,不是空转。f405823→27e7f48,含 feat(core): declare and resolve workflow globals and sys.* variables #104 workflow 全局变量、fix(core): keep unset workflow run timestamps null #105 空时间戳为 null、feat(core): cancel workflow runs by request and settle them at the scheduler #109 workflow 取消给WorkflowRun加的必填cancelRequestedAt字段),并给本 PR 新增的 fixture 补上cancelRequestedAt: null;rebase 后重跑上面全部门禁仍全绿。Merge Danger
Door: two-way
只改前端读取与展示,没有 schema、迁移、OpenAPI 或生成客户端的改动;回滚即恢复"读一次快照"的行为。
Blast Radius: frontend(
features/workflows的运行详情与历史列表、features/workflows/editor的节点参数摘要、src/test脚手架)运行处于非终态时页面每 5 秒发一次读请求;后端若不返回终态,轮询会持续到页面关闭。历史列表在有未终态运行时同样每 5 秒重读一次。
Refs ora-space/circus#203