fix: preserve runtime configuration references
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s

Projects, channels, conversations, and Agent Runtimes now retain dynamic default references while explicit model selections remain pinned. Compatible fallback resolution stays aligned across settings validation, runtime creation, labels, context metrics, DeepSeek Harness browser routing, and remote project saves.

Prepare the bilingual Desktop 0.12.11 release metadata and keep Agent 0.11.22 as the published remote baseline.

Release note: 修复默认模型与 Runtime 配置变化后项目、通道和对话选择失去跟随关系的问题;显式选择仍保持固定,新请求使用当前兼容配置。
This commit is contained in:
mesalogo
2026-09-10 06:27:48 +08:00
parent b01a856bae
commit 21ac8330ea
30 changed files with 378 additions and 139 deletions
@@ -677,7 +677,7 @@ describe('DeepSeek Harness real ACP control-plane E2E', () => {
assignments: ['deepseek-harness'],
secretConfigured: false,
transport: 'stdio',
command: process.execPath,
command: process.versions.electron ? 'node' : process.execPath,
args: [
resolve(
'tests',
@@ -832,6 +832,7 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
conversationId:
run?.request.conversationId ??
'deepseek-harness-tool-catalog',
browserTabId: run?.request.browserTabId,
workMode:
run?.request.workMode === 'ask'
? ('ask' as const)
@@ -873,6 +874,7 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
}
const context = {
conversationId: run.request.conversationId,
browserTabId: run.request.browserTabId,
workMode:
run.request.workMode === 'execute'
? ('execute' as const)
+2 -2
View File
@@ -75,7 +75,7 @@ describe('runtime discovery', () => {
await expect(validation).resolves.toEqual({ valid: false })
})
it('canonicalizes and validates a configured ordinary file first', async () => {
it('canonicalizes a configured ordinary file without launching it', async () => {
process.env.PATH = ''
process.env.Path = ''
@@ -90,7 +90,7 @@ describe('runtime discovery', () => {
path: await realpath(process.execPath),
source: 'configured'
})
expect(detection.version).toMatch(/^\d+\.\d+\.\d+/u)
expect(detection.version).toBeUndefined()
})
it('trusts a configured execution path without reading or launching it', async () => {
+17 -2
View File
@@ -5,6 +5,7 @@ import { join, resolve } from 'node:path'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import { z } from 'zod'
import {
browserTabIdSchema,
defaultContextCompressionSettings,
modelProtocolSchema
} from '../../shared/contracts'
@@ -1612,6 +1613,7 @@ describe.runIf(enabled)('runtime end-to-end', () => {
url,
origin: new URL(url).origin
}))
const browserTabId = browserTabIdSchema.parse(crypto.randomUUID())
const browserService = {
getOrigin: vi.fn(() => undefined),
navigate,
@@ -1621,7 +1623,19 @@ describe.runIf(enabled)('runtime end-to-end', () => {
select: vi.fn(),
back: vi.fn(),
screenshot: vi.fn(),
releaseConversation: vi.fn(async () => undefined)
releaseConversation: vi.fn(async () => undefined),
listTabs: vi.fn(() => [
{
conversationId,
tabId: browserTabId,
primary: true,
status: 'ready' as const,
isLoading: false,
canGoBack: false,
createdAt: Date.now(),
updatedAt: Date.now()
}
])
}
const gateway = new KnowledgeMcpGateway({} as never, {
browserService: browserService as never
@@ -1635,7 +1649,8 @@ describe.runIf(enabled)('runtime end-to-end', () => {
controller.signal,
'none',
undefined,
conversationId
conversationId,
browserTabId
)!
const common = {
modelProfile: {
+24
View File
@@ -100,6 +100,30 @@ function settings(
}
describe('runtime selection', () => {
it('resolves direct defaults per request while fixed selections and started requests stay unchanged', () => {
const before = settings()
const started = applyRuntimeSelection(before, { provider: 'model' })
const after = settings({ defaultModelProfileId: secondProfileId })
expect(applyRuntimeSelection(after, { provider: 'model' }).settings.defaultModelProfileId).toBe(secondProfileId)
expect(applyRuntimeSelection(after, { provider: 'model', profileId: defaultProfileId }).settings.defaultModelProfileId).toBe(defaultProfileId)
expect(started.settings.defaultModelProfileId).toBe(defaultProfileId)
})
it.each(['opencode', 'continue', 'deepseek-harness'] as const)(
'resolves %s references from its current configuration and preserves explicit overrides', (provider) => {
const base = settings()
const first = base.modelProfiles[4]!
const second = { ...first, id: responsesProfileId }
const key = provider === 'opencode' ? 'opencodeModelProfile' : provider === 'continue' ? 'continueModelProfile' : 'deepseekHarnessModelProfile'
const configured = settings({ [key]: first })
const started = applyRuntimeSelection(configured, { provider })
const changed = settings({ [key]: second })
expect(applyRuntimeSelection(changed, { provider }).settings[key]?.id).toBe(second.id)
expect(applyRuntimeSelection(changed, { provider, profileId: first.id }).settings[key]?.id).toBe(first.id)
expect(started.settings[key]?.id).toBe(first.id)
}
)
it('selects an independent direct model profile without changing defaults', () => {
const original = settings()
const selected = applyRuntimeSelection(original, {
@@ -5240,22 +5240,19 @@ describe('AssistantDatabase', () => {
expect(
database.getProject(imageChannelProject.id).runtimeSelection
).toEqual({
provider: 'model',
profileId: defaultProfileId
provider: 'model'
})
expect(
database.getProject(automaticChannelProject.id).runtimeSelection
).toEqual({
provider: 'model',
profileId: defaultProfileId
provider: 'model'
})
expect(
database.getConversation(
automaticRemoteConversation.id
).runtimeSelection
).toEqual({
provider: 'model',
profileId: defaultProfileId
provider: 'model'
})
database.close()
})
@@ -327,7 +327,7 @@ describe('RemoteProjectSaveService', () => {
])
})
it('persists the resolved Runtime selection', async () => {
it('validates a resolved Runtime but persists the requested reference', async () => {
const selection = {
provider: 'opencode' as const,
profileId: '00000000-0000-4000-8000-000000000099'
@@ -341,7 +341,7 @@ describe('RemoteProjectSaveService', () => {
draft
})
expect(value.writes[0]?.project.runtimeSelection).toEqual(selection)
expect(value.writes[0]?.project.runtimeSelection).toEqual(draft.runtimeSelection)
})
it('uses the preflight project revision for updates', async () => {
@@ -320,7 +320,7 @@ export class RemoteProjectSaveService {
signal.throwIfAborted()
runtimeLease.assertCurrent()
return {
runtimeSelection,
runtimeSelection: draft.runtimeSelection,
target,
installation,
connection,
+85 -4
View File
@@ -80,6 +80,89 @@ afterEach(async () => {
})
describe('RuntimeSettingsStore', () => {
it('persists default references separately from fixed profiles and resolves each settings generation', async () => {
const { store, filePath } = await createStore()
const firstId = '00000000-0000-4000-8000-000000000041'
const secondId = '00000000-0000-4000-8000-000000000042'
const modelProfiles = [firstId, secondId].map((id) => ({
id, name: id, baseUrl: 'https://gateway.example/v1', modelName: id,
protocol: 'openai-chat-completions' as const,
authentication: 'api-key' as const, imageGenerationQuality: 'auto' as const,
apiKey: { action: 'keep' as const }
}))
await store.update(settings({ modelProfiles, defaultModelProfileId: firstId,
opencodeModelSource: { kind: 'default' },
continueModelSource: { kind: 'profile', profileId: firstId },
deepseekHarnessModelSource: { kind: 'default' }
}))
const started = await store.getResolvedSettings()
await store.update(settings({ defaultModelProfileId: secondId, deepseekHarnessModelSource: { kind: 'default' } }))
const reloaded = new RuntimeSettingsStore(filePath, cipher, {})
await expect(reloaded.getPublicSettings()).resolves.toMatchObject({
opencodeModelSource: { kind: 'default' },
continueModelSource: { kind: 'profile', profileId: firstId },
deepseekHarnessModelSource: { kind: 'default' }
})
await expect(reloaded.getResolvedSettings()).resolves.toMatchObject({
opencodeModelProfile: { id: secondId },
continueModelProfile: { id: firstId },
deepseekHarnessModelProfile: { id: secondId }
})
expect(started.opencodeModelProfile?.id).toBe(firstId)
})
it('falls back to compatible profiles when the global default is incompatible with a Runtime', async () => {
const { store } = await createStore()
const textId = '00000000-0000-4000-8000-000000000043'
const imageId = '00000000-0000-4000-8000-000000000044'
const anthropicId = '00000000-0000-4000-8000-000000000045'
const modelProfiles = [
{
id: textId, name: 'OpenAI text', baseUrl: 'https://gateway.example/v1', modelName: 'text',
protocol: 'openai-chat-completions' as const, authentication: 'api-key' as const,
imageGenerationQuality: 'auto' as const, apiKey: { action: 'keep' as const }
},
{
id: imageId, name: 'Image', baseUrl: 'https://gateway.example/v1', modelName: 'image',
protocol: 'openai-images-generations' as const, authentication: 'api-key' as const,
imageGenerationQuality: 'auto' as const, apiKey: { action: 'keep' as const }
},
{
id: anthropicId, name: 'Anthropic text', baseUrl: 'https://gateway.example/v1', modelName: 'text',
protocol: 'anthropic-messages' as const, authentication: 'api-key' as const,
imageGenerationQuality: 'auto' as const, apiKey: { action: 'keep' as const }
}
]
const runtimeDefaults = settings({
modelProfiles,
defaultModelProfileId: textId,
opencodeModelSource: { kind: 'default' },
continueModelSource: { kind: 'default' },
deepseekHarnessModelSource: { kind: 'default' }
})
await store.update(runtimeDefaults)
await store.update({
...runtimeDefaults,
defaultModelProfileId: imageId
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
opencodeModelProfile: { id: textId },
continueModelProfile: { id: textId },
deepseekHarnessModelProfile: { id: textId }
})
await store.update({
...runtimeDefaults,
defaultModelProfileId: anthropicId
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
opencodeModelProfile: { id: anthropicId },
continueModelProfile: { id: anthropicId },
deepseekHarnessModelProfile: { id: textId }
})
})
it('seeds a local provider-neutral model connection', async () => {
const { store } = await createStore()
@@ -491,12 +574,10 @@ describe('RuntimeSettingsStore', () => {
await expect(store.getPublicSettings()).resolves.toMatchObject({
opencodeEmbedded: true,
opencodeModelSource: {
kind: 'profile',
profileId: '00000000-0000-4000-8000-000000000001'
kind: 'default'
},
continueModelSource: {
kind: 'profile',
profileId: '00000000-0000-4000-8000-000000000001'
kind: 'default'
}
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
+43 -14
View File
@@ -1431,7 +1431,13 @@ export class RuntimeSettingsStore {
): ResolvedModelProfile | undefined {
const source = settings.deepseekHarnessModelSource
if (source.kind === 'default') {
return modelProfiles.find((profile) => profile.id === settings.defaultModelProfileId)
return (
modelProfiles.find(
(profile) =>
profile.id === settings.defaultModelProfileId &&
isDeepSeekHarnessModelProfile(profile)
) ?? modelProfiles.find(isDeepSeekHarnessModelProfile)
)
}
if (source.kind === 'profile') {
return modelProfiles.find(
@@ -1939,13 +1945,19 @@ export class RuntimeSettingsStore {
const opencodeModelProfile =
!agent.opencodeBaseUrl &&
settings.opencodeModelSource.kind !== 'platform'
? profilesById.get(settings.opencodeModelSource.kind === 'default'
? settings.defaultModelProfileId : settings.opencodeModelSource.profileId)
? profilesById.get(
settings.opencodeModelSource.kind === 'default'
? compatibleTextProfileId(settings) ?? ''
: settings.opencodeModelSource.profileId
)
: undefined
const continueModelProfile =
settings.continueModelSource.kind !== 'platform'
? profilesById.get(settings.continueModelSource.kind === 'default'
? settings.defaultModelProfileId : settings.continueModelSource.profileId)
? profilesById.get(
settings.continueModelSource.kind === 'default'
? compatibleTextProfileId(settings) ?? ''
: settings.continueModelSource.profileId
)
: undefined
const deepseekHarnessModelProfile =
this.resolveDeepSeekHarnessModelProfile(settings, modelProfiles)
@@ -2366,12 +2378,22 @@ export class RuntimeSettingsStore {
source: RuntimeSettings['opencodeModelSource'],
runtimeLabel: 'OpenCode' | 'Continue'
): void => {
if (source.kind !== 'profile') {
if (source.kind === 'platform') {
return
}
const profile = modelProfiles.find(
(candidate) => candidate.id === source.profileId
)
const profile =
source.kind === 'default'
? (modelProfiles.find(
(candidate) =>
candidate.id === defaultModelProfileId &&
isAgentRuntimeModelProtocol(candidate.protocol)
) ??
modelProfiles.find((candidate) =>
isAgentRuntimeModelProtocol(candidate.protocol)
))
: modelProfiles.find(
(candidate) => candidate.id === source.profileId
)
if (!profile) {
throw new Error(`${runtimeLabel} 引用的模型连接不存在`)
}
@@ -2417,11 +2439,18 @@ export class RuntimeSettingsStore {
const requestedDeepSeekHarnessSource =
input.deepseekHarnessModelSource ??
current.deepseekHarnessModelSource
if (requestedDeepSeekHarnessSource.kind === 'profile') {
const profile = modelProfiles.find(
(candidate) =>
candidate.id === requestedDeepSeekHarnessSource.profileId
)
if (requestedDeepSeekHarnessSource.kind !== 'platform') {
const profile =
requestedDeepSeekHarnessSource.kind === 'default'
? (modelProfiles.find(
(candidate) =>
candidate.id === defaultModelProfileId &&
isDeepSeekHarnessModelProfile(candidate)
) ?? modelProfiles.find(isDeepSeekHarnessModelProfile))
: modelProfiles.find(
(candidate) =>
candidate.id === requestedDeepSeekHarnessSource.profileId
)
if (!profile) {
throw new Error('DeepSeek Harness 引用的模型连接不存在')
}