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).
Summary
flows run --reuse-from <run-id>reuses a step when itsstep_spec_hashand 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-workflowskill 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:
ts-typecheckrecordedexit=2— the repo was missing a workspace symlink (node_modules/@agent-relay/cli-surface).--reuse-from <run f>:ts-typecheckwas reused. No command ran. The evidence file on disk still saidexit=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: falsein 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
deterministicstep runningnpm 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:
Options, not exclusive:
reuse: neveron a step — the minimal, explicit fix.deterministicsteps to no-reuse unless they opt in. Reuse is most valuable for expensiveagentandllmsteps; a shell command is usually cheap to redo and much more likely to have external effects. This inverts the risk in the safer direction.reuseSummaryalready computesreusedSteps/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
--reuse-fromhonours it.--reuse-fromprints which steps were reused.Related
failOnErrorequivalent, which is why the recorder pattern exists.flows resumeis unusable on a spec run, which is why--reuse-fromis the only recovery path and therefore gets used heavily.Environment
CLI 2.0.22, macOS arm64. Reuse eligibility read from
kernel/relayflowd-core/src/memoization.rs(step_spec_hash,resolved_input,candidates).