@morlay/ui-conversation-message-actions 0.0.12 → 0.0.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/ui-conversation-message-actions",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Rewind / retry / fork orchestration (host) plus conversation.chat.node renderer replacement (browser) — message edit / retry actions on user bubbles.",
5
5
  "keywords": [
6
6
  "conversation-chat-node",
@@ -26,22 +26,19 @@
26
26
  ],
27
27
  "type": "module",
28
28
  "exports": {
29
- ".": {
30
- "types": "./dist/index.d.mts",
31
- "default": "./dist/index.mjs"
32
- },
33
- "./client": "./dist/client.js",
34
- "./invariant": {
35
- "types": "./dist/invariant.d.mts",
36
- "default": "./dist/invariant.mjs"
37
- },
29
+ ".": "./dist/index.mjs",
30
+ "./invariant": "./dist/invariant.mjs",
31
+ "./plan": "./dist/plan.mjs",
32
+ "./testing": "./dist/testing.mjs",
38
33
  "./package.json": "./package.json",
39
- "./cordis.patch.yml": "./cordis.patch.yml"
34
+ "./cordis.patch.yml": "./cordis.patch.yml",
35
+ "./client": "./dist/client.js"
40
36
  },
41
37
  "dependencies": {
42
- "@morlay/session-branch": "^0.0.2"
38
+ "@morlay/session-branch": "^0.0.3"
43
39
  },
44
40
  "devDependencies": {
41
+ "@morlay/session-rdb": "^0.0.13",
45
42
  "@types/react": "^19.2.18",
46
43
  "@types/react-dom": "^19.2.5",
47
44
  "lightningcss": "^1.32.0",
@@ -81,6 +78,29 @@
81
78
  ]
82
79
  }
83
80
  },
