Skip to content

Errors and exit codes

Exit codes

Exit code Meaning
0 Success (status:"ok"). run returns 0 whenever it could start the child; the child's own code is the exit_code field.
1 General error: bad arguments, missing input, boundary violation, I/O failure, lock timeout, syntax error.
2 A precondition did not hold: a selector matched nothing or more than one place, a count or hash guard did not match, or the target changed before tx commit or undo.
3 A read was cut at a limit (status:"partial"). The output is a success, not an error, and carries no error.code. Writes never return 3.

src/dispatch.lisp maps a command result to these codes: :ok to 0, :partial to 3, and an error to the exit code its error.code has in the catalog below.

The error envelope

Every error is one JSON object on standard error:

{"schema_version":1,"status":"error","command":"edit",
 "error":{"code":"selection.ambiguous","message":"...","exit_code":2,
          "repairs":[{"action":"read","detail":"...","command":"aitools read dup.txt"}],
          "candidates":[{"line":1,"text":"x"},{"line":2,"text":"x"}]}}
  • repairs always has at least one entry. make-error-envelope (packages/core/protocol/src/domain/envelope.lisp) signals an internal error rather than build an envelope without one.
  • repairs[].command is an aitools invocation to run next. Some repairs name only the command to use (aitools read for cat), not a complete invocation.
  • candidates, diagnostics, and conflicts appear only when the error has them.
  • When no command-specific repair applies, the repair is aitools schema <command>.

See JSON output for the field-by-field shape.

error.code catalog

The catalog lives in data/domain/protocol/error-catalog-data.lisp (aitools.data:*protocol-error-codes*); error-code-exit-code in packages/core/protocol/src/domain/error-catalog.lisp reads it. The table below is generated from that data and from the error codes each command schema declares.

error.code Exit code Declared by
argument.invalid 1 search, find, overview, code defs, code refs, read, info, diff, json get, json select, table read, table agg, archive read, snapshot create, edit, insert, replace, apply, transform, move-lines, write, split, transcode, move, copy, delete, mkdir, chmod, link, touch, mktemp, json set, json delete, json merge, json patch, json fmt, table set, archive extract, archive create, history, undo, tx drop, run, wait, bg start, bg logs, bg stop, git diff, git blame, git show, sys tools, time now, time convert, time diff, util encode, util decode, util redact, util tokens, util calc, util uuid, util random, schema, batch
input.not-found 1 search, find, overview, code outline, code defs, code refs, read, info, check, diff, json get, json select, json diff, table read, table agg, archive list, archive read, snapshot create, snapshot diff, edit, insert, replace, apply, transform, move-lines, write, split, transcode, move, copy, delete, mkdir, chmod, link, touch, mktemp, json set, json delete, json merge, json patch, json fmt, table set, archive extract, archive create, undo, tx status, tx diff, tx drop, tx rebase, tx commit, tx abort, wait, bg logs, bg status, bg stop, git log, git diff, git blame, git show, util encode, util decode, util redact, util tokens, schema
input.not-utf8 1 edit, insert, replace, apply, transform, move-lines, json set, json delete, json merge, json patch, json fmt, table set, util encode, util decode, util redact, util tokens, util calc
input.unsupported-format 1 code outline, read, check, json get, json select, json diff, table read, table agg, archive list, archive read, transcode, json set, json delete, json merge, json patch, json fmt, table set, archive extract
input.unsupported-language 1 code outline, read, archive read, edit, insert, replace, transform, move-lines, git blame, git show
input.syntax-error 1 search, read, check, json select, table read, table agg, archive read, edit, insert, replace, transform, move-lines, transcode, json set, json delete, json merge, json patch, json fmt, table set, archive extract, run, wait, bg logs, git blame, git show, time now, time convert, time diff, util decode, util calc, batch
selection.no-match 2 read, archive read, edit, insert, replace, apply, transform, move-lines, json patch, git blame, git show
selection.ambiguous 2 read, archive read, edit, insert, replace, apply, transform, move-lines, json patch, git blame, git show
selection.count-mismatch 2 edit, insert, replace, transform, move-lines
refusal.target-changed 2 edit, insert, replace, apply, transform, move-lines, write, transcode, move, copy, delete, link, json set, json delete, json merge, json patch, json fmt, table set, undo, tx rebase, tx commit, batch
refusal.redacted-input 1 edit, insert, replace, apply, transform, move-lines, write, split, transcode, move, copy, delete, mkdir, chmod, link, touch, mktemp, json set, json delete, json merge, json patch, json fmt, table set, archive extract, archive create, util decode
refusal.outside-workspace 1 edit, insert, replace, apply, transform, move-lines, write, split, transcode, move, copy, delete, mkdir, chmod, link, touch, mktemp, json set, json delete, json merge, json patch, json fmt, table set, archive extract, archive create, run, util decode
refusal.exists 1 edit, insert, replace, apply, transform, move-lines, write, split, transcode, move, copy, delete, mkdir, chmod, link, touch, mktemp, json set, json delete, json merge, json patch, json fmt, table set, archive extract, archive create, run, util decode
refusal.not-a-file 1 read, check, json get, json select, json diff, table read, table agg, archive list, archive read, edit, insert, replace, apply, transform, move-lines, write, split, transcode, move, copy, delete, mkdir, chmod, link, touch, mktemp, json set, json delete, json merge, json patch, json fmt, table set, archive extract, archive create, util decode
refusal.too-large 1 archive list, archive read, copy, archive extract
environment.io 1 search, read, info, check, diff, json get, json select, json diff, table read, table agg, archive list, archive read, snapshot create, snapshot diff, edit, insert, replace, apply, transform, move-lines, write, split, transcode, move, copy, delete, mkdir, chmod, link, touch, mktemp, json set, json delete, json merge, json patch, json fmt, table set, archive extract, archive create, history, undo, tx begin, tx status, tx diff, tx drop, tx rebase, tx commit, tx abort, run, wait, bg start, bg logs, bg status, bg stop, git status, git log, git diff, git blame, git show, util encode, util decode, util redact, util tokens, util calc
environment.busy 1 read, info, check, json get, json select, table read, table agg, archive list, archive read, edit, insert, replace, apply, transform, move-lines, write, split, transcode, move, copy, delete, mkdir, chmod, link, touch, mktemp, json set, json delete, json merge, json patch, json fmt, table set, archive extract, archive create, undo, tx begin, tx drop, tx rebase, tx commit, tx abort, bg start, util decode, batch
environment.timeout 1 wait
environment.unavailable 1 run, wait, bg start, bg logs, bg status, bg stop, git status, git log, git diff, git blame, git show, sys procs, sys ports, time now, time convert, time diff
internal.unexpected 1 search, find, overview, code outline, code defs, code refs, edit, insert, replace, apply, transform, move-lines, write, split, transcode, move, copy, delete, mkdir, chmod, link, touch, mktemp, json set, json delete, json merge, json patch, json fmt, table set, archive extract, archive create, sys info, sys env, sys tools, sys procs, sys ports, time now, time convert, time diff

"Declared by" lists only the commands whose own schema names the code. Three codes can also come from the composition root for any command, whether or not its schema declares them: argument.invalid for usage errors and --lock-timeout values that are not durations, environment.busy when recovery cannot take the workspace lock, and internal.unexpected for an uncaught condition (src/dispatch.lisp).

When each code occurs

