Repository navigation
No error explanation if a story run fails #377
Description
Activity
- changed the title
[-]Engine events are discarded when a turn fails, hiding the real error[/-][+]No error explanation if a story run fails[/+]on Sep 14, 2026 Adding the root cause, since I had the source open while debugging this.
Version:
main@771b647(0.12.0) · local source install,FACILITY_WORKSPACE_DRIVER=dockerWhat the error actually was
401 API key is invalid— a bad value in the project's managedANTHROPIC_API_KEY. Recovering
that meant finding the workspace container and grepping the Claude Code session transcript at
/workspace/.facility/claude/projects/<slug>/<session>.jsonl. Nothing in the UI, the story
timeline, orturn_eventsheld it:turn_eventshad exactly two rows,turn.startedand
turn.failed, the latter carrying onlyclaude_code exited with status 1.Root cause
The mechanism to surface this already exists and is correct — it is just scoped to one error
code.CliAgentEngine.executeattaches the parsed engine events to every failure it throws
(services/api/src/turns/engines.ts:93-101), for bothagent_session_corruptand
agent_engine_failed.TurnDispatcher.persistFailedEngineEvents
(services/api/src/turns/dispatcher.ts:536-551) writes those events with redaction and bounding,
exactly like the success path.But it has a single call site,
dispatcher.ts:222, inside this guard
(dispatcher.ts:215-221):} catch (error) { if ( !(error instanceof AgentEngineError) || error.code !== "agent_session_corrupt" || !session ) { throw error; } await this.persistFailedEngineEvents(error, eventBase, secrets);
So a resumed-session failure gets its events persisted, and every other engine failure rethrows
at line 220 without them. Anagent_engine_failed— which is what an API error such as a 401
produces — reaches the outer catch atdispatcher.ts:333-354, which records cost from
error.details.usageand writes oneturn.failedcarrying only the message string.
error.details.eventsis dropped on the floor.turn-dispatcher.integration.test.ts:506already proves the mechanism works and redacts correctly
on the corrupt-session path, so this is a narrowing bug rather than a missing feature.Why the message is always the generic fallback.
engines.ts:87:const message = result.stderr.trim() || `${this.name} exited with status ${result.exitCode}`;
The Claude Code CLI reports API errors as a
resultobject on stdout withis_error: true,
leaving stderr empty. So for the whole class of API-level failures — auth, rate limits, model
access — the fallback string is what surfaces. Confirmed by running the sameclaudeinvocation
manually in the workspace container (exit 0 on a valid key) and reading the failing run's
transcript, which carries"error":"authentication_failed"/Error: 401 API key is invalid.on
stdout.Suggested fix
Call
persistFailedEngineEventsfor anyAgentEngineError, not onlyagent_session_corrupt. The
rethrow atdispatcher.ts:231already forwardserror.details, so moving the single call into the
AgentEngineErrorbranch of the outer catch covers both paths without persisting twice.The headline message is a separate question — promoting the parsed error result into it would make
the failure read401 API key is invalidrather thanexited with status 1. Worth doing, but it
touches the parser rather than the dispatcher, so probably its own change.Related
#328 (
fix(github): name the kickstart failure instead of returning a bare 500) is the same class
of problem on a different path. The argument made there — that the first flow a new installation
runs is the worst place for an opaque answer — applies here too.Happy to open a PR for the dispatcher change if it is wanted.
Root cause and fix in #384
What you were doing
I set up facility for my project and created a story for an architect agent to plan the refactoring
Where it went wrong, or slower than expected
I've got an error after 3 minutes of running, no evidence of what exactly went wrong, only "claude_code exited with status 1".
After a while I researched that the agent was failing with authentication due to wrong API token
What you expected instead
Clear stacktrace/error message in UI so I understand what exactly went wrong so I can adjust from there
Time from clone to first agent run
2.5 hours