@keyou007/agent-bot 0.1.18-alpha.3 → 0.1.19-alpha.0

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 (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/codex/CodexRuntime.js +8 -3
  3. package/dist/codex/CodexRuntime.js.map +1 -1
  4. package/dist/codex/ThreadWriterProcess.js +344 -0
  5. package/dist/codex/ThreadWriterProcess.js.map +1 -0
  6. package/dist/console/ConsoleTurnPresenter.js +1 -0
  7. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  8. package/dist/feishu/CardRenderer.js +83 -0
  9. package/dist/feishu/CardRenderer.js.map +1 -1
  10. package/dist/feishu/FeishuTurnPresenter.js +13 -3
  11. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/logging/errorLogValue.js +18 -0
  15. package/dist/logging/errorLogValue.js.map +1 -0
  16. package/dist/logging/logger.js +4 -0
  17. package/dist/logging/logger.js.map +1 -1
  18. package/dist/presentation/OutboundRouter.js +2 -2
  19. package/dist/presentation/OutboundRouter.js.map +1 -1
  20. package/dist/proxy/ProxySessionController.js +329 -15
  21. package/dist/proxy/ProxySessionController.js.map +1 -1
  22. package/dist/supervision/SafeRestartScheduler.js +1 -1
  23. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  24. package/npm-shrinkwrap.json +2 -2
  25. package/package.json +1 -1
  26. package/src/codex/CodexRuntime.ts +8 -2
  27. package/src/codex/ThreadWriterProcess.ts +385 -0
  28. package/src/console/ConsoleTurnPresenter.ts +2 -1
  29. package/src/feishu/CardRenderer.ts +109 -0
  30. package/src/feishu/FeishuTurnPresenter.ts +16 -3
  31. package/src/index.ts +2 -2
  32. package/src/logging/errorLogValue.ts +26 -0
  33. package/src/logging/logger.ts +4 -0
  34. package/src/presentation/OutboundRouter.ts +15 -3
  35. package/src/proxy/ProxySessionController.ts +459 -24
  36. package/src/runtime/types.ts +1 -0
  37. package/src/supervision/SafeRestartScheduler.ts +1 -1
@@ -4,6 +4,11 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import type { Logger } from "pino";
6
6
  import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
7
+ import {
8
+ SystemThreadWriterProcessController,
9
+ type ThreadWriterProcess,
10
+ type ThreadWriterProcessController,
11
+ } from "../codex/ThreadWriterProcess.js";
7
12
  import { resolveUserPath } from "../config/paths.js";
8
13
  import {
9
14
  DEFAULT_GROUP_NAME_FORMAT,
@@ -31,6 +36,7 @@ import {
31
36
  type SessionTaskCardGroup,
32
37
  type ShellCommandCardView,
33
38
  type TaskListCardAction,
39
+ type ThreadWriterConflictCardView,
34
40
  } from "../feishu/CardRenderer.js";
35
41
  import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
36
42
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
@@ -42,6 +48,7 @@ import {
42
48
  import { normalizeFeishuPostText } from "../feishu/InboundText.js";
43
49
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
44
50
  import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
51
+ import { errorLogValue } from "../logging/errorLogValue.js";
45
52
  import type { OutboundRouter } from "../presentation/OutboundRouter.js";
46
53
  import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
47
54
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
@@ -116,6 +123,21 @@ interface ShellCommandJobMonitor {
116
123
  refreshing: boolean;
117
124
  }
118
125
 
126
+ interface RequestFailureDetails {
127
+ requestId?: string;
128
+ messageId?: string;
129
+ actionId?: string;
130
+ command?: string;
131
+ phase?: string;
132
+ }
133
+
134
+ class PresentedRequestError extends Error {
135
+ constructor(public readonly originalError: unknown) {
136
+ super(originalError instanceof Error ? originalError.message : String(originalError), { cause: originalError });
137
+ this.name = "PresentedRequestError";
138
+ }
139
+ }
140
+
119
141
  const MESSAGE_RECEIVED_REACTION = "OnIt";
120
142
  const MESSAGE_COMPLETED_REACTION = "DONE";
121
143
  const MESSAGE_FAILED_REACTION = "ERROR";
@@ -452,6 +474,7 @@ export class ProxySessionController {
452
474
  private readonly shellCommandJobs: ShellCommandJobManagerLike = new ShellCommandJobManager(
453
475
  path.join(path.dirname(config.storage.sqlitePath), "command-jobs"),
454
476
  ),
477
+ private readonly threadWriterProcesses: ThreadWriterProcessController = new SystemThreadWriterProcessController(),
455
478
  ) {
456
479
  for (const [, runtime] of this.runtimes.entries()) {
457
480
  this.unsubscribe.push(
@@ -608,7 +631,11 @@ export class ProxySessionController {
608
631
  await this.outbound.withReplyTarget(
609
632
  message.contextKey,
610
633
  replyTarget,
611
- () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`)),
634
+ () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`), {
635
+ requestId: message.messageId,
636
+ messageId: message.messageId,
637
+ phase: "merged_forward_read",
638
+ }),
612
639
  );
613
640
  return;
614
641
  }
@@ -643,7 +670,11 @@ export class ProxySessionController {
643
670
  await this.outbound.withReplyTarget(
644
671
  message.contextKey,
645
672
  replyTarget,
646
- () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`)),
673
+ () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`), {
674
+ requestId: message.messageId,
675
+ messageId: message.messageId,
676
+ phase: "referenced_message_read",
677
+ }),
647
678
  );
648
679
  return;
649
680
  }
@@ -681,7 +712,11 @@ export class ProxySessionController {
681
712
  await this.outbound.withReplyTarget(
682
713
  message.contextKey,
683
714
  replyTarget,
684
- () => this.sendError(message.contextKey, error),
715
+ () => this.sendError(message.contextKey, error, {
716
+ requestId: message.messageId,
717
+ messageId: message.messageId,
718
+ phase: "image_download",
719
+ }),
685
720
  );
686
721
  return;
687
722
  }
@@ -708,7 +743,11 @@ export class ProxySessionController {
708
743
  await this.outbound.withReplyTarget(
709
744
  message.contextKey,
710
745
  replyTarget,
711
- () => this.sendError(message.contextKey, error),
746
+ () => this.sendError(message.contextKey, error, {
747
+ requestId: message.messageId,
748
+ messageId: message.messageId,
749
+ phase: "file_download",
750
+ }),
712
751
  );
713
752
  return;
714
753
  }
@@ -724,10 +763,25 @@ export class ProxySessionController {
724
763
  await this.outbound.withReplyTarget(
725
764
  message.contextKey,
726
765
  replyTarget,
727
- () => this.sendError(message.contextKey, error),
766
+ () => this.sendError(message.contextKey, error, {
767
+ requestId: message.messageId,
768
+ messageId: message.messageId,
769
+ phase: "command_parse",
770
+ }),
728
771
  );
729
772
  return;
730
773
  }
774
+ if (command.type !== "prompt") {
775
+ this.logger.info(
776
+ {
777
+ requestId: message.messageId,
778
+ messageId: message.messageId,
779
+ contextKey: message.contextKey,
780
+ command: command.type,
781
+ },
782
+ "Processing Agent Bot command.",
783
+ );
784
+ }
731
785
  // Operational and read-only commands must remain available even if a prompt operation is slow.
732
786
  if (isQueueIndependentCommand(command)) {
733
787
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
@@ -747,7 +801,12 @@ export class ProxySessionController {
747
801
  }
748
802
  } catch (error) {
749
803
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
750
- await this.sendError(message.contextKey, error);
804
+ await this.sendError(message.contextKey, error, {
805
+ requestId: message.messageId,
806
+ messageId: message.messageId,
807
+ command: command.type,
808
+ phase: "command_execute",
809
+ });
751
810
  }
752
811
  });
