Skip to content

fix(workflow): reset the whole recovery body and count onFail max as recoveries (#1541, #1542) - #1601

Merged
Birfy merged 1 commit into
mainfrom
replay/pr-1545
Oct 28, 2026
Merged

Birfy merged 1 commit into
mainfrom
replay/pr-1545

Conversation

@Birfy

@Birfy Birfy commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Chief
Fixes #1541
Fixes #1542

Owner: Dev4
Reviewer: Reviewer
(Owner: is Dev4, the writers name; the original says Dev-4, which enqueue-guard refuses as a name the policy does not know. Same Bot, same claim.)

Replay of #1545: the original head is on a fork we cannot push to (maintainer_can_modify=false), so this PR carries the same change replayed onto current main; the original is closed as superseded.

Replayed onto main 571be6d4957f7c6427b39e5d16dcd63725aeead0 from the original head 65860b4e3644125f43dfa646330978447842e8bb (base 620235991e46c24836d03b16b76d642c9f7a4aa1, 32 commits behind). The original tip already carried a PASS; that conclusion names the old tip, so this replay needs its own review at its own tip.

What changed

Two defects that #1529 shipped in Workflow failure recovery, not a new feature.

  • fix(workflow): failure recovery resets only the two endpoints, not the nodes on the way back #1541 — a recovery reset only the two ends of the edge. failNode() called loopBody(graph, recovery.edge.to, nodeId), but loopBody(graph, source, target) wants the failing node first and the recovery target second — completeNode calls it the other way round. With the arguments reversed the forward/backward intersection is usually empty and the body falls back to its last two lines, so a middle node on the way back stayed done and the retry was fed the output it had just rejected. It is now loopBody(graph, nodeId, recovery.edge.to): every node from the recovery target through the failing node resets and re-runs in order.
  • fix(workflow): an onFail max of 1 never recovers, and recoveries come one short of max #1542 — max: 1 recovered zero times. failRecoveryDecision() compared ran >= max with ran = attempts + 1, so the first failure already read as the limit. max now counts recoveries (ran > max), so max: 1 is one real recovery and max: 8 is still at most eight. The success-path loop edge's max is untouched.

How the replay merged

Three of the nine original files were also changed by main; nothing main landed is reverted.

  • plugins/workflow/src/graph.js — main (fix(workflow): Detect needs-human and already-claimed to end runs early #1539 and later) added the checkEarlyEnd guard at the top of failNode() and left the two recovery lines exactly as the original base had them, so main's version is kept whole and only these two lines change: the loopBody argument order and the ran > max comparator, with the comment that documents what max means.
  • plugins/workflow/README.md — main rewrote the matcher line, the connectors paragraph and the delivery-template line. Those stay; the onFail paragraph and the "Settings → Workflow graphs can add a link, a condition, or a loop" line take the original's corrected wording, and define_workflow's edges list gains onFail? on top of main's connector wording.
  • docs/subsystems/workflow.md — main rewrote invariants 2, 3, 4, 6, 8, 12 and the service.js / ephemeral.js / client.js key-file rows. Those stay; the graph.js row and invariant 7 take the original's corrected recovery wording.
  • plugins/workflow/test/failure-recovery.test.js is byte-identical to the reviewed tip (main did not touch it), and the two new test files and both changelog fragments are byte-identical as well.
  • Not carried: the original's edit to changelog.d/1526.added.md. That fragment was consumed by a release on main and no longer exists; the entry is already in CHANGELOG.md as released history, and a feature PR never edits CHANGELOG.md. The corrected wording lives in the README and the subsystem doc instead.

Verification

Ran in a checkout of main at 571be6d4 with the replayed files written over it:

  • cd plugins/workflow && TZ=UTC node --import ./test/resolve.mjs --test test/failure-recovery.test.js test/failure-recovery-attempts.test.js test/failure-recovery-body.test.js — 10 tests, 10 pass, 0 fail (the three files this change owns).
  • The whole plugin suite, node --import ./test/resolve.mjs --test "test/*.test.js" — 197 pass, 1 fail: test/host.test.js, which stops at import with Cannot find package '@deepseek-ai/dsh-llm' because the DSH harness is not installed in this sandbox. It imports the host through ds-bot and never loads graph.js; the same file fails the same way on untouched main, so it is the environment, not this change.
  • node .github/scripts/check-mojibake.mjs --check — ok, 0 occurrences of U+FFFD.
  • Not run: the rest of npm run verify (core/ needs npm ci + npm run harness and this change touches no core/ or generated file), GitHub CI, the merge queue, the smoke test, the Desktop UI.

Checkpoint

{
  "pr": 1601,
  "claim": "Dev4",
  "issue": 1541,
  "summary": "Failure recovery, introduced by #1529, reset only the recovery target and the failing node, so a middle node on the way back stayed done and the retry was fed the output it had just rejected; recovery now resets every node from the target through the failing node. The same edge never recovered at max: 1 because the first failure already read as the limit, so max now counts recoveries and 1 is a real recovery. Two new test files pin the two defects separately, and the README, subsystem doc and changelog carry the corrected wording.",
  "files": [
    "plugins/workflow/src/graph.js",
    "plugins/workflow/test/failure-recovery-body.test.js",
    "plugins/workflow/test/failure-recovery-attempts.test.js",
    "plugins/workflow/test/failure-recovery.test.js",
    "plugins/workflow/README.md",
    "docs/subsystems/workflow.md",
    "changelog.d/1541.fixed.md",
    "changelog.d/1542.fixed.md"
  ],
  "checks": {
    "ran": "TZ=UTC node --import ./test/resolve.mjs --test test/failure-recovery*.test.js (plugins/workflow) plus the full plugin suite",
    "result": "green",
    "at": "0b751de611c2879a9b3d19fc06b93dcef1fd9811"
  },
  "next": "Reviewer: review this tip (the PASS on #1545 names its own tip, so it does not carry); then enqueue through the merge queue."
}

…recoveries (#1541, #1542)

Replay of #1545 onto main 571be6d: the original head (65860b4, HugoXK/FlyBot) is on a fork we cannot push to, so the same change is replayed here.

Cherry-picked from the reviewed tip: the failNode loopBody argument order (#1541) and the failRecoveryDecision comparator (#1542), plus both new test files, the corrected README / subsystem-doc wording and the two changelog fragments.

Fixes #1541
Fixes #1542
@Birfy

Birfy commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 0b751de
Written-by: Reviewer
Attestation: 0b751de role=Reviewer

Ran at this tip:

$ cd plugins/workflow && TZ=UTC DSH_HARNESS=/data/dsh/bot/workspace/rw1517/core/dev/harness node --import ./test/resolve.mjs --test test/failure-recovery-body.test.js test/failure-recovery-attempts.test.js test/failure-recovery.test.js
# tests 10  # pass 10  # fail 0
$ cp /tmp/graph.base.js src/graph.js && TZ=UTC DSH_HARNESS=/data/dsh/bot/workspace/rw1517/core/dev/harness node --import ./test/resolve.mjs --test test/failure-recovery-body.test.js test/failure-recovery-attempts.test.js test/failure-recovery.test.js
# tests 10  # pass 5  # fail 5

Red first, same files: replacing only plugins/workflow/src/graph.js with its blob at the replay base 571be6d4 reds failure-recovery-attempts (2 not ok), failure-recovery-body (2 not ok) and the moved failure-recovery pin (1 not ok) — 5 of 10 — and the tip is 10/10 green; the working tree is restored.

Replay fidelity: the two new test files and the moved pin are byte-identical to the original head 65860b4e (same blob shas); plugins/workflow/src/graph.js carries exactly the original two semantic lines on top of main's checkEarlyEnd guard — loopBody(graph, nodeId, recovery.edge.to) and ran > max. The dropped ninth file is the original's changelog.d/1526.added.md edit; that fragment no longer exists on main (consumed by a release), so dropping it is correct and CHANGELOG.md is untouched.

Scope: 8 files, all in the claimed set (plugins/workflow/{src,test,README}, docs/subsystems/workflow.md, changelog.d/1541, changelog.d/1542); no core/ movement, so no patch/client regeneration. Checkpoint block present and valid (1408 chars, summary 536, 8 files, claim: Dev4 = the writers name, checks.at = this tip).

CI at this tip: every check success — including tests-ok (Node 24), plugins (workflow) and merge-gate; the only red is enqueue-guard at creation, refused for no Verifier report names the tip — that is this comment's absence, not a code failure.

Not run: full npm run verify (the tip's tests-ok run covers the other suites); no smoke test (no core/ or host change).

@github-actions github-actions Bot added the pass label Oct 28, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit d2374e1 Oct 28, 2026
15 checks passed
@Birfy
Birfy deleted the replay/pr-1545 branch October 28, 2026 16:04
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant