ANSWERS · OPERATIONS: EXIT CODES

The indexer exited 3 (or 4). Did it fail?

AUTHOR · XERJ documentation team · REVIEWED BY · XERJ engineering team · PUBLISHED · 2026-08-22 · UPDATED · 2026-09-20

TL;DR — Exit 3 is a success. xerj autoindex exits 0 when the run completed, and 3 when it completed and recorded junk. Exit 4 means the estimate exceeded --max-minutes and nothing was indexed. Exit 2 is a usage error, and exit 1 is an endpoint or journal failure. Only 4 is a question.

PASTE THIS TO YOUR AI AGENT — IT DOES THE REST Act as a coding agent. Read https://xerj.org/llms.txt, run xerj autoindex on the folder, then branch on the process exit code before you report anything: 0 and 3 are finished runs, 4 is a decision request on stdout that you answer with --approve, 2 is a bad command line, and 1 is a real failure to retry or escalate.
RUN THIS XERJ COMMAND xerj autoindex --help Print the exit-code contract from the binary you actually installed.
RUN THIS XERJ COMMAND xerj autoindex ./corpus --url http://127.0.0.1:9200 --prefix ax --state-dir ./state --progress plain A normal run. Exit 0 means nothing was refused; exit 3 means it finished and refused something.
RUN THIS XERJ COMMAND xerj autoindex ./corpus --url http://127.0.0.1:9200 --prefix ax --state-dir ./state --max-minutes 1 --progress plain Set a low budget so the estimate can trip the decision gate and exit 4.
RUN THIS XERJ COMMAND xerj autoindex ./corpus --url http://127.0.0.1:9200 --prefix ax --state-dir ./state --approve proceed --progress plain Answer a decision request by re-running the same command with --approve.
RUN THIS XERJ COMMAND xerj autoindex status --url http://127.0.0.1:9200 --state-dir ./state Read the journal when the process is gone and you have no terminal line.

The codes, as the binary states them

The list below is quoted from xerj autoindex --help on the installed binary. Run that command yourself before you write a branch on any of these numbers.

ExitName in --helpWhat it means for the corpus
0completeThe run finished and refused nothing. A gate answered with --approve cancel also exits 0.
3completed-with-junkThe run finished. At least one file was refused and recorded. Never fatal. This includes a whole dataset whose mapping the server refused.
4NEEDS A DECISIONThe estimate exceeded --max-minutes. Nothing was indexed. A JSON decision request is on stdout.
2usageThe command line was wrong. Nothing ran.
1endpoint/journal failureThe node or the resume journal failed, a corpus removal was refused, or an unsafe state transition was refused.

Read the table in that order rather than in numeric order. It is the order the help text uses, and it is the order of decreasing goodness.

Exit 3 is a finished run

3 completed-with-junk (junk recorded, never fatal) is the phrase in the help text, and never fatal is doing the work in that sentence.

A folder of real files almost always contains something XERJ will not parse. An unknown binary, an image, a file above --max-file-gb. Those files are recorded in the catalog with a reason, and the rest of the folder is indexed and queryable.

An agent that treats a non-zero exit as a failure will therefore throw away a perfectly good index. Branch on the specific value.

The companion check is the refusal list, not the exit code: read autoindex-catalog and match each refused path to its reason. The skipped-files page covers that query.

Exit 3 can also mean a whole dataset is missing

The server can refuse the mapping of one dataset with an HTTP 400. That used to abort the entire run with exit 1 and index nothing. It now costs that one dataset: its files are recorded as junk with the server's reason, every other dataset is indexed, and the run exits 3.

The terminal line says so, and only when it happened:

xerj-done ok=true exit=3 reason=completed-with-junk wall=0.7s files=1 records=1 generation=1 datasets_refused=1 files_refused=2 code_files=0 code_files_indexed=0 code_files_junked=0

So an exit 3 is still a finished run, but it is not always a small gap. Read datasets_refused before you report a corpus as searchable. The refused-dataset page covers what is recorded and how to recover.

Only a 400 is a refusal. A 401, 403, 404, 408, 429 or 5xx on the same request says nothing about that dataset, so it still exits 1.

A 429 on a bulk is different, whether it is the whole request or some of its items marked status: 429. When the node's memory circuit breaker answers that way, the run re-sends the rejected items after a backoff and carries on; it exits 1 only after 600 seconds of re-sends the node never takes, with an error line that begins the server kept rejecting. The terminal line of a run that met back-pressure and finished carries bulk_retries=N. The back-pressure page covers it.

Exit 4 is a question, and nothing was written

Phase A reads and parses every file to sniff and sample it, so it measures throughput per format family on the machine you are on. It turns that into a range for the indexing phase, and the gate compares the upper end of the range against --max-minutes (default 10).

If the upper end is longer and no --approve or --yes was given, nothing is indexed. A JSON decision request goes to stdout and the process exits 4.

You answer it by re-running the same command with --approve proceed, --approve fast or --approve cancel. fast also applies --no-semantic --no-graph.

A person at a terminal is prompted instead, but only when the question can actually be seen: stdin a terminal, stderr a terminal, and the progress surface on. A piped or agent-driven run is never prompted, and --quiet silences the prompt as well. Every un-prompted run behaves identically — the JSON request goes to stdout, which --quiet does not silence, and the process exits 4. The payload's prompt_not_offered_because names which of the three conditions was missing.

xerj autoindex never waits on stdin for a question it did not print.

Exit 1 and exit 2 are narrower than "any error"

The common shorthand is that 1 means any error. The binary is more specific, and the difference matters to a retry policy.

2 usage is a bad command line. Nothing ran, and a retry of the same command will fail the same way.

1 is an endpoint or journal failure, a refused corpus removal, or a refused unsafe state transition. The last two are refusals by design, not crashes: removing an indexed file is refused before any remote mutation, because its documents are already live and nothing on that path deletes them.

So a 1 is not always something to retry. Read the error line first.

Two exit-1 endings that name themselves

On the --no-graph path, a run whose node answered its writes with HTTP 429 for the whole 600 seconds a bulk waits ends with a reason of its own instead of aborted:

autoindex: stopped by server back-pressure while applying <file>: N operation(s) are journaled applied, M are not (this one first) — the same command resumes from here once the node accepts writes again
xerj-done ok=false exit=1 reason=server-backpressure wall=… ops_applied=N ops_remaining=M

Forced on a real node with a 64 MiB memory cap, that line read xerj-done ok=false exit=1 reason=server-backpressure wall=609.1s ops_applied=0 ops_remaining=231. The applied operations are journaled, so the same command resumes with the M that remain once the node accepts writes again. On that node, restarted on its default cap, it did. It is exit 1 and not 3 on purpose: 3 means a finished run with nothing to retry, and this generation is not finished. The back-pressure page covers the node side.

A request the node calls too large is no longer an exit 1 at all. Until issue #955, both indexing paths sent the catalog as one _bulk request, one document per file, per dataset and per run. A corpus whose catalog held more than the engine's limits.max_actions_per_bulk (50,000 by default) applied every operation and then failed at the very end. On a 48,533-file corpus the --no-graph run ended 10,336 seconds in:

xerj-done ok=false exit=1 reason=aborted wall=10336.0s
error: prepared bulk contained 1 rejected items: {"type":"engine_exception","reason":"bulk request contains 102258 lines (~51129 actions); exceeds max_actions_per_bulk of 50000","status":413}

Every _bulk body now goes out in windows of at most 10,000 actions. A request the node still refuses for its size is cut in two and sent again, and the terminal line carries bulk_splits=N when that happened. Against real nodes with max_actions_per_bulk = 64 and with max_body_bytes = 98304, each run ended ok=true exit=3 records=1663 bulk_splits=1, the same records as a control run. Resuming that same generation with the change committed it:

xerj-done ok=true exit=3 reason=completed-with-junk wall=415.0s files=47444 records=821840 generation=1 code_files=34324 code_files_indexed=34324 code_files_junked=0

One record that is larger than the node's limits.max_body_bytes cannot be cut, and it still ends the run with exit 1 and an error that names that setting.

