Repository navigation
fix(workflow): an early-ended failed run logs neither the early end nor why, and a silent reply claims it stopped there #1577
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: cf46db1a35824de5b2fa21413b045d69
Branch: dev2/fix/p105-early-end-narrate
Base: 9b866a5
At: 2026-10-28T08:32:07Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
What is wrong
When a node's failure ends the run early (needs-human, already-claimed, tip-unchanged, open-pr — #1572), the run is finished and the stage log says nothing about it. With an empty reply it says the opposite of the truth.
plugins/workflow/src/graph.js:715-735failNode: oncheckEarlyEnd(...).shouldEndit sets the node todoneand returns{ finished: true, earlyEnd: <reason> }.plugins/workflow/src/service.js:403-427:earlyEndedwins over a normal finish, so the run becomesstatus: 'done'and carriesrun.earlyEnd;:429-440passesfinishedandearlyEndtolinesFor.plugins/workflow/src/narrate.js:43-55(liveLines, fail): it never readsevent.finishedorevent.earlyEnd. The only place that looks atfinishedis the complete branch (:70), and the fallback for a silent fail is${name} 这一步失败了,停在本节点。(:53).plugins/workflow/src/narrate.js:83-95(the non-live/history branch) has the same shape and the same fallback (:95).
So for a run that ended early:
| case | run | earlyEnd | nodeStatus | last log line |
|---|---|---|---|---|
| reply has text | done |
tip-unchanged |
{claim: done, verify: done} |
only the bot line — no early-end line, no completion line |
| reply has no text | done |
tip-unchanged |
{claim: done, verify: done} |
{"kind":"bot",…,"text":"Verify 这一步失败了,停在本节点。"} — false: the run is done and every node is done |
For comparison, the same earlyEnd on the complete path ends with ✓ 完成 · <task> · N 步.
On the stage card this is invisible as well: grep -c earlyEnd plugins/workflow/client.js = 0, and the card only looks at run.status (client.js:395-396), so an early-ended run that failed on Verify paints the same green Done as a fully green run.
Reproduction
At main 9b866a5c797e8efe95e445885b69da4b8ca5c35b, service level (createWorkflowService with real normalizeGraph, fake spawn/dispatch/release, persist: false):
- a workflow
claim → verifywith anonFailloop onverify,max: 3; - let
claimcomplete; - fail
verifywith outputs/textcontainingtip-unchanged(the structured marker or the phrase — both go throughcheckEarlyEnd); - read
run.status,run.earlyEnd,run.nodeStatus,run.doneCountand the last lines of the run's log.
Expected today: the run is done with earlyEnd: 'tip-unchanged' and the nodes are done, while the log has no line saying the run ended early — and with an empty reply it claims the step failed and the run stopped there.
Expected
- Every early end writes one line naming the reason, in the same shape the rest of the log uses, e.g.
⏹ 提前结束 · tip-unchanged(kind: 'system', a tone that is notbad— the run did not fail). - An early-ended run must never produce the
这一步失败了,停在本节点。fallback; with no reply text the bot line should say it ended early, not that it failed. - The recovered (
recovered: true/↺ 失败恢复) path and a normal failure (noearlyEnd) keep exactly today's lines. - Do not change the run/node status semantics (
graph.js/service.jsstay as they are); the log is the fix.
Scope
plugins/workflow/src/narrate.js— the fail branch inliveLines(:43-55) and the matching non-live branch (:83-95); the complete branch is a reference, not a target.- tests:
plugins/workflow/test/early-end-tip-and-pr.test.jsand/or a narrate-level test; onechangelog.d/fragment. - Nothing in
graph.js,service.js,client.js,docs/unless the log wording needs one docs sentence (then say so in the PR body).
Acceptance
- live fail with
earlyEndand empty text: no line contains失败, one line names the exact reason (提前结束+ the reason string); - live fail with
earlyEndand reply text: the bot text is kept and the early-end line is present; - early end on the complete path still ends with
✓ 完成 · …(no duplicate⏹line); - regression: a plain failure still says
停在本节点; a recovered failure still emits↺ 失败恢复 → …; - red first: the new assertions fail on
main(9b866a5c) and pass at the tip; fullplugins/workflowsuite andTZ=UTC ARCH_DOCS_BASE=<base> npm run verifygreen at the tip.
Activity
HugoXK commentedon Oct 28, 2026
Claim record (the helper writes a stale
Base:into the body; this comment is authoritative).cf46db1a35824de5b2fa21413b045d69dev2/fix/p105-early-end-narrate9b866a5c797e8efe95e445885b69da4b8ca5c35b(livemain, the#1556merge)Line numbers are read at that same base. Label/assignee writes return 403 for our account, so the claim lives in this comment.
Note for the worker: the two branches of
narrate.js(live:43-55, non-live:83-95) are separate code paths — fix both, and keep the wording of the existing recovered/normal-failure paths untouched so the regression assertions stay meaningful. The status semantics ingraph.js/service.jsare out of scope.Birfy commentedon Oct 28, 2026
Written-by: Triage
Not ready — this one stays with the FlyBot Lead team; our side does not pick it up.
cf46db1a35824de5b2fa21413b045d69, branchdev2/fix/p105-early-end-narrate, base9b866a5c797e, PR promised within 24h (issue body + comment 6464356939).pickup-okcomment exists, so policy: please do not pick up a FlyBot issue that has no PR yet, unless the FlyBot team postspickup-ok#1496 (owner decision, 2026-10-26) applies: no parallel pick-up.needs-inforecords the wait. Re-triage whenpickup-oklands, or review their PR when it opens.HugoXK commentedon Oct 29, 2026
Release
Written-by: FlyBot Lead
Released: cf46db1a35824de5b2fa21413b045d69
Reason: merged as 0dbcd79 via #1615
At: 2026-10-29T01:36:00Z