@keyou007/agent-bot 0.1.24-alpha.4 → 0.1.24-alpha.5

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 (40) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +15 -3
  3. package/README.zh.md +15 -3
  4. package/dist/codex/AppServerPermissions.js +34 -0
  5. package/dist/codex/AppServerPermissions.js.map +1 -0
  6. package/dist/codex/CodexLocalActivityDetector.js +37 -5
  7. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  8. package/dist/codex/CodexRuntime.js +37 -13
  9. package/dist/codex/CodexRuntime.js.map +1 -1
  10. package/dist/codex/ThreadWriterProcess.js +19 -5
  11. package/dist/codex/ThreadWriterProcess.js.map +1 -1
  12. package/dist/feishu/CardRenderer.js +80 -64
  13. package/dist/feishu/CardRenderer.js.map +1 -1
  14. package/dist/local-files/CsvPreview.js +148 -0
  15. package/dist/local-files/CsvPreview.js.map +1 -0
  16. package/dist/local-files/LocalFileViewerServer.js +53 -10
  17. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  18. package/dist/local-files/TurnPreviewPage.js +122 -11
  19. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  20. package/dist/presentation/TurnStateReducer.js +39 -0
  21. package/dist/presentation/TurnStateReducer.js.map +1 -1
  22. package/dist/proxy/ProxySessionController.js +152 -46
  23. package/dist/proxy/ProxySessionController.js.map +1 -1
  24. package/docs/technical-reference.md +5 -3
  25. package/docs/technical-reference.zh.md +4 -2
  26. package/npm-shrinkwrap.json +2 -2
  27. package/package.json +1 -1
  28. package/skills/agent-bot/SKILL.md +6 -0
  29. package/src/codex/AppServerPermissions.ts +39 -0
  30. package/src/codex/CodexLocalActivityDetector.ts +34 -4
  31. package/src/codex/CodexRuntime.ts +44 -17
  32. package/src/codex/ThreadWriterProcess.ts +17 -9
  33. package/src/feishu/CardRenderer.ts +78 -64
  34. package/src/local-files/CsvPreview.ts +132 -0
  35. package/src/local-files/LocalFileViewerServer.ts +54 -11
  36. package/src/local-files/TurnPreviewPage.ts +125 -12
  37. package/src/presentation/TurnStateReducer.ts +41 -0
  38. package/src/presentation/turnViewTypes.ts +4 -0
  39. package/src/proxy/ProxySessionController.ts +137 -48
  40. package/src/runtime/types.ts +2 -0
@@ -193,6 +193,7 @@ const REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS = 30_000;
193
193
  const MAX_LLM_TURN_RETRIES = 3;
194
194
  const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
195
195
  const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
196
+ const RECOVERY_RETRY_WINDOW_MS = 60_000;
196
197
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
197
198
  const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
198
199
  const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
@@ -383,7 +384,6 @@ interface AgentRemoteSessionSummary {
383
384
 
384
385
  interface SessionsCardOptions {
385
386
  updateMessageId?: string;
386
- forceSwitchTaskId?: string;
387
387
  page?: number;
388
388
  }
389
389
 
@@ -554,6 +554,9 @@ export class ProxySessionController {
554
554
  private readonly retriedFailureTurnIds = new Set<string>();
555
555
  private readonly queuedPromptRetryTimers = new Map<string, NodeJS.Timeout>();
556
556
  private readonly recoveryRetryTimers = new Map<string, NodeJS.Timeout>();
557
+ private readonly recoveryRetryStartedAt = new Map<string, number>();
558
+ private readonly recoveredLiveAttempts = new Set<string>();
559
+ private recoveryClosed = false;
557
560
  private readonly pendingMergedForwards = new Map<string, PendingForwardAttachment>();
558
561
  private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
559
562
  private readonly relatedReactionMessageIds = new Map<string, string[]>();
@@ -1208,7 +1211,7 @@ export class ProxySessionController {
1208
1211
  await this.stopSessionReference(contextKey, sessionId);
1209
1212
  if (scopedAction.value.cardView === "status") await this.refreshStatusCardFromAction(scopedAction, sessionId);
1210
1213
  else if (scopedAction.value.cardView !== "safe_restart") {
1211
- await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
1214
+ await this.refreshSessionsCardFromAction(scopedAction);
1212
1215
  }
1213
1216
  } else if (kind === "session_turns" || kind === "session_turns_page") {
1214
1217
  const sessionReference = String(scopedAction.value.sessionId ?? "").trim();
@@ -1353,6 +1356,7 @@ export class ProxySessionController {
1353
1356
  }
1354
1357
 
1355
1358
  close(): void {
1359
+ this.recoveryClosed = true;
1356
1360
  for (const unsubscribe of this.unsubscribe) unsubscribe();
1357
1361
  this.unsubscribe.length = 0;
1358
1362
  this.lastSessionListings.clear();
@@ -1372,6 +1376,8 @@ export class ProxySessionController {
1372
1376
  this.queuedPromptRetryTimers.clear();
1373
1377
  for (const timer of this.recoveryRetryTimers.values()) clearTimeout(timer);
1374
1378
  this.recoveryRetryTimers.clear();
1379
+ this.recoveryRetryStartedAt.clear();
1380
+ this.recoveredLiveAttempts.clear();
1375
1381
  for (const schedule of this.appServerReleaseSchedules.values()) {
1376
1382
  if (schedule.timer) clearTimeout(schedule.timer);
1377
1383
  }
@@ -2294,14 +2300,13 @@ export class ProxySessionController {
2294
2300
  continue;
2295
2301
  }
2296
2302
  if (!this.outbound.canRoute(attempt.contextKey)) continue;
2303
+ this.recoveryRetryStartedAt.set(attempt.attemptId, Date.now());
2297
2304
  try {
2298
2305
  await this.recoverTurnAttempt(attempt);
2299
2306
  } catch (error) {
2300
- this.logger.error(
2301
- { error, attemptId: attempt.attemptId, sessionId: attempt.localSessionId },
2302
- "Failed to recover an unfinished task; it will be retried while the server is running.",
2303
- );
2304
- this.scheduleRecoveryRetry(attempt.attemptId);
2307
+ await this.handleRecoveryError(attempt.attemptId, error);
2308
+ } finally {
2309
+ if (!this.recoveryRetryTimers.has(attempt.attemptId)) this.recoveryRetryStartedAt.delete(attempt.attemptId);
2305
2310
  }
2306
2311
  }
2307
2312
  this.restorePersistedQueuedPrompts();
@@ -2394,15 +2399,23 @@ export class ProxySessionController {
2394
2399
  }
2395
2400
  }
2396
2401
 
2397
- private async expireStaleTurnAttempt(attempt: TurnAttemptRecord): Promise<void> {
2398
- const session = this.store.getSession(attempt.localSessionId);
2402
+ private async expireStaleTurnAttempt(
2403
+ attempt: TurnAttemptRecord,
2404
+ message = "执行中断已超过 10 分钟,未自动恢复。",
2405
+ ): Promise<void> {
2406
+ clearTimeout(this.recoveryRetryTimers.get(attempt.attemptId));
2407
+ this.recoveryRetryTimers.delete(attempt.attemptId);
2408
+ this.recoveryRetryStartedAt.delete(attempt.attemptId);
2409
+ this.recoveredLiveAttempts.delete(attempt.attemptId);
2410
+ let session = this.store.getSession(attempt.localSessionId);
2411
+ this.store.updateTurnAttempt(attempt.attemptId, { status: "interrupted" });
2399
2412
  const oldCardTurnId = attempt.pendingTurnId ?? attempt.turnId;
2400
2413
  if (session && oldCardTurnId && this.outbound.canRoute(attempt.contextKey)) {
2401
2414
  await this.outbound.interruptTurnForRecovery(
2402
2415
  session.localSessionId,
2403
2416
  attempt.contextKey,
2404
2417
  oldCardTurnId,
2405
- "执行中断已超过 10 分钟,未自动恢复。",
2418
+ message,
2406
2419
  ).catch((error: unknown) => {
2407
2420
  this.logger.warn(
2408
2421
  { error, attemptId: attempt.attemptId, turnId: oldCardTurnId },
@@ -2410,13 +2423,21 @@ export class ProxySessionController {
2410
2423
  );
2411
2424
  });
2412
2425
  }
2413
- this.store.updateTurnAttempt(attempt.attemptId, { status: "interrupted" });
2426
+ session = this.store.getSession(attempt.localSessionId);
2414
2427
  if (!session || session.status === "closed") return;
2415
- if (attempt.turnId && session.lastTurnId === attempt.turnId) {
2428
+ const runtime = this.runtimes.forAgent(session.agentName);
2429
+ const activeTurnId = runtime.getSession(session.localSessionId)?.activeTurnId;
2430
+ const sameTurn = session.lastTurnId === attempt.turnId
2431
+ || session.lastTurnId === attempt.recoveredFromTurnId
2432
+ || !session.lastTurnId;
2433
+ if (sameTurn && session.lastTurnStatus !== "completed" && session.lastTurnStatus !== "failed"
2434
+ && (!activeTurnId || activeTurnId === attempt.turnId)) {
2435
+ // Forget monitoring, not the remote task. Never cancel an unknown writer.
2436
+ if (activeTurnId) runtime.forgetSession?.(session.localSessionId);
2416
2437
  this.store.updateRuntimeSession(session.localSessionId, { lastTurnStatus: "cancelled" });
2417
- }
2418
- if (session.status === "starting" || session.status === "running") {
2419
- this.store.updateSession(session.localSessionId, { status: "ready" });
2438
+ if (session.status === "starting" || session.status === "running") {
2439
+ this.store.updateSession(session.localSessionId, { status: "ready" });
2440
+ }
2420
2441
  }
2421
2442
  if (attempt.turnId && this.outbound.canRoute(attempt.contextKey)) {
2422
2443
  await this.finalizeTurnMessageReactions(attempt.turnId, "cancelled").catch((error: unknown) => {
@@ -2428,6 +2449,44 @@ export class ProxySessionController {
2428
2449
  }
2429
2450
  }
2430
2451
 
2452
+ private async handleRecoveryError(attemptId: string, error: unknown, activeTurnId?: string): Promise<void> {
2453
+ if (this.recoveryClosed) return;
2454
+ const attempt = this.store.getTurnAttempt(attemptId);
2455
+ if (!attempt || !isIncompleteTurnAttemptStatus(attempt.status)) return;
2456
+ this.logger.warn({ error, attemptId, sessionId: attempt.localSessionId }, "Task recovery failed.");
2457
+ const session = this.store.getSession(attempt.localSessionId);
2458
+ const writerConflict = session?.remoteSessionId === activeWriterThreadId(error) && Boolean(session?.remoteSessionId);
2459
+ const startedAt = this.recoveryRetryStartedAt.get(attemptId) ?? Date.now();
2460
+ this.recoveryRetryStartedAt.set(attemptId, startedAt);
2461
+ const localActiveTurnId = session ? this.runtimes.forAgent(session.agentName).getSession(session.localSessionId)?.activeTurnId : undefined;
2462
+ const monitoredTurnId = activeTurnId ?? (localActiveTurnId === attempt.turnId ? localActiveTurnId : undefined);
2463
+ // Once a live turn was confirmed, a monitoring outage must not make safe restart
2464
+ // consider it idle. Keep reconciling without cancelling or replaying the prompt.
2465
+ if (monitoredTurnId && this.recoveredLiveAttempts.has(attemptId) && !writerConflict) {
2466
+ this.scheduleRecoveryRetry(attemptId, monitoredTurnId);
2467
+ return;
2468
+ }
2469
+ if (writerConflict || error instanceof CodexVersionError || Date.now() - startedAt >= RECOVERY_RETRY_WINDOW_MS) {
2470
+ const message = writerConflict
2471
+ ? "自动恢复未完成:任务仍被另一个 App Server 占用。已停止自动重试,请确认占用进程后释放任务再继续。"
2472
+ : `自动恢复未完成,已停止自动重试。请检查 Agent 状态后重新发送消息。\n${error instanceof Error ? error.message : String(error)}`;
2473
+ await this.expireStaleTurnAttempt(attempt, message);
2474
+ try {
2475
+ const replyTarget = attempt.replyMessageId
2476
+ ? { messageId: attempt.replyMessageId, replyInThread: true as const } : undefined;
2477
+ await this.outbound.withReplyTarget(attempt.contextKey, replyTarget, async () => {
2478
+ await this.outbound.sendText(attempt.contextKey, message);
2479
+ if (writerConflict && session) await this.presentThreadWriterConflict(session, attempt.contextKey, error, false);
2480
+ });
2481
+ } catch (notificationError) {
2482
+ this.logger.warn({ error: notificationError, attemptId }, "Failed to notify about stopped task recovery.");
2483
+ }
2484
+ return;
2485
+ }
2486
+ this.store.updateTurnAttempt(attemptId, { status: "recovering" });
2487
+ this.scheduleRecoveryRetry(attemptId, monitoredTurnId);
2488
+ }
2489
+
2431
2490
  private persistActiveTurnHeartbeats(): void {
2432
2491
  for (const attempt of this.store.listIncompleteTurnAttempts()) {
2433
2492
  if (!attempt.turnId) continue;
@@ -2439,6 +2498,7 @@ export class ProxySessionController {
2439
2498
  }
2440
2499
 
2441
2500
  private async recoverTurnAttempt(attempt: TurnAttemptRecord, announce = true): Promise<void> {
2501
+ if (this.recoveryClosed) return;
2442
2502
  const session = this.store.getSession(attempt.localSessionId);
2443
2503
  if (!session || session.status === "closed") {
2444
2504
  this.store.updateTurnAttempt(attempt.attemptId, { status: "interrupted" });
@@ -2481,11 +2541,8 @@ export class ProxySessionController {
2481
2541
  try {
2482
2542
  remote = await runtime.readRemoteSession(session.remoteSessionId);
2483
2543
  } catch (error) {
2484
- if (error instanceof CodexVersionError) throw error;
2485
- this.logger.warn(
2486
- { error, attemptId: attempt.attemptId, remoteSessionId: session.remoteSessionId },
2487
- "Failed to read the remote task during startup recovery; treating the old local execution as interrupted.",
2488
- );
2544
+ // A failed activity check is not proof of interruption. Do not replay a prompt blindly.
2545
+ if (sourceTurnId || !isUnmaterializedCodexThreadError(error)) throw error;
2489
2546
  }
2490
2547
  }
2491
2548
  const remoteTurnBelongsToAttempt = remoteTurnMatchesAttempt(remote, attempt);
@@ -2538,12 +2595,20 @@ export class ProxySessionController {
2538
2595
  }
2539
2596
 
2540
2597
  private scheduleRecoveryRetry(attemptId: string, activeTurnId?: string): void {
2541
- if (this.recoveryRetryTimers.has(attemptId)) return;
2598
+ if (this.recoveryClosed || this.recoveryRetryTimers.has(attemptId)) return;
2599
+ if (!this.recoveryRetryStartedAt.has(attemptId)) this.recoveryRetryStartedAt.set(attemptId, Date.now());
2542
2600
  const timer = setTimeout(() => {
2543
2601
  this.recoveryRetryTimers.delete(attemptId);
2544
2602
  const attempt = this.store.getTurnAttempt(attemptId);
2545
- if (!attempt || !isIncompleteTurnAttemptStatus(attempt.status)) return;
2603
+ if (!attempt || !isIncompleteTurnAttemptStatus(attempt.status)) {
2604
+ this.recoveryRetryStartedAt.delete(attemptId);
2605
+ this.recoveredLiveAttempts.delete(attemptId);
2606
+ return;
2607
+ }
2546
2608
  const retry = async (): Promise<void> => {
2609
+ if (!activeTurnId && Date.now() - this.recoveryRetryStartedAt.get(attemptId)! >= RECOVERY_RETRY_WINDOW_MS) {
2610
+ throw new Error("等待恢复超过 1 分钟,尚未确认执行已恢复。");
2611
+ }
2547
2612
  let recoveryAttempt = attempt;
2548
2613
  if (activeTurnId) {
2549
2614
  const session = this.store.getSession(attempt.localSessionId);
@@ -2551,6 +2616,8 @@ export class ProxySessionController {
2551
2616
  const runtime = this.runtimes.forAgent(session.agentName);
2552
2617
  const synchronized = await runtime.synchronizeSession(session.localSessionId);
2553
2618
  if (synchronized.activeTurnId === activeTurnId) {
2619
+ this.recoveredLiveAttempts.add(attemptId);
2620
+ this.recoveryRetryStartedAt.delete(attemptId);
2554
2621
  this.scheduleRecoveryRetry(attemptId, activeTurnId);
2555
2622
  return;
2556
2623
  }
@@ -2560,13 +2627,12 @@ export class ProxySessionController {
2560
2627
  }
2561
2628
  }
2562
2629
  await this.recoverTurnAttempt(recoveryAttempt, false);
2630
+ if (!this.recoveryRetryTimers.has(attemptId)) this.recoveryRetryStartedAt.delete(attemptId);
2563
2631
  };
2564
2632
  void retry().catch((error: unknown) => {
2565
- this.logger.warn(
2566
- { error, attemptId, sessionId: attempt.localSessionId },
2567
- "Failed to retry an unfinished task recovery.",
2568
- );
2569
- this.scheduleRecoveryRetry(attemptId, activeTurnId);
2633
+ return this.handleRecoveryError(attemptId, error, activeTurnId);
2634
+ }).catch((error: unknown) => {
2635
+ this.logger.error({ error, attemptId }, "Failed to finalize task recovery.");
2570
2636
  });
2571
2637
  }, 5_000);
2572
2638
  timer.unref?.();
@@ -2579,6 +2645,13 @@ export class ProxySessionController {
2579
2645
  turnId: string,
2580
2646
  replyTarget?: MessageReplyTarget,
2581
2647
  ): Promise<void> {
2648
+ // Confirm resume before replacing the old card or announcing a running turn.
2649
+ const current = this.store.getSession(session.localSessionId) ?? session;
2650
+ const loaded = await this.loadSession({
2651
+ ...current, contextKey: attempt.contextKey, status: "running", lastTurnId: turnId, lastTurnStatus: "running",
2652
+ });
2653
+ const pendingAttempt = this.store.getTurnAttempt(attempt.attemptId);
2654
+ if (!pendingAttempt || !isIncompleteTurnAttemptStatus(pendingAttempt.status)) return;
2582
2655
  const oldCardTurnId = attempt.pendingTurnId ?? attempt.turnId;
2583
2656
  if (oldCardTurnId) {
2584
2657
  await this.outbound.interruptTurnForRecovery(
@@ -2607,16 +2680,12 @@ export class ProxySessionController {
2607
2680
  startedAt: Date.now(),
2608
2681
  });
2609
2682
  this.store.updateTurnAttempt(attempt.attemptId, { pendingTurnId: null });
2610
- const current = this.store.getSession(session.localSessionId) ?? session;
2611
- const loaded = await this.loadSession({
2612
- ...current,
2613
- contextKey: attempt.contextKey,
2614
- status: "running",
2615
- lastTurnId: turnId,
2616
- lastTurnStatus: "running",
2617
- });
2618
2683
  const synchronized = await loaded.runtime.synchronizeSession(session.localSessionId);
2619
- if (synchronized.activeTurnId) this.scheduleRecoveryRetry(attempt.attemptId, synchronized.activeTurnId);
2684
+ this.recoveryRetryStartedAt.delete(attempt.attemptId);
2685
+ if (synchronized.activeTurnId) {
2686
+ this.recoveredLiveAttempts.add(attempt.attemptId);
2687
+ this.scheduleRecoveryRetry(attempt.attemptId, synchronized.activeTurnId);
2688
+ }
2620
2689
  }
2621
2690
 
2622
2691
  private async continueInterruptedAttempt(
@@ -4638,6 +4707,11 @@ export class ProxySessionController {
4638
4707
 
4639
4708
  private async handleRuntimeEvent(event: RuntimeEvent): Promise<void> {
4640
4709
  if ("turnId" in event) {
4710
+ const recoveryAttempt = this.recoveryRetryStartedAt.size > 0
4711
+ ? this.store.findIncompleteTurnAttemptByTurnId(event.turnId) : undefined;
4712
+ if (recoveryAttempt && this.recoveryRetryStartedAt.has(recoveryAttempt.attemptId)) {
4713
+ this.recoveredLiveAttempts.add(recoveryAttempt.attemptId);
4714
+ }
4641
4715
  this.store.touchTurnAttempt(event.turnId);
4642
4716
  const source = this.store.getSession(event.sessionId);
4643
4717
  if (source?.remoteSessionId && this.isCodexSession(source)) {
@@ -4683,6 +4757,13 @@ export class ProxySessionController {
4683
4757
  this.store.updateSession(event.sessionId, { status: "running" });
4684
4758
  this.store.updateRuntimeSession(event.sessionId, { lastTurnId: event.turnId, lastTurnStatus: "running" });
4685
4759
  } else if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
4760
+ const attempt = this.store.findIncompleteTurnAttemptByTurnId(event.turnId);
4761
+ if (attempt) {
4762
+ clearTimeout(this.recoveryRetryTimers.get(attempt.attemptId));
4763
+ this.recoveryRetryTimers.delete(attempt.attemptId);
4764
+ this.recoveryRetryStartedAt.delete(attempt.attemptId);
4765
+ this.recoveredLiveAttempts.delete(attempt.attemptId);
4766
+ }
4686
4767
  this.store.markTurnAttemptTerminal(
4687
4768
  event.turnId,
4688
4769
  event.type === "turn_completed" ? "completed" : event.type === "turn_cancelled" ? "cancelled" : "failed",
@@ -6754,20 +6835,28 @@ export class ProxySessionController {
6754
6835
  const project = taskProjectInfo(entry);
6755
6836
  const projectActionSource = projectActionSources.get(project.key) ?? entry;
6756
6837
  const marker = entry.current ? "✅ " : entry.active ? "🟢 " : "";
6757
- const showStop = entry.status === "外部执行中"
6758
- && entry.reference !== options.forceSwitchTaskId
6759
- && entry.id !== options.forceSwitchTaskId;
6760
- const actions: TaskListCardAction[] = entry.current ? [] : [bindCardAction({
6761
- text: showStop ? "Stop" : "Switch",
6762
- type: showStop ? "danger" as const : "default" as const,
6838
+ const stopAction = entry.active ? bindCardAction({
6839
+ text: "Stop",
6840
+ type: "danger",
6763
6841
  value: {
6764
- action: showStop ? "session_stop" : "session_switch",
6842
+ action: "session_stop",
6765
6843
  sessionId: entry.reference,
6766
6844
  ...(searchTerm ? { searchTerm } : {}),
6767
6845
  page: String(page),
6768
6846
  contextKey,
6769
6847
  },
6770
- })];
6848
+ }) : undefined;
6849
+ const actions: TaskListCardAction[] = [];
6850
+ if (!entry.current && entry.status !== "外部执行中") actions.push(bindCardAction({
6851
+ text: "Switch",
6852
+ value: {
6853
+ action: "session_switch",
6854
+ sessionId: entry.reference,
6855
+ ...(searchTerm ? { searchTerm } : {}),
6856
+ page: String(page),
6857
+ contextKey,
6858
+ },
6859
+ }));
6771
6860
  actions.push(bindCardAction({
6772
6861
  text: "SwitchGroup",
6773
6862
  value: {
@@ -6833,7 +6922,7 @@ export class ProxySessionController {
6833
6922
  contextKey,
6834
6923
  },
6835
6924
  }));
6836
- actions.push(bindCardAction({
6925
+ if (!entry.active) actions.push(bindCardAction({
6837
6926
  text: "Archive",
6838
6927
  value: {
6839
6928
  action: "session_archive",
@@ -6855,6 +6944,7 @@ export class ProxySessionController {
6855
6944
  `**最新 Prompt**:${cardText(lastUserPrompt)}`,
6856
6945
  `<font color='grey'>最近更新:${cardText(entry.updatedLabel)}</font>`,
6857
6946
  ],
6947
+ stopAction,
6858
6948
  actions,
6859
6949
  current: entry.current,
6860
6950
  },
@@ -6884,7 +6974,7 @@ export class ProxySessionController {
6884
6974
  `第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
6885
6975
  "",
6886
6976
  "> 项目菜单:**New** 新建任务,**NewGroup** 新建群。",
6887
- "> 任务详情:**Switch** 切换,**Stop** 停止,**Fork** / **ForkGroup** 创建分支,**Status** 查看状态,**Archive** 归档。",
6977
+ "> 任务详情:**Switch** 切换,运行中任务可用 **Stop** 发送停止信号,**Fork** / **ForkGroup** 创建分支,**Status** 查看状态,**Archive** 归档。",
6888
6978
  ],
6889
6979
  [
6890
6980
  ...(hasPrevious ? [bindCardAction({
@@ -6959,7 +7049,7 @@ export class ProxySessionController {
6959
7049
 
6960
7050
  private async refreshSessionsCardFromAction(
6961
7051
  action: CardAction,
6962
- options: { forceSwitchTaskId?: string; page?: number } = {},
7052
+ options: { page?: number } = {},
6963
7053
  ): Promise<void> {
6964
7054
  if (!action.messageId) return;
6965
7055
  const searchTerm = typeof action.value.searchTerm === "string" && action.value.searchTerm.trim()
@@ -6967,7 +7057,6 @@ export class ProxySessionController {
6967
7057
  : undefined;
6968
7058
  await this.listSessions(action.contextKey, searchTerm, {
6969
7059
  updateMessageId: action.messageId,
6970
- forceSwitchTaskId: options.forceSwitchTaskId,
6971
7060
  page: options.page ?? parseSessionPage(action.value.page),
6972
7061
  });
6973
7062
  }
@@ -286,6 +286,8 @@ export interface AgentRuntime {
286
286
  resumeSession(input: ResumeRuntimeSessionInput): Promise<RuntimeSession>;
287
287
  forkSession?(input: ForkRuntimeSessionInput): Promise<RuntimeSession>;
288
288
  getSession(localSessionId: string): RuntimeSession | undefined;
289
+ // Drop local monitoring only; do not interrupt, unsubscribe, or archive remote work.
290
+ forgetSession?(localSessionId: string): void;
289
291
  readSessionMetadata(remoteSessionId: string): Promise<RuntimeSessionMetadata>;
290
292
  listRemoteSessions?(input?: { searchTerm?: string; cursor?: string; limit?: number }): Promise<RemoteSessionPage>;
291
293
  readRemoteSession?(remoteSessionId: string, view?: "metadata" | "latest" | "latest-full"): Promise<RemoteSessionSummary>;