Uh oh!
There was an error while loading. Please reload this page.
fix(run): exit non-zero on session.error so CI wrappers see failures - #71
Conversation
The `aictrl run` command's session.error event handler logged the error and accumulated it into a local string but never set `process.exitCode`. On auth/provider failures, the subsequent `session.status: idle` broke the run loop into the success branch and the process exited with code 0 — while stdout printed "Authentication Failed" or similar. This silently broke CI workflows wrapping `aictrl run` (observed on aictrl-dev/aictrl's "AI Review" workflow on 2026-05-20): step conclusion `success`, no actual work, badge stays green. Fix: set `process.exitCode = 1` when the session.error event fires for the primary session. Sub-agent failures keep their existing behavior of flowing into the parent agent. Use `exitCode` (not `process.exit`) so the event loop drains naturally — session_complete still emits before the process terminates. Regression test (test/cli/run-session-error-exit.test.ts) statically asserts the handler block sets a non-zero exit code, mirroring the existing run-event-race.test.ts style. Closes#70 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
byapparov
commented
May 21, 2026
Note on the failing `Aictrl AI Review` check: it's failing in the `Install Aictrl CLI` step (`npm install @aictrl/cli@latest` → `EUNSUPPORTEDPROTOCOL: Unsupported URL Type "workspace:": workspace:*`), not on this PR's code change. The `verify` check (typecheck + tests) passes. The published `@aictrl/cli@0.3.3` manifest ships unresolved `workspace:*` deps that plain `npm install` can't handle. Same failure affects every PR opened on this repo since 0.3.3 published — filed as #72 with reproduction, root cause hypothesis, and acceptance criteria. PR #71's change has been verified locally:
Suggest merging on `verify` ✓ alone, or waiting for #72 to land if you'd rather have AI Review running first. |
Uh oh!
There was an error while loading. Please reload this page.
v0.3.4 release (#71 + #72 fixes) couldn't actually publish @aictrl/cli because the publish workflow doesn't auto-stamp AICTRL_VERSION into the main package.json `version` fields — only platform binaries get the release version. On v0.3.4 the workflow tolerated "already-published" errors for cli 0.3.3 / util 1.2.16 / sdk 0.1.2 and the smoke test correctly caught it (ETARGET: No matching version found for @aictrl/cli@0.3.4). Bump cli and util so v0.3.5 actually publishes new versions carrying the resolver + exit-code fixes. Skip sdk: its catalog: deps are only in devDependencies, ignored by npm consumers, so 0.1.2 is functionally installable. Follow-up worth filing: publish.yml should stamp AICTRL_VERSION into all package manifests, not just platform binaries, so future releases don't need manual version bumps. Refs #71#72 Co-authored-by: Bulat Yapparov <by4pparov@yandex.ru> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
aictrl run'ssession.errorhandler inrun.tslogged the error and stashed it into a local string but never setprocess.exitCode— so when the model session subsequently emittedsession.status: idle, the run loop broke into the success branch and the process exited 0.aictrl runshows stepconclusion: successwhile the actual work (review, agent invocation, etc.) silently failed.process.exitCode = 1whensession.errorfires for the primary session (sub-agent failures keep their existing behavior). UsesexitCoderather thanprocess.exit()sosession_completestill emits before termination.Root cause
packages/cli/src/cli/cmd/run.ts:559-572— the handler block only updated the localerroraccumulator string and calledUI.error(). It did not propagate to the process exit code, so the success branch ofloopDone.then(...)(line 703) ran on every error.Trace for the auth-failure case observed in production:
session/processor.ts:388publishessession.error, sets statusidle.run.ts:559handler logs but doesn't touchexitCode.run.ts:614session.status idlebreaks the loop.run.ts:703success branch — emitssession_complete, returns.The neighboring error paths (
promptResult.catchat line 768,loopDone.catchat line 721) already setexitCode = 1for synchronous failures; thesession.errorevent path was the only gap.Reproduction (from aictrl-dev/aictrl#2058 investigation)
After the fix: exit
1.Test plan
packages/cli/test/cli/run-session-error-exit.test.ts) — static lock asserting the handler block sets a non-zero exit code, mirroring the existingrun-event-race.test.tsanti-pattern-lock style. Fails onorigin/main(verified locally), passes on this branch.bun test test/cli/— 69/69).bun typecheckpasses.Acceptance criteria (from #70)
aictrl runwith an invalidZHIPU_API_KEYexits non-zero — flows throughsession.errorfor primary session →exitCode = 1aictrl runwith a network failure to the provider exits non-zero — same path (provider-side error →MessageV2.fromError→Session.Event.Error)Related
1as a reasonable baseline; if feat: lite headless binary for Cloud Run #4 lands a structured convention, the constant can be aligned then without redesigning the propagation path.Closes#70
🤖 Generated with Claude Code