@keyou007/agent-bot 0.1.19-alpha.0 → 0.1.19-alpha.1

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 (42) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +3 -0
  3. package/README.zh.md +3 -0
  4. package/dist/cli/controlProtocol.js.map +1 -1
  5. package/dist/cli/help.js +2 -0
  6. package/dist/cli/help.js.map +1 -1
  7. package/dist/cli.js +20 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/codex/CodexProcessManager.js +48 -0
  10. package/dist/codex/CodexProcessManager.js.map +1 -1
  11. package/dist/codex/CodexRuntime.js +35 -2
  12. package/dist/codex/CodexRuntime.js.map +1 -1
  13. package/dist/commands/CommandRouter.js +5 -0
  14. package/dist/commands/CommandRouter.js.map +1 -1
  15. package/dist/feishu/CardRenderer.js +83 -0
  16. package/dist/feishu/CardRenderer.js.map +1 -1
  17. package/dist/feishu/MergedForwardMessage.js +6 -1
  18. package/dist/feishu/MergedForwardMessage.js.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/proxy/ProxySessionController.js +449 -4
  22. package/dist/proxy/ProxySessionController.js.map +1 -1
  23. package/dist/state/StateStore.js +10 -0
  24. package/dist/state/StateStore.js.map +1 -1
  25. package/docs/technical-reference.md +1 -1
  26. package/docs/technical-reference.zh.md +1 -1
  27. package/npm-shrinkwrap.json +2 -2
  28. package/package.json +1 -1
  29. package/skills/agent-bot/SKILL.md +3 -0
  30. package/src/cli/controlProtocol.ts +8 -0
  31. package/src/cli/help.ts +2 -0
  32. package/src/cli.ts +33 -0
  33. package/src/codex/CodexProcessManager.ts +43 -0
  34. package/src/codex/CodexRuntime.ts +33 -2
  35. package/src/commands/CommandRouter.ts +4 -0
  36. package/src/commands/commandTypes.ts +1 -0
  37. package/src/feishu/CardRenderer.ts +96 -0
  38. package/src/feishu/MergedForwardMessage.ts +5 -1
  39. package/src/index.ts +2 -0
  40. package/src/proxy/ProxySessionController.ts +544 -6
  41. package/src/runtime/types.ts +6 -0
  42. package/src/state/StateStore.ts +11 -0
@@ -26,6 +26,7 @@ import type {
26
26
  ReferencedMessageContent,
27
27
  } from "../feishu/types.js";
