Skip to content

fix(cli): source /context token total from the per-session chat#5764

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
qqqys:fix/context-usage-per-session-token-count
Jun 23, 2026
Merged

fix(cli): source /context token total from the per-session chat#5764
wenshao merged 1 commit into
QwenLM:mainfrom
qqqys:fix/context-usage-per-session-token-count

Conversation

@qqqys

@qqqys qqqys commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

collectContextData, which powers both CLI /context and the serve GET /session/:id/context-usage route, now reads the prompt token total from the current session's GeminiChat instead of the process-global uiTelemetryService singleton. It still falls back to the singleton when no chat is initialized yet.

Why it's needed

In a qwen serve daemon, multiple sessions share one process. The global telemetry singleton stores only the last completed turn across that process, so every session's /context-usage could report the same prompt-token total: whichever session most recently completed a turn. The per-session chat already tracks the correct value.

Reviewer Test Plan

How to verify

  1. Start qwen serve and create two sessions.
  2. Send a materially different prompt in each session so their context token totals diverge.
  3. Call GET /session/:id/context-usage for both sessions.
  4. Confirm each response reports that session's own prompt-token total rather than the most recent global total.
  5. In the CLI, run /context in a single active session and confirm the reported total remains unchanged from existing behavior.

Evidence (Before & After)

Before: collectContextData read uiTelemetryService.getLastPromptTokenCount(), a process-global value shared by all serve sessions.

After: it prefers config.getGeminiClient().getChat().getLastPromptTokenCount() when the session chat is initialized, with tests covering both the per-session path and the uninitialized fallback path.

Tested on

OS Status
macOS tested
Windows not tested
Linux not tested

Environment (optional)

Focused context data tests and existing context command / ACP agent suites.

Risk & Scope

  • Main risk or tradeoff: the headline prompt-token total now comes from the session chat when available, so serve sessions no longer share a stale cross-session value.
  • Cached-content token accounting remains on the global singleton because there is no per-chat mirror today; this only affects the messages-vs-cache split, not the headline total or tier.
  • Single-session CLI /context should be behaviorally unchanged because the global value and that session's chat value coincide there.
  • Breaking changes / migration notes: none expected.

Linked Issues

Closes #5763

中文说明

这个 PR 做了什么

collectContextData 同时支撑 CLI /context 和 serve 的 GET /session/:id/context-usage。它现在优先从当前 session 的 GeminiChat 读取 prompt token 总数,而不是读取进程级全局 uiTelemetryService 单例;当 chat 尚未初始化时仍回退到全局值。

为什么需要

qwen serve daemon 里多个 session 共享同一个进程。全局 telemetry 单例只保存整个进程最近一次完成 turn 的 token 数,因此不同 session 的 /context-usage 可能都显示同一个值:最后完成 turn 的那个 session 的值。每个 session 自己的 chat 已经保存了正确值。

评审验证步骤

  1. 启动 qwen serve 并创建两个 session。
  2. 在两个 session 中分别发送明显不同长度的 prompt,让 context token 总数不同。
  3. 分别调用两个 session 的 GET /session/:id/context-usage
  4. 确认每个响应返回各自 session 的 prompt-token 总数,而不是最近一次全局值。
  5. 在单个 CLI session 中运行 /context,确认输出与既有行为一致。

风险与范围

  • 主要风险或取舍:headline prompt-token 总数现在在可用时来自 session chat,因此 serve session 不再共享过期的跨 session 值。
  • cached-content token 仍使用全局单例,因为当前没有 per-chat 镜像;这只影响 messages-vs-cache 拆分,不影响 headline total 或 tier。
  • 单 session CLI /context 行为应保持不变,因为全局值与该 session chat 值一致。
  • 破坏性变更:无。

collectContextData read apiTotalTokens from uiTelemetryService, a
process-global singleton shared by every session in a serve daemon, so
each session's /context-usage reported whichever session most recently
completed a turn. Read the per-session GeminiChat's count instead
(config already in scope), falling back to the global singleton only
when no chat is initialized yet. Fixes both /context and the serve
GET /session/:id/context-usage route at the source.

Closes QwenLM#5763

Co-authored-by: Qwen-Coder <[email protected]>

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

