chore(integration): snapshot local main worktree

This commit is contained in:
inman
2026-08-31 10:23:21 +08:00
parent 48a9189939
commit 33fb31fb28
116 changed files with 8108 additions and 3026 deletions

View File

@@ -36,6 +36,8 @@ export type CodingConversationLoadState =
| 'recovering'
| 'error';
export type CodingConversationSnapshotLoadMode = 'loading' | 'recovering' | 'silent';
export interface CodingConversationEntry {
reducer: ConversationReducerState;
loadState: CodingConversationLoadState;
@@ -72,7 +74,10 @@ export interface CodingConversationStoreState {
primeConversation(snapshot: ConversationSnapshot): void;
clearConversationSelection(): void;
selectConversation(conversationId: string): Promise<void>;
loadSnapshot(conversationId: string, recovering?: boolean): Promise<ConversationSnapshot>;
loadSnapshot(
conversationId: string,
mode?: CodingConversationSnapshotLoadMode,
): Promise<ConversationSnapshot>;
recoverConversation(conversationId: string): Promise<void>;
connectEvents(): Promise<void>;
disconnectEvents(): void;
@@ -434,7 +439,7 @@ export function createCodingConversationStore(
recoveryRefreshKeys.set(conversationId, refreshKey);
queueMicrotask(() => {
if (!snapshotLoads.has(conversationId)) {
void store.getState().loadSnapshot(conversationId, true).catch(() => undefined);
void store.getState().loadSnapshot(conversationId, 'recovering').catch(() => undefined);
}
});
}
@@ -509,27 +514,32 @@ export function createCodingConversationStore(
const snapshotFlight = !entry?.reducer.snapshot
|| entry.reducer.invalidation
|| entry.loadState !== 'live'
? get().loadSnapshot(conversationId, Boolean(entry?.reducer.invalidation))
? get().loadSnapshot(
conversationId,
entry?.reducer.invalidation ? 'recovering' : 'loading',
)
: Promise.resolve(entry.reducer.snapshot);
await Promise.all([snapshotFlight, get().connectEvents()]);
},
loadSnapshot(conversationId, recovering = false) {
loadSnapshot(conversationId, mode = 'loading') {
const prior = snapshotLoads.get(conversationId);
if (prior) return prior;
set((state) => {
const entry = state.entriesByConversationId[conversationId] ?? emptyEntry();
return {
entriesByConversationId: {
...state.entriesByConversationId,
[conversationId]: {
...entry,
loadState: recovering ? 'recovering' : 'loading',
error: null,
if (mode !== 'silent') {
set((state) => {
const entry = state.entriesByConversationId[conversationId] ?? emptyEntry();
return {
entriesByConversationId: {
...state.entriesByConversationId,
[conversationId]: {
...entry,
loadState: mode,
error: null,
},
},
},
};
});
};
});
}
const flight = withPreparationDeadline(
deps.getSnapshot(conversationId),
deps.preparationTimeoutMs,
@@ -545,16 +555,18 @@ export function createCodingConversationStore(
return snapshot;
})
.catch((error) => {
const failure = errorDetails(error);
set((state) => {
const entry = state.entriesByConversationId[conversationId] ?? emptyEntry();
return {
entriesByConversationId: {
...state.entriesByConversationId,
[conversationId]: { ...entry, loadState: 'error', error: failure.message },
},
};
});
if (mode !== 'silent') {
const failure = errorDetails(error);
set((state) => {
const entry = state.entriesByConversationId[conversationId] ?? emptyEntry();
return {
entriesByConversationId: {
...state.entriesByConversationId,
[conversationId]: { ...entry, loadState: 'error', error: failure.message },
},
};
});
}
throw error;
})
.finally(() => {
@@ -579,7 +591,7 @@ export function createCodingConversationStore(
});
try {
await deps.recover(conversationId);
await get().loadSnapshot(conversationId, true);
await get().loadSnapshot(conversationId, 'recovering');
} catch (error) {
const failure = errorDetails(error);
set((state) => {
@@ -689,7 +701,10 @@ export function createCodingConversationStore(
async submitPrompt(input) {
let entry = get().entriesByConversationId[input.conversationId];
if (!entry?.reducer.snapshot || entry.reducer.invalidation) {
await get().loadSnapshot(input.conversationId, Boolean(entry?.reducer.invalidation));
await get().loadSnapshot(
input.conversationId,
entry?.reducer.invalidation ? 'recovering' : 'loading',
);
entry = get().entriesByConversationId[input.conversationId];
}
if (!entry?.reducer.snapshot) throw new Error('Conversation snapshot is unavailable');
@@ -915,30 +930,43 @@ export function createCodingConversationStore(
|| Boolean(currentBeforeBatch?.reducer.invalidation);
if (!validRange) {
if (!snapshotLoads.has(event.conversationId)) {
void get().loadSnapshot(event.conversationId, true).catch(() => undefined);
void get().loadSnapshot(event.conversationId, 'recovering').catch(() => undefined);
}
return;
}
let applicableEvent = event;
if (currentSnapshot
&& event.workerGeneration === currentSnapshot.cursor.workerGeneration
&& event.fromSeq <= currentSnapshot.cursor.seq) {
const unseenItems = event.items.filter((item) => item.seq > currentSnapshot.cursor.seq);
if (unseenItems.length === 0) return;
applicableEvent = {
...event,
fromSeq: unseenItems[0]!.seq,
toSeq: unseenItems.at(-1)!.seq,
items: unseenItems,
};
}
if (recovering
|| !currentSnapshot
|| event.workerGeneration !== currentSnapshot.cursor.workerGeneration
|| event.fromSeq !== currentSnapshot.cursor.seq + 1) {
bufferRecoveryBatch(event);
if (!snapshotLoads.has(event.conversationId)) {
void get().loadSnapshot(event.conversationId, true).catch(() => undefined);
|| applicableEvent.workerGeneration !== currentSnapshot.cursor.workerGeneration
|| applicableEvent.fromSeq !== currentSnapshot.cursor.seq + 1) {
bufferRecoveryBatch(applicableEvent);
if (!snapshotLoads.has(applicableEvent.conversationId)) {
void get().loadSnapshot(applicableEvent.conversationId, 'recovering').catch(() => undefined);
}
return;
}
const shouldSyncSettledSession = completesOrdinaryPrompt(event, currentSnapshot);
const shouldSyncSettledSession = completesOrdinaryPrompt(applicableEvent, currentSnapshot);
let recover = false;
let applied = false;
set((state) => {
const current = state.entriesByConversationId[event.conversationId] ?? emptyEntry();
const current = state.entriesByConversationId[applicableEvent.conversationId] ?? emptyEntry();
let reducer = current.reducer;
for (const item of event.items) {
for (const item of applicableEvent.items) {
reducer = reduceConversationPatch(reducer, {
conversationId: event.conversationId,
workerGeneration: event.workerGeneration,
conversationId: applicableEvent.conversationId,
workerGeneration: applicableEvent.workerGeneration,
...(item.runId ? { runId: item.runId } : {}),
seq: item.seq,
at: item.at,
@@ -951,10 +979,10 @@ export function createCodingConversationStore(
}
if (reducer === current.reducer) return state;
applied = true;
const incomingMessages = event.items.flatMap((item) => (
const incomingMessages = applicableEvent.items.flatMap((item) => (
item.patch.op === 'message.upsert' ? [item.patch.node] : []
));
const unread = state.selectedConversationId !== event.conversationId
const unread = state.selectedConversationId !== applicableEvent.conversationId
&& incomingMessages.some((message) => message.role === 'assistant')
? true
: current.unread;
@@ -965,7 +993,7 @@ export function createCodingConversationStore(
error: null,
unread,
};
const requests = state.requestsByConversationId[event.conversationId] ?? {};
const requests = state.requestsByConversationId[applicableEvent.conversationId] ?? {};
const nextRequests = reducer.snapshot
? withoutReconciledRequests(requests, reducer.snapshot)
: requests;
@@ -973,21 +1001,21 @@ export function createCodingConversationStore(
return {
entriesByConversationId: {
...state.entriesByConversationId,
[event.conversationId]: entry,
[applicableEvent.conversationId]: entry,
},
summariesByConversationId: summariesWithEntry(state.summariesByConversationId, entry),
requestsByConversationId: !requestsChanged
? state.requestsByConversationId
: {
...state.requestsByConversationId,
[event.conversationId]: nextRequests,
[applicableEvent.conversationId]: nextRequests,
},
};
});
if (recover) {
void get().loadSnapshot(event.conversationId, true).catch(() => undefined);
void get().loadSnapshot(applicableEvent.conversationId, 'recovering').catch(() => undefined);
} else if (applied && shouldSyncSettledSession) {
const settledSnapshot = selectCodingConversationSnapshot(event.conversationId)(get());
const settledSnapshot = selectCodingConversationSnapshot(applicableEvent.conversationId)(get());
if (settledSnapshot) deps.queueSettledSessionSync(settledSnapshot);
}
},