Skip to content

feat(telemetry): align session lifecycle with OpenTelemetry - #8616

Merged
wenshao merged 17 commits into
QwenLM:mainfrom
zjunothing:fix/issue-8589-otel-session-lifecycle
Aug 12, 2026
Merged

feat(telemetry): align session lifecycle with OpenTelemetry#8616
wenshao merged 17 commits into
QwenLM:mainfrom
zjunothing:fix/issue-8589-otel-session-lifecycle

Conversation

@zjunothing

@zjunothing zjunothing commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds OpenTelemetry General Session lifecycle events to Qwen Code. Each active session emits a standard session.start and session.end LogRecord with event.name and session.id. A resumed persisted conversation also includes session.previous_id. Existing Qwen-specific telemetry events remain unchanged for compatibility.

Why it's needed

Qwen Code already correlates GenAI spans with the session through session.id and gen_ai.conversation.id, but it did not expose standard session lifecycle events. Adding the lifecycle records makes session creation, continuation, replacement, and shutdown observable using the OpenTelemetry General Session semantic conventions.

Reviewer Test Plan

How to verify

  1. Review the implementation and design document in docs/design/otel-session-lifecycle-design.md.

  2. Run the focused telemetry/configuration tests:

    cd packages/core
    npx vitest run src/telemetry/session-events.test.ts src/telemetry/sdk.test.ts src/telemetry/loggers.test.ts src/config/config.test.ts --poolOptions.threads.singleThread
    
  3. Confirm that the tests cover:

    • session.start with session.id;
    • session.end with session.id;
    • the initial session.start after deferred TUI/ACP telemetry initialization;
    • session.previous_id only for a genuine persisted-session continuation;
    • no self-referential previous-session relationship when the session ID is unchanged;
    • no previous-session relationship for replacement sessions.
  4. Run the package typecheck, changed-file lint, and Core build commands listed below.

Evidence (Before & After)

Non-user-visible telemetry and documentation change; UI screenshots are not applicable (N/A). Before this follow-up, sandbox A/B verification found two lifecycle edge cases: deferred TUI/ACP initialization could drop the first session.start, and same-ID resume could emit session.previous_id === session.id. After this follow-up, the focused suite passes with integration regression tests for both cases, and the emitted lifecycle records remain covered by the existing tests.

Tested on

OS Status
macOS ✅ Tested
Windows N/A
Linux N/A

Environment (optional)

  • macOS
  • Node.js 22.x
  • Core package local workspace

Risk & Scope

  • Main risk: an additional pair of OTel LogRecords is emitted for session lifecycle transitions when the telemetry SDK is enabled.
  • Backward compatibility: existing Qwen-specific cli_config / end_session and RUM telemetry events are preserved; existing session.id and gen_ai.conversation.id correlation fields are unchanged.
  • Deferred telemetry initialization now emits the initial standard session.start after the SDK becomes ready, avoiding a session with an end but no start.
  • session.previous_id is emitted only when startNewSession() receives persisted session data and the incoming session ID differs from the outgoing ID.
  • Session events are best-effort and do not block session switching or telemetry shutdown.
  • Out of scope: changing the meaning of gen_ai.agent.id, redesigning trace parentage, or changing existing custom telemetry schemas.
  • No data migration is required.

Linked Issues

Closes #8589

中文说明

本 PR 为 Qwen Code 补充 OpenTelemetry General Session 生命周期事件。每个 session 会发送带有 event.namesession.id 的标准 session.start / session.end LogRecord;恢复持久化会话时额外携带 session.previous_id。现有 Qwen 自定义 telemetry 事件保持不变。

本次跟进还修复了两个边界问题:延迟初始化的 TUI/ACP 路径现在会在 SDK 就绪后补发初始 session.start;当恢复的 session ID 与当前 ID 相同时,不再生成自引用的 session.previous_id

验证结果

  • 定向 Vitest:643 项通过。
  • TypeScript typecheck:通过。
  • 改动文件 ESLint:通过。
  • packages/core build:通过。
  • 本地环境:macOS、Node.js 22.x。
  • 这是 telemetry 和文档变更,不涉及用户界面,因此截图不适用。

风险与范围

保留现有 session.idgen_ai.conversation.id 以及 Qwen 自定义事件;不改变 gen_ai.agent.id、trace parentage 或现有自定义 telemetry schema,也不需要数据迁移。

@zjunothing

Copy link
Copy Markdown
Collaborator Author

Verification report / 验证报告

English

Implemented the OpenTelemetry session lifecycle alignment requested in #8589.

  • Added session.start and session.end OTel LogRecords with event.name and required session.id.
  • Added session.previous_id only for persisted-session continuation; /clear/replacement sessions omit it.
  • Added end emission on session replacement and telemetry shutdown.
  • Preserved existing Qwen-specific telemetry events and documented the design.

Verified locally:

  • Targeted Vitest: 578 passed across telemetry and config tests.
  • npm run typecheck: passed.
  • Changed-file ESLint: passed.
  • npm run build in packages/core: passed.

中文

已实现 #8589 要求的 OpenTelemetry session lifecycle 对齐:

  • 新增带有 event.name 和必需 session.idsession.start / session.end OTel LogRecord。
  • 只有恢复持久化 session 时写入 session.previous_id/clear 或普通替换不会误报 continuation。
  • session 替换前及 telemetry shutdown 时发送 end 事件。
  • 保留现有 Qwen 自定义 telemetry 事件,并补充设计文档。

本地验证:定向 Vitest 578 项通过npm run typecheck、改动文件 ESLint、packages/corenpm run build 均通过。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

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

Qwen 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.

Thanks for the PR, @zjunothing — and for including the design doc and the linked issue, both of which match what issue triage asked for on #8589.

