@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.
@@ -28,7 +28,7 @@ const SESSION_PROMPT_PREVIEW_LENGTH = 50;
28
28
  const DIRECTORY_PAGE_SIZE = 15;
29
29
  const RESET_HISTORY_PAGE_SIZE = 10;
30
30
  const MAX_LLM_TURN_RETRIES = 3;
31
- const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
31
+ const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
32
32
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
33
33
  const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
34
34
  const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
@@ -1439,6 +1439,7 @@ export class ProxySessionController {
1439
1439
  return recovery;
1440
1440
  }
1441
1441
  async runStartupRecovery() {
1442
+ await this.finishPersistedCancellationRequests();
1442
1443
  await this.recoverShellCommandJobs();
1443
1444
  this.backfillTurnAttemptsForUpgrade();
1444
1445
  const attempts = this.store.listIncompleteTurnAttempts();
@@ -1464,6 +1465,31 @@ export class ProxySessionController {
1464
1465
  }
1465
1466
  this.restorePersistedQueuedPrompts();
1466
1467
  }
1468
+ async finishPersistedCancellationRequests() {
1469
+ for (const attempt of this.store.listCancellationRequestedTurnAttempts()) {
1470
+ const session = this.store.getSession(attempt.localSessionId);
1471
+ if (!attempt.turnId) {
1472
+ this.store.updateTurnAttempt(attempt.attemptId, { status: "cancelled" });
1473
+ if (session?.status === "starting" || session?.status === "running") {
1474
+ this.store.updateSession(session.localSessionId, { status: "ready" });
1475
+ }
1476
+ continue;
1477
+ }
1478
+ if (session) {
1479
+ this.outbound.registerSession(session.localSessionId, attempt.contextKey, session.title, session.cwd, this.agentLabel(session.agentName));
1480
+ }
1481
+ try {
1482
+ await this.handleRuntimeEvent({
1483
+ type: "turn_cancelled",
1484
+ sessionId: attempt.localSessionId,
1485
+ turnId: attempt.turnId,
1486
+ });
1487
+ }
1488
+ catch (error) {
1489
+ this.logger.warn({ error, attemptId: attempt.attemptId, turnId: attempt.turnId }, "Failed to finish a persisted user-requested turn cancellation.");
1490
+ }
1491
+ }
1492
+ }
1467
1493
  backfillTurnAttemptsForUpgrade() {
1468
1494
  for (const session of this.store.listAllSessions()) {
1469
1495
  if (session.status !== "starting" && session.status !== "running" && session.status !== "ready")
@@ -1521,7 +1547,7 @@ export class ProxySessionController {
1521
1547
  const session = this.store.getSession(attempt.localSessionId);
1522
1548
  const oldCardTurnId = attempt.pendingTurnId ?? attempt.turnId;
1523
1549
  if (session && oldCardTurnId && this.outbound.canRoute(attempt.contextKey)) {
1524
- await this.outbound.interruptTurnForRecovery(session.localSessionId, attempt.contextKey, oldCardTurnId, "执行中断已超过 5 分钟,未自动恢复。").catch((error) => {
1550
+ await this.outbound.interruptTurnForRecovery(session.localSessionId, attempt.contextKey, oldCardTurnId, "执行中断已超过 10 分钟,未自动恢复。").catch((error) => {
1525
1551
  this.logger.warn({ error, attemptId: attempt.attemptId, turnId: oldCardTurnId }, "Failed to close an expired interrupted thinking card.");
1526
1552
  });
1527
1553
  }
@@ -2382,17 +2408,68 @@ export class ProxySessionController {
2382
2408
  });
2383
2409
  }
2384
2410
  let turnId;
2411
+ let runtimeText = text;
2412
+ let cardPrompt = options.displayPrompt ?? text;
2413
+ let retriedStart = false;
2385
2414
  try {
2386
- const pendingTurnId = await this.outbound.startPendingTurn(loaded.record.localSessionId, loaded.record.contextKey, title, replyTarget, options.displayPrompt ?? text);
2387
- this.store.updateTurnAttempt(attemptId, { pendingTurnId: pendingTurnId ?? null });
2388
- turnId = await loaded.runtime.startTurn(loaded.record.localSessionId, runtimePrompt(text, localImagePaths));
2415
+ while (true) {
2416
+ let pendingTurnId;
2417
+ try {
2418
+ pendingTurnId = await this.outbound.startPendingTurn(loaded.record.localSessionId, loaded.record.contextKey, title, replyTarget, cardPrompt);
2419
+ }
2420
+ catch (error) {
2421
+ this.store.updateTurnAttempt(attemptId, {
2422
+ status: options.preserveAttemptOnFailure ? "recovering" : "failed",
2423
+ });
2424
+ throw error;
2425
+ }
2426
+ this.store.updateTurnAttempt(attemptId, { pendingTurnId: pendingTurnId ?? null });
2427
+ try {
2428
+ turnId = await loaded.runtime.startTurn(loaded.record.localSessionId, runtimePrompt(runtimeText, localImagePaths));
2429
+ break;
2430
+ }
2431
+ catch (error) {
2432
+ const message = error instanceof Error ? error.message : String(error);
2433
+ const attempt = this.store.getTurnAttempt(attemptId);
2434
+ const retryAttempt = attempt
2435
+ && isRetryableLlmTurnFailure(message)
2436
+ && attempt.retryCount < MAX_LLM_TURN_RETRIES
2437
+ ? this.store.prepareUnstartedTurnAttemptRetry(attemptId)
2438
+ : undefined;
2439
+ if (!retryAttempt) {
2440
+ const exhausted = attempt
2441
+ && isRetryableLlmTurnFailure(message)
2442
+ && attempt.retryCount >= MAX_LLM_TURN_RETRIES;
2443
+ const finalMessage = exhausted
2444
+ ? `${message}\n\n已自动重试 ${MAX_LLM_TURN_RETRIES} 次,仍未成功。`
2445
+ : message;
2446
+ this.store.updateTurnAttempt(attemptId, {
2447
+ status: options.preserveAttemptOnFailure ? "recovering" : "failed",
2448
+ });
2449
+ await this.outbound.failPendingTurn(loaded.record.localSessionId, finalMessage);
2450
+ if (finalMessage === message)
2451
+ throw error;
2452
+ throw new Error(finalMessage, { cause: error });
2453
+ }
2454
+ retriedStart = true;
2455
+ this.llmRetryingSessions.add(loaded.record.localSessionId);
2456
+ this.store.audit(loaded.record.contextKey, "llm_turn_start_retry_started", {
2457
+ attemptId,
2458
+ retryNumber: retryAttempt.retryCount,
2459
+ maxRetries: MAX_LLM_TURN_RETRIES,
2460
+ error: message,
2461
+ });
2462
+ await this.outbound.failPendingTurn(loaded.record.localSessionId, `${message}\n\n检测到临时模型服务错误,正在自动重试(${retryAttempt.retryCount}/${MAX_LLM_TURN_RETRIES})。`).catch((presentationError) => {
2463
+ this.logger.warn({ error: presentationError, sessionId: loaded.record.localSessionId }, "Failed to finalize the pending thinking card before an LLM start retry.");
2464
+ });
2465
+ runtimeText = llmRetryRuntimePrompt(retryAttempt.promptText, retryAttempt.retryCount);
2466
+ cardPrompt = llmRetryCardPrompt(options.displayPrompt ?? retryAttempt.promptText, retryAttempt.retryCount);
2467
+ }
2468
+ }
2389
2469
  }
2390
- catch (error) {
2391
- this.store.updateTurnAttempt(attemptId, {
2392
- status: options.preserveAttemptOnFailure ? "recovering" : "failed",
2393
- });
2394
- await this.outbound.failPendingTurn(loaded.record.localSessionId, error instanceof Error ? error.message : String(error));
2395
- throw error;
2470
+ finally {
2471
+ if (retriedStart)
2472
+ this.llmRetryingSessions.delete(loaded.record.localSessionId);
2396
2473
  }
2397
2474
  const attempt = this.store.getTurnAttempt(attemptId);
2398
2475
  this.store.updateTurnAttempt(attemptId, {
@@ -3043,7 +3120,15 @@ export class ProxySessionController {
3043
3120
  await this.outbound.sendText(record.contextKey, "当前没有正在执行的任务。");
3044
3121
  return;
3045
3122
  }
3046
- await runtime.interruptRemoteTurn(record.remoteSessionId, turnId);
3123
+ const cancellationAttempt = this.store.requestTurnAttemptCancellation(turnId);
3124
+ try {
3125
+ await runtime.interruptRemoteTurn(record.remoteSessionId, turnId);
3126
+ }
3127
+ catch (error) {
3128
+ if (cancellationAttempt)
3129
+ this.store.restoreTurnAttemptAfterCancellationFailure(cancellationAttempt);
3130
+ throw error;
3131
+ }
3047
3132
  this.store.audit(record.contextKey, "turn_interrupt_sent", {
3048
3133
  localSessionId: record.localSessionId,
3049
3134
  remoteSessionId: record.remoteSessionId,
@@ -3058,7 +3143,15 @@ export class ProxySessionController {
3058
3143
  await this.outbound.sendText(record.contextKey, "当前没有正在执行的任务。");
3059
3144
  return;
3060
3145
  }
3061
- await loaded.runtime.cancelTurn(record.localSessionId, turnId);
3146
+ const cancellationAttempt = this.store.requestTurnAttemptCancellation(turnId);
3147
+ try {
3148
+ await loaded.runtime.cancelTurn(record.localSessionId, turnId);
3149
+ }
3150
+ catch (error) {
3151
+ if (cancellationAttempt)
3152
+ this.store.restoreTurnAttemptAfterCancellationFailure(cancellationAttempt);
3153
+ throw error;
3154
+ }
3062
3155
  }
3063
3156
  async resetCurrentSessionToTurn(contextKey, sessionId, turnId) {
3064
3157
  if (!sessionId || !turnId)
@@ -3797,7 +3890,43 @@ export class ProxySessionController {
3797
3890
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
3798
3891
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
3799
3892
  }
3800
- const allTurns = this.store.listTaskTurnGraph(current.localSessionId);
3893
+ const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
3894
+ const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
3895
+ const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
3896
+ && !completedTurnIds.has(current.lastTurnId)
3897
+ ? current.lastTurnId
3898
+ : undefined;
3899
+ const persistedRunningSnapshot = runningTurnId
3900
+ ? turnViewSnapshot(this.store.getTurnSnapshot(runningTurnId))
3901
+ : undefined;
3902
+ const runningAttempt = runningTurnId
3903
+ ? this.store.findIncompleteTurnAttemptByTurnId(runningTurnId)
3904
+ : undefined;
3905
+ const attemptStartedAt = Date.parse(runningAttempt?.createdAt ?? "");
3906
+ const runningSnapshot = runningTurnId
3907
+ ? persistedRunningSnapshot ?? {
3908
+ sessionId: current.localSessionId,
3909
+ turnId: runningTurnId,
3910
+ prompt: runningAttempt?.promptText ?? current.title,
3911
+ status: "running",
3912
+ startedAt: Number.isFinite(attemptStartedAt) ? attemptStartedAt : Date.now(),
3913
+ assistantText: "",
3914
+ plan: [],
3915
+ activities: [],
3916
+ completedTools: [],
3917
+ failedTools: [],
3918
+ fileSummary: [],
3919
+ }
3920
+ : undefined;
3921
+ const runningTurn = runningTurnId && runningSnapshot
3922
+ ? {
3923
+ turnId: runningTurnId,
3924
+ parentTurnId: this.store.getTurnParent(runningTurnId, current.localSessionId),
3925
+ snapshot: runningSnapshot,
3926
+ updatedAt: runningAttempt?.updatedAt ?? new Date(runningSnapshot.startedAt).toISOString(),
3927
+ }
3928
+ : undefined;
3929
+ const allTurns = runningTurn ? [runningTurn, ...completedTurns] : completedTurns;
3801
3930
  const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
3802
3931
  turnId: turn.turnId,
3803
3932
  parentTurnId: turn.parentTurnId,
@@ -3814,16 +3943,18 @@ export class ProxySessionController {
3814
3943
  const graph = graphRows[offset + index];
3815
3944
  const summary = truncateText((snapshot.prompt ?? snapshot.taskTitle ?? "未记录对话内容").replace(/\s+/g, " ").trim(), 100);
3816
3945
  const isCurrent = current.lastTurnId === turn.turnId && current.lastTurnStatus === "completed";
3946
+ const isRunning = runningTurnId === turn.turnId;
3817
3947
  return [{
3818
3948
  sequence: graph.sequence,
3819
3949
  graphNodeLine: graph.nodeLine,
3820
3950
  graphConnectorLine: graph.connectorLine,
3821
3951
  lines: [
3822
- `**${cardText(summary || "未记录对话内容")}**`,
3823
- `${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${cardCode(turn.turnId)}`,
3952
+ cardText(summary || "未记录对话内容"),
3953
+ `${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${cardText(turn.turnId)}`,
3824
3954
  ],
3825
3955
  current: isCurrent,
3826
- actions: isCurrent ? undefined : [{
3956
+ running: isRunning,
3957
+ actions: isCurrent || isRunning ? undefined : [{
3827
3958
  text: "Reset",
3828
3959
  value: {
3829
3960
  action: "turn_reset",
@@ -3859,7 +3990,9 @@ export class ProxySessionController {
3859
3990
  const card = this.cardRenderer.renderResetHistoryCard({
3860
3991
  entries,
3861
3992
  footerLines: [
3862
- `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
3993
+ runningTurn
3994
+ ? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
3995
+ : `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
3863
3996
  ],
3864
3997
  pageActions,
3865
3998
  });
@@ -4202,8 +4335,8 @@ export class ProxySessionController {
4202
4335
  reference: entry.reference,
4203
4336
  summary: `${offset + index + 1}. ${marker}${title} · ${entry.agentName}`,
4204
4337
  detailLines: [
4205
- cardText(lastUserPrompt),
4206
- `**更新时间**:${cardText(entry.updatedLabel)}`,
4338
+ `**最新 Prompt**:${cardText(lastUserPrompt)}`,
4339
+ `<font color='grey'>最近更新:${cardText(entry.updatedLabel)}</font>`,
4207
4340
  ],
4208
4341
  actions,
4209
4342
  current: entry.current,
@@ -5716,7 +5849,8 @@ function isRetryableLlmTurnFailure(message) {
5716
5849
  /service unavailable|internal server error|bad gateway|gateway timeout/u,
5717
5850
  /(?:^|\D)(?:500|502|503|504)(?:\D|$)|upstream (?:error|failure|timeout)/u,
5718
5851
  /deadline exceeded|request timed out|request timeout|response timeout/u,
5719
- /connection (?:reset|closed|aborted)|network error|socket hang up/u,
5852
+ /connection (?:reset|closed|aborted|disconnected)|network error|socket hang up/u,
5853
+ /app server .*(?:closed|disconnected|exited|reset|aborted)/u,
5720
5854
  /stream (?:closed|disconnected|interrupted|error)/u,
5721
5855
  /\b(?:llm|model|provider|inference)\b.*\b(?:error|failed|failure|unavailable|timeout|timed out)\b/u,
5722
5856
  ].some((pattern) => pattern.test(normalized));