Skip to content
Errors

Errors

Every error Sortie produces falls into one of six categories: startup failures, tracker errors, agent errors, workspace errors, worker exit outcomes, and HTTP API errors. This page documents each: what it means, whether Sortie retries it, and what you should do.

Error kind strings appear in logs exactly as shown below. Search this page for the string you see in your output. For step-by-step diagnosis of the most common failures, see How to troubleshoot common failures.


Startup and configuration errors

These errors prevent Sortie from starting. They appear immediately on launch and cause exit code 1. None are retryable. Sortie exits. Fix the configuration and restart.

CheckLog outputAction
workflow_loadworkflow file cannot be loaded: <details>Provide the correct path as argument, or create ./WORKFLOW.md. If the file exists, fix YAML front matter syntax.
tracker.kindtracker.kind is requiredAdd tracker.kind to your WORKFLOW.md front matter.
tracker_adapterunknown tracker adapter kind "<kind>"; registered: [<list>]Set tracker.kind to one of the kinds the message lists.
tracker.api_keytracker.api_key is required for tracker kind "<kind>" (value may be empty after environment variable expansion)Set the environment variable referenced by tracker.api_key (e.g., $SORTIE_JIRA_API_KEY).
tracker.projecttracker.project is required for tracker kind "<kind>"Add the project field to the tracker section.
tracker.project.formattracker.project must be in owner/repo format (e.g. "sortie-ai/sortie")Use owner/repo format with exactly one / and no whitespace in either segment. Raised by the github and gitea adapters; gitlab validates its project field separately.
agent.kindagent.kind is requiredAdd agent.kind to your WORKFLOW.md front matter.
agent_adapterunknown agent adapter kind "<kind>"; registered: [<list>]Set agent.kind to one of the kinds the message lists.
agent.commandagent.command is required for agent kind "<kind>"The default kind launches a process, has no default command, and agent.command is unset. Set agent.command; agent-client-protocol always needs it.
agent.command<selector> selects agent kind "<kind>", which has no default command and launches agent.command only as the default agent kind (dispatch.default.agent, else agent.kind)A dispatch rule routes to a kind that has no default command while another kind is the default. Make that kind the default kind, or point the rule at a kind with a default command. <selector> names the field or rule that introduced the kind.
config.agent.commandconfig: agent.command: expected string or list, got <type> / config: agent.command: must not be an empty listWrite agent.command as a string or as a non-empty list of strings, or remove the key to take the kind’s default command.
config.agent.commandconfig: agent.command[<i>]: expected string, got <type> / config: agent.command[<i>]: must not be emptyElement <i> of the list is not a string or is empty; element 0, the program, may not be only whitespace either. Write every element as a non-empty string.
agent.turn_timeout_msconfig: agent.turn_timeout_ms: must be greater than 0Set agent.turn_timeout_ms to a positive number of milliseconds, or remove the field to use the default.
agent.max_cost_usdconfig: agent.max_cost_usd: must be non-negative / config: agent.max_cost_usd: must be a finite number / config: agent.max_cost_usd: invalid number value: <value>Set agent.max_cost_usd to a finite number of US dollars that is not negative, written as a number or a quoted number, or remove the field to leave the money ceiling off.
agent.max_daily_cost_usd, agent.max_weekly_cost_usd, agent.max_monthly_cost_usdconfig: <field>: must be non-negative / config: <field>: must be a finite number / config: <field>: invalid number value: <value>, where <field> is the setting’s full nameSet the setting to a finite number of US dollars that is not negative, written as a number or a quoted number, or remove it to leave that instance spend ceiling off.
config.<field> (e.g. config.polling.interval_ms, config.agent.turn_timeout_ms)config: <field>: value is outside the range an integer setting accepts, -9223372036854775808 to 9223372036854775807, with (from SORTIE_<VAR>) appended when the value came from a SORTIE_* overrideSet the field, or the environment variable, to a value within the range. The field’s own value checks still apply once the value is in range.
reactions.<kind> (review_comments, bot_review, auto_merge, merge_conflicts, merge_completion)invalid <field>: value is outside the range an integer setting accepts, -9223372036854775808 to 9223372036854775807, where <field> is poll_interval_ms for any of the five kinds, debounce_ms for review_comments only, watch_window_ms for review_comments, bot_review, auto_merge, or merge_conflicts, and max_continuation_turns for review_comments or bot_reviewSet the named kind’s field to a value within the range. See the reactions reference for when a reaction-configuration change takes effect.
reactions.ci_failure.watch_window_msconfig: reactions.ci_failure.watch_window_ms: must be non-negative, got <val> / config: reactions.ci_failure.watch_window_ms: must not exceed 9223372036854 (about 292 years); use 0 for no time limit, got <val>Set watch_window_ms to a non-negative number of milliseconds no greater than 9223372036854, or 0 to remove the bound.
reactions.<kind> (review_comments, bot_review, merge_conflicts, auto_merge)watch_window_ms must be non-negative, got <val> / watch_window_ms must not exceed 9223372036854 (about 292 years); use 0 for no time limit, got <val>Same fix as above, applied to the named kind’s watch_window_ms. Unlike the ci_failure row, this diagnostic carries no config: prefix and no field path; sortie validate reports it under the check name reactions.<kind>.
tracker.handoff_statetracker.handoff_state: "<val>" collides with active state "<state>" / collides with terminal state "<state>"Use a state that appears in neither active_states nor terminal_states. A handoff parks the issue for a person, so it is neither dispatchable nor terminal. Applies to every tracker.kind.
tracker.no_change_statetracker.no_change_state: requires tracker.handoff_state; a declared run performs no transition where no handoff path applies / tracker.no_change_state: "<val>" must equal tracker.handoff_state or name a member of tracker.terminal_statesSet tracker.handoff_state before setting no_change_state, and give no_change_state the same value as handoff_state or a value listed in terminal_states. See state machine reference.
config.tracker.terminal_statesconfig: tracker.terminal_states: must name at least one state for tracker kind "<kind>": a closed issue reads as its first entrytracker.kind is github, gitea, or gitlab and terminal_states is empty. Name at least one terminal state. A reload that empties the list keeps the previous configuration.
config.tracker.active_states[<i>]config: tracker.active_states[<i>]: needs a state name with a character other than white spaceAn entry of active_states is empty or only white space, on any tracker kind. Remove the entry or write a state name. The same rule and message shape apply to terminal_states[<i>].
tracker.handoff_evidencetracker.handoff_evidence: must be one of observed, strict, or offSet the field to observed, strict, or off, or leave it unset for the default observed.
tracker.in_progress_statetracker.in_progress_state: "<val>" is not in active_states / collides with terminal state / collides with handoff_statein_progress_state must be in active_states, must not be in terminal_states, and must not equal handoff_state.
tracker.commentstracker.comments: expected map, got <type>The comments value must be a YAML map, not a scalar or list.
tracker.comments.on_dispatchtracker.comments.on_dispatch: expected bool, got <type>Use true or false. Quoted strings like "true" are not accepted. Same applies to on_completion and on_failure.
config.reactions.<kind>.escalationconfig: reactions.<kind>.escalation: must be "label", "comment", or "none", got "<value>"Use label, none, or the deprecated comment. See escalation actions.
config.notifications[<i>].eventsconfig: notifications[<i>].events: expected sequence, got <type> / config: notifications[<i>].events[<j>]: expected string, got <type>Write events as a list of event names, such as events: [session.failed].
config.notifications[<i>].events[<j>]config: notifications[<i>].events[<j>]: unknown event type "<value>"; valid: <catalog>Use one of the names the message lists. See the event catalog.
config.notifications[<i>].events[<j>]config: notifications[<i>].events[<j>]: event type "<value>" is listed more than onceRemove the repeat.
config.notifications[<i>].events[<j>]config: notifications[<i>].events[<j>]: agent.message cannot be posted as a tracker commentRemove agent.message from the tracker_comment entry. Messages from the agent never reach the issue.
config.notifications[<i>].eventsconfig: notifications[<i>].events: a tracker_comment entry must list its events; write [] to post only what the deprecated settings enableAdd events to the entry. events: [] posts only the comments that the deprecated tracker.comments flags and escalation: comment enable.
config.notifications[<i>].kindconfig: notifications[<i>].kind: only one notifications entry may have kind tracker_commentMerge the entries into one.
config.notifications[<i>].kindconfig: notifications[<i>].kind: kind tracker_comment requires a configured trackerSet tracker.kind, or remove the entry.
config.notifications[<i>].<key>config: notifications[<i>].<key>: kind tracker_comment takes only kind and eventsRemove the key from the tracker_comment entry. This includes max_per_session.
config.notifications[<i>].kindconfig: notifications[<i>].kind: <reason>The entry receives an event Sortie produces and its kind is not a registered backend. The reason reads unknown notifier adapter kind "<kind>"; registered: [discord, slack, telegram, webhook]. Use webhook, slack, telegram, discord, or tracker_comment.
config.notifications[<i>]config: notifications[<i>]: <reason>The entry receives an event Sortie produces and its backend cannot be built, for example because a required secret resolved to the empty string. Fix the field the reason names.
config.notifications[<i>]config: notifications[<i>]: telegram notifier: bot_token is required / config: notifications[<i>]: telegram notifier: chat_id is requiredA telegram entry that receives an event Sortie produces has no bot_token or chat_id, or the value is blank, including a variable that is unset. Set the field.
config.notifications[<i>]config: notifications[<i>]: telegram notifier: bot_token: expected string, got <type> / config: notifications[<i>]: telegram notifier: chat_id: expected string or integer, got <type>A telegram entry that receives an event Sortie produces has a bot_token that is not a string, or a chat_id that is neither a string nor an integer. Fix the value.
config.notifications[<i>]config: notifications[<i>]: discord notifier: webhook_url is required / config: notifications[<i>]: discord notifier: webhook_url: expected string, got <type>A discord entry that receives an event Sortie produces has no webhook_url, a blank one (including a variable that is unset), or one that is not a string. Set the field to the webhook address as a string.
none (sortie validate reports nothing)failed to build session tool registry with error="notifier \"<kind>\": <kind> notifier: <field> is required", printed to stderr by sortie mcp-server, which then exits with code 1A telegram or discord entry (the same applies to slack and webhook) that lists agent.message but no event Sortie produces is built only when the agent session starts its sortie mcp-server process, so sortie validate and startup pass. The process stops and the agent gets no notify_operator tool. The field messages are the ones in the rows above, with the type form <field>: expected string, got <type>. Set the field the message names.
config.price_tableconfig: price_table: expected map, got <type>Write price_table as a map, or remove it.
config.price_table.embeddedconfig: price_table.embedded: expected boolean, got <type>Write true or false.
config.price_table.fetchconfig: price_table.fetch: expected boolean, got <type>Write true or false.
config.price_table.fetch_urlconfig: price_table.fetch_url: expected string, got <type>Write the address as a string, or remove the key to take the default address.
config.price_table.replace_urlconfig: price_table.replace_url: expected string, got <type>Write the address as a string, or remove the key.
price_table.fetch_urlconfig: price_table.fetch_url: resolved to empty (check environment variable)Set the environment variable the address references, or remove the key.
price_table.replace_urlconfig: price_table.replace_url: resolved to empty (check environment variable)Set the environment variable the address references, or remove the key.
price_table.replace_conflictconfig: price_table: replace_file and replace_url are mutually exclusiveKeep one of price_table.replace_file and price_table.replace_url. See price_table.
price_table.replace_fileconfig: price_table.replace_file: expected string, got <type> / config: price_table.replace_file: resolved to empty (check environment variable) / config: price_table.replace_file: <reason>Write the path as a string, set the environment variable it references, or fix the path or the file. <reason> is a read error or says the file is not a valid price table: it is not a JSON object, a rate is not a finite non-negative number, or no entry carries a rate. Sortie does not price from the built-in table instead. See price_table.
codex.approval_policy.interactivecodex.approval_policy is set to a value that lets the agent stop and ask for approval, and an unattended run has no one to answer; only "never" is supportedSet codex.approval_policy: never, or remove the field. See Codex validate-time checks.
claude-code.permission_mode.interactiveclaude-code.permission_mode is set to a value that lets the agent stop and ask for approval, and an unattended run has no one to answer; only "bypassPermissions" is supportedSet claude-code.permission_mode: bypassPermissions, or remove the field. See Claude Code validate-time checks.
dispatch.agent.missing_block<selector> selects agent kind "<kind>", but the workflow front matter carries no "<kind>" settings block; add a top-level "<kind>:" block for that kind, or write "<kind>: {}" / <selector> selects agent kind "<kind>", but the "<kind>" key in the workflow front matter holds <found> where a block of settings was expected; write the kind's settings as keys under "<kind>:", or write "<kind>: {}"Add a top-level block for the named kind, or turn the existing value into a mapping. <kind>: {} or a bare <kind>: key is enough. Fires only when dispatch.default.agent or a dispatch.rules[i].agent names a registered kind that differs from the top-level agent.kind. The first message is skipped when every selector of that kind is a rule that carries its own <kind> settings block, since no attempt reads the top-level block alone; the second fires whenever the top-level key holds a value that is not a mapping. An unregistered kind is reported separately as agent_adapter, and agent.kind itself is never covered.
dispatch.stage.collisiondispatch rule "<name>" (dispatch.rules[<i>].stage): stage label "<label>" collides with <source>; a stage label must not equal a tracker state or a label Sortie applies to issuesRename the rule’s stage label so that it differs, ignoring case, from the value <source> names: an active or terminal state (the tracker adapter’s fallback list counts when tracker.active_states or tracker.terminal_states is empty), tracker.handoff_state, tracker.in_progress_state, tracker.no_change_state, a reactions.<kind>.escalation_label, or the parking label (reactions.review_comments.escalation_label, else needs-human). One line is reported for each staged rule and each value it equals. See dispatch.
config.dispatch.partialsconfig: dispatch.partials: expected a list of paths, got <type>Write dispatch.partials as a list of paths, or remove the key. See partials.
config.dispatch.partials[<i>]config: dispatch.partials[<i>]: expected a path, got <type> / config: dispatch.partials[<i>]: expected a path, got no value / config: dispatch.partials[<i>]: needs a pathWrite entry <i> as a non-empty string: a file path or a glob pattern.
config.dispatch.partials[<i>]config: dispatch.partials[<i>]: partial path must be relative to WORKFLOW.md / config: dispatch.partials[<i>]: partial path escapes the workflow directory tree / config: dispatch.partials[<i>]: partial <path> escapes the workflow directory treeWrite the entry as a path relative to the directory that holds WORKFLOW.md, without a leading / or ~, and keep it inside that directory. The last form means a file the entry selects is a symlink whose target lies outside the directory. Point the link at a file inside it. See partials.
config.dispatch.partials[<i>]config: dispatch.partials[<i>]: matches no file / config: dispatch.partials[<i>]: malformed glob pattern: <details>Correct the pattern so it selects at least one file. Files whose names start with a dot are skipped unless the last path segment of the pattern starts with one.
config.dispatch.partials[<i>]config: dispatch.partials[<i>]: cannot read partial <path>: <details> / config: dispatch.partials[<i>]: partial <path> must be a regular fileCreate the missing file or fix its permissions, or remove a directory or other special file from the selection.
template_parsetemplate parse error in <file>: read partial: <details> / template parse error in <file>: partials must not carry front matterMake the file readable, or remove the --- front matter block from the partial. A partial holds only define blocks. A pattern that selects WORKFLOW.md raises the front matter error, so narrow the pattern. See partials.
template_parsetemplate parse error in <file> (line <n>): only define blocks, white space, and comments may appear outside a define blockMove the text or action in the partial into a define block. A dispatch.partials pattern that selects a prompt template file raises this error too, so narrow the pattern.
template_parsetemplate parse error in <file> (line <n>): template "<name>" is already defined in <file> / template parse error in <file> (line <n>): template name "prompt" is reserved for the prompt templateKeep one definition of <name> across the partials and the template, and do not name a block in a partial prompt. <file> in the first position holds the later definition.
template_parsetemplate parse error in <file> (line <n>): calls template "<name>", which is not defined / template parse error in <file> (line <n>): calls template "<name>", which is not defined, reached from <file>Define <name> in a partial or in the template, or correct the name in the call. Every branch is checked, including one no issue takes. The first <file> holds the call, and the second names the template that reaches it. See partials.
template_parsetemplate parse error in <file> (line <n>): template: <name>:<n>: <details>The template text is not valid: an unclosed action, a mismatched delimiter, or an unknown function. Open <file> at the line the message gives. The (line <n>) part is absent when the line is unknown. See how to write a prompt template.
agent.kind.session_resume<kind>.<key> stops this agent kind from resuming a session across separate agent launches, but Sortie resumes an issue's earlier session on a continuation, including one queued before a restart, and on the retry after a stall while Sortie keeps running, and every such turn fails. Change <kind>.<key>, or use an agent kind that can resume a session.Change the named key, or select an agent kind that resumes sessions. claude-code.session_persistence: false is the only value any built-in adapter declares this way; remove it or set it to true. See Claude Code validate-time checks.