Before review can proceed, the PR body needs to follow the pull request template. The current body uses free-form headings (Summary / Verification / OTel references) and is missing the required sections:

  • ## What this PR does
  • ## Why it's needed
  • ## Reviewer Test Plan — including ### How to verify, ### Evidence (Before & After), and the ### Tested on OS table
  • ## Risk & Scope
  • ## Linked Issues
  • The Chinese translation wrapped in a <details> block (the ## 中文说明 section is already there — it just needs to be inside <details> per the template)

The content you've written is good — the Summary maps directly onto What this PR does / Why it's needed, and the Verification section maps onto How to verify, so this is a restructure rather than a rewrite. The genuinely new parts: the Tested on table (which OSes did you verify on locally?) and Risk & Scope (e.g. backward compatibility with the existing Qwen-specific telemetry events, and what is explicitly out of scope such as gen_ai.agent.id).

One drive-by note while you're editing: the linked issue #8589 is a feature request titled feat(telemetry): ..., but this PR is titled fix(telemetry): .... Since the change adds new standard lifecycle events rather than repairing broken behavior, feat is the accurate conventional-commit type — worth fixing so it lands in the right changelog bucket.

Once the body follows the template, push a new commit or re-trigger with @qwen-code /triage and the gate will run again.

中文说明

感谢提交 PR,@zjunothing——也感谢你提供了设计文档和关联 issue,两者都符合 #8589 issue triage 阶段的要求。

在进入审查之前,PR 正文需要遵循 PR 模板。当前正文使用了自由格式的标题(Summary / Verification / OTel references),缺少以下必需章节:

  • ## What this PR does
  • ## Why it's needed
  • ## Reviewer Test Plan——包含 ### How to verify### Evidence (Before & After)### Tested on 操作系统表格
  • ## Risk & Scope
  • ## Linked Issues
  • 包在 <details> 块中的中文翻译(已有 ## 中文说明 章节,只需按模板放入 <details> 中)

你写的内容质量很好——Summary 可以直接对应 What this PR does / Why it's needed,Verification 章节可以对应 How to verify,所以这只是重新组织结构,而不是重写。真正需要补充的是:Tested on 表格(你在本地验证了哪些操作系统?)和 Risk & Scope(例如与现有 Qwen 自定义 telemetry 事件的向后兼容性,以及明确超出范围的部分,如 gen_ai.agent.id)。

顺带一提:关联 issue #8589 是标题为 feat(telemetry): ... 的 feature request,而本 PR 标题是 fix(telemetry): ...。由于该改动是新增标准生命周期事件而非修复损坏行为,feat 才是准确的 conventional commit 类型——建议一并修正,以便进入正确的 changelog 分类。

正文遵循模板后,推送新 commit 或使用 @qwen-code /triage 重新触发,门禁会再次运行。

Qwen Code · qwen3.8-max

@zjunothing zjunothing changed the title fix(telemetry): align session lifecycle with OpenTelemetry feat(telemetry): align session lifecycle with OpenTelemetry Aug 6, 2026
@zjunothing

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 41 passed · 2 failed · 43 total

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:41 通过 · 2 失败 · 43 总计

Verification report

PR 8616 — feat(telemetry): align session lifecycle with OpenTelemetry

Verdict: findings — 41 pass / 2 fail (43 scripted assertions; the 2 fails are the same reproduced design-claim violation on two arms, see F2). Verified head: b06954f420b52c95d96839706e700de0f6063fba (merge base 89b3d5ea8e). The central claim is proven load-bearing by a 0→N A/B; two concrete findings below (F1 medium, F2 low), neither blocking on its own.

中文摘要
  • 结论:findings。A/B 证明核心改动真实生效:base 构建在相同场景下导出 0 条 session.start/session.end 记录(遗留 qwen-code.config 记录两侧均为 4 条,证明导出管道在 base 侧同样工作),head 构建导出完整的 end→start 配对(见「Central claim」表与 01-ab-base-zero-session-events.png / 02-ab-head-full-lifecycle-pairs.png)。
  • F1(中):在交互式 TUI 与 ACP 模式(deferTelemetryInitialization 为 true 的主产品路径)下,首个 session 的 session.start 必然丢失,而 session.end 仍会在切换/关闭时发出——出现"有 end 无 start"的会话。设计文档与 docs/developers/development/telemetry.md 声称"初始 Config 初始化即发 session.start",与实测不符(03-deferred-tui-initial-start-dropped.png)。已给出并实测最小修复。
  • F2(低)/resume 选择当前活动会话时,session.start 会携带 session.previous_id == session.id,与设计文档"never equal"的声明矛盾;会话选择器未排除活动会话。已给出并实测一行修复。
  • 完整性报告(非合并条件):新增单测只钉住 emit 函数本身;Config.startNewSession 集成与 shutdownTelemetry 的 end 发射在单测层无覆盖(M2/M3 变异存活),由本报告的线级 harness 钉住。
  • 未覆盖:OTLP HTTP/gRPC 导出路径(仅验证 outfile 导出缝)、真实 TUI 端到端(以顺序重放代替)、仓库级 typecheck(仅 core)。

Central claim + A/B

Central claim: with the OTel pipeline enabled, every session lifecycle transition emits standard General-Session LogRecords — session.start/session.end with session.id, session.previous_id only on persisted continuation, end-before-start ordering, legacy events unchanged.

Harness: mock-free, drives the compiled packages/core dist through a real Config (initialize() → replacement startNewSession() → continuation startNewSession(id, sessionData) → same-id probe → shutdownTelemetry()), capturing records through the real FileLogExporter seam (telemetry.outfile), i.e. the SDK's own serialization, not a stub. Base control = git worktree at HEAD^1 rebuilt with the unchanged lockfile's node_modules (no @qwen-code links in core's nested node_modules; realpath of the imported dist asserted in-harness, B3).

cell build session.start session.end legacy qwen-code.config capture
base 89b3d5ea8e 0 0 4 01-ab-base-zero-session-events.png
head (headless ordering) b06954f420 4 (incl. initial) 4 4 02-ab-head-full-lifecycle-pairs.png
head (deferred = TUI/ACP ordering replay) b06954f420 3 (initial missing) 4 3 03-deferred-tui-initial-start-dropped.png

Ordering asserted per switch: end(outgoing) index < start(new) (H3c/H4c), shutdown's end(current) is the final session record (H5), continuation carries previous_id = outgoing id (H4b), replacement carries none (H3d), every record has non-empty event.name+session.id (H6), legacy records preserved on both arms (H8/B2). The base cell's positive control (legacy records present, B2) and the head cell ran symmetrically — the base absence is a real absence, not a dead pipeline.

Reviewer Test Plan, step by step

step result
1. Review implementation + design doc done; the doc's two falsifiable claims ("initial Config initialization" emission point; "previous_id never equal") are exactly F1 and F2
2. Focused vitest command 578/578 pass at head, identical command (05-reviewer-test-plan-gate-578-pass.png)
3. Tests cover the four behaviors partially: the four behaviors are pinned at the emitSessionStart/End level (M1 kills the right test, 04-mutation-m1-killed-by-own-test.png), but the Config/sdk.ts integration that decides when to call them is unpinned (M2/M3 survive 578/578 and 66/66) — the plan's step-3 claim overstates coverage; the integration is pinned only by this round's harness
4. Typecheck, lint, build core tsc --noEmit exit 0; eslint on all 7 changed source files clean with a planted-violation liveness probe caught first; base control build exit 0

Findings

F1 (medium) — initial session.start is deterministically dropped in TUI and ACP modes; first session gets an end without a start

Config.initialize() emits the initial logStartSession synchronously at its end, but in interactive/ACP modes cli/src/config/config.ts:2201 sets deferTelemetryInitialization: isAcpMode || (interactive && !question), so the SDK is initialized later by startup-prefetch.ts — after that emission. logStartSession returns early when !isTelemetrySdkInitialized(), and nothing re-emits the start when the SDK later settles. Measured by replaying that ordering (DEFER=1 arm): session.start for the initial session absent, session.end for it still emitted at the first switch (record [0] in 03-deferred-tui-initial-start-dropped.png). In headless mode the same emission is a race (the code comment at config.ts:2433 acknowledges pre-settle drops); this box won it (244 ms init), a cold machine may not. Design doc and docs/developers/development/telemetry.md both state the initial start is emitted. Impact: telemetry-only — every TUI/ACP session that never /clears or /resumes has a session.end with no session.start, and the lifecycle "creation" half of the PR's stated goal is unobservable in the product's primary modes.

Minimal suggested fix (measured)

In sdk.ts#initializeTelemetry, after setSessionContext(...):

if (config.isTelemetryInitializationDeferred() && sessionId) {
  emitSessionStart(sessionId);
}

plus isTelemetryInitializationDeferred(): boolean; on TelemetryRuntimeConfig (the getter already exists on Config). Measured in a scratch build: deferred arm gains exactly one session.start for the initial session and the record sequence becomes fully paired; headless arm is byte-identical in shape (4 starts, no double emission for S1). Cost: sdk.test.ts builds its mock configs as plain objects lacking the new getter — the init call then throws inside initializeTelemetry's try/catch and aborts init (measured: shell trace propagation wiring > sets shell trace propagation on init goes red, 640/641), so the three mock factories (lines ~152/1328/1392) need the getter, and the fix should ship with a fixture asserting the deferred start (the current suite pins nothing along that axis).

F2 (low) — session.previous_id can equal session.id, contradicting the design's "never equal"

startNewSession(sessionId, sessionData) passes previousSessionId through whenever sessionData is present, with no inequality guard. Reachable from the product: /resume's SessionPicker lists every session file (no active-session exclusion in listSessions or the picker; the active session is the newest by mtime), and useResumeCommand.handleResume calls config.startNewSession(sessionId, sessionData) with whatever was selected. Measured (same-id probe on both head arms): session.start session.id=S3-continued previous_id=S3-continued — the H7 assertion fails on both arms (the 2 fail counts in assertions.json). Semconv-level impact: a self-referential continuation edge a downstream correlator may treat as a cycle.

Minimal suggested fix (measured)

In config.ts#startNewSession:

sessionData && previousSessionId !== this.sessionId
  ? previousSessionId
  : undefined,

Measured in a scratch build: same-id probe's start carries no previous_id, genuine continuation still carries it (H4b), head arm 14 pass / 0 fail, focused suites 578/578.

Completeness (not merge conditions)

  • M2 (remove logSessionEnd(this) + the previousSessionId argument from config.ts): 578/578 stay green — the Config-side integration is unpinned by any unit test.
  • M3 (remove the shutdown emitSessionEnd from sdk.ts): 66/66 stay green — shutdown emission unpinned.
  • Both survivors are coverage gaps, not dead code: the A/B harness shows each removed line changes exported output. The new session-events.test.ts is not vacuous (M1 killed by exactly its continuation test). The integration is pinned only by this round's harness; adding a Config-level test (mocked SDK, asserted call sequence) would close the gap.

Not covered

  • OTLP HTTP/gRPC exporter chains — verified through the outfile exporter seam only; attribute serialization over OTLP wire not re-verified (the session.* attributes are plain strings, low risk).
  • Real TUI/ACP end-to-end — the deferred ordering was replayed faithfully (deferTelemetryInitialization + post-init initializeTelemetry, the exact code ordering), not driven through Ink.
  • ACP child path (acpAgent.ts:3069) — same deferred flag, same ordering; not driven separately.
  • Full core test suite and repo-wide typecheck — ran the telemetry directory (850/850), the three focused files (578/578), and core tsc --noEmit; the PR touches only core + docs.
  • The headless initial-start race was measured once (won); its distribution across machines was not sampled.

Methodology

Environment: CI verify container (node:22-bookworm), merge-ref checkout, npm ci+npm run build pre-run at head; base control rebuilt only packages/core in a scratch worktree at HEAD^1 with the unchanged lockfile's node_modules symlinked (realpath asserted in-harness). Harness harness-lifecycle.mjs (in this artifact dir) drives compiled dist through real Config/OTel SDK with HOME isolated per arm; records parsed from the FileLogExporter outfile. Mutations applied in a separate scratch worktree and reverted after each run. Raw logs: log-head.txt, log-base.txt, log-defer.txt; captures evidence/01–05*.png. Assertion counts: lifecycle arms 5+13+14 (2 fails = H7 on head and deferred arms), mutation matrix 4, gates 5 → 41 pass / 2 fail / 43 total.

Evidence images

01-ab-base-zero-session-events

02-ab-head-full-lifecycle-pairs

03-deferred-tui-initial-start-dropped

04-mutation-m1-killed-by-own-test

05-reviewer-test-plan-gate-578-pass

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@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.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Test Plan (not a blocker): src/telemetry/session-events.test.tsno such file or directory; src/telemetry/loggers.test.tsno such file or directory; src/config/config.test.tsno such file or directory.

中文说明

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

Test Plan(非阻断):src/telemetry/session-events.test.tsno such file or directory; src/telemetry/loggers.test.tsno such file or directory; src/config/config.test.tsno such file or directory

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +3845 to +3849
logStartSession(
this,
new StartSessionEvent(this),
sessionData ? previousSessionId : undefined,
);

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.

[Critical] Resuming the currently-active session emits session.start with session.previous_id === session.id, violating the invariant this PR's own design doc states ("it is never equal to the new session.id"). The gate keys only on sessionData presence, never on id change. The /resume picker does not exclude the current session (DialogManager renders SessionPicker without disabledIds, unlike the delete dialog right below it), the active session is persisted live and sorts first by mtime, and handleResume has no same-id guard — Failure scenario: /resume + Enter on the first row (the session the user is already in) → session.start with session.previous_id === session.id, a self-loop for any consumer chaining sessions by previous_id.

Suggested change
logStartSession(
this,
new StartSessionEvent(this),
sessionData ? previousSessionId : undefined,
);
logStartSession(
this,
new StartSessionEvent(this),
sessionData && previousSessionId !== this.sessionId
? previousSessionId
: undefined,
);
中文说明

恢复当前正在使用的 session 时,会发出 session.previous_id === session.idsession.start,违反本 PR 设计文档中"previous_id 永不等于新 session.id"的约定。续接判断仅依据 sessionData 是否存在,未校验 id 是否变化。/resume 选择器没有排除当前 session(DialogManager 渲染 SessionPicker 时未传 disabledIds,而紧随其后的删除对话框传了),当前 session 被实时持久化且按 mtime 排第一,handleResume 也没有同 id 防护——触发路径:/resume 后直接回车选中第一行(即用户当前所在 session)→ 产生自环路的续接记录。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +144 to +147
const currentSessionId = getCurrentSessionId();
if (currentSessionId) {
emitSessionEnd(currentSessionId);
}

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.

[Critical] This shutdown session.end is unconditional, but the initial session's session.start is gated behind isTelemetrySdkInitialized() inside logStartSession and is dropped when the SDK has not settled — probe-confirmed against the real SDK with a file exporter: replaying the interactive startup order (logStartSession before init settles → initializeTelemetryshutdownTelemetry) exports only ["session.end"], while the eager-order control exports ["session.start", "session.end"]. In interactive mode this is deterministic, not a race: deferTelemetryInitialization defers SDK init to post-render, so the initial logStartSession in Config.initialize() always hits the early-return gate — Failure scenario: every telemetry-enabled interactive run exports a session.end with no matching session.start; if the user never runs /clear that is the only session, so lifecycle consumers see a session with no start time. Suggested fix: track which session ids actually emitted session.start and gate the end emissions on it, or emit a catch-up session.start when initializeTelemetry settles (the fix spans session-events.ts/sdk.ts, so no inline suggestion here).

中文说明

关闭时的 session.end 无条件发出,但初始 session 的 session.startlogStartSession 内的 isTelemetrySdkInitialized() 门控,SDK 未就绪时被丢弃——已用真实 SDK + 文件 exporter 探针验证:复现交互式启动顺序只导出 ["session.end"],而先初始化再启动的对照组导出成对记录。交互模式下这是确定性的(遥测初始化被延迟到首屏渲染之后,Config.initialize() 中的首次 logStartSession 必然被门控拦截)——结果:每个启用遥测的交互式运行都会导出没有对应 start 的 end。建议:记录实际发出过 start 的 session id 并据此门控 end,或在 initializeTelemetry 完成时补发 start。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +144 to +147
const currentSessionId = getCurrentSessionId();
if (currentSessionId) {
emitSessionEnd(currentSessionId);
}

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.

[Critical] In daemon mode (qwen serve) this ends the wrong session: the process-global context holds the synthetic daemon:<pid> id set by the daemon telemetry runtime config — an id that never had a session.start — while the actually-served sessions (which DID emit session.start via Config.initialize()) never emit session.end: every per-session teardown passes shutdownTelemetry: false and nothing else on those paths calls logSessionEnd — Failure scenario: OTel-enabled qwen serve serving sessions A, B: on SIGTERM exactly one session.end fires, for daemon:<pid>; the served sessions are never ended and the one end has no start, so generic backends cannot determine session boundaries in daemon mode — the observability gap issue #8589 was filed to close ("important when one daemon serves multiple sessions"). Suggested fix: emit logSessionEnd(config) in the per-session teardown paths, and/or gate this shutdown end on ids that actually emitted a start; state multi-session semantics in the design doc.

中文说明

daemon 模式(qwen serve)下结束的是错误的 session:进程级全局 context 持有 daemon 遥测运行时配置写入的合成 id daemon:<pid>(从未发过 session.start),而真正发出过 start 的被服务 session 从不发 end——每个会话的 teardown 都传 shutdownTelemetry: false,且这些路径上没有其他 logSessionEnd 调用。触发场景:启用 OTel 的 qwen serve 服务会话 A、B,SIGTERM 时只发出唯一一条针对 daemon:<pid> 的 end;被服务会话永远不结束、唯一的 end 又没有对应的 start,通用后端无法在 daemon 模式下确定会话边界——这正是 #8589 要补上的观测缺口。建议:在每个会话的 teardown 路径补发 logSessionEnd(config),并将关闭时的 end 限制在实际发过 start 的 id 上。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment thread packages/core/src/config/config.ts Outdated
Comment on lines 3797 to 3799
const previousSessionId = this.sessionId;
logSessionEnd(this);
this.sessionId = sessionId ?? randomUUID();

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.

[Critical] This new logSessionEnd call breaks the existing packages/core/src/config/config-session-env.test.ts suite — measured against the merge base (base-tree + test-delta): the file passes on base and fails only with this PR. Its vi.mock('../telemetry/index.js', ...) factory predates the new export, so both tests exercising startNewSession throw [vitest] No "logSessionEnd" export is defined on the "../telemetry/index.js" mock — Failure scenario: npm test --workspace=packages/core (and thus CI) fails with exactly the +1 file / +2 tests delta measured here (the other 11 failing core files fail identically on base — pre-existing environment issues, not this PR). Fix: add logSessionEnd: vi.fn() to that mock factory, then re-run npx vitest run src/config/config-session-env.test.ts.

中文说明

新增的 logSessionEnd 调用破坏了现有 config-session-env.test.ts 测试套件——已对照合并基线测量(base-tree + test-delta):该文件在基线上通过、仅在本 PR 上失败。其 vi.mock('../telemetry/index.js', ...) 工厂早于新导出存在,导致两个执行 startNewSession 的测试抛出 mock 缺少导出的错误。修复:在该 mock 工厂中补上 logSessionEnd: vi.fn()(其余 11 个失败的 core 测试文件在基线上同样失败,属既有环境问题,与本 PR 无关)。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment thread packages/core/src/config/config.ts Outdated
Comment on lines 3797 to 3799
const previousSessionId = this.sessionId;
logSessionEnd(this);
this.sessionId = sessionId ?? randomUUID();

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] Untested-wiring pattern (1/6 — the end-before-swap ordering): the efficacy probe (harness validated) reverted this hunk on its own and every affected test stayed green; 6 review agents independently concur the lifecycle wiring has no test. Sibling locations: the continuation gate below (~line 3845), the config.ts import (~132), the telemetry/index.ts export (~35), the loggers.ts import (~143), the sdk.ts shutdown emission (~144) — Failure scenario: a future refactor moving logSessionEnd(this) below the sessionId reassignment ends the NEW session instead of the outgoing one on every /clear//resume//branch, and every test stays green. Fix: add a startNewSession test asserting session.end carries the pre-swap id and is emitted before the swap.

中文说明

未测试的接线(模式 1/6——先结束再换 id 的顺序):测试有效性探针(harness 已验证)单独回退该代码块后所有测试仍然通过;6 个审查 agent 独立确认生命周期接线无测试。同类位置:续接门控(约 3845 行)、config.ts 导入(约 132 行)、telemetry/index.ts 导出(约 35 行)、loggers.ts 导入(约 143 行)、sdk.ts 关闭时发射(约 144 行)。修复:新增 startNewSession 测试,断言 session.end 携带换 id 之前的旧 id 且先于换 id 发出。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +144 to +147
const currentSessionId = getCurrentSessionId();
if (currentSessionId) {
emitSessionEnd(currentSessionId);
}

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] Untested-wiring pattern (6/6 — this shutdown emission): deleting this block, or moving it below currentSdk.shutdown(), leaves the whole suite green — every session would be left unterminated at process exit (or the record emitted into an already-shut-down SDK and dropped), defeating the design doc's "Telemetry shutdown ends the currently active session" guarantee. sdk.test.ts has extensive shutdown coverage to host this — Fix: initialize telemetry, establish a session context, spy emitSessionEnd, assert it is called with the current session id before NodeSDK.prototype.shutdown; plus a no-session-context case asserting no emission.

中文说明

未测试的接线(模式 6/6——关闭时的发射):删除该代码块或把它移到 currentSdk.shutdown() 之后,整个测试套件仍然通过——进程退出时所有会话都不会被结束,设计文档"遥测关闭时结束当前活跃会话"的保证形同虚设。sdk.test.ts 已有大量 shutdown 覆盖可承载该测试。修复:初始化遥测、建立会话上下文、spy emitSessionEnd,断言其在 SDK shutdown 之前以当前 session id 被调用,并补充无会话上下文时不发射的用例。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +19 to +23
const attributes: LogAttributes = {
'event.name': EVENT_SESSION_START,
'session.id': sessionId,
...(previousSessionId ? { 'session.previous_id': previousSessionId } : {}),
};

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] These records omit event.timestamp, which every sibling emitter in loggers.ts sets explicitly (~30 sites, including the cli_config record emitted in the same logStartSession body), and which telemetry.md — edited in this same PR — states is universal: "All log records automatically include event.name and event.timestamp attributes". Nothing adds it downstream (plain BatchLogRecordProcessor) — Concrete cost: session.start/session.end become the only records without the attribute, the doc sentence becomes false, and any downstream query/filter keyed on event.timestamp silently drops both events. Apply the same addition in emitSessionEnd below.

Suggested change
const attributes: LogAttributes = {
'event.name': EVENT_SESSION_START,
'session.id': sessionId,
...(previousSessionId ? { 'session.previous_id': previousSessionId } : {}),
};
const attributes: LogAttributes = {
'event.name': EVENT_SESSION_START,
'event.timestamp': new Date().toISOString(),
'session.id': sessionId,
...(previousSessionId ? { 'session.previous_id': previousSessionId } : {}),
};
中文说明

新记录缺少 event.timestamploggers.ts 中所有同类 emitter 都显式设置该字段(约 30 处,包括同一个 logStartSession 里发出的 cli_config 记录),且本 PR 同时编辑的 telemetry.md 声称"所有日志记录自动包含 event.name 和 event.timestamp",而管道下游并不会自动补充。代价:这两个记录成为唯一缺少该字段的记录,文档表述变为错误,按 event.timestamp 过滤的下游查询会悄悄漏掉它们。下方的 emitSessionEnd 也需同样补充。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +868 to +870
The existing Qwen-specific `qwen-code.config`/`cli_config` and
`end_session` records remain available for compatibility. GenAI request spans
continue to use `gen_ai.conversation.id` for the same owning session ID.

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 sentence claims end_session records remain available, but no production code path emits them: EndSessionEvent is instantiated only in qwen-logger.test.ts, and QwenLogger.logEndSessionEvent has zero production callers. It also contradicts the design doc added in this same PR, which names the surviving events as "qwen-code.config / cli_config and RUM session_start" — Concrete cost: an operator following this doc queries their pipeline for end_session records and gets none; the two documents added by this PR disagree about which legacy events survive.

Suggested change
The existing Qwen-specific `qwen-code.config`/`cli_config` and
`end_session` records remain available for compatibility. GenAI request spans
continue to use `gen_ai.conversation.id` for the same owning session ID.
The existing Qwen-specific `qwen-code.config`/`cli_config` and RUM
`session_start` records remain available for compatibility. GenAI request
spans continue to use `gen_ai.conversation.id` for the same owning session ID.
中文说明

该句声称 end_session 记录仍然可用,但生产代码没有任何路径发出它:EndSessionEvent 只在 qwen-logger.test.ts 中实例化,QwenLogger.logEndSessionEvent 没有生产调用方。此表述还与本 PR 同时新增的设计文档矛盾(设计文档称保留的是 qwen-code.config / cli_config 和 RUM session_start)。代价:按此文档查询 end_session 的运维人员将一无所获。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +861 to +863
Session lifecycle is also exported through the OpenTelemetry General Session
semantic conventions. When the OTel logs pipeline is enabled, Qwen Code emits
`session.start` and `session.end` log events with the required `session.id`

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 LogRecord events are documented only as prose here in the Spans section, while every sibling log event has a bullet entry in the Logs catalog ("The following events are logged:" → "Core Session Events", ~line 556, where qwen-code.config etc. are listed) — Concrete cost: a consumer consulting that catalog to build a dashboard or query finds no session.start/session.end entries and concludes they don't exist. Suggested fix: add bullet entries there (with session.id and optional session.previous_id), keeping only a cross-reference in this section.

中文说明

