fix: allow replacing unavailable conversation models

Persist the selected model before opening dormant sessions, and reconfigure crashed workers on the same account. Preserve session history and report model removal through the model-unavailable contract instead of a generic runtime failure.
This commit is contained in:
2026-09-21 12:23:04 +08:00
parent d222d17500
commit 0f7093d173
8 changed files with 236 additions and 6 deletions

View File

@@ -34,6 +34,8 @@ import type {
PromptConversationInput,
} from '../../electron/coding-runtime/contracts';
import { archivePiConversationSession } from '../../electron/coding-runtime/pi/resource-loader';
import { PiProviderConfigError } from '../../electron/coding-runtime/pi/provider-config';
import type { ProductModelRef } from '../../shared/coding-conversation-contracts';
const roots: string[] = [];
const servers: Server[] = [];
@@ -273,6 +275,44 @@ describe('PI-100 coding core Host contract', () => {
});
});
it('switches a removed saved model before preparing the Conversation', async () => {
class RemovedModelRuntime extends InMemoryConversationRuntime {
override async validateModel(model: ProductModelRef) {
if (model.modelId === MODEL.modelId) {
throw new PiProviderConfigError('MODEL_UNAVAILABLE', 'Model is no longer available');
}
return await super.validateModel(model);
}
override async prepare(input: PrepareConversationInput) {
if (input.model.model) await this.validateModel(input.model.model);
return await super.prepare(input);
}
}
const result = await setup(new RemovedModelRuntime());
const conversation = await createConversation(result.conversations);
const nextModel = { ...MODEL, modelId: 'available-model' };
const prepare = vi.spyOn(result.runtime, 'prepare');
await expect(result.conversations.getSnapshot(conversation.id)).rejects.toMatchObject({
code: 'CODING_MODEL_UNAVAILABLE', status: 409,
});
await expect(result.conversations.setModel(conversation.id, MODEL)).rejects.toMatchObject({
code: 'CODING_MODEL_UNAVAILABLE', status: 409,
});
expect((await result.conversations.getConversation(conversation.id)).model).toEqual(MODEL);
await expect(result.conversations.setModel(conversation.id, nextModel)).resolves.toMatchObject({
model: nextModel,
modelResolution: 'resolved',
});
expect(prepare).toHaveBeenLastCalledWith(expect.objectContaining({
model: { model: nextModel, modelResolution: 'resolved' },
}));
expect(await result.projects.conversationStore(result.root).get(conversation.id)).toMatchObject({
id: conversation.id, model: nextModel, modelResolution: 'resolved',
});
});
it('switches a resolved active Conversation model through the target runtime without disposing it', async () => {
const result = await setup();
const conversation = await createConversation(result.conversations);