753
812
  return;
@@ -772,7 +831,12 @@ export class ProxySessionController {
772
831
  }
773
832
  } catch (error) {
774
833
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
775
- await this.sendError(message.contextKey, error);
834
+ await this.sendError(message.contextKey, error, {
835
+ requestId: message.messageId,
836
+ messageId: message.messageId,
837
+ command: command.type,
838
+ phase: "command_execute",
839
+ });
776
840
  }
777
841
  }),
778
842
  );
@@ -794,7 +858,11 @@ export class ProxySessionController {
794
858
  try {
795
859
  resolvedAction = this.resolveCardActionBinding(action);
796
860
  } catch (error) {
797
- await this.sendError(action.contextKey, error);
861
+ await this.sendError(action.contextKey, error, {
862
+ requestId: action.actionId,
863
+ actionId: action.actionId,
864
+ phase: "card_action_resolve",
865
+ });
798
866
  return;
799
867
  }
800
868
  const contextKey = this.cardActionContextKey(resolvedAction);
@@ -829,6 +897,16 @@ export class ProxySessionController {
829
897
  contextKey,
830
898
  String(scopedAction.value.jobId ?? ""),
831
899
  );
900
+ } else if (kind === "thread_writer_close") {
901
+ await this.closeThreadWriter(scopedAction, contextKey);
902
+ } else if (kind === "thread_writer_fork") {
903
+ const sessionId = String(scopedAction.value.sessionId ?? "");
904
+ const threadId = String(scopedAction.value.threadId ?? "");
905
+ const record = this.requireSession(contextKey, sessionId);
906
+ if (!threadId || record.remoteSessionId !== threadId) {
907
+ throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
908
+ }
909
+ await this.forkSessionReference(contextKey, sessionId);
832
910
  } else if (kind === "turn_reset") {
833
911
  const sessionId = String(scopedAction.value.sessionId ?? "");
834
912
  const turnId = String(scopedAction.value.turnId ?? "");
@@ -1069,7 +1147,12 @@ export class ProxySessionController {
1069
1147
  await this.resolveApproval(scopedAction);
1070
1148
  }
1071
1149
  } catch (error) {
1072
- await this.sendError(contextKey, error);
1150
+ await this.sendError(contextKey, error, {
1151
+ requestId: scopedAction.actionId,
1152
+ actionId: scopedAction.actionId,
1153
+ command: String(scopedAction.value.action ?? "unknown"),
1154
+ phase: "card_action_execute",
1155
+ });
1073
1156
  }
