返回 DeepSeek-Reasonix
submissionReducer.ts
根目录 / desktop / frontend / src / lib / submissionReducer.ts
1 import type { Action, State } from "./useController";
2 import { sessionIdentityStableKey } from "./sessionIdentity";
3 import { beginLocalSubmission, canonicalUserConfirmations, settleLocalSubmissions, updateLocalSubmission } from "./localSubmissionState";
4
5 export function submissionBindingCurrent(current: State | undefined, expected: State): boolean {
6 return current?.sessionGen === expected.sessionGen && sessionIdentityStableKey(current?.meta) === sessionIdentityStableKey(expected.meta);
7 }
8
9 export function resetTurnTiming(now = Date.now()): Pick<State, "turnStartAt" | "turnDoneAt" | "turnWaitAccumMs" | "promptWaitStartedAt" | "turnTokens" | "turnTotalTokens" | "turnUsage" | "turnOutputTokens" | "turnOutputChars" | "turnOutputCharsAtUsage" | "turnOutputEstimated" | "turnModelActiveAt" | "turnModelActiveMs" | "turnCost" | "turnRateBand" | "turnArgChars" | "pendingRequestModelMs"> {
10 return {
11 turnStartAt: now,
12 turnDoneAt: 0,
13 turnWaitAccumMs: 0,
14 promptWaitStartedAt: undefined,
15 turnTokens: 0,
16 turnTotalTokens: 0,
17 turnUsage: undefined,
18 turnOutputTokens: 0,
19 turnOutputChars: 0,
20 turnOutputCharsAtUsage: 0,
21 turnOutputEstimated: false,
22 turnModelActiveAt: undefined,
23 turnModelActiveMs: 0, pendingRequestModelMs: undefined,
24 turnCost: 0,
25 turnRateBand: undefined,
26 turnArgChars: 0,
27 };
28 }
29
30 export function confirmPendingUser(s: State, submissionId: string | undefined): State {
31 if (!submissionId) return s;
32 const next = s.pendingSubmissionId === submissionId ? { ...s, pendingUser: undefined, pendingSubmissionId: undefined } : s;
33 if (s.localSubmissions[submissionId]?.status === "failed") return next;
34 return updateLocalSubmission(next, submissionId, {
35 status: "accepted",
36 });
37 }
38
39
40 export function installTranscriptRecords(s: State, a: Extract<Action, { type: "transcript_records" }>): State {
41 const updates = new Map(a.projection.items.map(item => [item.id, item]));
42 const removed = new Set(a.projection.removeIds);
43 const items = s.items.filter(item => !removed.has(item.id)).map(item => {
44 const update = updates.get(item.id);
45 if (item.kind === "tool" && update?.kind === "tool" && update.resultMissing && item.status === "running") {
46 return { ...update, status: "running" as const, execution: item.execution, startedAt: item.startedAt };
47 }
48 if (item.kind === "assistant" && update?.kind === "assistant" && item.turnFinal && !update.turnFinal) {
49 return { ...update, turnFinal: true, turnDurationMs: item.turnDurationMs, turnUsage: item.turnUsage,
50 samplingCount: item.samplingCount, toolCount: item.toolCount };
51 }
52 return update ?? item;
53 });
54 const present = new Set(items.map(item => item.id));
55 const positions = new Map(a.projection.items.map((item, index) => [item.id, index]));
56 for (const item of updates.values()) {
57 if (present.has(item.id)) continue;
58 const following = items.findIndex(candidate => (positions.get(candidate.id) ?? -1) > positions.get(item.id)!);
59 const output = item.kind === "user" && item.turnId
60 ? items.findIndex(candidate => candidate.kind !== "user" && candidate.turnId === item.turnId) : -1;
61 const at = following >= 0 && output >= 0 ? Math.min(following, output) : Math.max(following, output);
62 items.splice(at < 0 ? items.length : at, 0, item);
63 present.add(item.id);
64 }
65 return settleLocalSubmissions({ ...s, items, historyHasOlder: a.projection.hasOlder, historyHasNewer: a.projection.hasNewer }, items,
66 [...a.confirmedUsers, ...canonicalUserConfirmations(a.projection.items)]);
67 }
68
69 export function startLocalSubmission(s: State, a: Extract<Action, { type: "user" }>, clock: number): State {
70 const seq = a.seq !== undefined ? a.seq : s.seq;
71 const userItemId = `u${seq}`;
72 const next = {
73 ...s,
74 completionSummary: undefined,
75 seq: seq + 1,
76 items: s.items.map(item => item.kind==="notice" && item.action==="recover_context" ? {...item,action:undefined} : item),
77 running: true,
78 pendingPrompt: false,
79 cancelRequested: false,
80 cancellable: true,
81 ...resetTurnTiming(),
82 turnLifecycleObservedAt: clock,
83 // New turn epoch: forget the previous prompt anchor so a genuinely new
84 // prompt re-anchors freshly instead of inheriting a stale id/time.
85 promptArrivedAt: undefined,
86 promptArrivedId: undefined,
87 pendingUser: a.text,
88 pendingSubmissionId: a.submissionId,
89 activeTurnId: s.turnActive ? s.activeTurnId : undefined,
90 currentAssistant: undefined,
91 assistantSegmentOrdinal: 0,
92 live: undefined,
93 streamAttemptJournal: undefined,
94 streamInterruptNoticeShown: undefined,
95 deliveryRecoveryActive: Boolean(a.deliveryRecovery),
96 discardTurn: false,
97 };
98 return beginLocalSubmission(next, {
99 submissionId: a.submissionId,
100 localId: userItemId,
101 text: a.text,
102 submitText: a.submitText,
103 createdAt: Date.now(),
104 sequence: seq,
105 anchorItemId: s.historyHasNewer ? undefined : s.items[s.items.length - 1]?.id,
106 placement: s.historyHasNewer ? "latest" : s.items.length ? "after" : "start",
107 });
108 }
109
109 lines TYPESCRIPT