28
28
  import {
29
+ type AppServerReleaseCardView,
29
30
  CardRenderer,
30
31
  type CardSection,
31
32
  type DirectoryBrowserCardEntry,
@@ -59,6 +60,7 @@ import type {
59
60
  PermissionMode,
60
61
  RemoteCompletedTurnSummary,
61
62
  RemoteSessionActivity,
63
+ RemoteSessionPage,
62
64
  RemoteSessionSummary,
63
65
  RuntimeGoal,
64
66
  RuntimeEvent,
@@ -117,12 +119,40 @@ interface ForwardAttachmentReservation {
117
119
  registry: Map<string, PendingForwardAttachment>;
118
120
  }
119
121
 
122
+ interface PreparedTopicRootPrompt {
123
+ text: string;
124
+ localImagePaths?: string[];
125
+ displayPrompt: string;
126
+ injectedRootMessageId?: string;
127
+ }
128
+
120
129
  interface ShellCommandJobMonitor {
121
130
  scheduler: CardUpdateScheduler<ShellCommandCardView>;
122
131
  timer: NodeJS.Timeout;
123
132
  refreshing: boolean;
124
133
  }
125
134
 
135
+ interface AppServerReleaseCardTarget {
136
+ contextKey: string;
137
+ messageId: string;
138
+ }
139
+
140
+ interface AppServerReleaseSchedule {
141
+ id: number;
142
+ agentName: string;
143
+ runtime: AgentRuntime;
144
+ status: AppServerReleaseCardView["status"];
145
+ blockingTaskCount: number;
146
+ targets: Map<string, AppServerReleaseCardTarget>;
147
+ forced: boolean;
148
+ releaseAt?: number;
149
+ releaseCountdownSeconds?: number;
150
+ releaseStarted?: boolean;
151
+ error?: string;
152
+ timer?: NodeJS.Timeout;
153
+ inFlight?: Promise<void>;
154
+ }
155
+
126
156
  interface RequestFailureDetails {
127
157
  requestId?: string;
128
158
  messageId?: string;
@@ -146,12 +176,15 @@ const SESSION_PAGE_SIZE = 10;
146
176
  const SESSION_PROMPT_PREVIEW_LENGTH = 50;
147
177
  const DIRECTORY_PAGE_SIZE = 15;
148
178
  const RESET_HISTORY_PAGE_SIZE = 10;
179
+ const REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS = 30_000;
149
180
  const MAX_LLM_TURN_RETRIES = 3;
150
181
  const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
151
182
  const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
152
183
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
153
184
  const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
154
185
  const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
186
+ const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
187
+ const APP_SERVER_RELEASE_IDLE_DELAY_MS = 5_000;
155
188
  const FORWARD_ATTACHMENT_WINDOW_MS = 800;
156
189
  const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
157
190
  const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
@@ -285,6 +318,10 @@ const HELP_COMMAND_SECTIONS: Array<{
285
318
  usage: "[--force]",
286
319
  description: "默认安全重启;--force 立即重启",
287
320
  },
321
+ {
322
+ command: "/release",
323
+ description: "释放 Agent Bot 占用的 App Server 任务",
324
+ },
288
325
  {
289
326
  command: "/mute",
290
327
  usage: "[on|off]",
@@ -413,6 +450,13 @@ export interface ControlTaskGroupResult {
413
450
  task: SessionRecord;
414
451
  }
415
452
 
453
+ export interface ControlTaskReleaseResult {
454
+ agentName: string;
455
+ status: "waiting" | "released";
456
+ blockingTaskCount: number;
457
+ releaseInSeconds?: number;
458
+ }
459
+
416
460
  export interface ProxyLifecycle {
417
461
  supervised?: boolean;
418
462
  restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
@@ -447,6 +491,7 @@ export class ProxySessionController {
447
491
  private readonly queuedPromptCardWrites = new Map<string, Promise<void>>();
448
492
  private readonly sessionResets = new Map<string, Promise<void>>();
449
493
  private readonly resetHistoryOperations = new Map<string, string>();
494
+ private readonly remoteTurnHistoryRefreshes = new Map<string, number>();
450
495
  private readonly lastSessionListings = new Map<string, string[]>();
451
496
  private readonly threadInitializations = new Map<string, Promise<void>>();
452
497
  private readonly llmRetryingSessions = new Set<string>();
@@ -457,6 +502,8 @@ export class ProxySessionController {
457
502
  private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
458
503
  private readonly relatedReactionMessageIds = new Map<string, string[]>();
459
504
  private readonly shellCommandJobMonitors = new Map<string, ShellCommandJobMonitor>();
505
+ private readonly appServerReleaseSchedules = new Map<string, AppServerReleaseSchedule>();
506
+ private nextAppServerReleaseScheduleId = 1;
460
507
  private readonly unsubscribe: Array<() => void> = [];
461
508
  private readonly recoveryActivityHeartbeat: NodeJS.Timeout;
462
509
  private startupRecovery?: Promise<void>;
@@ -816,16 +863,37 @@ export class ProxySessionController {
816
863
  const next = previous.catch(() => undefined).then(() =>
817
864
  this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
818
865
  try {
819
- if (command.type === "prompt") await this.ensureThreadFork(message);
866
+ let executableCommand = command;
867
+ let executableImagePaths = localImagePaths;
868
+ let executableMessage = message;
869
+ let injectedRootMessageId: string | undefined;
870
+ if (command.type === "prompt") {
871
+ await this.ensureThreadFork(message);
872
+ const prepared = await this.prepareTopicRootPrompt(message, command.text, localImagePaths);
873
+ executableCommand = { ...command, text: prepared.text };
874
+ executableImagePaths = prepared.localImagePaths;
875
+ executableMessage = { ...message, displayText: prepared.displayPrompt };
876
+ injectedRootMessageId = prepared.injectedRootMessageId;
877
+ }
820
878
  await this.execute(
821
879
  message.contextKey,
822
- command,
880
+ executableCommand,
823
881
  message.messageId,
824
882
  replyTarget,
825
- localImagePaths,
883
+ executableImagePaths,
826
884
  message.userId,
827
- message,
885
+ executableMessage,
828
886
  );
887
+ if (injectedRootMessageId) {
888
+ const current = this.currentSession(message.contextKey);
889
+ if (current) {
890
+ this.store.audit(message.contextKey, "topic_root_injected", {
891
+ localSessionId: current.localSessionId,
892
+ rootMessageId: injectedRootMessageId,
893
+ promptMessageId: message.messageId,
894
+ });
895
+ }
896
+ }
829
897
  if (!commandDefersReactionFinalization(command)) {
830
898
  await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
831
899
  }
@@ -907,6 +975,10 @@ export class ProxySessionController {
907
975
  throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
908
976
  }
909
977
  await this.forkSessionReference(contextKey, sessionId);
978
+ } else if (kind === "app_server_release_now") {
979
+ await this.releaseAppServerNow(scopedAction);
980
+ } else if (kind === "app_server_release_cancel") {
981
+ await this.cancelAppServerRelease(scopedAction);
910
982
  } else if (kind === "turn_reset") {
911
983
  const sessionId = String(scopedAction.value.sessionId ?? "");
912
984
  const turnId = String(scopedAction.value.turnId ?? "");
@@ -1177,6 +1249,7 @@ export class ProxySessionController {
1177
1249
  this.queuedPromptCardWrites.clear();
1178
1250
  this.sessionResets.clear();
1179
1251
  this.resetHistoryOperations.clear();
1252
+ this.remoteTurnHistoryRefreshes.clear();
1180
1253
  for (const pending of this.pendingMergedForwards.values()) pending.resolveAttachment(undefined);
1181
1254
  this.pendingMergedForwards.clear();
1182
1255
  for (const pending of this.pendingResourceForwards.values()) pending.resolveAttachment(undefined);
@@ -1186,6 +1259,10 @@ export class ProxySessionController {
1186
1259
  this.queuedPromptRetryTimers.clear();
1187
1260
  for (const timer of this.recoveryRetryTimers.values()) clearTimeout(timer);
1188
1261
  this.recoveryRetryTimers.clear();
1262
+ for (const schedule of this.appServerReleaseSchedules.values()) {
1263
+ if (schedule.timer) clearTimeout(schedule.timer);
1264
+ }
1265
+ this.appServerReleaseSchedules.clear();
1189
1266
  for (const [jobId] of this.shellCommandJobMonitors) this.stopShellCommandJobMonitor(jobId);
1190
1267
  clearInterval(this.recoveryActivityHeartbeat);
1191
1268
  }
@@ -1197,6 +1274,23 @@ export class ProxySessionController {
1197
1274
  return `Task stop requested: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
1198
1275
  }
1199
1276
 
1277
+ async controlReleaseTask(localSessionId: string): Promise<ControlTaskReleaseResult> {
1278
+ const record = this.requireControlSession(localSessionId);
1279
+ const contextKey = this.controlSessionContextKey(record);
1280
+ const result = await this.outbound.withReplyTarget(
1281
+ contextKey,
1282
+ this.controlSessionReplyTarget(record),
1283
+ () => this.requestAppServerRelease(contextKey, record),
1284
+ );
1285
+ if (result.status === "failed") throw new Error(result.error ?? "Failed to release App Server tasks.");
1286
+ return {
1287
+ agentName: result.agentName,
1288
+ status: result.status,
1289
+ blockingTaskCount: result.blockingTaskCount,
1290
+ ...(result.releaseInSeconds !== undefined ? { releaseInSeconds: result.releaseInSeconds } : {}),
1291
+ };
1292
+ }
1293
+
1200
1294
  async controlArchiveTask(localSessionId: string): Promise<{
1201
1295
  localSessionId: string;
1202
1296
  remoteSessionId: string;
@@ -1935,6 +2029,9 @@ export class ProxySessionController {
1935
2029
  if (!this.lifecycle) throw new Error("当前运行方式不支持自动重启。");
1936
2030
  await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
1937
2031
  return;
2032
+ case "release":
2033
+ await this.requestAppServerRelease(contextKey);
2034
+ return;
1938
2035
  case "mute":
1939
2036
  await this.setGroupMute(contextKey, command.enabled);
1940
2037
  return;
@@ -2740,6 +2837,61 @@ export class ProxySessionController {
2740
2837
  }
2741
2838
  }
2742
2839
 
2840
+ private async prepareTopicRootPrompt(
2841
+ message: IncomingMessage,
2842
+ text: string,
2843
+ localImagePaths?: string[],
2844
+ ): Promise<PreparedTopicRootPrompt> {
2845
+ const displayPrompt = message.displayText ?? text;
2846
+ const rootMessageId = topicRootMessageId(message);
2847
+ if (!rootMessageId) return { text, localImagePaths, displayPrompt };
2848
+
2849
+ const current = this.currentSession(message.contextKey);
2850
+ if (current && (
2851
+ this.store.hasInjectedTopicRoot(current.localSessionId, rootMessageId)
2852
+ || this.taskHistoryContainsMessage(current, rootMessageId)
2853
+ )) {
2854
+ return { text, localImagePaths, displayPrompt };
2855
+ }
2856
+
2857
+ try {
2858
+ const root = await this.resolveReferencedMessage(message.contextKey, rootMessageId);
2859
+ const rootImagePaths = await Promise.all(root.images.map((image) =>
2860
+ this.outbound.downloadImage(message.contextKey, image.messageId, image.imageKey)));
2861
+ let promptText = topicRootMessagePrompt(text, root.text);
2862
+ if (root.files.length > 0) {
2863
+ const downloadedFiles = await Promise.all(root.files.map(async (file) => ({
2864
+ fileName: file.fileName,
2865
+ filePath: await this.outbound.downloadFile(
2866
+ message.contextKey,
2867
+ file.messageId,
2868
+ file.fileKey,
2869
+ file.fileName,
2870
+ ),
2871
+ })));
2872
+ promptText = appendDownloadedFiles(promptText, downloadedFiles);
2873
+ }
2874
+ const combinedImagePaths = [...new Set([...(localImagePaths ?? []), ...rootImagePaths])];
2875
+ return {
2876
+ text: promptText,
2877
+ ...(combinedImagePaths.length > 0 ? { localImagePaths: combinedImagePaths } : {}),
2878
+ displayPrompt,
2879
+ injectedRootMessageId: rootMessageId,
2880
+ };
2881
+ } catch (error) {
2882
+ const detail = error instanceof Error ? error.message : String(error);
2883
+ throw new Error(`无法读取话题根消息:${detail}`);
2884
+ }
2885
+ }
2886
+
2887
+ private taskHistoryContainsMessage(record: SessionRecord, messageId: string): boolean {
2888
+ const anchor = this.store.findTurnAnchorByMessageId(messageId);
2889
+ if (!anchor) return false;
2890
+ if (record.lastTurnId === anchor.turnId) return true;
2891
+ return this.store.listTaskTurnGraph(record.localSessionId)
2892
+ .some((turn) => turn.turnId === anchor.turnId);
2893
+ }
2894
+
2743
2895
  private async forkThreadSession(message: IncomingMessage): Promise<void> {
2744
2896
  const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
2745
2897
  const agent = this.ensureAgent(source.agentName);
@@ -3335,6 +3487,39 @@ export class ProxySessionController {
3335
3487
  }
3336
3488
  }
3337
3489
 
3490
+ private async hydrateRemoteTurnHistory(
3491
+ contextKey: string,
3492
+ current: SessionRecord,
3493
+ ): Promise<void> {
3494
+ if (!current.remoteSessionId) return;
3495
+ const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
3496
+ if (refreshedAt !== undefined && Date.now() - refreshedAt < REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS) return;
3497
+ const forkSource = this.store.getForkHistorySource(current.localSessionId);
3498
+ if (forkSource?.sourceTurnId === current.lastTurnId
3499
+ && this.store.hasImportedForkTurnHistory(current.localSessionId)) return;
3500
+ const runtime = this.runtimes.forAgent(current.agentName);
3501
+ if (runtime.kind !== "codex" || !runtime.readRemoteSession) return;
3502
+ try {
3503
+ const remote = await runtime.readRemoteSession(current.remoteSessionId);
3504
+ this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
3505
+ const completedTurns = remote.completedTurns ?? [];
3506
+ if (completedTurns.length === 0) return;
3507
+ this.persistCompletedTurnHistory({
3508
+ localSessionId: current.localSessionId,
3509
+ contextKey,
3510
+ agentName: current.agentName,
3511
+ remoteSessionId: current.remoteSessionId,
3512
+ completedTurns,
3513
+ fallbackStart: parseIsoTimestamp(current.createdAt) ?? Date.now() - completedTurns.length,
3514
+ });
3515
+ } catch (error) {
3516
+ this.logger.warn(
3517
+ { error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId },
3518
+ "Failed to hydrate App Server Turn history; rendering the locally available history.",
3519
+ );
3520
+ }
3521
+ }
3522
+
3338
3523
  private async createProjectSessionFromReference(contextKey: string, reference: string): Promise<void> {
3339
3524
  const resolved = await this.resolveProjectSessionReference(contextKey, reference);
3340
3525
  const created = await this.createSession(
@@ -5417,6 +5602,7 @@ export class ProxySessionController {
5417
5602
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
5418
5603
  }
5419
5604
  await this.hydrateLegacyForkTurnHistory(contextKey, current);
5605
+ await this.hydrateRemoteTurnHistory(contextKey, current);
5420
5606
  const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
5421
5607
  const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
5422
5608
  const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
@@ -5751,7 +5937,7 @@ export class ProxySessionController {
5751
5937
  await Promise.all(this.runtimes.entries("codex").map(async ([agentName, runtime]) => {
5752
5938
  if (!runtime.listRemoteSessions) return;
5753
5939
  try {
5754
- const result = await runtime.listRemoteSessions({ searchTerm, limit: fetchLimit });
5940
+ const result = await this.listRemoteSessionsThroughCursor(runtime, searchTerm, fetchLimit);
5755
5941
  remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
5756
5942
  remoteHasMore ||= Boolean(result.nextCursor);
5757
5943
  } catch (error) {
@@ -5963,6 +6149,42 @@ export class ProxySessionController {
5963
6149
  }
5964
6150
  }
5965
6151
 
6152
+ private async listRemoteSessionsThroughCursor(
6153
+ runtime: AgentRuntime,
6154
+ searchTerm: string | undefined,
6155
+ limit: number,
6156
+ ): Promise<RemoteSessionPage> {
6157
+ const sessions: RemoteSessionSummary[] = [];
6158
+ const seenSessionIds = new Set<string>();
6159
+ const seenCursors = new Set<string>();
6160
+ let cursor: string | undefined;
6161
+
6162
+ while (sessions.length < limit) {
6163
+ const page = await runtime.listRemoteSessions!({
6164
+ searchTerm,
6165
+ ...(cursor ? { cursor } : {}),
6166
+ limit: limit - sessions.length,
6167
+ });
6168
+ for (const session of page.sessions) {
6169
+ if (seenSessionIds.has(session.id)) continue;
6170
+ seenSessionIds.add(session.id);
6171
+ sessions.push(session);
6172
+ }
6173
+
6174
+ const nextCursor = page.nextCursor?.trim() || undefined;
6175
+ if (!nextCursor) return { sessions };
6176
+ if (sessions.length >= limit) return { sessions, nextCursor };
6177
+ if (seenCursors.has(nextCursor)) {
6178
+ this.logger.warn({ cursor: nextCursor }, "App Server repeated a thread/list cursor; stopping pagination.");
6179
+ return { sessions };
6180
+ }
6181
+ seenCursors.add(nextCursor);
6182
+ cursor = nextCursor;
6183
+ }
6184
+
6185
+ return { sessions };
6186
+ }
6187
+
5966
6188
  private async refreshSessionsCardFromAction(
5967
6189
  action: CardAction,
5968
6190
  options: { forceSwitchTaskId?: string; page?: number } = {},
@@ -6739,6 +6961,311 @@ export class ProxySessionController {
6739
6961
  return record;
6740
6962
  }
6741
6963
 
6964
+ private async requestAppServerRelease(
6965
+ contextKey: string,
6966
+ requestedRecord?: SessionRecord,
6967
+ ): Promise<{
6968
+ agentName: string;
6969
+ status: "waiting" | "released" | "failed";
6970
+ blockingTaskCount: number;
6971
+ releaseInSeconds?: number;
6972
+ error?: string;
6973
+ }> {
6974
+ const record = requestedRecord ?? this.requireCurrentSession(contextKey);
6975
+ const runtime = this.runtimes.forAgent(record.agentName);
6976
+ if (!this.isCodexSession(record) || runtime.kind !== "codex" || !runtime.release) {
6977
+ throw new Error("当前 Agent 不使用可释放的 App Server。");
6978
+ }
6979
+
6980
+ let schedule = this.appServerReleaseSchedules.get(record.agentName);
6981
+ const created = !schedule;
6982
+ if (!schedule) {
6983
+ const blockingTaskCount = this.appServerReleaseBlockers(record.agentName, runtime).length;
6984
+ schedule = {
6985
+ id: this.nextAppServerReleaseScheduleId++,
6986
+ agentName: record.agentName,
6987
+ runtime,
6988
+ status: "waiting",
6989
+ blockingTaskCount,
6990
+ targets: new Map(),
6991
+ forced: false,
6992
+ ...(blockingTaskCount === 0
6993
+ ? {
6994
+ releaseCountdownSeconds: Math.ceil(APP_SERVER_RELEASE_IDLE_DELAY_MS / 1_000),
6995
+ }
6996
+ : {}),
6997
+ };
6998
+ this.appServerReleaseSchedules.set(record.agentName, schedule);
6999
+ }
7000
+
7001
+ try {
7002
+ const messageId = await this.outbound.sendInteractiveCard(
7003
+ contextKey,
7004
+ this.cardRenderer.renderAppServerRelease(this.appServerReleaseCardView(schedule, contextKey)),
7005
+ );
7006
+ if (messageId) {
7007
+ schedule.targets.set(`${contextKey}\u0000${messageId}`, { contextKey, messageId });
7008
+ }
7009
+ if (
7010
+ schedule.status === "waiting"
7011
+ && schedule.blockingTaskCount === 0
7012
+ && schedule.releaseCountdownSeconds !== undefined
7013
+ && schedule.releaseAt === undefined
7014
+ ) {
7015
+ schedule.releaseAt = Date.now() + APP_SERVER_RELEASE_IDLE_DELAY_MS;
7016
+ }
7017
+ } catch (error) {
7018
+ if (created && schedule.targets.size === 0) {
7019
+ this.clearAppServerReleaseSchedule(schedule);
7020
+ }
7021
+ throw error;
7022
+ }
7023
+
7024
+ this.armAppServerReleaseCheck(schedule);
7025
+ return {
7026
+ agentName: schedule.agentName,
7027
+ status: schedule.status === "failed" ? "failed" : schedule.status === "released" ? "released" : "waiting",
7028
+ blockingTaskCount: schedule.blockingTaskCount,
7029
+ ...(schedule.releaseCountdownSeconds !== undefined
7030
+ ? { releaseInSeconds: schedule.releaseCountdownSeconds }
7031
+ : {}),
7032
+ ...(schedule.error ? { error: schedule.error } : {}),
7033
+ };
7034
+ }
7035
+
7036
+ private async releaseAppServerNow(action: CardAction): Promise<void> {
7037
+ const scheduleId = Number(action.value.scheduleId);
7038
+ const agentName = typeof action.value.agentName === "string" ? action.value.agentName : "";
7039
+ const schedule = this.appServerReleaseSchedules.get(agentName);
7040
+ if (!Number.isSafeInteger(scheduleId) || !schedule || schedule.id !== scheduleId || schedule.status !== "waiting") {
7041
+ throw new Error("释放卡片已失效,请重新发送 /release。");
7042
+ }
7043
+ await this.performAppServerRelease(schedule, true);
7044
+ }
7045
+
7046
+ private async cancelAppServerRelease(action: CardAction): Promise<void> {
7047
+ const scheduleId = Number(action.value.scheduleId);
7048
+ const agentName = typeof action.value.agentName === "string" ? action.value.agentName : "";
7049
+ const schedule = this.appServerReleaseSchedules.get(agentName);
7050
+ const cancellable = schedule?.status === "waiting"
7051
+ || (schedule?.status === "releasing" && schedule.releaseStarted !== true);
7052
+ if (!Number.isSafeInteger(scheduleId) || !schedule || schedule.id !== scheduleId || !cancellable) {
7053
+ throw new Error("释放卡片已失效,请重新发送 /release。");
7054
+ }
7055
+ const pendingRelease = schedule.inFlight;
7056
+ schedule.status = "cancelled";
7057
+ if (schedule.timer) {
7058
+ clearTimeout(schedule.timer);
7059
+ schedule.timer = undefined;
7060
+ }
7061
+ if (pendingRelease) await pendingRelease;
7062
+ await this.publishAppServerReleaseSchedule(schedule);
7063
+ this.clearAppServerReleaseSchedule(schedule);
7064
+ }
7065
+
7066
+ private appServerReleaseBlockers(agentName: string, runtime: AgentRuntime): SessionRecord[] {
7067
+ return this.store.listAllSessions().filter((session) => {
7068
+ if (session.agentName !== agentName || session.status === "closed") return false;
7069
+ return session.status === "starting"
7070
+ || session.status === "running"
7071
+ || this.store.countQueuedPrompts(session.localSessionId) > 0
7072
+ || this.queuedPromptStarts.has(session.localSessionId)
7073
+ || this.sessionLoads.has(session.localSessionId)
7074
+ || Boolean(runtime.getSession(session.localSessionId)?.activeTurnId);
7075
+ });
7076
+ }
7077
+
7078
+ private armAppServerReleaseCheck(schedule: AppServerReleaseSchedule): void {
7079
+ if (schedule.status !== "waiting" || schedule.timer || schedule.inFlight) return;
7080
+ schedule.timer = setTimeout(() => {
7081
+ schedule.timer = undefined;
7082
+ void this.refreshAppServerReleaseSchedule(schedule).catch((error: unknown) => {
7083
+ this.logger.warn({ error, agentName: schedule.agentName }, "Failed to refresh App Server release status.");
7084
+ this.armAppServerReleaseCheck(schedule);
7085
+ });
7086
+ }, APP_SERVER_RELEASE_POLL_INTERVAL_MS);
7087
+ schedule.timer.unref?.();
7088
+ }
7089
+
7090
+ private async refreshAppServerReleaseSchedule(schedule: AppServerReleaseSchedule): Promise<void> {
7091
+ if (this.appServerReleaseSchedules.get(schedule.agentName) !== schedule || schedule.status !== "waiting") return;
7092
+ const blockingTaskCount = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length;
7093
+ if (blockingTaskCount > 0) {
7094
+ const changed = schedule.blockingTaskCount !== blockingTaskCount || schedule.releaseAt !== undefined;
7095
+ schedule.releaseAt = undefined;
7096
+ schedule.releaseCountdownSeconds = undefined;
7097
+ if (changed) {
7098
+ schedule.blockingTaskCount = blockingTaskCount;
7099
+ await this.publishAppServerReleaseSchedule(schedule);
7100
+ }
7101
+ this.armAppServerReleaseCheck(schedule);
7102
+ return;
7103
+ }
7104
+ schedule.blockingTaskCount = 0;
7105
+ if (schedule.releaseAt !== undefined) {
7106
+ const releaseCountdownSeconds = Math.max(0, Math.ceil((schedule.releaseAt - Date.now()) / 1_000));
7107
+ if (releaseCountdownSeconds > 0) {
7108
+ if (schedule.releaseCountdownSeconds !== releaseCountdownSeconds) {
7109
+ schedule.releaseCountdownSeconds = releaseCountdownSeconds;
7110
+ await this.publishAppServerReleaseSchedule(schedule);
7111
+ }
7112
+ this.armAppServerReleaseCheck(schedule);
7113
+ return;
7114
+ }
7115
+ }
7116
+ await this.performAppServerRelease(schedule, false);
7117
+ }
7118
+
7119
+ private async performAppServerRelease(schedule: AppServerReleaseSchedule, force: boolean): Promise<void> {
7120
+ if (schedule.inFlight) return schedule.inFlight;
7121
+ const operation = this.performAppServerReleaseNow(schedule, force);
7122
+ schedule.inFlight = operation;
7123
+ try {
7124
+ await operation;
7125
+ } finally {
7126
+ if (schedule.inFlight === operation) schedule.inFlight = undefined;
7127
+ if (schedule.status === "waiting") this.armAppServerReleaseCheck(schedule);
7128
+ }
7129
+ }
7130
+
7131
+ private async performAppServerReleaseNow(schedule: AppServerReleaseSchedule, force: boolean): Promise<void> {
7132
+ if (schedule.timer) {
7133
+ clearTimeout(schedule.timer);
7134
+ schedule.timer = undefined;
7135
+ }
7136
+ const interruptedWork = force && this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length > 0;
7137
+ if (force) await this.cancelQueuedPromptsForAppServerRelease(schedule.agentName);
7138
+ else {
7139
+ const blockingTaskCount = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length;
7140
+ if (blockingTaskCount > 0) {
7141
+ schedule.status = "waiting";
7142
+ schedule.blockingTaskCount = blockingTaskCount;
7143
+ schedule.releaseAt = undefined;
7144
+ schedule.releaseCountdownSeconds = undefined;
7145
+ schedule.releaseStarted = false;
7146
+ await this.publishAppServerReleaseSchedule(schedule);
7147
+ this.armAppServerReleaseCheck(schedule);
7148
+ return;
7149
+ }
7150
+ }
7151
+
7152
+ schedule.status = "releasing";
7153
+ schedule.blockingTaskCount = 0;
7154
+ schedule.releaseAt = undefined;
7155
+ schedule.releaseCountdownSeconds = undefined;
7156
+ schedule.releaseStarted = false;
7157
+ schedule.forced = interruptedWork;
7158
+ await this.publishAppServerReleaseSchedule(schedule);
7159
+ if (
7160
+ this.appServerReleaseWasCancelled(schedule)
7161
+ || this.appServerReleaseSchedules.get(schedule.agentName) !== schedule
7162
+ ) return;
7163
+ schedule.releaseStarted = true;
7164
+ try {
7165
+ const result = await schedule.runtime.release?.({ force });
7166
+ if (!result) throw new Error("当前 Agent 不支持释放 App Server。");
7167
+ if (result.status === "busy") {
7168
+ schedule.status = "waiting";
7169
+ schedule.blockingTaskCount = Math.max(
7170
+ result.activeSessionIds?.length ?? 0,
7171
+ this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length,
7172
+ 1,
7173
+ );
7174
+ schedule.forced = false;
7175
+ schedule.releaseAt = undefined;
7176
+ schedule.releaseCountdownSeconds = undefined;
7177
+ schedule.releaseStarted = false;
7178
+ await this.publishAppServerReleaseSchedule(schedule);
7179
+ this.armAppServerReleaseCheck(schedule);
7180
+ return;
7181
+ }
7182
+ schedule.status = "released";
7183
+ } catch (error) {
7184
+ schedule.status = "failed";
7185
+ schedule.error = error instanceof Error ? error.message : String(error);
7186
+ this.logger.warn({ error, agentName: schedule.agentName }, "Failed to release App Server tasks.");
7187
+ }
7188
+ await this.publishAppServerReleaseSchedule(schedule);
7189
+ this.clearAppServerReleaseSchedule(schedule);
7190
+ }
7191
+
7192
+ private appServerReleaseWasCancelled(schedule: AppServerReleaseSchedule): boolean {
7193
+ return schedule.status === "cancelled";
7194
+ }
7195
+
7196
+ private async cancelQueuedPromptsForAppServerRelease(agentName: string): Promise<void> {
7197
+ for (const session of this.store.listAllSessions()) {
7198
+ if (session.agentName !== agentName) continue;
7199
+ const retryTimer = this.queuedPromptRetryTimers.get(session.localSessionId);
7200
+ if (retryTimer) {
7201
+ clearTimeout(retryTimer);
7202
+ this.queuedPromptRetryTimers.delete(session.localSessionId);
7203
+ }
7204
+ const queued = this.store.listQueuedPrompts(session.localSessionId);
7205
+ for (const prompt of queued) {
7206
+ const cancelled = this.store.cancelQueuedPrompt(prompt.promptId, session.localSessionId);
7207
+ if (!cancelled?.messageId) continue;
7208
+ await this.finalizeStandaloneMessageReaction(cancelled.messageId, "cancelled").catch((error: unknown) => {
7209
+ this.logger.warn(
7210
+ { error, agentName, sessionId: session.localSessionId, promptId: cancelled.promptId },
7211
+ "Failed to finalize a queued Prompt reaction during App Server release.",
7212
+ );
7213
+ });
7214
+ }
7215
+ if (queued.length > 0) {
7216
+ await this.refreshPromptQueueCards(session.localSessionId).catch((error: unknown) => {
7217
+ this.logger.warn(
7218
+ { error, agentName, sessionId: session.localSessionId },
7219
+ "Failed to refresh queued Prompt cards during App Server release.",
7220
+ );
7221
+ });
7222
+ }
7223
+ }
7224
+ }
7225
+
7226
+ private appServerReleaseCardView(
7227
+ schedule: AppServerReleaseSchedule,
7228
+ contextKey: string,
7229
+ ): AppServerReleaseCardView {
7230
+ return {
7231
+ status: schedule.status,
7232
+ contextKey,
7233
+ scheduleId: schedule.id,
7234
+ agentName: schedule.agentName,
7235
+ ...(schedule.blockingTaskCount > 0 ? { blockingTaskCount: schedule.blockingTaskCount } : {}),
7236
+ ...(schedule.releaseCountdownSeconds !== undefined
7237
+ ? { releaseInSeconds: schedule.releaseCountdownSeconds }
7238
+ : {}),
7239
+ ...(schedule.forced ? { forced: true } : {}),
7240
+ ...(schedule.error ? { error: schedule.error } : {}),
7241
+ };
7242
+ }
7243
+
7244
+ private async publishAppServerReleaseSchedule(schedule: AppServerReleaseSchedule): Promise<void> {
7245
+ await Promise.all([...schedule.targets.values()].map(async (target) => {
7246
+ try {
7247
+ await this.outbound.updateInteractiveCard(
7248
+ target.contextKey,
7249
+ target.messageId,
7250
+ this.cardRenderer.renderAppServerRelease(this.appServerReleaseCardView(schedule, target.contextKey)),
7251
+ );
7252
+ } catch (error) {
7253
+ this.logger.warn(
7254
+ { error, agentName: schedule.agentName, messageId: target.messageId },
7255
+ "Failed to update App Server release card.",
7256
+ );
7257
+ }
7258
+ }));
7259
+ }
7260
+
7261
+ private clearAppServerReleaseSchedule(schedule: AppServerReleaseSchedule): void {
7262
+ if (schedule.timer) clearTimeout(schedule.timer);
7263
+ schedule.timer = undefined;
7264
+ if (this.appServerReleaseSchedules.get(schedule.agentName) === schedule) {
7265
+ this.appServerReleaseSchedules.delete(schedule.agentName);
7266
+ }
7267
+ }
7268
+
6742
7269
  private async presentThreadWriterConflict(
6743
7270
  record: SessionRecord,
6744
7271
  contextKey: string,
@@ -7629,7 +8156,7 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
7629
8156
  }
7630
8157
 
7631
8158
  function isQueueIndependentCommand(command: Command): boolean {
7632
- if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type)) return true;
8159
+ if (["archive", "dismiss", "stop", "status", "restart", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type)) return true;
7633
8160
  if (command.type === "agent") return command.agent === undefined;
7634
8161
  if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
7635
8162
  return false;
@@ -7858,6 +8385,17 @@ function referencedMessagePrompt(instruction: string, referencedMessage: string)
7858
8385
  return `${instruction}\n\n引用消息:\n${referencedMessage}`;
7859
8386
  }
7860
8387
 
8388
+ function topicRootMessagePrompt(instruction: string, rootMessage: string): string {
8389
+ return `${instruction}\n\n话题根消息:\n${rootMessage}`;
8390
+ }
8391
+
8392
+ function topicRootMessageId(message: IncomingMessage): string | undefined {
8393
+ if (!message.threadContext) return undefined;
8394
+ const rootMessageId = message.rootMessageId?.trim();
8395
+ if (!rootMessageId || rootMessageId === message.messageId) return undefined;
8396
+ return rootMessageId;
8397
+ }
8398
+
7861
8399
  function quotedMessageId(message: IncomingMessage): string | undefined {
7862
8400
  const messageId = message.parentMessageId?.trim();
7863
8401
  if (!messageId || message.contextKey.startsWith("console:")) return undefined;