error.code Occurs when
argument.invalid Argument parsing fails, a value has the wrong type or range, options that exclude each other are combined, a required guard or input is missing, chmod --mode sets a setuid, setgid, or sticky bit (only the low 9 permission bits are allowed), touch --mtime names a date that does not exist (2024-02-30), a numeric table agg aggregate meets a non-numeric column (the message names the column's type), or the command name is unknown. A malformed --range (3, +3, 3: 5) and a --range, --expect-count, duration, or size spelled with digits outside ASCII 0-9 are wrong values too.
input.not-found A file, symbol, JSON Pointer, archive entry, op, tx, bg process, or snapshot does not exist (a true ENOENT). Files and entries come with candidates. A file that exists but cannot be read is environment.io, not this code.
input.not-utf8 A text edit targets a file that is not valid UTF-8.
input.unsupported-format A file is not in a format the command handles (json, table, archive, check), or an encoding is not supported.
input.unsupported-language The code group, --symbol, or transform --op comment meets a language outside the pattern table.
input.syntax-error check finds a syntax error, a regular expression or diff is malformed, a regex is too complex to evaluate (a read --match, --between, or json select --where pattern that hits the regex step limit reports here, not a dedicated code), a value does not parse (util decode, util calc, time convert), or transcode meets bytes it cannot decode.
selection.no-match A selector matches nothing, an apply hunk does not fit, or a JSON Patch test fails. Similar places come as candidates.
selection.ambiguous A selector that must be unique matches more than once. The matches come as candidates.
selection.count-mismatch --expect-count differs from the actual count.
refusal.target-changed --expect-hash differs, or the target changed before tx commit, tx rebase, or undo. The paths come as conflicts.
refusal.redacted-input A write's input contains the literal text [REDACTED_SECRET].
refusal.outside-workspace A write would land outside the workspace root, through a symlink that leaves it, or inside the workspace's .git/ (its resolved git directory and common directory). archive extract entry paths and copy --recursive children are routed through the same boundary, so a .git/ entry or nested .git/ is refused. The state directory lives outside the root, so writes there are refused as outside-workspace too.
refusal.exists The destination exists and overwriting was not allowed.
refusal.not-a-file A file-only operation targets a directory, a read targets a FIFO, socket, or device (the message says X is not a regular file and the repair is aitools info X), a text edit targets a binary file, delete targets a non-empty directory, or chmod/touch targets a hard-linked file (link count > 1).
refusal.too-large archive extract, archive list, archive read, or copy --recursive exceeds its size or entry limit, or any command's envelope would pass the JSON writer's 16777216-character output limit. In the second case nothing is written to standard output; the error envelope on standard error is the only output, and its repair is aitools schema <command> (ask for less with --max-lines, --limit, or a narrower selector).
environment.io A read or write fails, including a permission-denied (EACCES) read of a file that exists: read, info, check, diff, and write --content-file report this rather than input.not-found for such a file. A failed rename names the path that blocks it, the destination.
environment.busy The workspace lock is not acquired within --lock-timeout. The repair repeats the same command.
environment.timeout wait times out.
environment.unavailable A required external command or OS facility is missing (git outside a repository, no source for sys ports, an argv that cannot start).
internal.unexpected An unexpected condition. The repair is aitools schema <command> when the command is known.

Unknown command names

A name that is not a registered command returns argument.invalid, exit code 1. src/dispatch.lisp chooses the repairs by the kind of name, in this order:

  1. A bare group name (json, bg, tx) is a real command node with no handler, so the message is <group> requires a subcommand and the repair is aitools schema <group>. That repair is runnable: aitools schema lists a group's subcommands when given the group name (%group-schemas in src/schema.lisp), so it no longer fails with input.not-found.
  2. A group subcommand typed without its group (get, status, logs, uuid) returns one run-instead repair per group whose subcommand list holds that name, each the grouped form of the invocation with the group token inserted and the globals and operands kept. This is driven by the live registry (%group-subcommand-repairs in src/dispatch.lisp), not by the correspondence table, so status returns aitools bg status, aitools git status, and aitools tx status.
  3. A shell command name in the correspondence table (cat, grep) returns that row's repairs, drawn from aitools.data:*correspondence-table* and shown in Using aitools from an agent.
  4. Any other name gets the repair aitools schema.
$ aitools cat hello.lisp
{"schema_version":1,"status":"error","command":"cat","error":{"code":"argument.invalid","message":"unknown command cat","exit_code":1,"repairs":[{"action":"run-instead","detail":"Read a file with line numbers and range control.","command":"aitools read"}]}}

$ aitools uuid
{"schema_version":1,"status":"error","command":"uuid","error":{"code":"argument.invalid","message":"unknown command uuid","exit_code":1,"repairs":[{"action":"run-instead","detail":"`uuid` is the `util` group's subcommand; include the group name.","command":"aitools util uuid"}]}}

schema also reports near-miss candidates as an agent types them (json get, not the dotted dispatch name json.get); %typed-name in src/schema.lisp builds them from the command schemas.