81
+ "inlinedDependencies": {
82
+ "@standard-schema/spec": "1.1.0",
83
+ "@vitest/pretty-format": "4.1.11",
84
+ "@vitest/runner": "4.1.11",
85
+ "@vitest/utils": "4.1.11",
86
+ "drizzle-orm": "1.0.0-rc.5-ab785fc",
87
+ "fflate": "0.8.3",
88
+ "pg": "8.23.0",
89
+ "pg-cloudflare": "1.4.0",
90
+ "pg-connection-string": "2.14.0",
91
+ "pg-int8": "1.0.1",
92
+ "pg-pool": "3.14.0",
93
+ "pg-protocol": "1.16.0",
94
+ "pg-types": "2.2.0",
95
+ "pgpass": "1.0.5",
96
+ "postgres-array": "2.0.0",
97
+ "postgres-bytea": "1.0.1",
98
+ "postgres-date": "1.0.7",
99
+ "postgres-interval": "1.2.0",
100
+ "split2": "4.2.0",
101
+ "tinyrainbow": "3.1.1",
102
+ "xtend": "4.0.2"
103
+ },
84
104
  "scripts": {
85
105
  "build": "pnpm exec tsdown"
86
106
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Service, type Context } from "@deepseek-ai/cordis";
2
- import type { AssistantMessage, ContentBlock, UserMessage } from "@deepseek-ai/dsh-llm";
2
+ import type { AssistantMessage, UserMessage } from "@deepseek-ai/dsh-llm";
3
3
  import type {
4
4
  Session,
5
5
  SessionEvent,
@@ -7,16 +7,11 @@ import type {
7
7
  SurfaceEventType,
8
8
  SurfaceIntent,
9
9
  } from "@deepseek-ai/dsh-session";
10
- import { randomUUID } from "node:crypto";
11
10
  import {
12
- SESSION_BRANCH_VERSION_SCHEMA,
13
11
  SessionBranchError,
14
12
  balanceRewindPrefix,
15
13
  type BranchBoundary,
16
14
  type BranchTimeline,
17
- type EditableBlockKind,
18
- type SessionBranchEffect,
19
- type SessionBranchVersionEvent,
20
15
  } from "@morlay/session-branch";
21
16
  import type {
22
17
  EditOperation,
@@ -33,6 +28,14 @@ import {
33
28
  type SessionEditorTimeline,
34
29
  toTimelinePayload,
35
30
  } from "./shared.ts";
31
+ import {
32
+ closedTurns,
33
+ editableMessages,
34
+ editPlan,
35
+ retryPlan,
36
+ rerollPlan,
37
+ retryableTurns,
38
+ } from "./plan.ts";
36
39
 
37
40
  export type { CascadePolicy, EditableBlockKind } from "@morlay/session-branch";
38
41
  export type {
@@ -60,6 +63,16 @@ declare module "@deepseek-ai/cordis" {
60
63
  export interface EditorAgent {
61
64
  readonly session: Session;
62
65
  followup(message: UserMessage): void;
66
+ /** 等待 agent 到达 quiescence(当前 turn/任务结束后 resolve)。 */
67
+ whenIdle(): Promise<void>;
68
+ /**
69
+ * 队列中是否有待处理输入(next-turn / next-step)。rewind 截断会删除这些
70
+ * 输入所属轮次的事件,残留的 inbox 状态会与截断后的 log 失配,使后续
71
+ * splice 落库后无法从日志重放——编辑前必须清空。
72
+ */
73
+ readonly inboxPending: boolean;
74
+ /** 清空待处理输入(落库 canceled splice,须在 rewind 前调用)。 */
75
+ clearInbox(): void;
63
76
  }
64
77
 
65
78
  export interface EditorAgentHandle {
@@ -87,306 +100,6 @@ export interface EditorAgentRegistry {
87
100
  }): Promise<EditorAgentHandle>;
88
101
  }
89
102
 
90
- interface ClosedTurn {
91
- turn: number;
92
- startSeq: number;
93
-
94
- endSeq?: number;
95
-
96
- closed: boolean;
97
- user?: SessionEvent<"user/message">;
98
- assistants: SessionEvent<"assistant/message">[];
99
- }
100
-
101
- function closedTurns(events: readonly SessionEvent[]): ClosedTurn[] {
102
- const result: ClosedTurn[] = [];
103
- let current: Omit<ClosedTurn, "endSeq" | "closed"> | undefined;
104
- for (const event of events) {
105
- if (event.type === "turn/start") {
106
- current = { turn: event.data.turn, startSeq: event.seq, assistants: [] };
107
- continue;
108
- }
109
- if (current === undefined) continue;
110
- if (
111
- event.type === "user/message" &&
112
- current.user === undefined &&
113
- event.data.source.kind === "user"
114
- ) {
115
- current.user = event;
116
- continue;
117
- }
118
- if (event.type === "assistant/message" && event.data.turn === current.turn) {
119
- current.assistants.push(event);
120
- continue;
121
- }
122
- if (event.type === "turn/end" && event.data.turn === current.turn) {
123
- result.push({ ...current, endSeq: event.seq, closed: true });
124
- current = undefined;
125
- }
126
- }
127
- // 未闭合轮次(无 turn/end)也保留:user 消息已落定,编辑时 rewind 到该
128
- // 消息(exclusive drop)重放即可。
129
- if (current !== undefined) result.push({ ...current, closed: false });
130
- return result;
131
- }
132
-
133
- function isTextualBlock(
134
- block: ContentBlock | undefined,
135
- ): block is Extract<ContentBlock, { type: "text" | "reasoning" }> {
136
- return block?.type === "text" || block?.type === "reasoning";
137
- }
138
-
139
- function userText(message: UserMessage): string {
140
- return message.content
141
- .filter((block): block is Extract<ContentBlock, { type: "text" }> => block.type === "text")
142
- .map((block) => block.text)
143
- .join("\n");
144
- }
145
-
146
- function cloneUser(
147
- message: UserMessage,
148
- content: ContentBlock[] = structuredClone(message.content),
149
- ): UserMessage {
150
- return Object.freeze({
151
- id: randomUUID(),
152
- role: "user",
153
- content: Object.freeze(content),
154
- source: Object.freeze({ kind: "user" }),
155
- }) as UserMessage;
156
- }
157
-
158
- function replaceTextBlock(
159
- content: readonly ContentBlock[],
160
- blockIndex: number,
161
- text: string,
162
- ): ContentBlock[] {
163
- const block = content[blockIndex];
164
- if (!isTextualBlock(block))
165
- throw new SessionBranchError("所选内容块不是可编辑文本。", "INVALID_BOUNDARY");
166
- return content.map((candidate, index) =>
167
- index === blockIndex ? ({ ...candidate, text } as ContentBlock) : structuredClone(candidate),
168
- );
169
- }
170
-
171
- function editableMessages(turns: readonly ClosedTurn[]): EditableMessageBlock[] {
172
- const result: EditableMessageBlock[] = [];
173
- for (const turn of turns) {
174
- if (turn.user !== undefined) {
175
- for (const [blockIndex, block] of turn.user.data.content.entries()) {
176
- if (block.type !== "text") continue;
177
- result.push({
178
- key: `${String(turn.user.seq)}:${String(blockIndex)}`,
179
- turn: turn.turn,
180
- eventSeq: turn.user.seq,
181
- blockIndex,
182
- kind: "user",
183
- text: block.text,
184
- time: turn.user.time,
185
- });
186
- }
187
- }
188
- // 未闭合轮次的助手消息是流式 partial,不可编辑。
189
- if (!turn.closed) continue;
190
- for (const event of turn.assistants) {
191
- for (const [blockIndex, block] of event.data.message.content.entries()) {
192
- if (!isTextualBlock(block)) continue;
193
- result.push({
194
- key: `${String(event.seq)}:${String(blockIndex)}`,
195
- turn: turn.turn,
196
- eventSeq: event.seq,
197
- blockIndex,
198
- kind: block.type === "reasoning" ? "assistant.reasoning" : "assistant.response",
199
- text: block.text,
200
- time: event.time,
201
- });
202
- }
203
- }
204
- }
205
- return result;
206
- }
207
-
208
- function retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[] {
209
- return turns.flatMap((turn): RetryableTurn[] =>
210
- // 未闭合轮次无已落定回复可重生成,不可重试。
211
- turn.user === undefined || !turn.closed
212
- ? []
213
- : [
214
- {
215
- turn: turn.turn,
216
- userEventSeq: turn.user.seq,
217
- preview: userText(turn.user.data),
218
- time: turn.user.time,
219
- },
220
- ],
221
- );
222
- }
223
-
224
- function downstreamUsers(turns: readonly ClosedTurn[], start: number): UserMessage[] {
225
- return turns
226
- .slice(start)
227
- .flatMap((turn): UserMessage[] => (turn.user === undefined ? [] : [cloneUser(turn.user.data)]));
228
- }
229
-
230
- function assistantReplacement(
231
- event: SessionEvent<"assistant/message">,
232
- blockIndex: number,
233
- text: string,
234
- ): AssistantMessage {
235
- const replaced = replaceTextBlock(event.data.message.content, blockIndex, text).filter(
236
- (block) => block.type === "text" || block.type === "reasoning",
237
- );
238
- return Object.freeze({
239
- id: randomUUID(),
240
- role: "assistant",
241
- content: Object.freeze(replaced),
242
- source: Object.freeze({
243
- kind: "model",
244
- provider: event.data.message.source.provider,
245
- model: event.data.message.source.model,
246
- }),
247
- }) as AssistantMessage;
248
- }
249
-
250
- interface OperationPlan {
251
- anchorSeq: number;
252
-
253
- rewindBoundary?: number;
254
- version: SessionBranchVersionEvent;
255
-
256
- manualTurn?: { turn: number; user: UserMessage; assistant: AssistantMessage };
257
-
258
- queuedUsers: UserMessage[];
259
- }
260
-
261
- function pairVersionEffect(
262
- sourceSessionId: SessionId,
263
- effect: Omit<SessionBranchEffect, "id">,
264
- ): SessionBranchVersionEvent {
265
- return {
266
- schemaVersion: SESSION_BRANCH_VERSION_SCHEMA,
267
- effect: { ...effect, id: randomUUID() },
268
- inverse: { kind: "restore-version", sessionId: sourceSessionId },
269
- };
270
- }
271
-
272
- function editPlan(operation: EditOperation, turns: readonly ClosedTurn[]): OperationPlan {
273
- const turnIndex = turns.findIndex(
274
- (turn) =>
275
- operation.eventSeq > turn.startSeq &&
276
- (turn.endSeq === undefined || operation.eventSeq < turn.endSeq),
277
- );
278
- const turn = turns[turnIndex];
279
- if (turn === undefined)
280
- throw new SessionBranchError("所选消息不属于已落定回合。", "INVALID_BOUNDARY");
281
- const event =
282
- turn.user?.seq === operation.eventSeq
283
- ? turn.user
284
- : turn.assistants.find((candidate) => candidate.seq === operation.eventSeq);
285
- if (event === undefined)
286
- throw new SessionBranchError("所选消息不存在或不可编辑。", "INVALID_BOUNDARY");
287
-
288
- if (event.type === "user/message") {
289
- const before = event.data.content[operation.blockIndex];
290
- if (before?.type !== "text")
291
- throw new SessionBranchError("所选用户消息块不是文本。", "INVALID_BOUNDARY");
292
- const edited = cloneUser(
293
- event.data,
294
- replaceTextBlock(event.data.content, operation.blockIndex, operation.text),
295
- );
296
- const later = operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : [];
297
- return {
298
- anchorSeq: turn.startSeq,
299
- // 未闭合轮次:rewind 到该 user 消息(exclusive drop),由编辑版替换;
300
- // 闭合轮次走前一轮 turn/end 的 inclusive 语义。
301
- ...(turn.closed ? {} : { rewindBoundary: event.seq }),
302
- version: pairVersionEffect(operation.sessionId, {
303
- operation: "edit",
304
- cascade: operation.cascade,
305
- targetTurn: turn.turn,
306
- targetEventSeq: event.seq,
307
- targetBlockIndex: operation.blockIndex,
308
- blockKind: "user",
309
- before: before.text,
310
- after: operation.text,
311
- }),
312
- queuedUsers: [edited, ...later],
313
- };
314
- }
315
-
316
- // 未闭合轮次的助手消息是流式 partial,无最终内容可编辑。
317
- if (!turn.closed)
318
- throw new SessionBranchError("未闭合轮次的助手消息不可编辑。", "INVALID_BOUNDARY");
319
- const before = event.data.message.content[operation.blockIndex];
320
- if (!isTextualBlock(before))
321
- throw new SessionBranchError("所选助手消息块不是文本或思考。", "INVALID_BOUNDARY");
322
- const blockKind: EditableBlockKind =
323
- before.type === "reasoning" ? "assistant.reasoning" : "assistant.response";
324
- if (turn.user === undefined)
325
- throw new SessionBranchError("所选助手消息没有可重建的用户输入。", "INVALID_BOUNDARY");
326
- return {
327
- anchorSeq: turn.startSeq,
328
- version: pairVersionEffect(operation.sessionId, {
329
- operation: "edit",
330
- cascade: operation.cascade,
331
- targetTurn: turn.turn,
332
- targetEventSeq: event.seq,
333
- targetBlockIndex: operation.blockIndex,
334
- blockKind,
335
- before: before.text,
336
- after: operation.text,
337
- }),
338
- manualTurn: {
339
- turn: turn.turn,
340
- user: cloneUser(turn.user.data),
341
- assistant: assistantReplacement(event, operation.blockIndex, operation.text),
342
- },
343
- queuedUsers: operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : [],
344
- };
345
- }
346
-
347
- function retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[]): OperationPlan {
348
- const turnIndex = turns.findIndex((turn) => turn.turn === operation.turn);
349
- const turn = turns[turnIndex];
350
- if (turn?.user === undefined)
351
- throw new SessionBranchError("所选回合没有可重放的用户输入。", "INVALID_BOUNDARY");
352
- return {
353
- anchorSeq: turn.startSeq,
354
- version: pairVersionEffect(operation.sessionId, {
355
- operation: "retry",
356
- cascade: operation.cascade,
357
- targetTurn: turn.turn,
358
- targetEventSeq: turn.user.seq,
359
- }),
360
- queuedUsers:
361
- operation.cascade === "preserve"
362
- ? downstreamUsers(turns, turnIndex)
363
- : [cloneUser(turn.user.data)],
364
- };
365
- }
366
-
367
- function rerollPlan(operation: RerollOperation, turns: readonly ClosedTurn[]): OperationPlan {
368
- for (let index = turns.length - 1; index >= 0; index -= 1) {
369
- const turn = turns[index];
370
- // 未闭合轮次无已落定的助手回复可重生成。
371
- if (turn?.user === undefined || !turn.closed) continue;
372
- const target = turn.assistants.findLast((event) =>
373
- event.data.message.content.some(isTextualBlock),
374
- );
375
- if (target === undefined) continue;
376
- return {
377
- anchorSeq: turn.startSeq,
378
- version: pairVersionEffect(operation.sessionId, {
379
- operation: "reroll",
380
- cascade: "truncate",
381
- targetTurn: turn.turn,
382
- targetEventSeq: target.seq,
383
- }),
384
- queuedUsers: [cloneUser(turn.user.data)],
385
- };
386
- }
387
- throw new SessionBranchError("当前会话没有可重生成的已落定助手回复。", "INVALID_BOUNDARY");
388
- }
389
-
390
103
  function appendLogSeedEvent(
391
104
  events: SessionEvent[],
392
105
  type: string,
@@ -574,8 +287,16 @@ export class SessionEditor extends Service {
574
287
  appendLogSeedEvent(seedSuffix, "session-branch/version", plan.version, true);
575
288
  if (plan.manualTurn !== undefined) appendManualTurn(seedSuffix, plan.manualTurn);
576
289
 
577
- // 就地编辑:不创建新会话、不改变 id。先 rewind 截断到目标轮之前的闭合
578
- // 边界,再 append 版本效果与重放输入,最后(可选)驱动 agent 重放。
290
+ // 就地编辑:不创建新会话、不改变 id。需要重放排队输入时,先在 rewind
291
+ // 前确保 agent 就绪(live agent 等其停;cold 先 resume)——rewind 截断后
292
+ // agent 无法再以完整会话 resume,且重放失败不应让截断静默丢弃内容。
293
+ const replay = await this.prepareReplay(
294
+ operation.sessionId,
295
+ plan.queuedUsers,
296
+ signal,
297
+ headerConfig,
298
+ );
299
+
579
300
  const turnIndex = turns.findIndex((turn) => turn.startSeq === plan.anchorSeq);
580
301
  const boundary =
581
302
  plan.rewindBoundary !== undefined
@@ -608,18 +329,19 @@ export class SessionEditor extends Service {
608
329
  }
609
330
  }
610
331
 
611
- // 可选增强:agent 驱动(重放排队用户输入)。缺失 agents 服务时退化为
612
- // durable 的就地版本。
613
- const queuedTurns = await this.driveAgent(
614
- operation.sessionId,
615
- plan.queuedUsers,
616
- signal,
617
- headerConfig,
618
- );
332
+ // 发起新的 user prompt:把排队输入交给就绪的 agent(rewind 后其 session
333
+ // 已被截断,followup 基于截断后历史开新轮重放)。无 agent(agents 服务
334
+ // 缺失)时退化为已 durable 的就地版本。
335
+ let queuedTurns = 0;
336
+ if (replay.agent !== undefined && plan.queuedUsers.length > 0) {
337
+ for (const message of plan.queuedUsers) replay.agent.followup(message);
338
+ await this.ctx.sessions.flush(replay.agent.session);
339
+ queuedTurns = plan.queuedUsers.length;
340
+ }
619
341
  return {
620
342
  sessionId: operation.sessionId,
621
343
  queuedTurns,
622
- // 操作后是否仍有 live owner(driveAgent 可能 resume 出 agent)。
344
+ // 操作后是否仍有 live owner(prepareReplay 可能 resume 出 agent)。
623
345
  live: this.ctx.sessions.get(operation.sessionId) !== undefined,
624
346
  };
625
347
  }
@@ -641,16 +363,34 @@ export class SessionEditor extends Service {
641
363
  return (await branch.readRawEvents(sessionId, signal)).events;
642
364
  }
643
365
 
644
- private async driveAgent(
366
+ /**
367
+ * rewind 前确保 agent 可驱动重放:live agent 先等待其停下(rewind 会截断其
368
+ * session 内存 log,须在 quiescence 后执行);cold 会话先 resume 出驻留
369
+ * agent(此时会话完整,resume 的 prepare 不与截断冲突)。agents 服务缺失或
370
+ * 无需重放时返回空——调用方退化为就地截断版本。
371
+ */
372
+ private async prepareReplay(
645
373
  sessionId: SessionId,
646
374
  queuedUsers: readonly UserMessage[],
647
- signal?: AbortSignal,
375
+ signal: AbortSignal | undefined,
648
376
  headerConfig?: { provider?: string; model?: string; maxTokens?: number },
649
- ): Promise<number> {
650
- if (queuedUsers.length === 0) return 0;
377
+ ): Promise<{ agent: EditorAgent | undefined }> {
378
+ if (queuedUsers.length === 0) return { agent: undefined };
651
379
  signal?.throwIfAborted();
652
380
  const agents = this.ctx.get("agents") as EditorAgentRegistry | undefined;
653
- if (agents === undefined) return 0;
381
+ if (agents === undefined) return { agent: undefined };
382
+ const existing = agents.get(sessionId);
383
+ if (existing !== undefined) {
384
+ await existing.whenIdle();
385
+ signal?.throwIfAborted();
386
+ // 清空 inbox 残留(rewind 将删除这些输入所属轮次的事件;残留消息若
387
+ // 不清空,agent 后续 splice 落库后无法从日志重放——inbox 增量投影
388
+ // 假设 log 只 append)。
389
+ if (existing.inboxPending) existing.clearInbox();
390
+ return { agent: existing };
391
+ }
392
+ // cold:resume 已持久化会话(create 会因「已存在持久化日志」失败)。
393
+ // resume 失败是硬错误:rewind 尚未发生,编辑保持原子(不截断不丢数据)。
654
394
  const provider = headerConfig?.provider ?? "";
655
395
  const model = headerConfig?.model ?? "";
656
396
  if (provider.length === 0 || model.length === 0) {
@@ -660,51 +400,25 @@ export class SessionEditor extends Service {
660
400
  .config;
661
401
  const fallbackProvider = config?.provider ?? "";
662
402
  const fallbackModel = config?.model ?? "";
663
- if (fallbackProvider.length === 0 || fallbackModel.length === 0) return 0;
664
- return this.queueThroughAgent(
665
- sessionId,
666
- queuedUsers,
667
- signal,
668
- fallbackProvider,
669
- fallbackModel,
670
- );
671
- }
672
- return this.queueThroughAgent(sessionId, queuedUsers, signal, provider, model);
673
- }
674
-
675
- private async queueThroughAgent(
676
- sessionId: SessionId,
677
- queuedUsers: readonly UserMessage[],
678
- signal: AbortSignal | undefined,
679
- provider: string,
680
- model: string,
681
- ): Promise<number> {
682
- const agents = this.ctx.get("agents") as EditorAgentRegistry | undefined;
683
- if (agents === undefined) return 0;
684
- // 现有 live agent:直接排队输入(其 session 内存已被 rewind 截断,
685
- // followup 基于截断后历史重放,不改 id、不重建)。
686
- const existing = agents.get(sessionId);
687
- if (existing !== undefined) {
688
- for (const message of queuedUsers) existing.followup(message);
689
- await this.ctx.sessions.flush(existing.session);
690
- return queuedUsers.length;
691
- }
692
- // cold:resume 已持久化会话(create 会因「已存在持久化日志」失败)。
693
- // resume 后 agent 驻留,不 dispose(dispose 会破坏客户端打开的窗口)。
694
- const handle = await agents
695
- .resume({ resumeSessionId: sessionId, agentOptions: { provider, model } })
696
- .catch((error: unknown) => {
697
- // agent 组合失败不应使已 durable 的版本失效:退化为持久化版本。
698
- this.ctx.logger.warn(
699
- "session-editor: agent resume failed (%s); version remains durable",
700
- String(error),
403
+ if (fallbackProvider.length === 0 || fallbackModel.length === 0) {
404
+ throw new SessionBranchError(
405
+ "无法重放:会话没有可解析的模型配置。",
406
+ "INVALID_BOUNDARY",
701
407
  );
702
- return undefined;
408
+ }
409
+ const handle = await agents.resume({
410
+ resumeSessionId: sessionId,
411
+ agentOptions: { provider: fallbackProvider, model: fallbackModel },
703
412
  });
704
- if (handle === undefined) return 0;
705
- for (const message of queuedUsers) handle.agent.followup(message);
706
- await this.ctx.sessions.flush(handle.agent.session);
707
- return queuedUsers.length;
413
+ signal?.throwIfAborted();
414
+ return { agent: handle.agent };
415
+ }
416
+ const handle = await agents.resume({
417
+ resumeSessionId: sessionId,
418
+ agentOptions: { provider, model },
419
+ });
420
+ signal?.throwIfAborted();
421
+ return { agent: handle.agent };
708
422
  }
709
423
  }
710
424