@keyou007/agent-bot 0.1.16-alpha.2 → 0.1.16-alpha.4

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.
@@ -119,7 +119,7 @@ const SESSION_PROMPT_PREVIEW_LENGTH = 50;
119
119
  const DIRECTORY_PAGE_SIZE = 15;
120
120
  const RESET_HISTORY_PAGE_SIZE = 10;
121
121
  const MAX_LLM_TURN_RETRIES = 3;
122
- const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
122
+ const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
123
123
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
124
124
  const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
125
125
  const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
@@ -1912,6 +1912,7 @@ export class ProxySessionController {
1912
1912
  }
1913
1913
 
1914
1914
  private async runStartupRecovery(): Promise<void> {
1915
+ await this.finishPersistedCancellationRequests();
1915
1916
  await this.recoverShellCommandJobs();
1916
1917
  this.backfillTurnAttemptsForUpgrade();
1917
1918
  const attempts = this.store.listIncompleteTurnAttempts();
@@ -1942,6 +1943,40 @@ export class ProxySessionController {
1942
1943
  this.restorePersistedQueuedPrompts();
1943
1944
  }
1944
1945
 
1946
+ private async finishPersistedCancellationRequests(): Promise<void> {
1947
+ for (const attempt of this.store.listCancellationRequestedTurnAttempts()) {
1948
+ const session = this.store.getSession(attempt.localSessionId);
1949
+ if (!attempt.turnId) {
1950
+ this.store.updateTurnAttempt(attempt.attemptId, { status: "cancelled" });
1951
+ if (session?.status === "starting" || session?.status === "running") {
1952
+ this.store.updateSession(session.localSessionId, { status: "ready" });
1953
+ }
1954
+ continue;
1955
+ }
1956
+ if (session) {
1957
+ this.outbound.registerSession(
1958
+ session.localSessionId,
1959
+ attempt.contextKey,
1960
+ session.title,
1961
+ session.cwd,
1962
+ this.agentLabel(session.agentName),
1963
+ );
1964
+ }
1965
+ try {
1966
+ await this.handleRuntimeEvent({
1967
+ type: "turn_cancelled",
1968
+ sessionId: attempt.localSessionId,
1969
+ turnId: attempt.turnId,
1970
+ });
1971
+ } catch (error) {
1972
+ this.logger.warn(
1973
+ { error, attemptId: attempt.attemptId, turnId: attempt.turnId },
1974
+ "Failed to finish a persisted user-requested turn cancellation.",
1975
+ );
1976
+ }
1977
+ }
1978
+ }
1979
+
1945
1980
  private backfillTurnAttemptsForUpgrade(): void {
1946
1981
  for (const session of this.store.listAllSessions()) {
1947
1982
  if (session.status !== "starting" && session.status !== "running" && session.status !== "ready") continue;
@@ -2001,7 +2036,7 @@ export class ProxySessionController {
2001
2036
  session.localSessionId,
2002
2037
  attempt.contextKey,
2003
2038
  oldCardTurnId,
2004
- "执行中断已超过 5 分钟,未自动恢复。",
2039
+ "执行中断已超过 10 分钟,未自动恢复。",
2005
2040
  ).catch((error: unknown) => {
2006
2041
  this.logger.warn(
2007
2042
  { error, attemptId: attempt.attemptId, turnId: oldCardTurnId },
@@ -3082,28 +3117,80 @@ export class ProxySessionController {
3082
3117
  });
3083
3118
  }
3084
3119
  let turnId: string;
3120
+ let runtimeText = text;
3121
+ let cardPrompt = options.displayPrompt ?? text;
3122
+ let retriedStart = false;
3085
3123
  try {
3086
- const pendingTurnId = await this.outbound.startPendingTurn(
3087
- loaded.record.localSessionId,
3088
- loaded.record.contextKey,
3089
- title,
3090
- replyTarget,
3091
- options.displayPrompt ?? text,
3092
- );
3093
- this.store.updateTurnAttempt(attemptId, { pendingTurnId: pendingTurnId ?? null });
3094
- turnId = await loaded.runtime.startTurn(
3095
- loaded.record.localSessionId,
3096
- runtimePrompt(text, localImagePaths),
3097
- );
3098
- } catch (error) {
3099
- this.store.updateTurnAttempt(attemptId, {
3100
- status: options.preserveAttemptOnFailure ? "recovering" : "failed",
3101
- });
3102
- await this.outbound.failPendingTurn(
3103
- loaded.record.localSessionId,
3104
- error instanceof Error ? error.message : String(error),
3105
- );
3106
- throw error;
3124
+ while (true) {
3125
+ let pendingTurnId: string | undefined;
3126
+ try {
3127
+ pendingTurnId = await this.outbound.startPendingTurn(
3128
+ loaded.record.localSessionId,
3129
+ loaded.record.contextKey,
3130
+ title,
3131
+ replyTarget,
3132
+ cardPrompt,
3133
+ );
3134
+ } catch (error) {
3135
+ this.store.updateTurnAttempt(attemptId, {
3136
+ status: options.preserveAttemptOnFailure ? "recovering" : "failed",
3137
+ });
3138
+ throw error;
3139
+ }
3140
+ this.store.updateTurnAttempt(attemptId, { pendingTurnId: pendingTurnId ?? null });
3141
+
3142
+ try {
3143
+ turnId = await loaded.runtime.startTurn(
3144
+ loaded.record.localSessionId,
3145
+ runtimePrompt(runtimeText, localImagePaths),
3146
+ );
3147
+ break;
3148
+ } catch (error) {
3149
+ const message = error instanceof Error ? error.message : String(error);
3150
+ const attempt = this.store.getTurnAttempt(attemptId);
3151
+ const retryAttempt = attempt
3152
+ && isRetryableLlmTurnFailure(message)
3153
+ && attempt.retryCount < MAX_LLM_TURN_RETRIES
3154
+ ? this.store.prepareUnstartedTurnAttemptRetry(attemptId)
3155
+ : undefined;
3156
+ if (!retryAttempt) {
3157
+ const exhausted = attempt
3158
+ && isRetryableLlmTurnFailure(message)
3159
+ && attempt.retryCount >= MAX_LLM_TURN_RETRIES;
3160
+ const finalMessage = exhausted
3161
+ ? `${message}\n\n已自动重试 ${MAX_LLM_TURN_RETRIES} 次,仍未成功。`
3162
+ : message;
3163
+ this.store.updateTurnAttempt(attemptId, {
3164
+ status: options.preserveAttemptOnFailure ? "recovering" : "failed",
3165
+ });
3166
+ await this.outbound.failPendingTurn(loaded.record.localSessionId, finalMessage);
3167
+ if (finalMessage === message) throw error;
3168
+ throw new Error(finalMessage, { cause: error });
3169
+ }
3170
+
3171
+ retriedStart = true;
3172
+ this.llmRetryingSessions.add(loaded.record.localSessionId);
3173
+ this.store.audit(loaded.record.contextKey, "llm_turn_start_retry_started", {
3174
+ attemptId,
3175
+ retryNumber: retryAttempt.retryCount,
3176
+ maxRetries: MAX_LLM_TURN_RETRIES,
3177
+ error: message,
3178
+ });
3179
+ await this.outbound.failPendingTurn(
3180
+ loaded.record.localSessionId,
3181
+ `${message}\n\n检测到临时模型服务错误,正在自动重试(${retryAttempt.retryCount}/${MAX_LLM_TURN_RETRIES})。`,
3182
+ ).catch((presentationError: unknown) => {
3183
+ this.logger.warn(
3184
+ { error: presentationError, sessionId: loaded.record.localSessionId },
3185
+ "Failed to finalize the pending thinking card before an LLM start retry.",
3186
+ );
3187
+ });
3188
+ runtimeText = llmRetryRuntimePrompt(retryAttempt.promptText, retryAttempt.retryCount);
3189
+ cardPrompt = llmRetryCardPrompt(options.displayPrompt ?? retryAttempt.promptText, retryAttempt.retryCount);
3190
+ }
3191
+ }
3192
+ } finally {
3193
+ if (retriedStart) this.llmRetryingSessions.delete(loaded.record.localSessionId);
3107
3194
  }
3108
3195
  const attempt = this.store.getTurnAttempt(attemptId);
3109
3196
  this.store.updateTurnAttempt(attemptId, {
@@ -3956,7 +4043,13 @@ export class ProxySessionController {
3956
4043
  await this.outbound.sendText(record.contextKey, "当前没有正在执行的任务。");
3957
4044
  return;
3958
4045
  }
3959
- await runtime.interruptRemoteTurn(record.remoteSessionId, turnId);
4046
+ const cancellationAttempt = this.store.requestTurnAttemptCancellation(turnId);
4047
+ try {
4048
+ await runtime.interruptRemoteTurn(record.remoteSessionId, turnId);
4049
+ } catch (error) {
4050
+ if (cancellationAttempt) this.store.restoreTurnAttemptAfterCancellationFailure(cancellationAttempt);
4051
+ throw error;
4052
+ }
3960
4053
  this.store.audit(record.contextKey, "turn_interrupt_sent", {
3961
4054
  localSessionId: record.localSessionId,
3962
4055
  remoteSessionId: record.remoteSessionId,
@@ -3972,7 +4065,13 @@ export class ProxySessionController {
3972
4065
  await this.outbound.sendText(record.contextKey, "当前没有正在执行的任务。");
3973
4066
  return;
3974
4067
  }
3975
- await loaded.runtime.cancelTurn(record.localSessionId, turnId);
4068
+ const cancellationAttempt = this.store.requestTurnAttemptCancellation(turnId);
4069
+ try {
4070
+ await loaded.runtime.cancelTurn(record.localSessionId, turnId);
4071
+ } catch (error) {
4072
+ if (cancellationAttempt) this.store.restoreTurnAttemptAfterCancellationFailure(cancellationAttempt);
4073
+ throw error;
4074
+ }
3976
4075
  }
3977
4076
 
3978
4077
  private async resetCurrentSessionToTurn(
@@ -4851,7 +4950,43 @@ export class ProxySessionController {
4851
4950
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
4852
4951
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
4853
4952
  }
4854
- const allTurns = this.store.listTaskTurnGraph(current.localSessionId);
4953
+ const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
4954
+ const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
4955
+ const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
4956
+ && !completedTurnIds.has(current.lastTurnId)
4957
+ ? current.lastTurnId
4958
+ : undefined;
4959
+ const persistedRunningSnapshot = runningTurnId
4960
+ ? turnViewSnapshot(this.store.getTurnSnapshot(runningTurnId))
4961
+ : undefined;
4962
+ const runningAttempt = runningTurnId
4963
+ ? this.store.findIncompleteTurnAttemptByTurnId(runningTurnId)
4964
+ : undefined;
4965
+ const attemptStartedAt = Date.parse(runningAttempt?.createdAt ?? "");
4966
+ const runningSnapshot: TurnViewState | undefined = runningTurnId
4967
+ ? persistedRunningSnapshot ?? {
4968
+ sessionId: current.localSessionId,
4969
+ turnId: runningTurnId,
4970
+ prompt: runningAttempt?.promptText ?? current.title,
4971
+ status: "running",
4972
+ startedAt: Number.isFinite(attemptStartedAt) ? attemptStartedAt : Date.now(),
4973
+ assistantText: "",
4974
+ plan: [],
4975
+ activities: [],
4976
+ completedTools: [],
4977
+ failedTools: [],
4978
+ fileSummary: [],
4979
+ }
4980
+ : undefined;
4981
+ const runningTurn = runningTurnId && runningSnapshot
4982
+ ? {
4983
+ turnId: runningTurnId,
4984
+ parentTurnId: this.store.getTurnParent(runningTurnId, current.localSessionId),
4985
+ snapshot: runningSnapshot,
4986
+ updatedAt: runningAttempt?.updatedAt ?? new Date(runningSnapshot.startedAt).toISOString(),
4987
+ }
4988
+ : undefined;
4989
+ const allTurns = runningTurn ? [runningTurn, ...completedTurns] : completedTurns;
4855
4990
  const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
4856
4991
  turnId: turn.turnId,
4857
4992
  parentTurnId: turn.parentTurnId,
@@ -4870,16 +5005,18 @@ export class ProxySessionController {
4870
5005
  100,
4871
5006
  );
4872
5007
  const isCurrent = current.lastTurnId === turn.turnId && current.lastTurnStatus === "completed";
5008
+ const isRunning = runningTurnId === turn.turnId;
4873
5009
  return [{
4874
5010
  sequence: graph.sequence,
4875
5011
  graphNodeLine: graph.nodeLine,
4876
5012
  graphConnectorLine: graph.connectorLine,
4877
5013
  lines: [
4878
- `**${cardText(summary || "未记录对话内容")}**`,
4879
- `${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${cardCode(turn.turnId)}`,
5014
+ cardText(summary || "未记录对话内容"),
5015
+ `${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${cardText(turn.turnId)}`,
4880
5016
  ],
4881
5017
  current: isCurrent,
4882
- actions: isCurrent ? undefined : [{
5018
+ running: isRunning,
5019
+ actions: isCurrent || isRunning ? undefined : [{
4883
5020
  text: "Reset",
4884
5021
  value: {
4885
5022
  action: "turn_reset",
@@ -4915,7 +5052,9 @@ export class ProxySessionController {
4915
5052
  const card = this.cardRenderer.renderResetHistoryCard({
4916
5053
  entries,
4917
5054
  footerLines: [
4918
- `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
5055
+ runningTurn
5056
+ ? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
5057
+ : `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
4919
5058
  ],
4920
5059
  pageActions,
4921
5060
  });
@@ -5282,8 +5421,8 @@ export class ProxySessionController {
5282
5421
  reference: entry.reference,
5283
5422
  summary: `${offset + index + 1}. ${marker}${title} · ${entry.agentName}`,
5284
5423
  detailLines: [
5285
- cardText(lastUserPrompt),
5286
- `**更新时间**:${cardText(entry.updatedLabel)}`,
5424
+ `**最新 Prompt**:${cardText(lastUserPrompt)}`,
5425
+ `<font color='grey'>最近更新:${cardText(entry.updatedLabel)}</font>`,
5287
5426
  ],
5288
5427
  actions,
5289
5428
  current: entry.current,
@@ -6996,7 +7135,8 @@ function isRetryableLlmTurnFailure(message: string): boolean {
6996
7135
  /service unavailable|internal server error|bad gateway|gateway timeout/u,
6997
7136
  /(?:^|\D)(?:500|502|503|504)(?:\D|$)|upstream (?:error|failure|timeout)/u,
6998
7137
  /deadline exceeded|request timed out|request timeout|response timeout/u,
6999
- /connection (?:reset|closed|aborted)|network error|socket hang up/u,
7138
+ /connection (?:reset|closed|aborted|disconnected)|network error|socket hang up/u,
7139
+ /app server .*(?:closed|disconnected|exited|reset|aborted)/u,
7000
7140
  /stream (?:closed|disconnected|interrupted|error)/u,
7001
7141
  /\b(?:llm|model|provider|inference)\b.*\b(?:error|failed|failure|unavailable|timeout|timed out)\b/u,
7002
7142
  ].some((pattern) => pattern.test(normalized));
@@ -9,33 +9,6 @@ export class SessionMetadataHydrator {
9
9
  ) {}
10
10
 
11
11
  async hydrate(session: SessionRecord): Promise<SessionRecord> {
12
- if (
13
- session.runtimeKind === "codex"
14
- && session.remoteSessionId
15
- && session.lastTurnId
16
- && session.status === "running"
17
- && session.lastTurnStatus === "running"
18
- ) {
19
- const runtime = this.runtimes.forAgent(session.agentName);
20
- const loaded = runtime.getSession(session.localSessionId) ?? await runtime.resumeSession({
21
- localSessionId: session.localSessionId,
22
- remoteSessionId: session.remoteSessionId,
23
- activeTurnId: session.lastTurnId,
24
- lastTurnId: session.lastTurnId,
25
- lastTurnStatus: session.lastTurnStatus,
26
- agentName: session.agentName,
27
- cwd: session.cwd,
28
- title: session.title,
29
- model: session.model,
30
- reasoningEffort: session.reasoningEffort,
31
- permissionMode: session.permissionMode ?? "auto",
32
- });
33
- const synchronized = await runtime.synchronizeSession(loaded.localSessionId);
34
- if (synchronized.title) {
35
- this.store.updateRuntimeSession(session.localSessionId, { title: synchronized.title });
36
- }
37
- return this.store.getSession(session.localSessionId) ?? session;
38
- }
39
12
  if (session.title || !session.runtimeKind || !session.remoteSessionId) return session;
40
13
  if (session.runtimeKind === "codex" && !session.lastTurnId) return session;
41
14
  const metadata = await this.runtimes.forAgent(session.agentName).readSessionMetadata(session.remoteSessionId);
@@ -111,6 +111,7 @@ export type TurnAttemptStatus =
111
111
  | "accepted"
112
112
  | "running"
113
113
  | "recovering"
114
+ | "cancelling"
114
115
  | "completed"
115
116
  | "failed"
116
117
  | "cancelled"
@@ -633,6 +634,15 @@ export class StateStore {
633
634
  return rows.map(mapTurnAttempt);
634
635
  }
635
636
 
637
+ listCancellationRequestedTurnAttempts(): TurnAttemptRecord[] {
638
+ const rows = this.db.prepare(`
639
+ SELECT * FROM turn_attempts
640
+ WHERE status = 'cancelling'
641
+ ORDER BY created_at ASC, rowid ASC
642
+ `).all() as TurnAttemptRow[];
643
+ return rows.map(mapTurnAttempt);
644
+ }
645
+
636
646
  updateTurnAttempt(
637
647
  attemptId: string,
638
648
  patch: Partial<{
@@ -697,6 +707,19 @@ export class StateStore {
697
707
  return row ? mapTurnAttempt(row) : undefined;
698
708
  }
699
709
 
710
+ requestTurnAttemptCancellation(turnId: string): TurnAttemptRecord | undefined {
711
+ const existing = this.findIncompleteTurnAttemptByTurnId(turnId);
712
+ if (!existing) return undefined;
713
+ this.updateTurnAttempt(existing.attemptId, { status: "cancelling" });
714
+ return existing;
715
+ }
716
+
717
+ restoreTurnAttemptAfterCancellationFailure(attempt: TurnAttemptRecord): void {
718
+ const current = this.getTurnAttempt(attempt.attemptId);
719
+ if (current?.status !== "cancelling") return;
720
+ this.updateTurnAttempt(attempt.attemptId, { status: attempt.status });
721
+ }
722
+
700
723
  touchTurnAttempt(turnId: string, activityAt = new Date()): void {
701
724
  const timestamp = activityAt.toISOString();
702
725
  const throttleCutoff = new Date(activityAt.getTime() - 15_000).toISOString();
@@ -737,6 +760,17 @@ export class StateStore {
737
760
  return this.getTurnAttempt(attemptId);
738
761
  }
739
762
 
763
+ prepareUnstartedTurnAttemptRetry(attemptId: string): TurnAttemptRecord | undefined {
764
+ const existing = this.getTurnAttempt(attemptId);
765
+ if (!existing || existing.turnId || !isIncompleteTurnAttemptStatus(existing.status)) return undefined;
766
+ this.updateTurnAttempt(attemptId, {
767
+ pendingTurnId: null,
768
+ retryCount: existing.retryCount + 1,
769
+ status: "recovering",
770
+ });
771
+ return this.getTurnAttempt(attemptId);
772
+ }
773
+
740
774
  rebindPendingTurnMessages(localSessionId: string, sourceTurnId: string, targetTurnId: string): void {
741
775
  const now = new Date().toISOString();
742
776
  const rebind = this.db.transaction(() => {
@@ -1057,6 +1091,15 @@ export class StateStore {
1057
1091
  `).run(turnId, localSessionId, parentTurnId ?? null, new Date().toISOString());
1058
1092
  }
1059
1093
 
1094
+ getTurnParent(turnId: string, localSessionId: string): string | undefined {
1095
+ const row = this.db.prepare(`
1096
+ SELECT parent_turn_id
1097
+ FROM turn_parent_links
1098
+ WHERE turn_id = ? AND local_session_id = ?
1099
+ `).get(turnId, localSessionId) as { parent_turn_id: string | null } | undefined;
1100
+ return row?.parent_turn_id ?? undefined;
1101
+ }
1102
+
1060
1103
  findLatestCompletedTurnId(localSessionId: string, contextKey: string): string | undefined {
1061
1104
  const row = this.db
1062
1105
  .prepare(`