@keyou007/agent-bot 0.1.20-alpha.1 → 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 (63) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +13 -0
  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/feishu/CardRenderer.js +36 -11
  21. package/dist/feishu/CardRenderer.js.map +1 -1
  22. package/dist/feishu/FeishuConnector.js +17 -5
  23. package/dist/feishu/FeishuConnector.js.map +1 -1
  24. package/dist/feishu/InboundText.js +11 -0
  25. package/dist/feishu/InboundText.js.map +1 -1
  26. package/dist/feishu/MergedForwardMessage.js +17 -5
  27. package/dist/feishu/MergedForwardMessage.js.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/local-files/LocalFileViewerServer.js +14 -4
  31. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  32. package/dist/local-files/MarkdownFileLinks.js +65 -0
  33. package/dist/local-files/MarkdownFileLinks.js.map +1 -0
  34. package/dist/proxy/ProxySessionController.js +399 -257
  35. package/dist/proxy/ProxySessionController.js.map +1 -1
  36. package/dist/state/StateStore.js +58 -65
  37. package/dist/state/StateStore.js.map +1 -1
  38. package/dist/utils/semanticVersion.js +39 -0
  39. package/dist/utils/semanticVersion.js.map +1 -0
  40. package/docs/technical-reference.md +5 -3
  41. package/docs/technical-reference.zh.md +5 -3
  42. package/npm-shrinkwrap.json +2 -2
  43. package/package.json +1 -1
  44. package/skills/agent-bot/SKILL.md +7 -1
  45. package/src/cli/AgentPrerequisites.ts +14 -40
  46. package/src/cli/DefaultAgentSelection.ts +1 -1
  47. package/src/cli.ts +13 -0
  48. package/src/codex/CodexLocalActivityDetector.ts +41 -9
  49. package/src/codex/CodexProcessManager.ts +44 -7
  50. package/src/codex/CodexRuntime.ts +244 -195
  51. package/src/codex/CodexVersion.ts +18 -0
  52. package/src/codex/ProviderSwitch.ts +36 -0
  53. package/src/feishu/CardRenderer.ts +37 -27
  54. package/src/feishu/FeishuConnector.ts +15 -5
  55. package/src/feishu/InboundText.ts +11 -0
  56. package/src/feishu/MergedForwardMessage.ts +15 -5
  57. package/src/index.ts +1 -1
  58. package/src/local-files/LocalFileViewerServer.ts +14 -4
  59. package/src/local-files/MarkdownFileLinks.ts +64 -0
  60. package/src/proxy/ProxySessionController.ts +369 -238
  61. package/src/runtime/types.ts +15 -2
  62. package/src/state/StateStore.ts +77 -90
  63. 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
  }
@@ -2284,6 +2321,7 @@ export class ProxySessionController {
2284
2321
  return { anchor, source, snapshot };
2285
2322
  }
