fix: close PI-140 cutover gaps
Back up unknown legacy Agents, reconnect settled Conversation observation, remove remaining active repository residue, and restore the bundled Python verifier import.
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
useUserProfileStore,
|
||||
} from '@/stores/user-profile';
|
||||
import type { RawMessage } from '@/types/chat';
|
||||
import type { ConversationSnapshot } from '@/types/coding-conversation';
|
||||
import type {
|
||||
AgentSessionData,
|
||||
AgentSessionMessage,
|
||||
@@ -338,14 +339,7 @@ async function flushUpload(key: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Queue a completed local session snapshot without blocking the chat turn. */
|
||||
export function queueAgentSessionSync(
|
||||
projectId: string,
|
||||
sessionId: string,
|
||||
messages: readonly RawMessage[],
|
||||
): void {
|
||||
const data = buildAgentSessionData(projectId, sessionId, messages);
|
||||
if (!data) return;
|
||||
function queueAgentSessionData(data: AgentSessionData): void {
|
||||
const userId = getCurrentAccountKey();
|
||||
if (!userId) return;
|
||||
|
||||
@@ -356,6 +350,44 @@ export function queueAgentSessionSync(
|
||||
scheduleUpload(key, SESSION_UPLOAD_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
/** Queue a completed local session snapshot without blocking the chat turn. */
|
||||
export function queueAgentSessionSync(
|
||||
projectId: string,
|
||||
sessionId: string,
|
||||
messages: readonly RawMessage[],
|
||||
): void {
|
||||
const data = buildAgentSessionData(projectId, sessionId, messages);
|
||||
if (!data) return;
|
||||
queueAgentSessionData(data);
|
||||
}
|
||||
|
||||
/** Queue the final product Conversation snapshot after a completed prompt turn. */
|
||||
export function queueCodingConversationSessionSync(snapshot: ConversationSnapshot): void {
|
||||
const messages: RawMessage[] = snapshot.nodes.flatMap((node) => {
|
||||
if (node.kind !== 'message') return [];
|
||||
return [{
|
||||
id: node.id,
|
||||
role: node.role,
|
||||
content: node.blocks.flatMap((block) => (
|
||||
block.kind === 'text' ? [{ type: 'text', text: block.text }] : []
|
||||
)),
|
||||
isError: node.status === 'error' || node.status === 'aborted',
|
||||
} satisfies RawMessage];
|
||||
});
|
||||
const settledAt = snapshot.run.settledAt;
|
||||
const updatedAt = typeof settledAt === 'number' && Number.isFinite(settledAt)
|
||||
? new Date(settledAt).toISOString()
|
||||
: new Date().toISOString();
|
||||
const data = buildAgentSessionData(
|
||||
snapshot.conversation.projectId,
|
||||
snapshot.conversation.id,
|
||||
messages,
|
||||
updatedAt,
|
||||
);
|
||||
if (!data?.messages.some((message) => message.role === 'assistant')) return;
|
||||
queueAgentSessionData(data);
|
||||
}
|
||||
|
||||
/** Flush persisted session snapshots after login or profile bootstrap. */
|
||||
export async function flushPendingAgentSessionSync(): Promise<void> {
|
||||
hydratePendingUploads();
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type ConversationReducerState,
|
||||
} from '../../shared/coding-conversation-reducer';
|
||||
import { AppError } from '@/lib/error-model';
|
||||
import { queueCodingConversationSessionSync } from '@/lib/agent-session-sync';
|
||||
import {
|
||||
getCodingConversationSnapshot,
|
||||
openCodingConversationEvents,
|
||||
@@ -55,6 +56,7 @@ interface CodingConversationStoreDependencies {
|
||||
openEvents(conversationId?: string): Promise<EventSource>;
|
||||
submitPrompt(input: SubmitCodingConversationPromptInput): Promise<PromptAcceptance>;
|
||||
recover(conversationId: string): Promise<void>;
|
||||
queueSettledSessionSync(snapshot: ConversationSnapshot): void;
|
||||
createId(kind: 'request' | 'node'): string;
|
||||
preparationTimeoutMs: number;
|
||||
}
|
||||
@@ -277,6 +279,7 @@ function defaultDependencies(): CodingConversationStoreDependencies {
|
||||
openEvents: openCodingConversationEvents,
|
||||
submitPrompt: submitCodingConversationPrompt,
|
||||
recover: recoverCodingConversation,
|
||||
queueSettledSessionSync: queueCodingConversationSessionSync,
|
||||
createId: () => crypto.randomUUID(),
|
||||
preparationTimeoutMs: 10_000,
|
||||
};
|
||||
@@ -332,6 +335,26 @@ function validPatchBatchRange(event: CodingConversationPatchBatchEvent): boolean
|
||||
));
|
||||
}
|
||||
|
||||
function completesOrdinaryPrompt(
|
||||
event: CodingConversationPatchBatchEvent,
|
||||
current: ConversationSnapshot,
|
||||
): boolean {
|
||||
let mode = current.run.mode;
|
||||
let observedActiveRun = current.run.status !== 'idle';
|
||||
for (const item of event.items) {
|
||||
if (item.patch.op !== 'run.state') continue;
|
||||
if (item.patch.run.mode) mode = item.patch.run.mode;
|
||||
if (item.patch.run.status !== 'idle') observedActiveRun = true;
|
||||
if (observedActiveRun
|
||||
&& mode === 'prompt'
|
||||
&& item.patch.run.status === 'idle'
|
||||
&& item.patch.run.terminalReason === 'completed') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function createCodingConversationStore(
|
||||
dependencies: Partial<CodingConversationStoreDependencies> = {},
|
||||
): StoreApi<CodingConversationStoreState> {
|
||||
@@ -892,7 +915,9 @@ export function createCodingConversationStore(
|
||||
}
|
||||
return;
|
||||
}
|
||||
const shouldSyncSettledSession = completesOrdinaryPrompt(event, currentSnapshot);
|
||||
let recover = false;
|
||||
let applied = false;
|
||||
set((state) => {
|
||||
const current = state.entriesByConversationId[event.conversationId] ?? emptyEntry();
|
||||
let reducer = current.reducer;
|
||||
@@ -911,6 +936,7 @@ export function createCodingConversationStore(
|
||||
}
|
||||
}
|
||||
if (reducer === current.reducer) return state;
|
||||
applied = true;
|
||||
const incomingMessages = event.items.flatMap((item) => (
|
||||
item.patch.op === 'message.upsert' ? [item.patch.node] : []
|
||||
));
|
||||
@@ -950,6 +976,9 @@ export function createCodingConversationStore(
|
||||
});
|
||||
if (recover) {
|
||||
void get().loadSnapshot(event.conversationId, true).catch(() => undefined);
|
||||
} else if (applied && shouldSyncSettledSession) {
|
||||
const settledSnapshot = selectCodingConversationSnapshot(event.conversationId)(get());
|
||||
if (settledSnapshot) deps.queueSettledSessionSync(settledSnapshot);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
export type SessionRunQueuedPrompt = {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type SessionRunPhase = 'idle' | 'posting' | 'running' | 'aborting';
|
||||
|
||||
export type SessionRunTerminalReason = 'completed' | 'aborted' | 'failed';
|
||||
|
||||
export type SessionRunState<TPrompt extends SessionRunQueuedPrompt = SessionRunQueuedPrompt> = {
|
||||
phase: SessionRunPhase;
|
||||
runId: number | null;
|
||||
promptId: string | null;
|
||||
queue: TPrompt[];
|
||||
terminalReason: SessionRunTerminalReason | null;
|
||||
error: string | null;
|
||||
suppressNextAbortError: boolean;
|
||||
};
|
||||
|
||||
export type SessionRunEvent<TPrompt extends SessionRunQueuedPrompt = SessionRunQueuedPrompt> =
|
||||
| { type: 'send_started'; runId: number; promptId: string }
|
||||
| { type: 'post_accepted'; runId: number }
|
||||
| { type: 'remote_busy'; runId: number }
|
||||
| { type: 'remote_idle'; runId: number }
|
||||
| { type: 'remote_aborted'; runId: number }
|
||||
| { type: 'remote_failed'; runId: number; error: string }
|
||||
| { type: 'abort_requested'; runId: number }
|
||||
| { type: 'abort_confirmed'; runId: number }
|
||||
| { type: 'abort_failed'; runId: number; error: string }
|
||||
| { type: 'queue_prompt'; prompt: TPrompt }
|
||||
| { type: 'cancel_queued_prompt'; promptId: string };
|
||||
|
||||
export function createIdleSessionRunState<TPrompt extends SessionRunQueuedPrompt>(
|
||||
queue: TPrompt[] = [],
|
||||
): SessionRunState<TPrompt> {
|
||||
return {
|
||||
phase: 'idle',
|
||||
runId: null,
|
||||
promptId: null,
|
||||
queue,
|
||||
terminalReason: null,
|
||||
error: null,
|
||||
suppressNextAbortError: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function isCurrentRunEvent<TPrompt extends SessionRunQueuedPrompt>(
|
||||
state: SessionRunState<TPrompt>,
|
||||
runId: number,
|
||||
): boolean {
|
||||
return state.runId === runId;
|
||||
}
|
||||
|
||||
export function isSessionRunActive<TPrompt extends SessionRunQueuedPrompt>(
|
||||
state: SessionRunState<TPrompt>,
|
||||
): boolean {
|
||||
return state.phase === 'posting' || state.phase === 'running';
|
||||
}
|
||||
|
||||
export function canStartQueuedPrompt<TPrompt extends SessionRunQueuedPrompt>(
|
||||
state: SessionRunState<TPrompt>,
|
||||
): boolean {
|
||||
return state.phase === 'idle'
|
||||
&& state.terminalReason !== 'failed'
|
||||
&& state.queue.length > 0;
|
||||
}
|
||||
|
||||
export function takeNextQueuedPrompt<TPrompt extends SessionRunQueuedPrompt>(
|
||||
state: SessionRunState<TPrompt>,
|
||||
): {
|
||||
prompt: TPrompt | null;
|
||||
state: SessionRunState<TPrompt>;
|
||||
} {
|
||||
const [prompt, ...queue] = state.queue;
|
||||
return {
|
||||
prompt: prompt ?? null,
|
||||
state: {
|
||||
...state,
|
||||
queue,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function didSuppressAbortError<TPrompt extends SessionRunQueuedPrompt>(
|
||||
previousState: SessionRunState<TPrompt>,
|
||||
nextState: SessionRunState<TPrompt>,
|
||||
): boolean {
|
||||
return previousState.suppressNextAbortError
|
||||
&& !nextState.suppressNextAbortError
|
||||
&& previousState.phase === nextState.phase
|
||||
&& previousState.runId === nextState.runId
|
||||
&& previousState.promptId === nextState.promptId
|
||||
&& nextState.terminalReason === previousState.terminalReason
|
||||
&& nextState.error === null;
|
||||
}
|
||||
|
||||
export function transitionSessionRunState<TPrompt extends SessionRunQueuedPrompt>(
|
||||
state: SessionRunState<TPrompt>,
|
||||
event: SessionRunEvent<TPrompt>,
|
||||
): SessionRunState<TPrompt> {
|
||||
if (event.type === 'send_started') {
|
||||
return {
|
||||
phase: 'posting',
|
||||
runId: event.runId,
|
||||
promptId: event.promptId,
|
||||
queue: state.queue,
|
||||
terminalReason: null,
|
||||
error: null,
|
||||
suppressNextAbortError: state.suppressNextAbortError,
|
||||
};
|
||||
}
|
||||
|
||||
if (event.type === 'queue_prompt') {
|
||||
return {
|
||||
...state,
|
||||
queue: [...state.queue, event.prompt],
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (event.type === 'cancel_queued_prompt') {
|
||||
return {
|
||||
...state,
|
||||
queue: state.queue.filter((prompt) => prompt.id !== event.promptId),
|
||||
};
|
||||
}
|
||||
|
||||
if (!isCurrentRunEvent(state, event.runId)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (
|
||||
state.phase === 'idle'
|
||||
&& state.terminalReason !== null
|
||||
&& (
|
||||
event.type === 'post_accepted'
|
||||
|| event.type === 'remote_busy'
|
||||
|| event.type === 'remote_idle'
|
||||
)
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'post_accepted':
|
||||
return state;
|
||||
case 'remote_busy':
|
||||
return {
|
||||
...state,
|
||||
phase: 'running',
|
||||
terminalReason: null,
|
||||
error: null,
|
||||
};
|
||||
case 'remote_idle':
|
||||
if (state.phase === 'posting') return state;
|
||||
return {
|
||||
...state,
|
||||
phase: 'idle',
|
||||
terminalReason: 'completed',
|
||||
error: null,
|
||||
suppressNextAbortError: false,
|
||||
};
|
||||
case 'remote_aborted':
|
||||
if (state.suppressNextAbortError) {
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
suppressNextAbortError: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
phase: 'idle',
|
||||
queue: [],
|
||||
terminalReason: 'aborted',
|
||||
error: null,
|
||||
suppressNextAbortError: false,
|
||||
};
|
||||
case 'remote_failed':
|
||||
return {
|
||||
...state,
|
||||
phase: 'idle',
|
||||
queue: [],
|
||||
terminalReason: 'failed',
|
||||
error: event.error,
|
||||
suppressNextAbortError: false,
|
||||
};
|
||||
case 'abort_requested':
|
||||
return {
|
||||
...state,
|
||||
phase: 'aborting',
|
||||
queue: [],
|
||||
terminalReason: 'aborted',
|
||||
error: null,
|
||||
suppressNextAbortError: true,
|
||||
};
|
||||
case 'abort_confirmed':
|
||||
return {
|
||||
...state,
|
||||
phase: 'idle',
|
||||
queue: [],
|
||||
terminalReason: 'aborted',
|
||||
error: null,
|
||||
suppressNextAbortError: false,
|
||||
};
|
||||
case 'abort_failed':
|
||||
return {
|
||||
...state,
|
||||
phase: 'idle',
|
||||
queue: [],
|
||||
terminalReason: 'failed',
|
||||
error: event.error,
|
||||
suppressNextAbortError: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user