Skip to content

outputFiles validator failure doesn't fail runs #5

Description

@edwinow

Hi team, another small one I came across; again sharing as an issue rather than a PR so you can decide if you want to take this shape or a different one. I'm not 100% sure if I'm misusing the runtime but below should provide enough detail.

What I observed

If a codon's outputFiles.beforeCopy (or outputFiles.copy) command exits non-zero, the failure is logged via handleError with "codonOutputBeforeCopy", but the runtime still reaches RunCompleted and the headless process exits 0.

Reproducer (synthetic, no model needed): construct a Hank with a passing model turn and an outputFiles.beforeCopy step that exits 1. The codon completes (model exit 0 = finalStatus: "completed"), then beforeCopy fails, then runtime shuts down via the "all codons completed" path. Final process exit code is 0 despite the validator failure.

As I currently understand it (unsure)

Both must happen:

  1. The catch block at the outputFiles loop logs the failure but doesn't propagate it — it calls handleError(..., "codonOutputBeforeCopy") which at ErrorSeverity.OPERATION just logs and notifies; doesn't transition currentRun.status to failed or call shutdown("codon failure").
  2. The exit-code derivation at shutdown reads getCurrentRun() after the RunCompleted transition has already cleared currentRunId — so even if currentRun.status had been flipped to "failed" before this point, the query at exit time returns null and falls through to finalExitCode = 0.

So a single-site fix in the catch block alone wouldn't work (the transition would still clear the run reference). Both sites need to change together.

A 1-commit fix on my fork

Diff (with "Able to merge" status): release/alpha...edwinow:hankweave-runtime:wo-213-validator-exit

1 commit ahead of release/alpha, 0 behind. 2 files changed (1 prod runtime + 1 new test). The compare page shows the green "Able to merge" banner — clean fast-forward.

Shape of the fix (in server/hankweave-runtime.ts):

  • Fix A (catch block at the outputFiles loop): after handleError(...), transition the run to RunFailed via stateManager.transition({ type: "RunFailed", data: { runId: this.currentRunId } }), then call shutdown("codon failure") — which has an existing path mapping reason === "codon failure" to finalExitCode = 1 directly. Fail fast: don't continue to the next output group.
  • Fix B (exit-code derivation at "all codons completed" shutdown): use the runForTelemetry snapshot captured before the terminal transition, instead of querying getCurrentRun() after it has cleared currentRunId. Without this, any future code path that marks the run failed before shutdown would silently lose the signal.

Both fixes use existing internal APIs (stateManager.transition({ type: "RunFailed", ... }) is already used at other sites; runForTelemetry is already captured at the relevant point for telemetry purposes — the fix just reuses it for exit-code derivation).

Tests added

  • tests/integration/output-beforecopy-exit.test.ts — replay-backed integration test (no live model call) that asserts the process exits non-zero when outputFiles.beforeCopy fails. Passes deterministically in ~3s.

Verification done

Locally on the same branch:

  • bun test tests/integration/output-beforecopy-exit.test.ts — pass
  • bun run tc — pass
  • bun run lint — pass
  • bun run build — pass
  • git diff --check HEAD — clean
  • Live reproducer with pi/openai-codex/gpt-5.5 against an external rig-first exemplar Hank — exited 1 (was exiting 0 before the fix) in 22s

Happy to open as a PR if you'd prefer that flow/if above is accurate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions