@keyou007/agent-bot 0.1.20-alpha.0 → 0.1.20

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 (66) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +18 -1
  3. package/README.zh.md +9 -0
  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/CodexLocalActivityDetector.js +32 -10
  11. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  12. package/dist/codex/CodexProcessManager.js +53 -7
  13. package/dist/codex/CodexProcessManager.js.map +1 -1
  14. package/dist/codex/CodexRuntime.js +225 -164
  15. package/dist/codex/CodexRuntime.js.map +1 -1
  16. package/dist/codex/CodexVersion.js +18 -0
  17. package/dist/codex/CodexVersion.js.map +1 -0
  18. package/dist/codex/ProviderSwitch.js +32 -0
  19. package/dist/codex/ProviderSwitch.js.map +1 -0
  20. package/dist/config/AgentExecutionDefaultsStore.js +1 -1
  21. package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
  22. package/dist/feishu/CardRenderer.js +36 -11
  23. package/dist/feishu/CardRenderer.js.map +1 -1
  24. package/dist/feishu/FeishuConnector.js +17 -5
  25. package/dist/feishu/FeishuConnector.js.map +1 -1
  26. package/dist/feishu/InboundText.js +11 -0
  27. package/dist/feishu/InboundText.js.map +1 -1
  28. package/dist/feishu/MergedForwardMessage.js +17 -5
  29. package/dist/feishu/MergedForwardMessage.js.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/local-files/LocalFileViewerServer.js +25 -7
  33. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  34. package/dist/local-files/MarkdownFileLinks.js +65 -0
  35. package/dist/local-files/MarkdownFileLinks.js.map +1 -0
  36. package/dist/proxy/ProxySessionController.js +400 -259
  37. package/dist/proxy/ProxySessionController.js.map +1 -1
  38. package/dist/state/StateStore.js +99 -65
  39. package/dist/state/StateStore.js.map +1 -1
  40. package/dist/utils/semanticVersion.js +39 -0
  41. package/dist/utils/semanticVersion.js.map +1 -0
  42. package/docs/technical-reference.md +5 -3
  43. package/docs/technical-reference.zh.md +5 -3
  44. package/npm-shrinkwrap.json +2 -2
  45. package/package.json +1 -1
  46. package/skills/agent-bot/SKILL.md +7 -1
  47. package/src/cli/AgentPrerequisites.ts +14 -40
  48. package/src/cli/DefaultAgentSelection.ts +1 -1
  49. package/src/cli.ts +13 -0
  50. package/src/codex/CodexLocalActivityDetector.ts +41 -9
  51. package/src/codex/CodexProcessManager.ts +44 -7
  52. package/src/codex/CodexRuntime.ts +244 -195
  53. package/src/codex/CodexVersion.ts +18 -0
  54. package/src/codex/ProviderSwitch.ts +36 -0
  55. package/src/config/AgentExecutionDefaultsStore.ts +1 -1
  56. package/src/feishu/CardRenderer.ts +37 -27
  57. package/src/feishu/FeishuConnector.ts +15 -5
  58. package/src/feishu/InboundText.ts +11 -0
  59. package/src/feishu/MergedForwardMessage.ts +15 -5
  60. package/src/index.ts +1 -1
  61. package/src/local-files/LocalFileViewerServer.ts +26 -7
  62. package/src/local-files/MarkdownFileLinks.ts +64 -0
  63. package/src/proxy/ProxySessionController.ts +370 -240
  64. package/src/runtime/types.ts +15 -2
  65. package/src/state/StateStore.ts +117 -90
  66. 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";
@@ -50,6 +52,22 @@ const FORWARD_ATTACHMENT_WINDOW_MS = 800;
50
52
  const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
51
53
  const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
52
54
  const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
55
+ const TURN_RESET_STARTED_MESSAGE = "Reset 正在进行,请稍后。期间发送的消息会自动排队。";
56
+ const CREATE_TASK_STARTED_MESSAGE = "正在创建任务,请稍后。";
57
+ const CREATE_GROUP_STARTED_MESSAGE = "正在创建群聊和任务,请稍后。";
58
+ const FORK_TASK_STARTED_MESSAGE = "正在创建分支任务,请稍后。";
59
+ const FORK_GROUP_STARTED_MESSAGE = "正在创建分支群聊,请稍后。";
60
+ const LIST_SESSIONS_STARTED_MESSAGE = "正在读取任务列表,请稍后。";
61
+ const LIST_TURNS_STARTED_MESSAGE = "正在读取历史轮次,请稍后。";
62
+ const SWITCH_TASK_STARTED_MESSAGE = "正在切换任务,请稍后。";
63
+ const STATUS_STARTED_MESSAGE = "正在读取任务状态,请稍后。";
64
+ const GOAL_STARTED_MESSAGE = "正在处理 Goal,请稍后。";
65
+ const ARCHIVE_TASK_STARTED_MESSAGE = "正在归档任务,请稍后。";
66
+ const DISMISS_GROUP_STARTED_MESSAGE = "正在归档任务并解散群聊,请稍后。";
67
+ const SEND_FILE_STARTED_MESSAGE = "正在发送文件,请稍后。";
68
+ const CHANGE_TITLE_STARTED_MESSAGE = "正在修改任务标题,请稍后。";
69
+ const STOP_TASK_STARTED_MESSAGE = "正在停止任务,请稍后。";
70
+ const CLOSE_WRITER_STARTED_MESSAGE = "正在检查并关闭占用进程,请稍后。";
53
71
  const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
54
72
  const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
