fix: guide coding model selection before sending

This commit is contained in:
鲨鱼辣椒 committed 2026-10-14 14:59:54 +08:00
1 parent f34e651774
commit bec42627f5
12 files changed
+1000 -101

No files matched your search

@@ -114,6 +114,11 @@ function runtimeError(error: unknown): never {
throw new CodingConversationServiceError(503, 'CODING_RUNTIME_UNAVAILABLE', 'The local coding runtime is unavailable');
}
function isModelSelectionError(error: unknown): boolean {
return error instanceof CodingConversationServiceError
&& (error.code === 'CODING_MIGRATION_MODEL_REQUIRED' || error.code === 'CODING_MODEL_UNAVAILABLE');
}
async function persist<T>(operation: () => Promise<T>): Promise<T> {
try {
return await operation();
@@ -415,13 +420,32 @@ export class CodingConversationService {
}
async getSnapshot(conversationId: string): Promise<ConversationSnapshot> {
this.assertNotDeleting(conversationId);
const { project, conversation } = await this.projects.findActiveConversation(conversationId);
const { config } = await this.projects.getConfig(project.id);
const agent = config.agents.find((item) => item.id === conversation.agentId);
if (this.options.readHistory && (!agent?.enabled || agent.archivedAt)) {
return await this.options.readHistory(project.id, conversation);
}
await this.ensurePrepared(conversationId);
if (this.options.readHistory && (!conversation.model || conversation.modelResolution !== 'resolved')) {
if (this.hasWorker(conversationId)) {
try { return await this.runtime.getSnapshot(conversationId); } catch (error) { runtimeError(error); }
}
if (!this.prepareFlights.has(conversationId)) {
return await this.readModelSelectionHistory(project.id, conversation);
}
}
try {
await this.ensurePrepared(conversationId);
} catch (error) {
if (this.options.readHistory && isModelSelectionError(error)) {
if (this.hasWorker(conversationId)) {
try { return await this.runtime.getSnapshot(conversationId); } catch (runtimeFailure) { runtimeError(runtimeFailure); }
}
return await this.readModelSelectionHistory(project.id, conversation);
}
throw error;
}
try {
return await this.runtime.getSnapshot(conversationId);
} catch (error) {
@@ -429,6 +453,47 @@ export class CodingConversationService {
}
}
private hasWorker(conversationId: string): boolean {
return this.runtime.getDiagnostics().workers.some((worker) => worker.conversationId === conversationId);
}
private async readModelSelectionHistory(
projectId: string,
conversation: CodingConversationV2,
): Promise<ConversationSnapshot> {
const snapshot = await this.options.readHistory!(projectId, conversation);
// A model selection or prompt may have started while durable history was read.
// Its real runtime state, including uncertain work, must remain authoritative.
const preparing = this.prepareFlights.get(conversation.id);
if (preparing) {
try { await preparing; } catch (error) {
if (!isModelSelectionError(error)) throw error;
}
}
this.assertNotDeleting(conversation.id);
if (this.hasWorker(conversation.id)) {
try { return await this.runtime.getSnapshot(conversation.id); } catch (error) { runtimeError(error); }
}
const { conversation: current } = await this.projects.findActiveConversation(conversation.id);
this.assertNotDeleting(conversation.id);
if (current.modelResolution !== conversation.modelResolution
|| JSON.stringify(current.model) !== JSON.stringify(conversation.model)) {
// A concurrent choice may have persisted and failed preparation before this
// read completed. Recheck that model instead of masking its real failure.
return await this.getSnapshot(conversation.id);
}
if (this.hasWorker(conversation.id)) {
try { return await this.runtime.getSnapshot(conversation.id); } catch (error) { runtimeError(error); }
}
return {
...snapshot,
conversation: {
...snapshot.conversation,
model: { model: null, modelResolution: 'required' },
},
};
}
/** Read existing facts for work restoration without opening a model session. */
async getStoredSnapshot(conversationId: string): Promise<ConversationSnapshot | null> {
const { project, conversation } = await this.projects.findActiveConversation(conversationId);