Skip to content

fix(daemon): Refresh workspace provider defaults - #5638

Merged
wenshao merged 10 commits into
QwenLM:mainfrom
doudouOUC:fix/workspace-providers-fresh-default
Jun 23, 2026
Merged

fix(daemon): Refresh workspace provider defaults#5638
wenshao merged 10 commits into
QwenLM:mainfrom
doudouOUC:fix/workspace-providers-fresh-default

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR makes GET /workspace/providers represent the workspace-level model catalog plus the default model that the next new session will use. The route now builds that snapshot daemon-side from fresh workspace settings and environment on every request instead of depending on a live ACP/session config, while still reporting whether the ACP channel is live as separate diagnostic state.

Existing sessions now initialize their current model and model list from session context model state first, with workspace providers used only as a fallback. When a session model change persists a new default model, the daemon emits a workspace settings change signal so clients that cache workspace provider data can reload.

Why it's needed

The previous behavior let /workspace/providers reflect live provider/session memory, which could be stale or unavailable before a session existed. That made landing pages and new-session entry points show the wrong default model, and it also encouraged clients to treat the workspace default as an existing session's current model. Separating workspace defaults from session context makes the pre-session default display fresh and keeps active session model state session-scoped.

Reviewer Test Plan

How to verify

Call GET /workspace/providers before creating a session after changing model settings and confirm current.modelId reflects the latest settings while initialized remains true even without a live ACP channel. In an existing session, set the session context current model to a different value from the workspace provider default and confirm the WebUI connection state shows the session context model, not the workspace default. For duplicate model IDs with different base URLs, confirm only the exact persisted base URL entry is marked current and credentials in provider base URLs are not exposed.

Focused checks run locally: cd packages/cli && npx vitest run src/serve/workspace-providers-status.test.ts src/serve/workspace-service/__tests__/facade.test.ts; cd packages/cli && npx vitest run src/serve/server.test.ts -t "returns workspace skills from the bridge and providers from daemon-local settings"; cd packages/acp-bridge && npx vitest run src/bridge.test.ts -t "setSessionModel"; cd packages/webui && npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx -t "session context models|provider models when session context|unmatched session model"; cd packages/acp-bridge && npm run typecheck; cd packages/webui && npm run typecheck; cd packages/sdk-typescript && npm run typecheck.

Evidence (Before & After)

N/A for screenshots. This is daemon/API and WebUI state-source behavior covered by focused unit tests.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

macOS local checkout, Node v22.22.3, npm 10.9.8.

Risk & Scope

  • Main risk or tradeoff: /workspace/providers now reports workspace defaults instead of live session model state, so clients that wanted a session current model must use session context instead.
  • Not validated / out of scope: full CLI package typecheck still fails on existing BaseTextInput.tsx Ink subpath type resolution errors unrelated to this change.
  • Breaking changes / migration notes: no route shape migration is required; existing fields are retained and acpChannelLive is additive.

Linked Issues

N/A

中文说明

What this PR does

这个 PR 将 GET /workspace/providers 明确定义为 workspace 级模型 catalog 加下一次新建 session 会使用的默认模型。该接口现在由 daemon 侧每次请求从最新 workspace settings 和环境变量构建快照,不再依赖 live ACP/session config,同时把 ACP channel 是否在线作为独立诊断状态返回。

已有 session 现在优先从 session context 的模型状态初始化当前模型和模型列表,只有缺少 context 模型信息时才 fallback 到 workspace providers。session 模型切换如果持久化了新的默认模型,daemon 会发送 workspace settings change 信号,让缓存 workspace provider 数据的客户端可以重新拉取。

Why it's needed

之前 /workspace/providers 可能反映 live provider/session 内存;在还没创建 session 时,这份内存可能不可用或过期,导致首页和新建 session 入口展示错误的默认模型,也会诱导客户端把 workspace default 当成已有 session 的 current model。拆分 workspace default 和 session context 后,新建 session 前的默认模型展示会保持 fresh,已有 session 的模型状态也保持 session-scoped。

Reviewer Test Plan

How to verify

修改模型 settings 后、创建 session 之前调用 GET /workspace/providers,确认 current.modelId 反映最新 settings,并且即使 ACP channel 不在线 initialized 仍为 true。对于已有 session,将 session context current model 设置成不同于 workspace provider default 的值,确认 WebUI connection state 展示 session context model,而不是 workspace default。对于相同 model id 但 base URL 不同的 provider,确认只有精确匹配持久化 base URL 的条目标记为 current,并且 provider base URL 不泄露凭据。

本地已运行的聚焦检查:cd packages/cli && npx vitest run src/serve/workspace-providers-status.test.ts src/serve/workspace-service/__tests__/facade.test.tscd packages/cli && npx vitest run src/serve/server.test.ts -t "returns workspace skills from the bridge and providers from daemon-local settings"cd packages/acp-bridge && npx vitest run src/bridge.test.ts -t "setSessionModel"cd packages/webui && npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx -t "session context models|provider models when session context|unmatched session model"cd packages/acp-bridge && npm run typecheckcd packages/webui && npm run typecheckcd packages/sdk-typescript && npm run typecheck

Evidence (Before & After)

N/A,无截图。这是 daemon/API 和 WebUI 状态来源行为变更,已由聚焦单测覆盖。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

macOS 本地 checkout,Node v22.22.3,npm 10.9.8。

Risk & Scope

  • Main risk or tradeoff: /workspace/providers 现在表达 workspace default,而不是 live session model state;需要 session current model 的客户端应改用 session context。
  • Not validated / out of scope: CLI package 的完整 typecheck 仍因既有 BaseTextInput.tsx Ink 子路径类型解析错误失败,和本次改动无关。
  • Breaking changes / migration notes: 不需要 route shape 迁移;现有字段保留,acpChannelLive 是新增可选字段。

Linked Issues

N/A

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC marked this pull request as ready for review June 22, 2026 14:04
Copilot AI review requested due to automatic review settings June 22, 2026 14:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: this is a well-scoped daemon/API fix — /workspace/providers now serves a fresh snapshot from workspace settings instead of reflecting live session state, which fixes stale defaults on landing pages and prevents clients from conflating workspace defaults with active session models. The separation of concerns is sound and squarely within the daemon's model-provider surface. Aligned with roadmap/model-inference.

On approach: the scope feels right for the stated goal. The 1389 additions are dominated by tests (~850+ lines), and the production changes are focused: a new daemon-local provider catalog builder (workspace-providers-status.ts), session-context model preference in the WebUI, credential sanitization consolidation (sanitizeProviderBaseUrl moved to a shared utility), and settings_changed event emission on model switches. No drive-by refactors or scope creep — each change directly serves the workspace-vs-session separation. The credential sanitization move from acpAgent.ts to acpModelUtils.ts is a clean consolidation, not a bolt-on.

Moving on to code review and testing. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是一个聚焦的 daemon/API 修复——/workspace/providers 现在从 workspace settings 构建新鲜快照,不再反映 live session 状态,修复了首页默认模型过期和客户端混淆 workspace 默认与 session 当前模型的问题。关注点分离合理,属于 daemon model-provider 范畴,与 roadmap/model-inference 对齐。

方案:范围与目标匹配。1389 行新增以测试为主(~850+ 行),生产代码改动聚焦:新增 daemon-local provider catalog 构建器(workspace-providers-status.ts)、WebUI 优先使用 session context 模型、凭据清洗函数整合(sanitizeProviderBaseUrl 移至共享工具)、以及 model switch 时发送 settings_changed 事件。没有夹带无关重构或范围蔓延。凭据清洗从 acpAgent.ts 移到 acpModelUtils.ts 是干净的整合。

进入代码审查和测试 🔍

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.

