Repository navigation
Resume silently replays the source stored at first run, so edits are ignored — unless nothing was stored, when it reads the current file #497
Description
Summary
resume_workflow does not run the workflow in the file you are running. It recompiles
the whole program source captured at the original run, stored in the run's metadata.
So a source edit made between the run and the resume has no effect, and there is no
warning that the code being executed is not the code on disk.
Add a step and resume: the step never runs, and the resume reports success. Delete a step
and resume: it runs anyway. Resume from a file that defines no workflow at all: the
original steps run.
And when the run stored no source — which is every run from an embedded NodusRuntime
(#496) — the same function falls back to reading the current file from
workflow_source_path, which does pick up edits. Two different rebuild sources,
selected by how the run was started, with opposite behaviour and nothing distinguishing
them to the author.
Reproduction
Verified against src/ at 5.0.4.
The original run (pE_run.nd):
workflow w {
state log = ""
step a { print(" a ran"); log = log + "a"; checkpoint "cp"; return 1i }
step b after a { print(" b ran"); log = log + "b"; return 2i }
}
fn main() {
let r = run_workflow(w)
let g = r["graph_id"]
let s = r["state"]
print("gid=\(g) state=\(s)")
}
a ran
b ran
gid=g_e3d501ff state={"log": "ab"}
Case 1 — a step added. A new file with the same workflow plus step c after a:
step c after a { print(" *** c ran -- c did not exist in the original run ***"); log = log + "c"; return 3i }
a ran
b ran
resumed state={"log": "aab"}
resumed steps={"a": 1, "b": 2}
c never ran. No error, no warning; the resume reports success and steps simply does
not mention it. (The "aab" is #486 — pre-checkpoint work re-executing — and is not this
issue.)
Case 2 — a step removed. A file whose workflow w contains only step a:
a ran
b ran
resumed state={"log": "aaab"}
resumed steps={"a": 1, "b": 2}
b ran, from code that is not in the file.
Case 3 — no workflow at all. The entire program:
fn main() {
let r = resume_workflow("g_e3d501ff", "cp")
print("result: \(r)")
}
a ran
b ran
result: {"tasks": {"task_1": 1, "task_2": 2}, "steps": {"a": 1, "b": 2}, ... "workflow": "w"}
Where the code comes from
src/nodus/orchestration/workflow_lowering.py:391 stores it on every run:
"workflow_source_code": getattr(vm, "source_code", None),vm.source_code is the whole module source, not the workflow — the stored blob for
the run above contains fn main() and everything else in the file.
src/nodus/vm/vm.py:1523-1533 is the fork:
source_code = metadata.get("workflow_source_code")
source_path = metadata.get("workflow_source_path")
if not isinstance(source_code, str):
if not isinstance(source_path, str) or not source_path or not os.path.exists(source_path):
raise WorkflowRebuildError(...)
with open(source_path, "r", encoding="utf-8") as f:
source_code = f.read()- Stored source present → pinned: edits ignored, silently.
- Stored source absent, path present → current file: edits picked up, silently.
- Neither →
WorkflowRebuildError. That is Workflow runs started from an embedded NodusRuntime cannot be resumed after the process exits #496.
Which branch a run takes is fixed at run time by its entry point, and vm.source_code is
set only by tooling/runner.py and dap/server.py.
Why this is worth fixing rather than documenting
Pinning is defensible on its own — arguably it is the right default, since replaying
against the source the run was planned for is what makes deterministic re-execution
meaningful, and it is what #494 would want. Two things make it a defect as it stands:
- It is silent in the direction that matters. The normal debugging loop is: a
workflow fails, you edit a step, you resume. Nodus runs the old step and reports
success. There is no signal at all — not a warning, not a field in the result map. - The behaviour is not a property of resume, it is a property of the entry point.
The same call means "replay the pinned program" from the CLI and "recompile whatever is
on disk now" from anything that did not setsource_code. Neither is documented, and
nothing in the API surface distinguishes them.
This is very likely the mechanism behind #470: that report edits the source and gets a
spurious Dependency cycle detected, which is what recompiling an edited file against a
task set planned for the old shape would produce — i.e. the source_path branch. Worth
confirming when that one is worked, because if so the two issues share a fix.
Fix direction
Pick one meaning and make the other impossible:
- Pin, and say so. Keep the stored source authoritative, and record a hash of it. On
resume, ifsource_pathexists and its current content hashes differently, report it —
in the result map at minimum, and probably as a refusal unless the caller opts in with
something likeresume_workflow(id, label, {allow_source_drift: true}). The author then
learns immediately that their edit is not in the run. - Remove the fallback. The
source_pathbranch is what makes the behaviour
entry-point dependent. If pinning is the intent, a run with no stored source should
fail rather than silently recompile a file that may have moved on — and Workflow runs started from an embedded NodusRuntime cannot be resumed after the process exits #496 should be
fixed by storing the source on the embedded path, not by leaning on the path branch.
Either way the decision belongs in one place. As written, it lives in the two-branch
if above, which is the shape CLAUDE.md § The recurring bug shape warns about.
Related
- Workflow runs started from an embedded NodusRuntime cannot be resumed after the process exits #496 — an embedded run stores neither, so it cannot resume at all. Same three lines.
- Resume does not validate graph topology: a source edit yields a spurious 'Dependency cycle detected' #470 — resume does not validate topology. Probably the same fork; see above.
- Resume re-executes with no determinism boundary: a replayed step observes a different clock, and nothing says so #494 — resume re-executes with no determinism boundary. Source pinning is one half
of a determinism story that has no other half. - A mid-step checkpoint is a whole-step re-entry label, so work before it re-executes on resume #486 — a mid-step checkpoint re-enters the whole step. Visible in the outputs above
as the repeateda.
Affected versions
v5.0.4 (current when filed). Not a regression.
Provenance
C:\codev\Langgraph research\langgraph, libs/checkpoint/langgraph/checkpoint/base/__init__.py
— read from the source tree rather than via the study document.
LangGraph's Checkpoint never refers to program text. It carries channel_values,
channel_versions, versions_seen ("map from node ID to map from channel name to
version seen — used to determine which nodes to execute next") and updated_channels.
The graph is supplied by the caller at resume; what the checkpoint pins is how far each
node got, not what the nodes were.
That is the opposite trade from Nodus's, and neither is obviously right — LangGraph's
means a resumed graph can silently differ from the checkpointed one, which is exactly
#470. What it does have is a mechanism for noticing: BaseChannel.__eq__ compares
policy rather than value (LastValue.__eq__ is true for any LastValue;
NamedBarrierValue compares its names set; BinaryOperatorAggregate compares
operators), so a rebuilt graph's channel spec can be checked against the checkpointed one.
Nodus stores neither a topology nor a spec — it stores the program and re-derives. Asking
what happens when the program changes underneath that is what produced this.
Activity
Masterplanner25 commentedon Aug 19, 2026
Correction to my own field measurement above. I reported
path-only(exists)=0and inferred from it that thesource_pathfallback branch "is not exercised by anything here". That is true of this repo's sample but false in general — and the case that exercises it is a common one.An embedded
NodusRuntime.run_file(...)run recordsworkflow_source_pathand notworkflow_source_code(verified againstsrc/at 5.0.4):So every embedded
run_filerun takes the fallback branch: on resume it re-reads the current file from disk and therefore does pick up source edits — the opposite of the CLI's pinned behaviour, from the sameresume_workflowcall.That makes the split in this issue sharper than I described it. It is not "pinned, with a fallback nothing reaches"; it is three entry points with three behaviours:
source_codesource_pathnodus run/nodus dapNodusRuntime.run_fileNodusRuntime.run_sourceSame call, three semantics, none documented, chosen by how the run happened to start.
Note also the interaction with #469's fix: setting
vm.source_codeinrun_sourcecloses that issue, but if it is also set forrun_fileit silently moves those users from "edits picked up" to "edits ignored". That is a behaviour change for existing embedders and should be a deliberate choice made here, not a side effect of fixing #469.#470/#497: a rebuilt graph must be the planned shape, and both source…
Masterplanner25 commentedon Aug 25, 2026
Closed by PR #563 (merged as
e3c33ae), which completes what #469 and the drift warning started. The fork this issue reported no longer exists for any run started since #469, and both surviving paths now announce themselves.Where each part of this issue landed:
source_drift: trueon the resume result map, present only when the file has changed, alongside the existing stderr warning andworkflow_source_driftevent._warn_on_source_drift's docstring at the time) is warn-not-refuse: a resume that stops working because someone touched the file would be worse than one that explains itself. What does refuse is a changed step structure, which is Resume does not validate graph topology: a source edit yields a spurious 'Dependency cycle detected' #470's fix in the same PR — the case where proceeding would manufacture false diagnoses.resume: run '<id>' predates source recording, so 'w' is rebuilt from <path> as it is now; edits made since the run started are in this resume.) and emitsworkflow_rebuild_unpinned. It decays away as pre-Embedded run_source workflows can never be resumed: NodusRuntime records no source, so the rebuild has nothing to rebuild from #469 runs age out.The decision lives in one place (
_rebuild_workflow_graph's single fork, each branch labelled), which is what the recurring-bug-shape section asks for.Guide §8 documents the pinning rule, the drift signals, and the structure refusal. Regression tests:
tests/test_resume_source_pinning.py(# closes: #497) andtests/test_resume_topology_validation.py(the unpinned-branch warning tests).release: 5.4.0 -- a resume that tells the truth, an inspection that c…
Masterplanner25 commentedon Aug 25, 2026
Shipped in v5.4.0 — live on PyPI: https://pypi.org/project/nodus-lang/5.4.0/ · release notes
Validated pre-publish (Gate 10: 6/6 dependent suites, 35/35 release-claim probes, 9/9 adversarial boundaries against the wheel in a clean venv) and post-publish (Stage 5, including a run started on 5.3.0 and resumed on 5.4.0). Eval documents:
docs/evals/v5.4.0/.