Skip to content

feat(core): Add ARMS session user ID - #7921

Merged
doudouOUC merged 1 commit into
QwenLM:mainfrom
doudouOUC:agent/arms-session-user-id
Jul 28, 2026
Merged

feat(core): Add ARMS session user ID#7921
doudouOUC merged 1 commit into
QwenLM:mainfrom
doudouOUC:agent/arms-session-user-id

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds an explicit, opt-in end-user identity for Alibaba Cloud ARMS Session Analysis. Operators can configure a stable pseudonymous ID through telemetry.userId or QWEN_TELEMETRY_USER_ID; the environment variable takes precedence after trimming, and a blank environment value falls back to settings.

The resolved value is attached at span creation as the ARMS extension gen_ai.user.id and propagated from the interaction through LLM, Tool, and Agent spans, including tool-result continuations and linked-root background agents. The implementation preserves the existing span names, kinds, parenting, retry topology, and outbound propagation behavior.

The new identity is not inferred, generated, written to Resource/logs/metrics, placed in outbound Baggage, or dual-written as enduser.id/user.id. Configuration schema, operator guidance, migration notes, unit coverage, and the permanent GenAI telemetry integration flow are updated together.

Why it's needed

ARMS Session Analysis reads its User ID from the span-level gen_ai.user.id attribute. A Resource attribute such as user.id remains a generic deployment dimension and leaves the ARMS Session User ID empty. This change lets single-user CLI and one-process-per-user deployments populate that field deliberately without exposing names, email addresses, model credentials, or automatically derived machine identity.

Reviewer Test Plan

How to verify

Configure telemetry.userId or QWEN_TELEMETRY_USER_ID, export a complete LLM → Tool → LLM turn, and confirm that the interaction, both LLM spans, and the Tool span carry the exact same gen_ai.user.id. Confirm the value is absent from Resource attributes, non-canonical telemetry records, enduser.id, user.id, and outbound Baggage. Repeat without either setting and confirm the attribute is omitted.

For precedence and validation, set both sources and confirm the trimmed environment value wins; set a blank environment value and confirm settings wins; provide a non-string settings value and confirm configuration fails rather than exporting null.

Automated verification passed: Core targeted tests 957/957, CLI targeted tests 492/492, GenAI telemetry integration tests 3/3, npm run build, npm run bundle, npm run typecheck, and npm run lint.

An online ARMS smoke with pseudonymous ID arms-smoke-20260728-1628 showed the exact value on three LLM spans, one Tool span, the Session view, and the User view. Raw LLM and Tool Attributes contained gen_ai.user.id; the Resource panel did not contain the new field or value. Direct content-query syntax still requires the workspace to configure this attribute as an SLS key-value index.

Evidence (Before & After)

N/A — no Qwen Code TUI change. Before this change the released CLI ignored QWEN_TELEMETRY_USER_ID; after this change ARMS Session and User views render and group the configured pseudonymous ID.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS, Node.js v24.12.0, local Qwen Code bundle, sandbox disabled for the fake OpenAI integration flow, and Alibaba Cloud ARMS in cn-hangzhou for the online smoke.

Risk & Scope

  • Main risk or tradeoff: The setting is process-wide, so assigning it on a shared multi-user daemon or channel would misattribute traffic. The schema and documentation explicitly restrict this first version to one-process-per-user deployments.
  • Not validated / out of scope: Dynamic request-scoped identity for shared daemon/channel processes, Windows and Linux local execution, and automatic SLS key-value index configuration are out of scope.
  • Breaking changes / migration notes: No code-level breaking change. Existing telemetry.resourceAttributes.user.id values are not promoted automatically; operators using that field for ARMS Session identity should remove it and configure the new span-level setting.

Linked Issues

N/A

中文说明

本 PR 做了什么

本 PR 为阿里云 ARMS 会话分析增加显式、按需启用的终端用户身份。运维人员可通过 telemetry.userIdQWEN_TELEMETRY_USER_ID 配置稳定的假名化 ID;两个值去除首尾空白后由环境变量优先,空白环境变量会回退到 settings。

解析后的值会在 Span 创建时以 ARMS 扩展字段 gen_ai.user.id 写入,并从 interaction 传播到 LLM、Tool 和 Agent Span,包括 ToolResult 延续轮次和 linked-root 后台 Agent。实现保持现有 Span 名称、SpanKind、父子关系、重试拓扑和出站传播行为不变。

新身份不会被自动推断或生成,不会写入 Resource、日志或指标,不会进入出站 Baggage,也不会双写为 enduser.id/user.id。配置 schema、运维说明、迁移说明、单元测试和永久 GenAI telemetry 集成流程同步更新。

为什么需要

ARMS 会话分析从 Span 级 gen_ai.user.id 读取 User ID。user.id 等 Resource 属性仍然只是通用部署维度,无法填充 ARMS Session User ID。本变更让单用户 CLI 和一进程一用户部署可以主动填充该字段,同时避免暴露姓名、邮箱、模型凭据或自动推断的机器身份。

Reviewer 测试计划

如何验证

配置 telemetry.userIdQWEN_TELEMETRY_USER_ID,导出一次完整的 LLM → Tool → LLM 流程,确认 interaction、两个 LLM Span 和 Tool Span 都携带完全相同的 gen_ai.user.id。确认该值不存在于 Resource 属性、非 canonical telemetry 记录、enduser.iduser.id 和出站 Baggage。删除两个配置后重跑,确认属性完全省略。