Preflight validation reports all failures at once in a single dispatch preflight failed: ... line.

The sortie validate subcommand runs these same checks without starting the orchestrator, and additionally emits advisory warnings for front matter issues (unknown keys, sub-keys, type mismatches), template problems (dot-context misuse in {{ range }}/{{ with }}, unknown variables, unknown sub-fields), and a configuration value that cannot reach the agent it is written for. Use it in CI pipelines or pre-commit hooks to catch configuration errors, typos, and template mistakes before deployment.


Tracker errors

Errors from tracker adapter API calls. They appear in logs with the format tracker: <kind>: <message>.

Three are configuration errors (before any API calls). Six occur at runtime during polling, state transitions, or issue fetches.

Configuration errors

Error kindDescriptionRetryableOperator action
unsupported_tracker_kindThe tracker.kind value has no registered adapter.NoSet tracker.kind to a registered kind. The startup error names every kind the binary registers.
missing_tracker_api_keyThe tracker.api_key field resolved to empty after environment variable expansion.NoSet the environment variable (e.g., SORTIE_JIRA_API_KEY).
missing_tracker_projectThe tracker.project field is absent and the adapter requires it.NoAdd project to the tracker section in WORKFLOW.md.

Runtime errors

Error kindDescriptionRetryableBackoffOperator action
tracker_transport_errorNetwork or connection failure (DNS, TCP timeout, TLS).YesExponentialCheck network connectivity to the tracker endpoint.
tracker_auth_errorAuthentication or authorization failure (HTTP 401/403).No-Verify API key or token and check account permissions.
tracker_api_errorNon-200 HTTP response from the tracker, including rate limiting and 5xx server errors.YesExponentialCheck tracker service status. Usually self-resolves; investigate if persistent.
tracker_not_foundThe requested resource does not exist (HTTP 404).No-Verify the project key and issue identifiers in your configuration.
tracker_payload_errorMalformed or unexpected response body from the tracker.No-Check tracker API version compatibility.
tracker_missing_end_cursorPagination integrity error: expected cursor missing from response.YesExponentialUsually transient. If persistent, report a bug.

