@keyou007/agent-bot 0.1.20-alpha.1 → 0.1.21

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 (92) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +27 -4
  3. package/README.zh.md +23 -4
  4. package/dist/cli/AgentPrerequisites.js +11 -47
  5. package/dist/cli/AgentPrerequisites.js.map +1 -1
  6. package/dist/cli/DefaultAgentSelection.js +1 -1
  7. package/dist/cli/DefaultAgentSelection.js.map +1 -1
  8. package/dist/cli.js +15 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/codex/CodexEventMapper.js +21 -1
  11. package/dist/codex/CodexEventMapper.js.map +1 -1
  12. package/dist/codex/CodexLocalActivityDetector.js +32 -10
  13. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  14. package/dist/codex/CodexProcessManager.js +56 -7
  15. package/dist/codex/CodexProcessManager.js.map +1 -1
  16. package/dist/codex/CodexRuntime.js +361 -168
  17. package/dist/codex/CodexRuntime.js.map +1 -1
  18. package/dist/codex/CodexVersion.js +18 -0
  19. package/dist/codex/CodexVersion.js.map +1 -0
  20. package/dist/codex/ProviderModels.js +159 -0
  21. package/dist/codex/ProviderModels.js.map +1 -0
  22. package/dist/codex/ProviderSwitch.js +32 -0
  23. package/dist/codex/ProviderSwitch.js.map +1 -0
  24. package/dist/commands/CommandRouter.js +8 -3
  25. package/dist/commands/CommandRouter.js.map +1 -1
  26. package/dist/console/ConsoleTurnPresenter.js +2 -1
  27. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  28. package/dist/feishu/CardRenderer.js +82 -27
  29. package/dist/feishu/CardRenderer.js.map +1 -1
  30. package/dist/feishu/FeishuConnector.js +17 -5
  31. package/dist/feishu/FeishuConnector.js.map +1 -1
  32. package/dist/feishu/FeishuTurnPresenter.js +34 -8
  33. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  34. package/dist/feishu/InboundText.js +11 -0
  35. package/dist/feishu/InboundText.js.map +1 -1
  36. package/dist/feishu/MergedForwardMessage.js +17 -5
  37. package/dist/feishu/MergedForwardMessage.js.map +1 -1
  38. package/dist/index.js +3 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/local-files/LocalFileViewerServer.js +164 -19
  41. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  42. package/dist/local-files/MarkdownFileLinks.js +65 -0
  43. package/dist/local-files/MarkdownFileLinks.js.map +1 -0
  44. package/dist/local-files/TurnPreviewPage.js +725 -0
  45. package/dist/local-files/TurnPreviewPage.js.map +1 -0
  46. package/dist/presentation/OutboundRouter.js +6 -1
  47. package/dist/presentation/OutboundRouter.js.map +1 -1
  48. package/dist/presentation/TurnStateReducer.js +142 -3
  49. package/dist/presentation/TurnStateReducer.js.map +1 -1
  50. package/dist/proxy/ProxySessionController.js +573 -325
  51. package/dist/proxy/ProxySessionController.js.map +1 -1
  52. package/dist/state/StateStore.js +65 -65
  53. package/dist/state/StateStore.js.map +1 -1
  54. package/dist/utils/formatStorageSize.js +15 -0
  55. package/dist/utils/formatStorageSize.js.map +1 -0
  56. package/dist/utils/semanticVersion.js +39 -0
  57. package/dist/utils/semanticVersion.js.map +1 -0
  58. package/docs/technical-reference.md +11 -3
  59. package/docs/technical-reference.zh.md +11 -3
  60. package/npm-shrinkwrap.json +2 -2
  61. package/package.json +1 -1
  62. package/skills/agent-bot/SKILL.md +11 -1
  63. package/src/cli/AgentPrerequisites.ts +14 -40
  64. package/src/cli/DefaultAgentSelection.ts +1 -1
  65. package/src/cli.ts +13 -0
  66. package/src/codex/CodexEventMapper.ts +31 -1
  67. package/src/codex/CodexLocalActivityDetector.ts +41 -9
  68. package/src/codex/CodexProcessManager.ts +48 -7
  69. package/src/codex/CodexRuntime.ts +397 -204
  70. package/src/codex/CodexVersion.ts +18 -0
  71. package/src/codex/ProviderModels.ts +185 -0
  72. package/src/codex/ProviderSwitch.ts +36 -0
  73. package/src/commands/CommandRouter.ts +7 -2
  74. package/src/commands/commandTypes.ts +1 -1
  75. package/src/console/ConsoleTurnPresenter.ts +3 -0
  76. package/src/feishu/CardRenderer.ts +84 -43
  77. package/src/feishu/FeishuConnector.ts +15 -5
  78. package/src/feishu/FeishuTurnPresenter.ts +46 -4
  79. package/src/feishu/InboundText.ts +11 -0
  80. package/src/feishu/MergedForwardMessage.ts +15 -5
  81. package/src/index.ts +3 -1
  82. package/src/local-files/LocalFileViewerServer.ts +179 -20
  83. package/src/local-files/MarkdownFileLinks.ts +64 -0
  84. package/src/local-files/TurnPreviewPage.ts +792 -0
  85. package/src/presentation/OutboundRouter.ts +8 -0
  86. package/src/presentation/TurnStateReducer.ts +175 -2
  87. package/src/presentation/turnViewTypes.ts +13 -0
  88. package/src/proxy/ProxySessionController.ts +547 -297
  89. package/src/runtime/types.ts +33 -3
  90. package/src/state/StateStore.ts +86 -90
  91. package/src/utils/formatStorageSize.ts +13 -0
  92. package/src/utils/semanticVersion.ts +28 -0
@@ -3,6 +3,8 @@ 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 { CodexVersionError } from "../codex/CodexVersion.js";
7
+ import { assertProviderSettingsApplied, isMissingRolloutError } from "../codex/ProviderSwitch.js";
6
8
  import { SystemThreadWriterProcessController, } from "../codex/ThreadWriterProcess.js";
7
9
  import { resolveUserPath } from "../config/paths.js";
8
10
  import { DEFAULT_GROUP_NAME_FORMAT, } from "../config/schema.js";
@@ -19,6 +21,7 @@ import { errorLogValue } from "../logging/errorLogValue.js";
19
21
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
20
22
  import { isActiveShellCommandJob, ShellCommandJobManager, } from "../shell/ShellCommandJobManager.js";
21
23
  import { createId } from "../utils/id.js";
24
+ import { formatStorageSize } from "../utils/formatStorageSize.js";
22
25
  import { truncateMiddle, truncateText } from "../utils/markdown.js";
23
26
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
24
27
  import { executeShellCommand, } from "../utils/executeShellCommand.js";
@@ -50,6 +53,22 @@ const FORWARD_ATTACHMENT_WINDOW_MS = 800;
50
53
  const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
51
54
  const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
52
55
  const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
56
+ const TURN_RESET_STARTED_MESSAGE = "Reset 正在进行,请稍后。期间发送的消息会自动排队。";
57
+ const CREATE_TASK_STARTED_MESSAGE = "正在创建任务,请稍后。";
58
+ const CREATE_GROUP_STARTED_MESSAGE = "正在创建群聊和任务,请稍后。";
59
+ const FORK_TASK_STARTED_MESSAGE = "正在创建分支任务,请稍后。";
60
+ const FORK_GROUP_STARTED_MESSAGE = "正在创建分支群聊,请稍后。";
61
+ const LIST_SESSIONS_STARTED_MESSAGE = "正在读取任务列表,请稍后。";
62
+ const LIST_TURNS_STARTED_MESSAGE = "正在读取历史轮次,请稍后。";
63
+ const SWITCH_TASK_STARTED_MESSAGE = "正在切换任务,请稍后。";
64
+ const STATUS_STARTED_MESSAGE = "正在读取任务状态,请稍后。";
65
+ const GOAL_STARTED_MESSAGE = "正在处理 Goal,请稍后。";
66
+ const ARCHIVE_TASK_STARTED_MESSAGE = "正在归档任务,请稍后。";
67
+ const DISMISS_GROUP_STARTED_MESSAGE = "正在归档任务并解散群聊,请稍后。";
68
+ const SEND_FILE_STARTED_MESSAGE = "正在发送文件,请稍后。";
69
+ const CHANGE_TITLE_STARTED_MESSAGE = "正在修改任务标题,请稍后。";
70
+ const STOP_TASK_STARTED_MESSAGE = "正在停止任务,请稍后。";
71
+ const CLOSE_WRITER_STARTED_MESSAGE = "正在检查并关闭占用进程,请稍后。";
53
72
  const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
54
73
  const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
