fix(coding-runtime): close Pi extension lifecycle races

This commit is contained in:
2026-08-23 10:13:10 +08:00
parent 3861c3286a
commit 31de325cd8
9 changed files with 197 additions and 14 deletions

View File

@@ -13,6 +13,7 @@ interface StoredInteraction {
interaction: ConversationInteraction;
generation: number;
labels: Map<string, string>;
phase: 'pending' | 'responding';
untrack(): void;
}
@@ -85,6 +86,7 @@ export class PiInteractionStore {
interaction,
generation,
labels,
phase: 'pending',
untrack: () => undefined,
};
stored.untrack = this.transport.trackGenerationResource({
@@ -102,6 +104,7 @@ export class PiInteractionStore {
async respond(conversationId: string, response: PiInteractionResponse): Promise<ConversationInteraction> {
const stored = this.pending.get(this.key(conversationId, response.interactionId));
if (!stored) throw new Error('Pi interaction is not pending');
if (stored.phase === 'responding') throw new Error('Pi interaction response is already in progress');
const state = this.transport.getState(conversationId);
const active = this.transport.getActiveRun(conversationId);
if (state?.generation !== stored.generation
@@ -126,7 +129,18 @@ export class PiInteractionStore {
} else {
throw new Error('Pi interaction response does not match its kind');
}
await this.transport.send(conversationId, command);
stored.phase = 'responding';
try {
await this.transport.send(conversationId, command);
} catch (error) {
if (this.pending.get(this.key(conversationId, stored.interaction.id)) === stored) {
stored.phase = 'pending';
}
throw error;
}
if (this.pending.get(this.key(conversationId, stored.interaction.id)) !== stored) {
throw new Error('Pi interaction belongs to a stale worker run');
}
return this.finish(stored, 'cancelled' in response
? 'cancelled'
: stored.interaction.kind === 'confirm' && 'confirmed' in response && !response.confirmed