Skip to content

fix(chain-runner): stop a rejected dispatch from poisoning the reliability signal - #1017

Merged
aaronjmars merged 2 commits into
aeonfun:mainfrom
Svector-anu:fix/upstream-chain-invalid-dispatch-cron-state
Sep 3, 2026
Merged

aaronjmars merged 2 commits into
aeonfun:mainfrom
Svector-anu:fix/upstream-chain-invalid-dispatch-cron-state

Conversation

@Svector-anu

@Svector-anu Svector-anu commented Sep 3, 2026 •

Copy link
Copy Markdown
Collaborator

what looked broken

after wiring a cross-model dev-loop chain on my fork, chain:dev-loop got flagged repeatedly by skill-health — six manual dispatches over two days, all recorded as failures, dragging the chain's lifetime success rate down and firing false P0s.

what's actually happening

not a defect in the loop. every one of those failures happened at the exact same point, before any skill ran:

##[error]dev-loop requires target=external:owner/repo or external:owner/repo#issue

both target guards (the top-level dev-loop check and the generalized $chain_target check any future chain can reuse) are working exactly as designed — rejecting a dispatch with no target input rather than silently falling back to feature's broad watched selector. target can't be made conditionally-required in the native workflow_dispatch schema, so it's easy to dispatch this chain manually (CLI or the Actions UI) and forget it. that part of the design is correct and untouched by this PR.

the actual bug

Update cron state runs if: always() and defaults STATUS="${CHAIN_STATUS:-failed}" for any early exit — so the correctly-rejected malformed dispatch gets written into memory/cron-state.json identical in shape to a genuine mid-run failure. that's the signal skill-health reads for lifetime reliability, so non-runs were making a working chain look broken.

confirmed both snapshot and feature dispatch has no correlation ID failure paths are unaffected and out of scope — they already correctly set CHAIN_FAILED=true; break, funneling through the proper final-status logic. only the two pre-flight guards bypass it entirely.

fix

mark the pre-flight rejection distinctly (CHAIN_STATUS=invalid-dispatch) at both guard sites, and skip the cron-state write for that case only. the job still fails loud — red x, ::error:: unchanged — for whoever ran the malformed dispatch; only the fleet-wide bookkeeping stops being polluted. a real failure or success still writes exactly as before.

tests

  • scripts/tests/test_chain_runner.sh — unchanged, still passes
  • scripts/tests/test_chain_runner_invalid_dispatch.sh (new) — extracts the guard verbatim from the workflow (same anchored-sed pattern the sibling test already uses for dispatch_skill()) and asserts: invalid-dispatch exits 0 without reaching the state write and logs why; a real failure (CHAIN_STATUS unset) and a real success both still reach the write unchanged

Svector-anu and others added 2 commits September 3, 2026 04:40
…ility signal

dev-loop's target guards (and the generalized $chain_target guard any
future chain can reuse) correctly reject a dispatch missing
target=external:owner/repo rather than silently falling back to the
watched-repos sweep. that part was never broken.

the bug is downstream: "Update cron state" runs `if: always()` and
defaults to STATUS="failed" for any early exit, so a caller-error
rejection that never ran a single skill gets written into
memory/cron-state.json identically to a genuine mid-run failure —
poisoning the chain's lifetime success-rate signal that skill-health
reads, and misreporting the loop as unreliable when it correctly
refused bad input.

found while dogfooding the same pattern on a fork (#54,
where 6 manual dispatches missing target= over two days produced 6 false
"chain:dev-loop failed" P0 flags with zero actual skill runs behind them);
confirmed this exact structure — both pre-flight guards, same if: always()
default — is present on main too, not fork-specific.

give the rejection a distinct CHAIN_STATUS=invalid-dispatch and skip the
cron-state write for it, at both guard sites. the job itself still fails
loud (red x, ::error:: unchanged) for whoever ran the malformed dispatch;
only the fleet-wide reliability bookkeeping stops being polluted. a real
failure or success still writes exactly as before — verified by the added
regression test, which extracts the guard from the workflow (same pattern
scripts/tests/test_chain_runner.sh already uses for dispatch_skill) and
asserts all three paths.

Claude-Session: https://claude.ai/code/session_01SbipDt7VmuDvosRPS8AfEx
The test file was committed but never invoked by ci-tests.yml, so the
regression guard it provides was dead. Wire it into the test job next to
the sibling chain-runner test.

Co-Authored-By: Svector-anu <svector.anu@users.noreply.github.com>
@aaronjmars
aaronjmars merged commit 66ab2b4 into aeonfun:main Sep 3, 2026
2 checks passed
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.

2 participants