Summary
Add a session_id field to 8 turn-lifecycle ObserverEvent variants (AgentStart, AgentEnd, LlmRequest, LlmResponse, ToolCallStart, ToolCall, MemoryRecall, MemoryStore), exported via OTel as gen_ai.conversation.id; rename the existing zeroclaw.turn_id to gen_ai.turn.id.
Problem statement
#7385 established single-turn span correlation via turn_id (gen_ai.agent.invoke parent span + llm.call/tool.call children). #6641's Non-goals explicitly excluded cross-turn session correlation. Nothing today links multiple turns of the same conversation — each turn is its own trace, and operators reconstruct conversations by hand from timestamps and channel/sender metadata. gen_ai.conversation.id is the OTel GenAI semantic convention's field for exactly this.
Proposed solution
- Add
session_id: Option<String> to the 8 variants (not RagRetrieve).
- Thread
session_id as an explicit field, same as turn_id today: ToolLoop/TurnCtx/TurnMeta gain the field, each construction site fills it from a value it already holds locally (history_key, session_key, memory_session_id, or process_message's existing session_id parameter). No Tokio task-local reads — see Alternatives.
- In
OtelObserver, replace every KeyValue::new("zeroclaw.turn_id", ...) with two lines: gen_ai.turn.id (renamed existing value) + gen_ai.conversation.id (new value).
render_memory_context gets a new session_id: Option<&str> parameter, distinct from the existing multi-scope sessions list — one is "which conversation this call belongs to," the other is "which memory partitions to search."
- Extend the
assert_all_events_share_turn_id-style regression helpers in both agent.rs and loop_.rs (two independently maintained copies) with the same existence + consistency checks for session_id.
Non-goals / out of scope
Acceptance criteria
- The 8 variants carry
session_id: Option<String>.
- Every turn-minting entry point populates it without adding new task-local scoping.
- OTel exports
gen_ai.turn.id + gen_ai.conversation.id.
- Non-OTel Observers (Prometheus/Verbose/Noop/eval) compile unchanged.
- Both
agent.rs and loop_.rs regression tests cover session_id.
- Two-turn e2e check: both traces share one
gen_ai.conversation.id, each keeps a distinct gen_ai.turn.id.
Architecture impact
zeroclaw-api::observability_traits (ObserverEvent), zeroclaw-runtime::agent::turn (TurnCtx/TurnMeta/ToolLoop), agent.rs/loop_.rs/tool_execution.rs/memory_inject.rs, construction sites in the channel orchestrator / gateway WS / ACP / RPC turn paths, observability/otel.rs, zeroclaw-log/observer_bridge.rs.
Risk and rollback
High: The risk comes from blast radius — session_id threads through the main runtime turn-lifecycle files (agent.rs, loop_.rs, turn/*, tool_execution.rs, memory_inject.rs) and every construction site across channel orchestrator / gateway WS / ACP / RPC turn paths, plus the regression tests in both agent.rs and loop_.rs need matching updates. All new ObserverEvent fields are additive Options and non-OTel Observers are unaffected, so the risk is scope/coordination, not compatibility.
Breaking change?
Yes, OTel span attribute rename only — no config/CLI impact.
Related work
Data hygiene checks
Summary
Add a
session_idfield to 8 turn-lifecycleObserverEventvariants (AgentStart,AgentEnd,LlmRequest,LlmResponse,ToolCallStart,ToolCall,MemoryRecall,MemoryStore), exported via OTel asgen_ai.conversation.id; rename the existingzeroclaw.turn_idtogen_ai.turn.id.Problem statement
#7385 established single-turn span correlation via
turn_id(gen_ai.agent.invokeparent span +llm.call/tool.callchildren). #6641's Non-goals explicitly excluded cross-turn session correlation. Nothing today links multiple turns of the same conversation — each turn is its own trace, and operators reconstruct conversations by hand from timestamps and channel/sender metadata.gen_ai.conversation.idis the OTel GenAI semantic convention's field for exactly this.Proposed solution
session_id: Option<String>to the 8 variants (notRagRetrieve).session_idas an explicit field, same asturn_idtoday:ToolLoop/TurnCtx/TurnMetagain the field, each construction site fills it from a value it already holds locally (history_key,session_key,memory_session_id, orprocess_message's existingsession_idparameter). No Tokio task-local reads — see Alternatives.OtelObserver, replace everyKeyValue::new("zeroclaw.turn_id", ...)with two lines:gen_ai.turn.id(renamed existing value) +gen_ai.conversation.id(new value).render_memory_contextgets a newsession_id: Option<&str>parameter, distinct from the existing multi-scopesessionslist — one is "which conversation this call belongs to," the other is "which memory partitions to search."assert_all_events_share_turn_id-style regression helpers in bothagent.rsandloop_.rs(two independently maintained copies) with the same existence + consistency checks forsession_id.Non-goals / out of scope
RagRetrieve— independent hardware-retrieval path, not part of this.turn_idor parent-span nesting added to memory/RAG events — that's feat(obs): nest memory and RAG spans under the turn trace #8752 (open PR).process_message's lifecycle restructure (feat(obs): add process_message turn lifecycle for memory span nesting #8844's scope).Acceptance criteria
session_id: Option<String>.gen_ai.turn.id+gen_ai.conversation.id.agent.rsandloop_.rsregression tests coversession_id.gen_ai.conversation.id, each keeps a distinctgen_ai.turn.id.Architecture impact
zeroclaw-api::observability_traits(ObserverEvent),zeroclaw-runtime::agent::turn(TurnCtx/TurnMeta/ToolLoop),agent.rs/loop_.rs/tool_execution.rs/memory_inject.rs, construction sites in the channel orchestrator / gateway WS / ACP / RPC turn paths,observability/otel.rs,zeroclaw-log/observer_bridge.rs.Risk and rollback
High: The risk comes from blast radius —
session_idthreads through the main runtime turn-lifecycle files (agent.rs,loop_.rs,turn/*,tool_execution.rs,memory_inject.rs) and every construction site across channel orchestrator / gateway WS / ACP / RPC turn paths, plus the regression tests in bothagent.rsandloop_.rsneed matching updates. All newObserverEventfields are additiveOptions and non-OTel Observers are unaffected, so the risk is scope/coordination, not compatibility.Breaking change?
Yes, OTel span attribute rename only — no config/CLI impact.
Related work
turn_idand span parent/child nesting (merged)turn_idto memory/RAG (open PR)process_messageturn lifecycle restructure; does not block this proposalData hygiene checks