验证优先级和校验逻辑时,同时设置两个来源并确认去除首尾空白后的环境变量生效;将环境变量设为空白并确认回退到 settings;提供非字符串 settings 值并确认配置失败,而不是导出 null

自动化验证已通过:Core 定向测试 957/957、CLI 定向测试 492/492、GenAI telemetry 集成测试 3/3,以及 npm run buildnpm run bundlenpm run typechecknpm run lint

使用假名化 ID arms-smoke-20260728-1628 的 ARMS 在线 smoke 验证中,三个 LLM Span、一个 Tool Span、Session 视图和 User 视图都显示了完全一致的值。LLM 和 Tool 的原始 Attributes 包含 gen_ai.user.id;Resource 面板不包含新字段或该值。直接使用内容查询语法仍要求工作空间将此字段配置为 SLS 键值索引。

证据(变更前后)

N/A——Qwen Code TUI 没有变化。变更前,已发布 CLI 会忽略 QWEN_TELEMETRY_USER_ID;变更后,ARMS Session 和 User 视图可以展示并按配置的假名化 ID 聚合。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS、Node.js v24.12.0、本地 Qwen Code bundle;fake OpenAI 集成流程关闭 sandbox;在线 smoke 使用 cn-hangzhou 的阿里云 ARMS。

风险与范围

  • 主要风险或权衡:该配置是进程级的,因此在共享的多用户 daemon 或 channel 中设置会导致流量身份归属错误。schema 和文档已明确将第一版限制为一进程一用户部署。
  • 未验证或不在范围内:共享 daemon/channel 的动态请求级身份、Windows 和 Linux 本地执行,以及自动配置 SLS 键值索引不在本 PR 范围内。
  • 破坏性变更或迁移说明:没有代码级破坏性变更。现有 telemetry.resourceAttributes.user.id 不会自动提升;依赖它填充 ARMS Session 身份的运维人员应删除旧 Resource 字段,改用新的 Span 级配置。

关联 Issue

N/A

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

E2E test report

Baseline

  • Global Qwen Code 0.21.0 was run with QWEN_TELEMETRY_USER_ID=baseline-user-id.
  • The released CLI ignored the variable, and the file exporter contained neither gen_ai.user.id nor the test value.

Local bundle

  • The permanent fake OpenAI LLM → Tool → LLM flow passed.
  • The interaction, two LLM spans, and Tool span carried the same configured gen_ai.user.id.
  • The value was absent from Resource attributes, non-canonical telemetry records, enduser.id, user.id, and outbound Baggage.
  • A second flow without either setting omitted the attribute.
  • cli/gen-ai-telemetry.test.ts: 3/3 passed.

ARMS online smoke

  • A real LLM → Tool → LLM flow was exported with pseudonymous ID arms-smoke-20260728-1628.
  • ARMS displayed the exact ID on three LLM spans and one Tool span.
  • The Session view associated the ID with the smoke session; the User view grouped the session under the same ID.
  • Raw LLM and Tool Attributes contained gen_ai.user.id; the Resource panel did not contain the new field or smoke value.
  • Direct content-query syntax requires the workspace to configure gen_ai.user.id as an SLS key-value index. This does not affect ingestion, Session rendering, or User-view grouping.

Additional verification

  • Core targeted tests: 957/957 passed.
  • CLI targeted tests: 492/492 passed.
  • npm run build, npm run bundle, npm run typecheck, and npm run lint passed.

@doudouOUC
doudouOUC marked this pull request as ready for review July 28, 2026 09:26
@doudouOUC
doudouOUC enabled auto-merge July 28, 2026 09:26
@doudouOUC doudouOUC self-assigned this Jul 28, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@yiliang114

Copy link
Copy Markdown
Collaborator

⚠️ Failed to process this request. Please re-mention the bot to retry.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, @doudouOUC — this is a clean continuation of the ARMS telemetry alignment work you shipped in #7536 and #7635.

Template ✓ — all required sections present, bilingual, well-structured.

Problem: real and demonstrated. ARMS Session Analysis reads gen_ai.user.id from span-level attributes, and there is currently no way to populate it. The ARMS smoke test with arms-smoke-20260728-1628 shows the value landing on LLM, Tool, Session, and User views. This is not theoretical.

