TL;DR
Session.ts (ACP/daemon path) calls chat.sendMessageStream() directly, bypassing GeminiClient.sendMessageStream() — which is where all session-tracing spans (interaction, tool, tool.execution), event logging (user_prompt, conversation_finished, tool_call, file_operation), and session.id attribute propagation live. Result: daemon-mode traces contain only bare qwen-code.llm_request spans with context=standalone, making them nearly unusable for debugging session-level issues.
The daemon code already acknowledges this gap — Session.ts:1871-1877:
"Mirrors the subagent/plan/arena branches in GeminiClient.sendMessageStream (client.ts:848-878) — the ACP path bypasses that code … porting it into the ACP path is tracked separately as part of the broader middleware-alignment work."
This issue tracks the telemetry side of that alignment.
Evidence (real production trace)
Trace 9b8ba2063e4f070eeff98e6a9ce67dcd from cn-zhangjiakou (2026-05-28, 57.6 min, 60 spans):
- 60/60 spans =
qwen-code.llm_request with context=standalone
- 0
user_prompt / tool / tool.execution / interaction / conversation_finished events
- 0
session.id attribute on any span
- 3 different user sessions interleaved in one trace (only distinguishable by parsing
prompt_id format {sessionId}########{turn})
- By contrast, a CLI-mode session of similar complexity produces ~100-200 spans across 8+ operation types with full hierarchy
What's missing vs CLI mode
| Span / Event |
CLI mode |
Daemon mode |
Why missing |
qwen-code.interaction |
✅ |
❌ |
startInteractionSpan() called in GeminiClient.sendMessageStream(), daemon bypasses this |
qwen-code.tool / .tool.execution |
✅ |
❌ |
CoreToolScheduler wires startToolSpan(); daemon has its own tool dispatch loop without span hooks |
qwen-code.user_prompt (log-bridge) |
✅ |
⚠️ logUserPrompt() is called (Session.ts:698), but the resulting log-bridge span lands under a different traceId, not co-located with the llm_request spans |
|
qwen-code.conversation_finished |
✅ |
❌ |
Emitted by GeminiClient turn-end logic |
qwen-code.tool_call / file_operation |
✅ |
❌ |
Emitted by CoreToolScheduler event hooks |
session.id span attribute |
✅ (on interaction + log-bridge spans) |
❌ |
llm_request span attributes don't include session.id; no interaction span to carry it; no log-bridge spans co-located |
Impact
-
Observability blind spot: daemon serves the majority of production user sessions (DataWorks Data Agent, web UI), but its traces are the least informative — can't see user intent, tool execution, turn boundaries, or which session a span belongs to.
-
Multi-session trace collision: all sessions in one daemon process share one traceId. Without session.id on spans and without interaction span boundaries, correlating "this LLM call belongs to user X's turn 3" requires parsing prompt_id string format — fragile and not queryable via ARMS Tags.
-
Debugging regression: issues like "session stuck", "wrong tool called", "model didn't finish turn" — which we can diagnose in ~2 minutes from a CLI trace — require manual log file analysis for daemon sessions because the trace lacks the necessary spans.
Suggested approach
Two progressive milestones, either independently valuable:
Milestone 1: span attribute parity (small diff)
Add session.id to llm_request / tool / tool.execution span attribute lists in session-tracing.ts. This is a 3-line change per span type and immediately makes daemon spans queryable by session.
Milestone 2: middleware alignment (larger, the tracked work)
Wire Session.ts's message-handling path through the same startInteractionSpan → CoreToolScheduler → endInteractionSpan pipeline that GeminiClient.sendMessageStream() uses, so daemon traces get the full span hierarchy. This is the "broader middleware-alignment work" referenced in the Session.ts comment.
A middle ground: keep Session.ts's own tool dispatch loop but add explicit startToolSpan() / endToolSpan() calls at the entry/exit points. This avoids the full GeminiClient refactor while restoring tool-level observability.
Affected
daemon_mode_b_main branch (production daemon deployment)
packages/cli/src/acp-integration/session/Session.ts:1304 — the chat.sendMessageStream() call that bypasses GeminiClient
packages/core/src/telemetry/session-tracing.ts — span attribute definitions (Milestone 1)
Related
TL;DR
Session.ts(ACP/daemon path) callschat.sendMessageStream()directly, bypassingGeminiClient.sendMessageStream()— which is where all session-tracing spans (interaction,tool,tool.execution), event logging (user_prompt,conversation_finished,tool_call,file_operation), andsession.idattribute propagation live. Result: daemon-mode traces contain only bareqwen-code.llm_requestspans withcontext=standalone, making them nearly unusable for debugging session-level issues.The daemon code already acknowledges this gap —
Session.ts:1871-1877:This issue tracks the telemetry side of that alignment.
Evidence (real production trace)
Trace
9b8ba2063e4f070eeff98e6a9ce67dcdfrom cn-zhangjiakou (2026-05-28, 57.6 min, 60 spans):qwen-code.llm_requestwithcontext=standaloneuser_prompt/tool/tool.execution/interaction/conversation_finishedeventssession.idattribute on any spanprompt_idformat{sessionId}########{turn})What's missing vs CLI mode
qwen-code.interactionstartInteractionSpan()called inGeminiClient.sendMessageStream(), daemon bypasses thisqwen-code.tool/.tool.executionCoreToolSchedulerwiresstartToolSpan(); daemon has its own tool dispatch loop without span hooksqwen-code.user_prompt(log-bridge)logUserPrompt()is called (Session.ts:698), but the resulting log-bridge span lands under a different traceId, not co-located with thellm_requestspansqwen-code.conversation_finishedqwen-code.tool_call/file_operationsession.idspan attributellm_requestspan attributes don't includesession.id; no interaction span to carry it; no log-bridge spans co-locatedImpact
Observability blind spot: daemon serves the majority of production user sessions (DataWorks Data Agent, web UI), but its traces are the least informative — can't see user intent, tool execution, turn boundaries, or which session a span belongs to.
Multi-session trace collision: all sessions in one daemon process share one traceId. Without
session.idon spans and without interaction span boundaries, correlating "this LLM call belongs to user X's turn 3" requires parsingprompt_idstring format — fragile and not queryable via ARMS Tags.Debugging regression: issues like "session stuck", "wrong tool called", "model didn't finish turn" — which we can diagnose in ~2 minutes from a CLI trace — require manual log file analysis for daemon sessions because the trace lacks the necessary spans.
Suggested approach
Two progressive milestones, either independently valuable:
Milestone 1: span attribute parity (small diff)
Add
session.idtollm_request/tool/tool.executionspan attribute lists insession-tracing.ts. This is a 3-line change per span type and immediately makes daemon spans queryable by session.Milestone 2: middleware alignment (larger, the tracked work)
Wire
Session.ts's message-handling path through the samestartInteractionSpan→CoreToolScheduler→endInteractionSpanpipeline thatGeminiClient.sendMessageStream()uses, so daemon traces get the full span hierarchy. This is the "broader middleware-alignment work" referenced in the Session.ts comment.A middle ground: keep Session.ts's own tool dispatch loop but add explicit
startToolSpan()/endToolSpan()calls at the entry/exit points. This avoids the full GeminiClient refactor while restoring tool-level observability.Affected
daemon_mode_b_mainbranch (production daemon deployment)packages/cli/src/acp-integration/session/Session.ts:1304— thechat.sendMessageStream()call that bypasses GeminiClientpackages/core/src/telemetry/session-tracing.ts— span attribute definitions (Milestone 1)Related
Session.ts:1871-1877comment — "porting into the ACP path is tracked separately"/tmp/qwen-traces/9b8ba2063e4f070eeff98e6a9ce67dcd.json(60 spans, daemon mode, cn-zhangjiakou)