@keyou007/agent-bot 0.1.14-alpha.2 → 0.1.14

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +7 -4
  3. package/README.zh.md +7 -4
  4. package/dist/cli/FeishuAppConfiguration.js +4 -0
  5. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  6. package/dist/cli/controlProtocol.js.map +1 -1
  7. package/dist/cli/help.js +2 -0
  8. package/dist/cli/help.js.map +1 -1
  9. package/dist/cli.js +18 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/CommandRouter.js +9 -0
  12. package/dist/commands/CommandRouter.js.map +1 -1
  13. package/dist/feishu/CardRenderer.js +94 -15
  14. package/dist/feishu/CardRenderer.js.map +1 -1
  15. package/dist/feishu/FeishuMessageClient.js +16 -0
  16. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  17. package/dist/feishu/FeishuTurnPresenter.js +42 -6
  18. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/presentation/OutboundRouter.js +7 -0
  22. package/dist/presentation/OutboundRouter.js.map +1 -1
  23. package/dist/presentation/TurnStateReducer.js +21 -7
  24. package/dist/presentation/TurnStateReducer.js.map +1 -1
  25. package/dist/proxy/ProxySessionController.js +102 -2
  26. package/dist/proxy/ProxySessionController.js.map +1 -1
  27. package/dist/state/StateStore.js +35 -0
  28. package/dist/state/StateStore.js.map +1 -1
  29. package/npm-shrinkwrap.json +2 -2
  30. package/package.json +1 -1
  31. package/skills/agent-bot/SKILL.md +2 -0
  32. package/src/cli/FeishuAppConfiguration.ts +4 -0
  33. package/src/cli/controlProtocol.ts +8 -0
  34. package/src/cli/help.ts +2 -0
  35. package/src/cli.ts +28 -0
  36. package/src/commands/CommandRouter.ts +8 -0
  37. package/src/commands/commandTypes.ts +1 -0
  38. package/src/feishu/CardRenderer.ts +114 -12
  39. package/src/feishu/FeishuMessageClient.ts +33 -0
  40. package/src/feishu/FeishuTurnPresenter.ts +43 -6
  41. package/src/feishu/types.ts +1 -0
  42. package/src/index.ts +2 -0
  43. package/src/presentation/OutboundRouter.ts +7 -0
  44. package/src/presentation/TurnStateReducer.ts +21 -7
  45. package/src/proxy/ProxySessionController.ts +131 -3
  46. package/src/state/StateStore.ts +37 -0
