Repository navigation
workflow: rejecting a draft and replaying it with feedback works again (#870) - #877
Merged
Merged
Conversation
#870) resume_workflow(id, "before_draft", {"feedback": "..."}) on a waiting run replays from that checkpoint with the payload visible to workflow_resume_payload(), and parks at the wait again. Post-wait steps do NOT run, so a side-effecting publish cannot fire on a rejection. #482 (5.5.0) refused it, on the stated grounds that "the rollback re-armed the wait and the payload was silently discarded". The payload was not discarded. Measured on 4.0.8, where the spelling worked, it reached the replayed step; what it did not do was SATISFY THE WAIT, which is a different thing and is exactly what a reviewer rejecting a draft wants. The capability was never actually removed, only its spelling. The refusal's condition includes `event_type is None`, so passing that VERIFICATION argument to the Python runner has skipped the refusal the whole time -- an accident, not a design, and relying on it was the state of the art. Confirmed on main before changing anything: the event_type path replays with the payload visible, publish does not fire, the run re-parks. BothPathsAgree pins the two spellings to one answer so the supported one cannot drift back onto the accident. The two-phase workaround people found instead is worse and the guide now says so: two calls, and phase one runs every post-wait step, so each must gate its own side effects on the payload. resume_workflow(id, "checkpoint") with no payload is STILL refused, and that is what #482 earned -- a rollback re-enters the waiting step, which waits again, so nothing changes behind a healthy-looking result. Its message names both recipes now rather than only the advancing one. The neighbouring mistakes were already covered elsewhere and are unchanged: a checkpoint the run has not reached and a typo'd one both fail with "Checkpoint not found". test_checkpoint_with_payload_is_refused_naming_the_discard asserted the false premise, so it is rewritten rather than kept -- the same shape as #102's scan test in #869, which pinned that defect too. The 5.5.0 changelog entry is ANNOTATED, not rewritten: it is what that release shipped, and silently editing it would leave the repo disagreeing with the published notes. The note says which sentence is wrong and why it mattered -- it is the sentence a future reader would use to decide whether the refusal can be relaxed. nodus_gate --all green, all ten phases, before the push this time (#869's lesson). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI's `probes` job failed on `5.4.0: a checkpoint resume of a waiting run is refused`. It is a source assertion, and one of its three assertions required the literal string "discards the payload" to be PRESENT in `resume_workflow` -- the sentence #870 removed because it is false. So the premise was pinned in three places at once: the changelog, the error message, and a release probe asserting the error message. The probe now asserts what the refusal actually guarantees: still refuses, still consults the persisted state, is scoped by `and resume_payload is None`, and no longer claims the payload is discarded. Added a BEHAVIOURAL probe beside it. A source assertion cannot tell you the capability works, which is exactly how the false premise survived -- it was asserted as a string rather than run. The new one parks a run, rejects with feedback, checks the replayed step saw it and the run re-parked, then approves. Both were falsified against the restored 5.5.0 refusal before being kept: the source probe fails on the scoping assertion, the behavioural one on the reject itself. Two process notes, because both nearly cost a second red CI: - `nodus_gate --all` does NOT run the probes. I ran the gate before pushing (#869's lesson) and still missed this, because the probes are a separate harness that CI runs as its own job. Run both. - The probe count did not move when I added the probe -- 129/129. `main()` invokes probes from an explicit list, so a decorated function nobody calls is dead code that looks live. That is #794's shape (declared, printed, read by nothing). Adding the call took it to 130/130, and the moving count is the only thing that showed it. Also: `@probe` swallows AssertionError into RESULTS, so calling a probe and seeing no exception proves nothing. Read RESULTS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Closes #870.
The finding
#482 (5.5.0) refused
resume_workflow(id, checkpoint, payload)on a waiting run,on the stated grounds that "the rollback re-armed the wait and the payload was
silently discarded".
The payload was not discarded. Measured on 4.0.8 in a clean venv, where the
spelling worked, it reached
workflow_resume_payload()in the replayed step:What it does not do is satisfy the wait. That is a different thing, and it is
exactly what a reviewer rejecting a draft wants: replay from before the draft
with the feedback, then park for approval again. So the refusal removed a working
pattern on a false premise — and the premise was in the error message and the
CHANGELOG, so a reader hit it twice.
The capability was never removed, only its spelling
The refusal's condition includes
event_type is None.event_typeis averification argument — "refuse unless the run is waiting on this event" —
and it suppresses the refusal only incidentally. Confirmed on
mainbeforechanging anything:
So the supported path was refused while the capability sat behind an accident.
BothPathsAgreenow pins the two spellings to one answer so the supported onecannot drift back onto it.
What changed, and what deliberately did not
checkpoint + payloadis allowed. One condition:and resume_payload is Noneadded to the refusal.
resume_workflow(id, "checkpoint")with no payload is still refused, andthat is the part of #482 that earned its place — a rollback re-enters the waiting
step, which waits again, so nothing changes behind a healthy-looking result. Its
message now names both recipes instead of only the advancing one.
The neighbouring mistakes needed nothing: a checkpoint the run has not reached,
and a typo'd one, already fail with
Checkpoint not found. Verified all fiveshapes rather than assuming:
publishdoes not fireCheckpoint not foundCheckpoint not foundpublishrunsWhy this beats the workaround
The two-phase dance the reporting project found — satisfy the wait, then roll
back — takes two calls and runs every post-wait step during phase one, so a
side-effecting
publishfires on a rejection unless each step gates on thepayload. The guide documents the workaround for 5.5.0–5.14.0 and says plainly
why it is a workaround.
The 5.5.0 record is annotated, not rewritten
Editing a released entry would leave the repo disagreeing with the published
notes. The entry stays as shipped with a note naming the wrong sentence and what
it cost — it is the sentence a future reader would use to decide whether the
refusal can be relaxed, which is the decision this PR just made.
Tests
tests/test_reject_and_revise.py(11) plus the rewritten #482 test. Threeper-decision neuters, restoring from saved byte copies:
test_checkpoint_with_payload_is_refused_naming_the_discardasserted the falsepremise, so it is rewritten rather than kept — the same shape as #102's scan test
in #869, which pinned that defect too. Two of #482's three tests survive
unchanged, which is the check that this narrowed the refusal rather than
gutting it.
Checks
test_reject_and_revise,test_waiting_run_checkpoint_resume,test_nodus_workflow_framework,test_parked_run_survives_age_bound,test_resume_rebuild,test_runner_results: 72 passedruffclean ·mypy src/nodus/clean — though it does not coversrc/nodus_lang_workflow/, where this change lives, so CI is the only typecheck for it
nodus_gate --allgreen, all ten phases, before the push — BUG-869: the local store's age filter hides waiting runs from listing, migration, rehydration and the #174 warning #869's lesson,where I ran two phases, reasoned the rest were irrelevant, and CI failed on
--flips🤖 Generated with Claude Code