@keyou007/agent-bot 0.1.18 → 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 (63) hide show
  1. package/CHANGELOG.md +11 -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 +43 -5
  12. package/dist/codex/CodexRuntime.js.map +1 -1
  13. package/dist/codex/ThreadWriterProcess.js +344 -0
  14. package/dist/codex/ThreadWriterProcess.js.map +1 -0
  15. package/dist/commands/CommandRouter.js +5 -0
  16. package/dist/commands/CommandRouter.js.map +1 -1
  17. package/dist/console/ConsoleTurnPresenter.js +1 -0
  18. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  19. package/dist/feishu/CardRenderer.js +166 -0
  20. package/dist/feishu/CardRenderer.js.map +1 -1
  21. package/dist/feishu/FeishuTurnPresenter.js +13 -3
  22. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  23. package/dist/feishu/MergedForwardMessage.js +6 -1
  24. package/dist/feishu/MergedForwardMessage.js.map +1 -1
  25. package/dist/index.js +4 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/logging/errorLogValue.js +18 -0
  28. package/dist/logging/errorLogValue.js.map +1 -0
  29. package/dist/logging/logger.js +4 -0
  30. package/dist/logging/logger.js.map +1 -1
  31. package/dist/presentation/OutboundRouter.js +2 -2
  32. package/dist/presentation/OutboundRouter.js.map +1 -1
  33. package/dist/proxy/ProxySessionController.js +778 -19
  34. package/dist/proxy/ProxySessionController.js.map +1 -1
  35. package/dist/state/StateStore.js +10 -0
  36. package/dist/state/StateStore.js.map +1 -1
  37. package/dist/supervision/SafeRestartScheduler.js +1 -1
  38. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  39. package/docs/technical-reference.md +1 -1
  40. package/docs/technical-reference.zh.md +1 -1
  41. package/npm-shrinkwrap.json +2 -2
  42. package/package.json +1 -1
  43. package/skills/agent-bot/SKILL.md +3 -0
  44. package/src/cli/controlProtocol.ts +8 -0
  45. package/src/cli/help.ts +2 -0
  46. package/src/cli.ts +33 -0
  47. package/src/codex/CodexProcessManager.ts +43 -0
  48. package/src/codex/CodexRuntime.ts +41 -4
  49. package/src/codex/ThreadWriterProcess.ts +385 -0
  50. package/src/commands/CommandRouter.ts +4 -0
  51. package/src/commands/commandTypes.ts +1 -0
  52. package/src/console/ConsoleTurnPresenter.ts +2 -1
  53. package/src/feishu/CardRenderer.ts +205 -0
  54. package/src/feishu/FeishuTurnPresenter.ts +16 -3
  55. package/src/feishu/MergedForwardMessage.ts +5 -1
  56. package/src/index.ts +4 -2
  57. package/src/logging/errorLogValue.ts +26 -0
  58. package/src/logging/logger.ts +4 -0
  59. package/src/presentation/OutboundRouter.ts +15 -3
  60. package/src/proxy/ProxySessionController.ts +1003 -30
  61. package/src/runtime/types.ts +7 -0
  62. package/src/state/StateStore.ts +11 -0
  63. package/src/supervision/SafeRestartScheduler.ts +1 -1
@@ -3,6 +3,7 @@ import { promises as fs } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
6
+ import { SystemThreadWriterProcessController, } from "../codex/ThreadWriterProcess.js";
6
7
  import { resolveUserPath } from "../config/paths.js";
7
8
  import { DEFAULT_GROUP_NAME_FORMAT, } from "../config/schema.js";
8
9
  import { CommandRouter } from "../commands/CommandRouter.js";
@@ -14,12 +15,21 @@ import { formatGroupNameDate, formatNewGroupName, parseTaskNameFromGroupName, }
14
15
  import { normalizeFeishuPostText } from "../feishu/InboundText.js";
15
16
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
16
17
  import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
18
+ import { errorLogValue } from "../logging/errorLogValue.js";
17
19
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
18
20
  import { isActiveShellCommandJob, ShellCommandJobManager, } from "../shell/ShellCommandJobManager.js";
19
21
  import { createId } from "../utils/id.js";
20
22
  import { truncateMiddle, truncateText } from "../utils/markdown.js";