@@ -42,12 +42,14 @@ export interface TurnPresentationStore {
42
42
  export interface FeishuTurnPresenterOptions {
43
43
  normalIntervalMs?: number;
44
44
  criticalGapMs?: number;
45
+ elapsedUpdateIntervalMs?: number;
45
46
  finalChunkLength?: number;
46
47
  onError?: (error: unknown) => void;
47
48
  finalRetryBackoffMs?: number[];
48
49
  }
49
50
 
50
51
  const MAX_FINAL_TABLES_PER_CARD = 5;
52
+ const DEFAULT_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
51
53
 
52
54
  interface TurnEntry {
53
55
  contextKey: string;
@@ -58,6 +60,7 @@ interface TurnEntry {
58
60
  reactionId?: string;
59
61
  reactionEmoji?: string;
60
62
  scheduler?: CardUpdateScheduler<TurnViewState>;
63
+ elapsedTimer?: ReturnType<typeof setInterval>;
61
64
  finalizing?: Promise<void>;
62
65
  lastFinalizedStatus?: TurnViewState["status"];
63
66
  }
@@ -148,6 +151,7 @@ export class FeishuTurnPresenter {
148
151
  await entry.initializing;
149
152
  return state.turnId;
150
153
  } catch (error) {
154
+ this.stopElapsedUpdates(entry);
151
155
  if (this.pendingEntries.get(sessionId) === entry) this.pendingEntries.delete(sessionId);
152
156
  this.entries.delete(state.turnId);
153
157
  throw error;
@@ -158,7 +162,15 @@ export class FeishuTurnPresenter {
158
162
  const entry = this.pendingEntries.get(sessionId);
159
163
  if (!entry) return;
160
164
  this.pendingEntries.delete(sessionId);
161
- entry.state = { ...entry.state, status: "failed", error: message, completedAt: Date.now() };
165
+ this.stopElapsedUpdates(entry);
166
+ const completedAt = Date.now();
167
+ entry.state = {
168
+ ...entry.state,
169
+ status: "failed",
170
+ error: message,
171
+ completedAt,
172
+ durationMs: Math.max(0, completedAt - entry.state.startedAt),
173
+ };
162
174
  this.store.saveTurnSnapshot(entry.state.turnId, sessionId, entry.state, entry.contextKey);
163
175
  await entry.initializing;
164
176
  try {
@@ -178,24 +190,28 @@ export class FeishuTurnPresenter {
178
190
  ): Promise<void> {
179
191
  const snapshot = this.store.getTurnSnapshot(turnId);
180
192
  if (!isTurnViewState(snapshot) || snapshot.sessionId !== sessionId || isTerminalViewStatus(snapshot.status)) return;
193
+ const completedAt = Date.now();
181
194
  const state: TurnViewState = {
182
195
  ...snapshot,
183
196
  status: "cancelled",
184
197
  progressText: message,
185
198
  activeTool: undefined,
186
199
  approval: undefined,
187
- completedAt: Date.now(),
200
+ completedAt,
201
+ durationMs: Math.max(0, completedAt - snapshot.startedAt),
188
202
  };
189
203
  this.store.saveTurnSnapshot(turnId, sessionId, state, contextKey);
190
- const delivery = this.store.getTurnDelivery(turnId);
191
- if (delivery?.progressMessageId) {
192
- await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.renderer.renderTurn(state));
193
- }
194
204
  const entry = this.entries.get(turnId);
195
205
  if (entry) {
206
+ entry.state = state;
207
+ this.stopElapsedUpdates(entry);
196
208
  this.entries.delete(turnId);
197
209
  if (this.pendingEntries.get(sessionId) === entry) this.pendingEntries.delete(sessionId);
198
210
  }
211
+ const delivery = this.store.getTurnDelivery(turnId);
212
+ if (delivery?.progressMessageId) {
213
+ await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.renderer.renderTurn(state));
214
+ }
199
215
  }
200
216
 
201
217
  async appendSteerMessage(
@@ -287,6 +303,7 @@ export class FeishuTurnPresenter {
287
303
  if (!entry.scheduler) return;
288
304
 
289
305
  if (isTerminalEvent(event)) {
306
+ this.stopElapsedUpdates(entry);
290
307
  if (entry.finalizing) await entry.finalizing;
291
308
  const terminalState = entry.state;
292
309
  if (entry.lastFinalizedStatus === terminalState.status) return;
@@ -434,10 +451,30 @@ export class FeishuTurnPresenter {
434
451
  onError: this.options.onError,
435
452
  });
436
453
  entry.scheduler.seed(sentState);
454
+ this.startElapsedUpdates(entry);
437
455
  if (entry.state !== sentState) entry.scheduler.update(entry.state, "critical");
438
456
  await this.syncThinkingCardReaction(entry);
439
457
  }
440
458
 
459
+ private startElapsedUpdates(entry: TurnEntry): void {
460
+ if (entry.elapsedTimer || isTerminalViewStatus(entry.state.status)) return;
461
+ const intervalMs = Math.max(1, this.options.elapsedUpdateIntervalMs ?? DEFAULT_ELAPSED_UPDATE_INTERVAL_MS);
462
+ entry.elapsedTimer = setInterval(() => {
463
+ if (isTerminalViewStatus(entry.state.status)) {
464
+ this.stopElapsedUpdates(entry);
465
+ return;
466
+ }
467
+ if (!entry.historySnapshot) entry.scheduler?.update(entry.state);
468
+ }, intervalMs);
469
+ entry.elapsedTimer.unref?.();
470
+ }
471
+
472
+ private stopElapsedUpdates(entry: TurnEntry): void {
473
+ if (!entry.elapsedTimer) return;
474
+ clearInterval(entry.elapsedTimer);
475
+ entry.elapsedTimer = undefined;
476
+ }
477
+
441
478
  private async finalize(entry: TurnEntry, state: TurnViewState): Promise<void> {
442
479
  let terminalCardError: unknown;
443
480
  if (!entry.historySnapshot) {
@@ -50,6 +50,7 @@ export interface ChatUpdatedEvent {
50
50
 
51
51
  export interface FeishuOutbound {
52
52
  createGroup?(input: CreateGroupInput): Promise<CreatedGroup>;
53
+ deleteGroup?(chatId: string): Promise<void>;
53
54
  addReaction?(messageId: string, emojiType: string): Promise<string | undefined>;
54
55
  deleteReaction?(messageId: string, reactionId: string): Promise<void>;
55
56
  downloadImage?(messageId: string, imageKey: string): Promise<string>;
package/src/index.ts CHANGED
@@ -403,6 +403,8 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
403
403
  return { ok: true, message: await controller.controlStopTask(request.localSessionId) };
404
404
  case "task_archive":
405
405
  return { ok: true, data: await controller.controlArchiveTask(request.localSessionId) };
406
+ case "task_dismiss":
407
+ return { ok: true, data: await controller.controlDismissTask(request.localSessionId) };
406
408
  case "task_title":
407
409
  return { ok: true, message: await controller.controlSetTaskTitle(request.localSessionId, request.title) };
408
410
  case "task_prompt":
@@ -172,6 +172,13 @@ export class OutboundRouter {
172
172
  return createGroup.call(outbound, input);
173
173
  }
174
174
 
175
+ async deleteGroup(contextKey: string, chatId: string): Promise<void> {
176
+ const outbound = this.route(contextKey).outbound;
177
+ const deleteGroup = outbound.deleteGroup;
178
+ if (!deleteGroup) throw new Error("当前消息通道不支持解散飞书群。");
179
+ await deleteGroup.call(outbound, chatId);
180
+ }
181
+
175
182
  withReplyTarget<T>(
176
183
  contextKey: string,
177
184
  target: MessageReplyTarget | undefined,
@@ -102,31 +102,45 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
102
102
  return { ...state, status: "waiting_for_approval", approval: event.request };
103
103
  case "approval_resolved":
104
104
  return { ...state, status: state.activeTool ? "tool_running" : "running", approval: undefined };
105
- case "turn_completed":
105
+ case "turn_completed": {
106
+ const durationMs = Math.max(0, event.durationMs ?? Date.now() - state.startedAt);
106
107
  return {
107
108
  ...state,
108
109
  status: "completed",
109
110
  activeTool: undefined,
110
111
  approval: undefined,
111
- completedAt: state.startedAt + (event.durationMs ?? Date.now() - state.startedAt),
112
- durationMs: event.durationMs ?? Date.now() - state.startedAt,
112
+ completedAt: state.startedAt + durationMs,
113
+ durationMs,
113
114
  finalResponse: appendGeneratedImageMarkdown(
114
115
  event.finalResponse,
115
116
  state.completedTools.flatMap((tool) =>
116
117
  tool.kind === "image_generation" && tool.imagePath ? [tool.imagePath] : []),
117
118
  ),
118
119
  };
119
- case "turn_cancelled":
120
- return { ...state, status: "cancelled", activeTool: undefined, approval: undefined, completedAt: Date.now() };
121
- case "turn_failed":
120
+ }
121
+ case "turn_cancelled": {
122
+ const completedAt = Date.now();
123
+ return {
124
+ ...state,
125
+ status: "cancelled",
126
+ activeTool: undefined,
127
+ approval: undefined,
128
+ completedAt,
129
+ durationMs: Math.max(0, completedAt - state.startedAt),
130
+ };
131
+ }
132
+ case "turn_failed": {
133
+ const completedAt = Date.now();
122
134
  return {
123
135
  ...state,
124
136
  status: "failed",
125
137
  activeTool: undefined,
126
138
  approval: undefined,
127
- completedAt: Date.now(),
139
+ completedAt,
140
+ durationMs: Math.max(0, completedAt - state.startedAt),
128
141
  error: bound(event.message),
129
142
  };
143
+ }
130
144
  }
131
145
  }
132
146
 
@@ -146,6 +146,7 @@ const HELP_COMMAND_SECTIONS: Array<{
146
146
  usage: "[序号或任务 ID]",
147
147
  description: "归档当前或指定任务",
148
148
  },
149
+ { command: "/dismiss", description: "归档当前任务并解散当前群" },
149
150
  {
150
151
  command: "/switch",
151
152
  usage: "[序号或任务 ID]",
@@ -498,7 +499,7 @@ export class ProxySessionController {
498
499
  if (isQueueIndependentCommand(command)) {
499
500
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
500
501
  try {
501
- if (command.type !== "forkgroup") await this.ensureThreadFork(message);
502
+ if (command.type !== "forkgroup" && command.type !== "dismiss") await this.ensureThreadFork(message);
502
503
  await this.execute(
503
504
  message.contextKey,
504
505
  command,
@@ -667,6 +668,20 @@ export class ProxySessionController {
667
668
  source: "sessions_card",
668
669
  });
669
670
  await this.refreshSessionsCardFromAction(scopedAction);
671
+ } else if (kind === "group_dismiss_keep") {
672
+ this.assertDismissGroupRequester(scopedAction);
673
+ await this.outbound.updateInteractiveCard(
674
+ contextKey,
675
+ requiredCardMessageId(scopedAction.messageId),
676
+ this.cardRenderer.renderDismissGroupKept(),
677
+ );
678
+ } else if (kind === "group_dismiss_confirm") {
679
+ this.assertDismissGroupRequester(scopedAction);
680
+ await this.dismissGroupAndArchiveTask(
681
+ contextKey,
682
+ String(scopedAction.value.sessionId ?? ""),
683
+ "command",
684
+ );
670
685
  } else if (kind === "session_stop") {
671
686
  const sessionId = String(scopedAction.value.sessionId ?? "");
672
687
  await this.stopSessionReference(contextKey, sessionId);
@@ -827,6 +842,21 @@ export class ProxySessionController {
827
842
  });
828
843
  }
829
844
 
845
+ async controlDismissTask(localSessionId: string): Promise<{
846
+ localSessionId: string;
847
+ remoteSessionId: string;
848
+ title: string;
849
+ chatId: string;
850
+ }> {
851
+ const record = this.store.getSession(localSessionId);
852
+ if (!record) throw new Error(`Task not found: ${localSessionId}`);
853
+ return this.dismissGroupAndArchiveTask(
854
+ this.controlSessionContextKey(record),
855
+ localSessionId,
856
+ "cli",
857
+ );
858
+ }
859
+
830
860
  async controlGetTaskStatus(localSessionId: string): Promise<{
831
861
  session: SessionRecord;
832
862
  snapshot?: TurnViewState;
@@ -1493,6 +1523,9 @@ export class ProxySessionController {
1493
1523
  });
1494
1524
  return;
1495
1525
  }
1526
+ case "dismiss":
1527
+ await this.openDismissGroupCard(contextKey, userId);
1528
+ return;
1496
1529
  case "status":
1497
1530
  await this.status(contextKey, command.sessionId);
1498
1531
  return;
@@ -4780,7 +4813,7 @@ export class ProxySessionController {
4780
4813
  private async archiveSessionReference(
4781
4814
  contextKey: string,
4782
4815
  reference: string,
4783
- options: { announce: boolean; source: "cli" | "command" | "sessions_card" },
4816
+ options: { announce: boolean; source: "cli" | "command" | "dismiss" | "sessions_card" },
4784
4817
  ): Promise<{ localSessionId: string; remoteSessionId: string; title: string }> {
4785
4818
  const taskId = this.resolveSessionReference(contextKey, reference);
4786
4819
  const direct = this.store.getSession(taskId);
@@ -4838,6 +4871,101 @@ export class ProxySessionController {
4838
4871
  };
4839
4872
  }
4840
4873
 
4874
+ private async openDismissGroupCard(contextKey: string, userId?: string): Promise<void> {
4875
+ const groupContextKey = this.dismissibleGroupContextKey(contextKey);
4876
+ if (!userId) throw new Error("无法识别当前用户,不能发起解散群操作。");
4877
+ const session = this.requireCurrentSession(groupContextKey);
4878
+ await this.outbound.sendInteractiveCard(groupContextKey, this.cardRenderer.renderDismissGroupConfirmation({
4879
+ contextKey: groupContextKey,
4880
+ sessionId: session.localSessionId,
4881
+ taskTitle: session.title ?? session.remoteSessionId ?? session.localSessionId,
4882
+ requestedBy: userId,
4883
+ }));
4884
+ }
4885
+
4886
+ private assertDismissGroupRequester(action: CardAction): void {
4887
+ const requestedBy = String(action.value.requestedBy ?? "");
4888
+ if (!requestedBy || !action.userId || action.userId !== requestedBy) {
4889
+ throw new Error("只有发起解散操作的用户可以点击这张卡片。");
4890
+ }
4891
+ }
4892
+
4893
+ private async dismissGroupAndArchiveTask(
4894
+ contextKey: string,
4895
+ localSessionId: string,
4896
+ source: "cli" | "command",
4897
+ ): Promise<{ localSessionId: string; remoteSessionId: string; title: string; chatId: string }> {
4898
+ const groupContextKey = this.dismissibleGroupContextKey(contextKey);
4899
+ const session = this.store.getSessionForContext(localSessionId, groupContextKey);
4900
+ if (!session) throw new Error("解散群卡片已失效,请重新发送 /dismiss。");
4901
+
4902
+ const currentSessionId = this.store.getUserContext(groupContextKey)?.currentSessionId;
4903
+ if (session.status !== "closed" && currentSessionId !== localSessionId) {
4904
+ throw new Error("当前任务已经切换,解散群卡片已失效,请重新发送 /dismiss。");
4905
+ }
4906
+ this.assertNoOtherActiveGroupTasks(groupContextKey, localSessionId);
4907
+
4908
+ const archived = session.status === "closed"
4909
+ ? {
4910
+ localSessionId: session.localSessionId,
4911
+ remoteSessionId: session.remoteSessionId ?? session.acpSessionId ?? "",
4912
+ title: session.title ?? session.remoteSessionId ?? session.localSessionId,
4913
+ }
4914
+ : await this.archiveSessionReference(groupContextKey, localSessionId, {
4915
+ announce: false,
4916
+ source: "dismiss",
4917
+ });
4918
+ const chatId = groupContextKey.slice("chat_id:".length);
4919
+ try {
4920
+ await this.outbound.deleteGroup(groupContextKey, chatId);
4921
+ } catch (error) {
4922
+ const message = error instanceof Error ? error.message : String(error);
4923
+ throw new Error(`当前任务已归档,但解散群失败:${message}`);
4924
+ }
4925
+
4926
+ const detachedSessionIds = this.store.removeChatContext(groupContextKey);
4927
+ for (const detachedSessionId of detachedSessionIds) {
4928
+ const linked = this.store.getSession(detachedSessionId);
4929
+ const routedContextKey = this.outbound.getSessionContextKey(detachedSessionId) ?? linked?.contextKey;
4930
+ if (routedContextKey && baseChatContextKey(routedContextKey) === groupContextKey) {
4931
+ this.outbound.unregisterSession(detachedSessionId);
4932
+ }
4933
+ }
4934
+ this.store.audit(groupContextKey, "group_dismissed", {
4935
+ chatId,
4936
+ localSessionId,
4937
+ remoteSessionId: archived.remoteSessionId,
4938
+ source,
4939
+ });
4940
+ return { ...archived, chatId };
4941
+ }
4942
+
4943
+ private assertNoOtherActiveGroupTasks(contextKey: string, dismissedSessionId: string): void {
4944
+ const active = this.store.listSessionsForChat(contextKey).find((session) => (
4945
+ session.localSessionId !== dismissedSessionId
4946
+ && session.status !== "closed"
4947
+ && (
4948
+ session.status === "running"
4949
+ || Boolean(this.runtimes.forAgent(session.agentName).getSession(session.localSessionId)?.activeTurnId)
4950
+ || this.store.countQueuedPrompts(session.localSessionId) > 0
4951
+ )
4952
+ ));
4953
+ if (active) {
4954
+ throw new Error(`群内任务仍在执行或有排队消息,暂时不能解散:${active.title ?? active.localSessionId}`);
4955
+ }
4956
+ }
4957
+
4958
+ private dismissibleGroupContextKey(contextKey: string): string {
4959
+ if (isThreadContextKey(contextKey)) {
4960
+ throw new Error("/dismiss 不能在话题中使用,请回到群聊正文后重试。");
4961
+ }
4962
+ const groupContextKey = baseChatContextKey(contextKey);
4963
+ if (!groupContextKey.startsWith("chat_id:") || this.store.getChatContext(groupContextKey)?.chatType !== "group") {
4964
+ throw new Error("/dismiss 仅适用于普通群聊。");
4965
+ }
4966
+ return groupContextKey;
4967
+ }
4968
+
4841
4969
  private resolveSessionReference(contextKey: string, reference?: string): string {
4842
4970
  if (reference === undefined) {
4843
4971
  const previous = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!).previousSessionId;
@@ -5763,7 +5891,7 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
5763
5891
  }
5764
5892
 
5765
5893
  function isQueueIndependentCommand(command: Command): boolean {
5766
- if (["archive", "stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type)) return true;
5894
+ if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type)) return true;
5767
5895
  if (command.type === "agent") return command.agent === undefined;
5768
5896
  if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
5769
5897
  return false;
@@ -344,6 +344,31 @@ export class StateStore {
344
344
  return rows.map(mapChatContext);
345
345
  }
346
346
 
347
+ removeChatContext(contextKey: string): string[] {
348
+ const threadPrefix = `${contextKey}:thread_id:`;
349
+ const linked = this.db.prepare(`
350
+ SELECT DISTINCT local_session_id
351
+ FROM context_sessions
352
+ WHERE context_key = ? OR instr(context_key, ?) = 1
353
+ `).all(contextKey, threadPrefix) as Array<{ local_session_id: string }>;
354
+ const remove = this.db.transaction(() => {
355
+ this.db.prepare(`
356
+ DELETE FROM context_sessions
357
+ WHERE context_key = ? OR instr(context_key, ?) = 1
358
+ `).run(contextKey, threadPrefix);
359
+ this.db.prepare(`
360
+ DELETE FROM user_contexts
361
+ WHERE context_key = ? OR instr(context_key, ?) = 1
362
+ `).run(contextKey, threadPrefix);
363
+ this.db.prepare(`
364
+ DELETE FROM chat_contexts
365
+ WHERE context_key = ? OR instr(context_key, ?) = 1
366
+ `).run(contextKey, threadPrefix);
367
+ });
368
+ remove();
369
+ return linked.map((row) => row.local_session_id);
370
+ }
371
+
347
372
  enqueuePrompt(input: {
348
373
  promptId: string;
349
374
  localSessionId: string;
@@ -1233,6 +1258,18 @@ export class StateStore {
1233
1258
  return rows.map((row) => ({ ...mapSession(row), contextKey }));
1234
1259
  }
1235
1260
 
1261
+ listSessionsForChat(contextKey: string): SessionRecord[] {
1262
+ const threadPrefix = `${contextKey}:thread_id:`;
1263
+ const rows = this.db.prepare(`
1264
+ SELECT DISTINCT sessions.*
1265
+ FROM context_sessions
1266
+ JOIN sessions ON sessions.local_session_id = context_sessions.local_session_id
1267
+ WHERE context_sessions.context_key = ? OR instr(context_sessions.context_key, ?) = 1
1268
+ ORDER BY sessions.updated_at DESC
1269
+ `).all(contextKey, threadPrefix) as SessionRow[];
1270
+ return rows.map(mapSession);
1271
+ }
1272
+
1236
1273
  listAllSessions(): SessionRecord[] {
1237
1274
  const rows = this.db
1238
1275
  .prepare("SELECT * FROM sessions ORDER BY updated_at DESC, created_at DESC")