scout and the mission chrono-canary. <run_id> stands for the 32-character run id XORCISE printed when you created the run.
What XORCISE ingests
XORCISE runs an embedded OTLP/HTTP receiver with two routes,POST /v1/traces and POST /v1/logs. Both matter. Claude Code’s assistant prose arrives on logs, and Codex’s entire narrative is logs-only, so a harness configured for traces alone produces a gutted replay or none at all. There is no gRPC receiver; JSON and protobuf are both accepted, and gzip is decompressed for you.
Every batch must carry the run id. XORCISE reads the xorcise.run_id OpenTelemetry resource attribute first — a span attribute does not correlate — and falls back to grepping the xorcise.run_id= marker out of the payload, which only works for the batches that happen to echo the connect prompt. A batch carrying neither is dropped, never blended into another run.
The port is the one xorcise up printed: 4318, unless it was busy and XORCISE moved up. Read the endpoint from the run’s launch profile rather than assuming it. The otel field on an agent record is stored and never read.
Running without telemetry
A run with no telemetry is fully valid and quantifiably safe. Your agent still joins the tailnet, submits artifacts over run-control, and callsPOST /complete; grading runs at completion. Deterministic checks are unaffected, because they read submitted artifacts and observed facts, not traces. The judge still runs, with an explicit note that no execution trace is available, and the criteria it cannot evidence are left unscored: they count as zero in the score, judge_status reads partial, and the result carries an upper bound that grants the unscored weight. What you lose is the replay — the timeline, terrain map, and event feed are empty — and any rubric criterion that depends on how the agent worked.
1. Read the run’s launch profile
The endpoint is computed per run and served by the brain, so no harness has to guess it.launch-cmd takes the launch mode; ask for host when your agent runs in a terminal. To keep the variables in a file, use xorcise run launch-profile <run_id> > launch.env — it writes the same values as dotenv lines, but takes no launch mode and returns the container profile for generic or a kind of your own.
kind you registered. For an agent with no built-in harness, that is the generic three:
Output
OTEL_EXPORTER_OTLP_ENDPOINT is a base URL. A standard OpenTelemetry SDK appends /v1/traces and /v1/logs itself; Codex does not, which is why its configuration looks different.
2. Export the variables before your agent starts
A bareKEY=VALUE paste is not inherited by the child process, so the agent starts with no exporter configured and nothing arrives.
Output
OTEL_RESOURCE_ATTRIBUTES for your own service metadata, append xorcise.run_id=<run_id> to it comma-separated rather than replacing it.
3. Configure your harness
XORCISE ships three built-in harness adapters plus a generic floor. You should see the block below that matches thekind on your agent record; xorcise run launch-cmd emits it for you, already filled in with this run’s id and endpoint.
Claude Code
kind: claude-code. The strongest coverage of the three: traces and logs, with resource-attr correlation. Claude Code’s SDK honours the standard OpenTelemetry variables, so the whole configuration is environment.
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 is the beta gate: without it there are no spans at all. OTEL_LOGS_EXPORTER=otlp opens the logs signal, which is where the assistant’s text and the user prompt live — a headless run never exports the interaction span, so logs are the only source. OTEL_LOG_ASSISTANT_RESPONSES=1 is what stops the response arriving as <REDACTED> and being dropped. OTEL_RESOURCE_ATTRIBUTES is the correlation. The 1000 ms export intervals exist so a short headless run flushes before it exits.
The block also exports XORCISE_RUN_BEARER, the run’s token. It is not telemetry: the prompt tells your agent to read the token from that variable, so the token stays off the command line. See Keep the token off command lines. --permission-mode bypassPermissions is the only mode a headless claude -p run can finish in, since there is no one to answer its permission prompts, so run it inside a sandbox.
Codex
kind: codex. Codex’s OpenTelemetry is driven by its own config, and it ignores every OTEL_EXPORTER_* environment variable. The exporter configuration rides -c overrides on the command, layered in memory per invocation — nothing is written to ~/.codex/config.toml and there is nothing to clean up.
otel.trace_exporter.otlp-http.*, logs under otel.exporter.otlp-http.*. Each endpoint carries the full per-signal path, because Codex does not append /v1/traces or /v1/logs itself. protocol = "binary" means http/protobuf. otel.log_user_prompt=true is Codex’s analogue of OTEL_LOG_USER_PROMPTS; without it the prompt logs as [REDACTED] and the replay cannot show the task. And OTEL_RESOURCE_ATTRIBUTES is the only telemetry environment variable that matters — the run id must be a resource attribute, since a span attribute does not correlate. XORCISE_RUN_BEARER is the run’s token for run-control, exported for the same reason as in the Claude Code block.
Codex’s replay is built entirely from the logs signal. Its trace adapter returns nothing by design, suppressing several hundred contentless runtime spans, so a Codex run with the logs endpoint misconfigured produces an empty replay even though spans are arriving. Codex’s telemetry also carries no assistant prose or reasoning text at all — expect the full action narrative without the model’s answers.
OpenHands
kind: openhands. OpenHands has a replay adapter and a launch command but no telemetry provider, so it falls back to the generic environment and to prompt-sentinel correlation. It is not the default kind: an agent registered without a harness is saved as generic, not as this, and the register form preselects nothing.
XORCISE_RUN_BEARER is the run’s token, exported as in the Claude Code block. Because correlation is best-effort, only the batches that echo the connect prompt attach to the run; deleting the xorcise.run_id= marker from the prompt means nothing attaches. OpenHands contributes traces only — it has no logs mapping. Configure its model and credentials before you launch, and expect its exporter to gzip every export, which XORCISE accepts.
If you drive the OpenHands SDK from your own Python rather than through the CLI, export the variables before your code imports openhands.sdk. The SDK starts its tracing once, at import, and that tracing is what carries the prompts, commands and outputs. Exported later, it stays off for the whole process, and only the spans your own code emits arrive. The launch hand-off shows the same tip.
Any other harness
Ageneric or unrecognised kind gets the generic adapter, the generic three variables, and prompt-sentinel correlation. The generic adapter classifies spans by name and attributes and still produces a readable feed.
4. Confirm events are arriving
Check the receiver first, then the run.Output
drops counts what the receiver has refused since XORCISE last started, and all zeros is the healthy reading. The receiver refuses a batch for one of two reasons, and each reason has its own counters:
Nothing else is counted. A batch whose
xorcise.run_id names a run that does not exist is not refused: it is stored under that id, and no run reads it. A malformed body gets a 400, and a database too busy to take the batch gets a 503, which exporters retry; neither is counted.
Each refusal also writes one warning line to the server’s error output, which is ~/.xorcise/serve.log when xorcise up started it, such as otlp drop: signal=traces reason=unroutable spans=34 run_id=- spooled=no. The line names the run for a sealed refusal, but carries no timestamp. The counters live in memory, so they start again from zero whenever XORCISE restarts, and neither xorcise status nor the web UI shows them.
To keep the refused batches themselves, set otel_drop_spool_enabled = true in config.toml and restart XORCISE. Each refused batch is then written as one JSON file under ~/.xorcise/otel-dropped/, with the time it arrived, its signal, the reason, the run id when it named one, and the batch as received. The spool keeps the newest 200 files and at most 64 MiB, deleting the oldest first, and a single batch bigger than that budget is counted but not written. It is a diagnostic, not evidence: nothing in XORCISE reads the files back, and a spooled batch never joins a run. The receiver takes no credential, so with the spool on, anyone who can reach the OTLP port can fill it up to those bounds. Configuration keys lists the four settings.
Then read what has landed against this run.
Output
5. Take the run into your own OTel stack
XORCISE stores the raw OTLP it received, unmodified, and will hand it back in the Collector’s own file format:./xorcise-run-<id8>-otlp.jsonl — spans first, then logs, one OTLP/JSON envelope per line, with no XORCISE framing of any kind. Point an OpenTelemetry Collector at it with the otlpjson file receiver and the run flows on to Jaeger, Tempo, or whatever else you already run. The result page offers the same file as OTLP Traces (.jsonl), and the endpoint behind both is GET /api/runs/{run_id}/otlp.jsonl.
It works mid-run: the evidence stores are append-only with whole-batch commits, so a snapshot taken while the run is live is a valid prefix of the final stream, and it is labelled as partial.
This is the export to use when you want the run in your own tooling. For a readable per-event view instead, xorcise run events export <run_id> writes the normalized stream.
To take a set of finished runs at once, xorcise run export --out <dir> writes each one’s raw stream to <dir>/<id8>/traces.otlp.jsonl, the same bytes, beside its normalized events, report and result. Filter by --mission, --agent or --since; see xorcise run export.
Troubleshooting
No events appear
Work down this list in order.- Wrong host. This is the single most common cause. Container mode bakes
host.docker.internal, which a plain terminal cannot resolve, so the exporter fails silently and nothing arrives. Ask for host mode:xorcise run launch-cmd <run_id> host. An agent that really does run in a container needs--add-host host.docker.internal:host-gateway. - The variables were not exported. A pasted
KEY=VALUEline is not inherited by the child process. Useset -a; source launch.env; set +a, or run the whole blocklaunch-cmdprinted. - The profile was empty. If
xorcise run launch-profileprints nothing, no collector host is configured for this instance. - Codex only. Setting
OTEL_EXPORTER_OTLP_ENDPOINTdoes nothing; Codex ignores it. The endpoints must be the-cflags, with the full/v1/tracesand/v1/logspaths. - Claude Code only. Without
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1there are no spans at all. - The receiver is unreachable.
GET /healthzon the OTLP port should return200with"status":"ok"and thedropscounters. A500with{"code": 12, ...}means the protobuf decoder is missing; reinstall withpip install --force-reinstall xorcise. A400with{"code": 3, "message": "malformed OTLP body"}means the payload itself is bad. - The exporter logs a
503.{"code": 14, "message": "telemetry store unavailable, retry"}, with aRetry-Afterheader, means the database was too busy to take the batch in time. Nothing was lost: exporters retry a503, and the retry is stored normally. A batch that carries several runs can be partly stored before the503, and its retry then stores that part a second time. It is transient. If it keeps happening, something else is holding the database, such as a long backup or another process writing toxorcise.db.
Events appear on the wrong run, or on no run
The receiver tells you directly. An export that cannot be routed comes back with a partial-success body:Output
rejectedLogRecords. The message says missing/unknown, but only a missing run id is refused: a batch naming a run that does not exist is stored under that id, and no run reads it. Causes, in order of likelihood:
- The run id is a span attribute, not a resource attribute. Only
resource.attributesis read. Set it throughOTEL_RESOURCE_ATTRIBUTES. - You overwrote your resource attributes. Append
xorcise.run_id=<run_id>comma-separated instead of replacingservice.nameand friends. - The prompt marker was stripped. For OpenHands and any generic harness the
xorcise.run_id=line in the connect prompt is the only correlation channel. - The run is already sealed. Sealing closes the control plane immediately and keeps OTLP admissible only for the drain window,
telemetry_drain_seconds, five seconds by default, so a harness that batches slowly loses its tail afterPOST /complete. Late spans are rejected by design, not merged, and a run sealed without them stays that way. Shorten the harness’s export interval so its last batch lands inside the window, or raise the setting; only a new run captures a tail that was already rejected. - The prompt came from an older run. Its marker names that run’s id.
/healthz counts the same refusals by reason, as step 4 shows: unroutable_* covers the first three causes, and sealed_* covers a run that had already ended, including an older run whose prompt you reused. ~/.xorcise/serve.log has one otlp drop: line per refused batch.
Only tool calls, no reasoning
- Claude Code. Missing
OTEL_LOG_TOOL_DETAILS=1orOTEL_LOG_TOOL_CONTENT=1gives tool cards with no content. MissingOTEL_LOG_ASSISTANT_RESPONSES=1makes the response arrive as<REDACTED>, and XORCISE drops it. MissingOTEL_LOGS_EXPORTER=otlpremoves the assistant text and the user prompt together. - Codex. Assistant prose and reasoning content are never available — its telemetry carries token counts, not text. Without
otel.log_user_prompt=trueyou also lose the task itself. - OpenHands. It emits no logs mapping at all, and drops a duplicate tool call when a dedicated action span already covers the command.
- The wrong
kind. If events render as raw span names with no grouping, the generic adapter was selected. Slugs are hyphenated:claude-code, notclaude_code. A friendly label inxorcise agent listis not an adapter —gemini,goose,cai, andlangchainrender a display name and nothing more.
The replay warns about unclassified spans or missing content
These are the two telemetry warnings. Both describe what the harness sent, so neither can be fixed for a run already recorded: fix the harness, then start a new run.span(s) matched no classification rule. The generic adapter rendered the run and recognised none of those span names. It looks for keywords in the name, such asshell,exec,command,bash,tool,read,write,edit,search,think,messageandllm. If your harness is Claude Code, Codex or OpenHands, register the agent with thatkindso its own adapter is used. Otherwise the spans are shown as they arrived, and nothing was dropped.carries a content-bearing attribute. Telemetry arrived with names and ids but no commands, output or text, so the judge graded from the submitted artifacts alone. For Claude Code, check theOTEL_LOG_*variables above. For the OpenHands SDK, export the variables before importingopenhands.sdk. For a harness of your own, put the content in attributes whose key ends in a segment the judge’s distiller keeps, such ascommand,output,stdout,prompt,content,text,inputorpath. A key underprocess.,code.,host.,service.or a few other namespaces that describe the harness itself is never kept, soprocess.commanddoes not count.
Next steps
Traces and events
How XORCISE replays what arrives — the timeline, the terrain map, and the event feed.