新的 LogRecord 事件仅以散文形式记录在 Spans 章节,而 Logs 目录("The following events are logged:" → "Core Session Events",约 556 行)中每个同级日志事件都有条目。代价:按该目录构建仪表板的用户找不到 session.start/session.end,会以为它们不存在。建议:在目录中补充条目,此处仅保留交叉引用。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines 202 to 206
export function logStartSession(
config: Config,
event: StartSessionEvent,
previousSessionId?: string,
): void {

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 new parameter is supplied at only one of the two logStartSession call sites: the startup path (Config.initialize(), config.ts ~3039) never passes it, so qwen --resume X --fork-session — a persisted continuation that creates a NEW session id, the exact case session.previous_id exists for — emits session.start{session.id: F} with no lineage to source conversation X, while the in-process analogue /branch does emit previous_id for the identical shape — Concrete cost: telemetry consumers cannot link a forked session back to its source conversation; issue #8589's rollover criterion is unmet for the startup fork path. Suggested fix: plumb the fork's source session id through to the initial call (or document startup fork lineage as intentionally untracked).

中文说明

新参数只在两个 logStartSession 调用点之一被传入:启动路径(Config.initialize())从不传入,因此 qwen --resume X --fork-session——创建新 session id 的持久化续接,正是 session.previous_id 存在的场景——发出的 session.start 不带源会话 X 的血缘信息,而进程内同形态的 /branch 却会发出 previous_id。代价:遥测消费方无法把 fork 会话关联回源会话。建议:把 fork 的源 session id 传入初始调用,或在文档中说明启动 fork 血缘有意不记录。

— qwen3.8-max via Qwen Code /review (v0.21.6)

@zjunothing

Copy link
Copy Markdown
Collaborator Author

Verification report — follow-up

Environment

  • macOS
  • Node.js 22.x
  • packages/core
  • Updated head: 5b9c8c6580

Reproduction and result

The sandbox A/B verification identified two edge cases in the previous head:

  • Deferred TUI/ACP telemetry initialization could lose the initial session.start while still emitting session.end later.
  • Resuming a session with the same ID could emit session.previous_id equal to session.id.

The follow-up now emits the initial standard start after deferred SDK initialization and suppresses self-referential session.previous_id values. Existing genuine continuation and replacement behavior remains covered.

Tests executed

  • npx vitest run src/telemetry/session-events.test.ts src/telemetry/sdk.test.ts src/telemetry/loggers.test.ts src/config/config.test.ts --poolOptions.threads.singleThread643 passed
  • npm run typecheck in packages/corePASS
  • Changed-file ESLint — PASS
  • npm run build in packages/corePASS
  • Pre-push AK leak detection — PASS

Evidence

The new regression tests cover deferred initialization and unchanged-ID continuation at the SDK/Config integration points. This is a non-visual telemetry change, so screenshots are not applicable.

中文验证报告

验证报告

环境

macOS、Node.js 22.x、packages/core,最新 head 为 5b9c8c6580

复现与结果

针对 sandbox A/B 验证发现的两个问题已完成修复:延迟 TUI/ACP telemetry 初始化会补发初始 session.start;当恢复 session ID 与当前 ID 相同时,不再写入自引用的 session.previous_id

已执行测试

  • 定向 Vitest:643 项通过。
  • npm run typecheck:通过。
  • 改动文件 ESLint:通过。
  • packages/core build:通过。
  • pre-push AK leak detection:通过。

证据

新增 SDK/Config 集成回归测试覆盖延迟初始化和相同 session ID 的恢复场景。这是非 UI 的 telemetry 变更,因此不适用截图。

@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.

Reviewed. Not reviewed: reverse audit — stopped before round 3 by the review time budget. Test Plan (not a blocker): src/telemetry/session-events.test.tsno such file or directory; src/telemetry/sdk.test.tsno such file or directory; src/telemetry/loggers.test.tsno such file or directory; src/config/config.test.tsno such file or directory.

中文说明

已审查。 未审查:反向审计——评审时间预算不足,未能开始第 3 轮。 Test Plan(非阻断):src/telemetry/session-events.test.tsno such file or directory; src/telemetry/sdk.test.tsno such file or directory; src/telemetry/loggers.test.tsno such file or directory; src/config/config.test.tsno such file or directory

— qwen3.8-max via Qwen Code /review (v0.21.6)

@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.

Reviewed. Suggestions are inline. Not reviewed: reverse audit — stopped before round 4 by the review time budget. Test Plan (not a blocker): src/telemetry/session-events.test.tsno such file or directory; src/telemetry/sdk.test.tsno such file or directory; src/telemetry/loggers.test.tsno such file or directory; src/config/config.test.tsno such file or directory.

中文说明

已审查。 建议见行内评论。 未审查:反向审计——评审时间预算不足,未能开始第 4 轮。 Test Plan(非阻断):src/telemetry/session-events.test.tsno such file or directory; src/telemetry/sdk.test.tsno such file or directory; src/telemetry/loggers.test.tsno such file or directory; src/config/config.test.tsno such file or directory

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment thread packages/core/src/telemetry/sdk.test.ts Outdated
Comment on lines +198 to +202
it('emits the initial session start after deferred telemetry initialization', async () => {
const deferredConfig = {
...mockConfig,
isTelemetryInitializationDeferred: () => true,
} as unknown as Config;

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 new test pins only the positive side of the catch-up guard: no test asserts that a NON-deferred initializeTelemetry does NOT emit the catch-up session.start. Measured mutation: removing the if (config.isTelemetryInitializationDeferred?.()) guard in sdk.ts survives the whole relevant suite (sdk.test.ts 64/64 pass with it applied).

Concrete cost: a future refactor that drops the guard double-emits session.start on every eager-path startup (catch-up at settle + Config.initialize's logStartSession once the SDK is up), and no test turns red.

Fix (belongs in an existing non-deferred init test, e.g. 'shares a single in-flight init across concurrent callers'):

expect(emitSessionStart).not.toHaveBeenCalled();
中文说明

该新测试只钉住了补发守卫的正向一侧:没有测试断言非延迟(non-deferred)的 initializeTelemetry 不会发出补发的 session.start。实测变异:移除 sdk.ts 中的 if (config.isTelemetryInitializationDeferred?.()) 守卫后,相关套件全部通过(sdk.test.ts 64/64)。

具体代价:未来若重构删除该守卫,每次 eager 路径启动都会双发 session.start(SDK 就绪时补发一次 + SDK 就绪后 Config.initializelogStartSession 再一次),且没有任何测试变红。

修复(应加在既有的非延迟初始化测试中,如 'shares a single in-flight init across concurrent callers'):expect(emitSessionStart).not.toHaveBeenCalled();

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +864 to +865
attribute. A resumed persisted conversation includes `session.previous_id` on
its `session.start` event. `/clear` and other replacement flows intentionally

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 sentence overstates when session.previous_id is emitted. The common /resume case keeps the same session id, and the new same-id guard in Config.startNewSession intentionally suppresses previous_id there (pinned by this PR's own new test); cold-start resumptions (--resume/--continue/--fork-session via Config.initialize) also never carry it.

Concrete cost: an operator building resume-lineage queries on session.previous_id from this doc gets zero rows for ordinary same-id resumes and concludes telemetry is broken or mis-models continuation. The design doc in this same PR states the invariant correctly ("never equal to the new session.id"); this page does not.

Suggested change
attribute. A resumed persisted conversation includes `session.previous_id` on
its `session.start` event. `/clear` and other replacement flows intentionally
attribute. A resumed persisted conversation includes `session.previous_id` on
its `session.start` event only when the resumed session id differs from the
current one. `/clear` and other replacement flows intentionally
中文说明

该句对 session.previous_id 的发射条件表述过度。最常见的 /resume 场景保持同一 session id,而 Config.startNewSession 中新增的同 id 守卫在该场景下有意不发 previous_id(本 PR 新增测试已钉住此行为);冷启动续接(--resume/--continue/--fork-session,经 Config.initialize)同样不携带。

具体代价:按此文档基于 session.previous_id 构建续接谱系查询的使用者,在普通同 id 续接上会得到零行结果,进而认为遥测损坏或对续接建模有误。同一 PR 的设计文档对该不变量表述正确("永不等于新的 session.id"),本页没有。

— qwen3.8-max via Qwen Code /review (v0.21.6)

@wenshao

wenshao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Runtime verification report (maintainer review)

I built a real end-to-end environment for this PR instead of relying on the unit suite alone: the actual CLI built from this PR's head, the real OpenTelemetry SDK, and two independent capture surfaces — the shipped FileLogExporter (--telemetry-outfile) and a real OTLP/HTTP receiver on 127.0.0.1:4318 that records the exact wire payload. A/B baseline is the PR's merge-base (41f9b83, verified to reproduce GitHub's canonical diff: 12 files, +232/−2), built into a second packages/core/dist so the two arms are swappable without a rebuild.

Bottom line: the feature itself works — session.start / session.end / session.previous_id all land on the wire exactly as designed, and /clear correctly does not claim continuation. But two lifecycle paths are wrong at runtime, and both are the same class of defect the PR's own risk section says it prevents. I'd hold merge for the first one.


Scenario matrix (real runs, both arms)

scenario matrix

Scenario deferTelemetryInitialization start end Runs Verdict
headless qwen -p '…' false 1 1 5/5 ✅ OK
qwen -i '…' (prompt-interactive) false 1 1 1/1 ✅ OK
interactive TUI (bare qwen) true 2 1 10/10 duplicate session.start
/clear (replacement) 1 (no previous_id) 1 ✅ OK
/resume (continuation) 1 (+ session.previous_id) 1 ✅ OK
ACP child (daemon-spawned) true 1 1 2/2 ✅ OK
daemon qwen serve (daemon:<pid>) not implemented 0 1 2/2 session.end with no session.start

BEFORE arm (merge-base): zero session.* records in every scenario — confirming the harness is measuring this PR and nothing else.


✅ What is confirmed working

Real OTLP/HTTP payloads captured off the wire for one interactive session (start → /resume → /quit):

otlp wire

{
  "timeUnixNano": "1786120488084000000",
  "body": { "stringValue": "Session started." },
  "attributes": [
    { "key": "event.name",          "value": { "stringValue": "session.start" } },
    { "key": "session.id",          "value": { "stringValue": "0788fc52-e991-431e-a169-0adbdce821ab" } },
    { "key": "session.previous_id", "value": { "stringValue": "1d289ffd-6a41-46e6-9d3d-cf31b870e4ee" } }
  ],
  "droppedAttributesCount": 0
}
  • event.name + session.id present on every record, on both the file exporter and the OTLP wire.
  • session.previous_id appears only on a genuine /resume continuation; /clear emits a bare session.start. Verified end-to-end, not just at the emitSessionStart() unit level.
  • Existing qwen-code.config, qwen-code.slash_command, gen_ai.* correlation and the RUM events are unchanged — no regression observed in any run.
  • The PR's declared focused suite is green as stated: 643 passed.

tui resume


❌ Finding 1 (blocking) — the interactive TUI emits session.start twice per session

10 out of 10 interactive runs. Reproduced on a clean, uninstrumented build, and visible on the OTLP wire (two records 114 ms apart with an identical session.id).

Root cause — stack traces captured at every emitSessionStart call in one TUI run:

root cause

deferTelemetryInitialization is isAcpMode || (interactive && !question) (packages/cli/src/config/config.ts:2201), so it is true for both ACP and the interactive TUI — but the two paths order config.initialize() differently relative to the deferred telemetry init:

  • ACPinitializeTelemetry() runs after config.initialize() has already returned, so logStartSession() was gated out by isTelemetrySdkInitialized(). The new sdk.ts fallback is the only emitter. ✅ This is the case the PR was written for, and it works.
  • Interactive TUIstartPostRenderPrefetches() schedules initializeTelemetry(), and AppContainer's mount effect awaits config.initialize() afterwards. Both emitters fire:
    1. sdk.ts:92 deferred fallback → session.start
    2. AppContainerconfig.initialize()logStartSession()loggers.ts:240session.start again

That the second one runs is directly observable without any instrumentation: qwen-code.config is present in the TUI's telemetry output, which can only happen if logStartSession() ran past the isTelemetrySdkInitialized() gate.

Why it matters: any consumer that counts sessions, or derives session duration from start/end pairs, double-counts every interactive session — which is the majority path. qwen -i '…' (same TUI, deferred=false) emits exactly one, which isolates the trigger to the deferred branch.

❌ Finding 2 — qwen serve emits session.end with no session.start

createDaemonTelemetryRuntimeConfig() (packages/cli/src/serve/run-qwen-serve.ts:479) builds its own TelemetryRuntimeConfig and does not implement the newly-added optional isTelemetryInitializationDeferred?(), so config.isTelemetryInitializationDeferred?.() is undefined and the fallback never fires. The daemon also never constructs a Config for its own daemon:<pid> session, so logStartSession() never runs for it either. shutdownTelemetry() meanwhile emits session.end unconditionally from getCurrentSessionId(), which the daemon did set.

Result on SIGTERM (2/2 runs):

session.end   session.id=daemon:2644179     <- no matching session.start

This is exactly the "a session with an end but no start" case the PR's Risk & Scope section says it avoids — it's just avoided for the TUI/ACP paths only.

⚠️ Finding 3 — the emission wiring is largely untested

I reverted each production hunk one at a time and re-ran the PR's declared suite:

mutation matrix

# Reverted hunk Suite
M1 session-events.ts — drop the session.previous_id attribute 1 failed ✅ killed
M2 config.ts — delete logSessionEnd(this) on session switch 643 passed survived
M3 config.ts — always pass previousSessionId (no gate) 1 failed ✅ killed
M4 config.ts — drop only the self-link (ids differ) guard 1 failed ✅ killed
M5 config.ts — drop only the sessionData continuation gate 643 passed survived
M6 sdk.ts — delete the deferred-init emitSessionStart 1 failed ✅ killed
M7 sdk.ts — delete the shutdown emitSessionEnd 643 passed survived
M8 loggers.ts — delete emitSessionStart in logStartSession 643 passed survived
M9 loggers.ts — make logSessionEnd a no-op 643 passed survived

Notably: every session.end emission point survives (M2/M7/M9), and the primary session.start wiring survives (M8) — in a PR whose stated goal is start/end symmetry. M5 is the one I'd fix regardless of the rest: deleting the sessionData && gate would make /clear claim continuation, which is the explicit non-goal in the design doc, and it ships green today.

The four killed mutants (M1/M3/M4/M6) map to the tests this follow-up added, and they are genuinely good — they pin exactly the two edge cases it was written for.


Candidate fix (validated on the same harness)

Not a requirement on how to fix it, just evidence that a small change closes both:

--- a/packages/core/src/telemetry/session-events.ts
+++ b/packages/core/src/telemetry/session-events.ts
+/**
+ * Session id whose `session.start` has already been emitted in this process.
+ * The deferred-init fallback in `sdk.ts` and `logStartSession()` can both run
+ * for the same session (the interactive TUI orders `config.initialize()` after
+ * the post-render telemetry init), so the emit must be idempotent per id.
+ */
+let startedSessionId: string | undefined;
+
 export function emitSessionStart(
   sessionId: string,
   previousSessionId?: string,
 ): void {
+  if (startedSessionId === sessionId) return;
+  startedSessionId = sessionId;
   const attributes: LogAttributes = {

 export function emitSessionEnd(sessionId: string): void {
+  if (startedSessionId === sessionId) startedSessionId = undefined;
   logs.getLogger(SERVICE_NAME).emit({

--- a/packages/cli/src/serve/run-qwen-serve.ts
+++ b/packages/cli/src/serve/run-qwen-serve.ts
     getSessionId: () => daemonSessionId,
+    isTelemetryInitializationDeferred: () => true,
     isInteractive: () => false,

Re-run of the full matrix against the patched build:

fix verified

Every scenario becomes start=1 end=1, including daemon:<pid>, and /resume still carries session.previous_id (regression control). One caveat worth knowing: session-events.test.ts reuses 'session-2' across two cases with no module-state reset, so the idempotence guard makes the second case fail until the ids are made distinct (or a test-only reset is exported) — that's the fix surfacing a latent test-isolation issue, not a defect in the guard.

Nit (non-blocking)

The OTel General Session convention does not define a body for session.start / session.end; this PR sets "Session started." / "Session ended.". Harmless for OTLP consumers, but worth a conscious decision since the design doc claims strict conformance.


Verdict

Design, semantics and documentation are sound, and the two edge cases this follow-up targets are genuinely fixed and genuinely tested. Finding 1 should be fixed before merge — it is deterministic on the primary interactive path and inverts the semantics of the very event being added. Finding 2 is a small, self-contained gap. Finding 3 is worth at least one wiring-level test for session.end and one for the /clear non-continuation rule.

Reproduction
# arms: PR head 716b243 vs merge-base 41f9b83, built into two packages/core/dist trees
# capture A: shipped file exporter
qwen --telemetry --telemetry-target local --telemetry-outfile /tmp/t.json …
# capture B: real OTLP/HTTP JSON receiver on 127.0.0.1:4318
qwen --telemetry --telemetry-otlp-protocol http --telemetry-otlp-endpoint http://127.0.0.1:4318
# daemon arm
QWEN_TELEMETRY_ENABLED=1 QWEN_TELEMETRY_TARGET=local QWEN_TELEMETRY_OUTFILE=/tmp/d.json \
  qwen serve --port 18616   # then SIGTERM

Scenarios driven in a real pty (tmux) against a local mock OpenAI server; model traffic is irrelevant to the lifecycle records but keeps the session realistic. Pairing audit = group all session.start / session.end records by session.id and require exactly one of each.

中文版报告

PR 8616 真实环境验证报告

我为这个 PR 搭建了完整的真机验证环境,没有只依赖单测:使用由 PR head 实际构建出来的 CLI真实的 OpenTelemetry SDK,并用两条独立的采集通道交叉验证 —— 仓库自带的 FileLogExporter--telemetry-outfile),以及一个跑在 127.0.0.1:4318真实 OTLP/HTTP 接收端(记录完全一致的线上 payload)。A/B 基线取 PR 的 merge-base(41f9b83,已核对能复现 GitHub 的规范 diff:12 files, +232/−2),单独编译成第二份 packages/core/dist,切换时无需重新构建。

结论: 功能本身是对的 —— session.start / session.end / session.previous_id 都按设计出现在 wire 上,/clear 也正确地不声明 continuation。但有两条生命周期路径在运行时是错的,而且恰好都属于 PR 风险说明里声称已经规避的那一类缺陷。第一条建议合并前修掉。

场景矩阵(真实运行,双臂对比)

场景 deferTelemetryInitialization start end 次数 结论
headless qwen -p '…' false 1 1 5/5 ✅ 正常
qwen -i '…' false 1 1 1/1 ✅ 正常
交互式 TUI(裸 qwen true 2 1 10/10 session.start 重复
/clear(替换) 1(无 previous_id 1 ✅ 正常
/resume(续接) 1(带 session.previous_id 1 ✅ 正常
ACP 子进程(daemon 拉起) true 1 1 2/2 ✅ 正常
daemon qwen servedaemon:<pid> 未实现 0 1 2/2 只有 end 没有 start

BEFORE 臂(merge-base)在所有场景下 session.* 记录数均为 0,证明这套 harness 度量的确实是本 PR 的改动。

✅ 已确认正常的部分

  • 每条记录的 event.name + session.id 都在,文件导出和 OTLP wire 两条通道一致。
  • session.previous_id 出现在真正的 /resume 续接上;/clear 发出的是不带 previous_idsession.start。这是端到端验证的,不只是 emitSessionStart() 的单元级别。
  • 现有的 qwen-code.configqwen-code.slash_commandgen_ai.* 关联字段和 RUM 事件均无变化,所有运行都未观察到回归。
  • PR 声明的定向测试套件确实是绿的:643 passed

❌ 问题 1(建议阻塞合并)—— 交互式 TUI 每个 session 发两次 session.start

10 次交互式运行 10 次复现,在去掉插桩的干净构建上同样复现,并且在 OTLP wire 上可见(相隔 114 ms 的两条记录,session.id 完全相同)。

根因:deferTelemetryInitializationisAcpMode || (interactive && !question)packages/cli/src/config/config.ts:2201),对 ACP 和交互式 TUI 都是 true,但两条路径里 config.initialize() 相对延迟 telemetry 初始化的顺序不一样:

  • ACP —— initializeTelemetry()config.initialize() 返回之后才跑,此时 logStartSession() 已被 isTelemetrySdkInitialized() 挡掉,sdk.ts 的兜底是唯一发射点。✅ 这正是 PR 想修的场景,确实修好了。
  • 交互式 TUI —— startPostRenderPrefetches() 先排入 initializeTelemetry()AppContainer 的 mount effect 之后才 await config.initialize(),于是两个发射点都触发:
    1. sdk.ts:92 延迟兜底 → session.start
    2. AppContainerconfig.initialize()logStartSession()loggers.ts:240 → 又一条 session.start

不用任何插桩也能看出第二条确实跑了:TUI 的 telemetry 输出里存在 qwen-code.config,而这只有在 logStartSession() 越过 isTelemetrySdkInitialized() 闸门时才可能发生。

影响:任何按 session 计数、或用 start/end 配对算 session 时长的消费端,都会把每个交互式 session 重复计一次 —— 而交互式是主路径。作为对照,qwen -i '…'(同样是 TUI,但 deferred=false)只发一条,说明触发条件就是 deferred 分支。

❌ 问题 2 —— qwen serve 只发 session.end,没有 session.start

createDaemonTelemetryRuntimeConfig()packages/cli/src/serve/run-qwen-serve.ts:479)自己构造 TelemetryRuntimeConfig,没有实现本 PR 新加的可选方法 isTelemetryInitializationDeferred?(),因此 config.isTelemetryInitializationDeferred?.()undefined,兜底不会触发;daemon 也从不为自己的 daemon:<pid> session 构造 Config,所以 logStartSession() 同样不会跑。而 shutdownTelemetry() 却无条件用 getCurrentSessionId()session.end —— 这个值 daemon 设置过的。

SIGTERM 后的结果(2/2 复现):

session.end   session.id=daemon:2644179     <- 没有对应的 session.start

这正是 PR 风险说明里写着"避免出现只有 end 没有 start 的 session"的那种情况,只是这次只在 TUI/ACP 路径上被避免了。

⚠️ 问题 3 —— 发射链路的测试覆盖不足

我逐个回滚生产代码 hunk 并重跑 PR 声明的测试套件:

# 回滚内容 套件
M1 session-events.ts 去掉 session.previous_id 属性 1 failed ✅ 被杀
M2 config.ts 删掉切换时的 logSessionEnd(this) 643 passed 存活
M3 config.ts 无条件传 previousSessionId 1 failed ✅ 被杀
M4 config.ts 只去掉自引用(id 不同)守卫 1 failed ✅ 被杀
M5 config.ts 只去掉 sessionData 续接闸门 643 passed 存活
M6 sdk.ts 删掉延迟初始化的 emitSessionStart 1 failed ✅ 被杀
M7 sdk.ts 删掉 shutdown 的 emitSessionEnd 643 passed 存活
M8 loggers.ts 删掉 logStartSession 里的 emitSessionStart 643 passed 存活
M9 loggers.tslogSessionEnd 改成空实现 643 passed 存活

值得注意的是:所有 session.end 的发射点(M2/M7/M9)全部存活,主 session.start 链路(M8)也存活 —— 而这个 PR 的核心诉求恰恰是 start/end 对称。M5 我建议无论如何都补一下:删掉 sessionData && 之后 /clear 就会声明 continuation,而这是设计文档里明确写的非目标,今天却能绿着合进去。

被杀掉的四个 mutant(M1/M3/M4/M6)对应的正是这次跟进新增的测试,质量确实是好的 —— 它们精确锁住了这次要修的两个边界。

候选修复(已在同一套 harness 上验证)

不是在规定该怎么修,只是给出"小改动能同时闭合这两点"的证据:session-events.ts 里加一个按 session id 幂等的守卫,run-qwen-serve.ts 里给 daemon 的 runtime config 补上 isTelemetryInitializationDeferred: () => true(共 12 行)。打完补丁重跑整个矩阵,所有场景都变成 start=1 end=1(含 daemon:<pid>),/resume 仍然带 session.previous_id(回归对照)。

一个需要知道的副作用:session-events.test.ts 有两个用例复用了 'session-2' 且没有重置模块状态,所以幂等守卫会让第二个用例失败,直到把 id 改成不同的(或导出一个仅测试用的 reset)—— 这是修复暴露出的既有测试隔离问题,不是守卫本身的缺陷。

小建议(不阻塞)

OTel General Session 约定并未为 session.start / session.end 定义 body,本 PR 设置了 "Session started." / "Session ended."。对 OTLP 消费端无害,但设计文档声称严格遵循约定,值得明确决策一下。

结论

设计、语义和文档都是扎实的,这次跟进针对的两个边界确实修好了、也确实有测试锁住。问题 1 建议合并前修掉 —— 它在主交互路径上是确定性复现的,而且直接推翻了所引入事件本身的语义。问题 2 是个小而独立的缺口。问题 3 建议至少为 session.end 补一个链路级测试,再为 /clear 不声明 continuation 的规则补一个。

@zjunothing

Copy link
Copy Markdown
Collaborator Author

Verification report

Environment

Reproduction and result

The maintainer reproduced two lifecycle defects: interactive TUI sessions emitted duplicate session.start records in 10/10 runs, and qwen serve emitted session.end without a matching session.start in 2/2 runs.

This follow-up:

  • makes session.start idempotent per session ID and clears that guard when the matching session ends;
  • marks daemon telemetry initialization as deferred so the daemon's initial session.start is emitted;
  • adds a regression test for duplicate starts and isolates the existing replacement-session test ID.

Tests executed

  • Local test suite — NOT RUN: the authenticated Git transport repeatedly failed while fetching the repository in this environment, so I did not claim a local test result.
  • Maintainer runtime A/B verification on the previous head — reproduced the two failures above and validated the proposed fix across the scenario matrix; see the maintainer report in this PR.

Evidence

Telemetry behavior is non-visual; no new screenshot is needed. The maintainer's real-process report includes file-exporter and OTLP/HTTP evidence and the before/after scenario matrix.

中文验证报告

验证报告

环境

复现与结果

维护者在 10/10 次交互式 TUI 运行中复现了重复 session.start,并在 2/2 次 qwen serve 运行中复现了没有对应 session.startsession.end

本次跟进:

  • 按 session ID 让 session.start 幂等,并在对应 session 结束时清除守卫;
  • 将 daemon telemetry 标记为延迟初始化,使 daemon 能发出初始 session.start
  • 增加重复 start 回归测试,并隔离原有 replacement-session 测试 ID。

已执行测试

  • 本地测试套件——未执行:当前环境中认证 Git 传输反复获取仓库失败,因此没有虚报本地测试结果。
  • 维护者在旧 head 上执行的真实 A/B 验证——复现了上述两个问题,并验证了候选修复的场景矩阵;详见本 PR 中的维护者报告。

证据

这是非视觉 telemetry 行为,不需要新增截图。维护者的真实进程报告包含文件导出器、OTLP/HTTP 证据及修复前后场景矩阵。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 7e30622, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

@wenshao

wenshao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

@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 Aug 7, 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. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [label, Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [label, Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@wenshao

wenshao commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Deep verification — merge-ready

Verdict: merge-ready — 33/33 scripted assertions passed, 0 unexpected failures, no new blocking findings.

  • Verified head: 2b160e0437ae65d4845ca2e710facee6f65f3c7f (PR head, resolved from gh pr view baseRefOid/headRefOid)
  • A/B control base: bb8f2c01292aa17a3ce74f7315ae9f06c797b4f6
  • Trial merge into current main (1cbf2e8fc7): conflict-free, lifecycle re-verified on the merged tree (11/11)
  • Environment: macOS / Node v24.18.1 — mock-free: real Config class + real OTel SDK + real OTLP HTTP exporter → loopback receiver, asserting on the decoded wire payloads
中文摘要(Chinese summary)
  • 结论:可合并。33/33 脚本断言通过,0 意外失败,无阻塞性发现。
  • A/B 核心验证:在真实 OTLP HTTP 链路上驱动真实 Config + 真实 OTel SDK,head 在线上发出全部 6 条 session 生命周期记录(初始 session.start 补发、替换会话 end→start、持久会话恢复携带 session.previous_id、同 ID 恢复零记录、shutdown 发 session.end);base 对照组 0 条记录,两侧 qwen-code.config 探针均存活,证明差异来自本 PR。init 顺序相反的幂等场景(SDK 先于 Config 初始化)只发 1 条 start,去重守卫生效。
  • 变异测试:3 处关键防护各做单行变异(去重守卫 / 同 ID 非转换守卫 / SDK 就绪补发),对应的新增测试全部变红并报出具体的 expected-vs-actual——测试非空转。
  • 门禁:PR 测试计划中的 4 个测试文件在 head 全部通过(session-events 5 / sdk 66 / loggers 78 / config 503 = 652/652);改动文件 ESLint 通过;typecheck 唯一报错(@lydell/node-pty TS7016)在 base 上逐字节一致(A/A 对照),与 PR 无关。
  • 合并验证:trial merge 到当前 main 无冲突,merged 树复测 lifecycle harness 11/11 通过。
  • 未覆盖:gRPC OTLP 协议、完整交互式 TUI/ACP 启动流程、仓库全量测试套件。
  • 完整报告与原始日志:tmp/pr8616-verify-20260809-065703/(report.md、assertions.json、verdict.txt、logs/、harnesses/)。

Central claim — A/B on the real OTLP wire

The PR exists to emit standard OTel General Session lifecycle LogRecords (session.start / session.end with event.name, session.id, and session.previous_id on genuine persisted-session continuation). One harness, byte-identical scenario, two source trees:

Cell Environment Wire oracle (OTLP JSON @ /v1/logs) Result
head 2b160e04 real Config + real SDK + real exporter 6 session.* records in exact expected order 6/6 — 11/11 assertions
base bb8f2c01 (control) same, control tree only 0 session.* records; 4 qwen-code.config records → probe live expected absence — 2/2
head, init-order flipped (SDK first) same exactly one session.start per session id (dedupe) 7/7
base, init-order flipped same 0 records; probe live 2/2
trial merge into main merge-tree of origin/main + head full 6-record lifecycle again 6/6 — 11/11

Head wire sequence:

session.start id=initial-session              <- deferred-init catch-up after SDK settle
session.end id=initial-session                <- replacement transition
session.start id=replacement-1                <- no previous_id (replacement, not continuation)
session.end id=replacement-1                  <- persisted-resume transition
session.start id=resumed-2 prev=replacement-1 <- previous_id only for genuine continuation
session.end id=resumed-2                      <- shutdown emission, before SDK teardown

Same-id resume (startNewSession('resumed-2', …) twice) adds no record; session.end always precedes session.start on transitions; every record carries event.timestamp.

Findings

No blocking or major findings. Three notes:

  1. Environmental, non-blocking — typecheck: one TS7016 (@lydell/node-pty) in shellExecutionService.ts, a file this PR does not touch. A/A comparison: tsc --noEmit error set is byte-identical on base and head (logs/tsc-*.log), so it is a pre-existing local dependency-resolution condition; the PR's changed files are type-clean.
  2. By design — disabled SDK emits nothing: both logSessionEnd and the session.start emit inside logStartSession are gated on isTelemetrySdkInitialized(), consistent with all existing events. The gate does not consume the idempotency token (pinned by the new loggers.test.ts suppression test and the init-order harness).
  3. Format: the shipped HTTP chain emits OTLP JSON; all session.* attributes arrive as string attributes per the semantic-convention shape in the design doc.

Vacuity check (mutation matrix) — new tests are load-bearing

One-line source mutations on the head worktree; each kills exactly the test that pins it (raw output in logs/mutations.log):

Mutation Test that goes red Verdict
M1: session-events.ts — dedupe guard disabled does not emit session.start twice for the same session — "expected spy to be called 1 times, but got 2 times" pinned
M2: config.tslogSessionEnd forced on every startNewSession records no lifecycle transition when resuming the current session id — "expected logSessionEnd to not be called at all, but actually been called 1 times" pinned
M3: sdk.ts — settle-time catch-up removed emits the initial session start after the SDK settles — "expected spy to be called with arguments: [ 'test-session' ]" pinned

All mutations restored afterwards; each failure names expected-vs-actual, so none failed for a vacuous reason.

Targeted gates

  • Focused suite at head (the PR's own test-plan command): 652/652 passedsession-events 5/5, sdk 66/66, loggers 78/78, config 503/503
  • ESLint on changed files: clean
  • Typecheck: only the pre-existing A/A-identical TS7016 above
  • Trial merge into current main: conflict-free; merged tree re-ran lifecycle 11/11 and the telemetry suite green

Not covered

  • Full interactive TUI/ACP startup (deferred-init ordering was driven at the Config+SDK level, both orders, not via a full interactive launch)
  • gRPC OTLP protocol (HTTP wire only; gRPC chain untouched by this PR)
  • QwenLogger (RUM) internals; repo-wide test suite; per-commit attribution of the 14-commit chain (aggregate head diff verified)

Evidence images

01-ab-head-vs-base

02-head-lifecycle

03-init-order-dedupe

04-targeted-gates

Maintainer-local round; full report, harnesses, and raw logs at tmp/pr8616-verify-20260809-065703/. Advisory evidence for humans — no merge action taken by this round.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round: no action (PR #8616)

No actionable feedback this round — no code changes were made.

  • Reviews: none listed.
  • Inline comments: none listed.
  • Failed checks: none listed (no still-red checks either).
  • Issue-level comments: one deep-verification report from @wenshao with a
    merge-ready verdict (33/33 scripted assertions passed, 0 unexpected
    failures, no blocking findings). Its three notes are informational only — a
    pre-existing environment-only TS7016 in a file this PR does not touch
    (confirmed byte-identical on base via A/A comparison), a by-design telemetry
    gate (disabled SDK emits nothing, pinned by existing tests), and an OTLP
    wire-format observation. None request a code change.
  • Deferred non-Critical feedback: Critical-only mode is active after five
    change-producing rounds. That section is an audit record, not work for this
    round, and contains no items in any case.

No conflicts were reported (--conflict false), so no base merge was
performed. The PR head remains at 2b160e0437ae65d4845ca2e710facee6f65f3c7f.

中文说明

Autofix 轮次:无操作(PR #8616

本轮没有需要处理的反馈——未做任何代码改动。

  • 评审(Reviews):无。
  • 行内评论(Inline comments):无。
  • 失败的检查(Failed checks):无(也没有持续失败的检查)。
  • Issue 级评论@wenshao 的一条深度验证报告,结论为可合并(merge-ready)(33/33 脚本断言通过,0 意外失败,无阻塞性发现)。其中三条备注均为信息性说明——一个未触碰文件上预先存在的环境性 TS7016 typecheck 警告(已通过 A/A 对照确认在 base 上逐字节一致)、一个按设计工作的遥测开关(SDK 未初始化时不发出任何事件,已由现有测试固化)、以及一条 OTLP 线上格式观察。均不要求进行代码改动。
  • 延后的非 Critical 反馈:经过 5 个产生改动的轮次后已进入仅处理 Critical 的模式。该部分为审计记录,不属于本轮工作,且其中也没有任何条目。

未报告冲突(--conflict false),因此未执行 base 合并。PR 头提交仍为
2b160e0437ae65d4845ca2e710facee6f65f3c7f

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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


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

pull Bot pushed a commit to Stars1233/qwen-code that referenced this pull request Aug 9, 2026
…await (QwenLM#8764)

* fix(external-context): read the response body with a reader, not for-await

Async-iterating a ReadableStream needs [Symbol.asyncIterator] on the
TYPE, and whether it is there depends on which lib set the program
resolves — @types/node's stream has it, the DOM lib's needs
lib.dom.asynciterable. That resolution flipped underneath this file on
2026-08-08: QwenLM#8693 installed @types/jsdom at the root, vitest's types
pull the jsdom types in wherever they exist, and jsdom's carry
/// <reference lib="dom" />. QwenLM#8693 shipped the tsconfig `types` guard in
the same commit, so main stayed green — but the guard travels with the
BRANCH while node_modules travel with the TRUSTED BASE in the autofix
verification build, so every managed branch behind QwenLM#8693 failed that
build with TS2504 on this line. Two legs measured on run 31276008548:
63 minutes of accepted agent work discarded per round, 18 more minutes
burned by a repair step that cannot fix a failure outside the PR's diff
(QwenLM#8614 reached attempt 13 that way; QwenLM#8616 died identically).

Reproduced locally in both directions before changing anything:
@types/jsdom installed + guard removed = the gate's exact error,
character for character; with the reader loop the same poisoned setup
builds clean. The guard stays — belt and suspenders — but the build no
longer depends on it, or on which lib set any future environment
resolves.

Behavior is unchanged and now pinned by tests the file never had:
multi-chunk assembly, the exact MAX_RESPONSE_BYTES boundary (bound is
strictly-greater), invalid-UTF-8 rejection, and the easy one to drop in
this rewrite — cancelling the stream on early exit, which `for await`
did implicitly via iterator return(). Mutation-tested: removing the
cancel fails exactly that test against an endless producer.

The package's other for-awaits iterate process.stdin (a Node stream,
async-iterable in every lib set) and are untouched.

* fix(external-context): await stream cancellation before rejecting the request

On early exit from the reader loop (the oversize throw) cancellation was
started fire-and-forget, so postJson() rejected while the stream's
teardown was still settling — `for await` had awaited its implicit
iterator return() before propagating. An immediate retry could overlap
the previous response transport's unfinished cancellation. Await
reader.cancel() before releaseLock(), and pin the sequencing with a
deferred-cancel regression test that fails against the fire-and-forget
form.

Also cover read() rejecting after a partial chunk was received: the
error maps to the request-did-not-complete transport error rather than
EOF-then-parse of the partial JSON, and the reader lock is still
released.

* fix(external-context): drop the types guard the reader rewrite made obsolete

The `"types": ["node"]` override existed solely to keep @types/jsdom's
lib.dom out of this program while http-client.ts read the response body
with `for await` — the DOM lib's ReadableStream is not async-iterable,
and the flip broke the build with TS2504 (QwenLM#8693). The reader loop that
replaced the `for await` types identically in every lib set, so the
guard is no longer load-bearing: with it removed, lib.dom re-enters the
program and the package still builds cleanly. Drop it with its stale
comment instead of leaving maintainers two contradicting stories about
whether it is needed.

Also export MAX_RESPONSE_BYTES and import it in the boundary tests
instead of re-declaring it locally, so the tests pin the real constant
rather than a copy that can silently drift.

* test(external-context): make the invalid-UTF-8 test pin fatal decoding

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Could not produce a passing fix for this feedback (round 6/100). This item now needs a human; the loop stays engaged and still picks up new feedback and base conflicts, but will not retry this item on its own.

What I found before stopping:
Qwen failed during address-review: status 125.

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31322652532


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Could not produce a passing fix for this feedback (round 7/100). This item now needs a human; the loop stays engaged and still picks up new feedback and base conflicts, but will not retry this item on its own.

What I found before stopping:
Qwen failed during address-review: status 125.

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31323352713


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

…ession-lifecycle

# Conflicts:
#	packages/core/src/telemetry/loggers.test.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 8/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 8/100 轮)。改动内容与我反驳保留之处如下:

Round summary — PR #8616 (Critical-only mode)

This round ran in Critical-only mode after five change-producing rounds. All actionable feedback sections (reviews, inline comments, issue-level comments, failed checks, still-red checks) were empty, and the Deferred non-Critical feedback audit record listed no items. Per the Critical-only rules, no code changes, thread resolutions, or comment replies were made for deferred items.

The round's in-scope work was the requested base-conflict resolution (--conflict true).

Feedback dispositions

# Feedback point Decision
(none actionable this round)

Conflict resolution

origin/main had advanced 25 commits past the branch's last merge. Merging it produced exactly one content conflict:

  • packages/core/src/telemetry/loggers.test.ts — both sides inserted new, independent describe blocks at the same location: the branch added its session lifecycle wiring tests, while main (via feat(acp): Protect against repeated tool execution failures #8469) added logRepeatedToolFailureGuard and logLoopDetected tests. Resolution: keep both blocks verbatim, closing the branch's block before main's blocks. No test logic on either side was modified.

All other files auto-merged. The seven files touched by both sides (config.ts, constants.ts, index.ts, loggers.test.ts, loggers.ts, sdk.test.ts, sdk.ts in packages/core) were each inspected after the merge: main's exporter-selector guard (#8703) and repeated-tool-failure-guard telemetry coexist with the branch's session-lifecycle catch-up, and their only interaction (settle-time emitSessionStart running after startSdkWithExplicitExporters) is ordering-safe because telemetryInitialized is set before the catch-up runs.

Resulting merge commit: 33916e2740 (one additive merge commit; no history rewritten). The branch's net diff against origin/main is byte-identical to the pre-merge PR diff (13 files, +488/−5).

Verification

  • git merge --no-edit origin/main — one conflict, resolved as described above; pre-commit hook passed on the merge commit
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • cd packages/core && npx vitest run src/telemetry src/config — 1477/1480 passed; the 3 failures were storage.test.ts home-path assertions tripping over the agent session's QWEN_HOME redirect (environmental, merge does not touch storage code)
  • cd packages/core && env -u QWEN_HOME npx vitest run src/config/storage.test.ts — 66/66 passed, confirming the failures above are environmental
  • cd packages/core && env -u QWEN_HOME HOME=<writable tmp> npx vitest run src/telemetry src/config — 35 files, 1480/1480 passed (clean-CI surrogate: writable home, no QWEN_HOME)
  • Integration tests — not run: the merged behavior is fully covered by the unit tests above; nothing in the PR's diff is exercised only through the bundled CLI
  • npm run generate:settings-schema — not needed: no settings source (settingsSchema.ts / settings.ts) changed in the merge
中文说明

轮次总结 — PR #8616(仅处理 Critical 模式)

本轮在完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。所有可执行反馈区域(评审、行内评论、issue 级评论、失败检查、持续失败检查)均为空,Deferred non-Critical feedback(延后的非 Critical 反馈)审计记录中也未列出任何条目。按照仅处理 Critical 的规则,未对延后条目做任何代码改动、线程解决或评论回复。

本轮范围内的工作是所请求的与 base 分支的冲突解决(--conflict true)。

反馈处置

# 反馈点 决定
(本轮无可执行条目)

冲突解决

origin/main 在本分支上次合并之后前进了 25 个提交。合并时产生了恰好一处内容冲突:

  • packages/core/src/telemetry/loggers.test.ts — 双方在同一位置各自插入了新的、相互独立的 describe 块:分支添加了自身的 session lifecycle wiring(会话生命周期接线)测试,而 main(经由 feat(acp): Protect against repeated tool execution failures #8469)添加了 logRepeatedToolFailureGuardlogLoopDetected 测试。解决方式:原样保留双方所有测试块,先闭合分支的测试块,再放置 main 的测试块。双方任何一方的测试逻辑均未做修改。

其余文件全部自动合并成功。双方都触碰过的 7 个文件(packages/core 中的 config.tsconstants.tsindex.tsloggers.test.tsloggers.tssdk.test.tssdk.ts)在合并后逐一检查:main 的导出器选择器防护(#8703)与重复工具失败守卫遥测,与分支的会话生命周期补偿发送(catch-up)共存;两者唯一的交互(settle 时的 emitSessionStartstartSdkWithExplicitExporters 之后运行)在顺序上是安全的,因为 telemetryInitialized 在补偿发送之前就已置位。

产生的合并提交:33916e2740(单个增量合并提交;未改写历史)。分支相对于 origin/main 的净差异与合并前的 PR 差异逐字节一致(13 个文件,+488/−5)。

验证

  • git merge --no-edit origin/main — 出现一处冲突,按上述方式解决;合并提交通过 pre-commit 钩子
  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • cd packages/core && npx vitest run src/telemetry src/config — 1477/1480 通过;3 个失败是 storage.test.ts 的家目录路径断言与 agent 会话的 QWEN_HOME 重定向相碰撞(环境问题,合并未触碰 storage 相关代码)
  • cd packages/core && env -u QWEN_HOME npx vitest run src/config/storage.test.ts — 66/66 通过,证实上述失败为环境问题
  • cd packages/core && env -u QWEN_HOME HOME=<可写临时目录> npx vitest run src/telemetry src/config — 35 个文件、1480/1480 全部通过(干净 CI 的等价环境:可写家目录、无 QWEN_HOME
  • 集成测试 — 未运行:合并涉及的行为已被上述单元测试完整覆盖,PR 差异中没有任何仅通过打包后 CLI 才能验证的内容
  • npm run generate:settings-schema — 无需运行:合并未改动任何 settings 源文件(settingsSchema.ts / settings.ts

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


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

@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.

Reviewed. Suggestions are inline. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally. Not explored to full depth (tool budget reached): This PR adds OpenTelemetry General Session lifecycle even...: did not execute the core test suite to confirm the new tests pass (static review only).. Test Plan (not a blocker): src/telemetry/session-events.test.tsno such file or directory; src/telemetry/sdk.test.tsno such file or directory; src/telemetry/loggers.test.tsno such file or directory; src/config/config.test.tsno such file or directory.

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。 未探索到全部深度(达到工具调用预算):This PR adds OpenTelemetry General Session lifecycle even...:did not execute the core test suite to confirm the new tests pass (static review only).。 Test Plan(非阻断):src/telemetry/session-events.test.tsno such file or directory; src/telemetry/sdk.test.tsno such file or directory; src/telemetry/loggers.test.tsno such file or directory; src/config/config.test.tsno such file or directory

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment on lines +2306 to +2308
config.startNewSession('resumed-session-id', {
conversation: { messages: [] },
} as unknown as ResumedSessionData);

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] R6-2: The resume-path test asserts only the logStartSession lineage argument and never asserts that logSessionEnd is called for the outgoing session, leaving the resume transition's session.end emission unguarded — probe-verified: gating logSessionEnd on isSessionTransition && !sessionData (dropping the end on every persisted resume) leaves all tests in this file green, and adding the missing assertion flips red against that mutant and green against the original. — Failure scenario: a refactor making that one-line change would ship silently: every /resume of a different persisted session would stop closing the outgoing session's window, leaving an unbalanced session.start/session.end stream — the unterminated-window condition the design doc warns backends about. Fix: mirror the sibling replacement test —

      const endedSessionIds: string[] = [];
      vi.mocked(logSessionEnd).mockClear();
      vi.mocked(logStartSession).mockClear();
      vi.mocked(logSessionEnd).mockImplementationOnce((cfg: Config) => {
        endedSessionIds.push(cfg.getSessionId());
      });

      config.startNewSession('resumed-session-id', {
        conversation: { messages: [] },
      } as unknown as ResumedSessionData);

      expect(endedSessionIds).toEqual([outgoingSessionId]);
      expect(vi.mocked(logSessionEnd).mock.invocationCallOrder[0]).toBeLessThan(
        vi.mocked(logStartSession).mock.invocationCallOrder[0],
      );
中文说明

该 resume 路径测试只断言了 logStartSession 的谱系参数,从未断言对离任会话调用了 logSessionEnd,resume 切换时的 session.end 发射因此没有任何测试防护——已用探针验证:把生产代码门控改为 isSessionTransition && !sessionData(即恢复持久化会话时丢弃离任会话的 end)后,本文件全部测试仍为绿;补上缺失断言后,该变异红、原始代码绿(双向翻转)。触发场景:一次单行重构即可悄悄改变行为——此后每次 /resume 到不同的持久化会话都不再关闭离任会话的窗口,产生不配对的 session.start/session.end 流——正是设计文档提醒后端防范的"未终结窗口"。修复:对照同组的 replacement 测试补齐——切换前 mockClear() logSessionEnd,用 mockImplementationOnce 捕获 cfg.getSessionId(),断言 expect(endedSessionIds).toEqual([outgoingSessionId]),并加 end 先于 start 的 invocationCallOrder 检查。

— qwen3.8-max via Qwen Code /review (v0.21.8)

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round: no action needed (PR #8616)

This round's actionable feedback is empty:

  • Reviews: none newer than the last evaluation (2026-08-08T23:11:13Z).
  • Inline comments: none.
  • Issue-level comments: none.
  • Failed checks: none.
  • Still-red checks: none.

The PR is in Critical-only mode after five change-producing rounds. The deferred non-Critical items listed in this round's audit section (the automated reviewer's review pullrequestreview-4892440173 and inline comment rc:3745122263 on packages/core/src/config/config.test.ts:2308) were intentionally left untouched — per the Critical-only rules, they are an audit record, not work: no code changes, no thread resolutions, and no comment replies for them this round. They remain open for human follow-up.

No commits were made and the branch was not modified.

中文说明

Autofix 轮次:无需处理(PR #8616

本轮可处理的反馈为空:

  • 评审(Reviews): 自上次评估(2026-08-08T23:11:13Z)以来没有新的评审。
  • 行内评论(Inline comments): 无。
  • Issue 级评论: 无。
  • 失败的检查(Failed checks): 无。
  • 持续失败的检查(Still-red checks): 无。

该 PR 在完成 5 个产生改动的轮次后已进入仅处理 Critical 的模式。本轮审计区域中列出的延后非 Critical 条目(自动化评审器的评审 pullrequestreview-4892440173,以及针对 packages/core/src/config/config.test.ts:2308 的行内评论 rc:3745122263)被有意保持不动——按照仅处理 Critical 的规则,它们属于审计记录而非本轮工作:本轮不为它们修改代码、不解决讨论串,也不回复评论。这些条目保持开放,留待人工跟进。

未产生任何提交,分支未做改动。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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


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

pull Bot pushed a commit to bit-cook/qwen-code that referenced this pull request Aug 10, 2026
…eak (QwenLM#8816)

* feat(ci): A/B deterministic gate rejections against the pre-round ref

A deterministic rejection in the autofix verification gate is only
chargeable to the round if the same check passes without the round's
commit. The gate charged every red to the fix unconditionally, and run
31276008548 measured what that costs when the premise is false: PR
8614's branch predated QwenLM#8693's tsconfig guard while node_modules came
from the post-QwenLM#8693 trusted base, so `npm run build` was equally red at
origin/<branch> — 63 minutes of accepted agent work discarded, an
18-minute repair burned on a failure the repair agent is forbidden to
touch (it may only amend the round's own fix), thirteen rounds in a
row, and the same again on the QwenLM#8616 leg.

On rejection the gate now re-runs the failing check at origin/<branch>
(the branch as pushed, before the round) in the same environment:

- baseline green: today's path exactly — outcome=failed,
  retryable=true, the repair pass gets its chance.
- baseline red too: outcome=failed with preexisting=true and NO
  retryable. The repair step keys on retryable and is skipped — it
  cannot reach a failure outside the round's diff by construction —
  and gate-rejection.md says outright that the branch needs a base
  update (merge main), which flows into the failure comment as-is.

Fail-closed toward today's semantics: any A/B infrastructure problem
(missing ref, checkout failure) charges the fix as before, and a
restore failure after the baseline run rejects outright since the tree
can no longer be trusted. The round's work is still not pushed — this
changes the verdict's honesty and cost, not the push policy.

Tested by executing the real script in a real two-remote git repo with
an npm stub whose failures are keyed by commit SHA: round-caused red
(baseline green), pre-existing red (both red), and the untouched green
path. Mutation-tested, 3 of 3 caught: skipping the A/B, claiming
pre-existing without measuring, and dropping the tree restore.

* Address review: bound the A/B to checks it can honestly compare

All seven findings verified before fixing; the three Criticals were
each a way the A/B compared something other than the check that failed.

R1-1 — the contracts check feeds on stdin, which its first run drains;
the baseline leg re-ran against EOF and checked an empty file list.
R1-3 — the schema check's verdict rides on packages/core/dist, which
the core-rebuild guard built from ROUND sources and which, being
gitignored, survives the detach. Both checks are now A/B-exempt
(run_check_no_ab): their baseline verdicts prove nothing, and their
rejections stay where the repair agent can actually act on them.

R1-2 — a workspace the round ADDS does not exist at the baseline, and
npm exits 1 there with "No workspaces found" (measured; --if-present
forgives a missing script, not a missing workspace) — a round-caused
failure misread as pre-existing, skipping the one repair that can fix
the round's own package. The per-package loop now A/Bs only when the
workspace exists at origin/<branch>.

R1-4 — a chatty PASSING baseline used to flood the tail -c 3000
evidence window and push the actual failure text out of
gate-rejection.md, the sole carrier into the repair feedback, the PR
comment, and the next round's LAST_REJECTION. The baseline transcript
now goes to a side log and only a FAILING tail is merged back, where it
is the evidence.

R1-5 — the pre-existing paragraph pushed gate-rejection.md past the
report's head -c 3500 cap, truncating the closing fence for branch
names past 44 characters. Cap raised to 3900, invariant comment
updated with the new arithmetic.

R1-6 — preexisting=true had no read site. It now flows verify →
Finalize verification → the failure report, whose headline swaps the
generic gate clause for "PRE-EXISTING failure … needs a base update
(merge main)".

R1-7 — the no-round-commit guard was unpinned (deleting it kept all
tests green). Now exercised through the core-rebuild path, the one
A/B-eligible check that runs before the commit gate.

Four new behavioral scenarios (chatty baseline, no-commit round,
A/B-exempt checks, round-added workspace) plus workflow pins for the
forwarding, the clause, and the cap. Mutation-tested, 4 of 4 caught:
schema back to A/B (3 tests), guard dropped, side log reverted,
no-commit guard dropped.

* Address review round 2: A/B only what it can prove, prove what it claims

Ten findings across two rounds, each verified before fixing. The three
deepest share one lesson: the A/B is only sound for a check whose
inputs travel entirely with the git ref, and whose failure it can
IDENTIFY, not merely observe.

R2-1 — rc=1 at both legs does not make them the same failure: the
branch can fail for reason A while the round fails for reason B, and a
baseline infrastructure hiccup is a nonzero exit too. Pre-existing now
requires a MATCHING failure identity — tsc diagnostics normalized to
file + error code (positions shift with the round's edits), compared
via comm(1) on a per-check transcript. No diagnostics on either side
means identity cannot be established and the round stays charged.

R2-2 / R2-7 — gitignored dist survives the detach carrying the ROUND's
build, so any dist-consuming check A/Bs reverted sources against
round-built artifacts: package tests (channel-base resolved through
dist exports) and typecheck (sdk-typescript resolves core's d.ts —
probe-verified three-arm flip). Both are now A/B-exempt, as is lint,
leaving `npm run build` — the incident class, and the one check that
rebuilds its own inputs from the checked-out sources — as the sole A/B
candidate. The workspace-existence guard dissolves with it.

R2-3 — the fixture inherited the caller's global git config; a failing
global pre-commit hook broke all seven cases. The harness now isolates
GIT_CONFIG_GLOBAL/SYSTEM for every git child, and the suite is proven
green under a deliberately hostile hooksPath.

R2-4 — Finalize verification now selects preexisting from the same
attempt whose outcome it selects (repair verification included).

R2-5 / R2-8 — the "merge main" advice is now conditional at both
layers: the script paragraph states the measured fact and hedges the
remedy; the report headline uses the compare the step already ran —
behind/diverged gets the base-update clause, an up-to-date branch is
told its own pre-round code needs attention.

R2-6 — the rejection document now sizes its evidence tail against its
preamble (floor 500 bytes, total under the 3900-byte render cap), so
the closing fence can no longer be truncated off by a long branch name.

R2-9 — dissolved by R2-2: package tests no longer A/B, the guard and
its uncovered positive branch are gone.

R2-10 — the baseline-evidence merge is now pinned: the pre-existing
scenario asserts the baseline leg's own failure line (keyed by its SHA)
reaches gate-rejection.md.

Eight behavioral scenarios; mutation-tested 5 of 5: identity dropped,
typecheck re-enrolled, package tests re-enrolled, evidence merge
dropped, fixed tail restored.

* Address review round 4: sharpen identity, stage the git failures, sync prose

Nine findings, all refinements — the design held, the edges did not.

Identity now keeps the diagnostic MESSAGE (file + code collide: two
unrelated TS2339s in one file compared equal, skipping a repair that
could have shipped — probe-reproduced by the review), and the fixture
emits a SHIFTED position on the baseline leg so the position strip is
load-bearing instead of decorative (deleting the sed survived every
test before; it fails one now). vite/esbuild failures still yield an
empty signature by design — documented as the fail-closed limit rather
than half-widened.

The fail_signature assignments take `|| true`: grep exits 1 on the
normal no-match case and survives errexit today only because the caller
sits in an if-condition — a future unconditional call site would crash
the gate verdict-less.

The restore-failure branch is now stageable and staged: the baseline
leg recreates (untracked) a file the branch tracks, the checkout back
refuses, and the test pins retryable-not-preexisting with the
'could not restore' label. Relaxing the branch to `|| true` fails it.

Prose synced to the mechanisms that replaced it: the render-cap
invariant restates against the dynamic tail budget (the old 3000-based
arithmetic would misguide the next retune), the no-round-commit guard
comment names the core rebuild (schema/contracts left the A/B last
round), the describe wording counts both A/B-eligible builds, and the
pre-existing clauses no longer claim "the repair pass was skipped" —
with REPAIR_PREEXISTING forwarded, repair may have RUN; they now state
the invariant that is true either way: repair may only amend the
round's own fix, so it cannot reach this failure.

Mutation-tested, 3 of 3 caught: position strip dropped, message dropped
from the identity, restore rejection relaxed.

* fix(ci): watchdog silent sandbox hangs and reap the containers they leak

Four autofix rounds have died the same way (QwenLM#8663 twice, QwenLM#8761 r3,
QwenLM#8763 r4): the agent's last output is the sandbox wrapper's
"ContainerName (regular): …" line at docker container entry, then
nothing — not one event — until the 2-hour absolute budget kills the
round. Four different runners, two image versions: systemic, not a bad
machine. Where exactly the container wedges is still unknown (that
needs docker state on the runner); what is certain from the logs is the
shape — a wedged sandbox produces NOTHING, and a legitimate run is
never silent for long (the fleet's longest tolerated quiet is the
review pipeline's 10-minute stream-idle window for thinking phases).

Two mitigations, each aimed at a measured half of the damage:

- run-agent.mjs gains an idle watchdog (QWEN_IDLE_TIMEOUT_MS, default
  20 minutes = 2x that longest legitimate silence): zero output for the
  window kills the agent with a distinct "idle-timeout … the sandbox
  likely hung at startup" detail, so the failure comment names the
  right knob and a hung round costs 20 minutes instead of 120. Polled,
  not reset-per-chunk — a busy stream should not spend its time
  re-arming timers.

- Both sandboxed jobs reap stale qwen-code-* containers at job start:
  a budget kill reaps the HOST-side docker client, not the container,
  so every killed sandbox keeps running on the persistent runner —
  observed directly when a later leg's container-name counter found
  qwen-code-0.21.8-0 already occupied and picked -1. One job per runner
  at a time makes any container alive at job start stale by definition.

Tested by executing the real run-agent.mjs end to end with stub agents:
the hang shape (one line, then silence) dies at the idle window naming
the idle limit, and a slow-but-talking agent that outputs every 400ms
across a 1500ms window survives to a clean exit — the test that
distinguishes a watchdog from a disguised absolute timer. Mutation-
tested, 3 of 3 caught: watchdog disabled, last-output tracking dropped
(the disguised-timer regression), cleanup dropped from a job.

* Address review round 5: the gate's verdict defects and the reaper's live kill

Budget-warning round — the five Criticals from both reviewers, no
suggestions (each deferred with a recorded reply).

fail_signature: `[^\n]*` in an ERE bracket expression does not mean
"rest of line" — in POSIX bracket expressions `\` is literal, so it
matched "neither backslash nor the letter n" and truncated every tsc
message at its first n. Nearly every real message has an early n
("Cannot find name", "is not assignable"), so distinct same-file
failures collapsed into identical signatures and a round-caused failure
could be labeled pre-existing, skipping the repair. grep is
line-oriented: `.*` is exactly the rest of the line. New fixture: two
messages differing only after their first n.

Pre-existing verdict: the intersection test mislabeled in both
directions. A round that ADDS a diagnostic sharing one normalized line
with the baseline was called pre-existing (repair skipped for a
round-caused, repairable failure); and `comm -12 | grep -q` under
`set -eo pipefail` SIGPIPEs comm (exit 141) once the shared output
outruns the pipe buffer, charging true pre-existing failures to the
round — the exact 18-minute repair waste the gate exists to kill.
Pre-existing now means the round's failing set is a SUBSET of the
baseline's, and the difference is captured before testing. New fixture:
a round adding a second diagnostic to a failing baseline.

Restore failure after the baseline leg: was retryable=true with HEAD
still detached at the baseline commit — the repair agent works in that
very checkout and does no git recovery, so its commit would land on the
baseline and be orphaned. Now rejected non-retryable (reject_fix grows
a third arg); the next round starts clean from the trusted checkout.
The restoreClash test pins the new semantics.

Stale-container reap: the premise "a runner runs one job at a time, so
any live qwen-code-* container is stale" holds per runner registration,
but the filter queries the docker daemon, which is per host — and this
pool runs several registrations on one OS. With per-issue/PR
serialization only, a concurrent job's sandbox is a substring match
away from `docker rm -f`. The reap now takes only provably-dead
containers (--filter status=exited/dead, both jobs) and the comment
says why a running one is left alone.

Preamble printf: the `\`` escapes sat inside a single-quoted format
where backslash is literal, so every pre-existing rejection rendered
raw backticks instead of code spans (shellcheck SC2016). Backticks
need no escaping there. Also syncs the side-log comment to the dynamic
tail_budget it actually renders.

Verified: scripts suite 140/140 (was 138; the two new fixtures and the
rewritten restoreClash test all fail against the pre-fix script),
npm run build / typecheck / lint pass, bash -n clean.

* Address review round 6: reap the kill's own orphan, tolerate the reaper

* Address review: hang-bound the reaper, unblock the kill path, pin the unpinned arms

- Wrap every docker call in the stale-container reap with timeout 30: an
  alive-but-wedged daemon blocks docker ps indefinitely, and the existing
  || guards only catch nonzero exits, not hangs (R3-1).
- Make the kill-path container removal async in run-agent.mjs: the
  spawnSync blocked the event loop between SIGTERM and the 10s SIGKILL
  backstop for up to its 30s timeout — in exactly the wedged-daemon
  scenario the watchdog exists for. The main flow awaits the removal so
  the leak warning stays deterministic (R3-6).
- Split the pre-existing gate clause for an empty CMP_R: a transient
  compare-API failure is "never measured", not "measured not-behind", and
  must not assert the branch's own code is at fault (R3-7).
- Swap the timeout breaker's closing remedy to the sandbox investigation
  when every counted timeout was idle, mirroring the round-level split
  (R3-11).
- Tests: pin the budget kill path separately from the idle kill path
  (R3-3), parameterize the idle-window parse guard over -1/0/NaN (R3-5),
  add a stderr-only liveness case (R3-12), pin the strict-subset A/B arm
  via a baseline-superset fixture knob (R3-15), and pin the breaker's
  current-round idle increment (R3-18).

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
@wenshao

wenshao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 101 passed · 0 failed · 101 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:101 通过 · 0 失败 · 101 总计

Verification report

<!-- qwen-triage:verify -->
<!-- qwen-triage:verify-substantive -->

Sandboxed verification: ✅ passed (agent verdict)merge-ready · 101 pass / 0 fail (101 scripted assertions) · verified head 33916e2740b07747d9ec8dd3bb8f33678f60ed4e (merge-ref base tip 3f6551f9cf4683870b13f8f937d210c59555ec91). Follow-up round: both round-1 findings re-measured as fixed at the new head, the two round-1 coverage gaps are now pinned by unit tests (mutation-killed), and no new finding surfaced.

中文 — 判定:✅ 通过(agent 判定)
  • 结论:merge-ready。101 项脚本断言全部通过,0 失败。
  • A/B 结论:base(3f6551f9cf)在 headless 与 deferred 两种初始化顺序下均导出 0 条 session.start/session.end,遗留 qwen-code.config 记录两侧存活(5/4 条,证明管道在 base 侧同样工作);head 导出完整配对的 8 记录生命周期流,含延续 previous_id、same-id 零转换、回跳会话的分离窗口、shutdown 收尾 end(见「Central claim」表与 01-ab-base-zero-vs-head-full-lifecycle.png)。
  • 上轮发现状态:F1(deferred TUI/ACP 首个 session.start 丢失)已修复——deferred 单元 26/26,初始 start 恰好一条且配对(02-deferred-initial-start-fixed.png),并新增 settle 前 /clear 边界探针 7/7(无"有 end 无 start");F2(same-id resume 自引用 previous_id)已修复——same-id resume 不产生任何标准记录,全流无 previous_id == session.id;上轮 M2/M3 覆盖缺口(Config 集成、shutdown 发射无单测钉住)本轮变异即红,已被新测试钉住(03-mutation-matrix-6-of-6-killed.png,6/6 killed,0 存活)。
  • 新发现:无。两条观察均定性为非发现并给出边界(legacy 初始记录在 settle 前丢弃为 base 既有行为;daemon/ACP 限制已在新设计文档中如实记录并推迟)。
  • 未覆盖:OTLP 线级序列化、真实 TUI/ACP 端到端(以精确顺序重放代替)、ACP 子进程单独驱动、仓库级 typecheck(仅 core)、逐 commit 归因(浅克隆不可达)。
Verification report

PR 8616 (follow-up round) — feat(telemetry): align session lifecycle with OpenTelemetry

Verdict: merge-ready — 101 pass / 0 fail (101 scripted assertions executed). Verified head: 33916e2740b07747d9ec8dd3bb8f33678f60ed4e. Merge-ref base tip: 3f6551f9cf4683870b13f8f937d210c59555ec91 (the metadata snapshot's baseRefOid 0a3d7bb5… predates the checkout — main advanced between snapshot and merge-ref creation; headRefOid matches HEAD^2 exactly, so the verified diff is the PR as merged onto current main).

Previous-round finding status

# round-1 finding severity status at 33916e27
F1 initial session.start deterministically dropped in deferred TUI/ACP modes (end without start) medium fixed — re-measured: deferred cell 26/26 with exactly one initial session.start, fully paired (02-deferred-initial-start-fixed.png); the unconditional settle-time catch-up plus the idempotency guard make the standard record deterministic in both race orders (driven: pre-settle logStartSession = deferred cell; post-settle redundant logStartSession = dedupe cell). New adjacent edge also probed: /clear before settle emits no records for the outgoing session and the catch-up starts the current one — 7/7, no end-without-start anywhere
F2 session.previous_id === session.id on same-id /resume low fixed — re-measured: same-id resume adds zero standard records (no end for a live session, no duplicate start), and no session.start anywhere carries previous_id == session.id (asserted in both head cells)
M2 Config-side integration unpinned (removing logSessionEnd + prev-arg left 578/578 green) completeness fixed — the same mutation now kills startNewSession > records no lifecycle transition when resuming the current session id (1/503 red, matrix row M3)
M3 shutdown emitSessionEnd unpinned (66/66 green) completeness fixed — the same mutation now kills lazy init lifecycle > ends the active session before the SDK shuts down (1/68 red, matrix row M2)
deferred items: OTLP wire, real TUI e2e, ACP child, headless race distribution not covered race distribution is now moot for the standard records (deterministic by construction, both orders driven); ACP-child limitation is now documented as a known limitation in the new design doc and deferred to a follow-up — agree with the deferral (multi-session processes need lifecycle design, out of this PR's scope)

Declined/deferred rows were re-measured, not diffed: every cell above was rebuilt and re-run at the new head; the input closure for the legacy-count comparison (loggers.ts legacy path, file-exporters, sdk-impl local target) is unchanged by this PR's diff except the additive emitSession* calls, and the base arm re-ran the identical harness.

Central claim + A/B

Central claim: with the OTel pipeline enabled, every session lifecycle transition emits standard General-Session LogRecords — session.start/session.end with session.id and event.timestamp; session.previous_id only on genuine persisted continuation and never equal to the new id; end-before-start ordering on switches; shutdown ends the active session; same-id resume records no transition; legacy events unchanged.

Harness harness-lifecycle.mjs (in this artifact dir) is mock-free: it drives the compiled packages/core dist through a real Config (initialize() → replacement startNewSession() → continuation → same-id resume → resume-back to the original id → shutdownTelemetry()), capturing records through the real FileLogExporter seam (telemetry.target=local + telemetry.outfile) — the SDK's own serialization, not a stub. Base control: scratch worktree at HEAD^1, packages/core rebuilt there against the unchanged lockfile's node_modules (the PR touches no package.json/lockfile; core's nested node_modules holds only third-party packages — no @qwen-code links — and the imported dist realpath was asserted in-harness on every arm). Scenario per arm: initial S1-initial, replacement S2 (random), continuation S3-continued (with sessionData), same-id resume of S3-continued, resume-back to S1-initial (disjoint window per the design doc).

cell build session.start session.end legacy qwen-code.config result
base headless 3f6551f9cf 0 0 5 (alive) 10/10
base deferred (TUI/ACP ordering) 3f6551f9cf 0 0 4 (alive; pre-settle initial legacy record gated — pre-existing) 11/11
head headless 33916e2740 4, paired, initial present 4, end-before-start per switch, shutdown last 5 25/25
head deferred 33916e2740 4, initial present (F1 fixed) 4, paired 4 26/26
head dedupe (redundant post-settle logStartSession) 33916e2740 4 (redundant call adds 0) 4 6 11/11
head deferred + pre-settle /clear 33916e2740 1 (current session only) 1 (shutdown) 7/7

Witnesses: 01-ab-base-zero-vs-head-full-lifecycle.png (base vs head headless side by side — base stream empty with legacy positive control, head stream [0] start S1 … [7] end S1 fully paired), 02-deferred-initial-start-fixed.png (deferred cell, round-1 F1 cell now shows [0] session.start S1-initial).

Per-arm assertions included: initial start exactly once with no previous_id; end(outgoing) index < start(new) on every switch; replacement carries no previous_id; continuation carries previous_id = outgoing id; same-id resume adds zero records; no start has previous_id == session.id; resume-back opens a second disjoint window for S1-initial with lineage; final record is session.end for the current session; every record carries event.name + session.id + ISO event.timestamp; 4 starts / 4 ends pairing; legacy positive control on both arms (base absence is a real absence).

Reviewer Test Plan, step by step

step result
1. Review implementation + design doc done; the design doc's falsifiable claims now match measured behavior, including the honest "Known limitations (daemon / ACP)" section
2. Focused vitest command 658/658 pass at head, identical command (04-focused-suite-658-green.png; round 1 measured 578 — +80 tests since)
3. Tests cover the behaviors now yes at both levels: emit-level (round 1) and integration level — mutation matrix below kills every guard deletion with its intended test
4. Typecheck, lint, build core tsc --noEmit exit 0 (empty output); eslint on all 11 changed core files exit 0 with a planted any violation caught first (@typescript-eslint/no-explicit-any), proving the gate live; head dist pre-built by CI; base control dist rebuilt in the scratch worktree (tsc + copy step) and validated in-harness by the realpath assertion and the legacy positive control

Mutation matrix (vacuity + round-1 gap re-measure)

Unmutated control green (658/658). Each mutant applied to a scratch edit, intended killer file run, restored via git checkout (tree verified clean after). 03-mutation-matrix-6-of-6-killed.png.

mutant deletion killer (first red) result
M1 sdk.ts settle-time catch-up emitSessionStart lazy init lifecycle > emits the initial session start after the SDK settles (+ concurrent-init test), 2/68 red killed
M2 sdk.ts shutdown emitSessionEnd lazy init lifecycle > ends the active session before the SDK shuts down, 1/68 red killed
M3 config.ts isSessionTransition guards (end + prev-arg) startNewSession > records no lifecycle transition when resuming the current session id, 1/503 red killed
M4 session-events.ts start idempotency guard does not emit session.start twice for the same session, 1/5 red killed
M5 session-events.ts guard reset on end emits session.start again for an id that was ended, 1/5 red killed
M6+ positive control: record body text 4 red across session-events + loggers wiring tests red as designed

6/6 killed, 0 survivors. The two round-1 coverage gaps (M2/M3 rows) are closed by the PR's new tests; attribution is exact (each mutant's first red is the test named for that behavior).

Findings

None new. Two observations, classified as non-findings with bounds:

  1. Pre-settle legacy record drop in deferred mode (4 vs 5 legacy records) — pre-existing: the base deferred cell shows the identical 4 (the initial logStartSession's legacy qwen-code.config record is gated on isTelemetrySdkInitialized() and dropped before settle). The PR does not change that path; the standard records are now deterministic regardless of the race. Not a regression, not introduced here.
  2. Same-id resume still emits the legacy qwen-code.config record — also pre-existing (base's startNewSession called logStartSession unconditionally); the design doc's "records no lifecycle transition at all" refers to the standard session.* records, which the measurements confirm are absent. Consistent with the doc's "existing events remain unchanged" contract.

Not covered

  • OTLP HTTP/gRPC wire serialization — verified through the outfile exporter seam only; session.* attributes are plain strings, low risk.
  • Real TUI/ACP end-to-end through Ink — the deferred ordering was replayed exactly (deferTelemetryInitialization + post-init initializeTelemetry, the same code order as startup-prefetch.ts/acpAgent.ts), including a pre-settle switch edge; not driven through the real UI.
  • ACP child process driven separately — its limitation (per-session Configs get a start but no end; shutdown ends the boot-time session) is now explicitly documented in docs/design/otel-session-lifecycle-design.md and deferred by the author; agree it needs a multi-session lifecycle design.
  • Repo-wide typecheck — core tsc --noEmit only; the PR touches only packages/core + docs.
  • Per-commit attribution — shallow depth-2 checkout (17 commits in metadata, only merge/base/head reachable locally); the aggregate HEAD^1..HEAD diff was verified instead.
  • Headless legacy-record race distribution — cosmetic only now (standard records deterministic); not sampled.

Methodology

Environment: CI verify container (node:22-bookworm), merge-ref checkout, npm ci + npm run build pre-run at head; base control rebuilt only packages/core in a scratch worktree at HEAD^1 with the root node_modules symlinked (lockfile untouched by the PR; no internal @qwen-code links in core's dependency set; imported-dist realpath asserted per arm). Harnesses harness-lifecycle.mjs (six cells), run-mutation-matrix.mjs (six mutants, self-restoring), run-ab.sh (capture wrapper) live in this artifact dir alongside raw logs (log-*.txt, log-gate-focused*.txt, log-typecheck-core.txt). Assertion counts: harness cells 10+11+25+26+11+7 = 90, matrix 6 kills + 1 unmutated control = 7, gates 4 (focused suite, typecheck, eslint clean, eslint liveness probe) → 101 pass / 0 fail.

Evidence images

01-ab-base-zero-vs-head-full-lifecycle

02-deferred-initial-start-fixed

03-mutation-matrix-6-of-6-killed

04-focused-suite-658-green

Qwen Code · sandboxed verification

Evidence images

01-ab-base-zero-vs-head-full-lifecycle

02-deferred-initial-start-fixed

03-mutation-matrix-6-of-6-killed

04-focused-suite-658-green

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, and for working through the many review rounds. Full gate re-run on 33916e2 — a main-merge over the deep-verified 2b160e0, so no behavioral change since the maintainer's verification.

Template ✓ — all required sections present, bilingual summary included.

Problem: real and scoped. Linked issue #8589 (triaged with type/feature-request, priority/P3, category/telemetry) documents a concrete gap: session lifecycle is only expressed through Qwen-specific events while the OpenTelemetry General Session conventions (session.start / session.end / session.previous_id) go unused. The issue predates the PR and carries maintainer-applied labels — not a solution looking for a problem.

Direction: aligned. Qwen Code already invests in OTel (OTLP exporters, gen_ai.conversation.id mapping); standard session lifecycle records are the natural completion. Telemetry is a maintainer-awareness area, but that bar is met here: this re-run was triggered by @wenshao, who also deep-verified the earlier head of this PR mock-free (33/33 wire assertions, merge-ready).

Size: core paths touched. Production logic 95 lines (config.ts 19, sdk.ts 12, session-events.ts 53, loggers.ts 8, constants/barrel 3) vs 278 test lines vs 120 docs lines — well under any escalation threshold.

Approach: scope feels right. Additive records through the existing logger idiom, an idempotency guard covering the deferred-init catch-up, and an honest design doc that defers the daemon/ACP multi-session gap instead of faking coverage. No drive-by changes. The /resume id-reuse decision (lineage edges can point backwards in time) is documented with backend guidance — the right call while /resume id minting stays out of scope.

Risk: no Stage 1e high-risk path matches. The critical findings from earlier rounds (self-referential previous_id, dropped first session.start on deferred init, daemon shutdown pairing) are all addressed at this head — verified against the diff.

Moving on to code review. 🔍

中文说明

感谢贡献,也感谢在多轮评审中的持续迭代。本次是对 33916e2 的完整 gate 重跑——该提交只是把 main 合并进了经过深度验证的 2b160e0,行为上没有变化。

模板 ✓ —— 各必需部分齐全,含中文摘要。

问题: 真实且范围明确。关联 issue #8589(已分诊:type/feature-requestpriority/P3category/telemetry)指出了具体缺口:session 生命周期目前只通过 Qwen 自定义事件表达,未采用 OpenTelemetry General Session 约定(session.start / session.end / session.previous_id)。issue 早于 PR 存在且带有维护者应用的标签,不是"为方案找问题"。

方向: 对齐。Qwen Code 已在 OTel 上有持续投入(OTLP exporter、gen_ai.conversation.id 映射),标准 session 生命周期事件是自然的补全。telemetry 属于需维护者关注的领域,但此处门槛已满足:本次重跑由 @wenshao 触发,且他已对更早的 head 做过无 mock 深度验证(33/33 线上断言,merge-ready)。

规模: 触及核心路径。生产逻辑 95 行(config.ts 19、sdk.ts 12、session-events.ts 53、loggers.ts 8、常量/桶导出 3),测试 278 行,文档 120 行——远低于任何升级阈值。

方案: 范围合理。通过现有 logger 惯用法做增量记录,用幂等守卫覆盖延迟初始化的补发,设计文档诚实地将 daemon/ACP 多 session 缺口延后处理而不是假装覆盖。无夹带改动。/resume 复用 id 的决策(血缘边可能指向过去)已记录并给出后端建议——在 /resume id 生成策略不在本次范围时,这是正确的处理。

风险: Stage 1e 高风险路径无命中。前几轮的关键问题(自引用 previous_id、延迟初始化丢失首个 session.start、daemon 关闭配对错误)在当前 head 均已解决——已对照 diff 核实。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 33916e2740b07747d9ec8dd3bb8f33678f60ed4e · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

My independent proposal for this issue was: a small emitter module, wired at the single session-rotation choke point (Config.startNewSession) plus a settle-time catch-up for deferred init and an end at SDK shutdown. This PR matches that shape almost exactly — good sign it found the natural design rather than bolting on one.

What I verified against the diff and the surrounding code:

  • All rotation paths are covered. /clear, /resume, and /branch all flow through Config.startNewSession; the only other sessionId assignment is the constructor, whose session.start comes from the settle-time catch-up in initializeTelemetry.
  • Ordering and ids are correct. logSessionEnd(this) runs before the id reassignment, so the outgoing session is ended under its own id; the test suite pins end-before-start via invocation order.
  • Lineage gate is sound. session.previous_id is forwarded only when sessionData && isSessionTransition — no self-referential previous-id on same-id resume, no continuation claim for replacement sessions. This closes the critical finding from the earlier round.
  • The idempotency guard behaves. The settle-time catch-up and logStartSession can both see the same session; the per-id token dedupes them, and suppression while the SDK is uninitialized does not consume the token (pinned by a dedicated test).
  • Conventions. emitSessionStart/emitSessionEnd use the exact established idiom — logs.getLogger(SERVICE_NAME) and an event.timestamp string attribute, same as the ~30 sibling emitters in loggers.ts. No parallel utility, no new dependency.
  • Backward compatibility. The existing QwenLogger/RUM path and session.id / gen_ai.conversation.id attributes are untouched; the runtime-status sidecar condition was refactored to the equivalent isSessionTransition predicate.

No critical blockers, no convention violations. The outstanding non-Critical suggestions in the thread (naming symmetry logStartSession/logSessionEnd, debug traces on suppression, a tracked follow-up issue for the ACP gap) are fine to land now and address later — they were already deferred by the autofix loop's critical-only mode.

The lifecycle flow, since emission ordering was the crux of the earlier criticals:

sequenceDiagram
    participant P1 as Session switch or startup
    participant P2 as Config and SDK lifecycle
    participant P3 as session-events guard
    participant P4 as OTel logs pipeline
    P1->>P2: startNewSession or initializeTelemetry
    P2->>P3: end outgoing id (real id change only)
    P2->>P3: start new id (previous_id on persisted continuation)
    P3-->>P3: per-id token dedupes the settle catch-up race
    P3->>P4: session.end then session.start LogRecords
    P1->>P2: shutdownTelemetry
    P2->>P3: end the current session context id
    P3->>P4: session.end LogRecord
Loading
Files changed (13)
File What changed
docs/design/otel-session-lifecycle-design.md New design doc: event model, resume id-reuse lineage, deferred daemon and ACP gap
docs/developers/development/telemetry.md Catalogs session.start and session.end in the logs list and the spans prose
packages/core/src/config/config.ts startNewSession ends the outgoing session before a real id change and forwards previous_id on persisted continuation
packages/core/src/config/config.test.ts Lifecycle tests: same-id resume no-op, end-before-start order, continuation lineage, rejected-switch emits nothing
packages/core/src/config/config-session-env.test.ts Adds logSessionEnd to the telemetry mock factory
packages/core/src/telemetry/constants.ts EVENT_SESSION_START and EVENT_SESSION_END constants
packages/core/src/telemetry/index.ts Barrel export for logSessionEnd
packages/core/src/telemetry/loggers.ts logStartSession gains an optional previousSessionId; new logSessionEnd
packages/core/src/telemetry/loggers.test.ts Wiring tests, including that SDK-uninitialized suppression does not consume the idempotency token
packages/core/src/telemetry/sdk.ts Settle-time catch-up session.start after SDK start; shutdown ends the current session
packages/core/src/telemetry/sdk.test.ts Pins catch-up emission after NodeSDK.start and shutdown end ordering
packages/core/src/telemetry/session-events.ts New emitters with a per-session-id idempotency guard
packages/core/src/telemetry/session-events.test.ts Attribute shape, dedupe, and restart-after-end behavior

Testing evidence

Evidence carried: the PR's own CI on the reviewed commit, quoted below (unattended run — no PR code executed here). The macOS/Windows matrix legs and the integration suite are skipped on this fork PR, consistent with every earlier commit on this branch; the ubuntu unit leg is the one that runs and it is green.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped (fork-PR gating)
Test (windows-latest, Node 22.x) ⏭️ skipped (fork-PR gating)
Integration Tests (CLI, No Sandbox) ⏭️ skipped (fork-PR gating)
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
precheck-pr / precheck ✅ success
Qwen Code CI (workflow run) ✅ success

On the behavioural claim (the records actually reach the wire): the unit suite pins the wiring but largely against mocked loggers. Two stronger signals exist — @wenshao's mock-free deep verification of the pre-merge head 2b160e0 drove a real OTel SDK against a loopback OTLP receiver and passed 33/33 scripted assertions (merge-ready), and a maintainer-triggered @qwen-code /verify sandboxed run is in flight on this exact head (run 31481525726) — it will A/B the emission claims against the base build. The only head delta since the deep verification is a merge of main, so no new behaviour is being asked of it.

Not verified here: the skipped integration matrix legs (fork gating) — covered by the in-flight verify run rather than by this review.

中文说明

代码审查

我独立提出的方案是:一个小的事件发射模块,接在唯一的 session 轮换汇聚点(Config.startNewSession),加上延迟初始化时 SDK 就绪后的补发和关闭时的 end。本 PR 的结构几乎与此完全一致——说明它找到了自然的设计,而不是硬拼上去的。

已对照 diff 与周边代码核实:所有轮换路径(/clear/resume/branch)都经过 Config.startNewSession,构造函数赋值由 settle 补发覆盖;logSessionEnd 在 id 重赋值之前调用,旧 session 以自己的 id 结束(测试用调用顺序固定了先 end 后 start);session.previous_id 仅在 sessionData && isSessionTransition 时携带——同 id resume 不自引用、替换会话不声称续接,关闭了上一轮的 Critical;幂等守卫正确去重 settle 补发竞争,且 SDK 未初始化时的抑制不消耗一次性令牌(有专门测试固定);新模块完全沿用 logs.getLogger(SERVICE_NAME) + event.timestamp 的既有惯用法,无平行工具、无新依赖;现有 RUM 路径与关联属性未动,runtime-status 条件重构语义等价。

无阻塞项、无规范违规。线程中遗留的非 Critical 建议(命名对称性、抑制路径的 debug 日志、为 ACP 缺口建跟踪 issue)可以合并后再处理。

测试证据

本评论携带的是 PR 自身在受审提交上的 CI 结果(无人值守运行,未执行 PR 代码)。macOS/Windows 矩阵与集成测试在该 fork PR 上被跳过(与本分支此前所有提交一致);实际运行的 ubuntu 单元测试为绿色。

行为性声明(记录真正到达链路)方面:单元测试固定了接线但多用 mock logger。有两个更强的信号——@wenshao 对合并前 head 2b160e0 的无 mock 深度验证(真实 OTel SDK + 回环 OTLP receiver,33/33 断言通过,merge-ready);以及维护者触发的 @qwen-code /verify 沙箱验证正在当前 head 上运行(run 31481525726),将对 base 构建做 A/B 验证。深度验证之后唯一的 head 差异是一次 main 合并,没有新行为。

此处未验证:被跳过的集成矩阵(fork 门控)——由进行中的 verify 运行覆盖,而非本评审。

Qwen Code · qwen3.8-max

Reviewed at 33916e2740b07747d9ec8dd3bb8f33678f60ed4e · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, minimal, and wire-verified; docking one point only because the integration matrix legs never ran on this fork and I'm relying on the in-flight /verify to close that last gap.

Stepping back: this is what a good fork contribution looks like. The problem was filed and triaged before any code existed, the design doc is honest about what it defers (the daemon/ACP multi-session gap, cold-start lineage) instead of overclaiming, and the implementation is 95 lines of additive, best-effort telemetry that follows the house idiom to the letter. My independent proposal for this issue and the PR's approach landed on the same shape — small emitter module, single choke point at startNewSession, settle-time catch-up, shutdown end — which tells me the design was found, not forced.

The history here also matters: five change-producing autofix rounds and three earlier critical findings (self-referential previous_id, a dropped first session.start, daemon shutdown pairing) each left a pinned regression test behind, and the maintainer's mock-free deep verification passed 33/33 wire assertions on the pre-merge head. The current head adds nothing but a merge of main. If I had to maintain this in six months, the idempotency-guard comment and the design doc's /resume lineage section are exactly the breadcrumbs I'd want.

Approving, pinned to the reviewed commit. Two housekeeping notes for the maintainer, neither blocking:

  1. Two stale CHANGES_REQUESTED reviews from this bot stand on superseded commits (the early template gate and an integration-skip concern that fork gating makes unactionable) — they may need dismissing for merge.
  2. The thread carries ~30 unresolved review-bot suggestions, most stale against the current head (the coverage they asked for was added in later rounds). The autofix loop deferred them as non-Critical by policy; a bulk-resolve pass would restore signal.
中文说明

置信度:4/5 —— 干净、最小化、且已做过线上验证;扣一分仅因为该 fork 的集成测试矩阵分支始终未运行,需要依靠进行中的 /verify 来补上最后一个缺口。

整体来看:这是一次高质量的外部贡献。问题在任何代码之前就已被提交并分诊;设计文档诚实地说明了延后项(daemon/ACP 多 session 缺口、冷启动血缘)而非夸大覆盖;实现是 95 行增量式、尽力而为的 telemetry,完全遵循项目惯用法。我对该 issue 的独立方案与 PR 的结构不谋而合——小的发射模块、startNewSession 单一汇聚点、settle 补发、关闭时 end——说明设计是被发现的,而不是硬凑的。

历史同样重要:五轮产生改动的 autofix 与三个早期 Critical(自引用 previous_id、丢失首个 session.start、daemon 关闭配对)都留下了固定的回归测试;维护者的无 mock 深度验证在合并前 head 上通过 33/33 线上断言。当前 head 仅多了一次 main 合并。若六个月后维护这段代码,幂等守卫的注释与设计文档中 /resume 血缘一节正是我想要的线索。

批准,固定在受审提交上。给维护者两条非阻塞的整理备注:

  1. 该机器人有两条停留在已被取代提交上的 CHANGES_REQUESTED 评审(早期模板门槛、以及 fork 门控下无法解决的集成跳过问题)——合并前可能需要 dismiss。
  2. 线程中有约 30 条未解决的评审机器人建议,多数相对当前 head 已过时(其要求的覆盖已在后续轮次补齐)。autofix 循环按政策将其作为非 Critical 延后;建议批量 resolve 以恢复信噪比。

Qwen Code · qwen3.8-max

Reviewed at 33916e2740b07747d9ec8dd3bb8f33678f60ed4e · 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. ✅

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round: no action needed

No changes were made in this round.

Triage of this round's feedback:

  • Reviews: none newer than the last evaluation (2026-08-09T20:23:25Z).
  • Inline comments: none actionable.
  • Issue-level comments: none actionable.
  • Failed checks: none. All reported checks are either successful or skipped; no still-red checks persist.
  • Base conflicts: none reported (--conflict false), so no merge of origin/main was performed.
  • Deferred non-Critical feedback (Critical-only mode): one deferred item — a PR comment by @qwen-code-ci-bot (issuecomment-5251860206). This PR has completed five change-producing rounds, so Critical-only mode is active and this section is an audit record, not work: it was intentionally left untouched for human follow-up, and no code changes, thread resolutions, or comment replies were made for it.

The branch head remains at commit 33916e2740 (merge of origin/main into fix/issue-8589-otel-session-lifecycle).

中文说明

Autofix 审查轮次:无需处理

本轮未做任何改动。

本轮反馈的分类处理:

  • 评审(Reviews): 自上次评估(2026-08-09T20:23:25Z)之后没有新的评审。
  • 行内评论(Inline comments): 没有可处理的条目。
  • Issue 级评论(Issue-level comments): 没有可处理的条目。
  • 失败的检查(Failed checks): 无。所有报告的检查要么成功、要么被跳过(SKIPPED),也没有持续失败的检查。
  • 与基础分支的冲突: 未报告冲突(--conflict false),因此未执行 origin/main 的合并。
  • 延后的非 Critical 反馈(仅 Critical 模式): 有一条延后条目——@qwen-code-ci-bot 的 PR 评论(issuecomment-5251860206)。本 PR 已完成五个产生改动的轮次,当前处于仅处理 Critical 的模式,该部分仅作为审计记录,不属于本轮工作:已按规则保持原样、留待人工跟进,未对其做任何代码改动、线程解决或评论回复。

分支头部仍为提交 33916e2740(将 origin/main 合并进 fix/issue-8589-otel-session-lifecycle 的合并提交)。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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


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

qwen-code-dev-bot added a commit that referenced this pull request Aug 11, 2026
…await (#8764)

* fix(external-context): read the response body with a reader, not for-await

Async-iterating a ReadableStream needs [Symbol.asyncIterator] on the
TYPE, and whether it is there depends on which lib set the program
resolves — @types/node's stream has it, the DOM lib's needs
lib.dom.asynciterable. That resolution flipped underneath this file on
2026-08-08: #8693 installed @types/jsdom at the root, vitest's types
pull the jsdom types in wherever they exist, and jsdom's carry
/// <reference lib="dom" />. #8693 shipped the tsconfig `types` guard in
the same commit, so main stayed green — but the guard travels with the
BRANCH while node_modules travel with the TRUSTED BASE in the autofix
verification build, so every managed branch behind #8693 failed that
build with TS2504 on this line. Two legs measured on run 31276008548:
63 minutes of accepted agent work discarded per round, 18 more minutes
burned by a repair step that cannot fix a failure outside the PR's diff
(#8614 reached attempt 13 that way; #8616 died identically).

Reproduced locally in both directions before changing anything:
@types/jsdom installed + guard removed = the gate's exact error,
character for character; with the reader loop the same poisoned setup
builds clean. The guard stays — belt and suspenders — but the build no
longer depends on it, or on which lib set any future environment
resolves.

Behavior is unchanged and now pinned by tests the file never had:
multi-chunk assembly, the exact MAX_RESPONSE_BYTES boundary (bound is
strictly-greater), invalid-UTF-8 rejection, and the easy one to drop in
this rewrite — cancelling the stream on early exit, which `for await`
did implicitly via iterator return(). Mutation-tested: removing the
cancel fails exactly that test against an endless producer.

The package's other for-awaits iterate process.stdin (a Node stream,
async-iterable in every lib set) and are untouched.

* fix(external-context): await stream cancellation before rejecting the request

On early exit from the reader loop (the oversize throw) cancellation was
started fire-and-forget, so postJson() rejected while the stream's
teardown was still settling — `for await` had awaited its implicit
iterator return() before propagating. An immediate retry could overlap
the previous response transport's unfinished cancellation. Await
reader.cancel() before releaseLock(), and pin the sequencing with a
deferred-cancel regression test that fails against the fire-and-forget
form.

Also cover read() rejecting after a partial chunk was received: the
error maps to the request-did-not-complete transport error rather than
EOF-then-parse of the partial JSON, and the reader lock is still
released.

* fix(external-context): drop the types guard the reader rewrite made obsolete

The `"types": ["node"]` override existed solely to keep @types/jsdom's
lib.dom out of this program while http-client.ts read the response body
with `for await` — the DOM lib's ReadableStream is not async-iterable,
and the flip broke the build with TS2504 (#8693). The reader loop that
replaced the `for await` types identically in every lib set, so the
guard is no longer load-bearing: with it removed, lib.dom re-enters the
program and the package still builds cleanly. Drop it with its stale
comment instead of leaving maintainers two contradicting stories about
whether it is needed.

Also export MAX_RESPONSE_BYTES and import it in the boundary tests
instead of re-declaring it locally, so the tests pin the real constant
rather than a copy that can silently drift.

* test(external-context): make the invalid-UTF-8 test pin fatal decoding

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Real-environment verification — head 33916e2 ✅ merge-ready

Maintainer verification round 2026-08-12, independent of the earlier scripted rounds (my 08-08 round predates autofix rounds 6–8, so this re-verifies the current head end-to-end in a real environment).

Setup

  • Isolated clone at PR head 33916e2, real production bundle (npm run bundledist/cli.js), Node v24.18.1, macOS.
  • Real OTLP/HTTP collector on 127.0.0.1:43117 receiving the actual logs pipeline (telemetry.otlpProtocol: http) — every assertion below is made against LogRecords the collector physically received, not against unit-test mocks.
  • Mock OpenAI-compatible model server (SSE streaming) so real turns run; isolated QWEN_HOME.

Lifecycle matrix (all observed at the collector)

# Scenario Expected Observed
1 Non-interactive qwen -p one session.start + one session.end, same id, no duplicate from the settle-time catch-up ✅ exactly 2 records
2 Interactive TUI startup (deferred telemetry init) initial session.start emitted after SDK settles (the edge case this follow-up fixed) ✅ emitted once
3 /clear (A→B) end(A) then start(B), no session.previous_id ✅ end-before-start, no previous_id
4 /resume A (persisted continuation B→A) end(B) then start(A) with session.previous_id=B; idempotency guard re-arms after step 3's end ✅ previous_id = B
5 /resume A while A is already active (same-id) zero lifecycle records, no self-referential previous_id ✅ 0 records (slash_command: resume confirms the command ran)
6 /quit shutdown ends the active session before SDK shutdown end(A)
7 Cold start --resume A start(A) without previous_id (documented cold-start behavior), end(A) on quit

Before/after A/B

Identical non-interactive run against the same collector, bundle from merge-base 0a3d7bb5 vs PR head: 0 → 2 lifecycle records, and the qwen-code.* event list and counts are byte-identical — the PR is purely additive to the existing telemetry schema.

Gates on PR head

  • Focused suite (session-events, sdk, loggers, config tests, single-thread): 658 passed / 0 failed.
  • npm run typecheck (all workspaces): pass.
  • CI Test (ubuntu-latest, Node 22.x): green on this head.

Evidence

Lifecycle records at the real collector Before/after A/B
lifecycle timeline before after
TUI after /resume (session A history restored) Focused suite + typecheck
tui resume tests

Notes

  • A TUI process killed hard (SIGKILL, no shutdown path) leaves a session.start without a matching session.end — consistent with the documented best-effort semantics; backends should key on (session.id, start-timestamp) windows as the design doc says.
  • The daemon/ACP multi-session gap is documented as a known limitation in docs/design/otel-session-lifecycle-design.md and was not re-tested here (out of scope for this PR).

Verdict: merge-ready. All seven lifecycle scenarios behave exactly as specified against a real OTLP pipeline, and the change is a no-op for existing telemetry consumers.

中文版本(Chinese version)

真实环境验证 — head 33916e2 ✅ 可合并

2026-08-12 维护者验证轮,独立于此前的脚本化验证(我 08-08 的那轮在 autofix 第 6–8 轮之前的旧 head 上,因此本轮对当前 head 做了端到端复验)。

环境

  • 隔离 clone 在 PR head 33916e2,真实产线 bundle(npm run bundledist/cli.js),Node v24.18.1,macOS。
  • 真实 OTLP/HTTP collector127.0.0.1:43117)接收真实 logs 管道(telemetry.otlpProtocol: http)——以下所有断言均基于 collector 实际收到的 LogRecord,而非单测 mock。
  • OpenAI 兼容 mock 模型服务(SSE 流式),保证真实对话轮次;隔离 QWEN_HOME

生命周期矩阵(均为 collector 实收记录)

# 场景 预期 实测
1 非交互 qwen -p 一对 session.start/session.end,同 id,settle 补发不产生重复 ✅ 恰好 2 条
2 TUI 启动(延迟初始化 telemetry) SDK 就绪后补发首个 session.start(本次跟进修复的边界) ✅ 补发且仅一次
3 /clear(A→B) end(A)start(B)不带 previous_id
4 /resume A(持久化续接 B→A) end(B)start(A) 携带 previous_id=B;幂等 guard 在第 3 步 end 后正确重置
5 A 激活时再次 /resume A(同 id) 零条生命周期记录,无自引用 previous_id ✅ 0 条(slash_command: resume 证明命令确已执行)
6 /quit shutdown 在 SDK 关闭前结束当前会话 end(A)
7 冷启动 --resume A start(A) 不带 previous_id(文档化的冷启动行为),退出时 end(A)

Before/After 对照

同一非交互场景、同一 collector,merge-base 0a3d7bb5 的 bundle 对比 PR head:生命周期记录 0 → 2,且 qwen-code.* 事件列表与计数完全一致——对既有 telemetry schema 纯增量、零改动。

门禁(PR head)

  • 定向测试套件(session-events / sdk / loggers / config,单线程):658 通过 / 0 失败
  • npm run typecheck(全 workspace):通过。
  • CI Test (ubuntu-latest, Node 22.x):本 head 已绿。

备注

  • 被强杀(SIGKILL、无 shutdown 路径)的 TUI 进程会留下有 startend 的会话——与文档化的 best-effort 语义一致;后端应按设计文档所述以(session.id,start 时间戳)窗口为键。
  • daemon/ACP 多会话缺口已在 docs/design/otel-session-lifecycle-design.md 中列为已知限制,不在本 PR 范围内,本轮未复测。

结论:可合并。 七个生命周期场景在真实 OTLP 管道上的行为与设计完全一致,对既有 telemetry 消费方零影响。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round — no action taken (PR #8616)

This round's feedback contains nothing actionable:

  • Reviews: none.
  • Inline comments: none.
  • Failed / still-red checks: none.
  • Deferred non-Critical feedback: critical-only mode is active (after 5 change-producing rounds), and the deferred list is empty.

The only issue-level comment is a maintainer real-environment verification report (2026-08-12) for PR head 33916e2, covering all seven lifecycle scenarios against a real OTLP/HTTP collector, plus a before/after A/B run and the focused test suite (658 passed / 0 failed). Its verdict is merge-ready, with no requested changes. Its two notes are confirmations of documented behavior, not defects:

  1. A SIGKILLed TUI leaves a session.start without a matching session.end — consistent with the documented best-effort semantics; backends are expected to key on (session.id, start-timestamp) windows per the design doc.
  2. The daemon/ACP multi-session gap — a documented known limitation in docs/design/otel-session-lifecycle-design.md, explicitly out of scope for this PR and not re-tested.

No code changes were made and no commit was created. The branch remains at 33916e2.

中文说明

Autofix 轮次 — 无需处理(PR #8616

本轮反馈没有任何可执行项:

  • Review:无。
  • 行内评论:无。
  • 失败 / 持续失败的检查:无。
  • 延后的非 Critical 反馈:已进入仅处理 Critical 模式(完成 5 个产生改动的轮次后),延后列表为空。

唯一一条 issue 级评论是维护者针对 PR head 33916e2 的真实环境验证报告(2026-08-12),在真实 OTLP/HTTP collector 上覆盖了全部七个生命周期场景,并包含 Before/After 对照与定向测试套件(658 通过 / 0 失败)。结论为可合并,未提出任何修改要求。其中的两条备注均为对已文档化行为的确认,而非缺陷:

  1. 被 SIGKILL 强杀的 TUI 会留下没有配对 session.endsession.start —— 与文档化的 best-effort 语义一致;按设计文档,后端应以(session.id,start 时间戳)窗口为键处理。
  2. daemon/ACP 多会话缺口 —— docs/design/otel-session-lifecycle-design.md 中已文档化的已知限制,明确不在本 PR 范围内,本轮未复测。

未做任何代码改动,未创建提交。分支保持在 33916e2

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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


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

@wenshao
wenshao added this pull request to the merge queue Aug 12, 2026
Merged via the queue into QwenLM:main with commit de470b9 Aug 12, 2026
77 checks passed
@QwenLM QwenLM deleted a comment Aug 12, 2026
@QwenLM QwenLM deleted a comment Aug 12, 2026
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.11.

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+) review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(telemetry): align session lifecycle with OpenTelemetry conventions

4 participants