@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.
- package/CHANGELOG.md +33 -0
- package/README.md +15 -3
- package/README.zh.md +15 -3
- package/dist/codex/AppServerPermissions.js +34 -0
- package/dist/codex/AppServerPermissions.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +37 -5
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexRuntime.js +37 -13
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ThreadWriterProcess.js +19 -5
- package/dist/codex/ThreadWriterProcess.js.map +1 -1
- package/dist/feishu/CardRenderer.js +80 -64
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/local-files/CsvPreview.js +148 -0
- package/dist/local-files/CsvPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +53 -10
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +122 -11
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +39 -0
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +152 -46
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/docs/technical-reference.md +5 -3
- package/docs/technical-reference.zh.md +4 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +6 -0
- package/src/codex/AppServerPermissions.ts +39 -0
- package/src/codex/CodexLocalActivityDetector.ts +34 -4
- package/src/codex/CodexRuntime.ts +44 -17
- package/src/codex/ThreadWriterProcess.ts +17 -9
- package/src/feishu/CardRenderer.ts +78 -64
- package/src/local-files/CsvPreview.ts +132 -0
- package/src/local-files/LocalFileViewerServer.ts +54 -11
- package/src/local-files/TurnPreviewPage.ts +125 -12
- package/src/presentation/TurnStateReducer.ts +41 -0
- package/src/presentation/turnViewTypes.ts +4 -0
- package/src/proxy/ProxySessionController.ts +137 -48
- 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
|
|
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.
|
|
2301
|
-
|
|
2302
|
-
|
|
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(
|
|
2398
|
-
|
|
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
|
-
|
|
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.
|
|
2426
|
+
session = this.store.getSession(attempt.localSessionId);
|
|
2414
2427
|
if (!session || session.status === "closed") return;
|
|
2415
|
-
|
|
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
|
-
|
|
2419
|
-
|
|
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
|
-
|
|
2485
|
-
|
|
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))
|
|
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.
|
|
2566
|
-
|
|
2567
|
-
|
|
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
|
-
|
|
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
|
|
6758
|
-
|
|
6759
|
-
|
|
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:
|
|
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**
|
|
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: {
|
|
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
|
}
|
package/src/runtime/types.ts
CHANGED
|
@@ -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>;
|