1074
1157
  });
1075
1158
  }
@@ -2401,6 +2484,9 @@ export class ProxySessionController {
2401
2484
  try {
2402
2485
  loaded = await this.loadSession(record);
2403
2486
  } catch (error) {
2487
+ if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
2488
+ throw new PresentedRequestError(error);
2489
+ }
2404
2490
  await this.outbound.failPendingTurn(record.localSessionId, error instanceof Error ? error.message : String(error));
2405
2491
  throw error;
2406
2492
  }
@@ -3809,6 +3895,7 @@ export class ProxySessionController {
3809
3895
  lastTurnStatus: record.lastTurnStatus,
3810
3896
  });
3811
3897
  } catch (error) {
3898
+ if (activeWriterThreadId(error)) throw error;
3812
3899
  if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error))) throw error;
3813
3900
  this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
3814
3901
  session = await runtime.createSession({
@@ -6509,18 +6596,34 @@ export class ProxySessionController {
6509
6596
  description: command.description,
6510
6597
  })),
6511
6598
  }));
6512
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard(
6513
- "Agent Bot 使用帮助",
6514
- [
6515
- "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
6516
- "点击命令按钮可执行默认形式;有必填参数的命令需要手动发送。",
6517
- "> 命令前缀示例:/sess 等同于 /sessions。",
6518
- "> 命令缩写示例:/fg 等同于 /forkgroup。",
6519
- "前缀或缩写匹配多个命令时,需要输入更长的形式。",
6520
- "**[参数]** 可选 **<参数>** 必填",
6521
- ],
6522
- sections,
6523
- ));
6599
+ const introLines = [
6600
+ "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
6601
+ "点击命令按钮可执行默认形式;有必填参数的命令需要手动发送。",
6602
+ "> 命令前缀示例:/sess 等同于 /sessions。",
6603
+ "> 命令缩写示例:/fg 等同于 /forkgroup。",
6604
+ "前缀或缩写匹配多个命令时,需要输入更长的形式。",
6605
+ "**[参数]** 可选 **<参数>** 必填",
6606
+ ];
6607
+ try {
6608
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard(
6609
+ "Agent Bot 使用帮助",
6610
+ introLines,
6611
+ sections,
6612
+ ));
6613
+ } catch (cardError) {
6614
+ this.logger.warn(
6615
+ { error: cardError, contextKey, command: "help", phase: "help_card_send" },
6616
+ "Failed to send the help card; falling back to plain text.",
6617
+ );
6618
+ try {
6619
+ await this.outbound.sendText(contextKey, renderPlainHelp(introLines, sections));
6620
+ } catch (textError) {
6621
+ throw new AggregateError(
6622
+ [cardError, textError],
6623
+ "帮助卡片和纯文本帮助均发送失败。",
6624
+ );
6625
+ }
6626
+ }
6524
6627
  }