[Suggestion] applyModelServiceId (used during session creation and attach-with-model-change) only broadcasts model_switched, but does not broadcast settings_changed. This creates an asymmetry with setSessionModel which now broadcasts both. If consumers start acting on settings_changed to refresh workspace provider state, model changes via attach would be silently missed. Consider adding the same settings_changed broadcast to applyModelServiceId for consistency.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/workspace-providers-status.ts
options: WorkspaceProvidersStatusProviderOptions,
): ServeWorkspaceProvidersStatus {
try {
const loaded = loadSettings(workspaceCwd);

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] loadSettings() performs synchronous file I/O (fs.readFileSync, fs.realpathSync, fs.existsSync) on every GET /workspace/providers request with no caching. Combined with ModelsConfig construction, this is non-trivial per-request work on the main thread.

Consider a short-lived TTL cache (e.g., 500ms) or event-driven invalidation using the settings_changed events this PR already introduces. This preserves the "fresh" semantics without paying full I/O cost on every poll:

let cached: { at: number; result: ServeWorkspaceProvidersStatus } | undefined;
const TTL_MS = 500;
return async (workspaceCwd, acpChannelLive) => {
  if (cached && Date.now() - cached.at < TTL_MS)
    return { ...cached.result, acpChannelLive };
  const result = buildWorkspaceProvidersStatus(workspaceCwd, acpChannelLive, options);
  cached = { at: Date.now(), result };
  return result;
};

— qwen3.7-max via Qwen Code /review

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: to solve "workspace defaults vs session model" confusion, I'd (1) make /workspace/providers answer from daemon-local settings instead of querying ACP, (2) have the WebUI prefer session context models over workspace providers for active sessions, and (3) broadcast a settings change signal when the model is switched so caching clients can refresh. The PR does exactly this.

No blockers found. Code is clean and follows project conventions.

One minor observation: stripAcpAuthSuffix in packages/webui/src/daemon/session/mappers.ts is functionally identical to the existing parseAcpBaseModelId in packages/cli/src/utils/acpModelUtils.ts — both strip a trailing (authType) suffix. Since they live in different packages (webui can't import from cli), this duplication is structural rather than fixable, so not flagging it.

The sanitizeProviderBaseUrl consolidation is a net improvement — the new version in acpModelUtils.ts is more robust than the old local copy in acpAgent.ts, handling malformed URLs with spaces/special chars in credentials via a URL parser + manual fallback approach. The 16-case parameterized test covers the important edge cases.

The settings_changed event emission in the bridge is correctly placed after publishModelSwitched and before the reconcile corrective switch, ensuring clients see the right ordering (verified by test).

Test Results

All PR-specific tests pass ✅

$ cd packages/cli && npx vitest run src/serve/workspace-providers-status.test.ts
 ✓ src/serve/workspace-providers-status.test.ts (9 tests) 62ms
 Test Files  1 passed (1)  Tests  9 passed (9)

$ cd packages/cli && npx vitest run src/serve/workspace-service/__tests__/facade.test.ts
 ✓ src/serve/workspace-service/__tests__/facade.test.ts (44 tests) 53ms
 Test Files  1 passed (1)  Tests  44 passed (44)

$ cd packages/cli && npx vitest run src/utils/acpModelUtils.test.ts
 ✓ src/utils/acpModelUtils.test.ts (21 tests) 10ms
 Test Files  1 passed (1)  Tests  21 passed (21)

$ cd packages/acp-bridge && npx vitest run src/bridge.test.ts -t "setSessionModel"
 ✓ src/bridge.test.ts (296 tests | 287 skipped) 36ms
 Test Files  1 passed (1)  Tests  9 passed | 287 skipped (296)

$ cd packages/webui && npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx \
    -t "session context models|provider models when session context|unmatched session model"
 ✓ src/daemon/session/DaemonSessionProvider.test.tsx (88 tests | 84 skipped) 57ms
 Test Files  1 passed (1)  Tests  4 passed | 84 skipped (88)

tmux real-scenario testing: N/A — this is a daemon/API and WebUI state-source change with no TUI-visible behavior to capture. The behavior (which model appears as "current" in the WebUI connection state and which /workspace/providers returns) is fully covered by the unit tests above. Attempting to demonstrate it via tmux would require standing up a full daemon + mock ACP + WebUI stack, which is beyond what terminal capture can show.

中文说明

代码审查

独立方案:为解决 "workspace 默认 vs session 模型" 混淆,我会 (1) 让 /workspace/providers 从 daemon-local settings 回答而不是查询 ACP,(2) WebUI 对活跃 session 优先使用 session context 模型,(3) model switch 时广播 settings change 信号让缓存客户端刷新。PR 正是这样做的。

未发现阻塞问题。 代码整洁,遵循项目规范。

一个小观察:mappers.ts 中的 stripAcpAuthSuffixacpModelUtils.ts 中已有的 parseAcpBaseModelId 功能相同,但由于分别属于 webui 和 cli 包(webui 不能导入 cli),这是结构性重复而非可修复的,不做标记。

sanitizeProviderBaseUrl 整合是净改进——新版本通过 URL parser + 手动回退处理含空格/特殊字符凭据的畸形 URL,比 acpAgent.ts 中的旧本地副本更健壮。16 个参数化测试用例覆盖了重要边界情况。

bridge 中的 settings_changed 事件在 publishModelSwitched 之后、reconcile 纠正 switch 之前发送,保证客户端看到正确顺序(已由测试验证)。

测试结果

所有 PR 相关测试通过 ✅

tmux 真实场景测试:N/A — 这是 daemon/API 和 WebUI 状态来源变更,无 TUI 可见行为可捕获。"哪个模型显示为 current" 和 /workspace/providers 返回内容由上述单测完全覆盖。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Stepping back: this PR solves a real problem — clients seeing stale or wrong default models before a session exists, and active sessions incorrectly reflecting workspace defaults instead of their own model state. The fix is architecturally clean: daemon builds a fresh provider snapshot from settings on each request, the WebUI correctly prioritizes session context over workspace defaults, and model switches propagate a settings_changed event so caching clients stay in sync.

The implementation matches my independent proposal. The test suite is thorough — 87+ focused tests across 5 suites, all passing. Credential sanitization is handled consistently across every output path (provider URLs, warning messages, current selection), with a 16-case parameterized test that covers tricky edge cases like spaces, quotes, and encoded characters in credentials.

The one minor duplication (stripAcpAuthSuffix vs parseAcpBaseModelId) is structural — different packages that can't cross-import — so it's not something to fix in this PR.

No concerns. Looks ready to ship. ✅

中文说明

回顾整体:这个 PR 解决了一个真实问题——客户端在 session 创建前看到过期或错误的默认模型,活跃 session 错误地反映 workspace 默认而非自己的模型状态。修复在架构上很干净:daemon 每次请求从 settings 构建新鲜 provider 快照,WebUI 正确优先使用 session context,model switch 时传播 settings_changed 事件让缓存客户端保持同步。

实现与我的独立方案一致。测试套件充分——5 个套件共 87+ 个聚焦测试全部通过。凭据清洗在每个输出路径(provider URL、警告信息、当前选择)都一致处理,16 个参数化测试覆盖了空格、引号、编码字符等棘手边界情况。

唯一的轻微重复(stripAcpAuthSuffix vs parseAcpBaseModelId)是结构性的——不同包之间不能交叉导入——不需要在这个 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.

LGTM, looks ready to ship. ✅

@doudouOUC doudouOUC self-assigned this Jun 22, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
wenshao
wenshao previously approved these changes Jun 22, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer local verification — real build, focused tests, mutation analysis & live-daemon e2e

Verified at commit 69e2ddd on macOS, Node v22.22. Clean npm ci && npm run build. Every focused suite is green, every typecheck is green (including the full cli package), all 7 mutation probes are caught, and a live qwen serve daemon passes 14/14 assertions on the real GET /workspace/providers route — including the headline "fresh settings on every request", proven by rewriting settings.json against a running daemon. Recommend merge.

1. Build

npm ci ✓ → npm run build ✓ (exit 0).

2. Focused unit tests (your Reviewer Test Plan) — all green

Suite Result
cli · workspace-providers-status.test.ts 6 / 6
cli · workspace-service/__tests__/facade.test.ts 44 / 44
cli · server.test.ts -t "…providers from daemon-local settings" 1 / 1
acp-bridge · bridge.test.ts -t "setSessionModel" 9 / 9
webui · DaemonSessionProvider.test.tsx -t "session context…" 3 / 3

📝 Minor: the test plan lists src/serve/workspaceProvidersStatus.test.ts, but the actual file is kebab-case src/serve/workspace-providers-status.test.ts. Copy-pasting the command as written prints "No test files found". Cosmetic — worth fixing in the description.

3. Mutation testing — are the new tests non-vacuous? Yes.

I reverted each production change one at a time (keeping the PR's tests) and re-ran. Every mutant is caught by exactly the test(s) that should catch it:

Production code reverted / mutated Suite Tests that flip to ❌
drop acpChannelLive from the snapshot wps 2 (fresh defaults; error path)
disable sanitizeProviderBaseUrl wps 1 (the credential-leak assertion)
matchesCurrentBaseUrlreturn true wps 2 (duplicate-baseUrl exact match)
remove the daemon-local delegation branch facade 1
remove workspaceProvidersStatusProvider route wiring server 1
remove both settings_changed broadcasts bridge 3 (2 of them by timing out waiting for the never-emitted event — confirms the live subscription genuinely depends on it)
revert session-context-first (DaemonSessionProvider + mappers) webui 3

4. Typecheck — all green

acp-bridge, webui, sdk-typescript: exit 0, 0 errors. The full cli tsc --noEmit also passes (0 errors) in a clean npm ci && npm run build tree — the BaseTextInput.tsx Ink-subpath caveat noted in Risk & Scope did not reproduce here, and no error touches any file in this PR.

5. Live-daemon e2e (the real HTTP route) — 14 / 14

Booted one real qwen serve (loopback, auth-free, no model creds) and hit GET /workspace/providers, rewriting the user settings.json between requests on the same running daemon. acpChannelLive:false throughout (no ACP child was ever spawned).

A — fresh defaults + credential redaction — settings: model-a selected, baseUrl https://user:secret@api-a.example/v1
initialized:true, acpChannelLive:false, current.modelId:"model-a(openai)", current.baseUrl:"https://api-a.example/v1" (userinfo stripped), secret appears nowhere in the body; model-a.isCurrent:true, model-b.isCurrent:false. ✓ (8/8)

B — fresh read, no restart — rewrote settings → model-b on the same daemon
→ route now returns current.modelId:"model-b(openai)". ✓ (2/2) This is the central claim: the snapshot is rebuilt from fresh workspace settings on every request, with no live ACP/session.

C — duplicate modelId, exact persisted baseUrl — two shared-model entries, model.baseUrl = api-two
→ only the api-two entry is isCurrent:true, api-one is isCurrent:false, no creds leaked. ✓ (4/4)

respA (trimmed to current + the openai provider)
{
  "v": 1,
  "initialized": true,
  "acpChannelLive": false,
  "current": { "authType": "openai", "modelId": "model-a(openai)", "baseUrl": "https://api-a.example/v1" },
  "providers": [
    { "kind": "model_provider", "status": "ok", "authType": "openai", "current": true, "models": [
      { "modelId": "model-a(openai)", "name": "Model A", "contextLimit": 131072, "baseUrl": "https://api-a.example/v1", "isCurrent": true,  "isRuntime": false },
      { "modelId": "model-b(openai)", "name": "Model B", "contextLimit": 131072, "baseUrl": "https://api-b.example/v1", "isCurrent": false, "isRuntime": false }
    ]}
  ]
}

Verdict

Behavior matches the PR's stated intent end-to-end; coverage is comprehensive and non-vacuous; types are clean; the two subtle/security-relevant bits — credential redaction and exact duplicate-baseUrl matching — are both correct and guarded by tests and confirmed live. LGTM / recommend merge. Only nit is the test-path typo in the description.

🇨🇳 中文版(完整对应)

✅ 维护者本地验证 —— 真实构建、聚焦测试、变异分析与真实 daemon 端到端

在 commit 69e2ddd、macOS、Node v22.22 上验证。干净的 npm ci && npm run build所有聚焦测试套件全绿、所有 typecheck 全绿(含完整 cli 包)、7 个变异探针全部被捕获,且真实 qwen serve daemon 对真实 GET /workspace/providers 路由通过 14/14 断言 —— 包括最核心的"每次请求读最新设置",方法是对一个正在运行的 daemon 改写 settings.json建议合并。

1. 构建

npm ci ✓ → npm run build ✓(exit 0)。

2. 聚焦单测(你的 Reviewer Test Plan)—— 全绿

套件 结果
cli · workspace-providers-status.test.ts 6 / 6
cli · workspace-service/__tests__/facade.test.ts 44 / 44
cli · server.test.ts -t "…providers from daemon-local settings" 1 / 1
acp-bridge · bridge.test.ts -t "setSessionModel" 9 / 9
webui · DaemonSessionProvider.test.tsx -t "session context…" 3 / 3

📝 小问题:测试计划里写的是 src/serve/workspaceProvidersStatus.test.ts,但实际文件是 kebab-case 的 src/serve/workspace-providers-status.test.ts。照搬命令会打印 "No test files found"。纯文档问题,建议在描述里修正。

3. 变异测试 —— 这些新测试是否"非空洞"?是的。

我逐个把生产代码改动还原(保留 PR 的测试)后重跑。每个变异体都恰好被应当捕获它的测试捕获:

还原/改动的生产代码 套件 翻红的测试 ❌
从快照中删掉 acpChannelLive wps 2(fresh defaults;错误路径)
关闭 sanitizeProviderBaseUrl wps 1(凭据泄露断言)
matchesCurrentBaseUrlreturn true wps 2(重复 baseUrl 精确匹配)
删除 daemon-local 委托分支 facade 1
删除 workspaceProvidersStatusProvider 路由接线 server 1
删除两处 settings_changed 广播 bridge 3(其中 2 个因等待永不到达的事件而超时失败 —— 证明实时订阅确实依赖它)
还原 session-context 优先逻辑(DaemonSessionProvider + mappers webui 3

4. Typecheck —— 全绿

acp-bridgewebuisdk-typescriptexit 0,0 错误。在干净的 npm ci && npm run build 树上,完整 clitsc --noEmit 同样通过(0 错误) —— Risk & Scope 里提到的 BaseTextInput.tsx Ink 子路径既有错误在此未复现,也没有任何错误涉及本 PR 的文件。

5. 真实 daemon 端到端(真实 HTTP 路由)—— 14 / 14

启动一个真实 qwen serve(loopback、免鉴权、无 model 凭据),对 GET /workspace/providers 发请求,并在同一运行中的 daemon 上于请求之间改写用户 settings.json。全程 acpChannelLive:false(从未拉起 ACP 子进程)。

A —— fresh 默认值 + 凭据脱敏 —— 设置:选中 model-a,baseUrl https://user:secret@api-a.example/v1
initialized:trueacpChannelLive:falsecurrent.modelId:"model-a(openai)"current.baseUrl:"https://api-a.example/v1"(userinfo 已剥离),整个响应体不含 secretmodel-a.isCurrent:truemodel-b.isCurrent:false。✓(8/8)

B —— 热读、不重启 —— 在同一 daemon 上把设置改写为 model-b
→ 路由立即返回 current.modelId:"model-b(openai)"。✓(2/2)这是核心声明:快照在每次请求时从最新 workspace 设置重建,无需 live ACP/session。

C —— 重复 modelId,精确匹配持久化的 baseUrl —— 两个 shared-model 条目,model.baseUrl = api-two
→ 仅 api-two 条目 isCurrent:trueapi-oneisCurrent:false,无凭据泄露。✓(4/4)

结论

行为与 PR 声明的意图端到端一致;覆盖全面且非空洞;类型干净;两个微妙/安全相关的点 —— 凭据脱敏重复 baseUrl 精确匹配 —— 都正确、有测试守护、并经真实运行确认。LGTM / 建议合并。 唯一小瑕疵是描述里的测试路径笔误。

Verification method: isolated git worktree of PR head + full npm ci/npm run build; mutation probes applied to production files only (PR tests kept) then reverted; e2e against a real loopback qwen serve binary. Working tree confirmed clean after all probes.

Comment thread packages/cli/src/serve/workspace-providers-status.ts
Comment thread packages/webui/src/daemon/session/DaemonSessionProvider.tsx
Sanitize provider warning URLs before returning workspace providers status and preserve session context-window fallback from provider catalog models.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qqqys
qqqys previously approved these changes Jun 22, 2026

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

Critical issues from the prior review are resolved in the current head: the no-baseUrl current-model matching no longer marks explicit baseUrl variants as current, and provider warning URLs are sanitized before they are returned. I did not find any new critical blocker in this pass.

Comment thread packages/acp-bridge/src/bridge.ts Outdated
Broadcast settings_changed inside the serialized model switch callback so it cannot be reordered behind reconcile's corrective model_switched event.

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

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

⚠️ Downgraded from Request Changes to Comment: CI still running.

[Critical] The catch block in buildWorkspaceProvidersStatus passes error.message into the API response without the credential sanitization that the success path applies. An exception from loadSettings(), ModelsConfig, or resolveCliGenerationConfig() could embed a credential-bearing URL into the error cell, leaking credentials to connected clients.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/workspace-providers-status.ts Outdated
: undefined;
const fastModelId =
typeof settings.fastModel === 'string' && settings.fastModel.length > 0
? settings.fastModel

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] isCurrent computation reads from two independent data sources: buildExplicitModelBaseUrls(settings.modelProviders) parses raw settings to determine which models have explicit base URLs, while the main iteration loop reads from modelsConfig.getAllConfiguredModels(). If ModelsConfig ever transforms the model list differently from raw settings (e.g., deduplication, system defaults, or model merging), the explicitModelBaseUrls set membership check could disagree with the iterated model, silently breaking isCurrent accuracy. Consider deriving both from the same source.

— qwen3.7-max via Qwen Code /review

]);
publishModelSwitched(entry, modelId, originatorClientId);
broadcastWorkspaceEvent({
type: 'settings_changed',

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 broadcastWorkspaceEvent({ type: 'settings_changed', ... }) block is duplicated verbatim at two call sites (here and in setSessionModel), always immediately after publishModelSwitched. Consider folding the broadcast into publishModelSwitched itself so the two events are always emitted as a pair. This also avoids the current silent asymmetry at the reconcile path (which calls publishModelSwitched without settings_changed).

— qwen3.7-max via Qwen Code /review

Sanitize provider construction error messages before returning them from /workspace/providers so credential-bearing URLs cannot leak through the error path.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/cli/src/utils/acpModelUtils.ts
Comment thread packages/cli/src/serve/workspace-providers-status.ts
transportClosed,
]);
publishModelSwitched(entry, modelId, originatorClientId);
broadcastWorkspaceEvent({

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] settings_changed is broadcast here and in setSessionModel (line 3955), but two other publishModelSwitched call sites were not updated:

  1. Line 1220 (agent-initiated): onModelPromoted callback — invoked when the ACP child sends current_model_update (agent changes its own model mid-conversation). Only publishModelSwitched is called, no settings_changed.
  2. Line 1999 (reconciliation): After session reattach, if the actual model diverges from cached currentModelId, the bridge corrects via publishModelSwitched — but no settings_changed.

Clients using settings_changed to refresh settings/model-name display will show stale data when the agent autonomously switches models or the bridge corrects on reconnect. The existing reconciliation test (around line 9337) only asserts settings_changed for the client-requested switch, not the corrective one.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/workspace-providers-status.ts
wenshao
wenshao previously approved these changes Jun 22, 2026

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No additional high-confidence issues found. LGTM.

-- GPT-5 Codex via Qwen Code /review

qqqys
qqqys previously approved these changes Jun 22, 2026

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

Critical issue from the latest review is fixed in current head: provider construction errors are sanitized before being returned from /workspace/providers, so credential-bearing URLs no longer leak through the error path. I did not find any new critical blocker in this pass.

@doudouOUC
doudouOUC dismissed stale reviews from qqqys and wenshao via 0639b8b June 22, 2026 23:12
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC force-pushed the fix/workspace-providers-fresh-default branch from 0639b8b to b4c1138 Compare June 22, 2026 23:15
@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

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

All 5 focused test suites from the PR description green locally (219 assertions across cli, acp-bridge, webui), typechecks green in all 4 affected packages, and the implementation matches the independent design baseline exactly. The workspace-defaults-vs-session-state separation is the right architectural cut, credential stripping is applied consistently, and the new settings_changed bridge event is the minimum needed for caching clients. No scope creep, no blockers.

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

Downgraded from Approve to Comment: CI still running.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/utils/acpModelUtils.ts Outdated
Comment thread packages/cli/src/serve/workspace-providers-status.ts Outdated
wenshao
wenshao previously approved these changes Jun 23, 2026
@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer verification — real local build + live qwen serve daemon + mutation tests

I built the real binary at this PR's head and exercised the daemon end-to-end (not just unit tests), then ran mutation tests to prove the new tests are non-vacuous. Sharing as a merge reference.

Under test: PR head b4c1138e3 · qwen 0.18.5 · isolated QWEN_HOME · macOS 26.5.1, Node v22.22.2, tmux 3.6a · real qwen serve on 127.0.0.1:56649 (loopback, no bearer).

1. Focused tests from the Reviewer Test Plan — all green

Suite Result
cli workspace-providers-status + workspace-service/facade + acpModelUtils 72 passed
cli server.test daemon-local providers 1 passed
acp-bridge bridge.test setSessionModel 9 passed
webui DaemonSessionProvider session-context / unmatched 4 passed
typecheck acp-bridge / webui / sdk-typescript clean (tsc --noEmit)

2. Live daemon E2E (the part unit tests can't prove)

Started a real qwen serve against an isolated QWEN_HOME, then hit GET /workspace/providers with curl:

# Scenario Result
1 Before any session initialized: true, current.modelId = model-a(openai) straight from settings; current.baseUrl sanitized — the user:SUPERSECRET@ in settings was stripped, zero credential leak in the whole payload
2 Core fix — fresh on every request Edited settings.json model.name model-a → model-b with the daemon still running (no restart); the very next request returned current.modelId = model-b(openai) and moved isCurrent accordingly
3 Duplicate model id, matching model.baseUrl Only the exact-baseUrl entry (api-two) is isCurrent: true; the same-id sibling (api-one) stays false, no warning
3b Duplicate id, mismatched model.baseUrl Falls back to first id-match and emits the disambiguation warning — and the warning itself is sanitized (credentials never appear even in the error text)

Check 2 is the load-bearing evidence: the ACP child is spawned at startup and does not watch settings.json, so a disk edit reflected on the next request can only come from a daemon-side re-read of fresh settings — exactly what this PR claims. (Note: the live daemon reported acpChannelLive: true because serve pre-warms an ACP channel even with zero sessions; the false path is covered by unit tests. Providers data was correct regardless of channel state — which is the whole point.)

3. Mutation tests — the new tests actually bite

Green → mutate production code → red → revert → green:

Mutation Effect
Neutralize the settings_changed broadcast in bridge.ts 3 setSessionModel tests go red (expected 'settings_changed_MUTANT' to be 'settings_changed')
Make sanitizeProviderBaseUrl stop stripping credentials 9 tests go red (7 acpModelUtils cases + 2 workspace-providers-status, incl. not.toContain('secret'))

Both reverted; worktree clean, tests green again.

Scope / caveats

  • WebUI session-context-priority and the settings_changed broadcast are verified via unit tests (+ mutation), not a live browser / SSE session — consistent with the PR's stated "unit-test covered, N/A screenshots" approach.
  • Credential sanitization held on every live path I could reach (current model, provider catalog, disambiguation warning).

Verdict

Behaves exactly as described: /workspace/providers is now a fresh, daemon-local snapshot that works before any session and never leaks credentials, and the model-switch settings_changed signal is in place. Tests are meaningful (mutation-proven). LGTM from a verification standpoint. 👍

中文版(完整对应)

✅ 维护者验证 —— 真实本地构建 + 实跑 qwen serve daemon + 变异测试

我在本 PR 的 head 上构建了真实二进制,并对 daemon 做了端到端验证(不止单测),再用变异测试证明新增用例并非空过。作为合并参考。

被测对象: PR head b4c1138e3 · qwen 0.18.5 · 隔离的 QWEN_HOME · macOS 26.5.1、Node v22.22.2、tmux 3.6a · 真实 qwen serve 监听 127.0.0.1:56649(loopback,无 bearer)。

1. Reviewer Test Plan 里的聚焦测试 —— 全绿

套件 结果
cli workspace-providers-status + workspace-service/facade + acpModelUtils 72 通过
cli server.test daemon-local providers 1 通过
acp-bridge bridge.test setSessionModel 9 通过
webui DaemonSessionProvider session-context / unmatched 4 通过
typecheck acp-bridge / webui / sdk-typescript 干净(tsc --noEmit)

2. 实跑 daemon 端到端(单测无法覆盖的部分)

对隔离的 QWEN_HOME 启动真实 qwen serve,再用 curl 请求 GET /workspace/providers:

# 场景 结果
1 任何 session 之前 initialized: true,current.modelId = model-a(openai) 直接来自 settings;current.baseUrl 已脱敏 —— settings 里的 user:SUPERSECRET@剥离,整个响应零凭据泄漏
2 核心修复 —— 每次请求都读最新 daemon 不重启的情况下把 settings.jsonmodel.namemodel-a 改成 model-b;下一次请求立即返回 current.modelId = model-b(openai),isCurrent 相应迁移
3 同名 model id、model.baseUrl 精确匹配 只有 baseUrl 精确匹配的条目(api-two)是 isCurrent: true,同 id 的 api-one 保持 false,无 warning
3b 同 id、model.baseUrl 不匹配 回退到第一个 id 匹配,发出 disambiguation warning —— 且警告本身也脱敏(凭据连错误文本里都不出现)

Check 2 是关键证据:ACP 子进程在启动时 spawn 且监听 settings.json,因此磁盘改动能在下一次请求体现,只能来自 daemon 侧对最新 settings 的重新读取 —— 正是本 PR 的主张。(注:实跑 daemon 报告 acpChannelLive: true,因为 serve 即便零 session 也会预热一个 ACP channel;false 分支由单测覆盖。无论 channel 状态如何,providers 数据都正确 —— 这正是本意。)

3. 变异测试 —— 新测试真的拦得住

绿 → 改坏生产代码 → 红 → 回退 → 绿:

变异 效果
破坏 bridge.ts 里的 settings_changed 广播 3setSessionModel 测试转红(expected 'settings_changed_MUTANT' to be 'settings_changed')
sanitizeProviderBaseUrl 不再剥离凭据 9 个测试转红(7 个 acpModelUtils 用例 + 2 个 workspace-providers-status,含 not.toContain('secret'))

两处变异均已回退;worktree 干净,测试重新全绿。

范围 / 说明

  • WebUI 的 session-context 优先逻辑与 settings_changed 广播由**单测(+ 变异)**验证,未做真实浏览器 / SSE 会话验证 —— 与 PR 自述的"单测覆盖、无需截图"一致。
  • 凭据脱敏在我能触达的每条实跑路径(current model、provider catalog、disambiguation warning)上都成立。

结论

行为与描述完全一致:/workspace/providers 现在是新鲜的、daemon 本地的快照,在任何 session 之前即可用、且从不泄漏凭据;模型切换的 settings_changed 信号也已就位。测试有效(经变异验证)。从验证角度 LGTM。 👍

@doudouOUC
doudouOUC dismissed stale reviews from wenshao and qwen-code-ci-bot via f19141c June 23, 2026 01:16
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC force-pushed the fix/workspace-providers-fresh-default branch from f19141c to 4d47fb5 Compare June 23, 2026 01:19
@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

🔁 Re-verification — new head 4d47fb54a (fix(daemon): bound invalid provider URL sanitization)

Follow-up to my earlier full verification (which was against b4c1138e3). The author pushed one new commit since; I rebuilt and re-ran the affected paths on the same live-daemon + mutation setup.

What 4d47fb54a changes: tightens sanitizeProviderBaseUrl's catch branch (URLs that fail new URL(), e.g. an invalid port) so it no longer over-strips, plus simplifies matchesCurrentModel and adds 2 test cases. Everything outside these two files is untouched, so my prior findings (fresh per-request snapshot, settings_changed broadcast, WebUI session-context priority) still stand.

Focused tests at new head — green

acpModelUtils 21 passed (incl. the 2 new invalid-port cases) · workspace-providers-status + facade 53 passed · server daemon-local providers 1 passed.

Live daemon — the new sanitization bound (real qwen serve, curl)

Put each tricky URL as a provider baseUrl and read back the sanitized catalog:

Provider baseUrl in settings Sanitized output Correct?
https://host:99999/path@domain https://host:99999/path@domain (unchanged) @ is in the path + 99999 is a port-shaped segment → not credentials, so not stripped
https://user@host:99999 https://host:99999 ✅ real userinfo stripped, invalid-port host kept
https://user:SECRET3@valid.example/v1 https://valid.example/v1 ✅ normal credential strip, SECRET3 absent from whole payload

This is exactly the regression the commit targets: the old catch logic took lastIndexOf('@'), saw host:99999/path contained a :, and stripped down to https://domain — mangling a valid URL. The new digit-port guard prevents that.

Core behaviors re-confirmed on the rebuilt binary: fresh per-request read (model.name a→b with no restart → current.modelId follows) and duplicate-id isCurrent (only the exact-baseUrl match is current, after the matchesCurrentModel simplification) — both PASS.

Mutation — the new bound is load-bearing

Removing the /^\d+$/ port check in findUnescapedUserInfoFallbackAt makes https://host:99999/path@domain collapse to https://domain, turning the new test case red (expected 'https://domain' to be 'https://host:99999/path@domain'). Reverted; clean & green again.

Verdict

The new commit does what it says and fixes a real over-stripping edge case; tests are non-vacuous (mutation-proven); no credential leaked on any live path. Prior full verification still holds. LGTM on 4d47fb54a. 👍

中文版(完整对应)

🔁 复验 —— 新 head 4d47fb54a(fix(daemon): bound invalid provider URL sanitization)

承接我之前的完整验证(针对 b4c1138e3)。作者此后又推了 1 个提交;我在同一套 live-daemon + 变异环境上重建并重跑了受影响的路径。

4d47fb54a 改了什么: 收紧 sanitizeProviderBaseUrlcatch 分支(new URL() 解析失败的 URL,如非法端口),使其不再过度剥离;并简化 matchesCurrentModel、新增 2 个测试用例。这两个文件之外的内容未动,所以我之前的结论(每请求新鲜快照、settings_changed 广播、WebUI session-context 优先)依然成立。

新 head 上的聚焦测试 —— 全绿

acpModelUtils 21 通过(含 2 个新非法端口用例) · workspace-providers-status + facade 53 通过 · server daemon-local providers 1 通过

Live daemon —— 验证新的脱敏边界(真实 qwen serve + curl)

把每个棘手 URL 作为 provider baseUrl,读回脱敏后的 catalog:

settings 里的 provider baseUrl 脱敏输出 是否正确
https://host:99999/path@domain https://host:99999/path@domain(不变) @ 在 path 里、99999 形似端口 → 不是凭据,因此不剥离
https://user@host:99999 https://host:99999 ✅ 真正的 userinfo 被剥离,非法端口 host 保留
https://user:SECRET3@valid.example/v1 https://valid.example/v1 ✅ 常规凭据剥离,SECRET3 不出现在整个响应里

这正是该 commit 针对的回归: catch 逻辑取 lastIndexOf('@'),看到 host:99999/path: 就一路剥离成 https://domain —— 把合法 URL 弄坏了。新的数字端口判定阻止了这一点。

在重建后的二进制上再次确认核心行为:每请求读最新(model.name a→b、不重启 → current.modelId 跟随)与同 id 的 isCurrent(matchesCurrentModel 简化后,只有 baseUrl 精确匹配的条目为 current)—— 均 PASS

变异 —— 新边界是 load-bearing 的

去掉 findUnescapedUserInfoFallbackAt 里的 /^\d+$/ 端口判定后,https://host:99999/path@domain 塌缩成 https://domain,新测试用例转红(expected 'https://domain' to be 'https://host:99999/path@domain')。已回退;重新干净且全绿。

结论

新提交名副其实,修掉了一个真实的过度剥离边界;测试非空过(经变异验证);任何 live 路径都未泄漏凭据。之前的完整验证依然成立。4d47fb54a LGTM。 👍

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

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

: undefined;
}

function stripAcpAuthSuffix(modelId: string): string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] stripAcpAuthSuffix is functionally identical to parseAcpBaseModelId in packages/cli/src/utils/acpModelUtils.ts — both locate the last () pair at the end of the string and strip it. The only difference is that parseAcpBaseModelId also trims whitespace.

Having two implementations across packages means future fixes to the stripping algorithm must be applied in two places. If one drifts, baseModelId values will diverge between daemon-local and webui contexts.

Consider exporting parseAcpBaseModelId from a shared package (e.g., @qwen-code/qwen-code-core) and reusing it here, or at minimum add a cross-reference comment in both files pointing to the other implementation.

— qwen3.7-max via Qwen Code /review

for (const rawModel of availableModels) {
const model = getRecord(rawModel);
const modelId =
getString(model, 'modelId') ??

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] mapSessionContextModels has three fallback keys for model IDs (modelIdidvalue), two for current model (currentModelIdcurrentModel), and four for context window (_meta.contextLimit_meta.contextWindowmodel.contextLimitmodel.contextWindow). The integration tests in DaemonSessionProvider.test.tsx only exercise the primary key in each chain. Additionally, the early-return edge cases (undefined input at line 83, empty models at line 126) have no tests.

These fallbacks exist for backward compatibility with older daemon session context formats. If any regress, older daemon versions would silently lose model information with no test to catch it.

Consider adding a direct unit test for mapSessionContextModels that exercises the alternate keys and edge-case returns.

— qwen3.7-max via Qwen Code /review

@wenshao
wenshao merged commit 896e6c5 into QwenLM:main Jun 23, 2026
40 checks passed
wenshao added a commit to wenshao/qwen-code that referenced this pull request Jun 23, 2026
…M#5089)

Reverts the structural changes from QwenLM#5089 back to the pre-QwenLM#5089 shape:
AuthType stays a fixed enum (not `string`), the Protocol enum is removed,
modelProviders is `Record<authType, ModelConfig[]>` again (not
`{ protocol, models }`), and createContentGenerator dispatches on authType.
The v4->v5 settings migration is removed and SETTINGS_VERSION reverts to 4.

Features merged on top of QwenLM#5089 are kept and re-adapted to the old
enum+array structure (not reverted):
- QwenLM#5632 fastOnly/voiceOnly model flags (test fixtures reshaped to arrays)
- QwenLM#5638 workspace provider defaults (readProviderModels already tolerates
  both shapes; test fixtures reshaped to arrays)
