Skip to content

fix(scenario): a certified halt-on-uncertainty is a PASS for a crash-resume scenario - #8

Merged
pjpoulose merged 1 commit into
mainfrom
fix/scenario-halt-certification
Jul 26, 2026
Merged

fix(scenario): a certified halt-on-uncertainty is a PASS for a crash-resume scenario#8
pjpoulose merged 1 commit into
mainfrom
fix/scenario-halt-certification

Conversation

@pjpoulose

Copy link
Copy Markdown
Contributor

One file, scripts/scenario.js, +83/-5, no test changes. Cherry-picked from fix/watchdog-deadman-resume as commit e50f284.

What it does

Adds certifyHaltOnUncertainty() and filters readRunState() to declared step ids, so that a correct halt-on-uncertainty is scored as a PASS for a crash-resume scenario instead of a failure. Certification requires a real unresolved intent and no duplicate effects — it does not accept a halt on faith.

Why it is a separate PR

ci-check-pr-separation.js rejects any PR that mixes evaluator/corpus files with behavior files (contracts/11, contracts/04 B17). scripts/scenario.js is in EVALUATOR_EXACT: it is the evaluator that scores candidates, so changing it alongside the behavior it scores is exactly the conflict of interest that guard exists to prevent. There is no exemption mechanism in the guard, deliberately, and none was added.

Verified on this branch: pass (1 changed file, no mixing).

Read this before looking at the red CI

This PR's CI is red because main is red, not because of this change.

main at b1df2f8 fails all six Phase-A legs, verify (ubuntu-latest, 18), and the publication-hygiene job. Anything branched off it inherits that.

So the change was measured directly instead. Full suite battery on Linux / node 18, against two trees differing only by this commit:

main — 11 failing gating suites

tests/assurance/gpt-sol-pro      tests/register/airgap/deepseek
tests/gsa-register/deepseek      tests/register/approver/mistral
tests/gsa-register/mistral       tests/register/obligations/deepseek
tests/gsa/deepseek               tests/scenario
tests/gsa/mistral                tests/watchdog/gpt-sol-pro
                                 tests/watchdog/grok

main + this commit — the same 11, minus one, plus one

fixed by this commit:  tests/scenario
newly appearing:       tests/state-store/grok

And the runner's own re-run classifier resolved the newcomer:

--- re-running tests/state-store/grok/run-tests.js once ---
    re-run PASSED -> FLAKY (a timing assumption), not a component regression

The failing case was concurrency.two-process-register-deregister+errors a=2 b=0 — a contention-timing case, in a suite scenario.js cannot influence, on main's unfixed copy of it. Commit 08ee6bf on the behavior branch fixes that class.

Net: this takes tests/scenario from red to green on main and introduces nothing. Its remaining CI failures are main's own, pre-existing and untouched.

Independently confirmed on Windows at main + this commit: scenario.js --selftest exit 0, tests/scenario 20 PASS / 0 FAIL, plus graphlint, gate.js --selftest and promote.js --selftest all clean.

What happens after this merges

fix/watchdog-deadman-resume rebases onto the new main. scripts/scenario.js leaves that PR's diff, the separation guard goes green legitimately rather than by override, and every behavior fix on it is present so the suites follow.

Known follow-up, not included here

scripts/scenario.js has two stdout report-emit sites that still use process.stdout.write(...) followed by process.exit(), which can lose a queued report on a POSIX pipe (measured: 8190 bytes of an 11886-byte report on real macOS, exit code 0). Every other affected script was fixed on the behavior branch in 0da6f20; scenario.js was deliberately left out so this PR stays a single-purpose evaluator change. It should land as its own follow-up once both branches are in.

…resume scenario

tests/scenario check 5c was flaky, and the flakiness was the evaluator marking the
component wrong for being right. A crash-resume scenario kills the manager the
instant the target step's checkpoint lands. Where that instant falls is
scheduling-dependent: it can land outside any side-effect window (resume runs to
completion) or inside a concurrent fan-out sibling's window, leaving an intent
recorded with no completion. In the second case the resumed run correctly HALTS
with UNRESOLVED SIDE EFFECT -- it refuses to guess about external state instead of
silently re-sending -- and the evaluator scored that as workflow_fault because
expected.completed_steps was not reached. Reproduced under 2-core load:
fanout-crash-resume, alternating sides, same commit.