6525
6628
 
6526
6629
  private async setGroupMute(contextKey: string, enabled: boolean): Promise<void> {
@@ -6636,10 +6739,342 @@ export class ProxySessionController {
6636
6739
  return record;
6637
6740
  }
6638
6741
 
6639
- private async sendError(contextKey: string, error: unknown): Promise<void> {
6640
- this.logger.warn({ error, contextKey }, "Request failed.");
6641
- await this.outbound.sendText(contextKey, error instanceof Error ? error.message : String(error));
6742
+ private async presentThreadWriterConflict(
6743
+ record: SessionRecord,
6744
+ contextKey: string,
6745
+ error: unknown,
6746
+ replacePendingTurn: boolean,
6747
+ ): Promise<boolean> {
6748
+ if (contextKey.startsWith("console:")) return false;
6749
+ const threadId = activeWriterThreadId(error);
6750
+ if (!threadId || record.remoteSessionId !== threadId) return false;
6751
+ const runtime = this.runtimes.forAgent(record.agentName);
6752
+ const lockPath = runtime.getThreadWriterLockPath?.(threadId);
6753
+ let owners: ThreadWriterProcess[] = [];
6754
+ if (lockPath) {
6755
+ try {
6756
+ owners = await this.threadWriterProcesses.inspect(lockPath);
6757
+ } catch (inspectionError) {
6758
+ this.logger.warn(
6759
+ { error: inspectionError, threadId, lockPath },
6760
+ "Failed to inspect the process holding a thread writer lock.",
6761
+ );
6762
+ }
6763
+ }
6764
+ const owner = owners.length === 1 ? owners[0] : undefined;
6765
+ const applicationIdle = owner && lockPath
6766
+ ? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, owner)
6767
+ : undefined;
6768
+ const card = this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(
6769
+ record,
6770
+ contextKey,
6771
+ threadId,
6772
+ owners,
6773
+ "occupied",
6774
+ lockPath
6775
+ ? undefined
6776
+ : "无法识别占用进程。",
6777
+ applicationIdle,
6778
+ ));
6779
+ if (replacePendingTurn) {
6780
+ const replaced = await this.outbound.failPendingTurn(
6781
+ record.localSessionId,
6782
+ "任务被占用。",
6783
+ card,
6784
+ );
6785
+ if (!replaced) await this.outbound.sendInteractiveCard(contextKey, card);
6786
+ } else {
6787
+ await this.outbound.sendInteractiveCard(contextKey, card);
6788
+ }
6789
+ return true;
6790
+ }
6791
+
6792
+ private threadWriterConflictView(
6793
+ record: SessionRecord,
6794
+ contextKey: string,
6795
+ threadId: string,
6796
+ owners: ThreadWriterProcess[],
6797
+ status: ThreadWriterConflictCardView["status"],
6798
+ notice?: string,
6799
+ applicationIdle?: boolean,
6800
+ ): ThreadWriterConflictCardView {
6801
+ const owner = owners.length === 1 ? owners[0] : undefined;
6802
+ return {
6803
+ status,
6804
+ contextKey,
6805
+ sessionId: record.localSessionId,
6806
+ threadId,
6807
+ taskTitle: record.title,
6808
+ notice: notice ?? (owners.length > 1
6809
+ ? `检测到 ${owners.length} 个相关进程,无法安全地自动关闭。`
6810
+ : undefined),
6811
+ applicationIdle,
6812
+ ...(owner ? {
6813
+ owner: {
6814
+ displayName: owner.displayName,
6815
+ writerPid: owner.writerPid,
6816
+ writerProcessName: owner.writerProcessName,
6817
+ writerStartedAt: owner.writerStartedAt,
6818
+ applicationPid: owner.applicationPid,
6819
+ applicationProcessName: owner.applicationProcessName,
6820
+ applicationStartedAt: owner.applicationStartedAt,
6821
+ canClose: owner.canClose,
6822
+ },
6823
+ } : {}),
6824
+ };
6642
6825
  }
