fix(pi): retain ownership for uncertain mutations
This commit is contained in:
@@ -426,6 +426,16 @@ function runIsTerminal(status: ConversationSnapshot['run']['status']): boolean {
|
||||
return status === 'idle' || status === 'error';
|
||||
}
|
||||
|
||||
const REQUEST_UNCERTAIN_MESSAGE = '请求确认延迟,可能仍在执行。请等待结果,或中止/恢复后再重试。';
|
||||
|
||||
function requestUncertainError(): CodingRuntimeContractError {
|
||||
return new CodingRuntimeContractError(
|
||||
'CODING_REQUEST_UNCERTAIN',
|
||||
REQUEST_UNCERTAIN_MESSAGE,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
function publicWorkerState(state: PiWorkerPoolState): ConversationSnapshot['worker'] {
|
||||
if (state.state === 'spawning') return { status: 'starting', generation: state.generation };
|
||||
if (state.state === 'crashed') {
|
||||
@@ -460,7 +470,7 @@ function runtimeFailure(error: unknown): CodingRuntimePublicError {
|
||||
if (error.code === 'PI_RPC_TIMEOUT') {
|
||||
return {
|
||||
code: 'CODING_REQUEST_UNCERTAIN',
|
||||
message: 'The local Agent did not confirm the request',
|
||||
message: REQUEST_UNCERTAIN_MESSAGE,
|
||||
recoverable: true,
|
||||
};
|
||||
}
|
||||
@@ -626,6 +636,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
|
||||
async prompt(input: PromptConversationInput): Promise<PromptAcceptance> {
|
||||
await this.waitForProjection(input.conversationId);
|
||||
this.assertNoUncertainMutation(input.conversationId);
|
||||
if (input.mode === 'steer') {
|
||||
const acceptance = await this.steer(input);
|
||||
return {
|
||||
@@ -765,6 +776,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
|
||||
async setModel(input: SetConversationModelInput): Promise<ConversationModelState> {
|
||||
await this.waitForProjection(input.conversationId);
|
||||
this.assertNoUncertainMutation(input.conversationId);
|
||||
const snapshot = this.snapshot(input.conversationId);
|
||||
const thinkingLevel = snapshot.conversation.model.model?.thinkingLevel ?? 'off';
|
||||
const selection = await this.resolveModel({
|
||||
@@ -825,6 +837,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
|
||||
async setThinking(input: SetThinkingLevelInput): Promise<ConversationModelState> {
|
||||
await this.waitForProjection(input.conversationId);
|
||||
this.assertNoUncertainMutation(input.conversationId);
|
||||
const current = this.snapshot(input.conversationId).conversation.model;
|
||||
if (!current.model) {
|
||||
throw new CodingRuntimeContractError(
|
||||
@@ -876,6 +889,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
|
||||
async compact(conversationId: string): Promise<void> {
|
||||
await this.waitForProjection(conversationId);
|
||||
this.assertNoUncertainMutation(conversationId);
|
||||
const runId = this.id('run');
|
||||
this.acquireRunBackgroundLease(conversationId, runId);
|
||||
const generation = this.pool.getState(conversationId)?.generation;
|
||||
@@ -905,6 +919,11 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
try {
|
||||
await ticket.accepted;
|
||||
} catch (error) {
|
||||
if (error instanceof PiProcessError && error.code === 'PI_RPC_TIMEOUT') {
|
||||
const failure = requestUncertainError();
|
||||
this.markRunUncertain(conversationId, runId, failure.publicError);
|
||||
throw failure;
|
||||
}
|
||||
await this.failRun(conversationId, runId, error);
|
||||
throw error;
|
||||
}
|
||||
@@ -912,6 +931,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
|
||||
async fork(input: ForkConversationInput): Promise<ForkResult> {
|
||||
await this.waitForProjection(input.sourceConversationId);
|
||||
this.assertNoUncertainMutation(input.sourceConversationId);
|
||||
this.snapshot(input.sourceConversationId);
|
||||
const registered = await this.registry.prepare(input.conversation);
|
||||
const canonicalInput: PrepareConversationInput = {
|
||||
@@ -1091,6 +1111,14 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
return Boolean(snapshot && !runIsTerminal(snapshot.run.status));
|
||||
}
|
||||
|
||||
private assertNoUncertainMutation(conversationId: string): void {
|
||||
const run = this.states.get(conversationId)?.snapshot?.run;
|
||||
if (!runIsTerminal(run?.status ?? 'idle')
|
||||
&& run?.error?.code === 'CODING_REQUEST_UNCERTAIN') {
|
||||
throw requestUncertainError();
|
||||
}
|
||||
}
|
||||
|
||||
markProviderStale(): void {
|
||||
this.pool.markProviderStale();
|
||||
}
|
||||
@@ -1137,6 +1165,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
input: QueueMessageInput,
|
||||
): Promise<QueueAcceptance> {
|
||||
await this.waitForProjection(input.conversationId);
|
||||
this.assertNoUncertainMutation(input.conversationId);
|
||||
const snapshot = this.snapshot(input.conversationId);
|
||||
if (snapshot.run.runId) {
|
||||
this.acquireRunBackgroundLease(input.conversationId, snapshot.run.runId);
|
||||
@@ -1255,6 +1284,11 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
true,
|
||||
)
|
||||
: error;
|
||||
if (failure instanceof PiProcessError && failure.code === 'PI_RPC_TIMEOUT') {
|
||||
const uncertain = requestUncertainError();
|
||||
this.markRunUncertain(conversationId, runId, uncertain.publicError);
|
||||
throw uncertain;
|
||||
}
|
||||
this.pool.failTopLevel(
|
||||
conversationId,
|
||||
runId,
|
||||
@@ -1265,6 +1299,19 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
private markRunUncertain(
|
||||
conversationId: string,
|
||||
runId: string,
|
||||
error: CodingRuntimePublicError,
|
||||
): void {
|
||||
const current = this.states.get(conversationId)?.snapshot?.run;
|
||||
if (!current || current.runId !== runId || runIsTerminal(current.status)) return;
|
||||
this.emit(conversationId, {
|
||||
op: 'run.state',
|
||||
run: { ...current, error: clone(error) },
|
||||
}, runId);
|
||||
}
|
||||
|
||||
private snapshot(conversationId: string): ConversationSnapshot {
|
||||
const snapshot = this.states.get(conversationId)?.snapshot;
|
||||
if (!snapshot) {
|
||||
@@ -1314,6 +1361,40 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
}
|
||||
|
||||
private onPoolEvent(event: PiWorkerPoolEvent): void {
|
||||
if (event.type === 'top-level.confirmed') {
|
||||
void this.enqueueProjection(event.conversationId, async () => {
|
||||
const snapshot = this.states.get(event.conversationId)?.snapshot;
|
||||
const current = snapshot?.run;
|
||||
if (!snapshot
|
||||
|| snapshot.cursor.workerGeneration !== event.generation
|
||||
|| current?.runId !== event.runId
|
||||
|| runIsTerminal(current.status)) return;
|
||||
const { error: _error, ...confirmed } = current;
|
||||
this.emit(event.conversationId, {
|
||||
op: 'run.state',
|
||||
run: {
|
||||
...confirmed,
|
||||
status: confirmed.status === 'queued' ? 'running' : confirmed.status,
|
||||
},
|
||||
}, event.runId);
|
||||
}).catch((error) => {
|
||||
this.recordProjectionFailure(event.conversationId, event.generation, error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event.type === 'top-level.failed') {
|
||||
void this.enqueueProjection(event.conversationId, async () => {
|
||||
await this.failRun(
|
||||
event.conversationId,
|
||||
event.runId,
|
||||
event.error,
|
||||
event.generation,
|
||||
);
|
||||
}).catch((error) => {
|
||||
this.recordProjectionFailure(event.conversationId, event.generation, error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event.type === 'worker.replaced') {
|
||||
if (!this.states.has(event.conversationId)) return;
|
||||
this.replaceWorkerGeneration(event.conversationId, event.state, true);
|
||||
|
||||
Reference in New Issue
Block a user