返回 DeepSeek-Reasonix
turnSubmissionFailure.ts
根目录 / desktop / frontend / src / lib / turnSubmissionFailure.ts
1 import type { AppBindings } from "./bridge";
2 import { asArray } from "./array";
3 import { removeEmptyAssistantItems } from "./assistantItems";
4 import type { Item, State } from "./useController";
5 import { removeLocalSubmission, updateLocalSubmission } from "./localSubmissionState";
6 import type { ManagementReceipt } from "./turnSubmit";
7 import { t } from "./i18n";
8 import { isTerminalSessionOperation } from "./sessionMaintenanceOperation";
9
10 export function reduceSubmitFailure(
11 state: State,
12 submissionId: string,
13 error: string,
14 conservative: boolean,
15 observedAt: number,
16 ): State {
17 if (!state.localSubmissions[submissionId] || state.localSubmissions[submissionId].settled) return state;
18 const ownsRequest = state.pendingSubmissionId === submissionId;
19 const ownsTurn = Boolean(state.activeTurnId && state.localSubmissions[submissionId].turnId === state.activeTurnId);
20 if (!ownsRequest && (state.pendingSubmissionId || !ownsTurn)) {
21 return updateLocalSubmission(state, submissionId, { status: "failed" });
22 }
23 const next = updateLocalSubmission({
24 ...state,
25 pendingUser: undefined,
26 pendingSubmissionId: undefined,
27 deliveryRecoveryActive: false,
28 cancelRequested: false,
29 seq: state.seq + 1,
30 items: [...(state.transcriptProtocol === 2 ? state.items : removeEmptyAssistantItems(state.items)), { kind: "notice", id: `n${state.seq}`, local: true, level: "warn", text: error } as Item],
31 }, submissionId, { status: "failed" });
32 return {
33 ...next,
34 running: conservative,
35 turnActive: conservative,
36 pendingPrompt: conservative && Boolean(state.approval || state.ask || state.mcpInteraction),
37 cancellable: conservative,
38 ...(conservative ? {} : {
39 activeTurnId: undefined,
40 currentAssistant: undefined,
41 assistantSegmentOrdinal: 0,
42 live: undefined,
43 streamAttemptJournal: undefined,
44 turnLifecycleObservedAt: observedAt,
45 }),
46 };
47 }
48
49 export function reduceSubmitQueued(state: State, submissionId: string, observedAt: number): State {
50 const ownsRequest = state.pendingSubmissionId === submissionId;
51 if (!ownsRequest) return removeLocalSubmission(state, submissionId);
52 // The message lives in the durable queue now; retract the optimistic bubble
53 // so it cannot duplicate the record the queued turn will install later.
54 return removeLocalSubmission({
55 ...state,
56 pendingUser: undefined,
57 pendingSubmissionId: undefined,
58 running: false,
59 turnActive: false,
60 pendingPrompt: false,
61 cancelRequested: false,
62 cancellable: false,
63 activeTurnId: undefined,
64 currentAssistant: undefined,
65 assistantSegmentOrdinal: 0,
66 live: undefined,
67 streamAttemptJournal: undefined,
68 deliveryRecoveryActive: false,
69 turnLifecycleObservedAt: observedAt,
70 }, submissionId);
71 }
72
73 export function reduceManagementConfirmation(state: State, submissionId: string, observedAt: number, receipt?: ManagementReceipt): State {
74 const ownsRequest = state.pendingSubmissionId === submissionId;
75 // Compact requests do not create optimistic chat turns. Only legacy
76 // management submissions with their own echo need conversational cleanup.
77 const next = removeLocalSubmission(ownsRequest ? {
78 ...state,
79 pendingUser: undefined,
80 pendingSubmissionId: undefined,
81 running: false,
82 turnActive: false,
83 pendingPrompt: false,
84 cancelRequested: false,
85 cancellable: false,
86 activeTurnId: undefined,
87 currentAssistant: undefined,
88 assistantSegmentOrdinal: 0,
89 live: undefined,
90 streamAttemptJournal: undefined,
91 deliveryRecoveryActive: false,
92 turnLifecycleObservedAt: observedAt,
93 } : state, submissionId);
94 if (!receipt?.errorCode) return next;
95 const busy = receipt.errorCode === "maintenance_busy";
96 // An admission response can arrive after the operation's durable terminal event.
97 if (busy && next.items.some(item => item.kind === "compaction" && item.operationId === receipt.operationId
98 && isTerminalSessionOperation(item.status))) return next;
99 const text = t(busy ? "compaction.alreadyRunning" : receipt.errorCode === "maintenance_recovery_required"
100 ? "compaction.recoveryRequired" : "compaction.unavailable");
101 const id = `management:${receipt.operationId || submissionId}:${receipt.errorCode}`;
102 return { ...next, items: [...next.items.filter(item => item.id !== id),
103 { kind: "notice", id, local: true, level: busy ? "info" : "warn", text } as Item] };
104 }
105
106 export async function findTabAfterSubmitFailure(
107 binding: Pick<AppBindings, "ListTabs">,
108 tabId: string,
109 delays: readonly number[],
110 clock: () => number,
111 ) {
112 for (const delay of delays) {
113 if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
114 try {
115 // Fence at read start, so a delayed response cannot override a turn or
116 // prompt observed while it was in flight. Preserve a sub-tick advance
117 // for synchronous bridges called in the initiating event's clock tick.
118 const snapshotAt = clock() + 0.001;
119 const tab = asArray(await binding.ListTabs()).find((candidate) => candidate.id === tabId);
120 return [tab, snapshotAt] as const;
121 } catch {
122 // The caller's stale-turn watchdog remains the long-tail backstop.
123 }
124 }
125 return undefined;
126 }
127
128 export function reduceSubmitUnknown(s: State, submissionId: string, error: string): State {
129 const local = s.localSubmissions[submissionId];
130 if (!local || local.settled || local.status === "failed") return s;
131 const ownsRequest = s.pendingSubmissionId === submissionId;
132 const ownsTurn = !s.pendingSubmissionId && s.activeTurnId && local.turnId === s.activeTurnId;
133 return updateLocalSubmission(ownsRequest || ownsTurn ? {
134 ...s, transcriptConnection: "disconnected", transcriptConnectionError: error,
135 } : s, submissionId, { status: "unknown" });
136 }
137
137 lines TYPESCRIPT