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.
| Check | Log output | Action |
|---|---|---|
workflow_load | workflow 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.kind | tracker.kind is required | Add tracker.kind to your WORKFLOW.md front matter. |
tracker_adapter | unknown tracker adapter kind "<kind>"; registered: [<list>] | Set tracker.kind to one of the kinds the message lists. |
tracker.api_key | tracker.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.project | tracker.project is required for tracker kind "<kind>" | Add the project field to the tracker section. |
tracker.project.format | tracker.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.kind | agent.kind is required | Add agent.kind to your WORKFLOW.md front matter. |
agent_adapter | unknown agent adapter kind "<kind>"; registered: [<list>] | Set agent.kind to one of the kinds the message lists. |
agent.command | agent.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.command | config: agent.command: expected string or list, got <type> / config: agent.command: must not be an empty list | Write 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.command | config: agent.command[<i>]: expected string, got <type> / config: agent.command[<i>]: must not be empty | Element <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_ms | config: agent.turn_timeout_ms: must be greater than 0 | Set agent.turn_timeout_ms to a positive number of milliseconds, or remove the field to use the default. |
agent.max_cost_usd | config: 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_usd | config: <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 name | Set 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_* override | Set 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_review | Set 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_ms | config: 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_state | tracker.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_state | tracker.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_states | Set 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_states | config: tracker.terminal_states: must name at least one state for tracker kind "<kind>": a closed issue reads as its first entry | tracker.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 space | An 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_evidence | tracker.handoff_evidence: must be one of observed, strict, or off | Set the field to observed, strict, or off, or leave it unset for the default observed. |
tracker.in_progress_state | tracker.in_progress_state: "<val>" is not in active_states / collides with terminal state / collides with handoff_state | in_progress_state must be in active_states, must not be in terminal_states, and must not equal handoff_state. |
tracker.comments | tracker.comments: expected map, got <type> | The comments value must be a YAML map, not a scalar or list. |
tracker.comments.on_dispatch | tracker.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>.escalation | config: reactions.<kind>.escalation: must be "label", "comment", or "none", got "<value>" | Use label, none, or the deprecated comment. See escalation actions. |
config.notifications[<i>].events | config: 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 once | Remove the repeat. |
config.notifications[<i>].events[<j>] | config: notifications[<i>].events[<j>]: agent.message cannot be posted as a tracker comment | Remove agent.message from the tracker_comment entry. Messages from the agent never reach the issue. |
config.notifications[<i>].events | config: notifications[<i>].events: a tracker_comment entry must list its events; write [] to post only what the deprecated settings enable | Add events to the entry. events: [] posts only the comments that the deprecated tracker.comments flags and escalation: comment enable. |
config.notifications[<i>].kind | config: notifications[<i>].kind: only one notifications entry may have kind tracker_comment | Merge the entries into one. |
config.notifications[<i>].kind | config: notifications[<i>].kind: kind tracker_comment requires a configured tracker | Set tracker.kind, or remove the entry. |
config.notifications[<i>].<key> | config: notifications[<i>].<key>: kind tracker_comment takes only kind and events | Remove the key from the tracker_comment entry. This includes max_per_session. |
config.notifications[<i>].kind | config: 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 required | A 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 1 | A 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_table | config: price_table: expected map, got <type> | Write price_table as a map, or remove it. |
config.price_table.embedded | config: price_table.embedded: expected boolean, got <type> | Write true or false. |
config.price_table.fetch | config: price_table.fetch: expected boolean, got <type> | Write true or false. |
config.price_table.fetch_url | config: 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_url | config: price_table.replace_url: expected string, got <type> | Write the address as a string, or remove the key. |
price_table.fetch_url | config: price_table.fetch_url: resolved to empty (check environment variable) | Set the environment variable the address references, or remove the key. |
price_table.replace_url | config: price_table.replace_url: resolved to empty (check environment variable) | Set the environment variable the address references, or remove the key. |
price_table.replace_conflict | config: price_table: replace_file and replace_url are mutually exclusive | Keep one of price_table.replace_file and price_table.replace_url. See price_table. |
price_table.replace_file | config: 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.interactive | codex.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 supported | Set codex.approval_policy: never, or remove the field. See Codex validate-time checks. |
claude-code.permission_mode.interactive | claude-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 supported | Set 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.collision | dispatch 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 issues | Rename 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.partials | config: 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 path | Write 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 tree | Write 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 file | Create the missing file or fix its permissions, or remove a directory or other special file from the selection. |
template_parse | template parse error in <file>: read partial: <details> / template parse error in <file>: partials must not carry front matter | Make 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_parse | template parse error in <file> (line <n>): only define blocks, white space, and comments may appear outside a define block | Move 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_parse | template 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 template | Keep 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_parse | template 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_parse | template 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 kind | Description | Retryable | Operator action |
|---|---|---|---|
unsupported_tracker_kind | The tracker.kind value has no registered adapter. | No | Set tracker.kind to a registered kind. The startup error names every kind the binary registers. |
missing_tracker_api_key | The tracker.api_key field resolved to empty after environment variable expansion. | No | Set the environment variable (e.g., SORTIE_JIRA_API_KEY). |
missing_tracker_project | The tracker.project field is absent and the adapter requires it. | No | Add project to the tracker section in WORKFLOW.md. |
Runtime errors
| Error kind | Description | Retryable | Backoff | Operator action |
|---|---|---|---|---|
tracker_transport_error | Network or connection failure (DNS, TCP timeout, TLS). | Yes | Exponential | Check network connectivity to the tracker endpoint. |
tracker_auth_error | Authentication or authorization failure (HTTP 401/403). | No | - | Verify API key or token and check account permissions. |
tracker_api_error | Non-200 HTTP response from the tracker, including rate limiting and 5xx server errors. | Yes | Exponential | Check tracker service status. Usually self-resolves; investigate if persistent. |
tracker_not_found | The requested resource does not exist (HTTP 404). | No | - | Verify the project key and issue identifiers in your configuration. |
tracker_payload_error | Malformed or unexpected response body from the tracker. | No | - | Check tracker API version compatibility. |
tracker_missing_end_cursor | Pagination integrity error: expected cursor missing from response. | Yes | Exponential | Usually transient. If persistent, report a bug. |
Agent errors
Errors from agent adapter sessions. They appear in logs with the format agent: <kind>: <message>.
| Error kind | Description | Retryable | Backoff | Operator action |
|---|---|---|---|---|
agent_not_found | Agent 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_cwd | The 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_timeout | Startup or synchronous communication timed out before the agent responded. | Yes | Exponential | Increase agent.read_timeout_ms if persistent. |
turn_timeout | A 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. | Yes | Exponential | Increase the timeout, or simplify the task so the agent finishes faster. |
port_exit | Agent 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. | Yes | Exponential | Read 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_error | Agent returned a protocol-level error response. | Yes | Exponential | Check agent version compatibility with Sortie. |
turn_failed | Agent 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. | Yes | Exponential | Review 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_cancelled | Turn 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_refused | The 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_limit | The 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_limit | The runtime ended the turn because its own request or turn budget within the session was exhausted. Reported by the Agent Client Protocol adapter. | Yes | Exponential | A 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_unknown | The 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_required | The 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_incomplete | The 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. | Yes | Exponential | Raise 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_unverified | Either 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. | Yes | Exponential | Read 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'| Property | Value |
|---|---|
| Line separator | A pipe with a space on either side, between the standard-error lines, which are joined into one line |
| Size bound | The last 8 KiB of that line |
| Cut marker | A leading [earlier output omitted] when the bound, or the collector’s own line limit, dropped earlier output |
| Nothing readable | no standard-error output in place of the text |
| Cleaning | Escape sequences and control characters removed; known credentials shown as [redacted] (see secrets and credential handling) |
| SSH connection failure | A 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.
| Operation | Meaning | Operator action |
|---|---|---|
sanitize | Issue identifier contains characters invalid for a directory name. | Check that your tracker returns clean identifiers. |
resolve | Workspace root path resolution failed (e.g., ~ expansion). | Verify workspace.root is a valid, absolute-resolvable path. |
containment | The 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. |
create | Directory creation failed (permission denied, disk full). | Check filesystem permissions and available disk space on workspace.root. |
stat | Filesystem stat failed on the workspace path. | Check that the path exists and is accessible. |
conflict | Directory 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.
| Operation | Meaning | Operator action |
|---|---|---|
validate | Empty 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. |
start | Failed 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. |
run | Script 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. |
timeout | Script 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.
| Detail | Meaning | Operator action |
|---|---|---|
<workspace path>: is a symbolic link | The 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 directory | The 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 directory | The 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 opened | The 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 kind | Meaning | What happens next |
|---|---|---|
normal | Turn 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. |
error | Fatal 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. |
cancelled | Cancelled (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 code | Error kind | Meaning | Retryable | Operator action |
|---|---|---|---|---|
255 | port_exit | SSH 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. |
127 | port_exit | Remote 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"
}
}| Code | HTTP status | Meaning |
|---|---|---|
issue_not_found | 404 | Neither the current runtime state nor run history holds the issue identifier. |
retry_not_found | 404 | POST /api/v1/retries/{identifier}/run found no scheduled retry for the identifier. |
reaction_not_found | 404 | POST /api/v1/reactions/{identifier}/{kind}/check found no pending reaction of that kind for the identifier. |
attention_item_not_found | 404 | POST /api/v1/attention/{id}/acknowledge found no current attention item with the id. |
run_events_not_retained | 404 | GET /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_parameter | 400 | GET /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_unavailable | 503 | Orchestrator state snapshot temporarily unavailable. Retry after a short delay. |
history_unavailable | 503 | Run history or the issue outcomes could not be read. Retry after a short delay. |
cross_origin_request_rejected | 403 | A 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_allowed | 405 | Wrong HTTP method for the endpoint (e.g., POST to a GET-only route). The Allow header indicates the correct method. |
acknowledgement_not_saved | 500 | POST /api/v1/attention/{id}/acknowledge could not save the acknowledgement. Nothing changed. |
retry_now_unavailable | 503 | POST /api/v1/retries/{identifier}/run was not taken. Nothing changed. |
acknowledge_unavailable | 503 | POST /api/v1/attention/{id}/acknowledge was not taken. Nothing changed. |
check_now_unavailable | 503 | POST /api/v1/reactions/{identifier}/{kind}/check was not taken. Nothing changed. |
internal_error | 500 | Server-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?