2286
2323
  async forkSessionReference(contextKey, reference) {
2324
+ await this.outbound.sendText(contextKey, FORK_TASK_STARTED_MESSAGE);
2287
2325
  const plan = await this.prepareForkSession(contextKey, reference);
2288
2326
  const forked = await this.forkSessionIntoContext(contextKey, plan);
2289
2327
  const forkSourceLabel = plan.forkedFromHistoricalTurn
@@ -2335,32 +2373,22 @@ export class ProxySessionController {
2335
2373
  if (!runtime.readRemoteSession && !source) {
2336
2374
  throw new Error("当前 App Server Agent 不支持读取指定任务。");
2337
2375
  }
2338
- const persistedCompletedTurns = source ? this.localForkCompletedTurns(source) : [];
2376
+ const persistedCompletedTurnId = source
2377
+ ? this.store.findLatestCompletedTurnId(source.localSessionId)
2378
+ : undefined;
2339
2379
  const sourceLastSnapshot = turnViewSnapshot(source?.lastTurnId ? this.store.getTurnSnapshot(source.lastTurnId) : undefined);
2340
- const localCompletedTurns = persistedCompletedTurns.length > 0
2341
- ? persistedCompletedTurns
2342
- : source?.lastTurnId
2380
+ const localCompletedTurnId = persistedCompletedTurnId
2381
+ ?? (source?.lastTurnId
2343
2382
  && (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
- }
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);
2362
2390
  }
2363
- const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurns.at(-1)?.id;
2391
+ const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurnId;
2364
2392
  const latestSnapshot = turnViewSnapshot(latestTurnId ? this.store.getTurnSnapshot(latestTurnId) : undefined);
2365
2393
  const isRunning = remote
2366
2394
  ? isRemoteSessionActive(remote)
@@ -2369,7 +2397,7 @@ export class ProxySessionController {
2369
2397
  || isTurnStillRunning(latestSnapshot?.status));
2370
2398
  const lastTurnId = remote?.lastCompletedTurnId
2371
2399
  ?? (remote?.lastTurnStatus === "completed" ? remote.lastTurnId : undefined)
2372
- ?? localCompletedTurns.at(-1)?.id;
2400
+ ?? localCompletedTurnId;
2373
2401
  if (!lastTurnId) {
2374
2402
  if (isRunning) {
2375
2403
  throw new Error(`${sourceLabel}正在执行,且还没有已完成轮次可供 fork。请等待当前轮次完成后重试。`);
@@ -2378,8 +2406,7 @@ export class ProxySessionController {
2378
2406
  }
2379
2407
  const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
2380
2408
  const forkedFromHistoricalTurn = lastTurnId !== latestTurnId;
2381
- const completedTurn = (remote?.completedTurns ?? localCompletedTurns)
2382
- .find((turn) => turn.id === lastTurnId);
2409
+ const completedTurn = remote?.completedTurns?.find((turn) => turn.id === lastTurnId);
2383
2410
  const cwd = remote?.cwd || source?.cwd;
2384
2411
  if (!cwd)
2385
2412
  throw new Error("指定的 App Server 任务没有可用的工作目录,暂时不能 fork。");
@@ -2421,27 +2448,10 @@ export class ProxySessionController {
2421
2448
  sourceTurnPrompt,
2422
2449
  sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
2423
2450
  sourceTurnCompletedAt,
2424
- sourceCompletedTurns: remote?.completedTurns
2425
- ?? (persistedCompletedTurns.length > 0 ? [] : localCompletedTurns),
2426
2451
  sourceWasRunning: isRunning,
2427
2452
  forkedFromHistoricalTurn,
2428
2453
  };
2429
2454
  }
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
2455
  async prepareForkGroupSession(contextKey, requestedTitle, incomingMessage) {
2446
2456
  if (!incomingMessage?.threadContext) {
2447
2457
  const plan = await this.prepareForkSession(contextKey, undefined, requestedTitle);
@@ -2511,7 +2521,6 @@ export class ProxySessionController {
2511
2521
  };
2512
2522
  }
2513
2523
  async forkSessionIntoContext(contextKey, plan) {
2514
- const sourceCompletedTurns = await this.resolveForkSourceCompletedTurns(plan);
2515
2524
  const localSessionId = createId("sess");
2516
2525
  const record = this.store.createSession({
2517
2526
  localSessionId,
@@ -2547,7 +2556,6 @@ export class ProxySessionController {
2547
2556
  lastTurnStatus: plan.lastTurnStatus,
2548
2557
  });
2549
2558
  this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName));
2550
- const sourceTurnHistoryCount = this.persistForkSourceHistory(contextKey, localSessionId, plan, sourceCompletedTurns);
2551
2559
  this.store.audit(contextKey, "session_forked", {
2552
2560
  sourceLocalSessionId: plan.source?.localSessionId,
2553
2561
  sourceRemoteSessionId: plan.remoteSessionId,
@@ -2555,7 +2563,6 @@ export class ProxySessionController {
2555
2563
  sourceTurnPrompt: plan.sourceTurnPrompt,
2556
2564
  sourceTurnStartedAt: plan.sourceTurnStartedAt,
2557
2565
  sourceTurnCompletedAt: plan.sourceTurnCompletedAt,
2558
- sourceTurnHistoryCount,
2559
2566
  sourceWasRunning: plan.sourceWasRunning,
2560
2567
  forkedLocalSessionId: localSessionId,
2561
2568
  forkedRemoteSessionId: forked.remoteSessionId,
@@ -2571,42 +2578,6 @@ export class ProxySessionController {
2571
2578
  throw error;
2572
2579
  }
2573
2580
  }
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
2581
  persistCompletedTurnHistory(input) {
2611
2582
  try {
2612
2583
  this.store.importCompletedTurnHistory({
@@ -2614,6 +2585,7 @@ export class ProxySessionController {
2614
2585
  contextKey: input.contextKey,
2615
2586
  agentName: input.agentName,
2616
2587
  remoteSessionId: input.remoteSessionId,
2588
+ replaceParents: input.replaceParents,
2617
2589
  turns: input.completedTurns.map((turn, index) => {
2618
2590
  const startedAt = turn.startedAt ?? input.fallbackStart + index;
2619
2591
  const completedAt = turn.completedAt ?? startedAt;
@@ -2644,80 +2616,198 @@ export class ProxySessionController {
2644
2616
  return 0;
2645
2617
  }
2646
2618
  }
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;
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
+ }
2652
2691
  if (!source.sourceRemoteSessionId)
2653
- return;
2692
+ return false;
2654
2693
  const runtime = this.runtimes.forAgent(current.agentName);
2655
- if (runtime.kind !== "codex" || !runtime.readRemoteSession)
2656
- return;
2694
+ if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries)
2695
+ return false;
2696
+ const seenCursors = new Set();
2657
2697
  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) {
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) {
2678
2744
  this.store.audit(contextKey, "fork_turn_history_imported", {
2679
2745
  forkedLocalSessionId: current.localSessionId,
2680
2746
  sourceRemoteSessionId: source.sourceRemoteSessionId,
2681
2747
  sourceTurnId: source.sourceTurnId,
2682
- importedTurnCount: imported,
2683
2748
  });
2684
2749
  }
2750
+ return !state.complete;
2685
2751
  }
2686
2752
  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.");
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.");
2688
2756
  }
2757
+ return true;
2689
2758
  }
2690
- async hydrateRemoteTurnHistory(contextKey, current) {
2759
+ async hydrateRemoteTurnHistory(contextKey, current, requiredCount) {
2691
2760
  if (!current.remoteSessionId)
2692
- return;
2761
+ return false;
2762
+ const key = JSON.stringify([current.agentName, current.remoteSessionId, "turn-card"]);
2693
2763
  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;
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 };
2700
2768
  const runtime = this.runtimes.forAgent(current.agentName);
2701
- if (runtime.kind !== "codex" || !runtime.readRemoteSession)
2702
- return;
2769
+ if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries)
2770
+ return false;
2771
+ const seen = new Set();
2703
2772
  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
- });
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;
2717
2804
  }
2718
2805
  catch (error) {
2806
+ if (error instanceof CodexVersionError)
2807
+ throw error;
2719
2808
  this.logger.warn({ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId }, "Failed to hydrate App Server Turn history; rendering the locally available history.");
2720
2809
  }
2810
+ return true;
2721
2811
  }
2722
2812
  async createProjectSessionFromReference(contextKey, reference) {
2723
2813
  const resolved = await this.resolveProjectSessionReference(contextKey, reference);
@@ -2749,7 +2839,7 @@ export class ProxySessionController {
2749
2839
  runtime = this.runtimes.forAgent(agentName);
2750
2840
  }
2751
2841
  else {
2752
- const resolved = await this.resolveRemoteCodexSession(taskId);
2842
+ const resolved = await this.resolveRemoteCodexSession(taskId, { view: "metadata" });
2753
2843
  agentName = resolved.agentName;
2754
2844
  runtime = resolved.runtime;
2755
2845
  remote = resolved.remote;
@@ -2757,10 +2847,10 @@ export class ProxySessionController {
2757
2847
  const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
2758
2848
  if (!remote && runtime.readRemoteSession && remoteSessionId) {
2759
2849
  try {
2760
- remote = await runtime.readRemoteSession(remoteSessionId);
2850
+ remote = await runtime.readRemoteSession(remoteSessionId, "metadata");
2761
2851
  }
2762
2852
  catch (error) {
2763
- if (!source)
2853
+ if (!source || error instanceof CodexVersionError)
2764
2854
  throw error;
2765
2855
  this.logger.warn({ error, contextKey, taskId: remoteSessionId }, "Failed to refresh the source task before creating a project task; using the local project path.");
2766
2856
  }
@@ -2991,6 +3081,7 @@ export class ProxySessionController {
2991
3081
  if (!userId?.startsWith("ou_")) {
2992
3082
  throw new Error("/forkgroup 只能由具有 open_id 的飞书用户消息触发。");
2993
3083
  }
3084
+ await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
2994
3085
  const prepared = await this.prepareForkGroupSession(sourceContextKey, requestedTitle, incomingMessage);
2995
3086
  await this.forkPreparedSessionToFeishuGroup(sourceContextKey, prepared, userId, incomingMessage?.threadContext ? prepared.sourceDescription : "当前任务");
2996
3087
  }
@@ -2998,6 +3089,7 @@ export class ProxySessionController {
2998
3089
  if (!userId?.startsWith("ou_")) {
2999
3090
  throw new Error("ForkGroup 只能由具有 open_id 的飞书用户触发。");
3000
3091
  }
3092
+ await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
3001
3093
  const plan = await this.prepareForkSession(sourceContextKey, reference);
3002
3094
  await this.forkPreparedSessionToFeishuGroup(sourceContextKey, {
3003
3095
  plan,
@@ -3106,7 +3198,8 @@ export class ProxySessionController {
3106
3198
  catch (error) {
3107
3199
  if (activeWriterThreadId(error))
3108
3200
  throw error;
3109
- 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)))
3110
3203
  throw error;
3111
3204
  this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
3112
3205
  session = await runtime.createSession({
@@ -3521,6 +3614,7 @@ export class ProxySessionController {
3521
3614
  }
3522
3615
  async cancel(contextKey) {
3523
3616
  const record = this.requireCurrentSession(contextKey);
3617
+ await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
3524
3618
  await this.cancelSession(record);
3525
3619
  }
3526
3620
  async cancelSession(record) {
@@ -3535,6 +3629,8 @@ export class ProxySessionController {
3535
3629
  await runtime.setGoal(record.localSessionId, { status: "paused" });
3536
3630
  }
3537
3631
  catch (error) {
3632
+ if (error instanceof CodexVersionError)
3633
+ throw error;
3538
3634
  this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to pause the active Agent goal before interrupting its turn.");
3539
3635
  }
3540
3636
  }
@@ -3547,6 +3643,8 @@ export class ProxySessionController {
3547
3643
  : undefined;
3548
3644
  }
3549
3645
  catch (error) {
3646
+ if (error instanceof CodexVersionError)
3647
+ throw error;
3550
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.");
3551
3649
  }
3552
3650
  }
@@ -3590,8 +3688,14 @@ export class ProxySessionController {
3590
3688
  async resetCurrentSessionToTurn(contextKey, sessionId, turnId) {
3591
3689
  if (!sessionId || !turnId)
3592
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) {
3593
3697
  const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
3594
- const reset = previous.catch(() => undefined).then(() => this.performCurrentSessionReset(contextKey, sessionId, turnId));
3698
+ const reset = previous.catch(() => undefined).then(operation);
3595
3699
  this.sessionResets.set(sessionId, reset);
3596
3700
  try {
3597
3701
  await reset;
@@ -3601,6 +3705,19 @@ export class ProxySessionController {
3601
3705
  this.sessionResets.delete(sessionId);
3602
3706
  }
3603
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
+ }
3604
3721
  async resetFromHistoryCard(contextKey, sessionId, turnId, updateMessageId, page) {
3605
3722
  if (!sessionId || !turnId)
3606
3723
  throw new Error("无效的 Reset 请求。请使用最新的历史轮次卡片重试。");
@@ -3611,16 +3728,11 @@ export class ProxySessionController {
3611
3728
  }
3612
3729
  this.resetHistoryOperations.set(sessionId, turnId);
3613
3730
  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);
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
+ });
3624
3736
  }
3625
3737
  catch (error) {
3626
3738
  if (this.resetHistoryOperations.get(sessionId) === turnId) {
@@ -3650,9 +3762,9 @@ export class ProxySessionController {
3650
3762
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
3651
3763
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
3652
3764
  }
3653
- const historyTurn = this.store.listTaskTurnGraph(current.localSessionId)
3765
+ const historyTurn = this.store.listTaskTurnGraphIndex(current.localSessionId)
3654
3766
  .find((turn) => turn.turnId === turnId);
3655
- const snapshot = turnViewSnapshot(historyTurn?.snapshot);
3767
+ const snapshot = historyTurn ? this.store.getTurnPromptSummary(turnId) : undefined;
3656
3768
  if (!snapshot || snapshot.status !== "completed") {
3657
3769
  throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
3658
3770
  }
@@ -3808,6 +3920,7 @@ export class ProxySessionController {
3808
3920
  const record = options.sessionId
3809
3921
  ? this.requireSession(contextKey, options.sessionId)
3810
3922
  : this.requireCurrentSession(contextKey);
3923
+ await this.outbound.sendText(contextKey, `正在切换到 Provider ${modelProvider},请稍后。`);
3811
3924
  const loaded = await this.loadSession(record);
3812
3925
  await this.assertModelProvider(loaded, modelProvider);
3813
3926
  const models = await loaded.runtime.listModels();
@@ -3825,7 +3938,7 @@ export class ProxySessionController {
3825
3938
  model: model.id,
3826
3939
  effort,
3827
3940
  mode: loaded.session.permissionMode,
3828
- }, options);
3941
+ }, options, false);
3829
3942
  }
3830
3943
  async openProviderSelector(contextKey, options = {}) {
3831
3944
  await this.openExecutionSettings(contextKey, "provider", options);
@@ -3880,28 +3993,21 @@ export class ProxySessionController {
3880
3993
  currentMode: permissionMode,
3881
3994
  }));
3882
3995
  }
3883
- async applyProviderSettings(contextKey, settings, options = {}) {
3996
+ async applyProviderSettings(contextKey, settings, options = {}, announceStart = true) {
3884
3997
  const record = options.sessionId
3885
3998
  ? this.requireSession(contextKey, options.sessionId)
3886
3999
  : this.requireCurrentSession(contextKey);
4000
+ if (announceStart) {
4001
+ await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
4002
+ }
3887
4003
  const loaded = await this.loadSession(record);
3888
4004
  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, {
4005
+ const session = await this.changeProviderSettings(loaded, {
3893
4006
  modelProvider: settings.provider,
3894
4007
  model: settings.model,
3895
4008
  reasoningEffort: settings.effort,
3896
4009
  permissionMode: settings.mode,
3897
4010
  });
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
4011
  const notice = [
3906
4012
  `Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
3907
4013
  `模型 ${cardCode(session.model ?? settings.model)}`,
@@ -3920,6 +4026,31 @@ export class ProxySessionController {
3920
4026
  await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
3921
4027
  }
3922
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
+ }
3923
4054
  async assertProviderModelSettings(loaded, modelProvider, model, effort) {
3924
4055
  await this.assertModelProvider(loaded, modelProvider);
3925
4056
  const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
@@ -4282,7 +4413,9 @@ export class ProxySessionController {
4282
4413
  const normalizedTitle = normalizeTaskTitle(title);
4283
4414
  if (!normalizedTitle)
4284
4415
  throw new Error("任务标题不能为空。");
4285
- 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);
4286
4419
  if (loaded.runtime.setTitle)
4287
4420
  await loaded.runtime.setTitle(loaded.record.localSessionId, normalizedTitle);
4288
4421
  else
@@ -4402,9 +4535,9 @@ export class ProxySessionController {
4402
4535
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
4403
4536
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
4404
4537
  }
4405
- await this.hydrateLegacyForkTurnHistory(contextKey, current);
4406
- await this.hydrateRemoteTurnHistory(contextKey, current);
4407
- 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);
4408
4541
  const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
4409
4542
  const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
4410
4543
  && !completedTurnIds.has(current.lastTurnId)
@@ -4446,13 +4579,13 @@ export class ProxySessionController {
4446
4579
  parentTurnId: turn.parentTurnId,
4447
4580
  })));
4448
4581
  const total = allTurns.length;
4449
- 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);
4450
4583
  const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
4451
4584
  const offset = page * RESET_HISTORY_PAGE_SIZE;
4452
4585
  const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
4453
4586
  const resettingTurnId = this.resetHistoryOperations.get(current.localSessionId);
4454
4587
  const entries = turns.flatMap((turn, index) => {
4455
- const snapshot = turnViewSnapshot(turn.snapshot);
4588
+ const snapshot = turn.turnId === runningTurnId ? runningSnapshot : this.store.getTurnPromptSummary(turn.turnId);
4456
4589
  if (!snapshot)
4457
4590
  return [];
4458
4591
  const graph = graphRows[offset + index];
@@ -4505,9 +4638,11 @@ export class ProxySessionController {
4505
4638
  const card = this.cardRenderer.renderResetHistoryCard({
4506
4639
  entries,
4507
4640
  footerLines: [
4508
- runningTurn
4509
- ? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
4510
- : `第 ${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`,
4511
4646
  ...(resettingTurnId ? ["正在 Reset 到所选轮次,请稍候…"] : []),
4512
4647
  ],
4513
4648
  pageActions,
@@ -4706,6 +4841,7 @@ export class ProxySessionController {
4706
4841
  const record = this.requireCurrentSession(contextKey);
4707
4842
  const filePath = resolveUserPath(requestedFilePath, record.cwd);
4708
4843
  await this.assertSendableFile(filePath);
4844
+ await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
4709
4845
  await this.outbound.sendFile(contextKey, filePath);
4710
4846
  }
4711
4847
  async listSessions(contextKey, searchTerm, options = {}) {
@@ -4996,7 +5132,7 @@ export class ProxySessionController {
4996
5132
  runtime,
4997
5133
  remote: options.forFork && runtime.readRemoteForkSource
4998
5134
  ? await runtime.readRemoteForkSource(remoteSessionId)
4999
- : await runtime.readRemoteSession(remoteSessionId),
5135
+ : await runtime.readRemoteSession(remoteSessionId, options.view ?? "latest"),
5000
5136
  };
5001
5137
  }));
5002
5138
  const matches = reads
@@ -5022,6 +5158,7 @@ export class ProxySessionController {
5022
5158
  if (direct?.status === "closed") {
5023
5159
  throw new Error(`找不到任务:${taskId}`);
5024
5160
  }
5161
+ await this.outbound.sendText(contextKey, SWITCH_TASK_STARTED_MESSAGE);
5025
5162
  const existing = direct ?? this.findStoredSessionByReference(taskId);
5026
5163
  if (existing) {
5027
5164
  const runtime = this.runtimes.forAgent(existing.agentName);
@@ -5106,6 +5243,9 @@ export class ProxySessionController {
5106
5243
  if (local && this.store.countQueuedPrompts(local.localSessionId) > 0) {
5107
5244
  throw new Error("任务仍有排队消息,无法归档。请先处理或取消排队消息。");
5108
5245
  }
5246
+ if (options.source !== "dismiss") {
5247
+ await this.outbound.sendText(contextKey, ARCHIVE_TASK_STARTED_MESSAGE);
5248
+ }
5109
5249
  const remoteReference = local
5110
5250
  ? remoteSessionReference(local.agentName, local.remoteSessionId)
5111
5251
  : taskId;
@@ -5313,28 +5453,32 @@ export class ProxySessionController {
5313
5453
  else {
5314
5454
  current = context.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
5315
5455
  }
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);
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;
5321
5462
  current = this.store.getSession(current.localSessionId) ?? current;
5322
5463
  }
5323
- }
5324
- catch (error) {
5325
- 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
+ }
5326
5469
  }
5327
5470
  }
5328
5471
  const localCurrent = current;
5329
- let remote;
5330
5472
  let goal;
5331
5473
  if (current?.remoteSessionId) {
5332
5474
  const runtime = this.runtimes.forAgent(current.agentName);
5333
- if (runtime.readRemoteSession) {
5475
+ if (!remote && runtime.readRemoteSession) {
5334
5476
  try {
5335
- remote = await runtime.readRemoteSession(current.remoteSessionId);
5477
+ remote = await runtime.readRemoteSession(current.remoteSessionId, "latest-full");
5336
5478
  }
5337
5479
  catch (error) {
5480
+ if (error instanceof CodexVersionError)
5481
+ throw error;
5338
5482
  this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task status.");
5339
5483
  }
5340
5484
  }
@@ -5430,7 +5574,7 @@ export class ProxySessionController {
5430
5574
  await this.outbound.sendInteractiveCard(contextKey, card);
5431
5575
  }
5432
5576
  async statusForCodexTask(contextKey, reference, options = {}) {
5433
- const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
5577
+ const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
5434
5578
  const actionReference = remoteSessionReference(agentName, remote.id);
5435
5579
  const sections = [
5436
5580
  {
@@ -5561,10 +5705,11 @@ export class ProxySessionController {
5561
5705
  throw new Error(`未知 agent:${agentName}`);
5562
5706
  return agent;
5563
5707
  }
5564
- async persistAgentExecutionDefaults(localSessionId) {
5565
- const session = this.store.getSession(localSessionId);
5566
- if (!session)
5708
+ async persistAgentExecutionDefaults(localSessionId, candidate) {
5709
+ const record = this.store.getSession(localSessionId);
5710
+ if (!record)
5567
5711
  throw new Error(`找不到任务:${localSessionId}`);
5712
+ const session = candidate ?? record;
5568
5713
  const agent = this.ensureAgent(session.agentName);
5569
5714
  const observed = {
5570
5715
  ...(session.modelProvider?.trim() ? { modelProvider: session.modelProvider } : {}),
@@ -5934,6 +6079,7 @@ export class ProxySessionController {
5934
6079
  const lockPath = runtime.getThreadWriterLockPath?.(threadId);
5935
6080
  if (!lockPath)
5936
6081
  throw new Error("当前 Agent 不支持自动关闭占用程序。");
6082
+ await this.outbound.sendText(contextKey, CLOSE_WRITER_STARTED_MESSAGE);
5937
6083
  let owners = await this.threadWriterProcesses.inspect(lockPath);
5938
6084
  const expectedOwner = owners.length === 1 && threadWriterMatches(owners[0], expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt) ? owners[0] : undefined;
5939
6085
  if (!expectedOwner) {
@@ -6259,10 +6405,6 @@ function bindSessionCardAction(action, bindings) {
6259
6405
  function runtimeErrorMessage(error) {
6260
6406
  return error instanceof Error ? error.message : String(error);
6261
6407
  }
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
6408
  function isUnmaterializedCodexThreadError(error) {
6267
6409
  const message = error instanceof Error ? error.message : String(error);
6268
6410
  return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);