Agent errors

Errors from agent adapter sessions. They appear in logs with the format agent: <kind>: <message>.

Error kindDescriptionRetryableBackoffOperator action
agent_not_foundAgent command or binary not found in PATH. An exit code 127 reports this kind only once the agent has written output; an agent that exits 127 before it responds, a remote binary missing from PATH included, reports the early exit report under port_exit instead.No-Install the agent binary, or set agent.command in WORKFLOW.md. For SSH workers, install the agent on the remote host.
invalid_workspace_cwdThe workspace path does not exist, is not a directory, or has become a symbolic link. Sortie re-checks the path under this error kind immediately before every agent subprocess launch: session start, every turn, and auxiliary commands such as credential verification. Hooks re-check the same path but report through the separate hook validate error instead.No-Check workspace.root permissions and available disk space. If the path is a symbolic link, find what replaced the directory (a hook script, a cleanup job, a manual ln -s) and remove the link.
response_timeoutStartup or synchronous communication timed out before the agent responded.YesExponentialIncrease agent.read_timeout_ms if persistent.
turn_timeoutA turn, including a self-review turn, exceeded the configured agent.turn_timeout_ms and the adapter reported it cancelled. A turn that completed or failed on its own keeps its own outcome, even when it finished after the deadline.YesExponentialIncrease the timeout, or simplify the task so the agent finishes faster.
port_exitAgent subprocess exited unexpectedly (non-zero exit code, pipe failure, crash, or death by a signal Sortie did not send, in which case the message names the signal), or the runtime reported no turn outcome and the adapter had no per-turn process exit to observe. An agent that exits before it responds, whatever its exit status, reports the early exit report: its exit status and the end of what it wrote to standard error.YesExponentialRead the message. An early exit report quotes the agent’s own explanation, such as a switch in agent.command it does not accept, a credential it rejects, or an interpreter it cannot find. For SSH workers, exit code 255 indicates an SSH connection failure. Check connectivity and verify the host is in worker.ssh_hosts.
response_errorAgent returned a protocol-level error response.YesExponentialCheck agent version compatibility with Sortie.
turn_failedAgent turn completed with a failure status (the agent reported its own failure), or the agent exited with code 0 after writing output, without reporting a turn outcome and without producing evidence that the model did any work this turn.YesExponentialReview the agent output in Sortie’s logs for failure details. For no-output failures, check WARN-level logs for the agent’s stderr content. Common causes include MCP config parse errors and missing model configuration.
turn_cancelledTurn was cancelled (reconciliation kill, stall detection, the agent.max_tokens or agent.max_cost_usd in-flight check, or shutdown). The adapter sees one cancellation signal in every case and cannot tell them apart; the run’s recorded status can. Also reported when that cancellation ends a codex, opencode, copilot-cli, or agent-client-protocol session start before its agent runtime began running.No-Expected during reconciliation. No action needed unless frequent outside of shutdown. A run recorded budget_stopped was the token or money ceiling, not reconciliation. See how to control agent costs.
turn_refusedThe runtime declined to continue the turn and reported so through its own protocol; everything from the declined point onward is excluded from what a later attempt sees, so a retry resumes a different conversation rather than the same one. Reported by the Agent Client Protocol adapter.No-Sortie releases the claim rather than scheduling a retry. Read the run’s own output for what the runtime declined and why; narrow the task or the prompt so the same input does not draw the same refusal.
turn_token_limitThe runtime ended the turn because it hit a token limit of its own, so the same input is expected to hit it again. Reported by the Agent Client Protocol adapter.No-Shorten the prompt or the task, or split the work across more turns.
turn_request_limitThe runtime ended the turn because its own request or turn budget within the session was exhausted. Reported by the Agent Client Protocol adapter.YesExponentialA fresh turn starts a new budget on the runtime’s side; if this recurs, the task likely needs more turns than the runtime allows per session.
turn_outcome_unknownThe runtime ended the turn reporting an outcome this client could not interpret: an unrecognized stop reason, a cancelled outcome neither side asked for, or a line exceeding the connection’s own bound. Reported by the Agent Client Protocol adapter.No-Sortie releases the claim rather than scheduling a retry. Check the agent’s own logs for what it reported; this is the runtime producing something the adapter does not yet recognize, not necessarily a failed turn.
turn_input_requiredThe agent asked for a decision only a person could give: a genuine question, or a permission the adapter had no way to refuse and let the turn continue. Sortie refuses rather than answering on a person’s behalf, and ends the attempt instead of waiting. The claim is released, and the run is recorded with status needs_person rather than failed.No-Read the notification event that precedes it: it names what the agent asked for. Then either satisfy the request outside the run (widen the sandbox, supply the missing decision in the issue or the prompt template) or narrow the task so the agent does not need it. Reconfiguring the agent does not remove this ending; every runtime is already launched non-interactively.
turn_incompleteThe runtime ended the turn with a clean exit but without the task-completion report its own protocol defines, so the turn stopped short of the work it was given. Reported by the Copilot CLI adapter when --max-autopilot-continues is reached before a session.task_complete event arrives; no other built-in adapter reports it today.YesExponentialRaise copilot-cli.max_autopilot_continues if the task needs more autopilot steps per turn. The retry starts a new session with a fresh continuation ceiling over the same workspace.
credential_unverifiedEither the credential check failed, or the runtime refused its credential during a working turn. Before the working session’s first turn, a worker attempt that holds no fresh proof of its agent setup sends one fixed request through a short-lived session of its own; the runtime refused it, never answered it, or otherwise failed to complete it. During a working turn, Claude Code, Codex, and agent-client-protocol runtimes that report an authentication failure in their own structured form end the turn with this kind and a message that reads the agent runtime refused its credential: followed by the runtime’s own text. See credential verification for what runs and when.YesExponentialRead the message: it names why the request failed. A stored login or API key that has expired or been revoked needs refreshing; see the environment variables reference for what each kind reads.