Hi @qqqys, thanks for the PR — the fix itself looks well-targeted.

The PR body is missing the Reviewer Test Plan section from the PR template. The template also expects Risk & Scope, Linked Issues, and a 中文说明 details block.

Could you add these sections? The Reviewer Test Plan in particular is important — without it, maintainers can't quickly verify the fix. Something like:

  • How to verify: steps to reproduce the multi-session /context-usage bug and confirm it's fixed
  • Tested on: which OS you verified on

The code change and tests look good — just the PR description needs the template structure. Happy to re-review once updated. 🙏

中文说明

@qqqys 你好,感谢 PR — 修复本身很精准。

PR 正文缺少 PR 模板 中要求的 Reviewer Test Plan 部分,同时也缺少 Risk & ScopeLinked Issues中文说明 折叠块。

请补充这些部分,特别是 Reviewer Test Plan — 维护者需要它来快速验证修复。建议包含:

  • How to verify: 复现多会话 /context-usage bug 并确认修复的步骤
  • Tested on: 验证过的操作系统

代码改动和测试看起来不错 — 只需要补全 PR 描述结构。更新后会重新审查。🙏

Qwen Code · qwen3.7-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.

No review findings. Downgraded from Approve to Comment: CI still running.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: this is a real bug — qwen serve sessions sharing a process-global token count means /context and /context-usage report wrong numbers for every session except the most recent one. The per-session chat already tracks the correct value, so reading it there is the obvious fix. Clearly aligned with the project. CHANGELOG has no direct reference to this specific issue, but multi-session serve correctness is squarely in scope.

On approach: the change is minimal — 12 lines of production code, 42 lines of tests covering both the happy path (per-session chat initialized) and the fallback (not yet initialized). No scope creep, no drive-by refactors. The optional chaining on config.getGeminiClient?.() and isInitialized?.() is appropriate defensive coding since the Config type parameter in collectContextData is imported and may be implemented by different classes. The comment explaining why cached-content tokens stay on the global singleton is helpful context for future readers.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是一个真实的 bug —— qwen serve 的多个 session 共享进程级全局 token 计数,导致 /context/context-usage 对除了最近一次之外的所有 session 都报告错误数字。每个 session 自己的 chat 已经保存了正确值,从那里读取是显而易见的修复方案。与项目方向明确对齐。CHANGELOG 没有直接提到这个具体问题,但多 session serve 模式的正确性完全在范围内。

方案:改动极小 —— 12 行生产代码,42 行测试覆盖了主路径(session chat 已初始化)和回退路径(尚未初始化)。没有范围蔓延,没有顺手重构。config.getGeminiClient?.()isInitialized?.() 上的可选链是合理的防御性编码,因为 collectContextData 中的 Config 类型是导入的,可能由不同类实现。关于 cached-content tokens 为何仍使用全局单例的注释对未来读者有帮助。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Clean fix. The approach — prefer per-session chat token count, fall back to global singleton — is the obvious and correct solution. I would have written the same code.

Reviewed the production change in contextCommand.ts:

  • Optional chaining on config.getGeminiClient?.() and geminiClient?.isInitialized?.() is appropriate — Config is a broad type and not every implementation guarantees these methods.
  • The getChat() call is safe because it's guarded by the isInitialized() check, which tests this.chat !== undefined (the same condition getChat() throws on).
  • Cached-content tokens correctly stay on the global singleton, with a clear comment explaining why.

No correctness issues, no security concerns, no regressions. The diff is minimal and focused — exactly what the stated goal needs, nothing more.

Tests

Both new tests pass (9/9 total in contextCommand.test.ts):

  • Per-session path: verifies getChat().getLastPromptTokenCount() wins over the global 999K value, and that the tier reflects the per-session 50K (safe, not hard).
  • Fallback path: verifies the global singleton is used when isInitialized() returns false and getChat() throws.
 ✓ src/ui/commands/contextCommand.test.ts (9 tests) 30ms

 Test Files  1 passed (1)
      Tests  9 passed (9)

Real-Scenario Testing

This bug only manifests in qwen serve with multiple concurrent sessions — the global singleton stores whichever session most recently completed a turn, so all other sessions report wrong numbers. A single-session CLI /context call produces identical output before and after this fix (the PR description explicitly notes this). Reproducing the bug in tmux would require standing up two serve sessions with real LLM API calls and comparing their /context-usage responses, which isn't feasible in this CI environment.

Verified the dev build runs correctly:

$ npm run dev -- -p 'what is 2+2? reply with just the number'
4

The unit tests adequately cover the logic change. The serve-mode integration scenario is best verified manually per the Reviewer Test Plan in the PR description.

中文说明

代码审查

干净的修复。优先使用 per-session chat 的 token 计数、回退到全局单例的方案是显而易见且正确的做法。我也会写同样的代码。

审查了 contextCommand.ts 的生产代码改动:

  • config.getGeminiClient?.()geminiClient?.isInitialized?.() 上的可选链是合理的 —— Config 是一个宽泛的类型,并非所有实现都保证有这些方法。
  • getChat() 调用是安全的,因为受 isInitialized() 保护,后者检查 this.chat !== undefined(正是 getChat() 抛异常的条件)。
  • Cached-content tokens 正确地保持使用全局单例,并有清晰的注释解释原因。

没有正确性问题,没有安全隐患,没有回归。Diff 最小且聚焦 —— 恰好是目标所需的,没有更多。

测试

两个新测试都通过了(contextCommand.test.ts 共 9/9):

  • Per-session 路径:验证 getChat().getLastPromptTokenCount() 优先于全局 999K 值,tier 反映 per-session 的 50K(safe 而非 hard)。
  • 回退路径:验证当 isInitialized() 返回 false 且 getChat() 抛异常时使用全局单例。

真实场景测试

这个 bug 只在 qwen serve 多个并发 session 中显现 —— 全局单例保存最近完成 turn 的 session 的值,其他 session 都报告错误数字。单 session CLI /context 在修复前后输出完全相同(PR 描述中明确说明了这一点)。在 tmux 中复现需要建立两个 serve session 并进行真实 LLM API 调用比较 /context-usage 响应,在当前 CI 环境中不可行。

已验证 dev 构建正常运行。单元测试充分覆盖了逻辑变更。Serve 模式集成场景最好按 PR 描述中的 Reviewer Test Plan 手动验证。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a textbook bugfix PR: real bug, minimal change, clear tests, honest description of tradeoffs.

The uiTelemetryService singleton was always going to be wrong in serve mode — it's a process-level object pretending to be a session-level metric. The fix reads from the right place (the per-session chat) with a sensible fallback for the uninitialized case. The cached-content token read correctly stays on the global singleton with a comment explaining why, which is the kind of future-reader thinking I want to see.

The diff is 12 production lines. No abstractions, no helper functions, no "while I'm here" refactors. Two focused tests that prove the fix works and that the fallback isn't broken. If every PR looked like this, maintainers would have a lot less to do.

LGTM. ✅

中文说明

这是一个教科书式的 bugfix PR:真实的 bug、最小的改动、清晰的测试、诚实的权衡描述。

uiTelemetryService 单例在 serve 模式下本来就会出错 —— 它是一个进程级对象却假装提供 session 级指标。修复从正确的位置(per-session chat)读取,并对未初始化的情况做了合理的回退。Cached-content token 的读取正确地保持在全局单例上,并有注释解释原因,这正是我希望看到的为未来读者考虑的做法。

Diff 只有 12 行生产代码。没有抽象,没有辅助函数,没有"顺便"重构。两个聚焦的测试证明修复有效且回退没有被破坏。如果每个 PR 都像这样,维护者的工作会轻松很多。

LGTM. ✅

Qwen Code · qwen3.7-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.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification — per-session /context token total in qwen serve

Verified end-to-end on a real built daemon (npm ci && npm run build, head df30f5d03, isolated worktree, Linux). Booted qwen serve under tmux against a deterministic mock OpenAI server, drove it with the real TypeScript SDK DaemonClient, and A/B'd against the merge-base 83caca65e.

Setup

  • Daemonqwen serve --workspace <ws> --port … --token …; env points the model at a mock OpenAI server. The bridge runs one qwen --acp child per workspace and multiplexes all sessions onto it (bridge.ts), so uiTelemetryService is genuinely shared across sessions — exactly the condition Multi-session /context-usage reports a process-global token count instead of the per-session value #5763 describes.
  • Mock — reports usage.prompt_tokens = N parsed from a TOKENS=N marker in the prompt, so each session records a distinct per-session total.
  • Two sessionScope:'thread' sessions. A completes its turn first (records 11111), then B last (records 222222) so the process-global singleton ends on B's value.

1) Serve GET /session/:id/context-usage — A/B

Session (last turn = B) Before (83caca65e) After (this PR)
A (TOKENS=11111) totalTokens=222222, tier hard ❌ (leaks B's global value) totalTokens=11111, tier safe
B (TOKENS=222222) totalTokens=222222, tier hard totalTokens=222222, tier hard

Before — both sessions report the last-completed-turn's global value; session A's own 11111 is lost:

[A] context-usage: totalTokens=222222 tier=hard
[B] context-usage: totalTokens=222222 tier=hard
RESULT: globalBugReproduced=true, perSessionCorrect=false

After — each session reports its own value (and its own tier):

[A] context-usage: totalTokens=11111  tier=safe
[B] context-usage: totalTokens=222222 tier=hard
RESULT: perSessionCorrect=true, globalBugReproduced=false

2) CLI /context unchanged (test-plan step 5)

Single TUI session, prompt TOKENS=12345, then /context:

█ Used                12.3k tokens (9.4%)
  Current tier                  safe

Headline total (12.3k = 12345) is the per-session chat value — single-session CLI /context is behaviorally correct/unchanged (the global and chat values coincide there).

Verdict

The fix holds end-to-end on a real daemon: concurrent serve sessions each report their own /context-usage prompt-token total and tier instead of the last-completed-turn's global value, while single-session CLI /context is unchanged. LGTM for merge.

中文版(合并参考)

✅ 本地真实验证 —— qwen serve 中按 session 的 /context token 总数

真实构建的 daemonnpm ci && npm run build,head df30f5d03,独立 worktree,Linux)上做了端到端验证:通过 tmux 启动 qwen serve,指向一个确定性 mock OpenAI 服务器,用真实的 TypeScript SDK DaemonClient 驱动,并与 merge-base 83caca65e 做 A/B 对比。

环境

  • Daemon —— qwen serve --workspace <ws> --port … --token …,env 把模型指向 mock OpenAI。bridge 每个 workspace 只起一个 qwen --acp 子进程,所有 session 多路复用到它上面(bridge.ts),因此 uiTelemetryService 单例在多个 session 间是真正共享的 —— 正是 Multi-session /context-usage reports a process-global token count instead of the per-session value #5763 描述的条件。
  • Mock —— 从 prompt 中的 TOKENS=N 标记解析并返回 usage.prompt_tokens = N,让每个 session 记录到不同的 per-session 总数。
  • 两个 sessionScope:'thread' session。A 先完成 turn(记录 11111),B 后完成(记录 222222),因此进程级全局单例最终停在 B 的值。

1) Serve GET /session/:id/context-usage —— A/B

Session(最后一个 turn = B) 改动前(83caca65e 改动后(本 PR)
ATOKENS=11111 totalTokens=222222,tier hard ❌(泄漏了 B 的全局值) totalTokens=11111,tier safe
BTOKENS=222222 totalTokens=222222,tier hard totalTokens=222222,tier hard

改动前 —— 两个 session 都报告最后完成 turn 的全局值,A 自己的 11111 丢失(globalBugReproduced=true)。
改动后 —— 每个 session 报告各自的值(以及各自的 tier):A=11111/safe,B=222222/hard(perSessionCorrect=true)。

2) CLI /context 行为不变(验证步骤 5)

单个 TUI session,发送 TOKENS=12345,再运行 /context:headline 总数 Used 12.3k tokens(=12345),tier safe —— 即 per-session chat 值;单 session CLI /context 行为正确/不变(此处全局值与 chat 值一致)。

结论

该修复在真实 daemon 上端到端成立:并发的 serve session 各自报告自己的 /context-usage prompt-token 总数和 tier,而不再是最后完成 turn 的全局值;同时单 session CLI /context 保持不变。LGTM,可以合并。

Verified by @wenshao on Linux via a real built daemon + mock OpenAI server driven over the TS SDK (head df30f5d03 vs merge-base 83caca65e).

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Local runtime verification (real qwen serve daemon via tmux + deterministic A/B)

Built two real bundles from this PR's head (df30f5d03) — one with the fix, one with contextCommand.ts reverted to the merge-base — and verified collectContextData both through a live qwen serve daemon (tmux, mock OpenAI model) and a deterministic unit A/B.

Architecture (the bug condition, confirmed)

qwen serve --http-bridge binds one workspace and multiplexes N sessions onto a single qwen --acp child (confirmed: one --acp child process, QwenAgent holds a Map<string, Session>). Every session's GeminiChat lives in that one process and mirrors its prompt-token count to the shared module-level uiTelemetryService singleton — so the singleton holds whichever session most recently completed a turn (#5763).

Cross-session divergence — deterministic A/B (authoritative)

packages/cli/src/ui/commands/contextCommand.test.ts reproduces the exact daemon condition: a "wrong" session's global value (999_000) vs this session's chat value (50_000).

source getLastPromptTokenCount() on the chat data.totalTokens tier
base (reverted) never called ❌ (reads the global singleton) would be 999_000 hard
PR called ✅ 50_000 safe
  • PR → both new tests pass.
  • base → the per-session test fails: expected "spy" to be called at least once — base reads uiTelemetryService directly and never consults the per-session chat, so a stale cross-session total (and a wrong hard tier) leaks through.
  • Full contextCommand.test.ts on PR: 9/9, no regression.

Live daemon E2E (the fix is wired into the real route)

Real qwen serve (PR bundle, tmux) + mock model that returns a controllable usage.prompt_tokens:

POST /session/:id/prompt  {prompt:[{type:"text",text:"PTOK=50000 hi"}]}
  → mock reports prompt_tokens=50000
GET  /session/:id/context-usage
  → usage.totalTokens = 50000, breakdown.currentTier = "safe"

This exercises the full path GET /session/:id/context-usage → bridge → ACP child → collectContextData → geminiClient.getChat().getLastPromptTokenCount() — i.e. the fix is live on the route that actually mattered (#5763).

Single-session parity (PR claim: CLI /context unchanged)

Same prompt on the base daemon vs the PR daemon → both report totalTokens=50000, tier=safe. In a single session the chat value and the global coincide, so single-session /context behavior is unchanged, as the PR states.

Note on a live 2-session A/B

A single daemon can't host two concurrent distinct sessions over HTTP (it binds one workspace and coalesces same-cwd POSTs; a foreign cwd returns workspace_mismatch; fork is an agentic sub-agent, too noisy to measure cleanly). The two-session bleed is therefore reproduced deterministically by the unit A/B above (which models the shared-singleton condition exactly), while the live daemon E2E confirms the route reads the per-session chat. Together they cover both the bug's manifestation and the fix's wiring.

Verdict

The fix sources the /context headline total from the per-session chat (with a correct fallback to the global when no chat exists yet), proven by a deterministic A/B and confirmed live on the real qwen serve route, with no single-session regression. LGTM.

中文版(合并参考)

✅ 本地真实运行时验证(tmux 下真实 qwen serve daemon + 确定性 A/B)

从本 PR 的最新 head(df30f5d03)构建了两个真实 bundle——一个含修复,另一个把 contextCommand.ts 回退到 merge-base——并通过 tmux 中真实的 qwen serve daemon(配 mock OpenAI 模型)以及确定性单元 A/B,验证了 collectContextData

架构(已确认的 bug 条件)

qwen serve --http-bridge 绑定一个 workspace,并把 N 个 session 复用到同一个 qwen --acp 子进程上(已确认:只有一个 --acp 子进程,QwenAgent 持有 Map<string, Session>)。每个 session 的 GeminiChat 都在这一个进程里,并把各自的 prompt-token 数镜像到共享的模块级 uiTelemetryService 单例——于是该单例只保存“最近完成 turn”的那个 session 的值(#5763)。

跨 session 串值——确定性 A/B(权威)

packages/cli/src/ui/commands/contextCommand.test.ts 复现了 daemon 的确切条件:某个“错误” session 的全局值(999_000)对比本 session 的 chat 值(50_000)。

源码 chat 上的 getLastPromptTokenCount() data.totalTokens tier
base(回退) 从未调用 ❌(读全局单例) 会是 999_000 hard
PR 调用 ✅ 50_000 safe
  • PR → 两个新测试通过。
  • base → per-session 测试失败expected "spy" to be called at least once——base 直接读 uiTelemetryService,从不查 per-session chat,于是过期的跨 session 值(以及错误的 hard tier)就泄漏了。
  • PR 上完整 contextCommand.test.ts9/9,无回归。

真实 daemon E2E(修复已接入真实 route)

真实 qwen serve(PR bundle,tmux)+ 可控 usage.prompt_tokens 的 mock 模型:

POST /session/:id/prompt  {prompt:[{type:"text",text:"PTOK=50000 hi"}]}
  → mock 返回 prompt_tokens=50000
GET  /session/:id/context-usage
  → usage.totalTokens = 50000, breakdown.currentTier = "safe"

这条链路 GET /session/:id/context-usage → bridge → ACP child → collectContextData → geminiClient.getChat().getLastPromptTokenCount() 完整跑通——即修复确实接在了真正出问题的那条 route 上(#5763)。

单 session 一致性(PR 声称:CLI /context 不变)

相同 prompt 在 base daemon 与 PR daemon 上都报 totalTokens=50000, tier=safe。单 session 时 chat 值与全局值一致,因此单 session 的 /context 行为不变,与 PR 描述一致。

关于实机 2-session A/B 的说明

单个 daemon 无法通过 HTTP 同时承载两个不同的 session(它绑定一个 workspace,并对同 cwd 的 POST 做合并;异 cwd 会返回 workspace_mismatchfork 是 agentic 子代理,测量噪声太大)。因此双 session 串值由上面的单元 A/B 确定性复现(它精确建模了共享单例的条件),而真实 daemon E2E 证明 route 读取的是 per-session chat。二者合起来覆盖了 bug 的表现与修复的接线。

结论

修复让 /context 的 headline 总数来自 per-session chat(并在尚无 chat 时正确回退到全局),经确定性 A/B 证明、并在真实 qwen serve route 上实机确认,单 session 无回归。LGTM。

@wenshao
wenshao merged commit ed95bb5 into QwenLM:main Jun 23, 2026
39 checks passed
@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Independent re-verification — the live 2-session serve A/B is reproducible

Re-ran this on a fresh build to settle the one point the two verification notes above disagree on: whether a single daemon can host two concurrent distinct sessions and show the cross-session leak live. It can — via the SDK's documented sessionScope:'thread' override — so the #5763 bleed reproduces live on the real GET /session/:id/context-usage route, not only in the unit A/B.

Setup

  • Fresh worktree at head df30f5d0, npm ci && npm run build, Linux.
  • Real qwen serve --workspace <ws> --port 0 --token … --hostname 127.0.0.1 booted in tmux; model env points at a local fake OpenAI server.
  • One daemon ⇒ one qwen --acp child, both sessions multiplexed onto it, so they share the module-level uiTelemetryService singleton — the exact Multi-session /context-usage reports a process-global token count instead of the per-session value #5763 condition.
  • Fake server returns usage.prompt_tokens/total_tokens keyed off a marker in the prompt: A → 11111, B → 77777.
  • Driver (real TS SDK DaemonClient): create A (sessionScope:'thread'), create B (sessionScope:'thread') → two distinct session IDs; prompt A, then prompt B (so the shared singleton ends on B's 77777); then GET …/context-usage for both.
  • A/B toggles only contextCommand.ts between the PR (HEAD) and its parent ac1e4737 (pre-fix), rebuilding just @qwen-code/qwen-code — identical daemon harness on both runs.

Result — GET /session/:id/context-usageusage.totalTokens

A (→11111) B (→77777, last turn) distinct sessions
Before (parent ac1e4737, dist fix-line = 0) 77777 ❌ — leaks B's global value 77777 ✅ true
After (PR df30f5d0, dist fix-line = 1) 11111 ✅ — its own 77777 ✅ true

Raw driver output (RESULT: lines, abbreviated):

AFTER : {"distinctSessions":true,"observed":{"A_totalTokens":11111,"B_totalTokens":77777},"A_isolated":true, "A_leaked_B":false}
BEFORE: {"distinctSessions":true,"observed":{"A_totalTokens":77777,"B_totalTokens":77777},"A_isolated":false,"A_leaked_B":true}

The discriminator is session A: before the fix it reports 77777 (B's count — the most-recently-completed turn); after the fix it reports its own 11111. B reads 77777 either way (correct by luck, since it ran last).

Note (reconciling the two notes above)

Two concurrent distinct sessions on one daemon are reachable — via POST /session with sessionScope:'thread' (the SDK's per-request override, CreateSessionRequest.sessionScope: "forces a distinct session for this call"). No workspace_mismatch, no fork sub-agent, no same-cwd coalescing. So the cross-session bleed is observable live on the route that actually mattered, complementing the deterministic unit A/B.

Verdict

Confirms the fix end-to-end on a real daemon: concurrent serve sessions each report their own /context-usage prompt-token total instead of the last-completed-turn's global value. LGTM for merge.

中文版(合并参考)

✅ 独立复核 —— 真实的「双 session」serve A/B 是可复现的

在一份全新构建上重跑,专门厘清上面两条验证说明分歧的那一点:单个 daemon 能否同时承载两个不同的并发 session、从而在线上复现跨 session 串值。可以 —— 通过 SDK 文档化的 sessionScope:'thread' 覆盖项 —— 因此 #5763 的串值能在真实的 GET /session/:id/context-usage route 上线上复现,而不仅是单元 A/B。

环境

  • 全新 worktree,head df30f5d0npm ci && npm run build,Linux。
  • tmux 中启动真实的 qwen serve --workspace <ws> --port 0 --token … --hostname 127.0.0.1;模型 env 指向本地 fake OpenAI server。
  • 一个 daemon ⇒ 一个 qwen --acp 子进程,两个 session 都复用到它上面,因此共享模块级 uiTelemetryService 单例 —— 正是 Multi-session /context-usage reports a process-global token count instead of the per-session value #5763 的条件。
  • fake server 根据 prompt 里的标记返回 usage.prompt_tokens/total_tokensA → 11111,B → 77777
  • 驱动(真实 TS SDK DaemonClient):创建 AsessionScope:'thread')、创建 BsessionScope:'thread')→ 两个不同的 session id;先 prompt A prompt B(使共享单例最终停在 B 的 77777);随后对两者分别 GET …/context-usage
  • A/B 在 PR(HEAD)与其父提交 ac1e4737(改动前)之间切换 contextCommand.ts,仅重建 @qwen-code/qwen-code —— 两次运行使用完全相同的 daemon 测试装置。

结果 —— GET /session/:id/context-usageusage.totalTokens

A(→11111 B(→77777,最后一个 turn) 是否不同 session
改动前(父提交 ac1e4737dist fix 行 = 0) 77777 ❌ —— 泄漏了 B 的全局值 77777 ✅ 是
改动后(PR df30f5d0dist fix 行 = 1) 11111 ✅ —— 各自的值 77777 ✅ 是

判别点在 session A:改动前它报告 77777(B 的值 —— 最近完成 turn 的那个);改动后它报告自己的 11111。B 两种情况都是 77777(碰巧正确,因为它最后运行)。

说明(与上面两条说明的对账)

单个 daemon 上的两个并发不同 session 是可达的 —— 通过 POST /sessionsessionScope:'thread'(SDK 的按请求覆盖项,CreateSessionRequest.sessionScope“forces a distinct session for this call”)。无需 workspace_mismatch、不依赖 fork 子 agent、也不会被同 cwd 合并。因此跨 session 串值能在真正出问题的那条 route 上线上观测到,与确定性单元 A/B 相互印证。

结论

在真实 daemon 上端到端确认了该修复:并发的 serve session 各自报告自己的 /context-usage prompt-token 总数,而不再是最后完成 turn 的全局值。LGTM,可以合并。

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-session /context-usage reports a process-global token count instead of the per-session value

3 participants