Do not read the code alone

Every run that reaches an exit, success or error, ends with one terminal line in every progress mode except none.

xerj-done ok=true exit=3 reason=completed-with-junk wall=0.6s files=4 records=1 generation=1

That line carries the reason string, the wall time and the code-coverage counters code_files, code_files_indexed and code_files_junked. A corpus whose source files were all dropped therefore cannot print the same line as a healthy one.

Two cases print no terminal line at all. --quiet and --progress none print none by design, so poll xerj autoindex status --state-dir <dir> or read the exit code instead of waiting for output that never comes. A run killed by a signal cannot print one either — a missing terminal line after the process is gone means it died, not that it finished.

What an agent should do with each code

ExitCorpus stateNext action
0indexed, nothing refused (or cancelled on purpose)Read the terminal line, then query.
3indexed, something refusedRead datasets_refused on the terminal line, query, then read autoindex-catalog for the refusals.
4untouchedRe-run the same command with --approve proceed, fast or cancel.
2untouchedFix the command line. Do not retry unchanged.
1partial or untouchedRead the error, check the node and the journal, then decide.

Pair this with the four-number reconciliation on the completeness page. The exit code tells you how the process ended; only the counts tell you what is in the index.

How this page was checked

The codes above were read from xerj autoindex --help on a built binary and cross-checked against the help string in engine/crates/xerj-autoindex/src/cli.rs. Both agree.

One code was forced for this page: the exit 3 of a refused dataset, on a throwaway node, by pre-creating the dataset's index with a conflicting field type. That capture is committed under benchmarks/autoindex-resilience/, together with the full-corpus exit 1 in finalize-catalog, the exit 3 of the same generation resumed after issue #955 was fixed (before-955.stderr.txt, after-955.full-corpus-resume.stderr.txt), and a reason=server-backpressure exit 1 forced with a 64 MiB memory cap (limits-real-node.txt). The other codes were not forced, so there is no capture for them here and no timing. If you want the codes on your own build, xerj autoindex --help prints them in one screen.

FAQ

What does indexer exit code 3 mean?

It means the run completed and recorded junk. xerj autoindex --help states it as 3 completed-with-junk (junk recorded, never fatal). Treat it as a success with a refusal list to read.

The indexer exited 3 (or 4). Did it fail?

3 did not fail. 4 did not fail either, and it did not index anything: it is a decision request waiting for --approve. Only 1 and 2 are failures.

Why did folder indexing stop and ask me to approve?

The measured phase-A estimate for the indexing phase was longer than --max-minutes (default 10). With no --approve or --yes, nothing is indexed, a JSON decision request goes to stdout and the process exits 4.

How do I tell a failed index from a completed one with junk files?

Read the exit code, not the log volume. 0 and 3 are both finished runs; 3 additionally refused at least one file, or a whole dataset whose mapping the server answered HTTP 400 to, in which case the xerj-done line carries datasets_refused and files_refused. 1 is an endpoint or journal failure and nothing about it is a junk report.

Is exit 1 the code for any error?

No, and that shorthand is wrong. --help scopes 1 to an endpoint or journal failure, a refused corpus removal, or a refused unsafe state transition. A bad command line is 2. On the --no-graph path, an exit 1 whose terminal line reads reason=server-backpressure means the node rejected every write for the whole 600 s a bulk waits; the line carries ops_applied and ops_remaining.

Does exit 0 always mean the folder was indexed?

No. Answering the decision gate with --approve cancel also exits 0, and that run indexes nothing on purpose. Read the terminal line or the journal before you report a corpus as searchable.

Why did my run print no terminal line at all?

Either --quiet or --progress none was in force, which prints no terminal line, or the process was killed by a signal. A missing terminal line after the process is gone means it died, not that it finished.

Which codes should an agent retry?

Retry nothing on 0 or 3. Re-run with --approve on 4. Fix the command line on 2. Investigate the endpoint and the journal on 1, because a blind retry repeats the same refusal. The one exit 1 to rerun unchanged is reason=server-backpressure, once the node accepts writes again: it resumes with the operations that remain.

Evidence

Related