This same check can also fail under an error kind of its own instead of credential_unverified. A failure that keeps the check from even starting its session, a missing agent binary (agent_not_found) or an invalid workspace path (invalid_workspace_cwd) most commonly, reports that kind unchanged, because the problem has nothing to do with the credential.

An agent that exits before it responds reports the early exit report under port_exit, including one that rejects its credential only by printing a message and exiting: the message it printed is the diagnosis.

And once the check’s one request is running, a failure whose own kind is non-retryable (for example turn_refused or turn_token_limit) or that is specifically an SSH connection failure (port_exit) also keeps that kind, because it already says something more specific than a credential failure would.

Every other way the request can fail to complete becomes credential_unverified: a retryable failure of a different kind, and, regardless of what else was happening, the request simply taking too long to answer.

Failure text is uniform across coding agents. A turn that exits 0 having written output, but no evidence that the model did any work, reports agent exited without producing output, followed by the signals the adapter looked for and did not find.

An adapter that reads both model-authored content and tool-call activity reports agent exited without producing output: no message from the agent and no tool call, which covers claude-code, copilot-cli, and opencode. A non-zero exit after the agent wrote output reports exit code N, except exit code 127, which reports agent_not_found.

A copilot-cli turn whose result event reports a failed exit reports the message of the last session.error event that turn, which is the model provider’s own message when the provider failed, and non-zero exit in result event when the runtime sent none.