This also put the evaluator's own determinism claim at risk: checks 1a and 6b
require the same seed to produce a byte-identical bundle, and pass/cause_code were
depending on CPU scheduling.

A halt is now an accepted outcome for a crash-resume scenario, and
completed_steps is inapplicable to a run that deliberately stopped -- but ONLY
when the on-disk state justifies the halt: a real unresolved intent in
intents.log, and no duplicated effect in effects.log (the halt came INSTEAD of a
re-send, not after one). This is the same PD-3 certification scripts/chaos.js
applies, so a run cannot earn a pass by printing the magic string. Every other
check still has to pass: the invariants, and no pre-crash step re-executed.
Four selftest cases pin the rule, including the two gaming attempts.

readRunState() also stops treating every *.json in the run directory as a
completed-step checkpoint. Runtime state that happens to be JSON (budget-state,
a watchdog capability snapshot) is not a step, and the resume check would demand
a '"status":"skipped"' line for it -- the same mis-detection that broke chaos.js.
Callers now pass the scenario's declared step ids.

Verified on Linux: tests/scenario 20 PASS / 0 FAIL (was 19/1); scenario
--selftest determinism_pass true; 6 consecutive full-corpus replays under 2-core
contention all clean AND byte-identical bundle hashes. Green on Windows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARvYLenCrqcDcr8VyefoxG

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 575527b213

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/scenario.js
Comment on lines +524 to +525
const halt = certifyHaltOnUncertainty(state, result2.stdout + result2.stderr);
if (halt.halted) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require the restart to actually halt before certifying it

In the tree-directory replay path, a worker can append an unmatched intent, print UNRESOLVED SIDE EFFECT, and call process.exit(0) before producing the remaining checkpoints; this branch treats that successful early exit as a certified halt and returns pass: true, bypassing the expected-step check below. I reproduced this with a two-step crash-resume scenario whose second worker did exactly that, and both candidate and baseline were scored ok. Gate this alternative on result2.code !== 0 (as chaos.js does) so merely emitting the marker and exiting successfully cannot earn a safety pass.

Useful? React with 👍 / 👎.

Comment thread scripts/scenario.js
Comment on lines +482 to +484
const cnt = {};
for (const e of state.effects) cnt[e] = (cnt[e] || 0) + 1;
const dupes = Object.entries(cnt).filter(([, n]) => n > 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count effect IDs without inheriting object properties

When an effect ID is __proto__—which is permitted by the scenario step-name pattern—assigning it on this ordinary object does not create an own numeric counter, so Object.entries(cnt) reports no duplicate even if effects.log contains that ID multiple times. A crash-resume run with duplicated __proto__ effects and another unresolved intent can therefore be certified and scored as a safe halt despite the explicit no-duplicate requirement. Use a Map or Object.create(null) for this counter (and the corresponding invariant counter) so all schema-valid IDs are counted.

Useful? React with 👍 / 👎.

Comment thread scripts/scenario.js
Comment on lines +528 to +530
return {
pass: violations.length === 0,
cause_code: violations.length > 0 ? "workflow_fault" : "ok",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict the alternate halt pass to crash-recovered outcomes

A scenario can legally contain fixture.crash_after_step while declaring expected.outcome as complete, halt, or budget-exceeded, because neither the published schema nor validateScenario() conditionally ties those fields together. If its restart genuinely halts with certified unresolved state, this unconditional pass ignores the already-computed outcome.pass and marks the scenario green even though the declared expected outcome was not observed. Only apply the alternate safety-pass semantics when the scenario explicitly expects crash-recovered (and/or is declared failure_mode: "crash-resume").

Useful? React with 👍 / 👎.

@pjpoulose
pjpoulose merged commit cd51423 into main Jul 26, 2026
5 of 26 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.

1 participant