ADR-0033: Trace links on runs and revisions¶
Status: Accepted Date: 2026-09-28 Deciders: Alex Nodeland
Context¶
RFC-0002 has a run record the OpenTelemetry trace id of each attempt, so that feedback on a turn or an artifact version can be attached to the trace it is about, and says SQL storage adds a migration for it. Three details were left open:
- Where the trace id comes from. Core is sans-IO (ADR-0001) and knows nothing of OpenTelemetry. The agent layer knows the trace of each attempt, because its
wrap_runruns inside it. - Which attempt wrote an artifact version. A run that pauses on a question and resumes runs twice, in two traces. Feedback on a version the first attempt wrote belongs to the first trace, and nothing in a run tells the attempts' writes apart.
- The migration. SQL storage keeps runs and revisions as the JSON of their Pydantic models (ADR-0021), beside only the columns that reads filter on.
Decision¶
run_startedcarries the attempt's trace id, set by the agent layer when the run is traced. Core appends it toRun.trace_ids, oldest first, and keeps the list across pauses and ends. The log therefore holds every link, and the run entity stays derivable from the log.- A revision carries the trace it was committed in (
Revision.trace_id), stamped by the workspace from the current span when it saves the revision, as it stamps an outcome'sseq. Feedback on a version then finds its trace directly, whichever attempt, surface or person wrote it. - No migration. Both fields live in the stored JSON and default to empty for rows written before them. No read filters on them, so they get no columns; a migration would change nothing. If a lookup from a trace to its run is needed later, it gets a table and a migration then.
Amendment (2026-09-29): envelopes carry the trace context¶
stackr RFC-0002 bridges artifactr's events into reflexr, and a run a bridged event starts should link back to the turn or request that caused it (#60). reflexr's envelopes carry traceparent for this; artifactr's did not.
Envelope.traceparentis the W3C trace context of the span that committed the event, orNone. It has the name, format and meaning of reflexr's field, andcurrent_traceparent()is reflexr's function. A trace id alone would not do: a link needs the span.- The workspace sets it from the current span as it saves: a command's
artifactr.commitspan, or whatever span is current for a recorded fact.Transaction.savetakes it beside the actor, as reflexr'sEntrydoes, so aStorageimplemented elsewhere must store it too. - No migration. It is stored in the envelope's JSON, and nothing filters on it. The protocol stays
artifactr.v1, with one new optional field.
The table below turned down a trace id on every envelope because finding a version's trace would mean scanning the log. That still holds for feedback. A follower of the log reads every envelope anyway.
Options considered¶
| Option | Precise for resumed runs | Schema change |
|---|---|---|
Trace ids on run_started and revisions, in JSON (chosen) |
Yes | None |
| Trace ids on runs only | No: a version maps to a run, not an attempt | None |
A trace_ids column on artifactr_runs |
No | A redundant copy of the JSON |
| A trace id on every envelope | Yes | None, but finding the event for a version or message means scanning the log |
Consequences¶
- Easier: turn feedback goes to the run's traces and artifact feedback to the exact trace of the write, with no extra reads.
- Easier: clients see trace ids in the log and in revisions, and can link to them.
- Harder:
trace_idis a field a host sets on a core model, likeseqon outcomes; hosts other thanWorkspacemust stamp it themselves.
Action items¶
-
run_started.trace_idandRun.trace_ids, with conformance cases. - The agent layer sets the trace id, and the workspace stamps revisions (RFC-0002 phase A1).
- Envelopes carry
traceparent(2026-09-29 amendment).