A runtime that reported no turn outcome and gave the adapter no process exit to observe reports runtime reported no turn outcome. A runtime whose protocol defines a task-completion report, but that ended the turn without one, reports agent stopped without reporting the task complete.

Early exit report

An agent that exits before it responds is reported the same way on every agent kind, on the credential check and on a working turn alike. The error kind is port_exit, retried with exponential backoff, and the message reads the agent runtime exited before responding: <status>. <status> is exit status N, or signal: <name> for a process ended by a signal Sortie did not send. The rest of the error text is the end of what the agent wrote to standard error:

agent: port_exit: the agent runtime exited before responding: exit status 2: error: unknown option '--sortie-unknown-switch'
PropertyValue
Line separatorA pipe with a space on either side, between the standard-error lines, which are joined into one line
Size boundThe last 8 KiB of that line
Cut markerA leading [earlier output omitted] when the bound, or the collector’s own line limit, dropped earlier output
Nothing readableno standard-error output in place of the text
CleaningEscape sequences and control characters removed; known credentials shown as [redacted] (see secrets and credential handling)
SSH connection failureA remote launch whose ssh exits 255 reports the SSH connection failure instead

What counts as responding depends on how the agent kind runs its runtime. A kind that starts a new process for every turn has responded once it writes a line on standard output that its adapter accepts as a response: a line the adapter’s decoder parses as a protocol message (claude-code, copilot-cli, opencode).

