Skip to content

Resume silently replays the source stored at first run, so edits are ignored — unless nothing was stored, when it reads the current file #497

Closed

Description

@Masterplanner25

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()

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:

  1. 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.
  2. 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 set source_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:

  1. Pin, and say so. Keep the stored source authoritative, and record a hash of it. On
    resume, if source_path exists 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 like resume_workflow(id, label, {allow_source_drift: true}). The author then
    learns immediately that their edit is not in the run.
  2. Remove the fallback. The source_path branch 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

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

  1. Masterplanner25 commented on Aug 19, 2026

    @Masterplanner25
    OwnerAuthor

    Correction to my own field measurement above. I reported path-only(exists)=0 and inferred from it that the source_path fallback 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 records workflow_source_path and not workflow_source_code (verified against src/ at 5.0.4):

    source_code: None
    source_path: C:\...\wf.nd
    

    So every embedded run_file run 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 same resume_workflow call.

    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:

    entry point source_code source_path resume replays
    nodus run / nodus dap set set the pinned source, edits ignored
    NodusRuntime.run_file unset set the current file, edits picked up
    NodusRuntime.run_source unset unset nothing — fails (#469)

    Same call, three semantics, none documented, chosen by how the run happened to start.

    Note also the interaction with #469's fix: setting vm.source_code in run_source closes that issue, but if it is also set for run_file it 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.

  2. added a commit that references this issue on Aug 25, 2026
  3. Masterplanner25 commented on Aug 25, 2026

    @Masterplanner25
    OwnerAuthor

    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:

    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) and tests/test_resume_topology_validation.py (the unpinned-branch warning tests).

  4. Masterplanner25 commented on Aug 25, 2026

    @Masterplanner25
    OwnerAuthor

    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/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfound-in:langgraph-researchSurfaced by the LangGraph research cross-check against 5.0.4found-in:orchestration-dsl-researchSurfaced by the Orchestration DSL research cross-check (argo/airflow/nix/dagster) against 5.0.4severity:mediumUX broken but workaround existssubsystem:workflow

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions