Skip to content

fix(api): persist strategy-expanded steps as the run workflow - #38

Open
raskevichai wants to merge 1 commit into
nullclaw:mainfrom
raskevichai:fix/strategy-expansion-persisted
Open

fix(api): persist strategy-expanded steps as the run workflow#38
raskevichai wants to merge 1 commit into
nullclaw:mainfrom
raskevichai:fix/strategy-expansion-persisted

Conversation

@raskevichai

Copy link
Copy Markdown
Contributor

Closes #36.

Root cause

handleCreateRun expands the strategy into effective_steps, then stores the raw request body as workflow_json:

ctx.store.insertRun(run_id, idempotency_key, "running", body, input_json, callbacks_json)

The expanded array was used only to prepopulate the steps table. Since the engine rebuilds its graph from workflow_json at tick time (engine.zig:416), it read steps with no depends_on, treated all of them as graph roots, and dispatched them concurrently.

So applyChain was deriving the edges correctly the whole time - they were just never written down. That also explains #34 (sequential not enforcing chain order): same root cause, observed from the scheduling side.

Changes

1. Store the expanded workflow. New buildExpandedWorkflowJson copies the top-level fields, swaps in effective_steps, and drops strategy and reduce - expansion has already consumed those into depends_on edges and a synthetic reduce step. Leaving them in would let a later reader re-derive a graph that disagrees with the steps stored beside it.

2. Idempotency comparisons moved to the same representation. This part is not in the issue but the fix is wrong without it.

workflow_json is compared against the incoming payload in two places (the early replay check, and the post-insert race path). With the expanded form stored and the raw body compared, an ordinary retry of a strategy run compares expanded-vs-raw, mismatches, and returns 409 conflict instead of a 200 replay. Expansion is therefore computed before the idempotency check so both sides use one representation.

3. No-strategy path unchanged. When there is no strategy, the body is still stored verbatim. Runs written before this change stay byte-comparable, so existing idempotency keys keep replaying correctly.

Tests

Four regression tests:

  • sequential strategy stores expanded depends_on in workflow_json - the stored workflow has depends_on and no longer has strategy.
  • workflow without strategy is stored verbatim - guards the backward-compatibility path above.
  • strategy run replays instead of conflicting on identical body - guards the idempotency interaction; without change 2 this returns 409.
  • strategy run still conflicts on a different body - conflict detection still works.

Sanity check: restoring the original insertRun(..., body, ...) line makes tests 1 and 3 fail, and restoring the fix produces a byte-identical tree.

Verification

zig fmt --check src/api.zig    # clean
zig build test --summary all   # 359/359 passed  (355 before, +4 from this PR)

Scope

Not included, though they touch neighbouring code:

I have not run the e2e suite (tests/test_e2e.sh) since it needs Python mock workers and a live server; the change is covered at the API layer instead.

createRun expanded the strategy into effective_steps and then stored the
raw request body as workflow_json. The expansion was used only to
prepopulate the steps table.

The engine rebuilds its graph from workflow_json at tick time, so it
read a step array with no depends_on, treated every step as a graph
root, and dispatched them all at once. That is why a sequential
strategy did not enforce chain order: applyChain derived the edges
correctly, they were just never written down.

Store the expanded workflow instead, dropping strategy and reduce since
expansion has consumed them into depends_on edges and a synthetic
reduce step.

Two idempotency checks compare the stored workflow_json against the
incoming payload. Both had to move to the expanded form as well, or a
plain retry of a strategy run would compare expanded-vs-raw and be
rejected as a payload change. Expansion is now computed before the
idempotency check so both sides use the same representation.

Runs without a strategy keep storing the body verbatim, so their
comparisons stay byte-stable against runs written before this change.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: strategy expansion discarded — raw body stored instead of expanded workflow (P0)

1 participant