21
23
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
22
24
  import { executeShellCommand, } from "../utils/executeShellCommand.js";
25
+ class PresentedRequestError extends Error {
26
+ originalError;
27
+ constructor(originalError) {
28
+ super(originalError instanceof Error ? originalError.message : String(originalError), { cause: originalError });
29
+ this.originalError = originalError;
30
+ this.name = "PresentedRequestError";
31
+ }
32
+ }
23
33
  const MESSAGE_RECEIVED_REACTION = "OnIt";
24
34
  const MESSAGE_COMPLETED_REACTION = "DONE";
25
35
  const MESSAGE_FAILED_REACTION = "ERROR";
@@ -28,12 +38,15 @@ const SESSION_PAGE_SIZE = 10;
28
38
  const SESSION_PROMPT_PREVIEW_LENGTH = 50;
29
39
  const DIRECTORY_PAGE_SIZE = 15;
30
40
  const RESET_HISTORY_PAGE_SIZE = 10;
41
+ const REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS = 30_000;
31
42
  const MAX_LLM_TURN_RETRIES = 3;
32
43
  const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
33
44
  const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
34
45
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
35
46
  const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
36
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;
37
50
  const FORWARD_ATTACHMENT_WINDOW_MS = 800;
38
51
  const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
39
52
  const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
@@ -156,6 +169,10 @@ const HELP_COMMAND_SECTIONS = [
156
169
  usage: "[--force]",
157
170
  description: "默认安全重启;--force 立即重启",
158
171
  },
172
+ {
173
+ command: "/release",
174
+ description: "释放 Agent Bot 占用的 App Server 任务",
175
+ },
159
176
  {
160
177
  command: "/mute",
161
178
  usage: "[on|off]",
@@ -178,6 +195,7 @@ export class ProxySessionController {
178
195
  shellCommandExecutor;
179
196
  windowsDriveLister;
180
197
  shellCommandJobs;
198
+ threadWriterProcesses;
181
199
  router = new CommandRouter();
182
200
  cardRenderer = new CardRenderer();
183
201
  messageQueues = new Map();
@@ -187,6 +205,7 @@ export class ProxySessionController {
187
205
  queuedPromptCardWrites = new Map();
188
206
  sessionResets = new Map();
189
207
  resetHistoryOperations = new Map();
208
+ remoteTurnHistoryRefreshes = new Map();
190
209
  lastSessionListings = new Map();
191
210
  threadInitializations = new Map();
192
211
  llmRetryingSessions = new Set();
@@ -197,11 +216,13 @@ export class ProxySessionController {
197
216
  pendingResourceForwards = new Map();
198
217
  relatedReactionMessageIds = new Map();
199
218
  shellCommandJobMonitors = new Map();
219
+ appServerReleaseSchedules = new Map();
220
+ nextAppServerReleaseScheduleId = 1;
200
221
  unsubscribe = [];
201
222
  recoveryActivityHeartbeat;
202
223
  startupRecovery;
203
224
  queuedPromptsRestored = false;
204
- constructor(config, store, runtimes, outbound, logger, lifecycle, shellCommandExecutor = executeShellCommand, windowsDriveLister = listWindowsDriveRoots, shellCommandJobs = new ShellCommandJobManager(path.join(path.dirname(config.storage.sqlitePath), "command-jobs"))) {
225
+ constructor(config, store, runtimes, outbound, logger, lifecycle, shellCommandExecutor = executeShellCommand, windowsDriveLister = listWindowsDriveRoots, shellCommandJobs = new ShellCommandJobManager(path.join(path.dirname(config.storage.sqlitePath), "command-jobs")), threadWriterProcesses = new SystemThreadWriterProcessController()) {
205
226
  this.config = config;
206
227
  this.store = store;
207
228
  this.runtimes = runtimes;
@@ -211,6 +232,7 @@ export class ProxySessionController {
211
232
  this.shellCommandExecutor = shellCommandExecutor;
212
233
  this.windowsDriveLister = windowsDriveLister;
213
234
  this.shellCommandJobs = shellCommandJobs;
235
+ this.threadWriterProcesses = threadWriterProcesses;
214
236
  for (const [, runtime] of this.runtimes.entries()) {
215
237
  this.unsubscribe.push(runtime.onEvent((event) => {
216
238
  void this.handleRuntimeEvent(event).catch((error) => {
@@ -328,7 +350,11 @@ export class ProxySessionController {
328
350
  : undefined;
329
351
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
330
352
  const detail = result.error instanceof Error ? result.error.message : String(result.error);
331
- await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`)));
353
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`), {
354
+ requestId: message.messageId,
355
+ messageId: message.messageId,
356
+ phase: "merged_forward_read",
357
+ }));
332
358
  return;
333
359
  }
334
360
  const instruction = message.displayText ?? DEFAULT_MERGED_FORWARD_INSTRUCTION;
@@ -360,7 +386,11 @@ export class ProxySessionController {
360
386
  : undefined;
361
387
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
362
388
  const detail = error instanceof Error ? error.message : String(error);
363
- await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`)));
389
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`), {
390
+ requestId: message.messageId,
391
+ messageId: message.messageId,
392
+ phase: "referenced_message_read",
393
+ }));
364
394
  return;
365
395
  }
366
396
  }
@@ -394,7 +424,11 @@ export class ProxySessionController {
394
424
  }
395
425
  catch (error) {
396
426
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
397
- await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error));
427
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error, {
428
+ requestId: message.messageId,
429
+ messageId: message.messageId,
430
+ phase: "image_download",
431
+ }));
398
432
  return;
399
433
  }
400
434
  }
@@ -413,7 +447,11 @@ export class ProxySessionController {
413
447
  }
414
448
  catch (error) {
415
449
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
416
- await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error));
450
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error, {
451
+ requestId: message.messageId,
452
+ messageId: message.messageId,
453
+ phase: "file_download",
454
+ }));
417
455
  return;
