@k2b/cloud 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/_internal/capabilities.ts +12 -0
- package/src/_internal/define-app.ts +8 -1
- package/src/_internal/process-identity.ts +7 -1
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/registry.ts +1 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/GroupCoverage.tsx +175 -0
- package/src/access/PermissionEditor.tsx +119 -99
- package/src/access/messages.ts +30 -0
- package/src/ai/admin.ts +1 -0
- package/src/ai/approval-routes.ts +5 -5
- package/src/ai/browser-code-contracts.ts +14 -2
- package/src/ai/browser.ts +8 -1
- package/src/ai/capabilities.ts +115 -33
- package/src/ai/chat/blocks.tsx +86 -179
- package/src/ai/chat/builtin-tools.tsx +83 -48
- package/src/ai/chat/file-tools.tsx +4 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +44 -0
- package/src/ai/chat/message-actions.tsx +6 -2
- package/src/ai/chat/message-utils.ts +17 -14
- package/src/ai/chat/messages.ts +330 -2
- package/src/ai/chat/presentation.tsx +278 -104
- package/src/ai/chat/tool-groups.ts +55 -35
- package/src/ai/chat/turn-layout.ts +141 -0
- package/src/ai/chat/turn-view.tsx +644 -0
- package/src/ai/client/controller.ts +60 -31
- package/src/ai/client/file-source.ts +20 -3
- package/src/ai/client/projection.ts +42 -6
- package/src/ai/code-mode-skill.ts +27 -27
- package/src/ai/code-runtime-tools.ts +10 -1
- package/src/ai/code-source-contracts.ts +54 -4
- package/src/ai/code-source-tools.ts +10 -3
- package/src/ai/credentials.ts +17 -3
- package/src/ai/data-analysis-skill.ts +2 -2
- package/src/ai/default-tools.ts +2 -2
- package/src/ai/executor.ts +177 -80
- package/src/ai/file-context.ts +14 -2
- package/src/ai/file-tools.ts +17 -3
- package/src/ai/files-store.ts +134 -11
- package/src/ai/grids-skill.ts +2 -2
- package/src/ai/index.ts +7 -0
- package/src/ai/memories.ts +14 -0
- package/src/ai/migrate.ts +125 -0
- package/src/ai/model-request-settings.ts +98 -0
- package/src/ai/protocol.ts +26 -4
- package/src/ai/provider-fetch.ts +67 -15
- package/src/ai/provider-retry.ts +105 -0
- package/src/ai/provider.ts +7 -1
- package/src/ai/quota-provider.ts +16 -7
- package/src/ai/request-headers.ts +117 -0
- package/src/ai/routes.ts +34 -6
- package/src/ai/runtime.ts +1 -1
- package/src/ai/settings.ts +19 -2
- package/src/ai/skill-seeds.ts +31 -3
- package/src/ai/skills.ts +26 -0
- package/src/ai/solid.ts +1 -1
- package/src/ai/store.ts +202 -59
- package/src/ai/stream.ts +182 -37
- package/src/ai/structured.ts +20 -5
- package/src/ai/system-prompt.ts +25 -0
- package/src/ai/timeline.ts +9 -11
- package/src/ai/tool-call-names.ts +45 -0
- package/src/ai/turn-policy.ts +247 -0
- package/src/ai/turn-timing.ts +31 -3
- package/src/ai/types.ts +36 -5
- package/src/api/admin-ai-quotas.ts +36 -1
- package/src/api/admin-core-settings.ts +16 -23
- package/src/api/admin-outgoing-mail.ts +62 -0
- package/src/api/index.ts +2 -0
- package/src/cli/admin/ai-quotas.ts +70 -1
- package/src/cli/admin/index.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +118 -0
- package/src/contracts/app.ts +2 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +77 -0
- package/src/contracts/registry.ts +4 -0
- package/src/services/index.ts +3 -0
- package/src/services/notifications/email.ts +16 -26
- package/src/services/outgoing-mail/index.ts +19 -0
- package/src/services/outgoing-mail/store.ts +286 -0
- package/src/services/outgoing-mail/test-send.ts +40 -0
- package/src/services/outgoing-mail/transport.ts +13 -0
- package/src/services/settings/core-settings.ts +1 -38
- package/src/services/settings/store.ts +5 -1
- package/src/shared/ai-model-request-settings.ts +21 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-request-options.ts +185 -0
- package/src/shared/app-presentation.ts +10 -2
- package/src/ssr/admin-navigation.ts +1 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/workspace-navigation.ts +7 -1
- package/src/styles/effects.css +69 -0
|
@@ -2,7 +2,7 @@ import { type Accessor, createMemo, createSignal, onCleanup } from "solid-js";
|
|
|
2
2
|
import { createStore, reconcile } from "solid-js/store";
|
|
3
3
|
import { type AiAttachmentRef, aiAttachmentMarker } from "../attachments";
|
|
4
4
|
import { aiChatMessages } from "../chat/messages";
|
|
5
|
-
import { type AiStreamEvent, type AiTurnBlock, type AiTurnSnapshot, steerMessageBlockId } from "../protocol";
|
|
5
|
+
import { AI_TURN_LEASE_MS, type AiStreamEvent, type AiTurnBlock, type AiTurnSnapshot, steerMessageBlockId } from "../protocol";
|
|
6
6
|
import { type AiResourceMarker, aiResourceMarker } from "../resource-markers";
|
|
7
7
|
import type {
|
|
8
8
|
AiClientToolId,
|
|
@@ -201,6 +201,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
201
201
|
const abortRequests = new Map<string, Promise<boolean>>();
|
|
202
202
|
let stream: AiStreamHandle | null = null;
|
|
203
203
|
let streamSession: AiStreamSession | null = null;
|
|
204
|
+
let silenceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
204
205
|
/** The chat error left by a stream that ended for good, until a stream opens again. */
|
|
205
206
|
let streamError: string | null = null;
|
|
206
207
|
let streamGeneration = 0;
|
|
@@ -294,8 +295,32 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
294
295
|
void markConversationViewed(session.conversationId);
|
|
295
296
|
};
|
|
296
297
|
|
|
298
|
+
const clearSilenceTimer = () => {
|
|
299
|
+
if (silenceTimer) clearTimeout(silenceTimer);
|
|
300
|
+
silenceTimer = undefined;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* A running turn whose open stream delivers nothing for one worker lease either works silently or lost an update,
|
|
305
|
+
* such as its end. The stream then starts again from the turn's saved state. The wait starts with the stream's
|
|
306
|
+
* first event, so a slow first state is never cut off. A turn that waits for an action in this browser is not
|
|
307
|
+
* silent, and a stream that reconnects brings a fresh state anyway.
|
|
308
|
+
*/
|
|
309
|
+
const watchSilence = (session: AiStreamSession) => {
|
|
310
|
+
clearSilenceTimer();
|
|
311
|
+
silenceTimer = setTimeout(() => {
|
|
312
|
+
silenceTimer = undefined;
|
|
313
|
+
if (!isCurrentStreamSession(streamSession, session)) return;
|
|
314
|
+
const status = streamStatus();
|
|
315
|
+
if (state.activeTurn?.status !== "running" || status === "connecting" || status === "reconnecting") return watchSilence(session);
|
|
316
|
+
closeStream();
|
|
317
|
+
openStream(session.conversationId);
|
|
318
|
+
}, AI_TURN_LEASE_MS);
|
|
319
|
+
};
|
|
320
|
+
|
|
297
321
|
const applyEvent = (session: AiStreamSession, event: AiStreamEvent) => {
|
|
298
322
|
if (!isCurrentStreamSession(streamSession, session)) return;
|
|
323
|
+
watchSilence(session);
|
|
299
324
|
const conversationId = session.conversationId;
|
|
300
325
|
if (event.type === "turn_finished" && pendingSends()[conversationId]) {
|
|
301
326
|
completedDuringSend.set(conversationId, event.turnId);
|
|
@@ -327,6 +352,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
327
352
|
};
|
|
328
353
|
|
|
329
354
|
const closeStream = () => {
|
|
355
|
+
clearSilenceTimer();
|
|
330
356
|
stream?.close();
|
|
331
357
|
stream = null;
|
|
332
358
|
streamSession = null;
|
|
@@ -400,15 +426,15 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
400
426
|
console.warn(`No frontend handler registered for AI tool "${name}" — leaving the action request pending.`);
|
|
401
427
|
return false;
|
|
402
428
|
}
|
|
429
|
+
let result: unknown;
|
|
403
430
|
try {
|
|
404
|
-
|
|
405
|
-
return submitTurnActionForConversation(conversationId, turnId, callId, { type: "tool_result", result });
|
|
431
|
+
result = await handler({ name, callId, args, turnId, conversationId });
|
|
406
432
|
} catch (toolError) {
|
|
407
|
-
|
|
408
|
-
type: "tool_result",
|
|
409
|
-
result: { error: toolError instanceof Error ? toolError.message : "Frontend tool failed" },
|
|
410
|
-
});
|
|
433
|
+
result = { error: toolError instanceof Error ? toolError.message : "Frontend tool failed" };
|
|
411
434
|
}
|
|
435
|
+
const submitted = await submitTurnActionForConversation(conversationId, turnId, callId, { type: "tool_result", result });
|
|
436
|
+
if (submitted) showActionResolved(conversationId, turnId, { type: "tool_result", callId, result });
|
|
437
|
+
return submitted;
|
|
412
438
|
};
|
|
413
439
|
|
|
414
440
|
// ---- conversation loading --------------------------------------------
|
|
@@ -1258,6 +1284,28 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1258
1284
|
}
|
|
1259
1285
|
};
|
|
1260
1286
|
|
|
1287
|
+
/**
|
|
1288
|
+
* Shows an action the server accepted as resolved, so the turn runs on before the stream confirms it. A turn that
|
|
1289
|
+
* runs is watched for silence again, which heals a confirmation that never arrives.
|
|
1290
|
+
*/
|
|
1291
|
+
const showActionResolved = (
|
|
1292
|
+
conversationId: string,
|
|
1293
|
+
turnId: string,
|
|
1294
|
+
action: { type: "approval_response"; callId: string; approved: boolean } | { type: "tool_result"; callId: string; result: unknown },
|
|
1295
|
+
) => {
|
|
1296
|
+
if (!isActiveConversation(conversationId) || state.activeTurn?.turnId !== turnId) return;
|
|
1297
|
+
const waitingStatus = action.type === "approval_response" ? "awaiting_approval" : "awaiting_client";
|
|
1298
|
+
setState("activeTurn", (current) => {
|
|
1299
|
+
if (!current || current.turnId !== turnId) return current;
|
|
1300
|
+
const pending = current.blocks.some(
|
|
1301
|
+
(block) => block.kind === "tool" && block.callId === action.callId && block.status === waitingStatus,
|
|
1302
|
+
);
|
|
1303
|
+
if (!pending) return current;
|
|
1304
|
+
return reconcileActiveTurnActions(current, [{ callId: action.callId, resolvedEvent: action }]);
|
|
1305
|
+
});
|
|
1306
|
+
cache.set(conversationId, { conversation: state.conversation, messages: state.messages, activeTurn: state.activeTurn });
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1261
1309
|
const submitTurnAction = (
|
|
1262
1310
|
turnId: string,
|
|
1263
1311
|
callId: string,
|
|
@@ -1277,18 +1325,9 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1277
1325
|
approved: input.approved,
|
|
1278
1326
|
remember: input.remember,
|
|
1279
1327
|
});
|
|
1280
|
-
if (
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
if (!current || current.turnId !== request.turnId) return current;
|
|
1284
|
-
const pending = current.blocks.some(
|
|
1285
|
-
(block) => block.kind === "tool" && block.callId === request.callId && block.status === "awaiting_approval",
|
|
1286
|
-
);
|
|
1287
|
-
if (!pending) return current;
|
|
1288
|
-
return reconcileActiveTurnActions(current, [{ callId: request.callId, resolvedEvent: action }]);
|
|
1289
|
-
});
|
|
1290
|
-
cache.set(conversationId, { conversation: state.conversation, messages: state.messages, activeTurn: state.activeTurn });
|
|
1291
|
-
return true;
|
|
1328
|
+
if (submitted)
|
|
1329
|
+
showActionResolved(conversationId, request.turnId, { type: "approval_response", callId: request.callId, approved: input.approved });
|
|
1330
|
+
return submitted;
|
|
1292
1331
|
};
|
|
1293
1332
|
|
|
1294
1333
|
const submitFrontendToolResult = async (request: { turnId: string; callId: string }, result: unknown) => {
|
|
@@ -1299,18 +1338,8 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1299
1338
|
type: "tool_result",
|
|
1300
1339
|
result,
|
|
1301
1340
|
});
|
|
1302
|
-
if (
|
|
1303
|
-
|
|
1304
|
-
setState("activeTurn", (current) => {
|
|
1305
|
-
if (!current || current.turnId !== request.turnId) return current;
|
|
1306
|
-
const pending = current.blocks.some(
|
|
1307
|
-
(block) => block.kind === "tool" && block.callId === request.callId && block.status === "awaiting_client",
|
|
1308
|
-
);
|
|
1309
|
-
if (!pending) return current;
|
|
1310
|
-
return reconcileActiveTurnActions(current, [{ callId: request.callId, resolvedEvent: action }]);
|
|
1311
|
-
});
|
|
1312
|
-
cache.set(conversationId, { conversation: state.conversation, messages: state.messages, activeTurn: state.activeTurn });
|
|
1313
|
-
return true;
|
|
1341
|
+
if (submitted) showActionResolved(conversationId, request.turnId, { type: "tool_result", callId: request.callId, result });
|
|
1342
|
+
return submitted;
|
|
1314
1343
|
};
|
|
1315
1344
|
|
|
1316
1345
|
if (options.initialConversationId) {
|
|
@@ -30,7 +30,14 @@ const bytesToBase64 = (bytes: Uint8Array): string => {
|
|
|
30
30
|
return btoa(binary);
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
export
|
|
33
|
+
export type ConversationFileSource = FileSource & {
|
|
34
|
+
/** Files below `prefix` (default `/`); with `page`, at most `page.limit` files by path after `page.after`. */
|
|
35
|
+
listFiles(prefix?: string, page?: { limit: number; after?: string }): Promise<AiFileStat[]>;
|
|
36
|
+
/** Delete a folder with everything below it. */
|
|
37
|
+
removeFolder(path: string): Promise<void>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const conversationFileSource = (baseUrl: string, conversationId: string): ConversationFileSource => {
|
|
34
41
|
const filesUrl = (suffix = "", params?: Record<string, string>) => {
|
|
35
42
|
const query = params ? `?${new URLSearchParams(params).toString()}` : "";
|
|
36
43
|
return `${baseUrl}/conversations/${conversationId}/files${suffix}${query}`;
|
|
@@ -45,8 +52,14 @@ export const conversationFileSource = (baseUrl: string, conversationId: string):
|
|
|
45
52
|
return (await response.json()) as T;
|
|
46
53
|
};
|
|
47
54
|
|
|
48
|
-
const listFiles = async (): Promise<AiFileStat[]> =>
|
|
49
|
-
|
|
55
|
+
const listFiles = async (prefix?: string, page?: { limit: number; after?: string }): Promise<AiFileStat[]> => {
|
|
56
|
+
const params: Record<string, string> = {
|
|
57
|
+
...(prefix ? { prefix } : {}),
|
|
58
|
+
...(page ? { limit: String(page.limit), ...(page.after ? { after: page.after } : {}) } : {}),
|
|
59
|
+
};
|
|
60
|
+
const url = Object.keys(params).length ? filesUrl("", params) : filesUrl();
|
|
61
|
+
return (await request<{ files: AiFileStat[] }>(url, { method: "GET" }, "Failed to load files")).files;
|
|
62
|
+
};
|
|
50
63
|
|
|
51
64
|
return {
|
|
52
65
|
previewPreferencesKey: "assistant.csv-preview",
|
|
@@ -80,6 +93,10 @@ export const conversationFileSource = (baseUrl: string, conversationId: string):
|
|
|
80
93
|
await request(filesUrl("", { path }), { method: "DELETE" }, "Failed to delete file");
|
|
81
94
|
},
|
|
82
95
|
|
|
96
|
+
async removeFolder(path) {
|
|
97
|
+
await request(filesUrl("", { path, recursive: "true" }), { method: "DELETE" }, "Failed to delete folder");
|
|
98
|
+
},
|
|
99
|
+
|
|
83
100
|
async rename(from, to) {
|
|
84
101
|
await request(filesUrl("/rename"), { method: "POST", body: JSON.stringify({ from, to }) }, "Failed to rename file");
|
|
85
102
|
},
|
|
@@ -9,6 +9,17 @@ export type AiActiveTurn = {
|
|
|
9
9
|
status: "running" | "waiting_for_action";
|
|
10
10
|
blocks: AiTurnBlock[];
|
|
11
11
|
modelProfileId: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* A model call failed transiently and waits for its retry. Live only: the next turn event ends it, and so does a
|
|
14
|
+
* snapshot that has moved past the retry.
|
|
15
|
+
*/
|
|
16
|
+
providerRetry?: boolean;
|
|
17
|
+
/** Server start of the turn, known from a state snapshot. */
|
|
18
|
+
createdAt?: string;
|
|
19
|
+
/** Time already spent waiting for answered user actions, from the last state snapshot. */
|
|
20
|
+
actionWaitMs?: number;
|
|
21
|
+
/** Start of the user action the turn waited for at the last state snapshot. */
|
|
22
|
+
waitingSince?: string | null;
|
|
12
23
|
};
|
|
13
24
|
|
|
14
25
|
export type AiChatProjection = {
|
|
@@ -90,7 +101,9 @@ export const mergeActiveTurn = (previous: AiActiveTurn | null, incoming: AiActiv
|
|
|
90
101
|
const pendingSteers = previous.blocks.filter((block) => block.kind === "steer_message" && block.status !== "consumed");
|
|
91
102
|
const known = new Set(incoming.blocks.map((block) => block.id));
|
|
92
103
|
const blocks = [...incoming.blocks, ...pendingSteers.filter((block) => !known.has(block.id))];
|
|
93
|
-
|
|
104
|
+
// A snapshot never carries a provider retry. One at the retry's own position has seen nothing after it, so the wait goes on.
|
|
105
|
+
const retry = previous.providerRetry && incoming.seq === previous.seq ? { providerRetry: true } : {};
|
|
106
|
+
return { ...turnTiming(previous), ...incoming, ...retry, blocks, status: deriveStatus(blocks) };
|
|
94
107
|
};
|
|
95
108
|
|
|
96
109
|
export const reconcileActiveTurnActions = (
|
|
@@ -112,9 +125,22 @@ export const activeTurnFromSnapshot = (snapshot: AiTurnSnapshot | null): AiActiv
|
|
|
112
125
|
status: snapshot.status === "waiting_for_action" ? "waiting_for_action" : "running",
|
|
113
126
|
blocks: normalizeCustomApprovalBlocks(snapshot.blocks),
|
|
114
127
|
modelProfileId: snapshot.modelProfileId,
|
|
128
|
+
createdAt: snapshot.createdAt,
|
|
129
|
+
...(snapshot.actionWaitMs !== undefined ? { actionWaitMs: snapshot.actionWaitMs } : {}),
|
|
130
|
+
...(snapshot.waitingSince !== undefined ? { waitingSince: snapshot.waitingSince } : {}),
|
|
115
131
|
};
|
|
116
132
|
};
|
|
117
133
|
|
|
134
|
+
/** Snapshot timing survives later attempts of the same turn, which carry no timing of their own. */
|
|
135
|
+
const turnTiming = (turn: AiActiveTurn | null | undefined): Pick<AiActiveTurn, "createdAt" | "actionWaitMs" | "waitingSince"> =>
|
|
136
|
+
turn
|
|
137
|
+
? {
|
|
138
|
+
...(turn.createdAt !== undefined ? { createdAt: turn.createdAt } : {}),
|
|
139
|
+
...(turn.actionWaitMs !== undefined ? { actionWaitMs: turn.actionWaitMs } : {}),
|
|
140
|
+
...(turn.waitingSince !== undefined ? { waitingSince: turn.waitingSince } : {}),
|
|
141
|
+
}
|
|
142
|
+
: {};
|
|
143
|
+
|
|
118
144
|
/**
|
|
119
145
|
* Fold one stream event into the projection. Pure and total: the client and any
|
|
120
146
|
* test converge on the same state for the same ordered event sequence.
|
|
@@ -125,6 +151,7 @@ export const activeTurnFromSnapshot = (snapshot: AiTurnSnapshot | null): AiActiv
|
|
|
125
151
|
* stale attempts are ignored. Older senders without a baseline reset to only
|
|
126
152
|
* locally pending steering blocks.
|
|
127
153
|
* - block events apply only when strictly newer than the active turn's cursor.
|
|
154
|
+
* - `provider_retry` marks the active turn until its next event.
|
|
128
155
|
* - `turn_finished` folds the turn's persisted messages in and clears the active turn.
|
|
129
156
|
*/
|
|
130
157
|
export const reduceProjection = (state: AiChatProjection, event: AiStreamEvent): AiChatProjection => {
|
|
@@ -146,6 +173,8 @@ export const reduceProjection = (state: AiChatProjection, event: AiStreamEvent):
|
|
|
146
173
|
return reduceWireEvent(state, event);
|
|
147
174
|
};
|
|
148
175
|
|
|
176
|
+
const withoutProviderRetry = ({ providerRetry: _, ...turn }: AiActiveTurn): AiActiveTurn => turn;
|
|
177
|
+
|
|
149
178
|
export const reduceWireEvent = (state: AiChatProjection, event: AiWireEvent): AiChatProjection => {
|
|
150
179
|
const active = state.activeTurn;
|
|
151
180
|
|
|
@@ -163,6 +192,7 @@ export const reduceWireEvent = (state: AiChatProjection, event: AiWireEvent): Ai
|
|
|
163
192
|
status: deriveStatus(blocks),
|
|
164
193
|
blocks,
|
|
165
194
|
modelProfileId: event.modelProfileId,
|
|
195
|
+
...turnTiming(active?.turnId === event.turnId ? active : null),
|
|
166
196
|
},
|
|
167
197
|
};
|
|
168
198
|
}
|
|
@@ -172,20 +202,26 @@ export const reduceWireEvent = (state: AiChatProjection, event: AiWireEvent): Ai
|
|
|
172
202
|
return { ...state, messages: mergeMessages(state.messages, event.messages ?? []), activeTurn: null };
|
|
173
203
|
}
|
|
174
204
|
|
|
205
|
+
if (!active || active.turnId !== event.turnId || !isNewerWireEvent(event, active)) return state;
|
|
206
|
+
|
|
207
|
+
if (event.type === "provider_retry") {
|
|
208
|
+
return { ...state, activeTurn: { ...active, seq: event.seq, attempt: event.attempt, providerRetry: true } };
|
|
209
|
+
}
|
|
210
|
+
|
|
175
211
|
if (event.type === "message_saved") {
|
|
176
|
-
if (!active || active.turnId !== event.turnId || !isNewerWireEvent(event, active)) return state;
|
|
177
212
|
return {
|
|
178
213
|
...state,
|
|
179
214
|
messages: mergeMessages(state.messages, [event.message]),
|
|
180
|
-
activeTurn: { ...active, seq: event.seq, attempt: event.attempt },
|
|
215
|
+
activeTurn: { ...withoutProviderRetry(active), seq: event.seq, attempt: event.attempt },
|
|
181
216
|
};
|
|
182
217
|
}
|
|
183
218
|
|
|
184
219
|
// block_set / block_delta
|
|
185
|
-
if (!active || active.turnId !== event.turnId) return state;
|
|
186
|
-
if (!isNewerWireEvent(event, active)) return state;
|
|
187
220
|
const blocks = applyWireEventToBlocks(active.blocks, event);
|
|
188
|
-
return {
|
|
221
|
+
return {
|
|
222
|
+
...state,
|
|
223
|
+
activeTurn: { ...withoutProviderRetry(active), attempt: event.attempt, seq: event.seq, blocks, status: deriveStatus(blocks) },
|
|
224
|
+
};
|
|
189
225
|
};
|
|
190
226
|
|
|
191
227
|
/** Assistant/tool messages of the active turn are represented by live blocks; hide them. */
|