@keyou007/agent-bot 0.1.24-alpha.8 → 0.1.24

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.
@@ -21,6 +21,7 @@ import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
21
21
  import { errorLogValue } from "../logging/errorLogValue.js";
22
22
  import { createTurnViewState, hydrateTurnViewState } from "../presentation/TurnStateReducer.js";
23
23
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
24
+ import { displayRuntimeError, isResponseContinuationError } from "../runtime/responseErrors.js";
24
25
  import { isActiveShellCommandJob, ShellCommandJobManager, } from "../shell/ShellCommandJobManager.js";
25
26
  import { SessionOperationQueue } from "./SessionOperationQueue.js";
26
27
  import { createId } from "../utils/id.js";
@@ -245,6 +246,7 @@ export class ProxySessionController {
245
246
  lastSessionListings = new Map();
246
247
  threadInitializations = new Map();
247
248
  llmRetryingSessions = new Set();
249
+ responseContinuations = new Map();
248
250
  planHandoffs = new Map();
249
251
  retriedFailureTurnIds = new Set();
250
252
  queuedPromptRetryTimers = new Map();
@@ -3132,6 +3134,16 @@ export class ProxySessionController {
3132
3134
  }
3133
3135
  async startTurn(loaded, text, replyTarget, localImagePaths, options = {}) {
3134
3136
  return this.settingsOperations.run(loaded.record.localSessionId, async () => {
3137
+ if (options.responseContinuationTurnId) {
3138
+ const current = this.store.getSession(loaded.record.localSessionId);
3139
+ if (!current || current.status === "closed" || current.lastTurnId !== options.responseContinuationTurnId
3140
+ || this.sessionResets.has(current.localSessionId)
3141
+ || this.responseContinuations.get(current.localSessionId)?.turnId !== options.responseContinuationTurnId
3142
+ || this.store.getPendingProviderSettings(current.localSessionId)) {
3143
+ throw new Error("任务或运行设置已变化,未自动恢复模型续接。请发送消息继续。");
3144
+ }
3145
+ return this.startTurnNow(loaded, text, replyTarget, localImagePaths, options);
3146
+ }
3135
3147
  try {
3136
3148
  await this.applyPendingProviderSettings(loaded);
3137
3149
  }
@@ -3196,15 +3208,26 @@ export class ProxySessionController {
3196
3208
  || this.sessionResets.has(loaded.record.localSessionId))) {
3197
3209
  throw new Error("方案执行已取消。");
3198
3210
  }
3199
- turnId = await loaded.runtime.startTurn(loaded.record.localSessionId, options.approvedPlanTurnId
3200
- ? { text: runtimeText, collaborationMode: "default" }
3201
- : runtimePrompt(runtimeText, localImagePaths));
3211
+ if (options.responseContinuationTurnId) {
3212
+ const recovery = this.responseContinuations.get(loaded.record.localSessionId);
3213
+ if (recovery?.turnId !== options.responseContinuationTurnId
3214
+ || this.sessionResets.has(loaded.record.localSessionId) || !loaded.runtime.recoverResponseContinuation) {
3215
+ throw new Error("模型续接恢复已取消。");
3216
+ }
3217
+ turnId = await loaded.runtime.recoverResponseContinuation(loaded.record.localSessionId, options.responseContinuationTurnId, recovery.abort.signal);
3218
+ }
3219
+ else {
3220
+ turnId = await loaded.runtime.startTurn(loaded.record.localSessionId, options.approvedPlanTurnId
3221
+ ? { text: runtimeText, collaborationMode: "default" }
3222
+ : runtimePrompt(runtimeText, localImagePaths));
3223
+ }
3202
3224
  break;
3203
3225
  }
3204
3226
  catch (error) {
3205
3227
  const message = error instanceof Error ? error.message : String(error);
3206
3228
  const attempt = this.store.getTurnAttempt(attemptId);
3207
3229
  const retryAttempt = attempt
3230
+ && !options.responseContinuationTurnId
3208
3231
  && isRetryableLlmTurnFailure(message)
3209
3232
  && attempt.retryCount < MAX_LLM_TURN_RETRIES
3210
3233
  ? this.store.prepareUnstartedTurnAttemptRetry(attemptId)
@@ -3770,7 +3793,23 @@ export class ProxySessionController {
3770
3793
  if (event.type === "turn_failed") {
3771
3794
  if (this.retriedFailureTurnIds.has(event.turnId))
3772
3795
  return;
3773
- if (failedAttempt && isRetryableLlmTurnFailure(event.message) && failedAttempt.retryCount < MAX_LLM_TURN_RETRIES) {
3796
+ const source = this.store.getSession(event.sessionId);
3797
+ const responseContinuation = failedAttempt?.retryCount === 0 && failedAttempt.status !== "cancelling"
3798
+ && source && source.status !== "closed" && source.lastTurnId === event.turnId
3799
+ && !this.sessionResets.has(event.sessionId) && !this.store.getPendingProviderSettings(event.sessionId)
3800
+ && Boolean(this.runtimes.forAgent(source.agentName).recoverResponseContinuation)
3801
+ && isResponseContinuationError(event.message);
3802
+ if (failedAttempt && responseContinuation) {
3803
+ const retryAttempt = this.store.prepareTurnAttemptRetry(failedAttempt.attemptId, event.turnId);
3804
+ if (retryAttempt) {
3805
+ this.retriedFailureTurnIds.add(event.turnId);
3806
+ this.responseContinuations.set(event.sessionId, { turnId: event.turnId, abort: new AbortController() });
3807
+ await this.retryFailedLlmTurn(event, retryAttempt, true);
3808
+ return;
3809
+ }
3810
+ }
3811
+ if (failedAttempt && !this.responseContinuations.has(event.sessionId)
3812
+ && isRetryableLlmTurnFailure(event.message) && failedAttempt.retryCount < MAX_LLM_TURN_RETRIES) {
3774
3813
  const retryAttempt = this.store.prepareTurnAttemptRetry(failedAttempt.attemptId, event.turnId);
3775
3814
  if (retryAttempt) {
3776
3815
  this.retriedFailureTurnIds.add(event.turnId);
@@ -3923,7 +3962,7 @@ export class ProxySessionController {
3923
3962
  await this.scheduleNextQueuedPrompt(sessionId);
3924
3963
  }
3925
3964
  }
3926
- async retryFailedLlmTurn(event, attempt) {
3965
+ async retryFailedLlmTurn(event, attempt, responseContinuation = false) {
3927
3966
  const retryNumber = attempt.retryCount;
3928
3967
  const session = this.store.getSession(event.sessionId);
3929
3968
  if (!session || session.status === "closed") {
@@ -3939,13 +3978,16 @@ export class ProxySessionController {
3939
3978
  attemptId: attempt.attemptId,
3940
3979
  failedTurnId: event.turnId,
3941
3980
  retryNumber,
3942
- maxRetries: MAX_LLM_TURN_RETRIES,
3981
+ maxRetries: responseContinuation ? 1 : MAX_LLM_TURN_RETRIES,
3982
+ responseContinuation,
3943
3983
  error: event.message,
3944
3984
  });
3945
3985
  try {
3946
3986
  await this.outbound.onEvent({
3947
3987
  ...event,
3948
- message: `${event.message}\n\n检测到临时模型服务错误,正在自动重试(${retryNumber}/${MAX_LLM_TURN_RETRIES})。`,
3988
+ message: responseContinuation
3989
+ ? `${event.message}\n\n模型续接状态失效,正在恢复连接并继续(1/1)。`
3990
+ : `${event.message}\n\n检测到临时模型服务错误,正在自动重试(${retryNumber}/${MAX_LLM_TURN_RETRIES})。`,
3949
3991
  });
3950
3992
  }
3951
3993
  catch (error) {
@@ -3957,25 +3999,36 @@ export class ProxySessionController {
3957
3999
  try {
3958
4000
  const current = this.store.getSession(session.localSessionId) ?? session;
3959
4001
  const loaded = await this.loadSession(current, Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
3960
- const retryTurnId = await this.startTurn(loaded, llmRetryRuntimePrompt(attempt.promptText, retryNumber), replyTarget, attempt.localImagePaths, {
4002
+ const retryTurnId = await this.startTurn(loaded, responseContinuation ? "" : llmRetryRuntimePrompt(attempt.promptText, retryNumber), replyTarget, responseContinuation ? undefined : attempt.localImagePaths, {
3961
4003
  attemptId: attempt.attemptId,
3962
4004
  messageId: attempt.messageId,
3963
- displayPrompt: llmRetryCardPrompt(attempt.promptText, retryNumber),
4005
+ displayPrompt: responseContinuation ? `恢复模型续接:${attempt.promptText}` : llmRetryCardPrompt(attempt.promptText, retryNumber),
3964
4006
  preserveAttemptOnFailure: true,
4007
+ ...(responseContinuation ? { responseContinuationTurnId: event.turnId } : {}),
3965
4008
  });
3966
4009
  this.store.rebindPendingTurnMessages(session.localSessionId, event.turnId, retryTurnId);
3967
4010
  }
3968
4011
  catch (error) {
3969
- this.store.updateTurnAttempt(attempt.attemptId, { status: "failed" });
3970
- this.store.updateSession(session.localSessionId, { status: "failed" });
3971
- await this.finalizeTurnMessageReactions(event.turnId, "failed").catch((reactionError) => {
4012
+ const aborted = responseContinuation && !this.responseContinuations.has(session.localSessionId);
4013
+ this.store.updateTurnAttempt(attempt.attemptId, { status: aborted ? "cancelled" : "failed" });
4014
+ const latest = this.store.getSession(session.localSessionId);
4015
+ if (!responseContinuation || (latest?.lastTurnId === event.turnId && latest.status !== "closed")) {
4016
+ this.store.updateSession(session.localSessionId, { status: aborted ? "ready" : "failed" });
4017
+ }
4018
+ await this.finalizeTurnMessageReactions(event.turnId, aborted ? "cancelled" : "failed").catch((reactionError) => {
3972
4019
  this.logger.warn({ error: reactionError, sessionId: session.localSessionId, turnId: event.turnId }, "Failed to finalize reactions after an LLM retry could not start.");
3973
4020
  });
3974
4021
  this.logger.error({ error, sessionId: session.localSessionId, retryNumber }, "Failed to start an automatic LLM turn retry.");
4022
+ if (responseContinuation && !aborted) {
4023
+ await this.outbound.sendText(attempt.contextKey, `模型续接恢复未能启动:${displayRuntimeError(runtimeErrorMessage(error))}。请发送消息继续。`);
4024
+ }
3975
4025
  await this.scheduleNextQueuedPrompt(session.localSessionId);
3976
4026
  }
3977
4027
  finally {
3978
4028
  this.llmRetryingSessions.delete(session.localSessionId);
4029
+ if (this.responseContinuations.get(session.localSessionId)?.turnId === event.turnId) {
4030
+ this.responseContinuations.delete(session.localSessionId);
4031
+ }
3979
4032
  }
3980
4033
  }
3981
4034
  scheduleNextQueuedPrompt(sessionId) {
@@ -4213,6 +4266,8 @@ export class ProxySessionController {
4213
4266
  await this.cancelSession(record);
4214
4267
  }
4215
4268
  async cancelSession(record) {
4269
+ this.responseContinuations.get(record.localSessionId)?.abort.abort();
4270
+ this.responseContinuations.delete(record.localSessionId);
4216
4271
  this.planHandoffs.delete(record.localSessionId);
4217
4272
  const runtime = this.runtimes.forAgent(record.agentName);
4218
4273
  if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
@@ -4290,6 +4345,8 @@ export class ProxySessionController {
4290
4345
  });
4291
4346
  }
4292
4347
  async runSessionReset(sessionId, operation) {
4348
+ this.responseContinuations.get(sessionId)?.abort.abort();
4349
+ this.responseContinuations.delete(sessionId);
4293
4350
  this.planHandoffs.delete(sessionId);
4294
4351
  const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
4295
4352
  const reset = previous.catch(() => undefined).then(operation);
@@ -4671,9 +4728,16 @@ export class ProxySessionController {
4671
4728
  throw new Error("当前运行时不支持 Provider 设置。");
4672
4729
  loaded.session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
4673
4730
  const remoteSessionId = loaded.session.remoteSessionId;
4674
- const persist = (candidate) => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
4731
+ const persist = (candidate, replacement) => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
4675
4732
  if (candidate.remoteSessionId !== remoteSessionId) {
4676
- throw new Error("设置切换返回了不同的任务 ID,未保存设置;不允许替换任务或更改历史。");
4733
+ const saved = this.store.getSession(loaded.record.localSessionId);
4734
+ if (replacement?.emptyRemoteSessionId !== remoteSessionId || !saved || saved.remoteSessionId !== remoteSessionId
4735
+ || saved.status === "closed" || saved.status === "running" || saved.lastTurnId || loaded.session.activeTurnId || candidate.activeTurnId
4736
+ || candidate.localSessionId !== saved.localSessionId || !candidate.remoteSessionId
4737
+ || candidate.agentName !== saved.agentName || candidate.cwd !== saved.cwd || candidate.runtimeKind !== loaded.session.runtimeKind
4738
+ || this.store.hasTurnAttemptsForSession(saved.localSessionId) || this.store.getForkHistorySource(saved.localSessionId)) {
4739
+ throw new Error("设置切换返回了不同的任务 ID,未保存设置;仅允许已验证的空会话更换底层任务。");
4740
+ }
4677
4741
  }
4678
4742
  assertProviderSettingsApplied(candidate, settings);
4679
4743
  const agent = this.ensureAgent(loaded.record.agentName);
@@ -4681,6 +4745,7 @@ export class ProxySessionController {
4681
4745
  await this.persistAgentExecutionDefaultsNow(loaded.record.localSessionId, candidate);
4682
4746
  try {
4683
4747
  this.store.updateRuntimeSession(loaded.record.localSessionId, {
4748
+ remoteSessionId: candidate.remoteSessionId,
4684
4749
  modelProvider: candidate.modelProvider,
4685
4750
  model: candidate.model,
4686
4751
  reasoningEffort: candidate.reasoningEffort,
@@ -4694,6 +4759,7 @@ export class ProxySessionController {
4694
4759
  }
4695
4760
  });
4696
4761
  const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
4762
+ loaded.record = this.store.getSession(loaded.record.localSessionId) ?? loaded.record;
4697
4763
  this.settingsOnlySessions.delete(loaded.record.localSessionId);
4698
4764
  this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
4699
4765
  return session;
@@ -5858,6 +5924,7 @@ export class ProxySessionController {
5858
5924
  throw new Error(`这不是目录:${directory}`);
5859
5925
  }
5860
5926
  async createDirectoryFromCard(contextKey, action) {
5927
+ const updateMessageId = requiredCardMessageId(action.messageId);
5861
5928
  const directory = directoryActionPath(action.value.directory);
5862
5929
  await this.assertBrowsableDirectory(directory);
5863
5930
  const folderName = directoryFolderName(cardFormValue(action.value, "folderName"));
@@ -5873,9 +5940,9 @@ export class ProxySessionController {
5873
5940
  throw new Error(`目录已存在:${newDirectory}`);
5874
5941
  throw new Error(`无法创建目录 ${newDirectory}:${runtimeErrorMessage(error)}`);
5875
5942
  }
5876
- await this.openDirectoryBrowser(contextKey, directory, {
5877
- updateMessageId: requiredCardMessageId(action.messageId),
5878
- page: directoryPageValue(action.value.page),
5943
+ await this.openDirectoryBrowser(contextKey, newDirectory, {
5944
+ updateMessageId,
5945
+ page: 0,
5879
5946
  });
5880
5947
  await this.outbound.sendText(contextKey, `已创建目录:${newDirectory}`);
5881
5948
  }