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:
- 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").
- 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.
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(oroutputFiles.copy) command exits non-zero, the failure is logged viahandleErrorwith"codonOutputBeforeCopy", but the runtime still reachesRunCompletedand the headless process exits 0.Reproducer (synthetic, no model needed): construct a Hank with a passing model turn and an
outputFiles.beforeCopystep 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:
handleError(..., "codonOutputBeforeCopy")which atErrorSeverity.OPERATIONjust logs and notifies; doesn't transitioncurrentRun.statustofailedor callshutdown("codon failure").getCurrentRun()after theRunCompletedtransition has already clearedcurrentRunId— so even ifcurrentRun.statushad been flipped to"failed"before this point, the query at exit time returns null and falls through tofinalExitCode = 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):handleError(...), transition the run toRunFailedviastateManager.transition({ type: "RunFailed", data: { runId: this.currentRunId } }), then callshutdown("codon failure")— which has an existing path mappingreason === "codon failure"tofinalExitCode = 1directly. Fail fast: don't continue to the next output group.runForTelemetrysnapshot captured before the terminal transition, instead of queryinggetCurrentRun()after it has clearedcurrentRunId. 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;runForTelemetryis 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 whenoutputFiles.beforeCopyfails. Passes deterministically in ~3s.Verification done
Locally on the same branch:
bun test tests/integration/output-beforecopy-exit.test.ts— passbun run tc— passbun run lint— passbun run build— passgit diff --check HEAD— cleanpi/openai-codex/gpt-5.5against an external rig-first exemplar Hank — exited 1 (was exiting 0 before the fix) in 22sHappy to open as a PR if you'd prefer that flow/if above is accurate.