# Observability The `src/main/ai/observability/` subsystem: OTel tracing, the local span projection, and the sink registry. "Trace / telemetry" is the user-facing surface; this doc covers the whole subsystem. ## What's instrumented Every AI SDK call run through Cherry produces an OpenTelemetry span tree: ``` chat.turn (root, created by context provider) ├── ai.streamText (AI SDK auto) │ ├── ai.streamText.doStream (AI SDK auto) │ ├── ai.toolCall (per tool invocation) (AI SDK auto) │ └── ai.streamText. (AI SDK auto) └── attributes: topicId, modelName, … (set by AiTurnTrace / AdapterTracer) ``` AI SDK's `experimental_telemetry` produces the inner spans; Cherry owns the root span through `AiTurnTrace` so it lands in the same observability path without going through the AI SDK adapter. The main-process observability boundary is `src/main/ai/observability`: - `core/` creates Cherry-owned turn roots and common `cs.*` attributes. - `adapters/aiSdk/` interprets AI SDK child spans. - `adapters/claudeCode/` interprets Claude Code OTLP spans and logs. - `storage/` keeps the in-memory span projection and JSONL-compatible history. - `sinks/` defines the extension point for local and future external export. ## Local history flush The container `traceId` is persisted on the topic / agent-session row (one trace tree per container), but the span tree is first collected in the main-process `TraceStorageService` memory store. The durable history file is written by the stream terminal path: - `PersistentChatContextProvider` attaches a `TraceFlushListener` to normal chat turns. - `AgentSessionRuntimeService` attaches the same listener to `agent-session:${sessionId}` turns, including queued follow-up turns. - On the topic-level terminal event (`done`, `paused`, or `error`), `TraceFlushListener` calls `TraceStorageService.saveSpans(topicId)`. - Flush errors are logged as warnings and do not affect message completion. Collection and persistence are main-process only. Spans live in `TraceStorageService`'s in-memory store and are flushed to the JSONL history file on the terminal event. The renderer trace viewer (`TracePage`) reads the persisted spans on demand through the `trace.getData` IPC — it never collects spans itself. ## AdapterTracer `src/main/ai/observability/adapters/aiSdk/adapterTracer.ts` wraps the OTel `Tracer` returned by the global provider. On every `startSpan` / `startActiveSpan` it: 1. Patches `span.end()` to also call `AiSdkSpanAdapter.convertToSpanEntity(...)` and hand the result to the observability sink registry. 2. Stamps `trace.topicId` and `trace.modelName` so the main-side `TraceStorageService` can key spans per topic. `AdapterTracer` is intentionally only for AI SDK child spans: - `buildTelemetry` (`runtime/aiSdk/params/buildTelemetry.ts`) — passed to AI SDK as `experimental_telemetry.tracer`. Captures every AI SDK auto-span. Returns `undefined` (no telemetry, no tracer) when there is no `topicId` or developer mode is off — see below. ## AiSdkSpanAdapter `src/main/ai/observability/adapters/aiSdk/aiSdkSpanAdapter.ts` converts an OTel span into the `SpanEntity` shape `TraceStorageService` stores and persists: - Reads span name, attributes, events, status, links. - Recovers AI SDK's hierarchical attribute conventions: `ai.xxx` is a level, `ai.xxx.yyy` is a sub-level under it. - Normalises usage attributes across the base and LLM spans, preferring AI SDK v6 keys with legacy and semantic-convention fallbacks: input from `ai.usage.inputTokens` / `ai.usage.promptTokens` / `gen_ai.usage.input_tokens` (and the single `ai.usage.tokens` embeddings shape), output from `ai.usage.outputTokens` / `ai.usage.completionTokens` / `gen_ai.usage.output_tokens`, plus `ai.usage.totalTokens`, `ai.usage.cachedInputTokens`, and `ai.usage.reasoningTokens` (emitted as `completion_tokens_details.reasoning_tokens`). Claude Code Agent SDK spans do not go through `AiSdkSpanAdapter`; they are converted by `src/main/ai/observability/adapters/claudeCode/ClaudeCodeOtlpAdapter.ts`. ## Sensitive data capture & redaction > Cross-referenced from `ClaudeCodeTraceBridgeService.prepareTrace`. The Claude Code OTLP bridge runs **only when developer mode is enabled**. When it does, it intentionally turns on verbose Claude Code telemetry: - `OTEL_LOG_USER_PROMPTS` — user prompt text - `OTEL_LOG_TOOL_DETAILS` / `OTEL_LOG_TOOL_CONTENT` — tool calls and their content - `OTEL_LOG_RAW_API_BODIES` — raw API request/response bodies These payloads land in span attributes that `TraceStorageService` persists as **plaintext JSONL trace files on disk**, so a trace can contain secrets (authorization headers, API keys embedded in raw bodies) alongside the prompt and tool content. **Redaction is deliberately not done.** Stripping secrets would mean parsing arbitrary OTLP attribute structures across the ingest path and would risk dropping legitimate trace data. The accepted tradeoff is that capture is **local-only and developer-gated**; turning that into a redaction/threat-model guarantee is a deferred decision. Treat exported trace files as sensitive. ## Developer-mode gating Dev mode only. The span projection (`TraceStorageService`) is built and persisted in the main process; the renderer trace viewer (`TracePage`) reads it on demand via the `trace.getData` IPC. Outside developer mode `buildTelemetry` returns `undefined`, so **no tracer is attached at all** and the AI SDK emits no spans — there is nothing to project, and the viewer shows an empty trace. ## Where to read more - Code: `src/main/ai/observability/` - Span projection: `src/main/ai/observability/storage/TraceStorageService.ts` - AI SDK telemetry docs: https://ai-sdk.dev/docs/reference/ai-sdk-core/telemetry