- QwenLM#5729 active-runtime-model listing (pre-QwenLM#5089 getAllConfiguredModels
  already enumerates Object.values(AuthType), so the runtime model is
  included natively)
- QwenLM#5728 ACP set_config_option deterministic provider fixture (reshaped to
  array; the flake fix is preserved)

KNOWN DOWNGRADE CAVEAT: settings already migrated to $version:5 (shipped in
v0.19.0) retain the v5 `{ protocol, models }` modelProviders shape, which
the reverted ModelRegistry consumes as an array. Such settings will throw
on load until re-configured. A v5->v4 downgrade guard/migration is a
separate follow-up if backward compatibility for migrated users is needed.
wenshao added a commit that referenced this pull request Jun 23, 2026
#5745)

* revert(core): revert Protocol enum & model-identity decoupling (#5089)

Reverts the structural changes from #5089 back to the pre-#5089 shape:
AuthType stays a fixed enum (not `string`), the Protocol enum is removed,
modelProviders is `Record<authType, ModelConfig[]>` again (not
`{ protocol, models }`), and createContentGenerator dispatches on authType.
The v4->v5 settings migration is removed and SETTINGS_VERSION reverts to 4.

Features merged on top of #5089 are kept and re-adapted to the old
enum+array structure (not reverted):
- #5632 fastOnly/voiceOnly model flags (test fixtures reshaped to arrays)
- #5638 workspace provider defaults (readProviderModels already tolerates
  both shapes; test fixtures reshaped to arrays)
