Skip to content

[Feature]: Add gen_ai.conversation.id for cross-turn session correlation in OTel export #8933

Description

@FTDGRT

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

  1. Add session_id: Option<String> to the 8 variants (not RagRetrieve).
  2. 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.
  3. 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).
  4. 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."
  5. 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

  • I removed personal/sensitive data from examples, payloads, and logs.
  • I used neutral, project-focused wording and placeholders.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions