@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
@@ -38,12 +38,15 @@ const SESSION_PAGE_SIZE = 10;
38
38
  const SESSION_PROMPT_PREVIEW_LENGTH = 50;
39
39
  const DIRECTORY_PAGE_SIZE = 15;
40
40
  const RESET_HISTORY_PAGE_SIZE = 10;
41
+ const REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS = 30_000;
41
42
  const MAX_LLM_TURN_RETRIES = 3;
42
43
  const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
43
44
  const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
44
45
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
45
46
  const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
46
47
  const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
48
+ const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
49
+ const APP_SERVER_RELEASE_IDLE_DELAY_MS = 5_000;
47
50
  const FORWARD_ATTACHMENT_WINDOW_MS = 800;
48
51
  const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
49
52
  const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
@@ -166,6 +169,10 @@ const HELP_COMMAND_SECTIONS = [
166
169
  usage: "[--force]",
167
170
  description: "默认安全重启;--force 立即重启",
168
171
  },
172
+ {
173
+ command: "/release",
174
+ description: "释放 Agent Bot 占用的 App Server 任务",
175
+ },
169
176
  {
170
177
  command: "/mute",
171
178
  usage: "[on|off]",
@@ -198,6 +205,7 @@ export class ProxySessionController {
198
205
  queuedPromptCardWrites = new Map();
199
206
  sessionResets = new Map();
200
207
  resetHistoryOperations = new Map();
208
+ remoteTurnHistoryRefreshes = new Map();
201
209
  lastSessionListings = new Map();
202
210
  threadInitializations = new Map();
203
211
  llmRetryingSessions = new Set();
@@ -208,6 +216,8 @@ export class ProxySessionController {
208
216
  pendingResourceForwards = new Map();
209
217
  relatedReactionMessageIds = new Map();
210
218
  shellCommandJobMonitors = new Map();
219
+ appServerReleaseSchedules = new Map();
220
+ nextAppServerReleaseScheduleId = 1;
211
221
  unsubscribe = [];
212
222
  recoveryActivityHeartbeat;
213
223
  startupRecovery;
@@ -495,9 +505,29 @@ export class ProxySessionController {
495
505
  const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
496
506
  const next = previous.catch(() => undefined).then(() => this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
497
507
  try {
498
- if (command.type === "prompt")
508
+ let executableCommand = command;
509
+ let executableImagePaths = localImagePaths;
510
+ let executableMessage = message;
511
+ let injectedRootMessageId;
512
+ if (command.type === "prompt") {
499
513
  await this.ensureThreadFork(message);
500
- await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
514
+ const prepared = await this.prepareTopicRootPrompt(message, command.text, localImagePaths);
515
+ executableCommand = { ...command, text: prepared.text };
516
+ executableImagePaths = prepared.localImagePaths;
517
+ executableMessage = { ...message, displayText: prepared.displayPrompt };
518
+ injectedRootMessageId = prepared.injectedRootMessageId;
519
+ }
520
+ await this.execute(message.contextKey, executableCommand, message.messageId, replyTarget, executableImagePaths, message.userId, executableMessage);
521
+ if (injectedRootMessageId) {
522
+ const current = this.currentSession(message.contextKey);
523
+ if (current) {
524
+ this.store.audit(message.contextKey, "topic_root_injected", {
525
+ localSessionId: current.localSessionId,
526
+ rootMessageId: injectedRootMessageId,
527
+ promptMessageId: message.messageId,
528
+ });
529
+ }
530
+ }
501
531
  if (!commandDefersReactionFinalization(command)) {
502
532
  await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
503
533
  }
@@ -576,6 +606,12 @@ export class ProxySessionController {
576
606
  }
577
607
  await this.forkSessionReference(contextKey, sessionId);
578
608
  }
609
+ else if (kind === "app_server_release_now") {
610
+ await this.releaseAppServerNow(scopedAction);
611
+ }
612
+ else if (kind === "app_server_release_cancel") {
613
+ await this.cancelAppServerRelease(scopedAction);
614
+ }
579
615
  else if (kind === "turn_reset") {
580
616
  const sessionId = String(scopedAction.value.sessionId ?? "");
581
617
  const turnId = String(scopedAction.value.turnId ?? "");
@@ -831,6 +867,7 @@ export class ProxySessionController {
831
867
  this.queuedPromptCardWrites.clear();
832
868
  this.sessionResets.clear();
833
869
  this.resetHistoryOperations.clear();
870
+ this.remoteTurnHistoryRefreshes.clear();
834
871
  for (const pending of this.pendingMergedForwards.values())
835
872
  pending.resolveAttachment(undefined);
836
873
  this.pendingMergedForwards.clear();
@@ -844,6 +881,11 @@ export class ProxySessionController {
844
881
  for (const timer of this.recoveryRetryTimers.values())
845
882
  clearTimeout(timer);
846
883
  this.recoveryRetryTimers.clear();
884
+ for (const schedule of this.appServerReleaseSchedules.values()) {
885
+ if (schedule.timer)
886
+ clearTimeout(schedule.timer);
887
+ }
888
+ this.appServerReleaseSchedules.clear();
847
889
  for (const [jobId] of this.shellCommandJobMonitors)
848
890
  this.stopShellCommandJobMonitor(jobId);
849
891
  clearInterval(this.recoveryActivityHeartbeat);
@@ -855,6 +897,19 @@ export class ProxySessionController {
855
897
  await this.cancelSession(record);
856
898
  return `Task stop requested: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
857
899
  }
900
+ async controlReleaseTask(localSessionId) {
901
+ const record = this.requireControlSession(localSessionId);
902
+ const contextKey = this.controlSessionContextKey(record);
903
+ const result = await this.outbound.withReplyTarget(contextKey, this.controlSessionReplyTarget(record), () => this.requestAppServerRelease(contextKey, record));
904
+ if (result.status === "failed")
905
+ throw new Error(result.error ?? "Failed to release App Server tasks.");
906
+ return {
907
+ agentName: result.agentName,
908
+ status: result.status,
909
+ blockingTaskCount: result.blockingTaskCount,
910
+ ...(result.releaseInSeconds !== undefined ? { releaseInSeconds: result.releaseInSeconds } : {}),
911
+ };
912
+ }
858
913
  async controlArchiveTask(localSessionId) {
859
914
  const record = this.requireControlSession(localSessionId);
860
915
  return this.archiveSessionReference(this.controlSessionContextKey(record), localSessionId, {
@@ -1448,6 +1503,9 @@ export class ProxySessionController {
1448
1503
  throw new Error("当前运行方式不支持自动重启。");
1449
1504
  await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
1450
1505
  return;
1506
+ case "release":
1507
+ await this.requestAppServerRelease(contextKey);
1508
+ return;
1451
1509
  case "mute":
1452
1510
  await this.setGroupMute(contextKey, command.enabled);
1453
1511
  return;
@@ -2095,6 +2153,49 @@ export class ProxySessionController {
2095
2153
  }
2096
2154
  }
2097
2155
  }
2156
+ async prepareTopicRootPrompt(message, text, localImagePaths) {
2157
+ const displayPrompt = message.displayText ?? text;
2158
+ const rootMessageId = topicRootMessageId(message);
2159
+ if (!rootMessageId)
2160
+ return { text, localImagePaths, displayPrompt };
2161
+ const current = this.currentSession(message.contextKey);
2162
+ if (current && (this.store.hasInjectedTopicRoot(current.localSessionId, rootMessageId)
2163
+ || this.taskHistoryContainsMessage(current, rootMessageId))) {
2164
+ return { text, localImagePaths, displayPrompt };
2165
+ }
2166
+ try {
2167
+ const root = await this.resolveReferencedMessage(message.contextKey, rootMessageId);
2168
+ const rootImagePaths = await Promise.all(root.images.map((image) => this.outbound.downloadImage(message.contextKey, image.messageId, image.imageKey)));
2169
+ let promptText = topicRootMessagePrompt(text, root.text);
2170
+ if (root.files.length > 0) {
2171
+ const downloadedFiles = await Promise.all(root.files.map(async (file) => ({
2172
+ fileName: file.fileName,
2173
+ filePath: await this.outbound.downloadFile(message.contextKey, file.messageId, file.fileKey, file.fileName),
2174
+ })));
2175
+ promptText = appendDownloadedFiles(promptText, downloadedFiles);
2176
+ }
2177
+ const combinedImagePaths = [...new Set([...(localImagePaths ?? []), ...rootImagePaths])];
2178
+ return {
2179
+ text: promptText,
2180
+ ...(combinedImagePaths.length > 0 ? { localImagePaths: combinedImagePaths } : {}),
2181
+ displayPrompt,
2182
+ injectedRootMessageId: rootMessageId,
2183
+ };
2184
+ }
2185
+ catch (error) {
2186
+ const detail = error instanceof Error ? error.message : String(error);
2187
+ throw new Error(`无法读取话题根消息:${detail}`);
2188
+ }
2189
+ }
2190
+ taskHistoryContainsMessage(record, messageId) {
2191
+ const anchor = this.store.findTurnAnchorByMessageId(messageId);
2192
+ if (!anchor)
2193
+ return false;
2194
+ if (record.lastTurnId === anchor.turnId)
2195
+ return true;
2196
+ return this.store.listTaskTurnGraph(record.localSessionId)
2197
+ .some((turn) => turn.turnId === anchor.turnId);
2198
+ }
2098
2199
  async forkThreadSession(message) {
2099
2200
  const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
2100
2201
  const agent = this.ensureAgent(source.agentName);
@@ -2589,6 +2690,38 @@ export class ProxySessionController {
2589
2690
  this.logger.warn({ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId }, "Failed to hydrate legacy Fork Turn history; rendering the locally available history.");
2590
2691
  }
2591
2692
  }
2693
+ async hydrateRemoteTurnHistory(contextKey, current) {
2694
+ if (!current.remoteSessionId)
2695
+ return;
2696
+ const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
2697
+ if (refreshedAt !== undefined && Date.now() - refreshedAt < REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS)
2698
+ return;
2699
+ const forkSource = this.store.getForkHistorySource(current.localSessionId);
2700
+ if (forkSource?.sourceTurnId === current.lastTurnId
2701
+ && this.store.hasImportedForkTurnHistory(current.localSessionId))
2702
+ return;
2703
+ const runtime = this.runtimes.forAgent(current.agentName);
2704
+ if (runtime.kind !== "codex" || !runtime.readRemoteSession)
2705
+ return;
2706
+ try {
2707
+ const remote = await runtime.readRemoteSession(current.remoteSessionId);
2708
+ this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
2709
+ const completedTurns = remote.completedTurns ?? [];
2710
+ if (completedTurns.length === 0)
2711
+ return;
2712
+ this.persistCompletedTurnHistory({
2713
+ localSessionId: current.localSessionId,
2714
+ contextKey,
2715
+ agentName: current.agentName,
2716
+ remoteSessionId: current.remoteSessionId,
2717
+ completedTurns,
2718
+ fallbackStart: parseIsoTimestamp(current.createdAt) ?? Date.now() - completedTurns.length,
2719
+ });
2720
+ }
2721
+ catch (error) {
2722
+ this.logger.warn({ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId }, "Failed to hydrate App Server Turn history; rendering the locally available history.");
2723
+ }
2724
+ }
2592
2725
  async createProjectSessionFromReference(contextKey, reference) {
2593
2726
  const resolved = await this.resolveProjectSessionReference(contextKey, reference);
2594
2727
  const created = await this.createSession(contextKey, resolved.agentName, resolved.cwd, true, false, undefined, undefined, undefined, resolved.executionSettings);
@@ -4273,6 +4406,7 @@ export class ProxySessionController {
4273
4406
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
4274
4407
  }
4275
4408
  await this.hydrateLegacyForkTurnHistory(contextKey, current);
4409
+ await this.hydrateRemoteTurnHistory(contextKey, current);
4276
4410
  const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
4277
4411
  const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
4278
4412
  const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
@@ -4590,7 +4724,7 @@ export class ProxySessionController {
4590
4724
  if (!runtime.listRemoteSessions)
4591
4725
  return;
4592
4726
  try {
4593
- const result = await runtime.listRemoteSessions({ searchTerm, limit: fetchLimit });
4727
+ const result = await this.listRemoteSessionsThroughCursor(runtime, searchTerm, fetchLimit);
4594
4728
  remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
4595
4729
  remoteHasMore ||= Boolean(result.nextCursor);
4596
4730
  }
@@ -4788,6 +4922,37 @@ export class ProxySessionController {
4788
4922
  }
4789
4923
  }
4790
4924
  }
4925
+ async listRemoteSessionsThroughCursor(runtime, searchTerm, limit) {
4926
+ const sessions = [];
4927
+ const seenSessionIds = new Set();
4928
+ const seenCursors = new Set();
4929
+ let cursor;
4930
+ while (sessions.length < limit) {
4931
+ const page = await runtime.listRemoteSessions({
4932
+ searchTerm,
4933
+ ...(cursor ? { cursor } : {}),
4934
+ limit: limit - sessions.length,
4935
+ });
4936
+ for (const session of page.sessions) {
4937
+ if (seenSessionIds.has(session.id))
4938
+ continue;
4939
+ seenSessionIds.add(session.id);
4940
+ sessions.push(session);
4941
+ }
4942
+ const nextCursor = page.nextCursor?.trim() || undefined;
4943
+ if (!nextCursor)
4944
+ return { sessions };
4945
+ if (sessions.length >= limit)
4946
+ return { sessions, nextCursor };
4947
+ if (seenCursors.has(nextCursor)) {
4948
+ this.logger.warn({ cursor: nextCursor }, "App Server repeated a thread/list cursor; stopping pagination.");
4949
+ return { sessions };
4950
+ }
4951
+ seenCursors.add(nextCursor);
4952
+ cursor = nextCursor;
4953
+ }
4954
+ return { sessions };
4955
+ }
4791
4956
  async refreshSessionsCardFromAction(action, options = {}) {
4792
4957
  if (!action.messageId)
4793
4958
  return;
@@ -5463,6 +5628,275 @@ export class ProxySessionController {
5463
5628
  throw new Error(`找不到任务:${sessionId}`);
5464
5629
  return record;
5465
5630
  }
5631
+ async requestAppServerRelease(contextKey, requestedRecord) {
5632
+ const record = requestedRecord ?? this.requireCurrentSession(contextKey);
5633
+ const runtime = this.runtimes.forAgent(record.agentName);
5634
+ if (!this.isCodexSession(record) || runtime.kind !== "codex" || !runtime.release) {
5635
+ throw new Error("当前 Agent 不使用可释放的 App Server。");
5636
+ }
5637
+ let schedule = this.appServerReleaseSchedules.get(record.agentName);
5638
+ const created = !schedule;
5639
+ if (!schedule) {
5640
+ const blockingTaskCount = this.appServerReleaseBlockers(record.agentName, runtime).length;
5641
+ schedule = {
5642
+ id: this.nextAppServerReleaseScheduleId++,
5643
+ agentName: record.agentName,
5644
+ runtime,
5645
+ status: "waiting",
5646
+ blockingTaskCount,
5647
+ targets: new Map(),
5648
+ forced: false,
5649
+ ...(blockingTaskCount === 0
5650
+ ? {
5651
+ releaseCountdownSeconds: Math.ceil(APP_SERVER_RELEASE_IDLE_DELAY_MS / 1_000),
5652
+ }
5653
+ : {}),
5654
+ };
5655
+ this.appServerReleaseSchedules.set(record.agentName, schedule);
5656
+ }
5657
+ try {
5658
+ const messageId = await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderAppServerRelease(this.appServerReleaseCardView(schedule, contextKey)));
5659
+ if (messageId) {
5660
+ schedule.targets.set(`${contextKey}\u0000${messageId}`, { contextKey, messageId });
5661
+ }
5662
+ if (schedule.status === "waiting"
5663
+ && schedule.blockingTaskCount === 0
5664
+ && schedule.releaseCountdownSeconds !== undefined
5665
+ && schedule.releaseAt === undefined) {
5666
+ schedule.releaseAt = Date.now() + APP_SERVER_RELEASE_IDLE_DELAY_MS;
5667
+ }
5668
+ }
5669
+ catch (error) {
5670
+ if (created && schedule.targets.size === 0) {
5671
+ this.clearAppServerReleaseSchedule(schedule);
5672
+ }
5673
+ throw error;
5674
+ }
5675
+ this.armAppServerReleaseCheck(schedule);
5676
+ return {
5677
+ agentName: schedule.agentName,
5678
+ status: schedule.status === "failed" ? "failed" : schedule.status === "released" ? "released" : "waiting",
5679
+ blockingTaskCount: schedule.blockingTaskCount,
5680
+ ...(schedule.releaseCountdownSeconds !== undefined
5681
+ ? { releaseInSeconds: schedule.releaseCountdownSeconds }
5682
+ : {}),
5683
+ ...(schedule.error ? { error: schedule.error } : {}),
5684
+ };
5685
+ }
5686
+ async releaseAppServerNow(action) {
5687
+ const scheduleId = Number(action.value.scheduleId);
5688
+ const agentName = typeof action.value.agentName === "string" ? action.value.agentName : "";
5689
+ const schedule = this.appServerReleaseSchedules.get(agentName);
5690
+ if (!Number.isSafeInteger(scheduleId) || !schedule || schedule.id !== scheduleId || schedule.status !== "waiting") {
5691
+ throw new Error("释放卡片已失效,请重新发送 /release。");
5692
+ }
5693
+ await this.performAppServerRelease(schedule, true);
5694
+ }
5695
+ async cancelAppServerRelease(action) {
5696
+ const scheduleId = Number(action.value.scheduleId);
5697
+ const agentName = typeof action.value.agentName === "string" ? action.value.agentName : "";
5698
+ const schedule = this.appServerReleaseSchedules.get(agentName);
5699
+ const cancellable = schedule?.status === "waiting"
5700
+ || (schedule?.status === "releasing" && schedule.releaseStarted !== true);
5701
+ if (!Number.isSafeInteger(scheduleId) || !schedule || schedule.id !== scheduleId || !cancellable) {
5702
+ throw new Error("释放卡片已失效,请重新发送 /release。");
5703
+ }
5704
+ const pendingRelease = schedule.inFlight;
5705
+ schedule.status = "cancelled";
5706
+ if (schedule.timer) {
5707
+ clearTimeout(schedule.timer);
5708
+ schedule.timer = undefined;
5709
+ }
5710
+ if (pendingRelease)
5711
+ await pendingRelease;
5712
+ await this.publishAppServerReleaseSchedule(schedule);
5713
+ this.clearAppServerReleaseSchedule(schedule);
5714
+ }
5715
+ appServerReleaseBlockers(agentName, runtime) {
5716
+ return this.store.listAllSessions().filter((session) => {
5717
+ if (session.agentName !== agentName || session.status === "closed")
5718
+ return false;
5719
+ return session.status === "starting"
5720
+ || session.status === "running"
5721
+ || this.store.countQueuedPrompts(session.localSessionId) > 0
5722
+ || this.queuedPromptStarts.has(session.localSessionId)
5723
+ || this.sessionLoads.has(session.localSessionId)
5724
+ || Boolean(runtime.getSession(session.localSessionId)?.activeTurnId);
5725
+ });
5726
+ }
5727
+ armAppServerReleaseCheck(schedule) {
5728
+ if (schedule.status !== "waiting" || schedule.timer || schedule.inFlight)
5729
+ return;
5730
+ schedule.timer = setTimeout(() => {
5731
+ schedule.timer = undefined;
5732
+ void this.refreshAppServerReleaseSchedule(schedule).catch((error) => {
5733
+ this.logger.warn({ error, agentName: schedule.agentName }, "Failed to refresh App Server release status.");
5734
+ this.armAppServerReleaseCheck(schedule);
5735
+ });
5736
+ }, APP_SERVER_RELEASE_POLL_INTERVAL_MS);
5737
+ schedule.timer.unref?.();
5738
+ }
5739
+ async refreshAppServerReleaseSchedule(schedule) {
5740
+ if (this.appServerReleaseSchedules.get(schedule.agentName) !== schedule || schedule.status !== "waiting")
5741
+ return;
5742
+ const blockingTaskCount = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length;
5743
+ if (blockingTaskCount > 0) {
5744
+ const changed = schedule.blockingTaskCount !== blockingTaskCount || schedule.releaseAt !== undefined;
5745
+ schedule.releaseAt = undefined;
5746
+ schedule.releaseCountdownSeconds = undefined;
5747
+ if (changed) {
5748
+ schedule.blockingTaskCount = blockingTaskCount;
5749
+ await this.publishAppServerReleaseSchedule(schedule);
5750
+ }
5751
+ this.armAppServerReleaseCheck(schedule);
5752
+ return;
5753
+ }
5754
+ schedule.blockingTaskCount = 0;
5755
+ if (schedule.releaseAt !== undefined) {
5756
+ const releaseCountdownSeconds = Math.max(0, Math.ceil((schedule.releaseAt - Date.now()) / 1_000));
5757
+ if (releaseCountdownSeconds > 0) {
5758
+ if (schedule.releaseCountdownSeconds !== releaseCountdownSeconds) {
5759
+ schedule.releaseCountdownSeconds = releaseCountdownSeconds;
5760
+ await this.publishAppServerReleaseSchedule(schedule);
5761
+ }
5762
+ this.armAppServerReleaseCheck(schedule);
5763
+ return;
5764
+ }
5765
+ }
5766
+ await this.performAppServerRelease(schedule, false);
5767
+ }
5768
+ async performAppServerRelease(schedule, force) {
5769
+ if (schedule.inFlight)
5770
+ return schedule.inFlight;
5771
+ const operation = this.performAppServerReleaseNow(schedule, force);
5772
+ schedule.inFlight = operation;
5773
+ try {
5774
+ await operation;
5775
+ }
5776
+ finally {
5777
+ if (schedule.inFlight === operation)
5778
+ schedule.inFlight = undefined;
5779
+ if (schedule.status === "waiting")
5780
+ this.armAppServerReleaseCheck(schedule);
5781
+ }
5782
+ }
5783
+ async performAppServerReleaseNow(schedule, force) {
5784
+ if (schedule.timer) {
5785
+ clearTimeout(schedule.timer);
5786
+ schedule.timer = undefined;
5787
+ }
5788
+ const interruptedWork = force && this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length > 0;
5789
+ if (force)
5790
+ await this.cancelQueuedPromptsForAppServerRelease(schedule.agentName);
5791
+ else {
5792
+ const blockingTaskCount = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length;
5793
+ if (blockingTaskCount > 0) {
5794
+ schedule.status = "waiting";
5795
+ schedule.blockingTaskCount = blockingTaskCount;
5796
+ schedule.releaseAt = undefined;
5797
+ schedule.releaseCountdownSeconds = undefined;
5798
+ schedule.releaseStarted = false;
5799
+ await this.publishAppServerReleaseSchedule(schedule);
5800
+ this.armAppServerReleaseCheck(schedule);
5801
+ return;
5802
+ }
5803
+ }
5804
+ schedule.status = "releasing";
5805
+ schedule.blockingTaskCount = 0;
5806
+ schedule.releaseAt = undefined;
5807
+ schedule.releaseCountdownSeconds = undefined;
5808
+ schedule.releaseStarted = false;
5809
+ schedule.forced = interruptedWork;
5810
+ await this.publishAppServerReleaseSchedule(schedule);
5811
+ if (this.appServerReleaseWasCancelled(schedule)
5812
+ || this.appServerReleaseSchedules.get(schedule.agentName) !== schedule)
5813
+ return;
5814
+ schedule.releaseStarted = true;
5815
+ try {
5816
+ const result = await schedule.runtime.release?.({ force });
5817
+ if (!result)
5818
+ throw new Error("当前 Agent 不支持释放 App Server。");
5819
+ if (result.status === "busy") {
5820
+ schedule.status = "waiting";
5821
+ schedule.blockingTaskCount = Math.max(result.activeSessionIds?.length ?? 0, this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length, 1);
5822
+ schedule.forced = false;
5823
+ schedule.releaseAt = undefined;
5824
+ schedule.releaseCountdownSeconds = undefined;
5825
+ schedule.releaseStarted = false;
5826
+ await this.publishAppServerReleaseSchedule(schedule);
5827
+ this.armAppServerReleaseCheck(schedule);
5828
+ return;
5829
+ }
5830
+ schedule.status = "released";
5831
+ }
5832
+ catch (error) {
5833
+ schedule.status = "failed";
5834
+ schedule.error = error instanceof Error ? error.message : String(error);
5835
+ this.logger.warn({ error, agentName: schedule.agentName }, "Failed to release App Server tasks.");
5836
+ }
5837
+ await this.publishAppServerReleaseSchedule(schedule);
5838
+ this.clearAppServerReleaseSchedule(schedule);
5839
+ }
5840
+ appServerReleaseWasCancelled(schedule) {
5841
+ return schedule.status === "cancelled";
5842
+ }
5843
+ async cancelQueuedPromptsForAppServerRelease(agentName) {
5844
+ for (const session of this.store.listAllSessions()) {
5845
+ if (session.agentName !== agentName)
5846
+ continue;
5847
+ const retryTimer = this.queuedPromptRetryTimers.get(session.localSessionId);
5848
+ if (retryTimer) {
5849
+ clearTimeout(retryTimer);
5850
+ this.queuedPromptRetryTimers.delete(session.localSessionId);
5851
+ }
5852
+ const queued = this.store.listQueuedPrompts(session.localSessionId);
5853
+ for (const prompt of queued) {
5854
+ const cancelled = this.store.cancelQueuedPrompt(prompt.promptId, session.localSessionId);
5855
+ if (!cancelled?.messageId)
5856
+ continue;
5857
+ await this.finalizeStandaloneMessageReaction(cancelled.messageId, "cancelled").catch((error) => {
5858
+ this.logger.warn({ error, agentName, sessionId: session.localSessionId, promptId: cancelled.promptId }, "Failed to finalize a queued Prompt reaction during App Server release.");
5859
+ });
5860
+ }
5861
+ if (queued.length > 0) {
5862
+ await this.refreshPromptQueueCards(session.localSessionId).catch((error) => {
5863
+ this.logger.warn({ error, agentName, sessionId: session.localSessionId }, "Failed to refresh queued Prompt cards during App Server release.");
5864
+ });
5865
+ }
5866
+ }
5867
+ }
5868
+ appServerReleaseCardView(schedule, contextKey) {
5869
+ return {
5870
+ status: schedule.status,
5871
+ contextKey,
5872
+ scheduleId: schedule.id,
5873
+ agentName: schedule.agentName,
5874
+ ...(schedule.blockingTaskCount > 0 ? { blockingTaskCount: schedule.blockingTaskCount } : {}),
5875
+ ...(schedule.releaseCountdownSeconds !== undefined
5876
+ ? { releaseInSeconds: schedule.releaseCountdownSeconds }
5877
+ : {}),
5878
+ ...(schedule.forced ? { forced: true } : {}),
5879
+ ...(schedule.error ? { error: schedule.error } : {}),
5880
+ };
5881
+ }
5882
+ async publishAppServerReleaseSchedule(schedule) {
5883
+ await Promise.all([...schedule.targets.values()].map(async (target) => {
5884
+ try {
5885
+ await this.outbound.updateInteractiveCard(target.contextKey, target.messageId, this.cardRenderer.renderAppServerRelease(this.appServerReleaseCardView(schedule, target.contextKey)));
5886
+ }
5887
+ catch (error) {
5888
+ this.logger.warn({ error, agentName: schedule.agentName, messageId: target.messageId }, "Failed to update App Server release card.");
5889
+ }
5890
+ }));
5891
+ }
5892
+ clearAppServerReleaseSchedule(schedule) {
5893
+ if (schedule.timer)
5894
+ clearTimeout(schedule.timer);
5895
+ schedule.timer = undefined;
5896
+ if (this.appServerReleaseSchedules.get(schedule.agentName) === schedule) {
5897
+ this.appServerReleaseSchedules.delete(schedule.agentName);
5898
+ }
5899
+ }
5466
5900
  async presentThreadWriterConflict(record, contextKey, error, replacePendingTurn) {
5467
5901
  if (contextKey.startsWith("console:"))
5468
5902
  return false;
@@ -6179,7 +6613,7 @@ function isBotOwnedActiveTurn(record, remote) {
6179
6613
  && record.lastTurnId === remote.lastTurnId;
6180
6614
  }
6181
6615
  function isQueueIndependentCommand(command) {
6182
- if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type))
6616
+ if (["archive", "dismiss", "stop", "status", "restart", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type))
6183
6617
  return true;
6184
6618
  if (command.type === "agent")
6185
6619
  return command.agent === undefined;
@@ -6391,6 +6825,17 @@ function mergedForwardPrompt(instruction, transcript) {
6391
6825
  function referencedMessagePrompt(instruction, referencedMessage) {
6392
6826
  return `${instruction}\n\n引用消息:\n${referencedMessage}`;
6393
6827
  }
6828
+ function topicRootMessagePrompt(instruction, rootMessage) {
6829
+ return `${instruction}\n\n话题根消息:\n${rootMessage}`;
6830
+ }
6831
+ function topicRootMessageId(message) {
6832
+ if (!message.threadContext)
6833
+ return undefined;
6834
+ const rootMessageId = message.rootMessageId?.trim();
6835
+ if (!rootMessageId || rootMessageId === message.messageId)
6836
+ return undefined;
6837
+ return rootMessageId;
6838
+ }
6394
6839
  function quotedMessageId(message) {
6395
6840
  const messageId = message.parentMessageId?.trim();
6396
6841
  if (!messageId || message.contextKey.startsWith("console:"))