A line the decoder rejects is not a response, so a turn whose process exits before an accepted line takes this report whatever its exit status, 0 and 127 included, even if it wrote other, unreadable output first.

A kind that keeps one process for the whole session has responded once its startup handshake completes, so a runtime that exits during the handshake takes this report. Each agent adapter reference names its kind’s subprocess model and output format. A process Sortie stopped itself, on cancellation, a timeout, or shutdown, is never reported this way.


Workspace errors

Errors during workspace preparation, hook execution, and the control-file writes that follow.

Path errors

Format: workspace <op>: <details>

Occur when Sortie prepares the per-issue workspace directory.

OperationMeaningOperator action
sanitizeIssue identifier contains characters invalid for a directory name.Check that your tracker returns clean identifiers.
resolveWorkspace root path resolution failed (e.g., ~ expansion).Verify workspace.root is a valid, absolute-resolvable path.
containmentThe computed workspace path escapes the workspace root. This is a security violation. An identifier like ../../etc was used.Investigate the issue identifier in your tracker. This should not happen with legitimate data.
createDirectory creation failed (permission denied, disk full).Check filesystem permissions and available disk space on workspace.root.
statFilesystem stat failed on the workspace path.Check that the path exists and is accessible.
conflictDirectory already exists when Sortie expected to create a fresh workspace.A previous run may not have cleaned up. Remove the conflicting directory manually, or check before_remove hook behavior.