55
73
  const IMAGE_FILE_EXTENSIONS = new Set([
@@ -205,6 +223,8 @@ export class ProxySessionController {
205
223
  sessionResets = new Map();
206
224
  resetHistoryOperations = new Map();
207
225
  remoteTurnHistoryRefreshes = new Map();
226
+ turnHistoryHydrations = new Map();
227
+ forkHistoryCursors = new Map();
208
228
  lastSessionListings = new Map();
209
229
  threadInitializations = new Map();
210
230
  llmRetryingSessions = new Set();
@@ -282,6 +302,7 @@ export class ProxySessionController {
282
302
  catch (error) {
283
303
  this.logger.warn({ error, messageId: message.messageId, contextKey: message.contextKey }, "Failed to acknowledge the incoming Feishu message with a reaction.");
284
304
  }
305
+ await this.waitForCurrentSessionReset(message.contextKey);
285
306
  if (attachmentReservation && this.completeForwardAttachment(message, attachmentReservation)) {
286
307
  this.store.audit(message.contextKey, `${attachmentReservation.sourceKind}_attachment_coalesced`, {
287
308
  messageId: message.messageId,
@@ -478,10 +499,11 @@ export class ProxySessionController {
478
499
  command: command.type,
479
500
  }, "Processing Agent Bot command.");
480
501
  }
481
- // Operational and read-only commands must remain available even if a prompt operation is slow.
502
+ // Operational and read-only commands bypass the prompt queue, but still wait for a Reset binding change.
482
503
  if (isQueueIndependentCommand(command)) {
483
504
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
484
505
  try {
506
+ await this.waitForCurrentSessionReset(message.contextKey);
485
507
  if (command.type === "prompt")
486
508
  await this.ensureThreadFork(message);
487
509
  await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
@@ -502,45 +524,48 @@ export class ProxySessionController {
502
524
  return;
503
525
  }
504
526
  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
- });
527
+ const next = previous.catch(() => undefined).then(async () => {
528
+ await this.waitForCurrentSessionReset(message.contextKey);
529
+ return this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
530
+ try {
531
+ let executableCommand = command;
532
+ let executableImagePaths = localImagePaths;
533
+ let executableMessage = message;
534
+ let injectedRootMessageId;
535
+ if (command.type === "prompt") {
536
+ await this.ensureThreadFork(message);
537
+ const prepared = await this.prepareTopicRootPrompt(message, command.text, localImagePaths);
538
+ executableCommand = { ...command, text: prepared.text };
539
+ executableImagePaths = prepared.localImagePaths;
540
+ executableMessage = { ...message, displayText: prepared.displayPrompt };
541
+ injectedRootMessageId = prepared.injectedRootMessageId;
542
+ }
543
+ await this.execute(message.contextKey, executableCommand, message.messageId, replyTarget, executableImagePaths, message.userId, executableMessage);
544
+ if (injectedRootMessageId) {
545
+ const current = this.currentSession(message.contextKey);
546
+ if (current) {
547
+ this.store.audit(message.contextKey, "topic_root_injected", {
548
+ localSessionId: current.localSessionId,
549
+ rootMessageId: injectedRootMessageId,
550
+ promptMessageId: message.messageId,
551
+ });
552
+ }
553
+ }
554
+ if (!commandDefersReactionFinalization(command)) {
555
+ await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
528
556
  }
529
557
  }
530
- if (!commandDefersReactionFinalization(command)) {
531
- await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
558
+ catch (error) {
559
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
560
+ await this.sendError(message.contextKey, error, {
561
+ requestId: message.messageId,
562
+ messageId: message.messageId,
563
+ command: command.type,
564
+ phase: "command_execute",
565
+ });
532
566
  }
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
- }));
567
+ });
568
+ });
544
569
  this.messageQueues.set(message.contextKey, next);
545
570
  await next;
546
571
  if (this.messageQueues.get(message.contextKey) === next)
@@ -653,6 +678,7 @@ export class ProxySessionController {
653
678
  else if (kind === "directory_send_file") {
654
679
  const filePath = directoryFileActionPath(scopedAction.value.filePath);
655
680
  await this.assertSendableFile(filePath);
681
+ await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
656
682
  await this.outbound.sendFile(contextKey, filePath);
657
683
  }
658
684
  else if (kind === "directory_new_folder_prompt") {
@@ -704,11 +730,13 @@ export class ProxySessionController {
704
730
  }
705
731
  else if (kind === "session_new") {
706
732
  const sessionId = String(scopedAction.value.sessionId ?? "");
733
+ await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
707
734
  await this.createProjectSessionFromReference(contextKey, sessionId);
708
735
  await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
709
736
  }
710
737
  else if (kind === "session_new_group") {
711
738
  const sessionId = String(scopedAction.value.sessionId ?? "");
739
+ await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
712
740
  await this.createFeishuGroupFromReference(contextKey, sessionId, scopedAction.userId);
713
741
  await this.refreshSessionsCardFromAction(scopedAction);
714
742
  }
@@ -726,10 +754,12 @@ export class ProxySessionController {
726
754
  }
727
755
  else if (kind === "group_dismiss_confirm") {
728
756
  this.assertDismissGroupRequester(scopedAction);
757
+ await this.outbound.sendText(contextKey, DISMISS_GROUP_STARTED_MESSAGE);
729
758
  await this.dismissGroupAndArchiveTask(contextKey, String(scopedAction.value.sessionId ?? ""), "command");
730
759
  }
731
760
  else if (kind === "session_stop") {
732
761
  const sessionId = String(scopedAction.value.sessionId ?? "");
762
+ await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
733
763
  await this.stopSessionReference(contextKey, sessionId);
734
764
  if (scopedAction.value.cardView === "status")
735
765
  await this.refreshStatusCardFromAction(scopedAction, sessionId);
@@ -737,6 +767,7 @@ export class ProxySessionController {
737
767
  await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
738
768
  }
739
769
  else if (kind === "session_status") {
770
+ await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
740
771
  await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
741
772
  }
742
773
  else if (kind === "session_status_refresh") {
@@ -867,6 +898,8 @@ export class ProxySessionController {
867
898
  this.sessionResets.clear();
868
899
  this.resetHistoryOperations.clear();
869
900
  this.remoteTurnHistoryRefreshes.clear();
901
+ this.forkHistoryCursors.clear();
902
+ this.turnHistoryHydrations.clear();
870
903
  for (const pending of this.pendingMergedForwards.values())
871
904
  pending.resolveAttachment(undefined);
872
905
  this.pendingMergedForwards.clear();
@@ -930,9 +963,11 @@ export class ProxySessionController {
930
963
  const runtime = this.runtimes.forAgent(record.agentName);
931
964
  if (runtime.readRemoteSession) {
932
965
  try {
933
- remote = await runtime.readRemoteSession(record.remoteSessionId);
966
+ remote = await runtime.readRemoteSession(record.remoteSessionId, "latest-full");
934
967
  }
935
968
  catch (error) {
969
+ if (error instanceof CodexVersionError)
970
+ throw error;
936
971
  this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to inspect App Server task status for CLI.");
937
972
  }
938
973
  }
@@ -1155,10 +1190,14 @@ export class ProxySessionController {
1155
1190
  }
1156
1191
  async controlTaskSettings(localSessionId, setting, value) {
1157
1192
  const record = this.requireControlSession(localSessionId);
1158
- const loaded = await this.loadSession(record);
1159
1193
  if (setting && !value?.trim())
1160
1194
  throw new Error(`task ${setting} requires a value.`);
1161
1195
  const nextValue = value?.trim();
1196
+ if (setting === "provider" && nextValue) {
1197
+ const contextKey = this.controlSessionContextKey(record);
1198
+ await this.outbound.withReplyTarget(contextKey, this.controlSessionReplyTarget(record), () => this.outbound.sendText(contextKey, `正在切换到 Provider ${nextValue},请稍后。`));
1199
+ }
1200
+ const loaded = await this.loadSession(record);
1162
1201
  if (setting === "provider" && nextValue) {
1163
1202
  await this.assertModelProvider(loaded, nextValue);
1164
1203
  const models = await loaded.runtime.listModels();
@@ -1171,20 +1210,12 @@ export class ProxySessionController {
1171
1210
  : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
1172
1211
  if (!effort)
1173
1212
  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, {
1213
+ await this.changeProviderSettings(loaded, {
1177
1214
  modelProvider: nextValue,
1178
1215
  model: model.id,
1179
1216
  reasoningEffort: effort,
1180
1217
  permissionMode: loaded.session.permissionMode,
1181
1218
  });
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
1219
  }
1189
1220
  else if (setting === "model" && nextValue) {
1190
1221
  const models = await loaded.runtime.listModels();
@@ -1222,13 +1253,13 @@ export class ProxySessionController {
1222
1253
  this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
1223
1254
  }
1224
1255
  const session = this.store.getSession(record.localSessionId) ?? record;
1225
- if (setting)
1256
+ if (setting && setting !== "provider")
1226
1257
  await this.persistAgentExecutionDefaults(session.localSessionId);
1227
1258
  const models = await loaded.runtime.listModels();
1228
1259
  const currentModel = models.find((candidate) => candidate.id === session.model)
1229
1260
  ?? models.find((candidate) => candidate.isDefault);
1230
1261
  const providers = loaded.runtime.listModelProviders
1231
- ? await loaded.runtime.listModelProviders()
1262
+ ? await this.modelProviderOptions(loaded)
1232
1263
  : [];
1233
1264
  return {
1234
1265
  session,
@@ -1271,17 +1302,18 @@ export class ProxySessionController {
1271
1302
  }),
1272
1303
  };
1273
1304
  }
1274
- controlListTaskTurns(localSessionId) {
1305
+ async controlListTaskTurns(localSessionId) {
1275
1306
  const session = this.requireControlSession(localSessionId);
1276
1307
  if (!session.remoteSessionId || !this.isCodexSession(session)) {
1277
1308
  throw new Error("The task is not an App Server task that can be reset.");
1278
1309
  }
1279
- const rows = this.store.listTaskTurnGraph(session.localSessionId);
1310
+ await this.hydrateTaskTurnHistory(session);
1311
+ const rows = this.store.listTaskTurnGraphIndex(session.localSessionId);
1280
1312
  const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
1281
1313
  return {
1282
1314
  session,
1283
1315
  turns: rows.flatMap((turn, index) => {
1284
- const snapshot = turnViewSnapshot(turn.snapshot);
1316
+ const snapshot = this.store.getTurnPromptSummary(turn.turnId);
1285
1317
  if (!snapshot)
1286
1318
  return [];
1287
1319
  return [{
@@ -1299,16 +1331,10 @@ export class ProxySessionController {
1299
1331
  async controlResetTaskToTurn(localSessionId, turnId) {
1300
1332
  const record = this.requireControlSession(localSessionId);
1301
1333
  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
- }
1334
+ await this.runSessionReset(record.localSessionId, async () => {
1335
+ await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
1336
+ await this.performSessionReset(contextKey, record, turnId, false);
1337
+ });
1312
1338
  return this.store.getSession(record.localSessionId) ?? record;
1313
1339
  }
1314
1340
  controlTaskMute(localSessionId, enabled) {
@@ -1438,6 +1464,7 @@ export class ProxySessionController {
1438
1464
  if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
1439
1465
  throw new Error("/new --nodir 仅支持 App Server Agent。");
1440
1466
  }
1467
+ await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
1441
1468
  await this.createSession(contextKey, context.defaultAgent, command.projectless
1442
1469
  ? undefined
1443
1470
  : command.cwd === undefined
@@ -1448,6 +1475,10 @@ export class ProxySessionController {
1448
1475
  if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
1449
1476
  throw new Error("/newgroup --nodir 仅支持 App Server Agent。");
1450
1477
  }
1478
+ if (!userId?.startsWith("ou_")) {
1479
+ throw new Error("/newgroup 只能由具有 open_id 的飞书用户消息触发。");
1480
+ }
1481
+ await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
1451
1482
  await this.createFeishuGroup(contextKey, context.defaultAgent, command.title, userId, command.cwd === undefined ? undefined : resolveUserPath(command.cwd), command.projectless === true);
1452
1483
  return;
1453
1484
  case "forkgroup":
@@ -1466,6 +1497,7 @@ export class ProxySessionController {
1466
1497
  await this.enqueueNoSteerPrompt(contextKey, command.text, messageId, replyTarget);
1467
1498
  return;
1468
1499
  case "sessions":
1500
+ await this.outbound.sendText(contextKey, LIST_SESSIONS_STARTED_MESSAGE);
1469
1501
  await this.listSessions(contextKey, command.searchTerm);
1470
1502
  return;
1471
1503
  case "switch":
@@ -1491,9 +1523,11 @@ export class ProxySessionController {
1491
1523
  await this.openDismissGroupCard(contextKey, userId);
1492
1524
  return;
1493
1525
  case "status":
1526
+ await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
1494
1527
  await this.status(contextKey, command.sessionId);
1495
1528
  return;
1496
1529
  case "goal":
1530
+ await this.outbound.sendText(contextKey, GOAL_STARTED_MESSAGE);
1497
1531
  await this.goal(contextKey, command);
1498
1532
  return;
1499
1533
  case "restart":
@@ -1508,6 +1542,7 @@ export class ProxySessionController {
1508
1542
  await this.setGroupMute(contextKey, command.enabled);
1509
1543
  return;
1510
1544
  case "turns":
1545
+ await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
1511
1546
  await this.openResetHistory(contextKey);
1512
1547
  return;
1513
1548
  case "model":
@@ -1747,6 +1782,8 @@ export class ProxySessionController {
1747
1782
  remote = await runtime.readRemoteSession(session.remoteSessionId);
1748
1783
  }
1749
1784
  catch (error) {
1785
+ if (error instanceof CodexVersionError)
1786
+ throw error;
1750
1787
  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
1788
  }
1752
1789
  }
@@ -2191,8 +2228,7 @@ export class ProxySessionController {
2191
2228
  return false;
2192
2229
  if (record.lastTurnId === anchor.turnId)
2193
2230
  return true;
2194
- return this.store.listTaskTurnGraph(record.localSessionId)
2195
- .some((turn) => turn.turnId === anchor.turnId);
2231
+ return this.store.hasTaskHistoryTurn(record.localSessionId, anchor.turnId);
2196
2232
  }
2197
2233
  async forkThreadSession(message) {
2198
2234
  const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
@@ -2285,6 +2321,7 @@ export class ProxySessionController {
2285
2321
  return { anchor, source, snapshot };
2286
2322
  }
2287
2323
  async forkSessionReference(contextKey, reference) {
2324
+ await this.outbound.sendText(contextKey, FORK_TASK_STARTED_MESSAGE);
2288
2325
  const plan = await this.prepareForkSession(contextKey, reference);
2289
2326
  const forked = await this.forkSessionIntoContext(contextKey, plan);
2290
2327
  const forkSourceLabel = plan.forkedFromHistoricalTurn
@@ -2336,32 +2373,22 @@ export class ProxySessionController {
2336
2373
  if (!runtime.readRemoteSession && !source) {
2337
2374
  throw new Error("当前 App Server Agent 不支持读取指定任务。");
2338
2375
  }
2339
- const persistedCompletedTurns = source ? this.localForkCompletedTurns(source) : [];
2376
+ const persistedCompletedTurnId = source
2377
+ ? this.store.findLatestCompletedTurnId(source.localSessionId)
2378
+ : undefined;
2340
2379
  const sourceLastSnapshot = turnViewSnapshot(source?.lastTurnId ? this.store.getTurnSnapshot(source.lastTurnId) : undefined);
2341
- const localCompletedTurns = persistedCompletedTurns.length > 0
2342
- ? persistedCompletedTurns
2343
- : source?.lastTurnId
2380
+ const localCompletedTurnId = persistedCompletedTurnId
2381
+ ?? (source?.lastTurnId
2344
2382
  && (source.lastTurnStatus === "completed" || sourceLastSnapshot?.status === "completed")
2345
- ? [{
2346
- id: source.lastTurnId,
2347
- prompt: latestUserPromptFromTurnView(sourceLastSnapshot) ?? source.title,
2348
- startedAt: sourceLastSnapshot?.startedAt ?? parseIsoTimestamp(source.updatedAt),
2349
- completedAt: sourceLastSnapshot?.completedAt
2350
- ?? sourceLastSnapshot?.startedAt
2351
- ?? parseIsoTimestamp(source.updatedAt),
2352
- }]
2353
- : [];
2354
- if (!remote && runtime.readRemoteSession && (!source || persistedCompletedTurns.length === 0)) {
2355
- try {
2356
- remote = await runtime.readRemoteSession(remoteSessionId);
2357
- }
2358
- catch (error) {
2359
- if (localCompletedTurns.length === 0)
2360
- throw error;
2361
- 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.");
2362
- }
2383
+ ? source.lastTurnId
2384
+ : undefined)
2385
+ ?? (source ? this.store.getForkHistorySource(source.localSessionId)?.sourceTurnId : undefined);
2386
+ if (!remote && !localCompletedTurnId) {
2387
+ remote = runtime.readRemoteForkSource
2388
+ ? await runtime.readRemoteForkSource(remoteSessionId)
2389
+ : await runtime.readRemoteSession?.(remoteSessionId);
2363
2390
  }
2364
- const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurns.at(-1)?.id;
2391
+ const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurnId;
2365
2392
  const latestSnapshot = turnViewSnapshot(latestTurnId ? this.store.getTurnSnapshot(latestTurnId) : undefined);
2366
2393
  const isRunning = remote
2367
2394
  ? isRemoteSessionActive(remote)
@@ -2370,7 +2397,7 @@ export class ProxySessionController {
2370
2397
  || isTurnStillRunning(latestSnapshot?.status));
2371
2398
  const lastTurnId = remote?.lastCompletedTurnId
2372
2399
  ?? (remote?.lastTurnStatus === "completed" ? remote.lastTurnId : undefined)
2373
- ?? localCompletedTurns.at(-1)?.id;
2400
+ ?? localCompletedTurnId;
2374
2401
  if (!lastTurnId) {
2375
2402
  if (isRunning) {
2376
2403
  throw new Error(`${sourceLabel}正在执行,且还没有已完成轮次可供 fork。请等待当前轮次完成后重试。`);
@@ -2379,8 +2406,7 @@ export class ProxySessionController {
2379
2406
  }
2380
2407
  const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
2381
2408
  const forkedFromHistoricalTurn = lastTurnId !== latestTurnId;
2382
- const completedTurn = (remote?.completedTurns ?? localCompletedTurns)
2383
- .find((turn) => turn.id === lastTurnId);
2409
+ const completedTurn = remote?.completedTurns?.find((turn) => turn.id === lastTurnId);
2384
2410
  const cwd = remote?.cwd || source?.cwd;
2385
2411
  if (!cwd)
2386
2412
  throw new Error("指定的 App Server 任务没有可用的工作目录,暂时不能 fork。");
@@ -2422,27 +2448,10 @@ export class ProxySessionController {
2422
2448
  sourceTurnPrompt,
2423
2449
  sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
2424
2450
  sourceTurnCompletedAt,
2425
- sourceCompletedTurns: remote?.completedTurns
2426
- ?? (persistedCompletedTurns.length > 0 ? [] : localCompletedTurns),
2427
2451
  sourceWasRunning: isRunning,
2428
2452
  forkedFromHistoricalTurn,
2429
2453
  };
2430
2454
  }
2431
- localForkCompletedTurns(source) {
2432
- return this.store.listTaskTurnGraph(source.localSessionId)
2433
- .slice()
2434
- .reverse()
2435
- .map((record) => {
2436
- const snapshot = turnViewSnapshot(record.snapshot);
2437
- const startedAt = snapshot?.startedAt ?? parseIsoTimestamp(record.updatedAt);
2438
- return {
2439
- id: record.turnId,
2440
- prompt: latestUserPromptFromTurnView(snapshot),
2441
- startedAt,
2442
- completedAt: snapshot?.completedAt ?? startedAt,
2443
- };
2444
- });
2445
- }
2446
2455
  async prepareForkGroupSession(contextKey, requestedTitle, incomingMessage) {
2447
2456
  if (!incomingMessage?.threadContext) {
2448
2457
  const plan = await this.prepareForkSession(contextKey, undefined, requestedTitle);
@@ -2512,7 +2521,6 @@ export class ProxySessionController {
2512
2521
  };
2513
2522
  }
2514
2523
  async forkSessionIntoContext(contextKey, plan) {
2515
- const sourceCompletedTurns = await this.resolveForkSourceCompletedTurns(plan);
2516
2524
  const localSessionId = createId("sess");
2517
2525
  const record = this.store.createSession({
2518
2526
  localSessionId,
@@ -2548,7 +2556,6 @@ export class ProxySessionController {
2548
2556
  lastTurnStatus: plan.lastTurnStatus,
2549
2557
  });
2550
2558
  this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName));
2551
- const sourceTurnHistoryCount = this.persistForkSourceHistory(contextKey, localSessionId, plan, sourceCompletedTurns);
2552
2559
  this.store.audit(contextKey, "session_forked", {
2553
2560
  sourceLocalSessionId: plan.source?.localSessionId,
2554
2561
  sourceRemoteSessionId: plan.remoteSessionId,
@@ -2556,7 +2563,6 @@ export class ProxySessionController {
2556
2563
  sourceTurnPrompt: plan.sourceTurnPrompt,
2557
2564
  sourceTurnStartedAt: plan.sourceTurnStartedAt,
2558
2565
  sourceTurnCompletedAt: plan.sourceTurnCompletedAt,
2559
- sourceTurnHistoryCount,
2560
2566
  sourceWasRunning: plan.sourceWasRunning,
2561
2567
  forkedLocalSessionId: localSessionId,
2562
2568
  forkedRemoteSessionId: forked.remoteSessionId,
@@ -2572,42 +2578,6 @@ export class ProxySessionController {
2572
2578
  throw error;
2573
2579
  }
2574
2580
  }
2575
- async resolveForkSourceCompletedTurns(plan) {
2576
- let completedTurns = plan.sourceCompletedTurns;
2577
- if (completedTurns === undefined && plan.runtime.readRemoteSession) {
2578
- try {
2579
- completedTurns = (await plan.runtime.readRemoteSession(plan.remoteSessionId)).completedTurns ?? [];
2580
- }
2581
- catch (error) {
2582
- 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.");
2583
- }
2584
- }
2585
- if (!completedTurns?.length)
2586
- return [];
2587
- const anchorIndex = completedTurns.findIndex((turn) => turn.id === plan.lastTurnId);
2588
- if (anchorIndex < 0) {
2589
- 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.");
2590
- return [];
2591
- }
2592
- return completedTurns.slice(0, anchorIndex + 1);
2593
- }
2594
- persistForkSourceHistory(contextKey, forkedLocalSessionId, plan, completedTurns) {
2595
- if (completedTurns.length === 0)
2596
- return 0;
2597
- const localSessionId = plan.source?.localSessionId ?? forkedLocalSessionId;
2598
- const historyContextKey = plan.source?.contextKey ?? contextKey;
2599
- const fallbackStart = plan.sourceTurnStartedAt
2600
- ?? plan.sourceTurnCompletedAt
2601
- ?? Date.now() - completedTurns.length;
2602
- return this.persistCompletedTurnHistory({
2603
- localSessionId,
2604
- contextKey: historyContextKey,
2605
- agentName: plan.agentName,
2606
- remoteSessionId: plan.remoteSessionId,
2607
- completedTurns,
2608
- fallbackStart,
2609
- });
2610
- }
2611
2581
  persistCompletedTurnHistory(input) {
2612
2582
  try {
2613
2583
  this.store.importCompletedTurnHistory({
@@ -2615,6 +2585,7 @@ export class ProxySessionController {
2615
2585
  contextKey: input.contextKey,
2616
2586
  agentName: input.agentName,
2617
2587
  remoteSessionId: input.remoteSessionId,
2588
+ replaceParents: input.replaceParents,
2618
2589
  turns: input.completedTurns.map((turn, index) => {
2619
2590
  const startedAt = turn.startedAt ?? input.fallbackStart + index;
2620
2591
  const completedAt = turn.completedAt ?? startedAt;
@@ -2645,80 +2616,198 @@ export class ProxySessionController {
2645
2616
  return 0;
2646
2617
  }
2647
2618
  }
2648
- async hydrateLegacyForkTurnHistory(contextKey, current) {
2649
- const source = this.store.getForkHistorySource(current.localSessionId);
2650
- if (!source || source.sourceTurnHistoryCount !== undefined
2651
- || this.store.hasImportedForkTurnHistory(current.localSessionId))
2652
- return;
2619
+ async hydrateTaskTurnHistory(current, requiredCount = RESET_HISTORY_PAGE_SIZE) {
2620
+ const existing = this.turnHistoryHydrations.get(current.localSessionId);
2621
+ if (existing)
2622
+ await existing;
2623
+ const hydration = (async () => {
2624
+ if (this.store.getForkHistorySource(current.localSessionId)) {
2625
+ return this.hydrateForkTurnHistory(current.contextKey, current, requiredCount);
2626
+ }
2627
+ return this.hydrateRemoteTurnHistory(current.contextKey, current, requiredCount);
2628
+ })();
2629
+ this.turnHistoryHydrations.set(current.localSessionId, hydration);
2630
+ try {
2631
+ return await hydration;
2632
+ }
2633
+ finally {
2634
+ if (this.turnHistoryHydrations.get(current.localSessionId) === hydration) {
2635
+ this.turnHistoryHydrations.delete(current.localSessionId);
2636
+ }
2637
+ }
2638
+ }
2639
+ async hydrateForkTurnHistory(contextKey, current, requiredCount, visited = new Set()) {
2640
+ if (visited.has(current.localSessionId))
2641
+ return false;
2642
+ visited.add(current.localSessionId);
2643
+ let source = this.store.getForkHistorySource(current.localSessionId);
2644
+ if (!source)
2645
+ return false;
2646
+ const completedHead = current.lastTurnStatus === "completed"
2647
+ ? current.lastTurnId
2648
+ : this.store.findLatestCompletedTurnId(current.localSessionId);
2649
+ if (completedHead && completedHead !== source.sourceTurnId && current.remoteSessionId) {
2650
+ // Once the branch has its own completed work, its remote history is the
2651
+ // authority for missing local turns, including work continued in Desktop.
2652
+ source = {
2653
+ sourceLocalSessionId: current.localSessionId,
2654
+ sourceRemoteSessionId: current.remoteSessionId,
2655
+ sourceTurnId: completedHead,
2656
+ createdAt: current.createdAt,
2657
+ };
2658
+ }
2659
+ if (this.store.hasImportedForkTurnHistory(current.localSessionId, source.sourceRemoteSessionId, source.sourceTurnId))
2660
+ return false;
2661
+ const localTurns = this.store.listTaskTurnGraphIndex(current.localSessionId);
2662
+ const availableCount = localTurns.length;
2663
+ const key = JSON.stringify([current.agentName, source.sourceRemoteSessionId, source.sourceTurnId]);
2664
+ let state = this.forkHistoryCursors.get(key) ?? { anchorFound: false, complete: false };
2665
+ const localAnchorIndex = localTurns.findIndex((turn) => turn.turnId === source.sourceTurnId);
2666
+ const ownTurnCount = source.sourceLocalSessionId === current.localSessionId
2667
+ ? Math.max(0, localAnchorIndex)
2668
+ : localAnchorIndex >= 0 ? localAnchorIndex : availableCount;
2669
+ const neededInheritedCount = Math.max(0, requiredCount - ownTurnCount);
2670
+ if (localAnchorIndex >= 0 && availableCount >= requiredCount
2671
+ && (!state.anchorFound || (state.loadedCount ?? 0) >= neededInheritedCount))
2672
+ return !state.complete;
2673
+ const sourceSession = source.sourceLocalSessionId
2674
+ ? this.store.getSession(source.sourceLocalSessionId)
2675
+ : undefined;
2676
+ // A branch with no new turns can reuse its ancestor's history, even before
2677
+ // the Agent materializes that intermediate branch.
2678
+ if (sourceSession && sourceSession.localSessionId !== current.localSessionId
2679
+ && this.store.getForkHistorySource(sourceSession.localSessionId)?.sourceTurnId === source.sourceTurnId) {
2680
+ const hasMore = await this.hydrateForkTurnHistory(sourceSession.contextKey, sourceSession, this.store.listTaskTurnGraphIndex(sourceSession.localSessionId).length + requiredCount - availableCount, visited);
2681
+ if (!hasMore && this.store.hasImportedForkTurnHistory(sourceSession.localSessionId)) {
2682
+ this.store.audit(contextKey, "fork_turn_history_imported", {
2683
+ forkedLocalSessionId: current.localSessionId,
2684
+ sourceRemoteSessionId: source.sourceRemoteSessionId,
2685
+ sourceTurnId: source.sourceTurnId,
2686
+ reusedLocalSessionId: sourceSession.localSessionId,
2687
+ });
2688
+ }
2689
+ return hasMore;
2690
+ }
2653
2691
  if (!source.sourceRemoteSessionId)
2654
- return;
2692
+ return false;
2655
2693
  const runtime = this.runtimes.forAgent(current.agentName);
2656
- if (runtime.kind !== "codex" || !runtime.readRemoteSession)
2657
- return;
2694
+ if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries)
2695
+ return false;
2696
+ const seenCursors = new Set();
2658
2697
  try {
2659
- const remote = await runtime.readRemoteSession(source.sourceRemoteSessionId);
2660
- const completedTurns = remote.completedTurns ?? [];
2661
- const anchorIndex = completedTurns.findIndex((turn) => turn.id === source.sourceTurnId);
2662
- if (anchorIndex < 0)
2663
- return;
2664
- const inheritedTurns = completedTurns.slice(0, anchorIndex + 1);
2665
- const sourceSession = source.sourceLocalSessionId
2666
- ? this.store.getSession(source.sourceLocalSessionId)
2667
- : undefined;
2668
- const imported = this.persistCompletedTurnHistory({
2669
- localSessionId: sourceSession?.localSessionId ?? current.localSessionId,
2670
- contextKey: sourceSession?.contextKey ?? contextKey,
2671
- agentName: current.agentName,
2672
- remoteSessionId: source.sourceRemoteSessionId,
2673
- completedTurns: inheritedTurns,
2674
- fallbackStart: source.sourceTurnStartedAt
2675
- ?? source.sourceTurnCompletedAt
2676
- ?? Date.now() - inheritedTurns.length,
2677
- });
2678
- if (imported > 0) {
2698
+ while (!state.complete && (state.loadedCount ?? 0) < neededInheritedCount) {
2699
+ const remaining = neededInheritedCount - (state.loadedCount ?? 0);
2700
+ const page = await runtime.listRemoteTurnSummaries(source.sourceRemoteSessionId, {
2701
+ cursor: state.cursor, limit: Math.min(RESET_HISTORY_PAGE_SIZE, remaining),
2702
+ });
2703
+ if (page.nextCursor && (page.nextCursor === state.cursor || seenCursors.has(page.nextCursor))) {
2704
+ throw new Error("App Server repeated a Turn history cursor.");
2705
+ }
2706
+ const anchorIndex = state.anchorFound ? 0 : page.turns.findIndex((turn) => turn.id === source.sourceTurnId);
2707
+ const inheritedTurns = anchorIndex >= 0 ? page.turns.slice(anchorIndex) : [];
2708
+ const anchorFound = state.anchorFound || anchorIndex >= 0;
2709
+ if (!anchorFound && !page.nextCursor) {
2710
+ this.forkHistoryCursors.delete(key);
2711
+ throw new Error("The Fork source Turn was not found in the available history.");
2712
+ }
2713
+ if (inheritedTurns.length > 0) {
2714
+ const ownerId = sourceSession?.localSessionId ?? current.localSessionId;
2715
+ const imported = this.persistCompletedTurnHistory({
2716
+ localSessionId: ownerId,
2717
+ contextKey: sourceSession?.contextKey ?? contextKey,
2718
+ agentName: current.agentName,
2719
+ remoteSessionId: source.sourceRemoteSessionId,
2720
+ completedTurns: inheritedTurns.slice().reverse(),
2721
+ // Adjacent remote summaries repair gaps in locally inferred ancestry.
2722
+ replaceParents: true,
2723
+ fallbackStart: (source.sourceTurnStartedAt ?? source.sourceTurnCompletedAt
2724
+ ?? parseIsoTimestamp(source.createdAt) ?? 0) - (state.loadedCount ?? 0) - inheritedTurns.length,
2725
+ });
2726
+ if (imported === 0)
2727
+ throw new Error("Failed to persist the requested Turn history page.");
2728
+ if (state.tailTurnId) {
2729
+ this.store.saveTurnParent(state.tailTurnId, ownerId, inheritedTurns[0].id, true);
2730
+ }
2731
+ }
2732
+ state = {
2733
+ cursor: page.nextCursor,
2734
+ anchorFound,
2735
+ tailTurnId: inheritedTurns.at(-1)?.id ?? state.tailTurnId,
2736
+ loadedCount: (state.loadedCount ?? 0) + inheritedTurns.length,
2737
+ complete: !page.nextCursor,
2738
+ };
2739
+ this.forkHistoryCursors.set(key, state);
2740
+ if (page.nextCursor)
2741
+ seenCursors.add(page.nextCursor);
2742
+ }
2743
+ if (state.complete) {
2679
2744
  this.store.audit(contextKey, "fork_turn_history_imported", {
2680
2745
  forkedLocalSessionId: current.localSessionId,
2681
2746
  sourceRemoteSessionId: source.sourceRemoteSessionId,
2682
2747
  sourceTurnId: source.sourceTurnId,
2683
- importedTurnCount: imported,
2684
2748
  });
2685
2749
  }
2750
+ return !state.complete;
2686
2751
  }
2687
2752
  catch (error) {
2688
- this.logger.warn({ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId }, "Failed to hydrate legacy Fork Turn history; rendering the locally available history.");
2753
+ if (error instanceof CodexVersionError)
2754
+ throw error;
2755
+ this.logger.warn({ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId }, "Failed to load the requested Fork Turn history page; rendering the locally available history.");
2689
2756
  }
2757
+ return true;
2690
2758
  }
2691
- async hydrateRemoteTurnHistory(contextKey, current) {
2759
+ async hydrateRemoteTurnHistory(contextKey, current, requiredCount) {
2692
2760
  if (!current.remoteSessionId)
2693
- return;
2761
+ return false;
2762
+ const key = JSON.stringify([current.agentName, current.remoteSessionId, "turn-card"]);
2694
2763
  const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
2695
- if (refreshedAt !== undefined && Date.now() - refreshedAt < REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS)
2696
- return;
2697
- const forkSource = this.store.getForkHistorySource(current.localSessionId);
2698
- if (forkSource?.sourceTurnId === current.lastTurnId
2699
- && this.store.hasImportedForkTurnHistory(current.localSessionId))
2700
- return;
2764
+ let state = this.forkHistoryCursors.get(key);
2765
+ if (refreshedAt === undefined || Date.now() - refreshedAt >= REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS)
2766
+ state = undefined;
2767
+ state ??= { anchorFound: true, complete: false };
2701
2768
  const runtime = this.runtimes.forAgent(current.agentName);
2702
- if (runtime.kind !== "codex" || !runtime.readRemoteSession)
2703
- return;
2769
+ if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries)
2770
+ return false;
2771
+ const seen = new Set();
2704
2772
  try {
2705
- const remote = await runtime.readRemoteSession(current.remoteSessionId);
2706
- this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
2707
- const completedTurns = remote.completedTurns ?? [];
2708
- if (completedTurns.length === 0)
2709
- return;
2710
- this.persistCompletedTurnHistory({
2711
- localSessionId: current.localSessionId,
2712
- contextKey,
2713
- agentName: current.agentName,
2714
- remoteSessionId: current.remoteSessionId,
2715
- completedTurns,
2716
- fallbackStart: parseIsoTimestamp(current.createdAt) ?? Date.now() - completedTurns.length,
2717
- });
2773
+ while (!state.complete && (state.loadedCount ?? 0) < requiredCount) {
2774
+ const page = await runtime.listRemoteTurnSummaries(current.remoteSessionId, {
2775
+ cursor: state.cursor,
2776
+ limit: Math.min(RESET_HISTORY_PAGE_SIZE, requiredCount - (state.loadedCount ?? 0)),
2777
+ });
2778
+ if (page.nextCursor && (page.nextCursor === state.cursor || seen.has(page.nextCursor))) {
2779
+ throw new Error("App Server repeated a Turn history cursor.");
2780
+ }
2781
+ if (page.turns.length) {
2782
+ const imported = this.persistCompletedTurnHistory({
2783
+ localSessionId: current.localSessionId, contextKey, agentName: current.agentName,
2784
+ remoteSessionId: current.remoteSessionId,
2785
+ completedTurns: page.turns.slice().reverse(), replaceParents: true,
2786
+ fallbackStart: (parseIsoTimestamp(current.createdAt) ?? 0) - (state.loadedCount ?? 0) - page.turns.length,
2787
+ });
2788
+ if (!imported)
2789
+ throw new Error("Failed to persist the requested Turn history page.");
2790
+ if (state.tailTurnId)
2791
+ this.store.saveTurnParent(state.tailTurnId, current.localSessionId, page.turns[0].id, true);
2792
+ }
2793
+ state = {
2794
+ anchorFound: true, cursor: page.nextCursor, complete: !page.nextCursor,
2795
+ loadedCount: (state.loadedCount ?? 0) + page.turns.length,
2796
+ tailTurnId: page.turns.at(-1)?.id ?? state.tailTurnId,
2797
+ };
2798
+ this.forkHistoryCursors.set(key, state);
2799
+ this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
2800
+ if (page.nextCursor)
2801
+ seen.add(page.nextCursor);
2802
+ }
2803
+ return !state.complete;
2718
2804
  }
2719
2805
  catch (error) {
2806
+ if (error instanceof CodexVersionError)
2807
+ throw error;
2720
2808
  this.logger.warn({ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId }, "Failed to hydrate App Server Turn history; rendering the locally available history.");
2721
2809
  }
2810
+ return true;
2722
2811
  }
2723
2812
  async createProjectSessionFromReference(contextKey, reference) {
2724
2813
  const resolved = await this.resolveProjectSessionReference(contextKey, reference);
@@ -2750,7 +2839,7 @@ export class ProxySessionController {
2750
2839
  runtime = this.runtimes.forAgent(agentName);
2751
2840
  }
2752
2841
  else {
2753
- const resolved = await this.resolveRemoteCodexSession(taskId);
2842
+ const resolved = await this.resolveRemoteCodexSession(taskId, { view: "metadata" });
2754
2843
  agentName = resolved.agentName;
2755
2844
  runtime = resolved.runtime;
2756
2845
  remote = resolved.remote;
@@ -2758,10 +2847,10 @@ export class ProxySessionController {
2758
2847
  const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
2759
2848
  if (!remote && runtime.readRemoteSession && remoteSessionId) {
2760
2849
  try {
2761
- remote = await runtime.readRemoteSession(remoteSessionId);
2850
+ remote = await runtime.readRemoteSession(remoteSessionId, "metadata");
2762
2851
  }
2763
2852
  catch (error) {
2764
- if (!source)
2853
+ if (!source || error instanceof CodexVersionError)
2765
2854
  throw error;
2766
2855
  this.logger.warn({ error, contextKey, taskId: remoteSessionId }, "Failed to refresh the source task before creating a project task; using the local project path.");
2767
2856
  }
@@ -2992,6 +3081,7 @@ export class ProxySessionController {
2992
3081
  if (!userId?.startsWith("ou_")) {
2993
3082
  throw new Error("/forkgroup 只能由具有 open_id 的飞书用户消息触发。");
2994
3083
  }
3084
+ await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
2995
3085
  const prepared = await this.prepareForkGroupSession(sourceContextKey, requestedTitle, incomingMessage);
2996
3086
  await this.forkPreparedSessionToFeishuGroup(sourceContextKey, prepared, userId, incomingMessage?.threadContext ? prepared.sourceDescription : "当前任务");
2997
3087
  }
@@ -2999,6 +3089,7 @@ export class ProxySessionController {
2999
3089
  if (!userId?.startsWith("ou_")) {
3000
3090
  throw new Error("ForkGroup 只能由具有 open_id 的飞书用户触发。");
3001
3091
  }
3092
+ await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
3002
3093
  const plan = await this.prepareForkSession(sourceContextKey, reference);
3003
3094
  await this.forkPreparedSessionToFeishuGroup(sourceContextKey, {
3004
3095
  plan,
@@ -3107,7 +3198,8 @@ export class ProxySessionController {
3107
3198
  catch (error) {
3108
3199
  if (activeWriterThreadId(error))
3109
3200
  throw error;
3110
- if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error)))
3201
+ if (!(agent.kind === "app-server" && !record.lastTurnId
3202
+ && !this.store.getForkHistorySource(record.localSessionId) && isMissingRolloutError(error)))
3111
3203
  throw error;
3112
3204
  this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
3113
3205
  session = await runtime.createSession({
@@ -3522,6 +3614,7 @@ export class ProxySessionController {
3522
3614
  }
3523
3615
  async cancel(contextKey) {
3524
3616
  const record = this.requireCurrentSession(contextKey);
3617
+ await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
3525
3618
  await this.cancelSession(record);
3526
3619
  }
3527
3620
  async cancelSession(record) {
@@ -3536,6 +3629,8 @@ export class ProxySessionController {
3536
3629
  await runtime.setGoal(record.localSessionId, { status: "paused" });
3537
3630
  }
3538
3631
  catch (error) {
3632
+ if (error instanceof CodexVersionError)
3633
+ throw error;
3539
3634
  this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to pause the active Agent goal before interrupting its turn.");
3540
3635
  }
3541
3636
  }
@@ -3548,6 +3643,8 @@ export class ProxySessionController {
3548
3643
  : undefined;
3549
3644
  }
3550
3645
  catch (error) {
3646
+ if (error instanceof CodexVersionError)
3647
+ throw error;
3551
3648
  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.");
3552
3649
  }
3553
3650
  }
@@ -3591,8 +3688,14 @@ export class ProxySessionController {
3591
3688
  async resetCurrentSessionToTurn(contextKey, sessionId, turnId) {
3592
3689
  if (!sessionId || !turnId)
3593
3690
  throw new Error("无效的 Reset 请求。请使用最新的思考卡片重试。");
3691
+ await this.runSessionReset(sessionId, async () => {
3692
+ await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
3693
+ await this.performCurrentSessionReset(contextKey, sessionId, turnId);
3694
+ });
3695
+ }
3696
+ async runSessionReset(sessionId, operation) {
3594
3697
  const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
3595
- const reset = previous.catch(() => undefined).then(() => this.performCurrentSessionReset(contextKey, sessionId, turnId));
3698
+ const reset = previous.catch(() => undefined).then(operation);
3596
3699
  this.sessionResets.set(sessionId, reset);
3597
3700
  try {
3598
3701
  await reset;
@@ -3602,6 +3705,19 @@ export class ProxySessionController {
3602
3705
  this.sessionResets.delete(sessionId);
3603
3706
  }
3604
3707
  }
3708
+ async waitForCurrentSessionReset(contextKey) {
3709
+ const sessionId = this.currentSession(contextKey)?.localSessionId;
3710
+ if (!sessionId)
3711
+ return;
3712
+ let reset = this.sessionResets.get(sessionId);
3713
+ while (reset) {
3714
+ await reset.catch(() => undefined);
3715
+ const next = this.sessionResets.get(sessionId);
3716
+ if (!next || next === reset)
3717
+ return;
3718
+ reset = next;
3719
+ }
3720
+ }
3605
3721
  async resetFromHistoryCard(contextKey, sessionId, turnId, updateMessageId, page) {
3606
3722
  if (!sessionId || !turnId)
3607
3723
  throw new Error("无效的 Reset 请求。请使用最新的历史轮次卡片重试。");
@@ -3612,16 +3728,11 @@ export class ProxySessionController {
3612
3728
  }
3613
3729
  this.resetHistoryOperations.set(sessionId, turnId);
3614
3730
  try {
3615
- await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
3616
- }
3617
- catch (error) {
3618
- if (this.resetHistoryOperations.get(sessionId) === turnId) {
3619
- this.resetHistoryOperations.delete(sessionId);
3620
- }
3621
- throw error;
3622
- }
3623
- try {
3624
- await this.resetCurrentSessionToTurn(contextKey, sessionId, turnId);
3731
+ await this.runSessionReset(sessionId, async () => {
3732
+ await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
3733
+ await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
3734
+ await this.performCurrentSessionReset(contextKey, sessionId, turnId);
3735
+ });
3625
3736
  }
3626
3737
  catch (error) {
3627
3738
  if (this.resetHistoryOperations.get(sessionId) === turnId) {
@@ -3651,9 +3762,9 @@ export class ProxySessionController {
3651
3762
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
3652
3763
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
3653
3764
  }
3654
- const historyTurn = this.store.listTaskTurnGraph(current.localSessionId)
3765
+ const historyTurn = this.store.listTaskTurnGraphIndex(current.localSessionId)
3655
3766
  .find((turn) => turn.turnId === turnId);
3656
- const snapshot = turnViewSnapshot(historyTurn?.snapshot);
3767
+ const snapshot = historyTurn ? this.store.getTurnPromptSummary(turnId) : undefined;
3657
3768
  if (!snapshot || snapshot.status !== "completed") {
3658
3769
  throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
3659
3770
  }
@@ -3809,6 +3920,7 @@ export class ProxySessionController {
3809
3920
  const record = options.sessionId
3810
3921
  ? this.requireSession(contextKey, options.sessionId)
3811
3922
  : this.requireCurrentSession(contextKey);
3923
+ await this.outbound.sendText(contextKey, `正在切换到 Provider ${modelProvider},请稍后。`);
3812
3924
  const loaded = await this.loadSession(record);
3813
3925
  await this.assertModelProvider(loaded, modelProvider);
3814
3926
  const models = await loaded.runtime.listModels();
@@ -3826,7 +3938,7 @@ export class ProxySessionController {
3826
3938
  model: model.id,
3827
3939
  effort,
3828
3940
  mode: loaded.session.permissionMode,
3829
- }, options);
3941
+ }, options, false);
3830
3942
  }
3831
3943
  async openProviderSelector(contextKey, options = {}) {
3832
3944
  await this.openExecutionSettings(contextKey, "provider", options);
@@ -3881,28 +3993,21 @@ export class ProxySessionController {
3881
3993
  currentMode: permissionMode,
3882
3994
  }));
3883
3995
  }
3884
- async applyProviderSettings(contextKey, settings, options = {}) {
3996
+ async applyProviderSettings(contextKey, settings, options = {}, announceStart = true) {
3885
3997
  const record = options.sessionId
3886
3998
  ? this.requireSession(contextKey, options.sessionId)
3887
3999
  : this.requireCurrentSession(contextKey);
4000
+ if (announceStart) {
4001
+ await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
4002
+ }
3888
4003
  const loaded = await this.loadSession(record);
3889
4004
  await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
3890
- if (!loaded.runtime.setExecutionSettings) {
3891
- throw new Error("当前运行时不支持 Provider 设置。");
3892
- }
3893
- const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
4005
+ const session = await this.changeProviderSettings(loaded, {
3894
4006
  modelProvider: settings.provider,
3895
4007
  model: settings.model,
3896
4008
  reasoningEffort: settings.effort,
3897
4009
  permissionMode: settings.mode,
3898
4010
  });
3899
- this.store.updateRuntimeSession(loaded.record.localSessionId, {
3900
- modelProvider: session.modelProvider ?? settings.provider,
3901
- model: session.model ?? settings.model,
3902
- reasoningEffort: session.reasoningEffort ?? settings.effort,
3903
- permissionMode: session.permissionMode,
3904
- });
3905
- await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
3906
4011
  const notice = [
3907
4012
  `Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
3908
4013
  `模型 ${cardCode(session.model ?? settings.model)}`,
@@ -3921,6 +4026,31 @@ export class ProxySessionController {
3921
4026
  await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
3922
4027
  }
3923
4028
  }
4029
+ async changeProviderSettings(loaded, settings) {
4030
+ if (!loaded.runtime.setExecutionSettings)
4031
+ throw new Error("当前运行时不支持 Provider 设置。");
4032
+ const persist = async (candidate) => {
4033
+ assertProviderSettingsApplied(candidate, settings);
4034
+ const agent = this.ensureAgent(loaded.record.agentName);
4035
+ const previousDefaults = agent.defaults;
4036
+ await this.persistAgentExecutionDefaults(loaded.record.localSessionId, candidate);
4037
+ try {
4038
+ this.store.updateRuntimeSession(loaded.record.localSessionId, {
4039
+ remoteSessionId: candidate.remoteSessionId,
4040
+ modelProvider: candidate.modelProvider,
4041
+ model: candidate.model,
4042
+ reasoningEffort: candidate.reasoningEffort,
4043
+ permissionMode: candidate.permissionMode,
4044
+ });
4045
+ }
4046
+ catch (error) {
4047
+ await this.lifecycle?.persistAgentExecutionDefaults?.(loaded.record.agentName, previousDefaults ?? {});
4048
+ agent.defaults = previousDefaults;
4049
+ throw error;
4050
+ }
4051
+ };
4052
+ return loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
4053
+ }
3924
4054
  async assertProviderModelSettings(loaded, modelProvider, model, effort) {
3925
4055
  await this.assertModelProvider(loaded, modelProvider);
3926
4056
  const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
@@ -4283,7 +4413,9 @@ export class ProxySessionController {
4283
4413
  const normalizedTitle = normalizeTaskTitle(title);
4284
4414
  if (!normalizedTitle)
4285
4415
  throw new Error("任务标题不能为空。");
4286
- const loaded = await this.loadSession(this.requireCurrentSession(contextKey));
4416
+ const current = this.requireCurrentSession(contextKey);
4417
+ await this.outbound.sendText(contextKey, CHANGE_TITLE_STARTED_MESSAGE);
4418
+ const loaded = await this.loadSession(current);
4287
4419
  if (loaded.runtime.setTitle)
4288
4420
  await loaded.runtime.setTitle(loaded.record.localSessionId, normalizedTitle);
4289
4421
  else
@@ -4403,9 +4535,9 @@ export class ProxySessionController {
4403
4535
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
4404
4536
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
4405
4537
  }
4406
- await this.hydrateLegacyForkTurnHistory(contextKey, current);
4407
- await this.hydrateRemoteTurnHistory(contextKey, current);
4408
- const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
4538
+ const requestedPage = Math.max(0, Math.trunc(options.page ?? 0));
4539
+ const hasMoreHistory = await this.hydrateTaskTurnHistory(current, (requestedPage + 1) * RESET_HISTORY_PAGE_SIZE);
4540
+ const completedTurns = this.store.listTaskTurnGraphIndex(current.localSessionId);
4409
4541
  const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
4410
4542
  const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
4411
4543
  && !completedTurnIds.has(current.lastTurnId)
@@ -4447,13 +4579,13 @@ export class ProxySessionController {
4447
4579
  parentTurnId: turn.parentTurnId,
4448
4580
  })));
4449
4581
  const total = allTurns.length;
4450
- const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE));
4582
+ const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE)) + (hasMoreHistory ? 1 : 0);
4451
4583
  const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
4452
4584
  const offset = page * RESET_HISTORY_PAGE_SIZE;
4453
4585
  const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
4454
4586
  const resettingTurnId = this.resetHistoryOperations.get(current.localSessionId);
4455
4587
  const entries = turns.flatMap((turn, index) => {
4456
- const snapshot = turnViewSnapshot(turn.snapshot);
4588
+ const snapshot = turn.turnId === runningTurnId ? runningSnapshot : this.store.getTurnPromptSummary(turn.turnId);
4457
4589
  if (!snapshot)
4458
4590
  return [];
4459
4591
  const graph = graphRows[offset + index];
@@ -4506,9 +4638,11 @@ export class ProxySessionController {
4506
4638
  const card = this.cardRenderer.renderResetHistoryCard({
4507
4639
  entries,
4508
4640
  footerLines: [
4509
- runningTurn
4510
- ? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
4511
- : `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
4641
+ hasMoreHistory
4642
+ ? `第 ${page + 1} 页 · 已加载 ${total} 个 turn`
4643
+ : runningTurn
4644
+ ? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
4645
+ : `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
4512
4646
  ...(resettingTurnId ? ["正在 Reset 到所选轮次,请稍候…"] : []),
4513
4647
  ],
4514
4648
  pageActions,
@@ -4707,6 +4841,7 @@ export class ProxySessionController {
4707
4841
  const record = this.requireCurrentSession(contextKey);
4708
4842
  const filePath = resolveUserPath(requestedFilePath, record.cwd);
4709
4843
  await this.assertSendableFile(filePath);
4844
+ await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
4710
4845
  await this.outbound.sendFile(contextKey, filePath);
4711
4846
  }
4712
4847
  async listSessions(contextKey, searchTerm, options = {}) {
@@ -4997,7 +5132,7 @@ export class ProxySessionController {
4997
5132
  runtime,
4998
5133
  remote: options.forFork && runtime.readRemoteForkSource
4999
5134
  ? await runtime.readRemoteForkSource(remoteSessionId)
5000
- : await runtime.readRemoteSession(remoteSessionId),
5135
+ : await runtime.readRemoteSession(remoteSessionId, options.view ?? "latest"),
5001
5136
  };
5002
5137
  }));
5003
5138
  const matches = reads
@@ -5023,6 +5158,7 @@ export class ProxySessionController {
5023
5158
  if (direct?.status === "closed") {
5024
5159
  throw new Error(`找不到任务:${taskId}`);
5025
5160
  }
5161
+ await this.outbound.sendText(contextKey, SWITCH_TASK_STARTED_MESSAGE);
5026
5162
  const existing = direct ?? this.findStoredSessionByReference(taskId);
5027
5163
  if (existing) {
5028
5164
  const runtime = this.runtimes.forAgent(existing.agentName);
@@ -5107,6 +5243,9 @@ export class ProxySessionController {
5107
5243
  if (local && this.store.countQueuedPrompts(local.localSessionId) > 0) {
5108
5244
  throw new Error("任务仍有排队消息,无法归档。请先处理或取消排队消息。");
5109
5245
  }
5246
+ if (options.source !== "dismiss") {
5247
+ await this.outbound.sendText(contextKey, ARCHIVE_TASK_STARTED_MESSAGE);
5248
+ }
5110
5249
  const remoteReference = local
5111
5250
  ? remoteSessionReference(local.agentName, local.remoteSessionId)
5112
5251
  : taskId;
@@ -5314,28 +5453,32 @@ export class ProxySessionController {
5314
5453
  else {
5315
5454
  current = context.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
5316
5455
  }
5317
- if (current && current.status === "running") {
5318
- try {
5319
- const runtime = this.runtimes.forAgent(current.agentName);
5320
- if (runtime.getSession(current.localSessionId)) {
5321
- await runtime.synchronizeSession(current.localSessionId);
5456
+ let remote;
5457
+ if (current?.status === "running") {
5458
+ const runtime = this.runtimes.forAgent(current.agentName);
5459
+ if (runtime.getSession(current.localSessionId)) {
5460
+ try {
5461
+ remote = (await runtime.synchronizeSession(current.localSessionId)).remoteSummary;
5322
5462
  current = this.store.getSession(current.localSessionId) ?? current;
5323
5463
  }
5324
- }
5325
- catch (error) {
5326
- this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
5464
+ catch (error) {
5465
+ if (error instanceof CodexVersionError)
5466
+ throw error;
5467
+ this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
5468
+ }
5327
5469
  }
5328
5470
  }
5329
5471
  const localCurrent = current;
5330
- let remote;
5331
5472
  let goal;
5332
5473
  if (current?.remoteSessionId) {
5333
5474
  const runtime = this.runtimes.forAgent(current.agentName);
5334
- if (runtime.readRemoteSession) {
5475
+ if (!remote && runtime.readRemoteSession) {
5335
5476
  try {
5336
- remote = await runtime.readRemoteSession(current.remoteSessionId);
5477
+ remote = await runtime.readRemoteSession(current.remoteSessionId, "latest-full");
5337
5478
  }
5338
5479
  catch (error) {
5480
+ if (error instanceof CodexVersionError)
5481
+ throw error;
5339
5482
  this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task status.");
5340
5483
  }
5341
5484
  }
@@ -5431,7 +5574,7 @@ export class ProxySessionController {
5431
5574
  await this.outbound.sendInteractiveCard(contextKey, card);
5432
5575
  }
5433
5576
  async statusForCodexTask(contextKey, reference, options = {}) {
5434
- const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
5577
+ const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
5435
5578
  const actionReference = remoteSessionReference(agentName, remote.id);
5436
5579
  const sections = [
5437
5580
  {
@@ -5562,10 +5705,11 @@ export class ProxySessionController {
5562
5705
  throw new Error(`未知 agent:${agentName}`);
5563
5706
  return agent;
5564
5707
  }
5565
- async persistAgentExecutionDefaults(localSessionId) {
5566
- const session = this.store.getSession(localSessionId);
5567
- if (!session)
5708
+ async persistAgentExecutionDefaults(localSessionId, candidate) {
5709
+ const record = this.store.getSession(localSessionId);
5710
+ if (!record)
5568
5711
  throw new Error(`找不到任务:${localSessionId}`);
5712
+ const session = candidate ?? record;
5569
5713
  const agent = this.ensureAgent(session.agentName);
5570
5714
  const observed = {
5571
5715
  ...(session.modelProvider?.trim() ? { modelProvider: session.modelProvider } : {}),
@@ -5935,6 +6079,7 @@ export class ProxySessionController {
5935
6079
  const lockPath = runtime.getThreadWriterLockPath?.(threadId);
5936
6080
  if (!lockPath)
5937
6081
  throw new Error("当前 Agent 不支持自动关闭占用程序。");
6082
+ await this.outbound.sendText(contextKey, CLOSE_WRITER_STARTED_MESSAGE);
5938
6083
  let owners = await this.threadWriterProcesses.inspect(lockPath);
5939
6084
  const expectedOwner = owners.length === 1 && threadWriterMatches(owners[0], expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt) ? owners[0] : undefined;
5940
6085
  if (!expectedOwner) {
@@ -6260,10 +6405,6 @@ function bindSessionCardAction(action, bindings) {
6260
6405
  function runtimeErrorMessage(error) {
6261
6406
  return error instanceof Error ? error.message : String(error);
6262
6407
  }
6263
- function isMissingRolloutError(error) {
6264
- const message = error instanceof Error ? error.message : String(error);
6265
- return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
6266
- }
6267
6408
  function isUnmaterializedCodexThreadError(error) {
6268
6409
  const message = error instanceof Error ? error.message : String(error);
6269
6410
  return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);