55
74
  const IMAGE_FILE_EXTENSIONS = new Set([
@@ -90,7 +109,7 @@ const HELP_COMMAND_SECTIONS = [
90
109
  usage: "[序号或任务 ID]",
91
110
  description: "从当前或指定任务最近完成轮次创建分支",
92
111
  },
93
- { command: "/turns", description: "浏览历史轮次,并可 Reset 对话上下文" },
112
+ { command: "/turns", usage: "[Turn ID 或序号]", description: "浏览历史轮次,并可 Reset 对话上下文;带参数查看运行详情" },
94
113
  {
95
114
  command: "/title",
96
115
  usage: "<新标题>",
@@ -205,6 +224,8 @@ export class ProxySessionController {
205
224
  sessionResets = new Map();
206
225
  resetHistoryOperations = new Map();
207
226
  remoteTurnHistoryRefreshes = new Map();
227
+ turnHistoryHydrations = new Map();
228
+ forkHistoryCursors = new Map();
208
229
  lastSessionListings = new Map();
209
230
  threadInitializations = new Map();
210
231
  llmRetryingSessions = new Set();
@@ -282,6 +303,7 @@ export class ProxySessionController {
282
303
  catch (error) {
283
304
  this.logger.warn({ error, messageId: message.messageId, contextKey: message.contextKey }, "Failed to acknowledge the incoming Feishu message with a reaction.");
284
305
  }
306
+ await this.waitForCurrentSessionReset(message.contextKey);
285
307
  if (attachmentReservation && this.completeForwardAttachment(message, attachmentReservation)) {
286
308
  this.store.audit(message.contextKey, `${attachmentReservation.sourceKind}_attachment_coalesced`, {
287
309
  messageId: message.messageId,
@@ -478,10 +500,11 @@ export class ProxySessionController {
478
500
  command: command.type,
479
501
  }, "Processing Agent Bot command.");
480
502
  }
481
- // Operational and read-only commands must remain available even if a prompt operation is slow.
503
+ // Operational and read-only commands bypass the prompt queue, but still wait for a Reset binding change.
482
504
  if (isQueueIndependentCommand(command)) {
483
505
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
484
506
  try {
507
+ await this.waitForCurrentSessionReset(message.contextKey);
485
508
  if (command.type === "prompt")
486
509
  await this.ensureThreadFork(message);
487
510
  await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
@@ -502,45 +525,48 @@ export class ProxySessionController {
502
525
  return;
503
526
  }
504
527
  const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
505
- const next = previous.catch(() => undefined).then(() => this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
506
- try {
507
- let executableCommand = command;
508
- let executableImagePaths = localImagePaths;
509
- let executableMessage = message;
510
- let injectedRootMessageId;
511
- if (command.type === "prompt") {
512
- await this.ensureThreadFork(message);
513
- const prepared = await this.prepareTopicRootPrompt(message, command.text, localImagePaths);
514
- executableCommand = { ...command, text: prepared.text };
515
- executableImagePaths = prepared.localImagePaths;
516
- executableMessage = { ...message, displayText: prepared.displayPrompt };
517
- injectedRootMessageId = prepared.injectedRootMessageId;
518
- }
519
- await this.execute(message.contextKey, executableCommand, message.messageId, replyTarget, executableImagePaths, message.userId, executableMessage);
520
- if (injectedRootMessageId) {
521
- const current = this.currentSession(message.contextKey);
522
- if (current) {
523
- this.store.audit(message.contextKey, "topic_root_injected", {
524
- localSessionId: current.localSessionId,
525
- rootMessageId: injectedRootMessageId,
526
- promptMessageId: message.messageId,
527
- });
528
+ const next = previous.catch(() => undefined).then(async () => {
529
+ await this.waitForCurrentSessionReset(message.contextKey);
530
+ return this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
531
+ try {
532
+ let executableCommand = command;
533
+ let executableImagePaths = localImagePaths;
534
+ let executableMessage = message;
535
+ let injectedRootMessageId;
536
+ if (command.type === "prompt") {
537
+ await this.ensureThreadFork(message);
538
+ const prepared = await this.prepareTopicRootPrompt(message, command.text, localImagePaths);
539
+ executableCommand = { ...command, text: prepared.text };
540
+ executableImagePaths = prepared.localImagePaths;
541
+ executableMessage = { ...message, displayText: prepared.displayPrompt };
542
+ injectedRootMessageId = prepared.injectedRootMessageId;
543
+ }
544
+ await this.execute(message.contextKey, executableCommand, message.messageId, replyTarget, executableImagePaths, message.userId, executableMessage);
545
+ if (injectedRootMessageId) {
546
+ const current = this.currentSession(message.contextKey);
547
+ if (current) {
548
+ this.store.audit(message.contextKey, "topic_root_injected", {
549
+ localSessionId: current.localSessionId,
550
+ rootMessageId: injectedRootMessageId,
551
+ promptMessageId: message.messageId,
552
+ });
553
+ }
554
+ }
555
+ if (!commandDefersReactionFinalization(command)) {
556
+ await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
528
557
  }
529
558
  }
530
- if (!commandDefersReactionFinalization(command)) {
531
- await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
559
+ catch (error) {
560
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
561
+ await this.sendError(message.contextKey, error, {
562
+ requestId: message.messageId,
563
+ messageId: message.messageId,
564
+ command: command.type,
565
+ phase: "command_execute",
566
+ });
532
567
  }
533
- }
534
- catch (error) {
535
- await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
536
- await this.sendError(message.contextKey, error, {
537
- requestId: message.messageId,
538
- messageId: message.messageId,
539
- command: command.type,
540
- phase: "command_execute",
541
- });
542
- }
543
- }));
568
+ });
569
+ });
544
570
  this.messageQueues.set(message.contextKey, next);
545
571
  await next;
546
572
  if (this.messageQueues.get(message.contextKey) === next)
@@ -653,6 +679,7 @@ export class ProxySessionController {
653
679
  else if (kind === "directory_send_file") {
654
680
  const filePath = directoryFileActionPath(scopedAction.value.filePath);
655
681
  await this.assertSendableFile(filePath);
682
+ await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
656
683
  await this.outbound.sendFile(contextKey, filePath);
657
684
  }
658
685
  else if (kind === "directory_new_folder_prompt") {
@@ -704,11 +731,13 @@ export class ProxySessionController {
704
731
  }
705
732
  else if (kind === "session_new") {
706
733
  const sessionId = String(scopedAction.value.sessionId ?? "");
734
+ await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
707
735
  await this.createProjectSessionFromReference(contextKey, sessionId);
708
736
  await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
709
737
  }
710
738
  else if (kind === "session_new_group") {
711
739
  const sessionId = String(scopedAction.value.sessionId ?? "");
740
+ await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
712
741
  await this.createFeishuGroupFromReference(contextKey, sessionId, scopedAction.userId);
713
742
  await this.refreshSessionsCardFromAction(scopedAction);
714
743
  }
@@ -726,10 +755,12 @@ export class ProxySessionController {
726
755
  }
727
756
  else if (kind === "group_dismiss_confirm") {
728
757
  this.assertDismissGroupRequester(scopedAction);
758
+ await this.outbound.sendText(contextKey, DISMISS_GROUP_STARTED_MESSAGE);
729
759
  await this.dismissGroupAndArchiveTask(contextKey, String(scopedAction.value.sessionId ?? ""), "command");
730
760
  }
731
761
  else if (kind === "session_stop") {
732
762
  const sessionId = String(scopedAction.value.sessionId ?? "");
763
+ await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
733
764
  await this.stopSessionReference(contextKey, sessionId);
734
765
  if (scopedAction.value.cardView === "status")
735
766
  await this.refreshStatusCardFromAction(scopedAction, sessionId);
@@ -737,6 +768,7 @@ export class ProxySessionController {
737
768
  await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
738
769
  }
739
770
  else if (kind === "session_status") {
771
+ await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
740
772
  await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
741
773
  }
742
774
  else if (kind === "session_status_refresh") {
@@ -867,6 +899,8 @@ export class ProxySessionController {
867
899
  this.sessionResets.clear();
868
900
  this.resetHistoryOperations.clear();
869
901
  this.remoteTurnHistoryRefreshes.clear();
902
+ this.forkHistoryCursors.clear();
903
+ this.turnHistoryHydrations.clear();
870
904
  for (const pending of this.pendingMergedForwards.values())
871
905
  pending.resolveAttachment(undefined);
872
906
  this.pendingMergedForwards.clear();
@@ -930,9 +964,11 @@ export class ProxySessionController {
930
964
  const runtime = this.runtimes.forAgent(record.agentName);
931
965
  if (runtime.readRemoteSession) {
932
966
  try {
933
- remote = await runtime.readRemoteSession(record.remoteSessionId);
967
+ remote = await runtime.readRemoteSession(record.remoteSessionId, "latest-full");
934
968
  }
935
969
  catch (error) {
970
+ if (error instanceof CodexVersionError)
971
+ throw error;
936
972
  this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to inspect App Server task status for CLI.");
937
973
  }
938
974
  }
@@ -996,7 +1032,7 @@ export class ProxySessionController {
996
1032
  ?? { ...record, contextKey: responseContextKey };
997
1033
  const previous = this.messageQueues.get(responseContextKey) ?? Promise.resolve();
998
1034
  const next = previous.catch(() => undefined).then(async () => {
999
- this.outbound.registerSession(scopedRecord.localSessionId, responseContextKey, scopedRecord.title, scopedRecord.cwd, this.agentLabel(scopedRecord.agentName));
1035
+ this.outbound.registerSession(scopedRecord.localSessionId, responseContextKey, scopedRecord.title, scopedRecord.cwd, this.agentLabel(scopedRecord.agentName), scopedRecord.model);
1000
1036
  const promptMessageId = await this.outbound.withReplyTarget(responseContextKey, existingReplyTarget, () => this.outbound.sendText(responseContextKey, promptText));
1001
1037
  const turnReplyTarget = isThreadContextKey(responseContextKey)
1002
1038
  ? promptMessageId
@@ -1155,44 +1191,36 @@ export class ProxySessionController {
1155
1191
  }
1156
1192
  async controlTaskSettings(localSessionId, setting, value) {
1157
1193
  const record = this.requireControlSession(localSessionId);
1158
- const loaded = await this.loadSession(record);
1159
1194
  if (setting && !value?.trim())
1160
1195
  throw new Error(`task ${setting} requires a value.`);
1161
1196
  const nextValue = value?.trim();
1197
+ if (setting === "provider" && nextValue) {
1198
+ const contextKey = this.controlSessionContextKey(record);
1199
+ await this.outbound.withReplyTarget(contextKey, this.controlSessionReplyTarget(record), () => this.outbound.sendText(contextKey, `正在切换到 Provider ${nextValue},请稍后。`));
1200
+ }
1201
+ const loaded = await this.loadSession(record);
1162
1202
  if (setting === "provider" && nextValue) {
1163
1203
  await this.assertModelProvider(loaded, nextValue);
1164
- const models = await loaded.runtime.listModels();
1165
- const model = models.find((candidate) => candidate.id === loaded.session.model)
1166
- ?? models.find((candidate) => candidate.isDefault);
1204
+ const models = await loaded.runtime.listModels(nextValue, loaded.session.model);
1205
+ const model = selectProviderModel(models, loaded.session.model);
1167
1206
  if (!model)
1168
1207
  throw new Error("The current runtime has no model available for a Provider change.");
1169
- const effort = model.supportedReasoningEfforts.some((candidate) => candidate.value === loaded.session.reasoningEffort)
1170
- ? loaded.session.reasoningEffort
1171
- : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
1172
- if (!effort)
1173
- throw new Error(`Model ${model.id} has no reasoning effort available for a Provider change.`);
1174
- if (!loaded.runtime.setExecutionSettings)
1175
- throw new Error("The current runtime does not support Provider settings.");
1176
- const updated = await loaded.runtime.setExecutionSettings(record.localSessionId, {
1208
+ const effort = selectProviderReasoningEffort(model, loaded.session.reasoningEffort);
1209
+ await this.changeProviderSettings(loaded, {
1177
1210
  modelProvider: nextValue,
1178
1211
  model: model.id,
1179
1212
  reasoningEffort: effort,
1180
1213
  permissionMode: loaded.session.permissionMode,
1181
1214
  });
1182
- this.store.updateRuntimeSession(record.localSessionId, {
1183
- modelProvider: updated.modelProvider ?? nextValue,
1184
- model: updated.model ?? model.id,
1185
- reasoningEffort: updated.reasoningEffort ?? effort,
1186
- permissionMode: updated.permissionMode,
1187
- });
1188
1215
  }
1189
1216
  else if (setting === "model" && nextValue) {
1190
- const models = await loaded.runtime.listModels();
1217
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
1191
1218
  const selected = models.find((candidate) => candidate.id === nextValue);
1192
1219
  if (!selected)
1193
1220
  throw new Error(`Unknown model: ${nextValue}`);
1194
1221
  const currentEffort = loaded.session.reasoningEffort;
1195
- const nextEffort = currentEffort && selected.supportedReasoningEfforts.some((candidate) => candidate.value === currentEffort)
1222
+ const nextEffort = currentEffort && (selected.supportedReasoningEfforts.length === 0
1223
+ || selected.supportedReasoningEfforts.some((candidate) => candidate.value === currentEffort))
1196
1224
  ? currentEffort
1197
1225
  : selected.defaultReasoningEffort;
1198
1226
  await loaded.runtime.setModel(record.localSessionId, nextValue);
@@ -1202,7 +1230,7 @@ export class ProxySessionController {
1202
1230
  this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
1203
1231
  }
1204
1232
  else if (setting === "thinking" && nextValue) {
1205
- const models = await loaded.runtime.listModels();
1233
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
1206
1234
  const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
1207
1235
  ?? models.find((candidate) => candidate.isDefault);
1208
1236
  if (!currentModel)
@@ -1222,13 +1250,13 @@ export class ProxySessionController {
1222
1250
  this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
1223
1251
  }
1224
1252
  const session = this.store.getSession(record.localSessionId) ?? record;
1225
- if (setting)
1253
+ if (setting && setting !== "provider")
1226
1254
  await this.persistAgentExecutionDefaults(session.localSessionId);
1227
- const models = await loaded.runtime.listModels();
1255
+ const models = await loaded.runtime.listModels(session.modelProvider, session.model);
1228
1256
  const currentModel = models.find((candidate) => candidate.id === session.model)
1229
1257
  ?? models.find((candidate) => candidate.isDefault);
1230
1258
  const providers = loaded.runtime.listModelProviders
1231
- ? await loaded.runtime.listModelProviders()
1259
+ ? await this.modelProviderOptions(loaded)
1232
1260
  : [];
1233
1261
  return {
1234
1262
  session,
@@ -1271,17 +1299,18 @@ export class ProxySessionController {
1271
1299
  }),
1272
1300
  };
1273
1301
  }
1274
- controlListTaskTurns(localSessionId) {
1302
+ async controlListTaskTurns(localSessionId) {
1275
1303
  const session = this.requireControlSession(localSessionId);
1276
1304
  if (!session.remoteSessionId || !this.isCodexSession(session)) {
1277
1305
  throw new Error("The task is not an App Server task that can be reset.");
1278
1306
  }
1279
- const rows = this.store.listTaskTurnGraph(session.localSessionId);
1307
+ await this.hydrateTaskTurnHistory(session);
1308
+ const rows = this.store.listTaskTurnGraphIndex(session.localSessionId);
1280
1309
  const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
1281
1310
  return {
1282
1311
  session,
1283
1312
  turns: rows.flatMap((turn, index) => {
1284
- const snapshot = turnViewSnapshot(turn.snapshot);
1313
+ const snapshot = this.store.getTurnPromptSummary(turn.turnId);
1285
1314
  if (!snapshot)
1286
1315
  return [];
1287
1316
  return [{
@@ -1299,16 +1328,10 @@ export class ProxySessionController {
1299
1328
  async controlResetTaskToTurn(localSessionId, turnId) {
1300
1329
  const record = this.requireControlSession(localSessionId);
1301
1330
  const contextKey = this.controlSessionContextKey(record);
1302
- const previous = this.sessionResets.get(record.localSessionId) ?? Promise.resolve();
1303
- const reset = previous.catch(() => undefined).then(() => this.performSessionReset(contextKey, record, turnId, false));
1304
- this.sessionResets.set(record.localSessionId, reset);
1305
- try {
1306
- await reset;
1307
- }
1308
- finally {
1309
- if (this.sessionResets.get(record.localSessionId) === reset)
1310
- this.sessionResets.delete(record.localSessionId);
1311
- }
1331
+ await this.runSessionReset(record.localSessionId, async () => {
1332
+ await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
1333
+ await this.performSessionReset(contextKey, record, turnId, false);
1334
+ });
1312
1335
  return this.store.getSession(record.localSessionId) ?? record;
1313
1336
  }
1314
1337
  controlTaskMute(localSessionId, enabled) {
@@ -1438,6 +1461,7 @@ export class ProxySessionController {
1438
1461
  if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
1439
1462
  throw new Error("/new --nodir 仅支持 App Server Agent。");
1440
1463
  }
1464
+ await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
1441
1465
  await this.createSession(contextKey, context.defaultAgent, command.projectless
1442
1466
  ? undefined
1443
1467
  : command.cwd === undefined
@@ -1448,6 +1472,10 @@ export class ProxySessionController {
1448
1472
  if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
1449
1473
  throw new Error("/newgroup --nodir 仅支持 App Server Agent。");
1450
1474
  }
1475
+ if (!userId?.startsWith("ou_")) {
1476
+ throw new Error("/newgroup 只能由具有 open_id 的飞书用户消息触发。");
1477
+ }
1478
+ await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
1451
1479
  await this.createFeishuGroup(contextKey, context.defaultAgent, command.title, userId, command.cwd === undefined ? undefined : resolveUserPath(command.cwd), command.projectless === true);
1452
1480
  return;
1453
1481
  case "forkgroup":
@@ -1466,6 +1494,7 @@ export class ProxySessionController {
1466
1494
  await this.enqueueNoSteerPrompt(contextKey, command.text, messageId, replyTarget);
1467
1495
  return;
1468
1496
  case "sessions":
1497
+ await this.outbound.sendText(contextKey, LIST_SESSIONS_STARTED_MESSAGE);
1469
1498
  await this.listSessions(contextKey, command.searchTerm);
1470
1499
  return;
1471
1500
  case "switch":
@@ -1491,9 +1520,11 @@ export class ProxySessionController {
1491
1520
  await this.openDismissGroupCard(contextKey, userId);
1492
1521
  return;
1493
1522
  case "status":
1523
+ await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
1494
1524
  await this.status(contextKey, command.sessionId);
1495
1525
  return;
1496
1526
  case "goal":
1527
+ await this.outbound.sendText(contextKey, GOAL_STARTED_MESSAGE);
1497
1528
  await this.goal(contextKey, command);
1498
1529
  return;
1499
1530
  case "restart":
@@ -1508,7 +1539,14 @@ export class ProxySessionController {
1508
1539
  await this.setGroupMute(contextKey, command.enabled);
1509
1540
  return;
1510
1541
  case "turns":
1511
- await this.openResetHistory(contextKey);
1542
+ if (command.turnReference) {
1543
+ await this.outbound.sendText(contextKey, "正在读取轮次运行信息,请稍后。");
1544
+ await this.openTurnDetails(contextKey, command.turnReference);
1545
+ }
1546
+ else {
1547
+ await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
1548
+ await this.openResetHistory(contextKey);
1549
+ }
1512
1550
  return;
1513
1551
  case "model":
1514
1552
  await this.openExecutionSettings(contextKey, "model");
@@ -1542,7 +1580,7 @@ export class ProxySessionController {
1542
1580
  : context.contextKey;
1543
1581
  if (!this.outbound.canRoute(turnContextKey))
1544
1582
  continue;
1545
- this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd, this.agentLabel(session.agentName));
1583
+ this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model);
1546
1584
  if (!session.lastTurnId)
1547
1585
  continue;
1548
1586
  void this.outbound.resumeDelivery(session.localSessionId, turnContextKey, session.lastTurnId).catch((error) => {
@@ -1616,7 +1654,7 @@ export class ProxySessionController {
1616
1654
  continue;
1617
1655
  }
1618
1656
  if (session) {
1619
- this.outbound.registerSession(session.localSessionId, attempt.contextKey, session.title, session.cwd, this.agentLabel(session.agentName));
1657
+ this.outbound.registerSession(session.localSessionId, attempt.contextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model);
1620
1658
  }
1621
1659
  try {
1622
1660
  await this.handleRuntimeEvent({
@@ -1728,7 +1766,7 @@ export class ProxySessionController {
1728
1766
  const replyTarget = attempt.replyMessageId
1729
1767
  ? { messageId: attempt.replyMessageId, replyInThread: true }
1730
1768
  : undefined;
1731
- this.outbound.registerSession(session.localSessionId, contextKey, session.title, session.cwd, this.agentLabel(session.agentName));
1769
+ this.outbound.registerSession(session.localSessionId, contextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model);
1732
1770
  if (announce) {
1733
1771
  await this.outbound.withReplyTarget(contextKey, replyTarget, () => this.outbound.sendText(contextKey, "检测到任务在 Agent Bot 重启前尚未完成,正在自动恢复。")).catch((error) => {
1734
1772
  this.logger.warn({ error, attemptId: attempt.attemptId, contextKey }, "Failed to send the task recovery notification.");
@@ -1747,6 +1785,8 @@ export class ProxySessionController {
1747
1785
  remote = await runtime.readRemoteSession(session.remoteSessionId);
1748
1786
  }
1749
1787
  catch (error) {
1788
+ if (error instanceof CodexVersionError)
1789
+ throw error;
1750
1790
  this.logger.warn({ error, attemptId: attempt.attemptId, remoteSessionId: session.remoteSessionId }, "Failed to read the remote task during startup recovery; treating the old local execution as interrupted.");
1751
1791
  }
1752
1792
  }
@@ -1928,7 +1968,7 @@ export class ProxySessionController {
1928
1968
  }
1929
1969
  const configuredRuntime = this.runtimes.forAgent(record.agentName);
1930
1970
  if (!configuredRuntime.getSession(record.localSessionId)) {
1931
- this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd, this.agentLabel(record.agentName));
1971
+ this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd, this.agentLabel(record.agentName), record.model);
1932
1972
  await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, displayPrompt ?? text);
1933
1973
  }
1934
1974
  let loaded;
@@ -2221,7 +2261,7 @@ export class ProxySessionController {
2221
2261
  permissionMode: source.permissionMode ?? "auto",
2222
2262
  });
2223
2263
  this.store.setCurrentSession(message.contextKey, localSessionId);
2224
- this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd, this.agentLabel(source.agentName));
2264
+ this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd, this.agentLabel(source.agentName), source.model);
2225
2265
  try {
2226
2266
  const forked = await runtime.forkSession({
2227
2267
  localSessionId,
@@ -2284,6 +2324,7 @@ export class ProxySessionController {
2284
2324
  return { anchor, source, snapshot };
2285
2325
  }
2286
2326
  async forkSessionReference(contextKey, reference) {
2327
+ await this.outbound.sendText(contextKey, FORK_TASK_STARTED_MESSAGE);
2287
2328
  const plan = await this.prepareForkSession(contextKey, reference);
2288
2329
  const forked = await this.forkSessionIntoContext(contextKey, plan);
2289
2330
  const forkSourceLabel = plan.forkedFromHistoricalTurn
@@ -2335,32 +2376,22 @@ export class ProxySessionController {
2335
2376
  if (!runtime.readRemoteSession && !source) {
2336
2377
  throw new Error("当前 App Server Agent 不支持读取指定任务。");
2337
2378
  }
2338
- const persistedCompletedTurns = source ? this.localForkCompletedTurns(source) : [];
2379
+ const persistedCompletedTurnId = source
2380
+ ? this.store.findLatestCompletedTurnId(source.localSessionId)
2381
+ : undefined;
2339
2382
  const sourceLastSnapshot = turnViewSnapshot(source?.lastTurnId ? this.store.getTurnSnapshot(source.lastTurnId) : undefined);
2340
- const localCompletedTurns = persistedCompletedTurns.length > 0
2341
- ? persistedCompletedTurns
2342
- : source?.lastTurnId
2383
+ const localCompletedTurnId = persistedCompletedTurnId
2384
+ ?? (source?.lastTurnId
2343
2385
  && (source.lastTurnStatus === "completed" || sourceLastSnapshot?.status === "completed")
2344
- ? [{
2345
- id: source.lastTurnId,
2346
- prompt: latestUserPromptFromTurnView(sourceLastSnapshot) ?? source.title,
2347
- startedAt: sourceLastSnapshot?.startedAt ?? parseIsoTimestamp(source.updatedAt),
2348
- completedAt: sourceLastSnapshot?.completedAt
2349
- ?? sourceLastSnapshot?.startedAt
2350
- ?? parseIsoTimestamp(source.updatedAt),
2351
- }]
2352
- : [];
2353
- if (!remote && runtime.readRemoteSession && (!source || persistedCompletedTurns.length === 0)) {
2354
- try {
2355
- remote = await runtime.readRemoteSession(remoteSessionId);
2356
- }
2357
- catch (error) {
2358
- if (localCompletedTurns.length === 0)
2359
- throw error;
2360
- this.logger.warn({ error, contextKey, remoteSessionId, sourceTurnId: localCompletedTurns.at(-1)?.id }, "Failed to read the complete source task before Fork; using the locally persisted completed Turn.");
2361
- }
2386
+ ? source.lastTurnId
2387
+ : undefined)
2388
+ ?? (source ? this.store.getForkHistorySource(source.localSessionId)?.sourceTurnId : undefined);
2389
+ if (!remote && !localCompletedTurnId) {
2390
+ remote = runtime.readRemoteForkSource
2391
+ ? await runtime.readRemoteForkSource(remoteSessionId)
2392
+ : await runtime.readRemoteSession?.(remoteSessionId);
2362
2393
  }
2363
- const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurns.at(-1)?.id;
2394
+ const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurnId;
2364
2395
  const latestSnapshot = turnViewSnapshot(latestTurnId ? this.store.getTurnSnapshot(latestTurnId) : undefined);
2365
2396
  const isRunning = remote
2366
2397
  ? isRemoteSessionActive(remote)
@@ -2369,7 +2400,7 @@ export class ProxySessionController {
2369
2400
  || isTurnStillRunning(latestSnapshot?.status));
2370
2401
  const lastTurnId = remote?.lastCompletedTurnId
2371
2402
  ?? (remote?.lastTurnStatus === "completed" ? remote.lastTurnId : undefined)
2372
- ?? localCompletedTurns.at(-1)?.id;
2403
+ ?? localCompletedTurnId;
2373
2404
  if (!lastTurnId) {
2374
2405
  if (isRunning) {
2375
2406
  throw new Error(`${sourceLabel}正在执行,且还没有已完成轮次可供 fork。请等待当前轮次完成后重试。`);
@@ -2378,8 +2409,7 @@ export class ProxySessionController {
2378
2409
  }
2379
2410
  const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
2380
2411
  const forkedFromHistoricalTurn = lastTurnId !== latestTurnId;
2381
- const completedTurn = (remote?.completedTurns ?? localCompletedTurns)
2382
- .find((turn) => turn.id === lastTurnId);
2412
+ const completedTurn = remote?.completedTurns?.find((turn) => turn.id === lastTurnId);
2383
2413
  const cwd = remote?.cwd || source?.cwd;
2384
2414
  if (!cwd)
2385
2415
  throw new Error("指定的 App Server 任务没有可用的工作目录,暂时不能 fork。");
@@ -2421,27 +2451,10 @@ export class ProxySessionController {
2421
2451
  sourceTurnPrompt,
2422
2452
  sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
2423
2453
  sourceTurnCompletedAt,
2424
- sourceCompletedTurns: remote?.completedTurns
2425
- ?? (persistedCompletedTurns.length > 0 ? [] : localCompletedTurns),
2426
2454
  sourceWasRunning: isRunning,
2427
2455
  forkedFromHistoricalTurn,
2428
2456
  };
2429
2457
  }
2430
- localForkCompletedTurns(source) {
2431
- return this.store.listTaskTurnGraph(source.localSessionId)
2432
- .slice()
2433
- .reverse()
2434
- .map((record) => {
2435
- const snapshot = turnViewSnapshot(record.snapshot);
2436
- const startedAt = snapshot?.startedAt ?? parseIsoTimestamp(record.updatedAt);
2437
- return {
2438
- id: record.turnId,
2439
- prompt: latestUserPromptFromTurnView(snapshot),
2440
- startedAt,
2441
- completedAt: snapshot?.completedAt ?? startedAt,
2442
- };
2443
- });
2444
- }
2445
2458
  async prepareForkGroupSession(contextKey, requestedTitle, incomingMessage) {
2446
2459
  if (!incomingMessage?.threadContext) {
2447
2460
  const plan = await this.prepareForkSession(contextKey, undefined, requestedTitle);
@@ -2511,7 +2524,6 @@ export class ProxySessionController {
2511
2524
  };
2512
2525
  }
2513
2526
  async forkSessionIntoContext(contextKey, plan) {
2514
- const sourceCompletedTurns = await this.resolveForkSourceCompletedTurns(plan);
2515
2527
  const localSessionId = createId("sess");
2516
2528
  const record = this.store.createSession({
2517
2529
  localSessionId,
@@ -2546,8 +2558,7 @@ export class ProxySessionController {
2546
2558
  lastTurnId: plan.lastTurnId,
2547
2559
  lastTurnStatus: plan.lastTurnStatus,
2548
2560
  });
2549
- this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName));
2550
- const sourceTurnHistoryCount = this.persistForkSourceHistory(contextKey, localSessionId, plan, sourceCompletedTurns);
2561
+ this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName), plan.model);
2551
2562
  this.store.audit(contextKey, "session_forked", {
2552
2563
  sourceLocalSessionId: plan.source?.localSessionId,
2553
2564
  sourceRemoteSessionId: plan.remoteSessionId,
@@ -2555,7 +2566,6 @@ export class ProxySessionController {
2555
2566
  sourceTurnPrompt: plan.sourceTurnPrompt,
2556
2567
  sourceTurnStartedAt: plan.sourceTurnStartedAt,
2557
2568
  sourceTurnCompletedAt: plan.sourceTurnCompletedAt,
2558
- sourceTurnHistoryCount,
2559
2569
  sourceWasRunning: plan.sourceWasRunning,
2560
2570
  forkedLocalSessionId: localSessionId,
2561
2571
  forkedRemoteSessionId: forked.remoteSessionId,
@@ -2571,42 +2581,6 @@ export class ProxySessionController {
2571
2581
  throw error;
2572
2582
  }
2573
2583
  }
2574
- async resolveForkSourceCompletedTurns(plan) {
2575
- let completedTurns = plan.sourceCompletedTurns;
2576
- if (completedTurns === undefined && plan.runtime.readRemoteSession) {
2577
- try {
2578
- completedTurns = (await plan.runtime.readRemoteSession(plan.remoteSessionId)).completedTurns ?? [];
2579
- }
2580
- catch (error) {
2581
- this.logger.warn({ error, remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId }, "Failed to read the source task Turn history before Fork; keeping the Fork anchor fallback.");
2582
- }
2583
- }
2584
- if (!completedTurns?.length)
2585
- return [];
2586
- const anchorIndex = completedTurns.findIndex((turn) => turn.id === plan.lastTurnId);
2587
- if (anchorIndex < 0) {
2588
- this.logger.warn({ remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId }, "The source task Turn history did not include the Fork anchor; keeping the Fork anchor fallback.");
2589
- return [];
2590
- }
2591
- return completedTurns.slice(0, anchorIndex + 1);
2592
- }
2593
- persistForkSourceHistory(contextKey, forkedLocalSessionId, plan, completedTurns) {
2594
- if (completedTurns.length === 0)
2595
- return 0;
2596
- const localSessionId = plan.source?.localSessionId ?? forkedLocalSessionId;
2597
- const historyContextKey = plan.source?.contextKey ?? contextKey;
2598
- const fallbackStart = plan.sourceTurnStartedAt
2599
- ?? plan.sourceTurnCompletedAt
2600
- ?? Date.now() - completedTurns.length;
2601
- return this.persistCompletedTurnHistory({
2602
- localSessionId,
2603
- contextKey: historyContextKey,
2604
- agentName: plan.agentName,
2605
- remoteSessionId: plan.remoteSessionId,
2606
- completedTurns,
2607
- fallbackStart,
2608
- });
2609
- }
2610
2584
  persistCompletedTurnHistory(input) {
2611
2585
  try {
2612
2586
  this.store.importCompletedTurnHistory({
@@ -2614,6 +2588,7 @@ export class ProxySessionController {
2614
2588
  contextKey: input.contextKey,
2615
2589
  agentName: input.agentName,
2616
2590
  remoteSessionId: input.remoteSessionId,
2591
+ replaceParents: input.replaceParents,
2617
2592
  turns: input.completedTurns.map((turn, index) => {
2618
2593
  const startedAt = turn.startedAt ?? input.fallbackStart + index;
2619
2594
  const completedAt = turn.completedAt ?? startedAt;
@@ -2644,80 +2619,202 @@ export class ProxySessionController {
2644
2619
  return 0;
2645
2620
  }
2646
2621
  }
2647
- async hydrateLegacyForkTurnHistory(contextKey, current) {
2648
- const source = this.store.getForkHistorySource(current.localSessionId);
2649
- if (!source || source.sourceTurnHistoryCount !== undefined
2650
- || this.store.hasImportedForkTurnHistory(current.localSessionId))
2651
- return;
2622
+ async hydrateTaskTurnHistory(current, requiredCount = RESET_HISTORY_PAGE_SIZE, targetTurnId) {
2623
+ const existing = this.turnHistoryHydrations.get(current.localSessionId);
2624
+ if (existing)
2625
+ await existing;
2626
+ const hydration = (async () => {
2627
+ if (this.store.getForkHistorySource(current.localSessionId)) {
2628
+ return this.hydrateForkTurnHistory(current.contextKey, current, requiredCount, new Set(), targetTurnId);
2629
+ }
2630
+ return this.hydrateRemoteTurnHistory(current.contextKey, current, requiredCount, targetTurnId);
2631
+ })();
2632
+ this.turnHistoryHydrations.set(current.localSessionId, hydration);
2633
+ try {
2634
+ return await hydration;
2635
+ }
2636
+ finally {
2637
+ if (this.turnHistoryHydrations.get(current.localSessionId) === hydration) {
2638
+ this.turnHistoryHydrations.delete(current.localSessionId);
2639
+ }
2640
+ }
2641
+ }
2642
+ async hydrateForkTurnHistory(contextKey, current, requiredCount, visited = new Set(), targetTurnId) {
2643
+ if (visited.has(current.localSessionId))
2644
+ return false;
2645
+ visited.add(current.localSessionId);
2646
+ let source = this.store.getForkHistorySource(current.localSessionId);
2647
+ if (!source)
2648
+ return false;
2649
+ const completedHead = current.lastTurnStatus === "completed"
2650
+ ? current.lastTurnId
2651
+ : this.store.findLatestCompletedTurnId(current.localSessionId);
2652
+ if (completedHead && completedHead !== source.sourceTurnId && current.remoteSessionId) {
2653
+ // Once the branch has its own completed work, its remote history is the
2654
+ // authority for missing local turns, including work continued in Desktop.
2655
+ source = {
2656
+ sourceLocalSessionId: current.localSessionId,
2657
+ sourceRemoteSessionId: current.remoteSessionId,
2658
+ sourceTurnId: completedHead,
2659
+ createdAt: current.createdAt,
2660
+ };
2661
+ }
2662
+ if (this.store.hasImportedForkTurnHistory(current.localSessionId, source.sourceRemoteSessionId, source.sourceTurnId))
2663
+ return false;
2664
+ const localTurns = this.store.listTaskTurnGraphIndex(current.localSessionId);
2665
+ const availableCount = localTurns.length;
2666
+ const key = JSON.stringify([current.agentName, source.sourceRemoteSessionId, source.sourceTurnId]);
2667
+ let state = this.forkHistoryCursors.get(key) ?? { anchorFound: false, complete: false };
2668
+ const localAnchorIndex = localTurns.findIndex((turn) => turn.turnId === source.sourceTurnId);
2669
+ const ownTurnCount = source.sourceLocalSessionId === current.localSessionId
2670
+ ? Math.max(0, localAnchorIndex)
2671
+ : localAnchorIndex >= 0 ? localAnchorIndex : availableCount;
2672
+ const neededInheritedCount = Math.max(0, requiredCount - ownTurnCount);
2673
+ if (!targetTurnId && localAnchorIndex >= 0 && availableCount >= requiredCount
2674
+ && (!state.anchorFound || (state.loadedCount ?? 0) >= neededInheritedCount))
2675
+ return !state.complete;
2676
+ const sourceSession = source.sourceLocalSessionId
2677
+ ? this.store.getSession(source.sourceLocalSessionId)
2678
+ : undefined;
2679
+ // A branch with no new turns can reuse its ancestor's history, even before
2680
+ // the Agent materializes that intermediate branch.
2681
+ if (sourceSession && sourceSession.localSessionId !== current.localSessionId
2682
+ && this.store.getForkHistorySource(sourceSession.localSessionId)?.sourceTurnId === source.sourceTurnId) {
2683
+ const hasMore = await this.hydrateForkTurnHistory(sourceSession.contextKey, sourceSession, this.store.listTaskTurnGraphIndex(sourceSession.localSessionId).length + requiredCount - availableCount, visited, targetTurnId);
2684
+ if (!hasMore && this.store.hasImportedForkTurnHistory(sourceSession.localSessionId)) {
2685
+ this.store.audit(contextKey, "fork_turn_history_imported", {
2686
+ forkedLocalSessionId: current.localSessionId,
2687
+ sourceRemoteSessionId: source.sourceRemoteSessionId,
2688
+ sourceTurnId: source.sourceTurnId,
2689
+ reusedLocalSessionId: sourceSession.localSessionId,
2690
+ });
2691
+ }
2692
+ return hasMore;
2693
+ }
2652
2694
  if (!source.sourceRemoteSessionId)
2653
- return;
2695
+ return false;
2654
2696
  const runtime = this.runtimes.forAgent(current.agentName);
2655
- if (runtime.kind !== "codex" || !runtime.readRemoteSession)
2656
- return;
2697
+ if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries)
2698
+ return false;
2699
+ const seenCursors = new Set();
2657
2700
  try {
2658
- const remote = await runtime.readRemoteSession(source.sourceRemoteSessionId);
2659
- const completedTurns = remote.completedTurns ?? [];
2660
- const anchorIndex = completedTurns.findIndex((turn) => turn.id === source.sourceTurnId);
2661
- if (anchorIndex < 0)
2662
- return;
2663
- const inheritedTurns = completedTurns.slice(0, anchorIndex + 1);
2664
- const sourceSession = source.sourceLocalSessionId
2665
- ? this.store.getSession(source.sourceLocalSessionId)
2666
- : undefined;
2667
- const imported = this.persistCompletedTurnHistory({
2668
- localSessionId: sourceSession?.localSessionId ?? current.localSessionId,
2669
- contextKey: sourceSession?.contextKey ?? contextKey,
2670
- agentName: current.agentName,
2671
- remoteSessionId: source.sourceRemoteSessionId,
2672
- completedTurns: inheritedTurns,
2673
- fallbackStart: source.sourceTurnStartedAt
2674
- ?? source.sourceTurnCompletedAt
2675
- ?? Date.now() - inheritedTurns.length,
2676
- });
2677
- if (imported > 0) {
2701
+ while (!state.complete && (targetTurnId
2702
+ ? !this.store.hasTaskHistoryTurn(current.localSessionId, targetTurnId)
2703
+ : (state.loadedCount ?? 0) < neededInheritedCount)) {
2704
+ const remaining = targetTurnId ? RESET_HISTORY_PAGE_SIZE : neededInheritedCount - (state.loadedCount ?? 0);
2705
+ const page = await runtime.listRemoteTurnSummaries(source.sourceRemoteSessionId, {
2706
+ cursor: state.cursor, limit: Math.min(RESET_HISTORY_PAGE_SIZE, remaining),
2707
+ });
2708
+ if (page.nextCursor && (page.nextCursor === state.cursor || seenCursors.has(page.nextCursor))) {
2709
+ throw new Error("App Server repeated a Turn history cursor.");
2710
+ }
2711
+ const anchorIndex = state.anchorFound ? 0 : page.turns.findIndex((turn) => turn.id === source.sourceTurnId);
2712
+ const inheritedTurns = anchorIndex >= 0 ? page.turns.slice(anchorIndex) : [];
2713
+ const anchorFound = state.anchorFound || anchorIndex >= 0;
2714
+ if (!anchorFound && !page.nextCursor) {
2715
+ this.forkHistoryCursors.delete(key);
2716
+ throw new Error("The Fork source Turn was not found in the available history.");
2717
+ }
2718
+ if (inheritedTurns.length > 0) {
2719
+ const ownerId = sourceSession?.localSessionId ?? current.localSessionId;
2720
+ const imported = this.persistCompletedTurnHistory({
2721
+ localSessionId: ownerId,
2722
+ contextKey: sourceSession?.contextKey ?? contextKey,
2723
+ agentName: current.agentName,
2724
+ remoteSessionId: source.sourceRemoteSessionId,
2725
+ completedTurns: inheritedTurns.slice().reverse(),
2726
+ // Adjacent remote summaries repair gaps in locally inferred ancestry.
2727
+ replaceParents: true,
2728
+ fallbackStart: (source.sourceTurnStartedAt ?? source.sourceTurnCompletedAt
2729
+ ?? parseIsoTimestamp(source.createdAt) ?? 0) - (state.loadedCount ?? 0) - inheritedTurns.length,
2730
+ });
2731
+ if (imported === 0)
2732
+ throw new Error("Failed to persist the requested Turn history page.");
2733
+ if (state.tailTurnId) {
2734
+ this.store.saveTurnParent(state.tailTurnId, ownerId, inheritedTurns[0].id, true);
2735
+ }
2736
+ }
2737
+ state = {
2738
+ cursor: page.nextCursor,
2739
+ anchorFound,
2740
+ tailTurnId: inheritedTurns.at(-1)?.id ?? state.tailTurnId,
2741
+ loadedCount: (state.loadedCount ?? 0) + inheritedTurns.length,
2742
+ complete: !page.nextCursor,
2743
+ };
2744
+ this.forkHistoryCursors.set(key, state);
2745
+ if (page.nextCursor)
2746
+ seenCursors.add(page.nextCursor);
2747
+ }
2748
+ if (state.complete) {
2678
2749
  this.store.audit(contextKey, "fork_turn_history_imported", {
2679
2750
  forkedLocalSessionId: current.localSessionId,
2680
2751
  sourceRemoteSessionId: source.sourceRemoteSessionId,
2681
2752
  sourceTurnId: source.sourceTurnId,
2682
- importedTurnCount: imported,
2683
2753
  });
2684
2754
  }
2755
+ return !state.complete;
2685
2756
  }
2686
2757
  catch (error) {
2687
- this.logger.warn({ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId }, "Failed to hydrate legacy Fork Turn history; rendering the locally available history.");
2758
+ if (error instanceof CodexVersionError)
2759
+ throw error;
2760
+ this.logger.warn({ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId }, "Failed to load the requested Fork Turn history page; rendering the locally available history.");
2688
2761
  }
2762
+ return true;
2689
2763
  }
2690
- async hydrateRemoteTurnHistory(contextKey, current) {
2764
+ async hydrateRemoteTurnHistory(contextKey, current, requiredCount, targetTurnId) {
2691
2765
  if (!current.remoteSessionId)
2692
- return;
2766
+ return false;
2767
+ const key = JSON.stringify([current.agentName, current.remoteSessionId, "turn-card"]);
2693
2768
  const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
2694
- if (refreshedAt !== undefined && Date.now() - refreshedAt < REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS)
2695
- return;
2696
- const forkSource = this.store.getForkHistorySource(current.localSessionId);
2697
- if (forkSource?.sourceTurnId === current.lastTurnId
2698
- && this.store.hasImportedForkTurnHistory(current.localSessionId))
2699
- return;
2769
+ let state = this.forkHistoryCursors.get(key);
2770
+ if (refreshedAt === undefined || Date.now() - refreshedAt >= REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS)
2771
+ state = undefined;
2772
+ state ??= { anchorFound: true, complete: false };
2700
2773
  const runtime = this.runtimes.forAgent(current.agentName);
2701
- if (runtime.kind !== "codex" || !runtime.readRemoteSession)
2702
- return;
2774
+ if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries)
2775
+ return false;
2776
+ const seen = new Set();
2703
2777
  try {
2704
- const remote = await runtime.readRemoteSession(current.remoteSessionId);
2705
- this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
2706
- const completedTurns = remote.completedTurns ?? [];
2707
- if (completedTurns.length === 0)
2708
- return;
2709
- this.persistCompletedTurnHistory({
2710
- localSessionId: current.localSessionId,
2711
- contextKey,
2712
- agentName: current.agentName,
2713
- remoteSessionId: current.remoteSessionId,
2714
- completedTurns,
2715
- fallbackStart: parseIsoTimestamp(current.createdAt) ?? Date.now() - completedTurns.length,
2716
- });
2778
+ while (!state.complete && (targetTurnId
2779
+ ? !this.store.hasTaskHistoryTurn(current.localSessionId, targetTurnId)
2780
+ : (state.loadedCount ?? 0) < requiredCount)) {
2781
+ const page = await runtime.listRemoteTurnSummaries(current.remoteSessionId, {
2782
+ cursor: state.cursor,
2783
+ limit: targetTurnId ? RESET_HISTORY_PAGE_SIZE : Math.min(RESET_HISTORY_PAGE_SIZE, requiredCount - (state.loadedCount ?? 0)),
2784
+ });
2785
+ if (page.nextCursor && (page.nextCursor === state.cursor || seen.has(page.nextCursor))) {
2786
+ throw new Error("App Server repeated a Turn history cursor.");
2787
+ }
2788
+ if (page.turns.length) {
2789
+ const imported = this.persistCompletedTurnHistory({
2790
+ localSessionId: current.localSessionId, contextKey, agentName: current.agentName,
2791
+ remoteSessionId: current.remoteSessionId,
2792
+ completedTurns: page.turns.slice().reverse(), replaceParents: true,
2793
+ fallbackStart: (parseIsoTimestamp(current.createdAt) ?? 0) - (state.loadedCount ?? 0) - page.turns.length,
2794
+ });
2795
+ if (!imported)
2796
+ throw new Error("Failed to persist the requested Turn history page.");
2797
+ if (state.tailTurnId)
2798
+ this.store.saveTurnParent(state.tailTurnId, current.localSessionId, page.turns[0].id, true);
2799
+ }
2800
+ state = {
2801
+ anchorFound: true, cursor: page.nextCursor, complete: !page.nextCursor,
2802
+ loadedCount: (state.loadedCount ?? 0) + page.turns.length,
2803
+ tailTurnId: page.turns.at(-1)?.id ?? state.tailTurnId,
2804
+ };
2805
+ this.forkHistoryCursors.set(key, state);
2806
+ this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
2807
+ if (page.nextCursor)
2808
+ seen.add(page.nextCursor);
2809
+ }
2810
+ return !state.complete;
2717
2811
  }
2718
2812
  catch (error) {
2813
+ if (error instanceof CodexVersionError)
2814
+ throw error;
2719
2815
  this.logger.warn({ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId }, "Failed to hydrate App Server Turn history; rendering the locally available history.");
2720
2816
  }
2817
+ return true;
2721
2818
  }
2722
2819
  async createProjectSessionFromReference(contextKey, reference) {
2723
2820
  const resolved = await this.resolveProjectSessionReference(contextKey, reference);
@@ -2749,7 +2846,7 @@ export class ProxySessionController {
2749
2846
  runtime = this.runtimes.forAgent(agentName);
2750
2847
  }
2751
2848
  else {
2752
- const resolved = await this.resolveRemoteCodexSession(taskId);
2849
+ const resolved = await this.resolveRemoteCodexSession(taskId, { view: "metadata" });
2753
2850
  agentName = resolved.agentName;
2754
2851
  runtime = resolved.runtime;
2755
2852
  remote = resolved.remote;
@@ -2757,10 +2854,10 @@ export class ProxySessionController {
2757
2854
  const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
2758
2855
  if (!remote && runtime.readRemoteSession && remoteSessionId) {
2759
2856
  try {
2760
- remote = await runtime.readRemoteSession(remoteSessionId);
2857
+ remote = await runtime.readRemoteSession(remoteSessionId, "metadata");
2761
2858
  }
2762
2859
  catch (error) {
2763
- if (!source)
2860
+ if (!source || error instanceof CodexVersionError)
2764
2861
  throw error;
2765
2862
  this.logger.warn({ error, contextKey, taskId: remoteSessionId }, "Failed to refresh the source task before creating a project task; using the local project path.");
2766
2863
  }
@@ -2908,7 +3005,7 @@ export class ProxySessionController {
2908
3005
  });
2909
3006
  }
2910
3007
  this.store.setCurrentSession(contextKey, localSessionId);
2911
- this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd, this.agentLabel(agentName));
3008
+ this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd, this.agentLabel(agentName), resolvedExecutionSettings.model);
2912
3009
  const runtime = this.runtimes.forAgent(agentName);
2913
3010
  try {
2914
3011
  if (prepareTurn) {
@@ -2925,6 +3022,7 @@ export class ProxySessionController {
2925
3022
  permissionMode: resolvedExecutionSettings.permissionMode ?? "auto",
2926
3023
  });
2927
3024
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
3025
+ this.outbound.updateSessionModel(localSessionId, session.model);
2928
3026
  const saved = this.store.getSession(localSessionId) ?? record;
2929
3027
  if (announce) {
2930
3028
  const task = initialTitle ? `${initialTitle}(${session.remoteSessionId})` : session.remoteSessionId;
@@ -2991,6 +3089,7 @@ export class ProxySessionController {
2991
3089
  if (!userId?.startsWith("ou_")) {
2992
3090
  throw new Error("/forkgroup 只能由具有 open_id 的飞书用户消息触发。");
2993
3091
  }
3092
+ await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
2994
3093
  const prepared = await this.prepareForkGroupSession(sourceContextKey, requestedTitle, incomingMessage);
2995
3094
  await this.forkPreparedSessionToFeishuGroup(sourceContextKey, prepared, userId, incomingMessage?.threadContext ? prepared.sourceDescription : "当前任务");
2996
3095
  }
@@ -2998,6 +3097,7 @@ export class ProxySessionController {
2998
3097
  if (!userId?.startsWith("ou_")) {
2999
3098
  throw new Error("ForkGroup 只能由具有 open_id 的飞书用户触发。");
3000
3099
  }
3100
+ await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
3001
3101
  const plan = await this.prepareForkSession(sourceContextKey, reference);
3002
3102
  await this.forkPreparedSessionToFeishuGroup(sourceContextKey, {
3003
3103
  plan,
@@ -3070,7 +3170,7 @@ export class ProxySessionController {
3070
3170
  record: { ...loaded.record, contextKey: record.contextKey },
3071
3171
  };
3072
3172
  }
3073
- this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName));
3173
+ this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName), record.model);
3074
3174
  const loading = (async () => {
3075
3175
  if (record.lastTurnId) {
3076
3176
  try {
@@ -3106,7 +3206,8 @@ export class ProxySessionController {
3106
3206
  catch (error) {
3107
3207
  if (activeWriterThreadId(error))
3108
3208
  throw error;
3109
- if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error)))
3209
+ if (!(agent.kind === "app-server" && !record.lastTurnId
3210
+ && !this.store.getForkHistorySource(record.localSessionId) && isMissingRolloutError(error)))
3110
3211
  throw error;
3111
3212
  this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
3112
3213
  session = await runtime.createSession({
@@ -3210,14 +3311,20 @@ export class ProxySessionController {
3210
3311
  lastTurnStatus: event.type === "turn_completed" ? "completed" : event.type === "turn_cancelled" ? "cancelled" : "failed",
3211
3312
  });
3212
3313
  }
3314
+ const enrichedEvent = event.type === "context_compaction"
3315
+ ? {
3316
+ ...event,
3317
+ turnCount: event.turnCount ?? this.store.countTaskTurns(event.sessionId),
3318
+ }
3319
+ : event;
3213
3320
  const presentationEvent = event.type === "turn_failed"
3214
3321
  && isRetryableLlmTurnFailure(event.message)
3215
3322
  && (failedAttempt?.retryCount ?? 0) >= MAX_LLM_TURN_RETRIES
3216
3323
  ? {
3217
- ...event,
3324
+ ...enrichedEvent,
3218
3325
  message: `${event.message}\n\n已自动重试 ${MAX_LLM_TURN_RETRIES} 次,仍未成功。`,
3219
3326
  }
3220
- : event;
3327
+ : enrichedEvent;
3221
3328
  let presentationError;
3222
3329
  try {
3223
3330
  await this.outbound.onEvent(presentationEvent);
@@ -3521,6 +3628,7 @@ export class ProxySessionController {
3521
3628
  }
3522
3629
  async cancel(contextKey) {
3523
3630
  const record = this.requireCurrentSession(contextKey);
3631
+ await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
3524
3632
  await this.cancelSession(record);
3525
3633
  }
3526
3634
  async cancelSession(record) {
@@ -3535,6 +3643,8 @@ export class ProxySessionController {
3535
3643
  await runtime.setGoal(record.localSessionId, { status: "paused" });
3536
3644
  }
3537
3645
  catch (error) {
3646
+ if (error instanceof CodexVersionError)
3647
+ throw error;
3538
3648
  this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to pause the active Agent goal before interrupting its turn.");
3539
3649
  }
3540
3650
  }
@@ -3547,6 +3657,8 @@ export class ProxySessionController {
3547
3657
  : undefined;
3548
3658
  }
3549
3659
  catch (error) {
3660
+ if (error instanceof CodexVersionError)
3661
+ throw error;
3550
3662
  this.logger.warn({ error, sessionId: record.localSessionId, remoteSessionId: record.remoteSessionId }, "Failed to inspect the current App Server turn before interrupting; using the locally tracked turn.");
3551
3663
  }
3552
3664
  }
@@ -3590,8 +3702,14 @@ export class ProxySessionController {
3590
3702
  async resetCurrentSessionToTurn(contextKey, sessionId, turnId) {
3591
3703
  if (!sessionId || !turnId)
3592
3704
  throw new Error("无效的 Reset 请求。请使用最新的思考卡片重试。");
3705
+ await this.runSessionReset(sessionId, async () => {
3706
+ await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
3707
+ await this.performCurrentSessionReset(contextKey, sessionId, turnId);
3708
+ });
3709
+ }
3710
+ async runSessionReset(sessionId, operation) {
3593
3711
  const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
3594
- const reset = previous.catch(() => undefined).then(() => this.performCurrentSessionReset(contextKey, sessionId, turnId));
3712
+ const reset = previous.catch(() => undefined).then(operation);
3595
3713
  this.sessionResets.set(sessionId, reset);
3596
3714
  try {
3597
3715
  await reset;
@@ -3601,6 +3719,19 @@ export class ProxySessionController {
3601
3719
  this.sessionResets.delete(sessionId);
3602
3720
  }
3603
3721
  }
3722
+ async waitForCurrentSessionReset(contextKey) {
3723
+ const sessionId = this.currentSession(contextKey)?.localSessionId;
3724
+ if (!sessionId)
3725
+ return;
3726
+ let reset = this.sessionResets.get(sessionId);
3727
+ while (reset) {
3728
+ await reset.catch(() => undefined);
3729
+ const next = this.sessionResets.get(sessionId);
3730
+ if (!next || next === reset)
3731
+ return;
3732
+ reset = next;
3733
+ }
3734
+ }
3604
3735
  async resetFromHistoryCard(contextKey, sessionId, turnId, updateMessageId, page) {
3605
3736
  if (!sessionId || !turnId)
3606
3737
  throw new Error("无效的 Reset 请求。请使用最新的历史轮次卡片重试。");
@@ -3611,16 +3742,11 @@ export class ProxySessionController {
3611
3742
  }
3612
3743
  this.resetHistoryOperations.set(sessionId, turnId);
3613
3744
  try {
3614
- await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
3615
- }
3616
- catch (error) {
3617
- if (this.resetHistoryOperations.get(sessionId) === turnId) {
3618
- this.resetHistoryOperations.delete(sessionId);
3619
- }
3620
- throw error;
3621
- }
3622
- try {
3623
- await this.resetCurrentSessionToTurn(contextKey, sessionId, turnId);
3745
+ await this.runSessionReset(sessionId, async () => {
3746
+ await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
3747
+ await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
3748
+ await this.performCurrentSessionReset(contextKey, sessionId, turnId);
3749
+ });
3624
3750
  }
3625
3751
  catch (error) {
3626
3752
  if (this.resetHistoryOperations.get(sessionId) === turnId) {
@@ -3650,9 +3776,9 @@ export class ProxySessionController {
3650
3776
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
3651
3777
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
3652
3778
  }
3653
- const historyTurn = this.store.listTaskTurnGraph(current.localSessionId)
3779
+ const historyTurn = this.store.listTaskTurnGraphIndex(current.localSessionId)
3654
3780
  .find((turn) => turn.turnId === turnId);
3655
- const snapshot = turnViewSnapshot(historyTurn?.snapshot);
3781
+ const snapshot = historyTurn ? this.store.getTurnPromptSummary(turnId) : undefined;
3656
3782
  if (!snapshot || snapshot.status !== "completed") {
3657
3783
  throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
3658
3784
  }
@@ -3695,7 +3821,7 @@ export class ProxySessionController {
3695
3821
  lastTurnId: turnId,
3696
3822
  lastTurnStatus: "completed",
3697
3823
  });
3698
- this.outbound.registerSession(current.localSessionId, contextKey, forked.title ?? current.title, current.cwd, this.agentLabel(current.agentName));
3824
+ this.outbound.registerSession(current.localSessionId, contextKey, forked.title ?? current.title, current.cwd, this.agentLabel(current.agentName), current.model);
3699
3825
  this.store.audit(contextKey, "session_reset_to_turn", {
3700
3826
  localSessionId: current.localSessionId,
3701
3827
  previousRemoteSessionId,
@@ -3745,12 +3871,13 @@ export class ProxySessionController {
3745
3871
  };
3746
3872
  if (record) {
3747
3873
  const loaded = await this.loadSession(record);
3748
- const models = await loaded.runtime.listModels();
3749
- const currentModel = models.find((model) => model.id === loaded.session.model)
3750
- ?? models.find((model) => model.isDefault);
3751
3874
  const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
3752
3875
  const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
3753
3876
  const currentProvider = loaded.session.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
3877
+ const models = await loaded.runtime.listModels(currentProvider, loaded.session.model);
3878
+ const currentModel = models.find((model) => model.id === loaded.session.model)
3879
+ ?? models.find((model) => model.isDefault)
3880
+ ?? models[0];
3754
3881
  const currentEffort = currentModel?.supportedReasoningEfforts.some((option) => option.value === loaded.session.reasoningEffort)
3755
3882
  ? loaded.session.reasoningEffort
3756
3883
  : currentModel?.defaultReasoningEffort ?? loaded.session.reasoningEffort;
@@ -3808,24 +3935,20 @@ export class ProxySessionController {
3808
3935
  const record = options.sessionId
3809
3936
  ? this.requireSession(contextKey, options.sessionId)
3810
3937
  : this.requireCurrentSession(contextKey);
3938
+ await this.outbound.sendText(contextKey, `正在切换到 Provider ${modelProvider},请稍后。`);
3811
3939
  const loaded = await this.loadSession(record);
3812
3940
  await this.assertModelProvider(loaded, modelProvider);
3813
- const models = await loaded.runtime.listModels();
3814
- const model = models.find((candidate) => candidate.id === loaded.session.model)
3815
- ?? models.find((candidate) => candidate.isDefault);
3941
+ const models = await loaded.runtime.listModels(modelProvider, loaded.session.model);
3942
+ const model = selectProviderModel(models, loaded.session.model);
3816
3943
  if (!model)
3817
3944
  throw new Error("当前运行时没有可用于 Provider 切换的模型。");
3818
- const effort = model.supportedReasoningEfforts.some((candidate) => candidate.value === loaded.session.reasoningEffort)
3819
- ? loaded.session.reasoningEffort
3820
- : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
3821
- if (!effort)
3822
- throw new Error(`模型 ${model.id} 没有可用于 Provider 切换的思考强度。`);
3945
+ const effort = selectProviderReasoningEffort(model, loaded.session.reasoningEffort);
3823
3946
  await this.applyProviderSettings(contextKey, {
3824
3947
  provider: modelProvider,
3825
3948
  model: model.id,
3826
3949
  effort,
3827
3950
  mode: loaded.session.permissionMode,
3828
- }, options);
3951
+ }, options, false);
3829
3952
  }
3830
3953
  async openProviderSelector(contextKey, options = {}) {
3831
3954
  await this.openExecutionSettings(contextKey, "provider", options);
@@ -3833,11 +3956,11 @@ export class ProxySessionController {
3833
3956
  async openProviderModelSelector(contextKey, modelProvider, permissionMode, sessionId, updateMessageId) {
3834
3957
  const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
3835
3958
  await this.assertModelProvider(loaded, modelProvider);
3836
- const models = await loaded.runtime.listModels();
3959
+ const models = await loaded.runtime.listModels(modelProvider, loaded.session.model);
3837
3960
  const card = this.cardRenderer.renderModelSelector({
3838
3961
  sessionId,
3839
3962
  contextKey,
3840
- currentModel: loaded.session.model ?? models.find((model) => model.isDefault)?.id,
3963
+ currentModel: selectProviderModel(models, loaded.session.model)?.id,
3841
3964
  reasoningEffort: loaded.session.reasoningEffort,
3842
3965
  models,
3843
3966
  modelProvider,
@@ -3849,20 +3972,23 @@ export class ProxySessionController {
3849
3972
  async openProviderReasoningSelector(contextKey, modelProvider, model, permissionMode, sessionId, updateMessageId) {
3850
3973
  const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
3851
3974
  await this.assertModelProvider(loaded, modelProvider);
3852
- const models = await loaded.runtime.listModels();
3975
+ const models = await loaded.runtime.listModels(modelProvider, model);
3853
3976
  const selected = models.find((candidate) => candidate.id === model);
3854
3977
  if (!selected)
3855
3978
  throw new Error(`未知模型:${model}`);
3856
- const currentEffort = selected.supportedReasoningEfforts.some((option) => option.value === loaded.session.reasoningEffort)
3979
+ const options = selected.supportedReasoningEfforts.length > 0
3980
+ ? selected.supportedReasoningEfforts
3981
+ : [{ value: selectProviderReasoningEffort(selected, loaded.session.reasoningEffort) }];
3982
+ const currentEffort = options.some((option) => option.value === loaded.session.reasoningEffort)
3857
3983
  ? loaded.session.reasoningEffort
3858
- : selected.defaultReasoningEffort;
3984
+ : selected.defaultReasoningEffort ?? options[0]?.value;
3859
3985
  const card = this.cardRenderer.renderReasoningSelector({
3860
3986
  sessionId,
3861
3987
  contextKey,
3862
3988
  modelProvider,
3863
3989
  model,
3864
3990
  currentEffort,
3865
- options: selected.supportedReasoningEfforts,
3991
+ options,
3866
3992
  permissionMode,
3867
3993
  unifiedSettings: true,
3868
3994
  });
@@ -3880,28 +4006,21 @@ export class ProxySessionController {
3880
4006
  currentMode: permissionMode,
3881
4007
  }));
3882
4008
  }
3883
- async applyProviderSettings(contextKey, settings, options = {}) {
4009
+ async applyProviderSettings(contextKey, settings, options = {}, announceStart = true) {
3884
4010
  const record = options.sessionId
3885
4011
  ? this.requireSession(contextKey, options.sessionId)
3886
4012
  : this.requireCurrentSession(contextKey);
4013
+ if (announceStart) {
4014
+ await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
4015
+ }
3887
4016
  const loaded = await this.loadSession(record);
3888
4017
  await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
3889
- if (!loaded.runtime.setExecutionSettings) {
3890
- throw new Error("当前运行时不支持 Provider 设置。");
3891
- }
3892
- const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
4018
+ const session = await this.changeProviderSettings(loaded, {
3893
4019
  modelProvider: settings.provider,
3894
4020
  model: settings.model,
3895
4021
  reasoningEffort: settings.effort,
3896
4022
  permissionMode: settings.mode,
3897
4023
  });
3898
- this.store.updateRuntimeSession(loaded.record.localSessionId, {
3899
- modelProvider: session.modelProvider ?? settings.provider,
3900
- model: session.model ?? settings.model,
3901
- reasoningEffort: session.reasoningEffort ?? settings.effort,
3902
- permissionMode: session.permissionMode,
3903
- });
3904
- await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
3905
4024
  const notice = [
3906
4025
  `Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
3907
4026
  `模型 ${cardCode(session.model ?? settings.model)}`,
@@ -3920,12 +4039,38 @@ export class ProxySessionController {
3920
4039
  await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
3921
4040
  }
3922
4041
  }
4042
+ async changeProviderSettings(loaded, settings) {
4043
+ if (!loaded.runtime.setExecutionSettings)
4044
+ throw new Error("当前运行时不支持 Provider 设置。");
4045
+ const persist = async (candidate) => {
4046
+ assertProviderSettingsApplied(candidate, settings);
4047
+ const agent = this.ensureAgent(loaded.record.agentName);
4048
+ const previousDefaults = agent.defaults;
4049
+ await this.persistAgentExecutionDefaults(loaded.record.localSessionId, candidate);
4050
+ try {
4051
+ this.store.updateRuntimeSession(loaded.record.localSessionId, {
4052
+ remoteSessionId: candidate.remoteSessionId,
4053
+ modelProvider: candidate.modelProvider,
4054
+ model: candidate.model,
4055
+ reasoningEffort: candidate.reasoningEffort,
4056
+ permissionMode: candidate.permissionMode,
4057
+ });
4058
+ }
4059
+ catch (error) {
4060
+ await this.lifecycle?.persistAgentExecutionDefaults?.(loaded.record.agentName, previousDefaults ?? {});
4061
+ agent.defaults = previousDefaults;
4062
+ throw error;
4063
+ }
4064
+ };
4065
+ return loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
4066
+ }
3923
4067
  async assertProviderModelSettings(loaded, modelProvider, model, effort) {
3924
4068
  await this.assertModelProvider(loaded, modelProvider);
3925
- const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
4069
+ const selected = (await loaded.runtime.listModels(modelProvider, model)).find((candidate) => candidate.id === model);
3926
4070
  if (!selected)
3927
4071
  throw new Error(`未知模型:${model}`);
3928
- if (!selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
4072
+ if (selected.supportedReasoningEfforts.length > 0
4073
+ && !selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
3929
4074
  const supported = selected.supportedReasoningEfforts.map((option) => option.value).join("、") || "无";
3930
4075
  throw new Error(`模型 ${model} 不支持思考强度 ${effort}。支持的强度:${supported}`);
3931
4076
  }
@@ -3956,13 +4101,14 @@ export class ProxySessionController {
3956
4101
  ? this.requireSession(contextKey, options.sessionId)
3957
4102
  : this.requireCurrentSession(contextKey);
3958
4103
  const loaded = await this.loadSession(record);
3959
- const models = await loaded.runtime.listModels();
4104
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
3960
4105
  const selected = models.find((item) => item.id === model);
3961
4106
  if (!selected)
3962
4107
  throw new Error(`未知模型:${model}`);
3963
4108
  const currentEffort = loaded.session.reasoningEffort;
3964
4109
  const compatible = currentEffort
3965
- ? selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
4110
+ ? selected.supportedReasoningEfforts.length === 0
4111
+ || selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
3966
4112
  : false;
3967
4113
  const nextEffort = compatible ? currentEffort : selected.defaultReasoningEffort;
3968
4114
  await loaded.runtime.setModel(loaded.record.localSessionId, model);
@@ -4282,7 +4428,9 @@ export class ProxySessionController {
4282
4428
  const normalizedTitle = normalizeTaskTitle(title);
4283
4429
  if (!normalizedTitle)
4284
4430
  throw new Error("任务标题不能为空。");
4285
- const loaded = await this.loadSession(this.requireCurrentSession(contextKey));
4431
+ const current = this.requireCurrentSession(contextKey);
4432
+ await this.outbound.sendText(contextKey, CHANGE_TITLE_STARTED_MESSAGE);
4433
+ const loaded = await this.loadSession(current);
4286
4434
  if (loaded.runtime.setTitle)
4287
4435
  await loaded.runtime.setTitle(loaded.record.localSessionId, normalizedTitle);
4288
4436
  else
@@ -4341,7 +4489,7 @@ export class ProxySessionController {
4341
4489
  ? this.requireSession(contextKey, options.sessionId)
4342
4490
  : this.requireCurrentSession(contextKey);
4343
4491
  const loaded = await this.loadSession(record);
4344
- const models = await loaded.runtime.listModels();
4492
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
4345
4493
  const currentModel = models.find((item) => item.id === loaded.session.model)
4346
4494
  ?? models.find((item) => item.isDefault);
4347
4495
  if (!currentModel)
@@ -4394,6 +4542,73 @@ export class ProxySessionController {
4394
4542
  ? `已切换为自动执行模式,并已保存为 ${loaded.record.agentName} Agent 的默认设置。`
4395
4543
  : `已切换为执行前确认模式,并已保存为 ${loaded.record.agentName} Agent 的默认设置。`);
4396
4544
  }
4545
+ taskTurnIds(current) {
4546
+ const ids = this.store.listTaskTurnGraphIndex(current.localSessionId).map((turn) => turn.turnId);
4547
+ if (current.lastTurnStatus === "running" && current.lastTurnId && !ids.includes(current.lastTurnId)) {
4548
+ ids.unshift(current.lastTurnId);
4549
+ }
4550
+ return ids;
4551
+ }
4552
+ runningTurnSnapshot(current, turnId) {
4553
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
4554
+ if (snapshot)
4555
+ return snapshot;
4556
+ const attempt = this.store.findIncompleteTurnAttemptByTurnId(turnId);
4557
+ const startedAt = Date.parse(attempt?.createdAt ?? "");
4558
+ return {
4559
+ sessionId: current.localSessionId,
4560
+ turnId,
4561
+ taskTitle: current.title,
4562
+ prompt: attempt?.promptText ?? current.title,
4563
+ status: "running",
4564
+ startedAt: Number.isFinite(startedAt) ? startedAt : Date.now(),
4565
+ assistantText: "", plan: [], activities: [], completedTools: [], failedTools: [], fileSummary: [],
4566
+ };
4567
+ }
4568
+ async openTurnDetails(contextKey, reference) {
4569
+ const current = this.requireCurrentSession(contextKey);
4570
+ let turnId;
4571
+ if (/^\d+$/.test(reference)) {
4572
+ const index = Number(reference);
4573
+ const runningCount = current.lastTurnStatus === "running" && current.lastTurnId ? 1 : 0;
4574
+ const hasMore = index > runningCount && this.isCodexSession(current)
4575
+ ? await this.hydrateTaskTurnHistory(current, index - runningCount)
4576
+ : false;
4577
+ turnId = this.taskTurnIds(current)[index - 1];
4578
+ if (!turnId) {
4579
+ throw new Error(hasMore
4580
+ ? "轮次历史暂未加载完成,请稍后重试。"
4581
+ : `未找到序号为 ${reference} 的轮次。请发送 /turn 查看当前轮次序号。`);
4582
+ }
4583
+ }
4584
+ else {
4585
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(reference));
4586
+ if (snapshot && (snapshot.sessionId === current.localSessionId
4587
+ || this.store.hasTaskHistoryTurn(current.localSessionId, reference))) {
4588
+ turnId = reference;
4589
+ }
4590
+ else if (current.lastTurnStatus === "running" && current.lastTurnId === reference) {
4591
+ turnId = reference;
4592
+ }
4593
+ else if (this.isCodexSession(current) && (!snapshot || this.store.getForkHistorySource(current.localSessionId))) {
4594
+ const hasMore = await this.hydrateTaskTurnHistory(current, 0, reference);
4595
+ if (this.store.hasTaskHistoryTurn(current.localSessionId, reference))
4596
+ turnId = reference;
4597
+ else if (hasMore)
4598
+ throw new Error("轮次历史暂未加载完成,请稍后重试。");
4599
+ }
4600
+ if (!turnId && snapshot)
4601
+ throw new Error("这个 Turn 不属于当前任务的历史。请先切换到对应任务。");
4602
+ if (!turnId)
4603
+ throw new Error(`未找到当前任务的 Turn:${reference}`);
4604
+ }
4605
+ if (current.lastTurnStatus === "running" && current.lastTurnId === turnId
4606
+ && !this.store.getTurnPromptSummary(turnId)) {
4607
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderTurnDetails(this.runningTurnSnapshot(current, turnId)));
4608
+ return;
4609
+ }
4610
+ await this.outbound.showDetails(contextKey, turnId);
4611
+ }
4397
4612
  async openResetHistory(contextKey, options = {}) {
4398
4613
  const current = this.requireCurrentSession(contextKey);
4399
4614
  if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
@@ -4402,35 +4617,19 @@ export class ProxySessionController {
4402
4617
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
4403
4618
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
4404
4619
  }
4405
- await this.hydrateLegacyForkTurnHistory(contextKey, current);
4406
- await this.hydrateRemoteTurnHistory(contextKey, current);
4407
- const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
4620
+ const requestedPage = Math.max(0, Math.trunc(options.page ?? 0));
4621
+ const hasMoreHistory = await this.hydrateTaskTurnHistory(current, (requestedPage + 1) * RESET_HISTORY_PAGE_SIZE);
4622
+ const completedTurns = this.store.listTaskTurnGraphIndex(current.localSessionId);
4408
4623
  const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
4409
4624
  const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
4410
4625
  && !completedTurnIds.has(current.lastTurnId)
4411
4626
  ? current.lastTurnId
4412
4627
  : undefined;
4413
- const persistedRunningSnapshot = runningTurnId
4414
- ? turnViewSnapshot(this.store.getTurnSnapshot(runningTurnId))
4415
- : undefined;
4416
4628
  const runningAttempt = runningTurnId
4417
4629
  ? this.store.findIncompleteTurnAttemptByTurnId(runningTurnId)
4418
4630
  : undefined;
4419
- const attemptStartedAt = Date.parse(runningAttempt?.createdAt ?? "");
4420
4631
  const runningSnapshot = runningTurnId
4421
- ? persistedRunningSnapshot ?? {
4422
- sessionId: current.localSessionId,
4423
- turnId: runningTurnId,
4424
- prompt: runningAttempt?.promptText ?? current.title,
4425
- status: "running",
4426
- startedAt: Number.isFinite(attemptStartedAt) ? attemptStartedAt : Date.now(),
4427
- assistantText: "",
4428
- plan: [],
4429
- activities: [],
4430
- completedTools: [],
4431
- failedTools: [],
4432
- fileSummary: [],
4433
- }
4632
+ ? this.runningTurnSnapshot(current, runningTurnId)
4434
4633
  : undefined;
4435
4634
  const runningTurn = runningTurnId && runningSnapshot
4436
4635
  ? {
@@ -4446,13 +4645,13 @@ export class ProxySessionController {
4446
4645
  parentTurnId: turn.parentTurnId,
4447
4646
  })));
4448
4647
  const total = allTurns.length;
4449
- const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE));
4648
+ const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE)) + (hasMoreHistory ? 1 : 0);
4450
4649
  const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
4451
4650
  const offset = page * RESET_HISTORY_PAGE_SIZE;
4452
4651
  const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
4453
4652
  const resettingTurnId = this.resetHistoryOperations.get(current.localSessionId);
4454
4653
  const entries = turns.flatMap((turn, index) => {
4455
- const snapshot = turnViewSnapshot(turn.snapshot);
4654
+ const snapshot = turn.turnId === runningTurnId ? runningSnapshot : this.store.getTurnPromptSummary(turn.turnId);
4456
4655
  if (!snapshot)
4457
4656
  return [];
4458
4657
  const graph = graphRows[offset + index];
@@ -4505,9 +4704,11 @@ export class ProxySessionController {
4505
4704
  const card = this.cardRenderer.renderResetHistoryCard({
4506
4705
  entries,
4507
4706
  footerLines: [
4508
- runningTurn
4509
- ? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
4510
- : `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
4707
+ hasMoreHistory
4708
+ ? `第 ${page + 1} 页 · 已加载 ${total} 个 turn`
4709
+ : runningTurn
4710
+ ? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
4711
+ : `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
4511
4712
  ...(resettingTurnId ? ["正在 Reset 到所选轮次,请稍候…"] : []),
4512
4713
  ],
4513
4714
  pageActions,
@@ -4706,6 +4907,7 @@ export class ProxySessionController {
4706
4907
  const record = this.requireCurrentSession(contextKey);
4707
4908
  const filePath = resolveUserPath(requestedFilePath, record.cwd);
4708
4909
  await this.assertSendableFile(filePath);
4910
+ await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
4709
4911
  await this.outbound.sendFile(contextKey, filePath);
4710
4912
  }
4711
4913
  async listSessions(contextKey, searchTerm, options = {}) {
@@ -4996,7 +5198,7 @@ export class ProxySessionController {
4996
5198
  runtime,
4997
5199
  remote: options.forFork && runtime.readRemoteForkSource
4998
5200
  ? await runtime.readRemoteForkSource(remoteSessionId)
4999
- : await runtime.readRemoteSession(remoteSessionId),
5201
+ : await runtime.readRemoteSession(remoteSessionId, options.view ?? "latest"),
5000
5202
  };
5001
5203
  }));
5002
5204
  const matches = reads
@@ -5022,13 +5224,14 @@ export class ProxySessionController {
5022
5224
  if (direct?.status === "closed") {
5023
5225
  throw new Error(`找不到任务:${taskId}`);
5024
5226
  }
5227
+ await this.outbound.sendText(contextKey, SWITCH_TASK_STARTED_MESSAGE);
5025
5228
  const existing = direct ?? this.findStoredSessionByReference(taskId);
5026
5229
  if (existing) {
5027
5230
  const runtime = this.runtimes.forAgent(existing.agentName);
5028
5231
  await this.assertSessionTurnOwnership(existing, runtime);
5029
5232
  this.store.attachSessionToContext(contextKey, existing.localSessionId);
5030
5233
  this.store.setCurrentSession(contextKey, existing.localSessionId);
5031
- this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd, this.agentLabel(existing.agentName));
5234
+ this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd, this.agentLabel(existing.agentName), existing.model);
5032
5235
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
5033
5236
  return;
5034
5237
  }
@@ -5055,7 +5258,7 @@ export class ProxySessionController {
5055
5258
  lastTurnStatus: mapRemoteTurnStatus(remote.lastTurnStatus),
5056
5259
  });
5057
5260
  this.store.setCurrentSession(contextKey, localSessionId);
5058
- this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd, this.agentLabel(agentName));
5261
+ this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd, this.agentLabel(agentName), remote.model);
5059
5262
  await this.outbound.sendText(contextKey, `已切换到任务:${remote.title ?? remote.preview ?? remote.id}。历史消息不会重新发送。`);
5060
5263
  }
5061
5264
  async stopSessionReference(contextKey, taskId) {
@@ -5106,6 +5309,9 @@ export class ProxySessionController {
5106
5309
  if (local && this.store.countQueuedPrompts(local.localSessionId) > 0) {
5107
5310
  throw new Error("任务仍有排队消息,无法归档。请先处理或取消排队消息。");
5108
5311
  }
5312
+ if (options.source !== "dismiss") {
5313
+ await this.outbound.sendText(contextKey, ARCHIVE_TASK_STARTED_MESSAGE);
5314
+ }
5109
5315
  const remoteReference = local
5110
5316
  ? remoteSessionReference(local.agentName, local.remoteSessionId)
5111
5317
  : taskId;
@@ -5313,28 +5519,33 @@ export class ProxySessionController {
5313
5519
  else {
5314
5520
  current = context.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
5315
5521
  }
5316
- if (current && current.status === "running") {
5317
- try {
5318
- const runtime = this.runtimes.forAgent(current.agentName);
5319
- if (runtime.getSession(current.localSessionId)) {
5320
- await runtime.synchronizeSession(current.localSessionId);
5522
+ let remote;
5523
+ let metrics;
5524
+ if (current?.status === "running") {
5525
+ const runtime = this.runtimes.forAgent(current.agentName);
5526
+ if (runtime.getSession(current.localSessionId)) {
5527
+ try {
5528
+ remote = (await runtime.synchronizeSession(current.localSessionId)).remoteSummary;
5321
5529
  current = this.store.getSession(current.localSessionId) ?? current;
5322
5530
  }
5323
- }
5324
- catch (error) {
5325
- this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
5531
+ catch (error) {
5532
+ if (error instanceof CodexVersionError)
5533
+ throw error;
5534
+ this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
5535
+ }
5326
5536
  }
5327
5537
  }
5328
5538
  const localCurrent = current;
5329
- let remote;
5330
5539
  let goal;
5331
5540
  if (current?.remoteSessionId) {
5332
5541
  const runtime = this.runtimes.forAgent(current.agentName);
5333
- if (runtime.readRemoteSession) {
5542
+ if (!remote && runtime.readRemoteSession) {
5334
5543
  try {
5335
- remote = await runtime.readRemoteSession(current.remoteSessionId);
5544
+ remote = await runtime.readRemoteSession(current.remoteSessionId, "latest-full");
5336
5545
  }
5337
5546
  catch (error) {
5547
+ if (error instanceof CodexVersionError)
5548
+ throw error;
5338
5549
  this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task status.");
5339
5550
  }
5340
5551
  }
@@ -5347,6 +5558,14 @@ export class ProxySessionController {
5347
5558
  this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect Agent goal status.");
5348
5559
  }
5349
5560
  }
5561
+ if (runtime.readRemoteSessionMetrics) {
5562
+ try {
5563
+ metrics = await runtime.readRemoteSessionMetrics(current.remoteSessionId);
5564
+ }
5565
+ catch (error) {
5566
+ this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task metrics.");
5567
+ }
5568
+ }
5350
5569
  }
5351
5570
  if (current)
5352
5571
  current = mergeRemoteTaskStatus(current, remote);
@@ -5373,7 +5592,7 @@ export class ProxySessionController {
5373
5592
  const resultLabel = remoteActive
5374
5593
  ? "执行中"
5375
5594
  : remoteTurnStatusLabel(remote?.lastTurnStatus ?? current.lastTurnStatus);
5376
- taskLines.push(`**标题**:${cardCode(current.title ?? "未命名任务")}`, `**工作目录**:${cardCode(current.cwd)}`, `**Provider / 模型 / 思考强度**:${cardCode(current.modelProvider ?? "Agent 默认")} / ${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`, `**状态 / 最近结果**:${statusLabel} / ${resultLabel}`, `**Agent**:${cardCode(agent.title)}`, `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`, `**App Server 任务 ID**:${cardCode(current.remoteSessionId ?? "尚未创建")}`, `**当前执行 / 排队消息**:${activeTurnId ? cardCode(activeTurnId) : "无"} / ${queued} 条`, `**创建时间 / 最近活动**:${formatStatusTime(current.createdAt)} / ${formatStatusTime(current.updatedAt)}`);
5595
+ taskLines.push(`**标题**:${cardCode(current.title ?? "未命名任务")}`, `**工作目录**:${cardCode(current.cwd)}`, `**Provider / 模型 / 思考强度**:${cardCode(current.modelProvider ?? "Agent 默认")} / ${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`, `**状态 / 最近结果**:${statusLabel} / ${resultLabel}`, `**Agent**:${cardCode(agent.title)}`, `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`, `**App Server 任务 ID**:${cardCode(current.remoteSessionId ?? "尚未创建")}`, statusTaskMetricsLine(metrics?.turnCount ?? this.store.countTaskTurns(current.localSessionId), metrics?.storageBytes), `**当前执行 / 排队消息**:${activeTurnId ? cardCode(activeTurnId) : "无"} / ${queued} 条`, `**创建时间 / 最近活动**:${formatStatusTime(current.createdAt)} / ${formatStatusTime(current.updatedAt)}`);
5377
5596
  }
5378
5597
  const sections = [
5379
5598
  { title: targetSessionId ? "指定任务" : "当前任务", lines: taskLines },
@@ -5430,7 +5649,17 @@ export class ProxySessionController {
5430
5649
  await this.outbound.sendInteractiveCard(contextKey, card);
5431
5650
  }
5432
5651
  async statusForCodexTask(contextKey, reference, options = {}) {
5433
- const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
5652
+ const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
5653
+ const runtime = this.runtimes.forAgent(agentName);
5654
+ let metrics;
5655
+ if (runtime.readRemoteSessionMetrics) {
5656
+ try {
5657
+ metrics = await runtime.readRemoteSessionMetrics(remote.id);
5658
+ }
5659
+ catch (error) {
5660
+ this.logger.warn({ error, remoteSessionId: remote.id }, "Failed to inspect App Server task metrics.");
5661
+ }
5662
+ }
5434
5663
  const actionReference = remoteSessionReference(agentName, remote.id);
5435
5664
  const sections = [
5436
5665
  {
@@ -5441,6 +5670,7 @@ export class ProxySessionController {
5441
5670
  `**状态 / 当前任务**:${remoteSessionDetailStatus(remote)} / 未切换`,
5442
5671
  `**Agent**:${cardCode(agentName)}`,
5443
5672
  `**App Server 任务 ID**:${cardCode(remote.id)}`,
5673
+ statusTaskMetricsLine(metrics?.turnCount, metrics?.storageBytes),
5444
5674
  `**最近回合**:${cardCode(remote.lastTurnId ?? "无")} ${remoteTurnStatusLabel(remote.lastTurnStatus)}`,
5445
5675
  `**创建时间 / 最近活动**:${formatRemoteTime(remote.createdAt)} / ${formatRemoteTime(latestRemoteTimestamp(remote.recencyAt, remote.updatedAt))}`,
5446
5676
  ],
@@ -5561,10 +5791,11 @@ export class ProxySessionController {
5561
5791
  throw new Error(`未知 agent:${agentName}`);
5562
5792
  return agent;
5563
5793
  }
5564
- async persistAgentExecutionDefaults(localSessionId) {
5565
- const session = this.store.getSession(localSessionId);
5566
- if (!session)
5794
+ async persistAgentExecutionDefaults(localSessionId, candidate) {
5795
+ const record = this.store.getSession(localSessionId);
5796
+ if (!record)
5567
5797
  throw new Error(`找不到任务:${localSessionId}`);
5798
+ const session = candidate ?? record;
5568
5799
  const agent = this.ensureAgent(session.agentName);
5569
5800
  const observed = {
5570
5801
  ...(session.modelProvider?.trim() ? { modelProvider: session.modelProvider } : {}),
@@ -5934,6 +6165,7 @@ export class ProxySessionController {
5934
6165
  const lockPath = runtime.getThreadWriterLockPath?.(threadId);
5935
6166
  if (!lockPath)
5936
6167
  throw new Error("当前 Agent 不支持自动关闭占用程序。");
6168
+ await this.outbound.sendText(contextKey, CLOSE_WRITER_STARTED_MESSAGE);
5937
6169
  let owners = await this.threadWriterProcesses.inspect(lockPath);
5938
6170
  const expectedOwner = owners.length === 1 && threadWriterMatches(owners[0], expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt) ? owners[0] : undefined;
5939
6171
  if (!expectedOwner) {
@@ -6259,10 +6491,6 @@ function bindSessionCardAction(action, bindings) {
6259
6491
  function runtimeErrorMessage(error) {
6260
6492
  return error instanceof Error ? error.message : String(error);
6261
6493
  }
6262
- function isMissingRolloutError(error) {
6263
- const message = error instanceof Error ? error.message : String(error);
6264
- return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
6265
- }
6266
6494
  function isUnmaterializedCodexThreadError(error) {
6267
6495
  const message = error instanceof Error ? error.message : String(error);
6268
6496
  return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
@@ -6436,6 +6664,11 @@ function statusExcerpt(value, maxLength) {
6436
6664
  function formatDuration(milliseconds) {
6437
6665
  return milliseconds < 1_000 ? `${milliseconds}ms` : `${(milliseconds / 1_000).toFixed(1)}s`;
6438
6666
  }
6667
+ function statusTaskMetricsLine(turnCount, storageBytes) {
6668
+ const turns = turnCount === undefined ? "未知" : `${Math.max(0, Math.round(turnCount)).toLocaleString("en-US")} 轮`;
6669
+ const storage = storageBytes === undefined ? "未知" : formatStorageSize(storageBytes);
6670
+ return `**已执行轮次 / 磁盘占用**:${turns} / ${storage}`;
6671
+ }
6439
6672
  function formatStatusTime(value) {
6440
6673
  const date = new Date(value);
6441
6674
  if (Number.isNaN(date.getTime()))
@@ -6870,6 +7103,21 @@ function deduplicateFileReferences(files) {
6870
7103
  return true;
6871
7104
  });
6872
7105
  }
7106
+ function selectProviderModel(models, previousModel) {
7107
+ return models.find((model) => model.id === previousModel)
7108
+ ?? models.find((model) => model.isDefault)
7109
+ ?? models[0];
7110
+ }
7111
+ function selectProviderReasoningEffort(model, previousEffort) {
7112
+ if (previousEffort && (model.supportedReasoningEfforts.length === 0
7113
+ || model.supportedReasoningEfforts.some((option) => option.value === previousEffort))) {
7114
+ return previousEffort;
7115
+ }
7116
+ return model.defaultReasoningEffort
7117
+ ?? model.supportedReasoningEfforts[0]?.value
7118
+ ?? previousEffort
7119
+ ?? "medium";
7120
+ }
6873
7121
  function recoveryCardPrompt(prompt) {
6874
7122
  return `恢复重启前的任务:${prompt}`;
6875
7123
  }