Hook errors

Format: hook <op>: <details>

Occur when lifecycle hook scripts (after_create, before_run, after_run, before_remove) execute.

OperationMeaningOperator action
validateEmpty script body, invalid timeout (non-positive hooks.timeout_ms), or the workspace directory fails re-verification immediately before the hook starts (missing, not a directory, or replaced with a symbolic link).Fix your hook script or set a valid hooks.timeout_ms. For a failed workspace re-verification, find what replaced or removed the directory between workspace preparation and the hook’s start.
startFailed to spawn the hook subprocess (missing shell, permission denied), or, on Windows, to resume it after Sortie assigned it to its Job Object.On POSIX, check that /bin/sh exists and is executable. On Windows, check that cmd.exe is available.
runScript exited with non-zero exit code. The failure WARN record carries the script’s combined stdout and stderr under hook_output (the last 8 KiB).Read hook_output on the WARN record to diagnose the script failure.
timeoutScript exceeded hooks.timeout_ms, or the run it belongs to was cancelled, whether before the script began executing or while it ran.Increase hooks.timeout_ms, or make the hook script faster.

Hook errors in after_create prevent the worker from starting. The error is retryable. Hook errors in before_remove are logged but ignored; workspace cleanup still proceeds.

Control-file errors

Format: mcp config generation: <details>

Occur while the worker writes the MCP tool-server configuration (.sortie/mcp.json and .sortie/.gitignore), after the workspace is ready but before the agent session starts.

DetailMeaningOperator action
<workspace path>: is a symbolic linkThe workspace directory itself was replaced with a symbolic link after Sortie prepared it. Sortie refuses to write .sortie/mcp.json through it.Find and remove whatever replaced the workspace directory with a symlink (a hook script, a prior agent action), then retry.
<workspace path>: is not a directoryThe workspace path exists as a regular file or other non-directory entry.Remove whatever occupies the workspace path so Sortie can use it as a directory again.
<workspace path>: lstat <workspace path>: no such file or directoryThe workspace directory was removed after Sortie prepared it.Find what removed the workspace directory between hook execution and this write, then retry.
<workspace path>: changed while being openedThe workspace directory was replaced between the check and the write (a race, not a stable misconfiguration).Retry. If it recurs, find what is swapping the workspace directory concurrently with Sortie.
.sortie: is a symbolic link.sortie in the workspace is a symbolic link rather than an ordinary directory. Sortie refuses to write through it.Find and remove whatever replaced .sortie with a symlink (a hook script, a prior agent action), then retry.
.sortie: is not a directory.sortie exists as a regular file, not a directory.Remove the file so Sortie can create .sortie as a directory.
.sortie: changed while being opened.sortie was replaced between the check and the write (a race, not a stable misconfiguration).Retry. If it recurs, find what is rewriting .sortie concurrently with Sortie.

Every detail above appears after an open .sortie directory: prefix in the full message. This error is retryable with exponential backoff. A run blocked this way never reaches the agent session.

Workspace preparation on the next attempt recreates a workspace directory that is missing (after_create runs again if configured), so that specific cause usually clears on its own; a workspace path still occupied by a symbolic link or another non-directory entry is rejected there instead, as a workspace conflict error (see Path errors above), so retries continue failing, under that different error, until the entry is removed by hand.


Worker exit kinds

Not errors per se, but essential for understanding session outcomes. Appear in logs as worker exiting exit_kind=<kind>.

Exit kindMeaningWhat happens next
normalTurn loop completed without error.If the tracker reports the issue in a terminal state: handoff suppressed, claim released. If handoff_state configured and issue still active: transition attempt, claim released on success, continuation retry on failure. Where the run’s dispatch rule carries next, that exit adds the stage label of the rule next names instead: no tracker state write, claim released and queued retry cancelled, the other stage labels the issue carried at dispatch removed, and the next stage dispatched once a read shows the added label. It takes the handoff transition after all when the label add fails or the issue’s hop count has reached dispatch.max_consecutive_hops; see the state machine reference for every path. If issue still active with no handoff state configured: continuation retry (1s delay). If issue no longer active: claim released. A .sortie/status soft stop suppresses the continuation retry in every case; blocked also suppresses the handoff transition and, where the dispatch drives issue state, parks the issue with the escalation label and holds it out of dispatch. A run whose handoff-evidence verdict withholds the handoff makes no tracker write; Sortie reads the issue state once more before recording that outcome, and unless that read reports a terminal state the run is recorded as failed rather than succeeded and takes exponential backoff instead of the one-second continuation retry. A terminal result there gives the terminal outcome above instead: handoff suppressed, claim released, no failure record and no retry. A no-change-needed declaration that stood through self-review is exempt from that verdict entirely (it always counts as work observed) and targets tracker.no_change_state instead of handoff_state where that field is set and no stage hop is made; see declaring that nothing needed changing.
errorFatal error during session.If the error is retryable: exponential backoff retry. If not: claim released immediately, the issue becomes re-dispatchable on the next poll cycle.
cancelledCancelled (reconciliation kill, stall detection, the agent.max_tokens or agent.max_cost_usd in-flight check, or shutdown).Claim released unless reconciliation pre-scheduled a retry. No automatic retry: reconciliation handles re-dispatch. The run is recorded cancelled, except when the token or money ceiling’s own cancellation ends it, which is recorded budget_stopped with the tokens used and the ceiling, or the cost and the ceiling, in its error text. A run recorded cancelled carries the reason Sortie cancelled it in its error: stall detection, the issue reaching a final state or leaving the active states, or shutdown. See how to control agent costs.

