Skip to content

--reuse-from silently reuses steps whose real output is on disk, resurrecting stale evidence; no way to declare external effects #523

Description

@khaliqgant

Summary

flows run --reuse-from <run-id> reuses a step when its step_spec_hash and resolved input match. That is correct for a step whose output is its journaled value. It is wrong for a step whose real output is a side effect on disk, and there is no way for such a step to say so.

The result is silent: the step is skipped, the file it would have rewritten keeps its old contents, and a later gate reads a verdict describing a tree that no longer exists.

Evidence

The flow uses the repair-before-failure pattern that the relay-80-100-workflow skill prescribes. A gate step wraps its command in a recorder that journals {command, exitCode, verdict, tail} to a file and always exits 0, so a red command becomes work for a repair agent rather than a dead run.

That design makes every one of those steps reusable — the step succeeded even though the command it wrapped failed.

Observed across two runs of the same flow:

  • Run f: ts-typecheck recorded exit=2 — the repo was missing a workspace symlink (node_modules/@agent-relay/cli-surface).
  • I repaired the link. The same command now exits 0 in 13s.
  • Run g, launched with --reuse-from <run f>: ts-typecheck was reused. No command ran. The evidence file on disk still said exit=2, describing a tree that had been fixed 20 minutes earlier.

Nothing reported this. The step showed as done.

Why this is not just "the author's problem"

The recorder is not an exotic pattern — it is the only way to express failOnError: false in v2 (see #509), so any flow doing repair-before-failure has this shape, and every one of its gate steps is silently reusable.

More generally: reuse assumes a step's observable output is in the journal. A deterministic step running npm run build, git commit, or a code generator has its real output on disk. Reuse cannot see that, and today it cannot be told.

What to change

Let a step declare that its effects are external and must not be reused:

- id: run-tests
  type: deterministic
  command: 'node gates.mjs record --name unit-tests -- npm test'
  reuse: never        # or: effects: external

Options, not exclusive:

  • reuse: never on a step — the minimal, explicit fix.
  • Default deterministic steps to no-reuse unless they opt in. Reuse is most valuable for expensive agent and llm steps; a shell command is usually cheap to redo and much more likely to have external effects. This inverts the risk in the safer direction.
  • Report what was reused. reuseSummary already computes reusedSteps / executedSteps (packages/sdk/src/cli/reuse.ts). Printing the reused step ids at run start would have made this visible in seconds instead of two runs later.

The last one is worth doing regardless of the others.

Acceptance

  • A step can declare that it must not be reused, and --reuse-from honours it.
  • A run started with --reuse-from prints which steps were reused.
  • A test covers a reused step whose side effect was subsequently undone.

Related

Environment

CLI 2.0.22, macOS arm64. Reuse eligibility read from kernel/relayflowd-core/src/memoization.rs (step_spec_hash, resolved_input, candidates).

Activity

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 workinggarden-readyScoped and ready for an agent to pick up

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions