fix(daemon): language switch writes to wrong output-language.md path#4938
Conversation
## Problem `POST /session/:id/language` (PR #4705) always writes `output-language.md` to the global `~/.qwen/` path, but `Config.outputLanguageFilePath` may point to a project-level `<cwd>/.qwen/output-language.md` (when it existed at startup). Since `refreshHierarchicalMemory` reads from the Config-bound path, the language switch silently fails when a project-level file exists. Additionally, on a fresh environment where no `output-language.md` exists, the first language switch creates the file but `Config.outputLanguageFilePath` remains `undefined` (readonly), so `refreshHierarchicalMemory` never reads the newly created file. ## Fix 1. **Config.outputLanguageFilePath**: remove `readonly`, add `setOutputLanguageFilePath()` so the path can be registered after first-time file creation. 2. **languageUtils.ts**: add optional `targetPath` parameter to `writeOutputLanguageFile()` and `updateOutputLanguageFile()`. Export `getOutputLanguageFilePath()` for callers that need the global default. 3. **acpAgent.ts**: write to the session Config's actual path. On first-time creation (path was undefined), register the global path on Config. On multi-session refresh, also update each session's own file if its path differs from the one already written. 4. **languageCommand.ts** and **SettingsDialog.tsx**: same Config-bound path fix for the CLI `/language` command and settings dialog. 5. **server.ts**: expose `supportedLanguages` array in `GET /capabilities` so clients can discover valid language codes before calling the endpoint. 6. **SDK**: add `DaemonClient.setSessionLanguage()` method and `SetSessionLanguageResult` type. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Hey @chiga0, thanks for this PR — the bug fix itself looks well-targeted and the description is thorough.
However, the PR body doesn't follow our PR template. A few headings are renamed and some required sections are missing, which makes it harder for reviewers to find what they need. Could you reformat to match?
What's there but with wrong headings:
## Summary→ should be## What this PR does## Changes/## How it ensures read/write consistency→ should be## Why it's needed## Test plan→ should be## Reviewer Test Planwith### How to verifyand### Evidence (Before & After)subsections## Related→ should be## Linked Issues
Missing entirely:
## Risk & Scope(risk/tradeoff, out of scope, breaking changes)### Tested ontable (macOS / Windows / Linux status)<details>中文说明</details>— the bottom block is empty
Once the template is in place, happy to dig into the code. 🔍
中文说明
感谢贡献!Bug 修复方向很好,描述也很详细。
但 PR 正文没有按照 PR 模板 的格式来写。有些标题名称不对,还有一些必填章节缺失,这会让 reviewer 难以快速定位信息。能否按模板重新整理一下?
标题不对的部分:
## Summary→ 应为## What this PR does## Changes/## How it ensures read/write consistency→ 应为## Why it's needed## Test plan→ 应为## Reviewer Test Plan,包含### How to verify和### Evidence (Before & After)子章节## Related→ 应为## Linked Issues
完全缺失的部分:
## Risk & Scope(风险/取舍、不在范围内的、破坏性变更)### Tested on表格(macOS / Windows / Linux 测试状态)<details>中文说明</details>— 底部中文翻译区块是空的
模板格式调整后,我们会继续看代码。🔍
| workspaceCwd: boundWorkspace, | ||
| // Active mediation policy under the `policy` namespace. | ||
| policy: { permission: bridge.permissionPolicy }, | ||
| supportedLanguages: LANGUAGE_CODES, |
There was a problem hiding this comment.
[Critical] supportedLanguages is added to the response object but the CapabilitiesEnvelope interface in packages/cli/src/serve/types.ts was not updated. This causes a TypeScript build error (TS2353: Object literal may only specify known properties).
| supportedLanguages: LANGUAGE_CODES, | |
| policy: { permission: bridge.permissionPolicy }, | |
| supportedLanguages: LANGUAGE_CODES, |
Add supportedLanguages?: string[] to the CapabilitiesEnvelope interface in types.ts (after the policy field, around line 234):
/**
* Language codes supported by this daemon. Additive — older
* daemons omit this field. Clients should treat absence as
* "unknown" rather than "none".
*/
supportedLanguages?: string[];— qwen3.7-plus via Qwen Code /review
There was a problem hiding this comment.
Fixed — added supportedLanguages?: string[] to CapabilitiesEnvelope in types.ts.
| // Update output language rule file immediately (no restart needed for LLM effect) | ||
| if (key === 'general.outputLanguage' && typeof parsed === 'string') { | ||
| updateOutputLanguageFile(parsed); | ||
| updateOutputLanguageFile(parsed, config?.getOutputLanguageFilePath?.()); |
There was a problem hiding this comment.
[Suggestion] Unlike languageCommand.ts (lines 129-134) and acpAgent.ts (lines 2938-2942), this caller does not register the path via setOutputLanguageFilePath after the first-time write. When config.getOutputLanguageFilePath() returns undefined (no file existed at startup), the file is correctly written to the global default, but config.outputLanguageFilePath stays undefined for the session's lifetime.
This means subsequent reads via config.getOutputLanguageFilePath() (e.g., in side queries) continue to return undefined until the daemon restarts and Config is re-constructed.
| updateOutputLanguageFile(parsed, config?.getOutputLanguageFilePath?.()); | |
| if (key === 'general.outputLanguage' && typeof parsed === 'string') { | |
| const targetPath = config?.getOutputLanguageFilePath?.(); | |
| updateOutputLanguageFile(parsed, targetPath); | |
| if (!targetPath) { | |
| config?.setOutputLanguageFilePath?.(getOutputLanguageFilePath()); | |
| } | |
| } |
— qwen3.7-plus via Qwen Code /review
There was a problem hiding this comment.
Fixed — extracted writeOutputLanguageAndRegisterPath() helper in languageUtils.ts, all 3 call sites (acpAgent, languageCommand, SettingsDialog) now include the registration step.
| const results = await Promise.allSettled( | ||
| allSessions.map(async (s) => { | ||
| const cfg = s.getConfig(); | ||
| const sessionPath = cfg.getOutputLanguageFilePath(); |
There was a problem hiding this comment.
[Suggestion] In the multi-session refresh loop, if updateOutputLanguageFile throws for any session (e.g., EACCES on a project-level .qwen/ directory), the subsequent refreshHierarchicalMemory() and refreshSystemInstruction() calls are skipped for that session — leaving it with a stale system instruction.
Consider wrapping the file writes in a try/catch so the refresh calls always execute:
allSessions.map(async (s) => {
const cfg = s.getConfig();
const sessionPath = cfg.getOutputLanguageFilePath();
try {
if (sessionPath && sessionPath !== writtenPath) {
updateOutputLanguageFile(settingValue, sessionPath);
}
if (!sessionPath) {
const fallback = getOutputLanguageFilePath();
updateOutputLanguageFile(settingValue, fallback);
cfg.setOutputLanguageFilePath(fallback);
}
} catch (err) {
debugLogger.warn('Failed to write output-language.md for session:', err);
}
await cfg.refreshHierarchicalMemory();
await cfg.getGeminiClient()?.refreshSystemInstruction();
}),Additionally, when multiple sessions share the same path (e.g., all undefined → global default), the same file is written N times with identical content. A Set<string> of already-written paths would avoid the redundant I/O.
— qwen3.7-plus via Qwen Code /review
There was a problem hiding this comment.
Fixed — wrapped file writes in try/catch so refreshHierarchicalMemory() and refreshSystemInstruction() always execute. Skipped Set<string> dedup (1-3 sessions, negligible I/O).
| ? OUTPUT_LANGUAGE_AUTO | ||
| : resolved; | ||
|
|
||
| const targetPath = session.getConfig().getOutputLanguageFilePath(); |
There was a problem hiding this comment.
[Suggestion] The three-step sequence here — get targetPath from config, call updateOutputLanguageFile(settingValue, targetPath), then register the global fallback via setOutputLanguageFilePath() when targetPath is undefined — is copy-pasted identically in languageCommand.ts:128-134, and partially (missing the registration step) in SettingsDialog.tsx:382.
This duplication has already produced a concrete bug within this PR: SettingsDialog.tsx omits the registration step, so when getOutputLanguageFilePath() returns undefined, the global path is never registered on the config. Any future call site that omits or misorders a step will silently break language switching.
Consider extracting a helper in languageUtils.ts that encapsulates the full sequence:
export function writeOutputLanguageAndRegisterPath(
settingValue: string,
config: {
getOutputLanguageFilePath(): string | undefined;
setOutputLanguageFilePath(p: string): void;
},
): void {
const targetPath = config.getOutputLanguageFilePath();
updateOutputLanguageFile(settingValue, targetPath);
if (!targetPath) {
config.setOutputLanguageFilePath(getOutputLanguageFilePath());
}
}Then replace the three inline copies with a single call each.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Done — writeOutputLanguageAndRegisterPath() in languageUtils.ts replaces all 3 inline copies. This also fixed the SettingsDialog bug (comment #2) as a side effect.
- Add `supportedLanguages` to `CapabilitiesEnvelope` interface (TS2353) - Extract `writeOutputLanguageAndRegisterPath()` helper in languageUtils to eliminate the duplicated get-path/write/register sequence across acpAgent, languageCommand, and SettingsDialog (fixes SettingsDialog missing the registration step) - Wrap file writes in the multi-session refresh loop with try/catch so `refreshHierarchicalMemory` and `refreshSystemInstruction` always run even when a project-level write fails Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
已处理所有 review 评论,修复已推送。 评论处理结果:
跳过的部分:
|
chiga0
left a comment
There was a problem hiding this comment.
All 4 review findings addressed in commit f4e3ad9:
Comment 1 (server.ts:998 — supportedLanguages type): Fixed — added supportedLanguages?: string[] to CapabilitiesEnvelope in types.ts.
Comment 2 (SettingsDialog.tsx:383 — missing registration): Fixed — extracted writeOutputLanguageAndRegisterPath() helper, all 3 call sites now include the registration step.
Comment 3 (acpAgent.ts:2967 — multi-session error handling): Fixed — wrapped file writes in try/catch so refreshHierarchicalMemory() and refreshSystemInstruction() always execute. Skipped Set<string> dedup (1-3 sessions, negligible I/O).
Comment 4 (acpAgent.ts:2934 — extract helper): Done — writeOutputLanguageAndRegisterPath() in languageUtils.ts replaces all 3 inline copies.
Code Review Overview (AI Generated)PR: #4938 Independent Review Result: 0 findingsPhase 1 blind review of all 11 file diffs found no Critical, Major, Minor, or Nit-level issues. The bug fix is well-targeted, the dedup helper is clean, error handling is thorough, and SDK additions are backward-compatible. Recommendation: Approve (pending wenshao's re-review at HEAD). Note: PR has merge conflicts with Cross-Validationwenshao posted 4 findings on commit 1 (
ci-bot comment on acpAgent.ts duplicates #4. Additional Audit CoverageAreas I independently checked beyond wenshao's findings:
This review was generated by QoderWork AI |
- acpAgent.ts: consolidate duplicate languageUtils imports into one block - SettingsDialog.tsx: keep writeOutputLanguageAndRegisterPath + take useVimModeState/useVimModeActions from base - sdk.test.ts: fix unused import lint error from base Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No high-confidence issues found. The bug fix is correct and well-tested (845 tests pass). Two low-confidence suggestions for human review: (1) incomplete vi.mock for languageUtils.js in acpAgent.test.ts may cause confusing errors in future tests; (2) DaemonCapabilities SDK type could include supportedLanguages for typed discovery. — qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Code Review Summary
Thanks for this PR! The core fix — extracting writeOutputLanguageAndRegisterPath and adding multi-session fan-out — is a solid improvement. Deterministic checks (tsc, eslint, unit tests) all pass.
Below are findings from automated review across multiple dimensions (correctness, security, performance, test coverage, oncall readiness).
🔴 Critical: Missed call site — setCoreValue handler at line 6209
acpAgent.ts:6209 — updateOutputLanguageFile(normalizedValue) still uses the old API without session-specific path or session propagation. When an external client (e.g., desktop app via SDK extMethod) changes general.outputLanguage through qwen/settings/setCoreValue:
- It always writes to the global default path, ignoring any session-specific
outputLanguageFilePath - It never calls
refreshHierarchicalMemory()orrefreshSystemInstruction()on any session
This is the same bug class the PR fixes in the other three call sites. External clients setting the output language via the settings API will silently fail to update running sessions.
Suggested fix: After the updateOutputLanguageFile call, iterate this.sessions similarly to the language endpoint handler: write to each session's path and call refreshHierarchicalMemory() + refreshSystemInstruction().
🔵 Suggestion: Missing unit test for writeOutputLanguageAndRegisterPath
languageUtils.ts:183-197 — The new helper encapsulates the get-path → write → register-fallback sequence and is exercised from 3 call sites, but there is no languageUtils.test.ts. A direct unit test would catch regressions in the registration side-effect more precisely than integration-only coverage.
Inline comments below cover the remaining findings (refreshed flag semantics, redundant write, mock assertion gap).
| const results = await Promise.allSettled( | ||
| allSessions.map(async (s) => { | ||
| const cfg = s.getConfig(); | ||
| try { |
There was a problem hiding this comment.
[Critical] The inner try/catch around file writes swallows errors before allSettled sees them, so failedCount only tracks refreshHierarchicalMemory() / refreshSystemInstruction() failures. The refreshed flag will be true even when every other session's output-language.md write failed silently (permission denied, disk full).
On session restart, those sessions will revert to the previous language with no user-visible error.
Suggestion: Let the file-write error propagate (remove the inner try/catch) so allSettled captures it as a rejection, or track write failures separately:
let fileWriteFailed = false;
try {
const sessionPath = cfg.getOutputLanguageFilePath();
if (sessionPath && sessionPath !== writtenPath) {
updateOutputLanguageFile(settingValue, sessionPath);
}
if (!sessionPath) {
cfg.setOutputLanguageFilePath(getOutputLanguageFilePath());
}
} catch (err) {
fileWriteFailed = true;
debugLogger.warn('Failed to write output-language.md for session:', err);
}
await cfg.refreshHierarchicalMemory();
await cfg.getGeminiClient()?.refreshSystemInstruction();
// ...
refreshed = results.length === 0 || (failedCount === 0 && !fileWriteFailed);Also, when !sessionPath, writeOutputLanguageAndRegisterPath(settingValue, cfg) writes to the global default path which was already written above (identical to writtenPath). Only the registration side-effect (cfg.setOutputLanguageFilePath(...)) is needed — the write is redundant.
There was a problem hiding this comment.
Fixed in ec3b541:
- Removed inner try/catch — write errors now propagate to
allSettled, makingfailedCountandrefreshedaccurate. - For
!sessionPathcase: only register the path (cfg.setOutputLanguageFilePath(...)) — the global file was already written, so the write was indeed redundant.
| config: { | ||
| getModel: vi.fn().mockReturnValue('test-model'), | ||
| getOutputLanguageFilePath: vi.fn().mockReturnValue(undefined), | ||
| setOutputLanguageFilePath: vi.fn(), |
There was a problem hiding this comment.
[Medium] The mock provides setOutputLanguageFilePath: vi.fn() (here and at lines 467, 510), but no test asserts that setOutputLanguageFilePath was actually called with the expected argument.
The path-registration side-effect (cfg.setOutputLanguageFilePath(getOutputLanguageFilePath())) is the key new behavior of writeOutputLanguageAndRegisterPath. Without an assertion like:
expect(config.setOutputLanguageFilePath).toHaveBeenCalledWith(
expect.any(String),
);…a future refactor could silently drop the registration call and tests would still pass. Consider adding this assertion to at least one test case that exercises the write-and-register path.
There was a problem hiding this comment.
Added assertion in the "should save LLM output language setting" test that setOutputLanguageFilePath is called with a string when getOutputLanguageFilePath returns undefined.
Follow-up: Symlink Attack VectorAfter the initial review, the attacker mindset audit identified a security issue worth flagging: Severity: Critical (security) File: Issue: Attack scenario:
Suggested fix: Add a symlink check before writing: export function writeOutputLanguageFile(language: string, targetPath?: string): void {
const filePath = targetPath ?? getOutputLanguageFilePath();
if (fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink()) {
throw new Error(`Refusing to write to symlink: ${filePath}`);
}
const content = generateOutputLanguageFileContent(language);
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(filePath, content, 'utf-8');
}This is a denial-of-service / file corruption attack rather than code execution, but overwriting |
Follow-up:
|
- Remove inner try/catch in multi-session loop so file-write failures are captured by Promise.allSettled and reflected in `refreshed` - For sessions with no path: only register the global path (the file was already written by the primary write), skip the redundant write - Add test assertion that setOutputLanguageFilePath is called on first-time creation Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| cfg.setOutputLanguageFilePath(getOutputLanguageFilePath()); | ||
| } | ||
| await cfg.refreshHierarchicalMemory(); | ||
| await cfg.getGeminiClient()?.refreshSystemInstruction(); |
There was a problem hiding this comment.
[Critical] When the initiating session has a project-level outputLanguageFilePath (e.g. <cwd>/.qwen/output-language.md), writtenPath is that project path. But sessions without a path are registered to getOutputLanguageFilePath() (the global default ~/.qwen/output-language.md) — a file that was never written. Their refreshHierarchicalMemory() then reads stale content.
Fix: use writtenPath so the session points to the file that was actually written:
| await cfg.getGeminiClient()?.refreshSystemInstruction(); | |
| cfg.setOutputLanguageFilePath(writtenPath); |
— qwen3.7-plus via Qwen Code /review
There was a problem hiding this comment.
Fixed — restored writeOutputLanguageAndRegisterPath(settingValue, cfg) for !sessionPath sessions. When writtenPath is a project path, the global file was indeed never written, so we need the write here (not just registration).
| const cfg = s.getConfig(); | ||
| const sessionPath = cfg.getOutputLanguageFilePath(); | ||
| if (sessionPath && sessionPath !== writtenPath) { | ||
| updateOutputLanguageFile(settingValue, sessionPath); |
There was a problem hiding this comment.
[Suggestion] Commit ec3b541 removed the inner try/catch so write errors propagate to allSettled — but this means a write failure (EACCES, ENOSPC) throws out of the entire callback, skipping refreshHierarchicalMemory() and refreshSystemInstruction() for that session. The R1 reply said "wrapped file writes in try/catch so refresh always runs" but the actual code does the opposite.
Wrap only the write in try/catch so the refresh always executes:
const cfg = s.getConfig();
const sessionPath = cfg.getOutputLanguageFilePath();
try {
if (sessionPath && sessionPath !== writtenPath) {
updateOutputLanguageFile(settingValue, sessionPath);
}
} catch (err) {
debugLogger.warn(`Failed to write output-language.md for session:`, err);
}
if (!sessionPath) {
cfg.setOutputLanguageFilePath(writtenPath);
}
await cfg.refreshHierarchicalMemory();
await cfg.getGeminiClient()?.refreshSystemInstruction();— qwen3.7-plus via Qwen Code /review
There was a problem hiding this comment.
Fixed — restored try/catch around file writes only, so refreshHierarchicalMemory() and refreshSystemInstruction() always execute regardless of write outcome.
| ); | ||
| // Verify path registration on first-time creation (getOutputLanguageFilePath returned undefined) | ||
| expect( | ||
| (mockContext.services.config as Record<string, unknown>) |
There was a problem hiding this comment.
[Suggestion] This cast triggers two tsc errors:
- TS2352:
Conversion of type 'Config | null' to type 'Record<string, unknown>' may be a mistake - TS4111:
Property 'setOutputLanguageFilePath' comes from an index signature, so it must be accessed with ['setOutputLanguageFilePath']
Fix: use bracket notation with a double cast, or type the mock more precisely:
| (mockContext.services.config as Record<string, unknown>) | |
| expect( | |
| (mockContext.services.config as unknown as Record<string, unknown>)[ | |
| 'setOutputLanguageFilePath' | |
| ], | |
| ).toHaveBeenCalledWith(expect.any(String)); |
— qwen3.7-plus via Qwen Code /review
There was a problem hiding this comment.
Fixed — switched to double cast (as unknown as Record<string, unknown>) with bracket notation.
- Restore try/catch around file writes in multi-session loop so refresh always runs (write failures are logged, not propagated) - Restore writeOutputLanguageAndRegisterPath for !sessionPath sessions to handle the case where writtenPath is a project-level path and the global file was never written - Fix TS cast in test assertion (double-cast + bracket notation) Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| const writtenPath = | ||
| session.getConfig().getOutputLanguageFilePath() ?? | ||
| getOutputLanguageFilePath(); | ||
| const allSessions = [...this.sessions.values()]; |
There was a problem hiding this comment.
[Suggestion] The multi-session fan-out loop (lines 5542–5571) is the most complex new logic in this PR — three branching conditions handle different outputLanguageFilePath scenarios across sessions — but has no test coverage. A future refactor of the path comparison, write/refresh ordering, or writtenPath computation could silently break multi-session language propagation without any test catching it.
Consider adding a test that creates multiple mock sessions with varying getOutputLanguageFilePath() return values (project path, undefined, same as originating), triggers the language ext-method with syncOutputLanguage: true, and asserts:
- Files are written to correct paths
setOutputLanguageFilePathis called on sessions with no pathrefreshHierarchicalMemoryis called on every session
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Agreed this loop deserves dedicated test coverage. Filing as a follow-up — the multi-session fan-out requires mocking QwenAgent + multiple Session instances with varying Config, which is a meaningful test infrastructure addition beyond this bug fix scope.
There was a problem hiding this comment.
Added in 250b50b — test covers 3 session scenarios (project path / different project path / no path) and asserts file writes, path registration, and refresh calls for each.
Local runtime verification report (A/B: base
|
| base (bug) | merged (fix) | |
|---|---|---|
| HTTP response | {"language":"zh","outputLanguage":"Chinese","refreshed":true} (claims success) |
same |
| project file after switch | unchanged (English) | rewritten to Chinese ✅ |
global ~/.qwen/output-language.md |
leaked write (Chinese) | untouched ✅ |
| next captured model request | still English directive — switch silently ineffective | Chinese directive present ✅ |
The base branch reports success on all surfaces (REST 200, TUI shows "LLM output language set to Chinese") while the model keeps receiving the old language — exactly the silent failure described. The fix makes write path = read path (config.getOutputLanguageFilePath()).
S4 — one agent process, two sessions in different cwds (Zed-style; drove cli.js --acp over stdio, both workspaces have project files; switch on session 1):
- base: neither project file updated (write leaked to global); session 2's next request still carried the old English sentinel.
- merged: both sessions' project files updated (
sessionPath !== writtenPathbranch works), and session 2's next captured request actually carried the Chinese directive. The cross-session refresh is now effective instead of re-reading stale files. - Note (by design, worth being aware of): the per-session endpoint now writes other sessions' project files too — intentional propagation, consistent with the pre-existing refresh-all-sessions behavior; in daemon mode children are pooled per-cwd so this only materializes for direct ACP/Zed multi-cwd connections.
S5 — interactive TUI /language output zh (tmux, project file present): same split — base writes global and the TUI still prints the success message; merged writes the project file. Fix covers languageCommand.ts correctly.
S3 — GET /capabilities: base has no supportedLanguages; merged returns ["en","zh-TW","zh","ru","de","ja","pt","fr","ca","auto"], matching what POST /session/:id/language validates against.
SDK: built DaemonClient from the merged tree against a live daemon — capabilities().supportedLanguages ✅, setSessionLanguage(sid,'fr',{syncOutputLanguage:true}) → {"language":"fr","outputLanguage":"French","refreshed":true} and the workspace file flipped to French in real time ✅, invalid language → clean 400 with the allowed list ✅. (Heads-up: the method is capabilities(), not getCapabilities() — the PR body's test plan is fine, just noting for SDK users.)
Unit tests A/B — identical on both sides: languageCommand 48✅, languageUtils 45✅, SettingsDialog 53✅, server.test -t language 7✅, core config.test 202✅, acpAgent 108✅+2❌ — the 2 failures (status ext methods … workspace snapshots) fail identically on base, pre-existing and unrelated.
Finding 1 (should fix before merge): SettingsDialog.test.tsx now overwrites the developer's real ~/.qwen/output-language.md
Bisected and A/B-confirmed: running vitest run src/ui/components/SettingsDialog.test.tsx on the merged tree rewrites the real user-level ~/.qwen/output-language.md (my machine's file went 中文 → English; mtime matched the test run). The same suite on base does not touch it; languageUtils.test.ts / languageCommand.test.ts are clean on both sides.
Root cause: the suite's mock spreads the actual module and only stubs the old entry point:
vi.mock('../../utils/languageUtils.js', async () => {
const actual = await vi.importActual('../../utils/languageUtils.js');
return { ...actual, updateOutputLanguageFile: vi.fn() };
});The dialog now calls writeOutputLanguageAndRegisterPath, which resolves to the real implementation via ...actual; its internal call to updateOutputLanguageFile is an intra-module binding that bypasses the mock → real fs.writeFileSync to the global path on every test run (dev machines and CI runners alike). Tests still pass, so it's invisible.
One-line fix, verified locally (53/53 still pass, no more file write):
return { ...actual, updateOutputLanguageFile: vi.fn(), writeOutputLanguageAndRegisterPath: vi.fn() };Finding 2 (accuracy note on the PR description): the "first-time creation" branch is effectively unreachable in real flows
gemini.tsx calls initializeLlmOutputLanguage() in main() before loadCliConfig for every non-bare invocation — including serve and each spawned --acp child. Verified empirically: starting a daemon with a completely fresh $HOME auto-creates the global file immediately, so by the time any session Config is constructed, a file exists and outputLanguageFilePath is bound (S2 fresh-env A/B: both base and merged behave identically and correctly there — base passes that scenario not because of registration, but because the bound path is the global one). The setOutputLanguageFilePath() registration branch is defensively correct and unit-tested, but in production it only triggers in corner cases (file deleted while the daemon runs + a later session, startup write failure, --bare). No action needed — just don't rely on the PR-description framing of fix #2 when reviewing.
Finding 3 (base-branch health, not caused by this PR)
daemon_mode_b_maincurrently failsnpm run build:packages/core src/telemetry/sdk.test.ts(68,1) TS6133(unusedcreateSessionRootContext). This PR incidentally fixes it (the_createSessionRootContextrename that looks like formatting noise is actually what un-breaks the branch build).- Second pre-existing break on both sides:
packages/acp-bridge src/status.ts(782,14) TS2739— the serve hook-event meta table is missingUserPromptExpansionandInstructionsLoaded(fallout from a main merge).tscfails for acp-bridge and cascades intopackages/clibuilds, though emitted JS still lets the esbuild bundle work. Needs a separate fix on the branch.
Minor notes
- ~10 files in the diff (
trace-context*,useGeminiStream, i18n locales,extensions/consent|utils,forkCommand, coreconfig.test,qc-helper/SKILL.md) are prettier-only reformatting unrelated to the fix — all verified prettier-clean per repo config, so they won't flip back; just review noise. - The two-dot GitHub diff makes it look like
DaemonClient.reloadEnv()(feat(daemon): add POST /workspace/reload-env for hot-reloading env vars and session auth #4924) gets deleted — it does not: verified the 3-way merge result keeps it (the PR branched before feat(daemon): add POST /workspace/reload-env for hot-reloading env vars and session auth #4924; that hunk is an artifact of the stale fork point). Mergedsdk-typescriptbuilds clean. - Code-reading observation (not runtime-verified): in
--baremode,/language outputviawriteOutputLanguageAndRegisterPathwould register the global path on a Config that deliberately skipped language-file context at load; probably irrelevant in practice, mentioning for completeness.
Methodology / environment
- Worktrees: base =
origin/daemon_mode_b_main@89921cc7f; merged = cleangit mergeofpr-4938head (07d99b8bc) into base (26 files, no conflicts; matchesgit merge-tree). - Both sides: full
npm run build+npm run bundle(base needed the test-only TS6133 one-liner pre-applied to compile at all — see Finding 3; zero runtime effect, esbuild bundles from sources). - Bundle distinctness proven: merged
dist/cli.jscontains 5 hits forsetOutputLanguageFilePath|writeOutputLanguageAndRegisterPathand the serve chunk containssupportedLanguages; base has 0. - Daemons under tmux with isolated
$HOMEper scenario; authselectedType=openai+OPENAI_BASE_URLpointed at a local mock provider that dumps every/chat/completionsrequest body to disk — assertions are made on what the model would actually receive, not on logs. - S2 fresh-env, S1 project-file, S4 dual-cwd ACP stdio, S5 tmux TUI, S3/SDK live REST.
Verify the fan-out loop handles three session scenarios correctly: - Session A (project path): writeOutputLanguageAndRegisterPath called - Session B (different project path): updateOutputLanguageFile called - Session C (no path): writeOutputLanguageAndRegisterPath + path registration - All sessions: refreshHierarchicalMemory + refreshSystemInstruction Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- acpAgent.ts: consolidate languageUtils imports (base moved them) - sdk.test.ts: drop unused createSessionRootContext import (base removed it) Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
[Critical] packages/sdk-typescript/src/index.ts — SetSessionLanguageResult is exported from daemon/index.ts (line 389) but not re-exported from the top-level SDK barrel (src/index.ts). Peer types like SetModelResult (line 168) are properly re-exported. SDK consumers importing from @qwen-code/sdk cannot access this type, making setSessionLanguage() return type unusable.
Fix: Add type SetSessionLanguageResult to the export list in packages/sdk-typescript/src/index.ts.
Test coverage gaps (Suggestion):
writeOutputLanguageAndRegisterPath(languageUtils.ts:183) — the central new helper replacing 3 inline copies — has no unit tests. All 3 path scenarios (project path, global path, first-time creation) are untested.- The new
targetPathparameter onwriteOutputLanguageFileandupdateOutputLanguageFileis untested — only the global-default branch is exercised. supportedLanguagesin/capabilitiesresponse (server.ts:998) — no test asserts its presence or value.- SDK
setSessionLanguage()method (DaemonClient.ts:1618) — no tests for URL, request body, or error handling. setOutputLanguageFilePath()setter (config.ts:3030) — no test in config.test.ts.- SettingsDialog.test.tsx mock is stale — still mocks
updateOutputLanguageFile, notwriteOutputLanguageAndRegisterPath. - Fan-out test covers only the happy path — same-path skip, write error, and
fileWriteOk = falsebranches are untested.
Observability gap (Suggestion):
- Error logs at acpAgent.ts:5526 and 5559 omit session ID and target file path. When multiple sessions fail, logs are indistinguishable.
— qwen3.7-max via Qwen Code /review
| } | ||
| } catch (err) { | ||
| debugLogger.warn( | ||
| 'Failed to write output-language.md for session:', |
There was a problem hiding this comment.
[Suggestion] The error log here ('Failed to write output-language.md for session:') omits the session ID and target file path. When multiple sessions fail writes concurrently, the logs are indistinguishable — an operator cannot tell which session or which .qwen/ directory failed.
The same applies to the initiator's log at line 5526.
| 'Failed to write output-language.md for session:', | |
| debugLogger.warn( | |
| `Failed to write output-language.md for session ${s.getId()} (path=${sessionPath ?? 'global-default'}):`, | |
| err, | |
| ); |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Good catch — 已在日志中加入 session ID 和目标路径,方便排查多 session 并发写入失败的场景。
| PromptResult, | ||
| PromptTextContent, | ||
| SetModelResult, | ||
| SetSessionLanguageResult, |
There was a problem hiding this comment.
[Critical] SetSessionLanguageResult is added to the daemon barrel export here, but is not re-exported from the top-level SDK barrel at packages/sdk-typescript/src/index.ts. Peer types like SetModelResult (line 168 of src/index.ts) are properly re-exported. This means SDK consumers importing from @qwen-code/sdk cannot use the return type of setSessionLanguage().
Fix: Add type SetSessionLanguageResult to the export list in packages/sdk-typescript/src/index.ts, alongside the existing SetModelResult and SessionMetadataResult exports.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
确实漏了,已在 packages/sdk-typescript/src/index.ts 中补上 type SetSessionLanguageResult 的 re-export,与 SetModelResult 同级。
| } | ||
|
|
||
| this.sessionOrThrow(sessionId); | ||
| const session = this.sessionOrThrow(sessionId); |
There was a problem hiding this comment.
[Suggestion] Lines 5475-5478 compute [...SUPPORTED_LANGUAGES.map((l) => l.code), 'auto'] inline, duplicating the identical expression hoisted as LANGUAGE_CODES in server.ts:971. If a new language code is added to one site but not the other, the ACP and HTTP endpoints will silently disagree on what is valid.
Extract a single shared constant (e.g., export LANGUAGE_CODES from i18n/index.ts or languageUtils.ts) and import it in both server.ts and acpAgent.ts.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
认同重复计算的问题。不过提取共享常量属于跨模块重构(server.ts 在 serve 层,acpAgent 在 ACP 层),放到 follow-up 更合适,避免本 PR 范围扩大。
| * that was previously duplicated across acpAgent, languageCommand, and | ||
| * SettingsDialog. | ||
| */ | ||
| export function writeOutputLanguageAndRegisterPath( |
There was a problem hiding this comment.
[Suggestion] writeOutputLanguageAndRegisterPath — the core fix function — has no direct unit tests in languageUtils.test.ts. The only coverage comes from acpAgent.test.ts where it is fully mocked, so the real implementation is never exercised.
Key untested behaviors:
- Writes to
config.getOutputLanguageFilePath()when it returns a path - Falls back to the global path when config path is
undefined - Calls
setOutputLanguageFilePathwith the global path only on first creation - Handles
null/undefinedconfig gracefully
Add a describe('writeOutputLanguageAndRegisterPath') block covering these cases, plus a test for writeOutputLanguageFile(language, targetPath) verifying the custom path is used.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
已在 languageUtils.test.ts 中新增 4 个直接测试:config-bound 路径写入、undefined 回退 + 注册、null config、undefined config。覆盖了核心修复函数的关键分支。
- Include session ID and target path in multi-session write error logs - Re-export SetSessionLanguageResult from top-level SDK barrel - Add 4 unit tests for writeOutputLanguageAndRegisterPath covering config-bound path, undefined fallback, null/undefined config Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| } | ||
| } catch (err) { | ||
| debugLogger.warn( | ||
| `Failed to write output-language.md for session ${s.getId()} (path=${sessionPath ?? 'global-default'}):`, |
There was a problem hiding this comment.
[Critical] sessionPath is declared with const inside the try block (line 5554) but referenced here in the catch block. These are separate block scopes — sessionPath is not visible in catch.
Impact:
- TypeScript compilation error —
TS2304: Cannot find name 'sessionPath'(confirmed withtsc --noEmit --strict). - Runtime
ReferenceError: sessionPath is not defined— ifcfg.getOutputLanguageFilePath()throws, the catch block will throw a second error that masks the original, making debugging harder.
Suggested fix: Declare sessionPath before the try block so it is accessible in both try and catch:
let sessionPath: string | undefined;
try {
sessionPath = cfg.getOutputLanguageFilePath();
if (sessionPath && sessionPath !== writtenPath) {
updateOutputLanguageFile(settingValue, sessionPath);
}
if (!sessionPath) {
writeOutputLanguageAndRegisterPath(settingValue, cfg);
}
} catch (err) {
debugLogger.warn(
`Failed to write output-language.md for session ${s.getId()} (path=${sessionPath ?? 'global-default'}):`,
err,
);
}— automated review (qwen-code)
There was a problem hiding this comment.
好眼力,确实是块作用域 bug。已将 const sessionPath 提到 try 外改为 let sessionPath: string | undefined,catch 中可以正常访问了。
sessionPath was declared with const inside try but referenced in catch, causing a block-scope ReferenceError. Move to let before try. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| } | ||
| } catch (err) { | ||
| debugLogger.warn( | ||
| `Failed to write output-language.md for session ${s.getId()} (path=${sessionPath ?? 'global-default'}):`, |
There was a problem hiding this comment.
[Critical] sessionPath is declared with const inside the try block (line 5554), but is referenced here in the catch block. Since const is block-scoped, this is a TypeScript compile error (TS2304: Cannot find name 'sessionPath'). At runtime, the catch block would throw a ReferenceError, masking the original file-write error and skipping refreshHierarchicalMemory() / refreshSystemInstruction() for that session.
| `Failed to write output-language.md for session ${s.getId()} (path=${sessionPath ?? 'global-default'}):`, | |
| const cfg = s.getConfig(); | |
| let sessionPath: string | undefined; | |
| try { | |
| sessionPath = cfg.getOutputLanguageFilePath(); | |
| if (sessionPath && sessionPath !== writtenPath) { | |
| updateOutputLanguageFile(settingValue, sessionPath); | |
| } | |
| if (!sessionPath) { | |
| writeOutputLanguageAndRegisterPath(settingValue, cfg); | |
| } | |
| } catch (err) { | |
| debugLogger.warn( | |
| `Failed to write output-language.md for session ${s.getId()} (path=${sessionPath ?? 'global-default'}):`, | |
| err, | |
| ); | |
| } |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
已在上一个 commit (47217b9) 中修复了这个问题,const 已改为 let 并提升到 try 外。
|
|
||
| mockConnectionState.resolve(); | ||
| await agentPromise; | ||
| }); |
There was a problem hiding this comment.
[Suggestion] The multi-session fan-out test only covers the happy path where all writes succeed. The catch branch in the fan-out loop is never exercised — which is exactly why the sessionPath scoping bug (see comment on acpAgent.ts:5563) was not caught by tests.
Consider adding a test where one session's getOutputLanguageFilePath() throws, verifying that:
- The error is caught and logged without crashing the callback
- Other sessions' writes and refreshes still execute
refreshHierarchicalMemory()/refreshSystemInstruction()still run for the failed session
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
已添加 catch 分支测试 — 模拟一个 session 的 updateOutputLanguageFile 抛 EACCES,验证该 session 的 refreshHierarchicalMemory 和 refreshSystemInstruction 仍然被调用。
| ); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
[Suggestion] writeOutputLanguageFile and updateOutputLanguageFile both gained a new optional targetPath parameter, but no test passes a targetPath argument. The fan-out logic in acpAgent.ts depends on updateOutputLanguageFile(settingValue, sessionPath) writing to the correct path — this dependency is untested.
Suggested tests:
it('writes to targetPath when provided', () => {
writeOutputLanguageFile('Chinese', '/custom/.qwen/output-language.md');
expect(fs.writeFileSync).toHaveBeenCalledWith(
'/custom/.qwen/output-language.md',
expect.stringContaining('Chinese'),
'utf-8',
);
});
it('forwards targetPath to writeOutputLanguageFile', () => {
updateOutputLanguageFile('French', '/proj/.qwen/output-language.md');
expect(fs.writeFileSync).toHaveBeenCalledWith(
'/proj/.qwen/output-language.md',
expect.stringContaining('French'),
'utf-8',
);
});— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
已在 writeOutputLanguageFile 的 describe 块中添加了 targetPath 参数测试,验证自定义路径写入和目录创建。
| @@ -1651,6 +1652,33 @@ export class DaemonClient { | |||
| ); | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
[Suggestion] This new SDK method has no tests. It includes URL encoding, body serialization, syncOutputLanguage default (false), and error handling via failOnError — all unverified. Consider adding a test that asserts the correct HTTP request is made and the response is properly deserialized.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
DaemonClient.setSessionLanguage() 的 SDK 测试涉及 HTTP client mock 基础设施,和本 PR 的 bug fix 范围有距离。作为 follow-up 更合适,避免 scope 继续扩大。
- acpAgent: test that refreshHierarchicalMemory still runs when a session's file write throws (catch branch coverage) - languageUtils: test writeOutputLanguageFile with custom targetPath Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
ytahdn
left a comment
There was a problem hiding this comment.
No high-confidence critical issues found. The core bug fix is correct and well-tested (100 tests pass). The Suggestion-level findings (setCoreValue handler consistency, refreshed flag semantics, supportedLanguages test, SettingsDialog try/catch) can be addressed as follow-ups. — qwen3.7-max via Qwen Code /review
| @@ -997,6 +999,7 @@ export function createServeApp( | |||
| workspaceCwd: boundWorkspace, | |||
There was a problem hiding this comment.
[Suggestion] supportedLanguages in /capabilities response has no test assertion.
None of the existing GET /capabilities tests verify the presence or contents of the new supportedLanguages field. If accidentally removed or returning wrong values, no test catches it.
| workspaceCwd: boundWorkspace, | |
| // In server.test.ts ~line 1490, add: | |
| expect(res.body.supportedLanguages).toEqual(expect.arrayContaining(['zh', 'auto'])); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| // Update output language rule file immediately (no restart needed for LLM effect) | ||
| if (key === 'general.outputLanguage' && typeof parsed === 'string') { | ||
| updateOutputLanguageFile(parsed); | ||
| writeOutputLanguageAndRegisterPath(parsed, config); |
There was a problem hiding this comment.
[Suggestion] SettingsDialog test mock not updated for writeOutputLanguageAndRegisterPath.
The production code now calls writeOutputLanguageAndRegisterPath(parsed, config), but the test mock at line 173 still only provides updateOutputLanguageFile: vi.fn(). No assertion verifies that config.setOutputLanguageFilePath is called on first-time path registration via the Settings dialog.
| writeOutputLanguageAndRegisterPath(parsed, config); | |
| // Add to the mock: | |
| writeOutputLanguageAndRegisterPath: vi.fn(), | |
| // Add assertion in the Output Language test: | |
| expect(writeOutputLanguageAndRegisterPath).toHaveBeenCalledWith(expect.any(String), config); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| * Returns the path to the LLM output language rule file (~/.qwen/output-language.md). | ||
| */ | ||
| function getOutputLanguageFilePath(): string { | ||
| export function getOutputLanguageFilePath(): string { |
There was a problem hiding this comment.
[Suggestion] Name collision: module-level getOutputLanguageFilePath() shares a name with the Config instance method config.getOutputLanguageFilePath().
Both appear on consecutive lines in acpAgent.ts:5546-5547:
session.getConfig().getOutputLanguageFilePath() ?? // instance — session-bound
getOutputLanguageFilePath(); // module — always globalA future maintainer reading this without cross-referencing imports won't realize these are different functions returning potentially different values.
| export function getOutputLanguageFilePath(): string { | |
| // Rename the module export to getGlobalOutputLanguageFilePath() |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| ); | ||
| } | ||
| const writtenPath = | ||
| session.getConfig().getOutputLanguageFilePath() ?? |
There was a problem hiding this comment.
[Suggestion] writtenPath computed with a ?? getOutputLanguageFilePath() fallback that lacks documentation.
The fallback handles the edge case where writeOutputLanguageAndRegisterPath succeeded but the config path was still undefined (registration falls through to global default). The semantics require 3 hops of reasoning to understand.
| session.getConfig().getOutputLanguageFilePath() ?? | |
| // Path written by writeOutputLanguageAndRegisterPath above; | |
| // falls back to global when the initiator session had no | |
| // pre-existing config-bound path. | |
| const writtenPath = | |
| session.getConfig().getOutputLanguageFilePath() ?? | |
| getOutputLanguageFilePath(); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
doudouOUC
left a comment
There was a problem hiding this comment.
[Suggestion] SDK DaemonCapabilities type missing supportedLanguages field
The server-side CapabilitiesEnvelope adds supportedLanguages?: string[] and GET /capabilities emits it, but the SDK's DaemonCapabilities interface (types.ts:24) was not updated. TypeScript consumers of DaemonClient.capabilities() cannot access supportedLanguages through the typed API.
// In packages/sdk-typescript/src/daemon/types.ts, add to DaemonCapabilities:
supportedLanguages?: string[];
— qwen3.7-max via Qwen Code /review
| workspaceCwd: boundWorkspace, | ||
| // Active mediation policy under the `policy` namespace. | ||
| policy: { permission: bridge.permissionPolicy }, | ||
| supportedLanguages: LANGUAGE_CODES, |
There was a problem hiding this comment.
[Suggestion] supportedLanguages in /capabilities response has no test assertion.
None of the existing GET /capabilities tests verify the presence or contents of the new supportedLanguages field. If accidentally removed or returning wrong values, no test catches it.
| supportedLanguages: LANGUAGE_CODES, | |
| // In server.test.ts ~line 1490, add: | |
| expect(res.body.supportedLanguages).toEqual(expect.arrayContaining(['zh', 'auto'])); |
— qwen3.7-max via Qwen Code /review
| // Update output language rule file immediately (no restart needed for LLM effect) | ||
| if (key === 'general.outputLanguage' && typeof parsed === 'string') { | ||
| updateOutputLanguageFile(parsed); | ||
| writeOutputLanguageAndRegisterPath(parsed, config); |
There was a problem hiding this comment.
[Suggestion] SettingsDialog test mock not updated for writeOutputLanguageAndRegisterPath.
The production code now calls writeOutputLanguageAndRegisterPath(parsed, config), but the test mock (line 173 of SettingsDialog.test.tsx) still only provides updateOutputLanguageFile: vi.fn(). No assertion verifies that config.setOutputLanguageFilePath is called on first-time path registration via the Settings dialog.
| writeOutputLanguageAndRegisterPath(parsed, config); | |
| // Add to the mock in SettingsDialog.test.tsx: | |
| writeOutputLanguageAndRegisterPath: vi.fn(), | |
| // Add assertion in the Output Language test: | |
| expect(writeOutputLanguageAndRegisterPath).toHaveBeenCalledWith(expect.any(String), config); |
— qwen3.7-max via Qwen Code /review
| * Returns the path to the LLM output language rule file (~/.qwen/output-language.md). | ||
| */ | ||
| function getOutputLanguageFilePath(): string { | ||
| export function getOutputLanguageFilePath(): string { |
There was a problem hiding this comment.
[Suggestion] Name collision: module-level getOutputLanguageFilePath() shares a name with the Config instance method config.getOutputLanguageFilePath().
Both appear on consecutive lines in acpAgent.ts:5547-5548:
session.getConfig().getOutputLanguageFilePath() ?? // instance — session-bound
getOutputLanguageFilePath(); // module — always globalA future maintainer reading this without cross-referencing imports won't realize these are different functions returning potentially different values (string | undefined vs string).
| export function getOutputLanguageFilePath(): string { | |
| // Rename the module export: | |
| export function getGlobalOutputLanguageFilePath(): string { |
— qwen3.7-max via Qwen Code /review
Summary
Fixes a bug in
POST /session/:id/language(introduced in #4705) where the language switch silently fails because the output-language file is written to the wrong path.Root cause:
updateOutputLanguageFile()always writes to the global~/.qwen/output-language.md, butConfig.outputLanguageFilePathmay point to a project-level<cwd>/.qwen/output-language.md(when it existed at daemon startup). SincerefreshHierarchicalMemoryreads from the Config-bound path, the written file is never read back — the language switch has no effect.Additional issue: On a fresh environment where no
output-language.mdexists, the first language switch creates the file at the global path, butConfig.outputLanguageFilePathremainsundefined(wasreadonly), sorefreshHierarchicalMemorynever discovers the newly created file.Changes
Bug fixes
Config.outputLanguageFilePath: removereadonly, addsetOutputLanguageFilePath()setter so the path can be registered after first-time file creationlanguageUtils.ts: add optionaltargetPathparameter towriteOutputLanguageFile()andupdateOutputLanguageFile(), exportgetOutputLanguageFilePath()for callers that need the global defaultacpAgent.ts: write to the session Config's actual path; on first-time creation register the path on Config; on multi-session refresh also update each session's own file if its path differslanguageCommand.tsandSettingsDialog.tsx: same Config-bound path fix for the CLI/languagecommand and settings dialogEnhancements
server.ts: exposesupportedLanguagesarray inGET /capabilitiesresponse so clients can discover valid language codes before calling the endpointDaemonClient.setSessionLanguage()method andSetSessionLanguageResulttypeHow it ensures read/write consistency
config.getOutputLanguageFilePath()= project pathconfig.getOutputLanguageFilePath()= global pathsetOutputLanguageFilePath()registers itTest plan
npx vitest run packages/cli/src/ui/commands/languageCommand.test.ts— 48 tests passnpx vitest run packages/cli/src/serve/server.test.ts -t "language"— 7 language route tests passGET /capabilitiesreturnssupportedLanguages,POST /session/:id/languagevalidates against the list.qwen/output-language.mdand verify language switch takes effectRelated
🤖 Generated with Qwen Code