The logs and the run history database keep the error text as Sortie recorded it, such as handoff withheld: absence of work observed under observed policy or agent turn 1: agent: turn_cancelled: turn cancelled: context canceled. The error of a run and of a retry in the HTTP API, and the retry’s last_error, serve a plain sentence for a withheld handoff, an agent error, a worker stage error, a failed CI check, a budget stop, and the reason a cancelled run was cancelled, such as Sortie stopped the session before the agent finished turn 1. or The agent sent no event within the stall timeout, so Sortie stopped the run. Any other error is served as recorded.


SSH worker errors

When extensions.worker.ssh_hosts is configured, two exit codes carry special meaning.

Exit codeError kindMeaningRetryableOperator action
255port_exitSSH connection failure (refused, timeout, host unreachable).Yes (exponential)Check SSH connectivity. Verify host is in worker.ssh_hosts. Retry prefers the same host but falls back to the least-loaded alternative.
127port_exitRemote agent binary not found in PATH, reported as the early exit report: exit status 127 followed by the remote shell’s own message.Yes (exponential)Install the agent on the remote host. Verify PATH for the SSH user.

HTTP API errors

The JSON API returns errors in a standard envelope:

{
  "error": {
    "code": "issue_not_found",
    "message": "issue identifier \"FOO-999\" not found in current state or run history"
  }
}
CodeHTTP statusMeaning
issue_not_found404Neither the current runtime state nor run history holds the issue identifier.
retry_not_found404POST /api/v1/retries/{identifier}/run found no scheduled retry for the identifier.
reaction_not_found404POST /api/v1/reactions/{identifier}/{kind}/check found no pending reaction of that kind for the identifier.
attention_item_not_found404POST /api/v1/attention/{id}/acknowledge found no current attention item with the id.
run_events_not_retained404GET /api/v1/runs/{dispatch_id}/events has no events for the run: no run carries the dispatch ID, or its events were never kept, pruned, or lost.
invalid_parameter400GET /api/v1/runs, GET /api/v1/runs/issues, GET /api/v1/stats, or GET /api/v1/events rejected a query parameter: unknown, repeated, malformed, out of range, or a misordered pair of bounds. The message names the parameter and the rule.
snapshot_unavailable503Orchestrator state snapshot temporarily unavailable. Retry after a short delay.
history_unavailable503Run history or the issue outcomes could not be read. Retry after a short delay.
cross_origin_request_rejected403A browser sent a state-changing request from a page on another origin, or one Sortie could not confirm came from its own origin. Nothing changed. Which requests are refused, and what a reverse proxy must pass on, is in cross-origin requests.
method_not_allowed405Wrong HTTP method for the endpoint (e.g., POST to a GET-only route). The Allow header indicates the correct method.
acknowledgement_not_saved500POST /api/v1/attention/{id}/acknowledge could not save the acknowledgement. Nothing changed.
retry_now_unavailable503POST /api/v1/retries/{identifier}/run was not taken. Nothing changed.
acknowledge_unavailable503POST /api/v1/attention/{id}/acknowledge was not taken. Nothing changed.
check_now_unavailable503POST /api/v1/reactions/{identifier}/{kind}/check was not taken. Nothing changed.
internal_error500Server-side JSON encoding failure or unexpected error.

For full endpoint documentation, request/response shapes, and curl examples, see HTTP API reference.


Retry behavior

Exponential backoff applies to retryable errors. The next attempt is scheduled with:

delay = min(10000ms × 2^(attempt-1), max_retry_backoff_ms)

With the default max_retry_backoff_ms of 300,000 (5 minutes), the progression is: 10s → 20s → 40s → 80s → 160s → 300s → 300s → …

A refusal by a provider usage limit that reports when it resets is the exception: the retry waits for that reset, past the cap, for at most 7 days.

Non-retryable errors release the claim immediately. The issue becomes dispatchable again on the next poll cycle if it’s still in an active tracker state.

Continuation retries fire after a normal worker exit when max_turns was reached but the issue remains active. These use a fixed 1-second delay with no exponential backoff.

The backoff cap is configurable via agent.max_retry_backoff_ms in WORKFLOW.md.

Was this page helpful?