- #5729 active-runtime-model listing (pre-#5089 getAllConfiguredModels
  already enumerates Object.values(AuthType), so the runtime model is
  included natively)
- #5728 ACP set_config_option deterministic provider fixture (reshaped to
  array; the flake fix is preserved)

KNOWN DOWNGRADE CAVEAT: settings already migrated to $version:5 (shipped in
v0.19.0) retain the v5 `{ protocol, models }` modelProviders shape, which
the reverted ModelRegistry consumes as an array. Such settings will throw
on load until re-configured. A v5->v4 downgrade guard/migration is a
separate follow-up if backward compatibility for migrated users is needed.

* feat(cli): add v5->v4 settings downgrade migration for #5089 revert

After reverting #5089, settings already migrated to $version:5 (shipped in
v0.19.0) carry a modelProviders `{ protocol, models }` shape that the
reverted v4 readers consume as arrays, throwing "models is not iterable"
on load. This adds the inverse migration so those configs auto-converge to
v4 on load (the user-facing "automatically migrate $version:5 to 4").

- V5ToV4Migration: unwraps each modelProviders `{ protocol, models }` back
  to its `models` array, drops the now-implicit protocol (warning only when
  the explicit protocol differs from the key-derived one), and resets
  $version to 4.
- DOWNGRADE_MIGRATIONS keeps the downgrade out of the ascending forward
  ALL_MIGRATIONS chain (preserving its invariants); runMigrations and
  needsMigration consider both via a combined convergence set.
- needsMigration now gates on `=== SETTINGS_VERSION` instead of `>=`, so a
  newer-but-handled version (v5) is reported as needing migration while a
  genuinely unknown newer version (v6+) is still left untouched.

Covered by unit tests for the migration, the framework wiring, and an
end-to-end loadSettings downgrade-on-load test.

* fix(test): align integration settings-version constant with reverted v4

The integration suites hard-coded CURRENT_SETTINGS_VERSION = 5 (introduced
by #5676), which mismatched the reverted SETTINGS_VERSION = 4 and failed the
migration assertions ($version now writes 4, not 5). Revert the constant to
4 in both settings-migration and qwen-config-dir integration tests.

Verified: QWEN_SANDBOX=false vitest run --root ./integration-tests
cli/settings-migration.test.ts cli/qwen-config-dir.test.ts → 21 passed.

* fix: harden v5-era settings handling on the #5089 revert path

Addresses /qreview feedback on the revert:

- vscode findOpenaiModels: restore read-side tolerance for the V5
  { protocol, models } shape. The extension reads/writes settings.json
  without running the CLI v5->v4 migration, so a not-yet-downgraded
  $version:5 file would otherwise return [] and silently drop existing
  OpenAI models on the next write. (Critical)
- modelRegistry.registerAuthTypeModels: guard against a non-array provider
  value (skip + warn) instead of throwing an opaque "models is not
  iterable" — covers hand-edited or unmigrated files the downgrade misses.
- needsMigration JSDoc: update the stale ">= SETTINGS_VERSION" wording to
  match the "=== SETTINGS_VERSION, else fall through" logic the downgrade
  path depends on.
- settings.test.ts: also assert the v5->v4 downgrade is persisted to disk
  (.tmp write-back), not just the in-memory merged result.

Adds tests for the registry guard and the vscode V5 read tolerance.

* fix(core): break contentGenerator import cycle + cover reverted error paths

Addresses /review suggestions on the revert:

- contentGenerator: import PROVIDER_SOURCED_FIELDS from constants.js (where
  it is actually defined) instead of modelsConfig.js, breaking the runtime
  import cycle contentGenerator -> modelsConfig -> contentGenerator.
  constants.js only references contentGenerator at the type level, which is
  erased at runtime, so no cycle remains.
- contentGenerator.test: add coverage for the two authType error paths the
  revert restored (missing authType -> "must have an authType"; unknown
  authType -> "Unsupported authType"), which #5089's protocol-based tests
  had replaced. Neither was covered before.

The acpAgent z.nativeEnum(AuthType).parse(methodId) suggestion is left as-is:
that line is byte-identical to pre-#5089, so it is pre-existing behavior the
revert faithfully restores rather than a regression of this PR.
@doudouOUC
doudouOUC deleted the fix/workspace-providers-fresh-default branch June 23, 2026 12:57
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.

5 participants