418
456
  }
419
457
  }
@@ -426,9 +464,21 @@ export class ProxySessionController {
426
464
  }
427
465
  catch (error) {
428
466
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
429
- await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error));
467
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error, {
468
+ requestId: message.messageId,
469
+ messageId: message.messageId,
470
+ phase: "command_parse",
471
+ }));
430
472
  return;
431
473
  }
474
+ if (command.type !== "prompt") {
475
+ this.logger.info({
476
+ requestId: message.messageId,
477
+ messageId: message.messageId,
478
+ contextKey: message.contextKey,
479
+ command: command.type,
480
+ }, "Processing Agent Bot command.");
481
+ }
432
482
  // Operational and read-only commands must remain available even if a prompt operation is slow.
433
483
  if (isQueueIndependentCommand(command)) {
434
484
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
@@ -442,7 +492,12 @@ export class ProxySessionController {
442
492
  }
443
493
  catch (error) {
444
494
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
445
- await this.sendError(message.contextKey, error);
495
+ await this.sendError(message.contextKey, error, {
496
+ requestId: message.messageId,
497
+ messageId: message.messageId,
498
+ command: command.type,
499
+ phase: "command_execute",
500
+ });
446
501
  }
447
502
  });
448
503
  return;
@@ -450,16 +505,41 @@ export class ProxySessionController {
450
505
  const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
451
506
  const next = previous.catch(() => undefined).then(() => this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
452
507
  try {
453
- if (command.type === "prompt")
508
+ let executableCommand = command;
509
+ let executableImagePaths = localImagePaths;
510
+ let executableMessage = message;
511
+ let injectedRootMessageId;
512
+ if (command.type === "prompt") {
454
513
  await this.ensureThreadFork(message);
455
- 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
+ }
456
531
  if (!commandDefersReactionFinalization(command)) {
457
532
  await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
458
533
  }
459
534
  }
460
535
  catch (error) {
461
536
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
462
- await this.sendError(message.contextKey, error);
537
+ await this.sendError(message.contextKey, error, {
538
+ requestId: message.messageId,
539
+ messageId: message.messageId,
540
+ command: command.type,
541
+ phase: "command_execute",
542
+ });
463
543
  }
464
544
  }));
465
545
  this.messageQueues.set(message.contextKey, next);
@@ -479,7 +559,11 @@ export class ProxySessionController {
479
559
  resolvedAction = this.resolveCardActionBinding(action);
480
560
  }
481
561
  catch (error) {
482
- await this.sendError(action.contextKey, error);
562
+ await this.sendError(action.contextKey, error, {
563
+ requestId: action.actionId,
564
+ actionId: action.actionId,
565
+ phase: "card_action_resolve",
566
+ });
483
567
  return;
484
568
  }