Direction: aligned with the established ARMS alignment trajectory visible in the CHANGELOG (#7536, #7635). The feature is opt-in, does not change default behavior, and the PR explicitly scopes out shared-daemon identity. That said, this PR touches telemetry configuration and span propagation — a policy escalation area. Escalating to a maintainer for direction sign-off before proceeding with full code review.

Size: 98 production logic lines (config resolution + span attribute propagation), 410 test lines, 65 docs lines, 4 schema lines. Well within bounds.

Approach: the scope feels right. Configuration resolution follows the existing resolveTelemetrySettings pattern (env precedence, trim, blank fallback). The interactionIdentityByPromptId map is a reasonable way to carry identity across the interaction→tool-result gap without keeping ended spans alive. Anti-spoofing (tool attributes cannot override the canonical user ID) and TTL expiry are good touches. No drive-by changes or scope creep.

Risk: packages/cli/src/acp-integration/session/Session.ts matches a high-risk path pattern (acp-integration). The change there is minimal (one line — passing promptId to startToolSpan), but flagging it for reviewer awareness.

@doudouOUC — this is a policy gate, not a quality rejection. The PR looks well-built; it just needs a maintainer to confirm the telemetry direction before we proceed.

@wenshao — could you confirm the ARMS session user ID direction? This adds telemetry.userId / QWEN_TELEMETRY_USER_IDgen_ai.user.id on interaction, LLM, Tool, and Agent spans. Opt-in, process-wide, no Resource/Baggage leakage.

中文说明

感谢 @doudouOUC 的 PR——这是你在 #7536#7635 中已落地的 ARMS 遥测对齐工作的干净延续。

模板 ✓——所有必填段落齐全,双语,结构清晰。

问题: 真实且已验证。ARMS 会话分析从 Span 级属性读取 gen_ai.user.id,目前没有途径填充该字段。使用 arms-smoke-20260728-1628 的 ARMS 在线 smoke 验证显示该值已出现在 LLM、Tool、Session 和 User 视图。这不是理论性问题。

方向: 与 CHANGELOG 中可见的 ARMS 对齐轨迹一致(#7536#7635)。该功能为按需启用,不改变默认行为,PR 也明确排除了共享 daemon 身份。但本 PR 触及了遥测配置和 Span 传播——属于策略升级区域。在进行完整代码审查之前,先转交维护者确认方向。

规模: 98 行生产逻辑(配置解析 + Span 属性传播),410 行测试,65 行文档,4 行 schema。在合理范围内。

方案: 范围合理。配置解析遵循现有 resolveTelemetrySettings 模式(环境变量优先、trim、空白回退)。interactionIdentityByPromptId Map 是在 interaction→tool-result 间隙传递身份的合理方式,无需保持已结束 Span 的存活。防伪造(Tool 属性无法覆盖规范用户 ID)和 TTL 过期是好的设计。无顺手改动或范围蔓延。

风险: packages/cli/src/acp-integration/session/Session.ts 匹配高风险路径模式(acp-integration)。该处改动极小(一行——将 promptId 传递给 startToolSpan),但标记以供审阅者注意。

@doudouOUC——这是策略门控,不是质量否定。PR 看起来构建良好;只是需要维护者确认遥测方向后再继续。

@wenshao——能否确认 ARMS 会话用户 ID 的方向?本 PR 添加 telemetry.userId / QWEN_TELEMETRY_USER_ID → 在 interaction、LLM、Tool 和 Agent Span 上写入 gen_ai.user.id。按需启用,进程级,无 Resource/Baggage 泄漏。

Qwen Code · qwen3.8-max-preview

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Jul 28, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 1 finishedview run. See this round's report below.

中文说明

AutoFix 第 1 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round — no code change

The only feedback newer than the last evaluation is a CHANGES_REQUESTED
review from the automated reviewer. That review is explicitly a policy gate,
not a quality rejection
: it states the PR "looks well-built," names no
correctness bug, broken build/test, or security issue, and escalates to a
maintainer (@wenshao) to confirm the ARMS session-user-ID direction before
full code review proceeds.

This open item is a product/scope decision, not a code defect, so it is not
mine to settle. Per the review workflow I am leaving the question for the
maintainer rather than implementing or declining a contested direction.

  • Decision needed (maintainer): confirm the ARMS direction — adding
    telemetry.userId / QWEN_TELEMETRY_USER_IDgen_ai.user.id on
    interaction, LLM, Tool, and Agent spans (opt-in, process-wide, no
    Resource/Baggage leakage).
  • Inline findings: none.
  • Still-red checks: none (precheck-pr / precheck was CANCELLED, not
    failed).
  • Code changes this round: none — the branch is unchanged.

No action is required from the author beyond awaiting the maintainer's
direction sign-off.

中文说明

Autofix 审查轮次——无代码改动

本次评估之后唯一的反馈是来自自动审查机器人的一条 CHANGES_REQUESTED
审查。该审查明确是策略门控,而非质量否定:它指出 PR "构建良好",未指出任何
正确性缺陷、构建/测试失败或安全问题,并转交维护者(@wenshao)确认 ARMS 会话用户 ID
方向,然后再进行完整代码审查。

该未决项是一个产品/范围决策,而非代码缺陷,因此不应由我来定夺。按照审查流程,我将该
问题留给维护者,而不会去实现或拒绝一个有争议的方向。

  • 需维护者决策: 确认 ARMS 方向——添加
    telemetry.userId / QWEN_TELEMETRY_USER_ID → 在 interaction、LLM、Tool 和
    Agent Span 上写入 gen_ai.user.id(按需启用,进程级,无 Resource/Baggage 泄漏)。
  • 行内意见: 无。
  • 仍失败的检查: 无(precheck-pr / precheck 为 CANCELLED,并非失败)。
  • 本轮代码改动: 无——分支保持不变。

作者无需采取任何行动,只需等待维护者的方向确认。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@wenshao

wenshao commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Local build & verification report (maintainer-side, merge reference)

Independently verified head 5afc2bc95 in an isolated worktree with its own real dependency install, npm run generate + build + bundle, plus a base-bundle A/B built from merge-base 9461aa860. Everything below ran locally on macOS against the real bundled CLI and the repo's fake OpenAI server — no mocked telemetry.

Verdict: behavior verified end-to-end, causally attributable to this PR, tests bite, no leaks found. Merge-ready from the verification standpoint (direction sign-off itself is the pending policy gate from the triage bot; this report is the reference for it).

1. PR test suites replicated

  • packages/core: 642/642 (session-tracing 144 incl. the 14 new gen_ai.user.id propagation cases; telemetry config 47; config 451)
  • packages/cli: 492/492 (settingsSchema + acp Session)
  • Integration (real bundle + fake OpenAI): 3/3
  • Settings-schema regen gate (CI-enforced): regenerated settings.schema.json is byte-identical to the committed file

test suites

2. Do the new tests actually bite? (mutation checks)

  • M1 — swap env/settings precedence in telemetry/config.ts2 tests fail (killed, reverted)
  • M2 — drop the gen_ai.user.id attribute from startToolSpan9 tests fail across propagation / anti-spoof / continuation / subagent-inherit (killed, reverted)

Both mutations were verified to have actually landed (grep count) before trusting the red run.

3. Maintainer E2E scenarios (not in the PR — real bundled CLI, full LLM→Tool→LLM turns)

These cover paths the PR itself only proves at unit level:

Scenario Result
S1 — both sources set → trimmed env wins; losing settings value absent from the entire export
S2 — blank (" ") env → settings value wins
S3 — whitespace-padded env → trimmed before export, padded form absent
S4 — non-string telemetry.userId (number) → CLI exits non-zero with the config error, nothing exported

Span-level evidence for S1: interaction, both LLM spans and the Tool span carry the identical id; the second LLM span is the tool-result continuation (ctx=standalone, interaction span already ended) and inherits the id via the promptId identity map with parenting unchanged — the exact mechanism this PR adds. Leak checks over the whole export: no Resource-attribute write, no enduser.id/user.id dual-writes, outbound requests carry traceparent but no user id in baggage, non-canonical records clean.

span evidence

4. Causal A/B — is this PR the cause?

Same tree, same tests, only dist/ swapped. The base bundle (merge-base, PR marker grep-absent) fails S1 with gen_ai.user.id = undefined on every span — the released behavior ignores both sources. Restoring the PR bundle turns all four scenarios green again.

causal A/B

5. Observations (non-blocking)

  1. Background housekeeping LLM usage is excluded from per-user attribution. In a real single-turn flow, the auto-memory extractor's LLM request exports as ctx=standalone with a different promptId and carries no gen_ai.user.id. This matches the PR's declared design (its own unit test asserts standalone spans omit the id), but under "one process = one user" it means ARMS per-user token totals exclude housekeeping requests (memory extraction, and any similar background calls). Fine for v1 identity/session analysis; worth a follow-up if full per-user cost attribution is ever the goal.
  2. Anti-spoof ordering is one-sided by design. startToolSpan spreads ...attrs before the canonical id, so a caller-supplied gen_ai.user.id cannot override a configured identity (unit-asserted). When no identity is configured, a caller-supplied value would pass through — currently theoretical, since all three production call sites only pass tool metadata.
  3. Identity-map entries expire with the existing 30-min span TTL, so a continuation arriving later than that loses the id — consistent with the existing span-lifetime design.
  4. The one red CI check (precheck-pr in run 30346425052) is a cancelled superseded run (concurrency group preemption), not a failure; the follow-up run passed. All code gates are green.

Not verified here

ARMS online rendering (covered by the author's arms-smoke-20260728-1628 smoke), Windows/Linux local execution, shared multi-user daemon scenarios (explicitly out of scope; docs carry the warning).

中文版本

本地构建与验证报告(维护者侧,合并参考)

在隔离 worktree 中独立验证了 head 5afc2bc95:真实依赖安装、npm run generate + build + bundle,并另外从 merge-base 9461aa860 构建了基线 bundle 做 A/B。以下全部在 macOS 本地、真实打包 CLI + 仓库自带 fake OpenAI server 上运行——遥测链路无任何 mock。

结论:行为端到端验证通过,因果上可归因于本 PR,测试确实"咬人",未发现泄漏。从验证角度看已具备合并条件(方向确认本身是 triage bot 挂起的策略门,本报告即为其参考依据)。

1. PR 测试套件复跑

  • packages/core:642/642(session-tracing 144,含 14 个新增 gen_ai.user.id 传播用例;telemetry config 47;config 451)
  • packages/cli:492/492(settingsSchema + acp Session)
  • 集成测试(真实 bundle + fake OpenAI):3/3
  • Settings schema 再生成门(CI 强制):重新生成的 settings.schema.json 与提交文件逐字节一致

2. 新测试是否真的有效?(变异检验)

  • M1——在 telemetry/config.ts 中交换 env/settings 优先级 → 2 个测试失败(已击杀,已还原)
  • M2——从 startToolSpan 删除 gen_ai.user.id 属性 → 9 个测试失败,覆盖传播/防伪造/延续轮次/子代理继承(已击杀,已还原)

两个变异均先以 grep 计数确认真实落地后才采信红色结果。

3. 维护者侧 E2E 场景(非 PR 内容——真实打包 CLI,完整 LLM→Tool→LLM 流程)

覆盖 PR 仅在单元层面证明的路径:

场景 结果
S1——双源同设 → 去空白后 env 生效;败方 settings 值在整个导出中不存在
S2——空白(" ")env → 回退到 settings 值
S3——带空白填充的 env → 导出前被 trim,原始带空白形式不存在
S4——非字符串 telemetry.userId(数字)→ CLI 以配置错误非零退出,不导出任何值

S1 的 Span 级证据:interaction、两个 LLM Span 和 Tool Span 携带完全相同的 id;第二个 LLM Span 是 ToolResult 延续(ctx=standalone,interaction Span 已结束),通过 promptId 身份映射继承 id 且父子关系不变——正是本 PR 新增的机制。全量导出泄漏检查:无 Resource 属性写入、无 enduser.id/user.id 双写、出站请求带 traceparentbaggage 不含用户 id、非 canonical 记录干净。

4. 因果 A/B——行为确实由本 PR 引入吗?

同一棵树、同一测试,只交换 dist/。基线 bundle(merge-base 构建,grep 确认无 PR 标记)在 S1 上失败:所有 Span 的 gen_ai.user.id = undefined——已发布行为忽略两个配置源。换回 PR bundle 后四个场景全部转绿。

5. 观察项(不阻塞合并)

  1. 后台家务型 LLM 用量不计入按用户归因。 真实单轮流程中,auto-memory extractor 的 LLM 请求以 ctx=standalone + 不同 promptId 导出,不携带 gen_ai.user.id。这符合 PR 声明的设计(其单元测试明确断言 standalone Span 省略该 id),但在"一进程一用户"语义下,ARMS 按用户统计的 token 总量会漏掉家务型请求(记忆提取及类似后台调用)。作为 v1 身份/会话分析没有问题;若未来要做完整的按用户成本归因,值得后续跟进。
  2. 防伪造顺序是单向设计。 startToolSpan 先展开 ...attrs 再写规范 id,因此调用方传入的 gen_ai.user.id 无法覆盖已配置身份(有单元断言)。当配置身份时,调用方传入的值会透传——目前纯理论,三个生产调用点都只传工具元数据。
  3. 身份映射条目随现有 30 分钟 Span TTL 过期,晚于该窗口的延续轮次会丢失 id——与现有 Span 生命周期设计一致。
  4. 唯一红色 CI check(run 30346425052 的 precheck-pr)是被并发组抢占取消的运行,不是失败;后续运行已通过。所有代码门全绿。

本次未验证

ARMS 在线渲染(作者的 arms-smoke-20260728-1628 smoke 已覆盖)、Windows/Linux 本地执行、共享多用户 daemon 场景(明确不在范围内;文档已含警告)。

@wenshao

wenshao commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @doudouOUC — this is a clean continuation of the ARMS telemetry alignment work from #7536 and #7635.

Template looks good ✓

Problem: real and demonstrated. ARMS Session Analysis reads gen_ai.user.id from span-level attributes, and there is currently no way to populate it. The ARMS smoke test with arms-smoke-20260728-1628 shows the value landing on LLM, Tool, Session, and User views. This is an observed operational gap, not theoretical hardening.

Direction: aligned with the established ARMS alignment trajectory. The feature is opt-in, does not change default behavior, and the PR explicitly scopes out shared-daemon identity. The previous run escalated this for telemetry direction review; @wenshao has since approved, confirming the direction. CHANGELOG has no direct gen_ai.user.id reference, but the ARMS alignment area is actively developed.

Size: 163 production logic lines (config resolution + span attribute propagation + docs), 410 test lines, 4 schema lines. Core paths touched (packages/core/src/**, packages/*/src/config/**); well under the 500-line maintainer-awareness threshold.

Approach: the scope feels right. Configuration resolution follows the existing resolveTelemetrySettings pattern (env precedence, trim, blank fallback). The interactionIdentityByPromptId map carries identity across the interaction→tool-result gap without keeping ended spans alive — a reasonable design that reuses the existing TTL sweep. Anti-spoofing (tool attributes cannot override the canonical user ID) and the Baggage leakage check in the integration test are good touches. No drive-by changes or scope creep.

Risk: packages/cli/src/acp-integration/session/Session.ts matches a high-risk path pattern (acp-integration). The change there is minimal (one line — passing promptId to startToolSpan), but flagging it for reviewer awareness.

Moving on to code review. 🔍

中文说明

感谢 @doudouOUC 的 PR——这是 #7536#7635 中 ARMS 遥测对齐工作的干净延续。

模板完整 ✓

问题:真实且已验证。ARMS 会话分析从 Span 级属性读取 gen_ai.user.id,目前没有途径填充该字段。使用 arms-smoke-20260728-1628 的 ARMS 在线 smoke 验证显示该值已出现在 LLM、Tool、Session 和 User 视图。这是已观测到的运维缺口,不是理论性加固。

方向:与已建立的 ARMS 对齐轨迹一致。该功能为按需启用,不改变默认行为,PR 也明确排除了共享 daemon 身份。上一次运行因遥测方向升级而暂停;@wenshao 已批准,确认了方向。CHANGELOG 中没有直接的 gen_ai.user.id 引用,但 ARMS 对齐领域正在积极开发。

规模:163 行生产逻辑(配置解析 + Span 属性传播 + 文档),410 行测试,4 行 schema。触及核心路径(packages/core/src/**packages/*/src/config/**);远低于 500 行维护者关注阈值。

方案:范围合理。配置解析遵循现有 resolveTelemetrySettings 模式(环境变量优先、trim、空白回退)。interactionIdentityByPromptId Map 在 interaction→tool-result 间隙传递身份,无需保持已结束 Span 的存活——合理的设计,复用了现有 TTL 清理。防伪造(Tool 属性无法覆盖规范用户 ID)和集成测试中的 Baggage 泄漏检查是好的设计。无顺手改动或范围蔓延。

风险:packages/cli/src/acp-integration/session/Session.ts 匹配高风险路径模式(acp-integration)。该处改动极小(一行——将 promptId 传递给 startToolSpan),但标记以供审阅者注意。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 5afc2bc954d4fd01056f3b5bdd6f57734f037c56 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal: given the problem (ARMS reads gen_ai.user.id from span-level attributes, no way to populate it), I would: add a telemetry.userId setting with QWEN_TELEMETRY_USER_ID env override, resolve it in resolveTelemetrySettings() with trim/blank handling, expose via Config.getTelemetryUserId(), set it on the interaction span at creation, propagate through the existing AsyncLocalStorage context to LLM/Tool/Agent spans, and handle the interaction→tool-result continuation gap with a prompt-ID-keyed identity map that reuses the existing TTL sweep. Tests for config resolution, propagation, anti-spoofing, TTL expiry, and Baggage non-leakage.

Comparison: the PR matches this approach almost exactly. The implementation is clean and follows existing patterns throughout. No correctness bugs, security holes, or regressions found. Specific observations:

  • Config resolution (packages/core/src/telemetry/config.ts): parseTelemetryUserId validates type at the system boundary (throws FatalConfigError for non-string), trims, and treats blank as undefined. Env precedence over settings is correct. Follows the same pattern as parseSensitiveSpanAttributeMaxLengthSetting.
  • Span propagation (packages/core/src/telemetry/session-tracing.ts): resolveGenAiUserId reads from the parent context's attributes, falling back to the interactionIdentityByPromptId map by prompt ID. The map stores only startTime + identity attributes — no full SpanContext retained — and is swept by the existing 30-minute TTL. Clean.
  • Anti-spoofing: in startToolSpan, the canonical gen_ai.user.id is spread after ...attrs, so caller-supplied attributes cannot override it. Tested explicitly ("does not let tool attributes override the interaction user ID").
  • startToolSpan signature: the new promptId?: string parameter is backward-compatible. All three call sites (two in coreToolScheduler.ts, one in Session.ts) are updated.
  • startSubagentSpan context resolution: the explicit subagentContext ?? toolContext ?? interactionContext chain matches the existing resolution order. Continuation-turn subagents inherit via the tool context (which already resolved via prompt ID fallback), so no separate prompt ID parameter is needed here.
  • withInteractionSpan duplication: the identity map logic is duplicated between startInteractionSpan and withInteractionSpan, but both functions already duplicate the full span creation flow — this follows the existing pattern rather than introducing new duplication.
  • Integration test: the Baggage non-leakage assertion (expect(String(request.headers['baggage'] ?? '')).not.toContain('integration-user-079458')) is a good addition. The fake-openai-server.ts change to capture request headers is minimal and well-scoped.

No blockers. No AGENTS.md violations.

sequenceDiagram
    participant Config as resolveTelemetrySettings
    participant Interaction as startInteractionSpan
    participant Map as interactionIdentityByPromptId
    participant LLM as startLLMRequestSpan
    participant Tool as startToolSpan
    participant Agent as startSubagentSpan

    Config->>Config: env QWEN_TELEMETRY_USER_ID ?? settings.userId (trim, blank→undefined)
    Interaction->>Interaction: gen_ai.user.id on span attributes
    Interaction->>Map: store {startTime, gen_ai.user.id} by promptId
    LLM->>LLM: resolve from parent context
    Tool->>Map: continuation turn: resolve by promptId
    Tool->>Tool: gen_ai.user.id (after ...attrs, anti-spoof)
    Agent->>Agent: resolve from subagent/tool/interaction context chain
    Note over Map: swept by existing 30-min TTL
Loading
Files changed (18 total)
File What changed
docs/design/gen-ai-arms-field-alignment.md Documents the opt-in ARMS end-user identity extension
docs/developers/daemon/17-configuration.md Adds userId to the telemetry config key table with daemon warning
docs/developers/development/telemetry.md Full operator guidance: config table, precedence, ARMS setup example, span attribute docs
docs/users/configuration/settings.md User-facing settings and env var documentation
integration-tests/cli/gen-ai-telemetry.test.ts Asserts gen_ai.user.id on interaction/LLM/Tool spans, Resource/Baggage non-leakage, omission when unconfigured
integration-tests/fake-openai-server.ts Captures request headers for Baggage assertion
packages/cli/src/acp-integration/session/Session.ts Passes promptId to startToolSpan (1 line)
packages/cli/src/acp-integration/session/Session.test.ts Updates startToolSpan call expectations
packages/cli/src/config/settingsSchema.ts Adds userId to telemetry JSON schema override
packages/cli/src/config/settingsSchema.test.ts Schema definition test
packages/core/src/config/config.ts TelemetrySettings.userId field, Config.getTelemetryUserId(), trim on construction
packages/core/src/config/config.test.ts Config accessor tests (trim, default undefined)
packages/core/src/core/coreToolScheduler.ts Passes prompt_id to startToolSpan at both call sites
packages/core/src/telemetry/config.ts parseTelemetryUserId validation, env-over-settings resolution
packages/core/src/telemetry/config.test.ts Resolution, precedence, blank fallback, non-string rejection tests
packages/core/src/telemetry/session-tracing.ts interactionIdentityByPromptId map, resolveGenAiUserId, propagation to all span types, TTL sweep
packages/core/src/telemetry/session-tracing.test.ts Propagation, anti-spoof, TTL expiry, concurrent isolation, subagent inheritance tests
packages/vscode-ide-companion/schemas/settings.schema.json VS Code settings schema for telemetry.userId

Testing

CI evidence (fetched via API for 5afc2bc954d4fd01056f3b5bdd6f57734f037c56):

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
precheck-pr / precheck ✅ success (1 cancelled duplicate)
Classify PR ✅ success
label ✅ success

Ubuntu unit tests pass. macOS, Windows, and integration tests are skipped — expected for a fork PR (no secrets access). No failures.

Real-scenario testing: N/A — no TUI change. The PR adds a telemetry span attribute; the author's ARMS smoke test (arms-smoke-20260728-1628) is their own claim, not independently verified here. The sandboxed /tmux and /verify lanes are unavailable for this fork PR (author lacks write access); a maintainer can check out the PR and verify the ARMS claim by hand if desired.

中文说明

代码审查

独立方案: 给定问题(ARMS 从 Span 级属性读取 gen_ai.user.id,目前无法填充),我的方案是:添加 telemetry.userId 配置和 QWEN_TELEMETRY_USER_ID 环境变量覆盖,在 resolveTelemetrySettings() 中解析(trim/空白处理),通过 Config.getTelemetryUserId() 暴露,在 interaction span 创建时设置,通过现有 AsyncLocalStorage 上下文传播到 LLM/Tool/Agent span,并用 prompt ID 键控的身份 Map 处理 interaction→tool-result 延续间隙,复用现有 TTL 清理。测试覆盖配置解析、传播、防伪造、TTL 过期和 Baggage 非泄漏。

对比: PR 与此方案几乎完全一致。实现干净,全程遵循现有模式。未发现正确性 bug、安全漏洞或回归。

  • 配置解析遵循 parseSensitiveSpanAttributeMaxLengthSetting 的相同模式
  • resolveGenAiUserId 从父上下文属性读取,回退到 prompt ID Map
  • 防伪造:规范用户 ID 在 ...attrs 之后展开,调用方属性无法覆盖
  • startToolSpan 新参数向后兼容,所有调用点已更新
  • 集成测试中的 Baggage 非泄漏断言是好的补充

无阻塞问题。无 AGENTS.md 违规。

测试

CI 证据:Ubuntu 单元测试通过。macOS、Windows 和集成测试跳过——fork PR 预期行为。无失败。

真实场景测试:N/A——无 TUI 变更。作者的 ARMS smoke 测试是其自身声明,未在此独立验证。沙箱化的 /tmux/verify 通道对此 fork PR 不可用(作者无写入权限);维护者可手动检出 PR 验证 ARMS 声明。

Qwen Code · qwen3.8-max-preview

Reviewed at 5afc2bc954d4fd01056f3b5bdd6f57734f037c56 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — solid implementation that follows existing patterns, comprehensive tests, clean scope; the only gap is that macOS/Windows/integration CI is skipped (fork PR) and the ARMS smoke is the author's claim.

Stepping back: this PR does one thing well. It adds an opt-in span-level identity for ARMS Session Analysis, following the exact same config resolution, span propagation, and TTL patterns already established in the codebase. The independent proposal I wrote before reading the diff matches what the author built — no simpler path was missed.

The code is straightforward. parseTelemetryUserId is six lines. resolveGenAiUserId is five. The interactionIdentityByPromptId map is the only new data structure, and it's swept by the existing TTL mechanism rather than introducing a new cleanup path. Every change in the diff serves the stated goal — no drive-by refactors, no formatting churn, no scope creep.

The test coverage is thorough: config resolution with precedence and blank fallback, propagation across all span types, anti-spoofing, TTL expiry, concurrent scoped interaction isolation, subagent inheritance (all three invocation kinds), continuation-turn tool-result resolution, and Baggage non-leakage. The integration test adds a "not configured → omitted" case that guards against accidental always-on behavior.

The previous run escalated for telemetry direction review; @wenshao approved, confirming the direction. The high-risk path match (acp-integration/Session.ts) is a single-line change passing promptId through — minimal risk.

Non-blocking note: the withInteractionSpan / startInteractionSpan identity map duplication follows the existing pattern where both functions duplicate the full span creation flow. If these are ever unified, the identity logic should come along. Not a blocker.

Approving. ✅

中文说明

置信度:4/5——实现扎实,遵循现有模式,测试全面,范围干净;唯一的缺口是 macOS/Windows/集成 CI 跳过(fork PR),ARMS smoke 是作者声明。

退一步看:这个 PR 做好了一件事。它为 ARMS 会话分析添加了按需启用的 Span 级身份,完全遵循代码库中已建立的配置解析、Span 传播和 TTL 模式。我在阅读 diff 之前写的独立方案与作者构建的一致——没有遗漏更简路径。

代码直截了当。parseTelemetryUserId 六行。resolveGenAiUserId 五行。interactionIdentityByPromptId Map 是唯一的新数据结构,由现有 TTL 机制清理,而非引入新的清理路径。diff 中的每个改动都服务于既定目标——无顺手重构、无格式变动、无范围蔓延。

测试覆盖全面:配置解析(优先级和空白回退)、所有 Span 类型的传播、防伪造、TTL 过期、并发作用域交互隔离、子 Agent 继承(三种调用类型)、延续轮 tool-result 解析、Baggage 非泄漏。集成测试添加了"未配置→省略"用例,防止意外的始终启用行为。

上一次运行因遥测方向升级而暂停;@wenshao 已批准,确认了方向。高风险路径匹配(acp-integration/Session.ts)是单行改动——风险极小。

非阻塞备注:withInteractionSpan / startInteractionSpan 的身份 Map 重复遵循了现有模式(两个函数已重复完整的 Span 创建流程)。如果将来统一,身份逻辑应一并处理。非阻塞。

批准 ✅

Qwen Code · qwen3.8-max-preview

Reviewed at 5afc2bc954d4fd01056f3b5bdd6f57734f037c56 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@doudouOUC
doudouOUC added this pull request to the merge queue Jul 28, 2026
Merged via the queue into QwenLM:main with commit 788e5cd Jul 28, 2026
94 of 95 checks passed
@doudouOUC
doudouOUC deleted the agent/arms-session-user-id branch July 28, 2026 11:11

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. Not reviewed: build-and-test — "Integration Tests (CLI, No Sandbox)" was skipped in CI and its suite (integration-tests/cli/gen-ai-telemetry.test.ts) was not run locally.

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — "Integration Tests (CLI, No Sandbox)" was skipped in CI and its suite (integration-tests/cli/gen-ai-telemetry.test.ts) was not run locally。

— qwen3.8-max-preview via Qwen Code /review

Comment on lines 874 to +876
attrs?: Record<string, string | number | boolean>,
description?: string,
promptId?: string,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The new promptId is a 4th positional optional string, sitting immediately after the also-optional string description. Two adjacent optional string parameters with different meanings are a silent-swap footgun: a future call site written as startToolSpan(name, attrs, promptId) compiles cleanly (both slots are string | undefined), lands the value in description (emitted as a bogus gen_ai.tool.description), and leaves promptId undefined. On a tool-result continuation turn — where the interaction span has ended and no live ALS context exists — resolveGenAiUserId(undefined, undefined) returns undefined, so gen_ai.user.id is silently dropped. Because startSubagentSpan has no promptId fallback, an Agent tool launched from that span loses the user ID across its whole LLM/tool/agent subtree. All three current call sites pass the argument correctly, so this is hardening rather than a live bug. — Failure scenario: a future refactor mis-passes promptId → silent ARMS attribution gaps with no compile error and no failing test.

Suggested fix (spans the signature plus its three call sites, so not a one-click suggestion): collapse the trailing optionals into an options object:

export function startToolSpan(
  toolName: string,
  attrs?: Record<string, string | number | boolean>,
  opts?: { description?: string; promptId?: string },
): Span {
中文说明

新增的 promptId 是第 4 个位置可选 string 参数,紧跟在同样是可选 stringdescription 之后。两个相邻、含义不同的可选 string 参数构成一个“静默交换”陷阱:未来某个调用点若写成 startToolSpan(name, attrs, promptId),TypeScript 会正常编译(两个位置都是 string | undefined),该值会落入 description(被写成伪造的 gen_ai.tool.description),而 promptId 保持 undefined。在 tool-result 延续轮次中(此时 interaction span 已结束,没有存活的 ALS 上下文),resolveGenAiUserId(undefined, undefined) 返回 undefined,于是 gen_ai.user.id 被静默丢弃。由于 startSubagentSpan 没有 promptId 回退,从该 span 启动的 Agent tool 会在其整个 LLM/tool/agent 子树中丢失用户 ID。当前三个调用点都正确传参,因此这是加固性建议,而非现存 bug。—— 失败场景:未来重构误传 promptId → ARMS 归属出现静默缺口,且没有编译错误、没有失败的测试。

建议修复(涉及签名及其三个调用点,故非一键 suggestion):将尾部可选参数收敛为一个 options 对象。

— qwen3.8-max-preview via Qwen Code /review

Comment on lines +373 to +377
const canonicalSpanNames = new Set([
'qwen-code.interaction',
'qwen-code.llm_request',
'qwen-code.tool',
]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This allowlist omits qwen-code.subagent, so the negative-assertion loop below it (if (!record.name || !canonicalSpanNames.has(record.name)) expect(attributes).not.toHaveProperty('gen_ai.user.id')) encodes the invariant “only interaction/llm_request/tool spans may carry gen_ai.user.id”. That contradicts what this same PR establishes elsewhere: docs/developers/development/telemetry.md lists the subagent span as carrying the optional gen_ai.user.id, the unit tests assert all three invocation kinds (foreground/fork/background) inherit it, and startSubagentSpan (session-tracing.ts) adds it. Today the scenario emits no subagent span, so the assertion never fires; but if the scenario is ever extended to invoke a subagent, this assertion fails on correct code — steering a developer to remove gen_ai.user.id from subagent spans to go green, regressing the behavior the unit tests guard. — Failure scenario: extend the integration scenario to invoke an Agent → the negative assertion fails on correct code → a developer removes the attribute to fix the test.

Suggested change
const canonicalSpanNames = new Set([
'qwen-code.interaction',
'qwen-code.llm_request',
'qwen-code.tool',
]);
const canonicalSpanNames = new Set([
'qwen-code.interaction',
'qwen-code.llm_request',
'qwen-code.tool',
'qwen-code.subagent',
]);
中文说明

该允许列表遗漏了 qwen-code.subagent,因此下方的负向断言循环(if (!record.name || !canonicalSpanNames.has(record.name)) expect(attributes).not.toHaveProperty('gen_ai.user.id'))编码了“只有 interaction/llm_request/tool span 可以携带 gen_ai.user.id”这一不变量。这与本 PR 在其他地方确立的契约相矛盾:docs/developers/development/telemetry.md 将 subagent span 列为携带可选 gen_ai.user.id,单元测试断言三种调用方式(foreground/fork/background)都会继承该属性,startSubagentSpan(session-tracing.ts)也会写入它。当前场景不会产生 subagent span,所以该断言永远不会触发;但一旦场景扩展为调用 subagent,这个断言就会在正确的代码上失败——诱导开发者为了变绿而移除 subagent span 上的 gen_ai.user.id,从而回退了单元测试所保护的行为。—— 失败场景:扩展集成场景以调用 Agent → 负向断言在正确代码上失败 → 开发者为修复测试而移除该属性。

— qwen3.8-max-preview via Qwen Code /review

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants