trace_id in both lanes and stamps run_id on every span in each — so a broken run is one search, not two disconnected ones.The broker (pramaan-functions src/clerk_worker/runner.py) already injects W3C trace context as
meta.traceparent on the agent_invoke.v1 envelope, and the LLM lane already adopts it: agents/atlas/observability/identity.py's
agent_invoke_observability_context extracts the carrier and attaches it before contextvars.copy_context(),
so the copied brain-thread context carries the remote parent (Story-1439 / 1463 proved the Logfire-lane spans join it). The GAP: nothing
taught the GENERAL lane the same trick, and the run_id dimension landed on a general-lane span only when a
manual_span happened to pass it explicitly.
traceparent again in the general lane and start a root span from it."
That re-implements context propagation a second time, fights ADOT for the global provider, and risks a set_tracer_provider override
(forbidden — pramaan-docs ADR-104, ONE tracing system). The trace continuity already rides the single attach + copy_context()
in stream_invoke — it is lane-agnostic OTel context, so the general lane inherits it for free. The only thing actually missing was the
ambient enricher on the global provider.The LLM lane gets the converged AmbientIdentitySpanProcessor on its dedicated provider (in configure_observability).
The general lane — the ADOT-owned global TracerProvider — never got it. observability/identity.py adds the
one wiring function that fixes that, without ever owning or overriding the global provider.
# agents/atlas/observability/identity.py def configure_general_lane_identity(provider=None) -> bool: # enrich the EXISTING global provider; never construct, never set_tracer_provider (ADR-104) target = provider or _otel_trace.get_tracer_provider() add = getattr(target, "add_span_processor", None) if not callable(add): # API default proxy (no ADOT) -> clean no-op (local/CI) return False add(AmbientIdentitySpanProcessor()) # the SAME single converged enricher the LLM lane uses return True # idempotent (warm-container guard), never raises
Now run_id / firm_id / form_id / model_id / agent.key land on every general-lane span at on_start — including ADOT
auto-instrumented spans the brain never touches — sourced exactly as ADR-124 §D10 requires (run_id/form_id from allowlisted baggage;
firm_id from the process-local ContextVar, never baggage). main.py calls it once at cold start,
unconditionally (not gated on LOGFIRE_TOKEN): the general lane is correct even with the Logfire lane off.
It is the same attach for both. runtime.py stream_invoke wraps the context capture so the
remote parent rides into the brain thread; the general-lane manual_span (via the global tracer) and the LLM-lane Pydantic-AI span
both create children of that one attached parent — so both inherit the upstream trace_id.
# agents/atlas/runtime.py — ONE extraction, both lanes ride it with agent_invoke_observability_context(meta, model_id=config.model_id): ctx = contextvars.copy_context() # remote parent now in the copied brain-thread context brain = threading.Thread(target=lambda: ctx.run(_pump), ...) # both lanes' spans descend from it
| Lane | Provider | Adopts upstream trace | run_id on every span |
|---|---|---|---|
| LLM (Logfire) | dedicated, X-Ray id gen | via attach (Story-1439) | enricher on dedicated provider |
| GENERAL (ADOT/X-Ray) | global (ADOT-owned) | via the SAME attach | enricher on global provider (this Story) |
traceparent must still mint an X-Ray-format (epoch-prefixed)
root, so an orphan run still joins the X-Ray/CloudWatch lane. The enricher only stamps attributes at on_start — it touches no
id_generator. The general lane's root id generator is ADOT's own AwsXRayIdGenerator (the dedicated LLM lane already
builds its own). Nothing in this Story changes id minting, so the fallback is untouched by construction.agents/atlas/tests/test_atlas_observability.py gains four tests mirroring the existing #1439/#1463 harness (real OTel SDK + in-memory exporters + the broker→AgentCore thread hop; no network):
# both lanes share ONE trace_id == the injected parent, run_id on EVERY span root = ATLAS_INVOKE # general lane (manual); auto = adot-auto-span # general lane (raw tracer, NO explicit run_id) chat = pydantic-ai chat span # LLM lane assert root.trace_id == parent and auto.trace_id == parent and chat.trace_id == parent # ✓ one id, both lanes assert auto.attributes["run_id"] == "run-1843" # ✓ stamped by the enricher, not an explicit pass # no inbound traceparent -> general-lane root keeps the X-Ray-format fallback _assert_xray_format_trace_id(root.trace_id, captured_at=now) # ✓ epoch-prefixed, not W3C-random
The adot-auto-span is the sharp proof: it is created through the raw global tracer with no explicit attributes, so its
run_id can come only from the enricher on the global provider — exactly the gap this Story closes. Two focused tests pin the
wiring itself (added once, idempotent; clean no-op when ADOT is absent / the provider has no add_span_processor).
ruff clean on agents/atlas and tests/ scripts/.
pytest 618 passed in agents/atlas (4 new + existing) and 146 passed governance.
brain-egress R12 gate clean — no new egress, no baggage write outside the helper, no set_tracer_provider,
no provider construction or X-Ray-SDK outside the bootstrap. Observability ONLY: the user-facing answer path is byte-identical
(pramaan-docs ADR-110 thin brain).
attach into the brain thread for both lanes — Story-1843
adds the missing ambient enricher to the ADOT global provider so run_id lands on every general-lane span too, making one
trace_id open both telescopes while the no-parent X-Ray-format fallback stays untouched.