485
569
  const contextKey = this.cardActionContextKey(resolvedAction);
@@ -510,6 +594,24 @@ export class ProxySessionController {
510
594
  else if (kind === "shell_command_cancel") {
511
595
  await this.cancelShellCommandJob(contextKey, String(scopedAction.value.jobId ?? ""));
512
596
  }
597
+ else if (kind === "thread_writer_close") {
598
+ await this.closeThreadWriter(scopedAction, contextKey);
599
+ }
600
+ else if (kind === "thread_writer_fork") {
601
+ const sessionId = String(scopedAction.value.sessionId ?? "");
602
+ const threadId = String(scopedAction.value.threadId ?? "");
603
+ const record = this.requireSession(contextKey, sessionId);
604
+ if (!threadId || record.remoteSessionId !== threadId) {
605
+ throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
606
+ }
607
+ await this.forkSessionReference(contextKey, sessionId);
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
+ }
513
615
  else if (kind === "turn_reset") {
514
616
  const sessionId = String(scopedAction.value.sessionId ?? "");
515
617
  const turnId = String(scopedAction.value.turnId ?? "");
@@ -734,7 +836,12 @@ export class ProxySessionController {
734
836
  }
735
837
  }
736
838
  catch (error) {
737
- await this.sendError(contextKey, error);
839
+ await this.sendError(contextKey, error, {
840
+ requestId: scopedAction.actionId,
841
+ actionId: scopedAction.actionId,
842
+ command: String(scopedAction.value.action ?? "unknown"),
843
+ phase: "card_action_execute",
844
+ });
738
845
  }
739
846
  });
740
847
  }
@@ -760,6 +867,7 @@ export class ProxySessionController {
760
867
  this.queuedPromptCardWrites.clear();
761
868
  this.sessionResets.clear();
762
869
  this.resetHistoryOperations.clear();
870
+ this.remoteTurnHistoryRefreshes.clear();
763
871
  for (const pending of this.pendingMergedForwards.values())
764
872
  pending.resolveAttachment(undefined);
765
873
  this.pendingMergedForwards.clear();
@@ -773,6 +881,11 @@ export class ProxySessionController {
773
881
  for (const timer of this.recoveryRetryTimers.values())
774
882
  clearTimeout(timer);
775
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();
776
889
  for (const [jobId] of this.shellCommandJobMonitors)
777
890
  this.stopShellCommandJobMonitor(jobId);
778
891
  clearInterval(this.recoveryActivityHeartbeat);
@@ -784,6 +897,19 @@ export class ProxySessionController {
784
897
  await this.cancelSession(record);
785
898
  return `Task stop requested: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
786
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
+ }
787
913
  async controlArchiveTask(localSessionId) {
788
914
  const record = this.requireControlSession(localSessionId);
789
915
  return this.archiveSessionReference(this.controlSessionContextKey(record), localSessionId, {
@@ -1377,6 +1503,9 @@ export class ProxySessionController {
1377
1503
  throw new Error("当前运行方式不支持自动重启。");
1378
1504
  await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
1379
1505
  return;
1506
+ case "release":
1507
+ await this.requestAppServerRelease(contextKey);
1508
+ return;
1380
1509
  case "mute":
1381
1510
  await this.setGroupMute(contextKey, command.enabled);
1382
1511
  return;
@@ -1809,6 +1938,9 @@ export class ProxySessionController {
1809
1938
  loaded = await this.loadSession(record);
1810
1939
  }
1811
1940
  catch (error) {
1941
+ if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
1942
+ throw new PresentedRequestError(error);
1943
+ }
1812
1944
  await this.outbound.failPendingTurn(record.localSessionId, error instanceof Error ? error.message : String(error));
1813
1945
  throw error;
1814
1946
  }
@@ -2021,6 +2153,49 @@ export class ProxySessionController {
2021
2153
  }
2022
2154
  }
2023
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
+ }
2024
2199
  async forkThreadSession(message) {
2025
2200
  const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
2026
2201
  const agent = this.ensureAgent(source.agentName);
@@ -2515,6 +2690,38 @@ export class ProxySessionController {
2515
2690
  this.logger.warn({ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId }, "Failed to hydrate legacy Fork Turn history; rendering the locally available history.");
2516
2691
  }
2517
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
+ }
2518
2725
  async createProjectSessionFromReference(contextKey, reference) {
2519
2726
  const resolved = await this.resolveProjectSessionReference(contextKey, reference);
2520
2727
  const created = await this.createSession(contextKey, resolved.agentName, resolved.cwd, true, false, undefined, undefined, undefined, resolved.executionSettings);
@@ -2900,6 +3107,8 @@ export class ProxySessionController {
2900
3107
  });
2901
3108
  }
2902
3109
  catch (error) {
3110
+ if (activeWriterThreadId(error))
3111
+ throw error;
2903
3112
  if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error)))
2904
3113
  throw error;
2905
3114
  this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
@@ -4197,6 +4406,7 @@ export class ProxySessionController {
4197
4406
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
4198
4407
  }
4199
4408
  await this.hydrateLegacyForkTurnHistory(contextKey, current);
4409
+ await this.hydrateRemoteTurnHistory(contextKey, current);
4200
4410
  const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
4201
4411
  const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
4202
4412
  const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
@@ -4514,7 +4724,7 @@ export class ProxySessionController {
4514
4724
  if (!runtime.listRemoteSessions)
4515
4725
  return;
4516
4726
  try {
4517
- const result = await runtime.listRemoteSessions({ searchTerm, limit: fetchLimit });
4727
+ const result = await this.listRemoteSessionsThroughCursor(runtime, searchTerm, fetchLimit);
4518
4728
  remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
4519
4729
  remoteHasMore ||= Boolean(result.nextCursor);
4520
4730
  }
@@ -4712,6 +4922,37 @@ export class ProxySessionController {
4712
4922
  }
4713
4923
  }
4714
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
+ }
4715
4956
  async refreshSessionsCardFromAction(action, options = {}) {
4716
4957
  if (!action.messageId)
4717
4958
  return;
@@ -5262,14 +5503,26 @@ export class ProxySessionController {
5262
5503
  description: command.description,
5263
5504
  })),
5264
5505
  }));
5265
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard("Agent Bot 使用帮助", [
5506
+ const introLines = [
5266
5507
  "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
5267
5508
  "点击命令按钮可执行默认形式;有必填参数的命令需要手动发送。",
5268
5509
  "> 命令前缀示例:/sess 等同于 /sessions。",
5269
5510
  "> 命令缩写示例:/fg 等同于 /forkgroup。",
5270
5511
  "前缀或缩写匹配多个命令时,需要输入更长的形式。",
5271
5512
  "**[参数]** 可选 **&#60;参数&#62;** 必填",
5272
- ], sections));
5513
+ ];
5514
+ try {
5515
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard("Agent Bot 使用帮助", introLines, sections));
5516
+ }
5517
+ catch (cardError) {
5518
+ this.logger.warn({ error: cardError, contextKey, command: "help", phase: "help_card_send" }, "Failed to send the help card; falling back to plain text.");
5519
+ try {
5520
+ await this.outbound.sendText(contextKey, renderPlainHelp(introLines, sections));
5521
+ }
5522
+ catch (textError) {
5523
+ throw new AggregateError([cardError, textError], "帮助卡片和纯文本帮助均发送失败。");
5524
+ }
5525
+ }
5273
5526
  }
5274
5527
  async setGroupMute(contextKey, enabled) {
5275
5528
  const chatContextKey = baseChatContextKey(contextKey);
@@ -5375,11 +5628,506 @@ export class ProxySessionController {
5375
5628
  throw new Error(`找不到任务:${sessionId}`);
5376
5629
  return record;
5377
5630
  }
5378
- async sendError(contextKey, error) {
5379
- this.logger.warn({ error, contextKey }, "Request failed.");
5380
- await this.outbound.sendText(contextKey, error instanceof Error ? error.message : String(error));
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
+ }
5900
+ async presentThreadWriterConflict(record, contextKey, error, replacePendingTurn) {
5901
+ if (contextKey.startsWith("console:"))
5902
+ return false;
5903
+ const threadId = activeWriterThreadId(error);
5904
+ if (!threadId || record.remoteSessionId !== threadId)
5905
+ return false;
5906
+ const runtime = this.runtimes.forAgent(record.agentName);
5907
+ const lockPath = runtime.getThreadWriterLockPath?.(threadId);
5908
+ let owners = [];
5909
+ if (lockPath) {
5910
+ try {
5911
+ owners = await this.threadWriterProcesses.inspect(lockPath);
5912
+ }
5913
+ catch (inspectionError) {
5914
+ this.logger.warn({ error: inspectionError, threadId, lockPath }, "Failed to inspect the process holding a thread writer lock.");
5915
+ }
5916
+ }
5917
+ const owner = owners.length === 1 ? owners[0] : undefined;
5918
+ const applicationIdle = owner && lockPath
5919
+ ? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, owner)
5920
+ : undefined;
5921
+ const card = this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(record, contextKey, threadId, owners, "occupied", lockPath
5922
+ ? undefined
5923
+ : "无法识别占用进程。", applicationIdle));
5924
+ if (replacePendingTurn) {
5925
+ const replaced = await this.outbound.failPendingTurn(record.localSessionId, "任务被占用。", card);
5926
+ if (!replaced)
5927
+ await this.outbound.sendInteractiveCard(contextKey, card);
5928
+ }
5929
+ else {
5930
+ await this.outbound.sendInteractiveCard(contextKey, card);
5931
+ }
5932
+ return true;
5933
+ }
5934
+ threadWriterConflictView(record, contextKey, threadId, owners, status, notice, applicationIdle) {
5935
+ const owner = owners.length === 1 ? owners[0] : undefined;
5936
+ return {
5937
+ status,
5938
+ contextKey,
5939
+ sessionId: record.localSessionId,
5940
+ threadId,
5941
+ taskTitle: record.title,
5942
+ notice: notice ?? (owners.length > 1
5943
+ ? `检测到 ${owners.length} 个相关进程,无法安全地自动关闭。`
5944
+ : undefined),
5945
+ applicationIdle,
5946
+ ...(owner ? {
5947
+ owner: {
5948
+ displayName: owner.displayName,
5949
+ writerPid: owner.writerPid,
5950
+ writerProcessName: owner.writerProcessName,
5951
+ writerStartedAt: owner.writerStartedAt,
5952
+ applicationPid: owner.applicationPid,
5953
+ applicationProcessName: owner.applicationProcessName,
5954
+ applicationStartedAt: owner.applicationStartedAt,
5955
+ canClose: owner.canClose,
5956
+ },
5957
+ } : {}),
5958
+ };
5959
+ }
5960
+ async closeThreadWriter(action, contextKey) {
5961
+ const messageId = requiredCardMessageId(action.messageId);
5962
+ const sessionId = String(action.value.sessionId ?? "");
5963
+ const threadId = String(action.value.threadId ?? "");
5964
+ const expectedWriterPid = positiveActionPid(action.value.writerPid);
5965
+ const expectedApplicationPid = positiveActionPid(action.value.applicationPid);
5966
+ const expectedWriterStartedAt = String(action.value.writerStartedAt ?? "");
5967
+ const expectedApplicationStartedAt = String(action.value.applicationStartedAt ?? "");
5968
+ const force = action.value.force === true || action.value.force === "true";
5969
+ const record = this.requireSession(contextKey, sessionId);
5970
+ if (!threadId || record.remoteSessionId !== threadId) {
5971
+ throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
5972
+ }
5973
+ const runtime = this.runtimes.forAgent(record.agentName);
5974
+ const lockPath = runtime.getThreadWriterLockPath?.(threadId);
5975
+ if (!lockPath)
5976
+ throw new Error("当前 Agent 不支持自动关闭占用程序。");
5977
+ let owners = await this.threadWriterProcesses.inspect(lockPath);
5978
+ const expectedOwner = owners.length === 1 && threadWriterMatches(owners[0], expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt) ? owners[0] : undefined;
5979
+ if (!expectedOwner) {
5980
+ const currentOwner = owners.length === 1 ? owners[0] : undefined;
5981
+ const applicationIdle = currentOwner
5982
+ ? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
5983
+ : undefined;
5984
+ await this.outbound.updateInteractiveCard(contextKey, messageId, this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(record, contextKey, threadId, owners, owners.length === 0 ? "released" : "occupied", owners.length === 0
5985
+ ? "请重新发送消息。"
5986
+ : "占用进程已变化,请重试。", applicationIdle)));
5987
+ return;
5988
+ }
5989
+ if (!expectedOwner.canClose)
5990
+ throw new Error("无法确认占用者是 Agent 程序,已拒绝关闭。");
5991
+ try {
5992
+ await this.threadWriterProcesses.close(expectedOwner, force);
5993
+ }
5994
+ catch (error) {
5995
+ owners = await this.threadWriterProcesses.inspect(lockPath);
5996
+ if (owners.some((owner) => threadWriterMatches(owner, expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt)))
5997
+ throw error;
5998
+ }
5999
+ owners = await this.waitForThreadWriterChange(lockPath, expectedOwner);
6000
+ const sameOwnerRemains = owners.some((owner) => threadWriterMatches(owner, expectedOwner.writerPid, expectedOwner.writerStartedAt ?? "", expectedOwner.applicationPid, expectedOwner.applicationStartedAt ?? ""));
6001
+ if (sameOwnerRemains && force) {
6002
+ throw new Error(`无法关闭 ${expectedOwner.displayName}(PID ${expectedOwner.applicationPid})。`);
6003
+ }
6004
+ const status = owners.length === 0
6005
+ ? "released"
6006
+ : sameOwnerRemains ? "force_required" : "occupied";
6007
+ const notice = owners.length === 0
6008
+ ? "请重新发送消息。"
6009
+ : sameOwnerRemains
6010
+ ? "普通关闭失败,可强制关闭。"
6011
+ : "占用进程已变化,请重试。";
6012
+ const currentOwner = owners.length === 1 ? owners[0] : undefined;
6013
+ const applicationIdle = currentOwner
6014
+ ? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
6015
+ : undefined;
6016
+ await this.outbound.updateInteractiveCard(contextKey, messageId, this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(record, contextKey, threadId, owners, status, notice, applicationIdle)));
6017
+ }
6018
+ async inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, owner) {
6019
+ if (!owner.applicationStartedAt
6020
+ || !this.threadWriterProcesses.inspectApplicationThreadIds
6021
+ || !runtime.inspectRemoteSessionActivity)
6022
+ return undefined;
6023
+ try {
6024
+ const ownedThreadIds = await this.threadWriterProcesses.inspectApplicationThreadIds(lockPath, owner);
6025
+ if (!ownedThreadIds.includes(threadId))
6026
+ return undefined;
6027
+ const activities = await Promise.all(ownedThreadIds.map((ownedThreadId) => runtime.inspectRemoteSessionActivity(ownedThreadId)));
6028
+ return activities.every((activity) => !activity.active);
6029
+ }
6030
+ catch (error) {
6031
+ this.logger.warn({ error, threadId, applicationPid: owner.applicationPid }, "Failed to inspect thread-writer application activity.");
6032
+ return undefined;
6033
+ }
6034
+ }
6035
+ async waitForThreadWriterChange(lockPath, expectedOwner) {
6036
+ const deadline = Date.now() + 5_000;
6037
+ let owners = [];
6038
+ do {
6039
+ owners = await this.threadWriterProcesses.inspect(lockPath);
6040
+ if (!owners.some((owner) => threadWriterMatches(owner, expectedOwner.writerPid, expectedOwner.writerStartedAt ?? "", expectedOwner.applicationPid, expectedOwner.applicationStartedAt ?? "")))
6041
+ return owners;
6042
+ await delay(250);
6043
+ } while (Date.now() < deadline);
6044
+ return owners;
6045
+ }
6046
+ async sendError(contextKey, error, details = {}) {
6047
+ if (error instanceof PresentedRequestError) {
6048
+ this.logger.warn({ error: errorLogValue(error.originalError), contextKey, ...details }, "Request failed after a dedicated error card was presented.");
6049
+ return;
6050
+ }
6051
+ const record = this.currentSession(contextKey);
6052
+ if (record) {
6053
+ try {
6054
+ if (await this.presentThreadWriterConflict(record, contextKey, error, false))
6055
+ return;
6056
+ }
6057
+ catch (presentationError) {
6058
+ this.logger.warn({ error: presentationError, originalError: errorLogValue(error), contextKey, ...details }, "Failed to present the thread writer conflict card.");
6059
+ }
6060
+ }
6061
+ this.logger.warn({ error, contextKey, ...details }, "Request failed.");
6062
+ try {
6063
+ await this.outbound.sendText(contextKey, error instanceof Error ? error.message : String(error));
6064
+ }
6065
+ catch (deliveryError) {
6066
+ this.logger.error({
6067
+ error: deliveryError,
6068
+ originalError: errorLogValue(error),
6069
+ contextKey,
6070
+ ...details,
6071
+ phase: "error_response_send",
6072
+ }, "Failed to deliver the request failure to the user.");
6073
+ }
5381
6074
  }
5382
6075
  }
6076
+ function activeWriterThreadId(error) {
6077
+ const visited = new Set();
6078
+ let current = error;
6079
+ while (current !== undefined && current !== null && !visited.has(current)) {
6080
+ visited.add(current);
6081
+ const message = current instanceof Error
6082
+ ? current.message
6083
+ : typeof current === "string" ? current : undefined;
6084
+ const match = message?.match(/thread(?:\/resume failed:\s*thread)?\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s+already has an active writer/iu);
6085
+ if (match?.[1])
6086
+ return match[1];
6087
+ current = current instanceof Error ? current.cause : undefined;
6088
+ }
6089
+ return undefined;
6090
+ }
6091
+ function positiveActionPid(value) {
6092
+ const pid = typeof value === "number" ? value : Number(value);
6093
+ if (!Number.isSafeInteger(pid) || pid <= 4) {
6094
+ throw new Error("任务占用卡片中的进程信息无效,请重新发送消息获取最新状态。");
6095
+ }
6096
+ return pid;
6097
+ }
6098
+ function threadWriterMatches(owner, writerPid, writerStartedAt, applicationPid, applicationStartedAt) {
6099
+ return Boolean(writerStartedAt)
6100
+ && Boolean(applicationStartedAt)
6101
+ && owner.writerPid === writerPid
6102
+ && owner.writerStartedAt === writerStartedAt
6103
+ && owner.applicationPid === applicationPid
6104
+ && owner.applicationStartedAt === applicationStartedAt;
6105
+ }
6106
+ function delay(milliseconds) {
6107
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
6108
+ }
6109
+ function renderPlainHelp(introLines, sections) {
6110
+ return [
6111
+ "Agent Bot 使用帮助",
6112
+ "",
6113
+ ...introLines.map((line) => decodeHelpEntities(line.replace(/^>\s*/u, "").replaceAll("**", ""))),
6114
+ ...sections.flatMap((section) => [
6115
+ "",
6116
+ section.title,
6117
+ ...section.commands.map((command) => [
6118
+ command.text,
6119
+ command.usage ? decodeHelpEntities(command.usage) : "",
6120
+ `- ${command.description}`,
6121
+ ].filter(Boolean).join(" ")),
6122
+ ]),
6123
+ ].join("\n");
6124
+ }
6125
+ function decodeHelpEntities(value) {
6126
+ return value
6127
+ .replaceAll("&#60;", "<")
6128
+ .replaceAll("&#62;", ">")
6129
+ .replaceAll("&amp;", "&");
6130
+ }
5383
6131
  function threadForkAnchorMessageIds(message) {
5384
6132
  return [...new Set([message.rootMessageId, message.parentMessageId]
5385
6133
  .filter((messageId) => Boolean(messageId && messageId !== message.messageId)))];
@@ -5865,7 +6613,7 @@ function isBotOwnedActiveTurn(record, remote) {
5865
6613
  && record.lastTurnId === remote.lastTurnId;
5866
6614
  }
5867
6615
  function isQueueIndependentCommand(command) {
5868
- 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))
5869
6617
  return true;
5870
6618
  if (command.type === "agent")
5871
6619
  return command.agent === undefined;
@@ -6077,6 +6825,17 @@ function mergedForwardPrompt(instruction, transcript) {
6077
6825
  function referencedMessagePrompt(instruction, referencedMessage) {
6078
6826
  return `${instruction}\n\n引用消息:\n${referencedMessage}`;
6079
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
+ }
6080
6839
  function quotedMessageId(message) {
6081
6840
  const messageId = message.parentMessageId?.trim();
6082
6841
  if (!messageId || message.contextKey.startsWith("console:"))