6826
+
6827
+ private async closeThreadWriter(action: CardAction, contextKey: string): Promise<void> {
6828
+ const messageId = requiredCardMessageId(action.messageId);
6829
+ const sessionId = String(action.value.sessionId ?? "");
6830
+ const threadId = String(action.value.threadId ?? "");
6831
+ const expectedWriterPid = positiveActionPid(action.value.writerPid);
6832
+ const expectedApplicationPid = positiveActionPid(action.value.applicationPid);
6833
+ const expectedWriterStartedAt = String(action.value.writerStartedAt ?? "");
6834
+ const expectedApplicationStartedAt = String(action.value.applicationStartedAt ?? "");
6835
+ const force = action.value.force === true || action.value.force === "true";
6836
+ const record = this.requireSession(contextKey, sessionId);
6837
+ if (!threadId || record.remoteSessionId !== threadId) {
6838
+ throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
6839
+ }
6840
+ const runtime = this.runtimes.forAgent(record.agentName);
6841
+ const lockPath = runtime.getThreadWriterLockPath?.(threadId);
6842
+ if (!lockPath) throw new Error("当前 Agent 不支持自动关闭占用程序。");
6843
+
6844
+ let owners = await this.threadWriterProcesses.inspect(lockPath);
6845
+ const expectedOwner = owners.length === 1 && threadWriterMatches(
6846
+ owners[0],
6847
+ expectedWriterPid,
6848
+ expectedWriterStartedAt,
6849
+ expectedApplicationPid,
6850
+ expectedApplicationStartedAt,
6851
+ ) ? owners[0] : undefined;
6852
+ if (!expectedOwner) {
6853
+ const currentOwner = owners.length === 1 ? owners[0] : undefined;
6854
+ const applicationIdle = currentOwner
6855
+ ? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
6856
+ : undefined;
6857
+ await this.outbound.updateInteractiveCard(
6858
+ contextKey,
6859
+ messageId,
6860
+ this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(
6861
+ record,
6862
+ contextKey,
6863
+ threadId,
6864
+ owners,
6865
+ owners.length === 0 ? "released" : "occupied",
6866
+ owners.length === 0
6867
+ ? "请重新发送消息。"
6868
+ : "占用进程已变化,请重试。",
6869
+ applicationIdle,
6870
+ )),
6871
+ );
6872
+ return;
6873
+ }
6874
+ if (!expectedOwner.canClose) throw new Error("无法确认占用者是 Agent 程序,已拒绝关闭。");
6875
+
6876
+ try {
6877
+ await this.threadWriterProcesses.close(expectedOwner, force);
6878
+ } catch (error) {
6879
+ owners = await this.threadWriterProcesses.inspect(lockPath);
6880
+ if (owners.some((owner) => threadWriterMatches(
6881
+ owner,
6882
+ expectedWriterPid,
6883
+ expectedWriterStartedAt,
6884
+ expectedApplicationPid,
6885
+ expectedApplicationStartedAt,
6886
+ ))) throw error;
6887
+ }
6888
+ owners = await this.waitForThreadWriterChange(lockPath, expectedOwner);
6889
+ const sameOwnerRemains = owners.some((owner) => threadWriterMatches(
6890
+ owner,
6891
+ expectedOwner.writerPid,
6892
+ expectedOwner.writerStartedAt ?? "",
6893
+ expectedOwner.applicationPid,
6894
+ expectedOwner.applicationStartedAt ?? "",
6895
+ ));
6896
+ if (sameOwnerRemains && force) {
6897
+ throw new Error(`无法关闭 ${expectedOwner.displayName}(PID ${expectedOwner.applicationPid})。`);
6898
+ }
6899
+ const status: ThreadWriterConflictCardView["status"] = owners.length === 0
6900
+ ? "released"
6901
+ : sameOwnerRemains ? "force_required" : "occupied";
6902
+ const notice = owners.length === 0
6903
+ ? "请重新发送消息。"
6904
+ : sameOwnerRemains
6905
+ ? "普通关闭失败,可强制关闭。"
6906
+ : "占用进程已变化,请重试。";
6907
+ const currentOwner = owners.length === 1 ? owners[0] : undefined;
6908
+ const applicationIdle = currentOwner
6909
+ ? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
6910
+ : undefined;
6911
+ await this.outbound.updateInteractiveCard(
6912
+ contextKey,
6913
+ messageId,
6914
+ this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(
6915
+ record,
6916
+ contextKey,
6917
+ threadId,
6918
+ owners,
6919
+ status,
6920
+ notice,
6921
+ applicationIdle,
6922
+ )),
6923
+ );
6924
+ }
6925
+
6926
+ private async inspectThreadWriterApplicationIdle(
6927
+ runtime: AgentRuntime,
6928
+ lockPath: string,
6929
+ threadId: string,
6930
+ owner: ThreadWriterProcess,
6931
+ ): Promise<boolean | undefined> {
6932
+ if (!owner.applicationStartedAt
6933
+ || !this.threadWriterProcesses.inspectApplicationThreadIds
6934
+ || !runtime.inspectRemoteSessionActivity) return undefined;
6935
+ try {
6936
+ const ownedThreadIds = await this.threadWriterProcesses.inspectApplicationThreadIds(lockPath, owner);
6937
+ if (!ownedThreadIds.includes(threadId)) return undefined;
6938
+ const activities = await Promise.all(
6939
+ ownedThreadIds.map((ownedThreadId) => runtime.inspectRemoteSessionActivity!(ownedThreadId)),
6940
+ );
6941
+ return activities.every((activity) => !activity.active);
6942
+ } catch (error) {
6943
+ this.logger.warn(
6944
+ { error, threadId, applicationPid: owner.applicationPid },
6945
+ "Failed to inspect thread-writer application activity.",
6946
+ );
6947
+ return undefined;
6948
+ }
6949
+ }
6950
+
6951
+ private async waitForThreadWriterChange(
6952
+ lockPath: string,
6953
+ expectedOwner: ThreadWriterProcess,
6954
+ ): Promise<ThreadWriterProcess[]> {
6955
+ const deadline = Date.now() + 5_000;
6956
+ let owners: ThreadWriterProcess[] = [];
6957
+ do {
6958
+ owners = await this.threadWriterProcesses.inspect(lockPath);
6959
+ if (!owners.some((owner) => threadWriterMatches(
6960
+ owner,
6961
+ expectedOwner.writerPid,
6962
+ expectedOwner.writerStartedAt ?? "",
6963
+ expectedOwner.applicationPid,
6964
+ expectedOwner.applicationStartedAt ?? "",
6965
+ ))) return owners;
6966
+ await delay(250);
6967
+ } while (Date.now() < deadline);
6968
+ return owners;
6969
+ }
6970
+
6971
+ private async sendError(
6972
+ contextKey: string,
6973
+ error: unknown,
6974
+ details: RequestFailureDetails = {},
6975
+ ): Promise<void> {
6976
+ if (error instanceof PresentedRequestError) {
6977
+ this.logger.warn(
6978
+ { error: errorLogValue(error.originalError), contextKey, ...details },
6979
+ "Request failed after a dedicated error card was presented.",
6980
+ );
6981
+ return;
6982
+ }
6983
+ const record = this.currentSession(contextKey);
6984
+ if (record) {
6985
+ try {
6986
+ if (await this.presentThreadWriterConflict(record, contextKey, error, false)) return;
6987
+ } catch (presentationError) {
6988
+ this.logger.warn(
6989
+ { error: presentationError, originalError: errorLogValue(error), contextKey, ...details },
6990
+ "Failed to present the thread writer conflict card.",
6991
+ );
6992
+ }
6993
+ }
6994
+ this.logger.warn({ error, contextKey, ...details }, "Request failed.");
6995
+ try {
6996
+ await this.outbound.sendText(contextKey, error instanceof Error ? error.message : String(error));
6997
+ } catch (deliveryError) {
6998
+ this.logger.error(
6999
+ {
7000
+ error: deliveryError,
7001
+ originalError: errorLogValue(error),
7002
+ contextKey,
7003
+ ...details,
7004
+ phase: "error_response_send",
7005
+ },
7006
+ "Failed to deliver the request failure to the user.",
7007
+ );
7008
+ }
7009
+ }
7010
+ }
7011
+
7012
+ function activeWriterThreadId(error: unknown): string | undefined {
7013
+ const visited = new Set<unknown>();
7014
+ let current = error;
7015
+ while (current !== undefined && current !== null && !visited.has(current)) {
7016
+ visited.add(current);
7017
+ const message = current instanceof Error
7018
+ ? current.message
7019
+ : typeof current === "string" ? current : undefined;
7020
+ const match = message?.match(
7021
+ /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,
7022
+ );
7023
+ if (match?.[1]) return match[1];
7024
+ current = current instanceof Error ? current.cause : undefined;
7025
+ }
7026
+ return undefined;
7027
+ }
7028
+
7029
+ function positiveActionPid(value: unknown): number {
7030
+ const pid = typeof value === "number" ? value : Number(value);
7031
+ if (!Number.isSafeInteger(pid) || pid <= 4) {
7032
+ throw new Error("任务占用卡片中的进程信息无效,请重新发送消息获取最新状态。");
7033
+ }
7034
+ return pid;
7035
+ }
7036
+
7037
+ function threadWriterMatches(
7038
+ owner: ThreadWriterProcess,
7039
+ writerPid: number,
7040
+ writerStartedAt: string,
7041
+ applicationPid: number,
7042
+ applicationStartedAt: string,
7043
+ ): boolean {
7044
+ return Boolean(writerStartedAt)
7045
+ && Boolean(applicationStartedAt)
7046
+ && owner.writerPid === writerPid
7047
+ && owner.writerStartedAt === writerStartedAt
7048
+ && owner.applicationPid === applicationPid
7049
+ && owner.applicationStartedAt === applicationStartedAt;
7050
+ }
7051
+
7052
+ function delay(milliseconds: number): Promise<void> {
7053
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
7054
+ }
7055
+
7056
+ function renderPlainHelp(introLines: string[], sections: HelpCardSection[]): string {
7057
+ return [
7058
+ "Agent Bot 使用帮助",
7059
+ "",
7060
+ ...introLines.map((line) => decodeHelpEntities(line.replace(/^>\s*/u, "").replaceAll("**", ""))),
7061
+ ...sections.flatMap((section) => [
7062
+ "",
7063
+ section.title,
7064
+ ...section.commands.map((command) => [
7065
+ command.text,
7066
+ command.usage ? decodeHelpEntities(command.usage) : "",
7067
+ `- ${command.description}`,
7068
+ ].filter(Boolean).join(" ")),
7069
+ ]),
7070
+ ].join("\n");
7071
+ }
7072
+
7073
+ function decodeHelpEntities(value: string): string {
7074
+ return value
7075
+ .replaceAll("&#60;", "<")
7076
+ .replaceAll("&#62;", ">")
7077
+ .replaceAll("&amp;", "&");
6643
7078
  }
6644
7079
 
6645
7080
  function threadForkAnchorMessageIds(message: IncomingMessage): string[] {
@@ -217,6 +217,7 @@ export interface ReasoningEffortOption {
217
217
  export interface AgentRuntime {
218
218
  readonly kind: RuntimeKind;
219
219
  getProcessInfo?(): AgentProcessInfo;
220
+ getThreadWriterLockPath?(remoteSessionId: string): string | undefined;
220
221
  createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession>;
221
222
  resumeSession(input: ResumeRuntimeSessionInput): Promise<RuntimeSession>;
222
223
  forkSession?(input: ForkRuntimeSessionInput): Promise<RuntimeSession>;
@@ -140,7 +140,7 @@ export class SafeRestartScheduler {
140
140
  await this.restart(reason, scheduleId, state);
141
141
  return;
142
142
  }
143
- const quietPeriodMs = this.options.quietPeriodMs ?? 15_000;
143
+ const quietPeriodMs = this.options.quietPeriodMs ?? 5_000;
144
144
  const latestInboundAt = state.latestInboundAt ?? "";
145
145
  if (this.idleSince === undefined || this.idleInboundAt !== latestInboundAt) {
146
146
  this.idleSince = Date.now();