@keyou007/agent-bot 0.1.24-alpha.4 → 0.1.24-alpha.6
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 +59 -0
- package/README.md +42 -6
- package/README.zh.md +42 -6
- package/SECURITY.md +12 -0
- package/config.example.yaml +5 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/serverStatus.js +1 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- 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 +238 -127
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +12 -13
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/codex/ThreadWriterProcess.js +19 -5
- package/dist/codex/ThreadWriterProcess.js.map +1 -1
- package/dist/config/AgentExecutionDefaultsStore.js +4 -0
- package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +3 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +146 -72
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +1 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +121 -17
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +43 -6
- package/dist/index.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 +104 -14
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +128 -12
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +80 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +767 -260
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/proxy/SessionOperationQueue.js +14 -0
- package/dist/proxy/SessionOperationQueue.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +4 -2
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/state/StateStore.js +22 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +19 -7
- package/dist/state/TurnPreviewJournal.js.map +1 -1
- package/dist/state/migrations.js +6 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +1 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +19 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/terminal/TerminalCards.js +13 -0
- package/dist/terminal/TerminalCards.js.map +1 -0
- package/dist/terminal/TerminalLinks.js +76 -0
- package/dist/terminal/TerminalLinks.js.map +1 -0
- package/dist/terminal/TerminalPage.js +212 -0
- package/dist/terminal/TerminalPage.js.map +1 -0
- package/dist/terminal/TerminalProcess.js +71 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +639 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +53 -3
- package/docs/technical-reference.zh.md +52 -2
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +15 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/AppServerPermissions.ts +39 -0
- package/src/codex/CodexLocalActivityDetector.ts +34 -4
- package/src/codex/CodexRuntime.ts +243 -139
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/codex/ThreadWriterProcess.ts +17 -9
- package/src/config/AgentExecutionDefaultsStore.ts +2 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +5 -1
- package/src/feishu/CardRenderer.ts +159 -72
- package/src/feishu/FeishuConnector.ts +1 -0
- package/src/feishu/FeishuTurnPresenter.ts +108 -16
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +44 -7
- package/src/local-files/CsvPreview.ts +132 -0
- package/src/local-files/LocalFileViewerServer.ts +102 -15
- package/src/local-files/TurnPreviewPage.ts +131 -13
- package/src/presentation/TurnStateReducer.ts +87 -7
- package/src/presentation/turnViewTypes.ts +9 -0
- package/src/proxy/ProxySessionController.ts +762 -259
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +10 -3
- package/src/state/StateStore.ts +38 -1
- package/src/state/TurnPreviewJournal.ts +19 -7
- package/src/state/migrations.ts +6 -0
- package/src/supervision/SafeRestartNotifier.ts +1 -0
- package/src/supervision/SafeRestartScheduler.ts +18 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +71 -0
- package/src/terminal/TerminalServer.ts +513 -0
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import type { Logger } from "pino";
|
|
6
6
|
import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
7
7
|
import { CodexVersionError } from "../codex/CodexVersion.js";
|
|
8
|
-
import { assertProviderSettingsApplied,
|
|
8
|
+
import { assertProviderSettingsApplied, ProviderSwitchError } from "../codex/ProviderSwitch.js";
|
|
9
9
|
import {
|
|
10
10
|
SystemThreadWriterProcessController,
|
|
11
11
|
type ThreadWriterProcess,
|
|
@@ -90,11 +90,13 @@ import {
|
|
|
90
90
|
type MessageReactionRecord,
|
|
91
91
|
type MessageReactionStatus,
|
|
92
92
|
type QueuedPromptRecord,
|
|
93
|
+
type PendingProviderSettings,
|
|
93
94
|
type SessionRecord,
|
|
94
95
|
type TurnAttemptRecord,
|
|
95
96
|
type TurnAnchorRecord,
|
|
96
97
|
type TurnRuntimeOriginRecord,
|
|
97
98
|
} from "../state/StateStore.js";
|
|
99
|
+
import { SessionOperationQueue } from "./SessionOperationQueue.js";
|
|
98
100
|
import { createId } from "../utils/id.js";
|
|
99
101
|
import { formatStorageSize } from "../utils/formatStorageSize.js";
|
|
100
102
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
@@ -117,10 +119,12 @@ interface StartTurnOptions {
|
|
|
117
119
|
messageId?: string;
|
|
118
120
|
displayPrompt?: string;
|
|
119
121
|
preserveAttemptOnFailure?: boolean;
|
|
122
|
+
approvedPlanTurnId?: string;
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
interface PendingForwardAttachment {
|
|
123
126
|
contextKey: string;
|
|
127
|
+
userId?: string;
|
|
124
128
|
attachmentMessageId?: string;
|
|
125
129
|
attachmentPromise: Promise<IncomingMessage | undefined>;
|
|
126
130
|
resolveAttachment: (message: IncomingMessage | undefined) => void;
|
|
@@ -193,11 +197,12 @@ const REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS = 30_000;
|
|
|
193
197
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
194
198
|
const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
|
|
195
199
|
const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
|
|
200
|
+
const RECOVERY_RETRY_WINDOW_MS = 60_000;
|
|
196
201
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
197
202
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
198
203
|
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
199
204
|
const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
|
|
200
|
-
const FORWARD_ATTACHMENT_WINDOW_MS =
|
|
205
|
+
const FORWARD_ATTACHMENT_WINDOW_MS = 1_500;
|
|
201
206
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
202
207
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
203
208
|
const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
|
|
@@ -383,7 +388,6 @@ interface AgentRemoteSessionSummary {
|
|
|
383
388
|
|
|
384
389
|
interface SessionsCardOptions {
|
|
385
390
|
updateMessageId?: string;
|
|
386
|
-
forceSwitchTaskId?: string;
|
|
387
391
|
page?: number;
|
|
388
392
|
}
|
|
389
393
|
|
|
@@ -406,6 +410,8 @@ interface ResetHistoryCardOptions {
|
|
|
406
410
|
}
|
|
407
411
|
|
|
408
412
|
interface ModelCardOptions {
|
|
413
|
+
modelPage?: number;
|
|
414
|
+
expectedProvider?: string;
|
|
409
415
|
sessionId?: string;
|
|
410
416
|
updateMessageId?: string;
|
|
411
417
|
}
|
|
@@ -416,6 +422,7 @@ interface ThinkingCardOptions extends ModelCardOptions {
|
|
|
416
422
|
|
|
417
423
|
interface ExecutionSettingsCardOptions extends ModelCardOptions {
|
|
418
424
|
notice?: string;
|
|
425
|
+
warning?: string;
|
|
419
426
|
}
|
|
420
427
|
|
|
421
428
|
type SessionExecutionSettings = AgentExecutionDefaults;
|
|
@@ -506,6 +513,8 @@ export interface ControlTaskReleaseResult {
|
|
|
506
513
|
}
|
|
507
514
|
|
|
508
515
|
export interface ProxyLifecycle {
|
|
516
|
+
terminalUrl?(jobId: string): string | undefined;
|
|
517
|
+
authorizeTerminal?(action: CardAction): Promise<string>;
|
|
509
518
|
supervised?: boolean;
|
|
510
519
|
restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
|
|
511
520
|
cancelSafeRestart?(scheduleId: number): Promise<boolean>;
|
|
@@ -539,9 +548,14 @@ export class ProxySessionController {
|
|
|
539
548
|
private readonly messageQueues = new Map<string, Promise<void>>();
|
|
540
549
|
private readonly switchGroupOperations = new Set<string>();
|
|
541
550
|
private readonly sessionLoads = new Map<string, Promise<LoadedSession>>();
|
|
551
|
+
private readonly settingsOnlySessions = new Set<string>();
|
|
542
552
|
private readonly queuedPromptStarts = new Map<string, Promise<void>>();
|
|
543
553
|
private readonly queuedPromptCards = new Map<string, Map<string, string>>();
|
|
544
554
|
private readonly queuedPromptCardWrites = new Map<string, Promise<void>>();
|
|
555
|
+
private readonly settingsOperations = new SessionOperationQueue();
|
|
556
|
+
// Loads may run inside settingsOperations; never acquire that queue while hydrating.
|
|
557
|
+
private readonly sessionHydrationOperations = new SessionOperationQueue();
|
|
558
|
+
private readonly agentDefaultsOperations = new SessionOperationQueue();
|
|
545
559
|
private readonly sessionResets = new Map<string, Promise<void>>();
|
|
546
560
|
private readonly resetHistoryOperations = new Map<string, string>();
|
|
547
561
|
private readonly remoteTurnHistoryRefreshes = new Map<string, number>();
|
|
@@ -551,9 +565,13 @@ export class ProxySessionController {
|
|
|
551
565
|
private readonly lastSessionListings = new Map<string, string[]>();
|
|
552
566
|
private readonly threadInitializations = new Map<string, Promise<void>>();
|
|
553
567
|
private readonly llmRetryingSessions = new Set<string>();
|
|
568
|
+
private readonly planHandoffs = new Map<string, string>();
|
|
554
569
|
private readonly retriedFailureTurnIds = new Set<string>();
|
|
555
570
|
private readonly queuedPromptRetryTimers = new Map<string, NodeJS.Timeout>();
|
|
556
571
|
private readonly recoveryRetryTimers = new Map<string, NodeJS.Timeout>();
|
|
572
|
+
private readonly recoveryRetryStartedAt = new Map<string, number>();
|
|
573
|
+
private readonly recoveredLiveAttempts = new Set<string>();
|
|
574
|
+
private recoveryClosed = false;
|
|
557
575
|
private readonly pendingMergedForwards = new Map<string, PendingForwardAttachment>();
|
|
558
576
|
private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
|
|
559
577
|
private readonly relatedReactionMessageIds = new Map<string, string[]>();
|
|
@@ -630,7 +648,7 @@ export class ProxySessionController {
|
|
|
630
648
|
const pendingMergedForward = message.mergedForwardMessageId
|
|
631
649
|
? this.registerPendingForwardAttachment(message, this.pendingMergedForwards)
|
|
632
650
|
: undefined;
|
|
633
|
-
const pendingResourceForward = !attachmentReservation &&
|
|
651
|
+
const pendingResourceForward = !attachmentReservation && isForwardAttachmentSource(message)
|
|
634
652
|
? this.registerPendingForwardAttachment(message, this.pendingResourceForwards)
|
|
635
653
|
: undefined;
|
|
636
654
|
try {
|
|
@@ -680,7 +698,9 @@ export class ProxySessionController {
|
|
|
680
698
|
);
|
|
681
699
|
if (attachment) {
|
|
682
700
|
coalescedForwardAttachment = true;
|
|
683
|
-
const
|
|
701
|
+
const sourceText = resourceMessage.text.trim();
|
|
702
|
+
const instruction = normalizeFeishuPostText(attachment.text)
|
|
703
|
+
|| (sourceText ? DEFAULT_REFERENCED_MESSAGE_INSTRUCTION : defaultResourcePrompt(resourceMessage));
|
|
684
704
|
forwardedImages = (resourceMessage.images ?? []).map((image) => ({
|
|
685
705
|
messageId: resourceMessage.messageId,
|
|
686
706
|
imageKey: image.imageKey,
|
|
@@ -692,7 +712,7 @@ export class ProxySessionController {
|
|
|
692
712
|
}));
|
|
693
713
|
message = {
|
|
694
714
|
...attachment,
|
|
695
|
-
text: instruction,
|
|
715
|
+
text: sourceText ? referencedMessagePrompt(instruction, sourceText) : instruction,
|
|
696
716
|
displayText: instruction,
|
|
697
717
|
};
|
|
698
718
|
this.relatedReactionMessageIds.set(message.messageId, [resourceMessage.messageId]);
|
|
@@ -1012,7 +1032,10 @@ export class ProxySessionController {
|
|
|
1012
1032
|
await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
|
|
1013
1033
|
try {
|
|
1014
1034
|
const kind = String(scopedAction.value.action ?? "");
|
|
1015
|
-
if (kind === "
|
|
1035
|
+
if (kind === "terminal_authorize") {
|
|
1036
|
+
if (!this.lifecycle?.authorizeTerminal) throw new Error("网页终端未启用。");
|
|
1037
|
+
await this.outbound.sendText(contextKey, await this.lifecycle.authorizeTerminal(scopedAction));
|
|
1038
|
+
} else if (kind === "agentbot_update_cancel") {
|
|
1016
1039
|
if (!this.lifecycle?.cancelAutomaticUpdate) throw new Error("自动更新功能未启用。");
|
|
1017
1040
|
await this.lifecycle.cancelAutomaticUpdate(scopedAction);
|
|
1018
1041
|
} else if (kind === "agentbot_update_select") {
|
|
@@ -1208,7 +1231,7 @@ export class ProxySessionController {
|
|
|
1208
1231
|
await this.stopSessionReference(contextKey, sessionId);
|
|
1209
1232
|
if (scopedAction.value.cardView === "status") await this.refreshStatusCardFromAction(scopedAction, sessionId);
|
|
1210
1233
|
else if (scopedAction.value.cardView !== "safe_restart") {
|
|
1211
|
-
await this.refreshSessionsCardFromAction(scopedAction
|
|
1234
|
+
await this.refreshSessionsCardFromAction(scopedAction);
|
|
1212
1235
|
}
|
|
1213
1236
|
} else if (kind === "session_turns" || kind === "session_turns_page") {
|
|
1214
1237
|
const sessionReference = String(scopedAction.value.sessionId ?? "").trim();
|
|
@@ -1228,10 +1251,14 @@ export class ProxySessionController {
|
|
|
1228
1251
|
} else if (kind === "session_status_refresh") {
|
|
1229
1252
|
await this.refreshStatusCardFromAction(scopedAction);
|
|
1230
1253
|
} else if (kind === "settings_tab_open") {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1254
|
+
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
1255
|
+
await this.openExecutionSettings(
|
|
1256
|
+
contextKey, executionSettingsTabValue(scopedAction.value.tab), {
|
|
1257
|
+
sessionId, updateMessageId: scopedAction.messageId,
|
|
1258
|
+
modelPage: modelCardPageValue(scopedAction.value.page),
|
|
1259
|
+
expectedProvider: typeof scopedAction.value.provider === "string" ? scopedAction.value.provider : undefined,
|
|
1260
|
+
},
|
|
1261
|
+
);
|
|
1235
1262
|
} else if (kind === "settings_agent_select") {
|
|
1236
1263
|
await this.setDefaultAgent(contextKey, String(scopedAction.value.agent ?? ""), {
|
|
1237
1264
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
@@ -1244,11 +1271,13 @@ export class ProxySessionController {
|
|
|
1244
1271
|
});
|
|
1245
1272
|
} else if (kind === "settings_model_select") {
|
|
1246
1273
|
await this.model(contextKey, String(scopedAction.value.model ?? ""), {
|
|
1274
|
+
expectedProvider: String(scopedAction.value.provider ?? ""),
|
|
1247
1275
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
1248
1276
|
updateMessageId: scopedAction.messageId,
|
|
1249
1277
|
});
|
|
1250
1278
|
} else if (kind === "settings_thinking_select") {
|
|
1251
1279
|
await this.thinking(contextKey, String(scopedAction.value.effort ?? ""), {
|
|
1280
|
+
expectedProvider: String(scopedAction.value.provider ?? ""),
|
|
1252
1281
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
1253
1282
|
updateMessageId: scopedAction.messageId,
|
|
1254
1283
|
expectedModel: String(scopedAction.value.model ?? ""),
|
|
@@ -1270,6 +1299,7 @@ export class ProxySessionController {
|
|
|
1270
1299
|
await this.model(contextKey, undefined, {
|
|
1271
1300
|
sessionId,
|
|
1272
1301
|
updateMessageId: scopedAction.messageId,
|
|
1302
|
+
modelPage: modelCardPageValue(scopedAction.value.page),
|
|
1273
1303
|
});
|
|
1274
1304
|
} else if (kind === "reasoning_select") {
|
|
1275
1305
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
@@ -1292,6 +1322,7 @@ export class ProxySessionController {
|
|
|
1292
1322
|
permissionModeValue(scopedAction.value.permissionMode),
|
|
1293
1323
|
String(scopedAction.value.sessionId ?? ""),
|
|
1294
1324
|
requiredCardMessageId(scopedAction.messageId),
|
|
1325
|
+
modelCardPageValue(scopedAction.value.page),
|
|
1295
1326
|
);
|
|
1296
1327
|
} else if (kind === "provider_model_select" || kind === "provider_reasoning_open") {
|
|
1297
1328
|
await this.openProviderReasoningSelector(
|
|
@@ -1353,12 +1384,14 @@ export class ProxySessionController {
|
|
|
1353
1384
|
}
|
|
1354
1385
|
|
|
1355
1386
|
close(): void {
|
|
1387
|
+
this.recoveryClosed = true;
|
|
1356
1388
|
for (const unsubscribe of this.unsubscribe) unsubscribe();
|
|
1357
1389
|
this.unsubscribe.length = 0;
|
|
1358
1390
|
this.lastSessionListings.clear();
|
|
1359
1391
|
this.queuedPromptCards.clear();
|
|
1360
1392
|
this.queuedPromptCardWrites.clear();
|
|
1361
1393
|
this.sessionResets.clear();
|
|
1394
|
+
this.planHandoffs.clear();
|
|
1362
1395
|
this.resetHistoryOperations.clear();
|
|
1363
1396
|
this.remoteTurnHistoryRefreshes.clear();
|
|
1364
1397
|
this.forkHistoryCursors.clear();
|
|
@@ -1372,6 +1405,8 @@ export class ProxySessionController {
|
|
|
1372
1405
|
this.queuedPromptRetryTimers.clear();
|
|
1373
1406
|
for (const timer of this.recoveryRetryTimers.values()) clearTimeout(timer);
|
|
1374
1407
|
this.recoveryRetryTimers.clear();
|
|
1408
|
+
this.recoveryRetryStartedAt.clear();
|
|
1409
|
+
this.recoveredLiveAttempts.clear();
|
|
1375
1410
|
for (const schedule of this.appServerReleaseSchedules.values()) {
|
|
1376
1411
|
if (schedule.timer) clearTimeout(schedule.timer);
|
|
1377
1412
|
}
|
|
@@ -1760,79 +1795,44 @@ export class ProxySessionController {
|
|
|
1760
1795
|
models: Awaited<ReturnType<AgentRuntime["listModels"]>>;
|
|
1761
1796
|
reasoningOptions: Awaited<ReturnType<AgentRuntime["listModels"]>>[number]["supportedReasoningEfforts"];
|
|
1762
1797
|
permissionModes: PermissionMode[];
|
|
1798
|
+
pendingProvider?: PendingProviderSettings;
|
|
1763
1799
|
}> {
|
|
1764
1800
|
const record = this.requireControlSession(localSessionId);
|
|
1765
1801
|
if (setting && !value?.trim()) throw new Error(`task ${setting} requires a value.`);
|
|
1766
1802
|
const nextValue = value?.trim();
|
|
1767
|
-
if (setting
|
|
1803
|
+
if (setting && nextValue) {
|
|
1768
1804
|
const contextKey = this.controlSessionContextKey(record);
|
|
1769
|
-
await this.outbound.withReplyTarget(
|
|
1770
|
-
|
|
1771
|
-
this.
|
|
1772
|
-
()
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
const models = await loaded.runtime.listModels(nextValue, loaded.session.model);
|
|
1779
|
-
const model = selectProviderModel(models, loaded.session.model);
|
|
1780
|
-
if (!model) throw new Error("The current runtime has no model available for a Provider change.");
|
|
1781
|
-
const effort = selectProviderReasoningEffort(model, loaded.session.reasoningEffort);
|
|
1782
|
-
await this.changeProviderSettings(loaded, {
|
|
1783
|
-
modelProvider: nextValue,
|
|
1784
|
-
model: model.id,
|
|
1785
|
-
reasoningEffort: effort,
|
|
1786
|
-
permissionMode: loaded.session.permissionMode,
|
|
1805
|
+
await this.outbound.withReplyTarget(contextKey, this.controlSessionReplyTarget(record), async () => {
|
|
1806
|
+
const options = { sessionId: record.localSessionId };
|
|
1807
|
+
if (setting === "provider") await this.selectProvider(contextKey, nextValue, options);
|
|
1808
|
+
else if (setting === "model") await this.model(contextKey, nextValue, options);
|
|
1809
|
+
else if (setting === "thinking") await this.thinking(contextKey, nextValue, options);
|
|
1810
|
+
else {
|
|
1811
|
+
if (nextValue !== "auto" && nextValue !== "confirm") throw new Error("task permissions accepts auto or confirm.");
|
|
1812
|
+
await this.permissions(contextKey, nextValue, options);
|
|
1813
|
+
}
|
|
1787
1814
|
});
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
const
|
|
1791
|
-
|
|
1792
|
-
const
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
: selected.defaultReasoningEffort;
|
|
1797
|
-
await loaded.runtime.setModel(record.localSessionId, nextValue);
|
|
1798
|
-
if (nextEffort && nextEffort !== currentEffort) {
|
|
1799
|
-
await loaded.runtime.setReasoningEffort(record.localSessionId, nextEffort);
|
|
1800
|
-
}
|
|
1801
|
-
this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
|
|
1802
|
-
this.outbound.updateSessionModel(record.localSessionId, nextValue, loaded.session.modelProvider);
|
|
1803
|
-
} else if (setting === "thinking" && nextValue) {
|
|
1804
|
-
const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
|
|
1805
|
-
const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
|
|
1815
|
+
}
|
|
1816
|
+
return this.settingsOperations.run(localSessionId, async () => {
|
|
1817
|
+
const loaded = await this.loadSession(this.store.getSession(localSessionId) ?? record, true);
|
|
1818
|
+
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1819
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
1820
|
+
const selected = pending?.settings ?? session;
|
|
1821
|
+
const models = await loaded.runtime.listModels(selected.modelProvider, selected.model);
|
|
1822
|
+
const currentModel = models.find((candidate) => candidate.id === selected.model)
|
|
1806
1823
|
?? models.find((candidate) => candidate.isDefault);
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
|
|
1820
|
-
}
|
|
1821
|
-
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1822
|
-
if (setting && setting !== "provider") await this.persistAgentExecutionDefaults(session.localSessionId);
|
|
1823
|
-
const models = await loaded.runtime.listModels(session.modelProvider, session.model);
|
|
1824
|
-
const currentModel = models.find((candidate) => candidate.id === session.model)
|
|
1825
|
-
?? models.find((candidate) => candidate.isDefault);
|
|
1826
|
-
const providers = loaded.runtime.listModelProviders
|
|
1827
|
-
? await this.modelProviderOptions(loaded)
|
|
1828
|
-
: [];
|
|
1829
|
-
return {
|
|
1830
|
-
session,
|
|
1831
|
-
providers,
|
|
1832
|
-
models,
|
|
1833
|
-
reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
|
|
1834
|
-
permissionModes: ["auto", "confirm"],
|
|
1835
|
-
};
|
|
1824
|
+
const providers = loaded.runtime.listModelProviders
|
|
1825
|
+
? await this.modelProviderOptions(loaded)
|
|
1826
|
+
: [];
|
|
1827
|
+
return {
|
|
1828
|
+
session,
|
|
1829
|
+
pendingProvider: pending,
|
|
1830
|
+
providers,
|
|
1831
|
+
models,
|
|
1832
|
+
reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
|
|
1833
|
+
permissionModes: ["auto", "confirm"],
|
|
1834
|
+
};
|
|
1835
|
+
});
|
|
1836
1836
|
}
|
|
1837
1837
|
|
|
1838
1838
|
async controlTaskGoal(
|
|
@@ -2294,16 +2294,18 @@ export class ProxySessionController {
|
|
|
2294
2294
|
continue;
|
|
2295
2295
|
}
|
|
2296
2296
|
if (!this.outbound.canRoute(attempt.contextKey)) continue;
|
|
2297
|
+
this.recoveryRetryStartedAt.set(attempt.attemptId, Date.now());
|
|
2297
2298
|
try {
|
|
2298
2299
|
await this.recoverTurnAttempt(attempt);
|
|
2299
2300
|
} catch (error) {
|
|
2300
|
-
this.
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
);
|
|
2304
|
-
this.scheduleRecoveryRetry(attempt.attemptId);
|
|
2301
|
+
await this.handleRecoveryError(attempt.attemptId, error);
|
|
2302
|
+
} finally {
|
|
2303
|
+
if (!this.recoveryRetryTimers.has(attempt.attemptId)) this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
2305
2304
|
}
|
|
2306
2305
|
}
|
|
2306
|
+
for (const pending of this.store.listPendingProviderSettings()) {
|
|
2307
|
+
await this.applyPendingProviderWhenIdle(pending.localSessionId);
|
|
2308
|
+
}
|
|
2307
2309
|
this.restorePersistedQueuedPrompts();
|
|
2308
2310
|
}
|
|
2309
2311
|
|
|
@@ -2394,15 +2396,23 @@ export class ProxySessionController {
|
|
|
2394
2396
|
}
|
|
2395
2397
|
}
|
|
2396
2398
|
|
|
2397
|
-
private async expireStaleTurnAttempt(
|
|
2398
|
-
|
|
2399
|
+
private async expireStaleTurnAttempt(
|
|
2400
|
+
attempt: TurnAttemptRecord,
|
|
2401
|
+
message = "执行中断已超过 10 分钟,未自动恢复。",
|
|
2402
|
+
): Promise<void> {
|
|
2403
|
+
clearTimeout(this.recoveryRetryTimers.get(attempt.attemptId));
|
|
2404
|
+
this.recoveryRetryTimers.delete(attempt.attemptId);
|
|
2405
|
+
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
2406
|
+
this.recoveredLiveAttempts.delete(attempt.attemptId);
|
|
2407
|
+
let session = this.store.getSession(attempt.localSessionId);
|
|
2408
|
+
this.store.updateTurnAttempt(attempt.attemptId, { status: "interrupted" });
|
|
2399
2409
|
const oldCardTurnId = attempt.pendingTurnId ?? attempt.turnId;
|
|
2400
2410
|
if (session && oldCardTurnId && this.outbound.canRoute(attempt.contextKey)) {
|
|
2401
2411
|
await this.outbound.interruptTurnForRecovery(
|
|
2402
2412
|
session.localSessionId,
|
|
2403
2413
|
attempt.contextKey,
|
|
2404
2414
|
oldCardTurnId,
|
|
2405
|
-
|
|
2415
|
+
message,
|
|
2406
2416
|
).catch((error: unknown) => {
|
|
2407
2417
|
this.logger.warn(
|
|
2408
2418
|
{ error, attemptId: attempt.attemptId, turnId: oldCardTurnId },
|
|
@@ -2410,13 +2420,21 @@ export class ProxySessionController {
|
|
|
2410
2420
|
);
|
|
2411
2421
|
});
|
|
2412
2422
|
}
|
|
2413
|
-
this.store.
|
|
2423
|
+
session = this.store.getSession(attempt.localSessionId);
|
|
2414
2424
|
if (!session || session.status === "closed") return;
|
|
2415
|
-
|
|
2425
|
+
const runtime = this.runtimes.forAgent(session.agentName);
|
|
2426
|
+
const activeTurnId = runtime.getSession(session.localSessionId)?.activeTurnId;
|
|
2427
|
+
const sameTurn = session.lastTurnId === attempt.turnId
|
|
2428
|
+
|| session.lastTurnId === attempt.recoveredFromTurnId
|
|
2429
|
+
|| !session.lastTurnId;
|
|
2430
|
+
if (sameTurn && session.lastTurnStatus !== "completed" && session.lastTurnStatus !== "failed"
|
|
2431
|
+
&& (!activeTurnId || activeTurnId === attempt.turnId)) {
|
|
2432
|
+
// Forget monitoring, not the remote task. Never cancel an unknown writer.
|
|
2433
|
+
if (activeTurnId) runtime.forgetSession?.(session.localSessionId);
|
|
2416
2434
|
this.store.updateRuntimeSession(session.localSessionId, { lastTurnStatus: "cancelled" });
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2435
|
+
if (session.status === "starting" || session.status === "running") {
|
|
2436
|
+
this.store.updateSession(session.localSessionId, { status: "ready" });
|
|
2437
|
+
}
|
|
2420
2438
|
}
|
|
2421
2439
|
if (attempt.turnId && this.outbound.canRoute(attempt.contextKey)) {
|
|
2422
2440
|
await this.finalizeTurnMessageReactions(attempt.turnId, "cancelled").catch((error: unknown) => {
|
|
@@ -2428,6 +2446,44 @@ export class ProxySessionController {
|
|
|
2428
2446
|
}
|
|
2429
2447
|
}
|
|
2430
2448
|
|
|
2449
|
+
private async handleRecoveryError(attemptId: string, error: unknown, activeTurnId?: string): Promise<void> {
|
|
2450
|
+
if (this.recoveryClosed) return;
|
|
2451
|
+
const attempt = this.store.getTurnAttempt(attemptId);
|
|
2452
|
+
if (!attempt || !isIncompleteTurnAttemptStatus(attempt.status)) return;
|
|
2453
|
+
this.logger.warn({ error, attemptId, sessionId: attempt.localSessionId }, "Task recovery failed.");
|
|
2454
|
+
const session = this.store.getSession(attempt.localSessionId);
|
|
2455
|
+
const writerConflict = session?.remoteSessionId === activeWriterThreadId(error) && Boolean(session?.remoteSessionId);
|
|
2456
|
+
const startedAt = this.recoveryRetryStartedAt.get(attemptId) ?? Date.now();
|
|
2457
|
+
this.recoveryRetryStartedAt.set(attemptId, startedAt);
|
|
2458
|
+
const localActiveTurnId = session ? this.runtimes.forAgent(session.agentName).getSession(session.localSessionId)?.activeTurnId : undefined;
|
|
2459
|
+
const monitoredTurnId = activeTurnId ?? (localActiveTurnId === attempt.turnId ? localActiveTurnId : undefined);
|
|
2460
|
+
// Once a live turn was confirmed, a monitoring outage must not make safe restart
|
|
2461
|
+
// consider it idle. Keep reconciling without cancelling or replaying the prompt.
|
|
2462
|
+
if (monitoredTurnId && this.recoveredLiveAttempts.has(attemptId) && !writerConflict) {
|
|
2463
|
+
this.scheduleRecoveryRetry(attemptId, monitoredTurnId);
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
if (writerConflict || error instanceof CodexVersionError || Date.now() - startedAt >= RECOVERY_RETRY_WINDOW_MS) {
|
|
2467
|
+
const message = writerConflict
|
|
2468
|
+
? "自动恢复未完成:任务仍被另一个 App Server 占用。已停止自动重试,请确认占用进程后释放任务再继续。"
|
|
2469
|
+
: `自动恢复未完成,已停止自动重试。请检查 Agent 状态后重新发送消息。\n${error instanceof Error ? error.message : String(error)}`;
|
|
2470
|
+
await this.expireStaleTurnAttempt(attempt, message);
|
|
2471
|
+
try {
|
|
2472
|
+
const replyTarget = attempt.replyMessageId
|
|
2473
|
+
? { messageId: attempt.replyMessageId, replyInThread: true as const } : undefined;
|
|
2474
|
+
await this.outbound.withReplyTarget(attempt.contextKey, replyTarget, async () => {
|
|
2475
|
+
await this.outbound.sendText(attempt.contextKey, message);
|
|
2476
|
+
if (writerConflict && session) await this.presentThreadWriterConflict(session, attempt.contextKey, error, false);
|
|
2477
|
+
});
|
|
2478
|
+
} catch (notificationError) {
|
|
2479
|
+
this.logger.warn({ error: notificationError, attemptId }, "Failed to notify about stopped task recovery.");
|
|
2480
|
+
}
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
this.store.updateTurnAttempt(attemptId, { status: "recovering" });
|
|
2484
|
+
this.scheduleRecoveryRetry(attemptId, monitoredTurnId);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2431
2487
|
private persistActiveTurnHeartbeats(): void {
|
|
2432
2488
|
for (const attempt of this.store.listIncompleteTurnAttempts()) {
|
|
2433
2489
|
if (!attempt.turnId) continue;
|
|
@@ -2439,6 +2495,7 @@ export class ProxySessionController {
|
|
|
2439
2495
|
}
|
|
2440
2496
|
|
|
2441
2497
|
private async recoverTurnAttempt(attempt: TurnAttemptRecord, announce = true): Promise<void> {
|
|
2498
|
+
if (this.recoveryClosed) return;
|
|
2442
2499
|
const session = this.store.getSession(attempt.localSessionId);
|
|
2443
2500
|
if (!session || session.status === "closed") {
|
|
2444
2501
|
this.store.updateTurnAttempt(attempt.attemptId, { status: "interrupted" });
|
|
@@ -2481,11 +2538,8 @@ export class ProxySessionController {
|
|
|
2481
2538
|
try {
|
|
2482
2539
|
remote = await runtime.readRemoteSession(session.remoteSessionId);
|
|
2483
2540
|
} 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
|
-
);
|
|
2541
|
+
// A failed activity check is not proof of interruption. Do not replay a prompt blindly.
|
|
2542
|
+
if (sourceTurnId || !isUnmaterializedCodexThreadError(error)) throw error;
|
|
2489
2543
|
}
|
|
2490
2544
|
}
|
|
2491
2545
|
const remoteTurnBelongsToAttempt = remoteTurnMatchesAttempt(remote, attempt);
|
|
@@ -2538,12 +2592,20 @@ export class ProxySessionController {
|
|
|
2538
2592
|
}
|
|
2539
2593
|
|
|
2540
2594
|
private scheduleRecoveryRetry(attemptId: string, activeTurnId?: string): void {
|
|
2541
|
-
if (this.recoveryRetryTimers.has(attemptId)) return;
|
|
2595
|
+
if (this.recoveryClosed || this.recoveryRetryTimers.has(attemptId)) return;
|
|
2596
|
+
if (!this.recoveryRetryStartedAt.has(attemptId)) this.recoveryRetryStartedAt.set(attemptId, Date.now());
|
|
2542
2597
|
const timer = setTimeout(() => {
|
|
2543
2598
|
this.recoveryRetryTimers.delete(attemptId);
|
|
2544
2599
|
const attempt = this.store.getTurnAttempt(attemptId);
|
|
2545
|
-
if (!attempt || !isIncompleteTurnAttemptStatus(attempt.status))
|
|
2600
|
+
if (!attempt || !isIncompleteTurnAttemptStatus(attempt.status)) {
|
|
2601
|
+
this.recoveryRetryStartedAt.delete(attemptId);
|
|
2602
|
+
this.recoveredLiveAttempts.delete(attemptId);
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2546
2605
|
const retry = async (): Promise<void> => {
|
|
2606
|
+
if (!activeTurnId && Date.now() - this.recoveryRetryStartedAt.get(attemptId)! >= RECOVERY_RETRY_WINDOW_MS) {
|
|
2607
|
+
throw new Error("等待恢复超过 1 分钟,尚未确认执行已恢复。");
|
|
2608
|
+
}
|
|
2547
2609
|
let recoveryAttempt = attempt;
|
|
2548
2610
|
if (activeTurnId) {
|
|
2549
2611
|
const session = this.store.getSession(attempt.localSessionId);
|
|
@@ -2551,6 +2613,8 @@ export class ProxySessionController {
|
|
|
2551
2613
|
const runtime = this.runtimes.forAgent(session.agentName);
|
|
2552
2614
|
const synchronized = await runtime.synchronizeSession(session.localSessionId);
|
|
2553
2615
|
if (synchronized.activeTurnId === activeTurnId) {
|
|
2616
|
+
this.recoveredLiveAttempts.add(attemptId);
|
|
2617
|
+
this.recoveryRetryStartedAt.delete(attemptId);
|
|
2554
2618
|
this.scheduleRecoveryRetry(attemptId, activeTurnId);
|
|
2555
2619
|
return;
|
|
2556
2620
|
}
|
|
@@ -2560,13 +2624,12 @@ export class ProxySessionController {
|
|
|
2560
2624
|
}
|
|
2561
2625
|
}
|
|
2562
2626
|
await this.recoverTurnAttempt(recoveryAttempt, false);
|
|
2627
|
+
if (!this.recoveryRetryTimers.has(attemptId)) this.recoveryRetryStartedAt.delete(attemptId);
|
|
2563
2628
|
};
|
|
2564
2629
|
void retry().catch((error: unknown) => {
|
|
2565
|
-
this.
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
);
|
|
2569
|
-
this.scheduleRecoveryRetry(attemptId, activeTurnId);
|
|
2630
|
+
return this.handleRecoveryError(attemptId, error, activeTurnId);
|
|
2631
|
+
}).catch((error: unknown) => {
|
|
2632
|
+
this.logger.error({ error, attemptId }, "Failed to finalize task recovery.");
|
|
2570
2633
|
});
|
|
2571
2634
|
}, 5_000);
|
|
2572
2635
|
timer.unref?.();
|
|
@@ -2579,6 +2642,23 @@ export class ProxySessionController {
|
|
|
2579
2642
|
turnId: string,
|
|
2580
2643
|
replyTarget?: MessageReplyTarget,
|
|
2581
2644
|
): Promise<void> {
|
|
2645
|
+
// Confirm resume before replacing the old card or announcing a running turn.
|
|
2646
|
+
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
2647
|
+
const loaded = await this.loadSession({
|
|
2648
|
+
...current, contextKey: attempt.contextKey, status: "running", lastTurnId: turnId, lastTurnStatus: "running",
|
|
2649
|
+
});
|
|
2650
|
+
const pendingAttempt = this.store.getTurnAttempt(attempt.attemptId);
|
|
2651
|
+
if (!pendingAttempt || !isIncompleteTurnAttemptStatus(pendingAttempt.status)) return;
|
|
2652
|
+
const continued = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
2653
|
+
if (continued?.continuedFromTurnId && this.store.getTurnDelivery(turnId)?.progressMessageId) {
|
|
2654
|
+
this.store.updateTurnAttempt(attempt.attemptId, { pendingTurnId: null, turnId, status: "running" });
|
|
2655
|
+
await this.outbound.onEvent({ type: "turn_started", sessionId: session.localSessionId,
|
|
2656
|
+
turnId, startedAt: continued.startedAt });
|
|
2657
|
+
const synchronized = await loaded.runtime.synchronizeSession(session.localSessionId);
|
|
2658
|
+
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
2659
|
+
if (synchronized.activeTurnId) this.recoveredLiveAttempts.add(attempt.attemptId);
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2582
2662
|
const oldCardTurnId = attempt.pendingTurnId ?? attempt.turnId;
|
|
2583
2663
|
if (oldCardTurnId) {
|
|
2584
2664
|
await this.outbound.interruptTurnForRecovery(
|
|
@@ -2607,16 +2687,12 @@ export class ProxySessionController {
|
|
|
2607
2687
|
startedAt: Date.now(),
|
|
2608
2688
|
});
|
|
2609
2689
|
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
2690
|
const synchronized = await loaded.runtime.synchronizeSession(session.localSessionId);
|
|
2619
|
-
|
|
2691
|
+
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
2692
|
+
if (synchronized.activeTurnId) {
|
|
2693
|
+
this.recoveredLiveAttempts.add(attempt.attemptId);
|
|
2694
|
+
this.scheduleRecoveryRetry(attempt.attemptId, synchronized.activeTurnId);
|
|
2695
|
+
}
|
|
2620
2696
|
}
|
|
2621
2697
|
|
|
2622
2698
|
private async continueInterruptedAttempt(
|
|
@@ -2648,7 +2724,8 @@ export class ProxySessionController {
|
|
|
2648
2724
|
});
|
|
2649
2725
|
}
|
|
2650
2726
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
2651
|
-
const loaded = await this.loadSession({ ...current, contextKey: attempt.contextKey }
|
|
2727
|
+
const loaded = await this.loadSession({ ...current, contextKey: attempt.contextKey },
|
|
2728
|
+
Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
2652
2729
|
const recoveryText = recoveryRuntimePrompt(attempt.promptText);
|
|
2653
2730
|
const turnId = await this.startTurn(
|
|
2654
2731
|
loaded,
|
|
@@ -2715,7 +2792,7 @@ export class ProxySessionController {
|
|
|
2715
2792
|
localImagePaths?: string[],
|
|
2716
2793
|
displayPrompt?: string,
|
|
2717
2794
|
): Promise<void> {
|
|
2718
|
-
if (this.llmRetryingSessions.has(record.localSessionId)) {
|
|
2795
|
+
if (this.llmRetryingSessions.has(record.localSessionId) || this.planHandoffs.has(record.localSessionId)) {
|
|
2719
2796
|
const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
|
|
2720
2797
|
localImagePaths,
|
|
2721
2798
|
messageId,
|
|
@@ -2751,7 +2828,7 @@ export class ProxySessionController {
|
|
|
2751
2828
|
}
|
|
2752
2829
|
let loaded: LoadedSession;
|
|
2753
2830
|
try {
|
|
2754
|
-
loaded = await this.loadSession(record);
|
|
2831
|
+
loaded = await this.loadSession(record, Boolean(this.store.getPendingProviderSettings(record.localSessionId)));
|
|
2755
2832
|
} catch (error) {
|
|
2756
2833
|
if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
|
|
2757
2834
|
throw new PresentedRequestError(error);
|
|
@@ -3891,6 +3968,25 @@ export class ProxySessionController {
|
|
|
3891
3968
|
}
|
|
3892
3969
|
|
|
3893
3970
|
private async startTurn(
|
|
3971
|
+
loaded: LoadedSession, text: string, replyTarget?: MessageReplyTarget,
|
|
3972
|
+
localImagePaths?: string[], options: StartTurnOptions = {},
|
|
3973
|
+
): Promise<string> {
|
|
3974
|
+
return this.settingsOperations.run(loaded.record.localSessionId, async () => {
|
|
3975
|
+
try {
|
|
3976
|
+
await this.applyPendingProviderSettings(loaded);
|
|
3977
|
+
} catch (error) {
|
|
3978
|
+
await this.outbound.failPendingTurn(loaded.record.localSessionId, runtimeErrorMessage(error));
|
|
3979
|
+
throw error;
|
|
3980
|
+
}
|
|
3981
|
+
if (this.store.getPendingProviderSettings(loaded.record.localSessionId)) {
|
|
3982
|
+
throw new Error("Provider 切换尚未完成,请稍后重试。");
|
|
3983
|
+
}
|
|
3984
|
+
loaded.session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
|
|
3985
|
+
return this.startTurnNow(loaded, text, replyTarget, localImagePaths, options);
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
private async startTurnNow(
|
|
3894
3990
|
loaded: LoadedSession,
|
|
3895
3991
|
text: string,
|
|
3896
3992
|
replyTarget?: MessageReplyTarget,
|
|
@@ -3946,9 +4042,15 @@ export class ProxySessionController {
|
|
|
3946
4042
|
this.store.updateTurnAttempt(attemptId, { pendingTurnId: pendingTurnId ?? null });
|
|
3947
4043
|
|
|
3948
4044
|
try {
|
|
4045
|
+
if (options.approvedPlanTurnId && (this.planHandoffs.get(loaded.record.localSessionId) !== options.approvedPlanTurnId
|
|
4046
|
+
|| this.sessionResets.has(loaded.record.localSessionId))) {
|
|
4047
|
+
throw new Error("方案执行已取消。");
|
|
4048
|
+
}
|
|
3949
4049
|
turnId = await loaded.runtime.startTurn(
|
|
3950
4050
|
loaded.record.localSessionId,
|
|
3951
|
-
|
|
4051
|
+
options.approvedPlanTurnId
|
|
4052
|
+
? { text: runtimeText, collaborationMode: "default" }
|
|
4053
|
+
: runtimePrompt(runtimeText, localImagePaths),
|
|
3952
4054
|
);
|
|
3953
4055
|
break;
|
|
3954
4056
|
} catch (error) {
|
|
@@ -4521,18 +4623,27 @@ export class ProxySessionController {
|
|
|
4521
4623
|
return { chatId: group.chatId, contextKey: groupContextKey, name: group.name };
|
|
4522
4624
|
}
|
|
4523
4625
|
|
|
4524
|
-
private async loadSession(record: SessionRecord): Promise<LoadedSession> {
|
|
4626
|
+
private async loadSession(record: SessionRecord, forSettings = false): Promise<LoadedSession> {
|
|
4525
4627
|
const agent = this.ensureAgent(record.agentName);
|
|
4526
4628
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
4527
4629
|
const existing = runtime.getSession(record.localSessionId);
|
|
4528
|
-
if (existing
|
|
4630
|
+
if (existing && (forSettings || !this.settingsOnlySessions.has(record.localSessionId))) {
|
|
4631
|
+
return { record, runtime, session: existing };
|
|
4632
|
+
}
|
|
4529
4633
|
const pending = this.sessionLoads.get(record.localSessionId);
|
|
4530
4634
|
if (pending) {
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4635
|
+
try {
|
|
4636
|
+
const loaded = await pending;
|
|
4637
|
+
if (!forSettings && this.settingsOnlySessions.has(record.localSessionId)) {
|
|
4638
|
+
if (this.sessionLoads.get(record.localSessionId) === pending) this.sessionLoads.delete(record.localSessionId);
|
|
4639
|
+
return this.loadSession(this.store.getSession(record.localSessionId) ?? record);
|
|
4640
|
+
}
|
|
4641
|
+
return { ...loaded, record: { ...loaded.record, contextKey: record.contextKey } };
|
|
4642
|
+
} catch (error) {
|
|
4643
|
+
if (!forSettings || !runtime.prepareSessionForSettings) throw error;
|
|
4644
|
+
if (this.sessionLoads.get(record.localSessionId) === pending) this.sessionLoads.delete(record.localSessionId);
|
|
4645
|
+
return this.loadSession(record, forSettings);
|
|
4646
|
+
}
|
|
4536
4647
|
}
|
|
4537
4648
|
|
|
4538
4649
|
this.outbound.registerSession(
|
|
@@ -4544,7 +4655,16 @@ export class ProxySessionController {
|
|
|
4544
4655
|
record.model,
|
|
4545
4656
|
record.modelProvider,
|
|
4546
4657
|
);
|
|
4547
|
-
const loading = (async (): Promise<LoadedSession> => {
|
|
4658
|
+
const loading = this.sessionHydrationOperations.run(record.localSessionId, async (): Promise<LoadedSession> => {
|
|
4659
|
+
const latest = this.store.getSession(record.localSessionId);
|
|
4660
|
+
if (latest) record = {
|
|
4661
|
+
...record, modelProvider: latest.modelProvider, model: latest.model,
|
|
4662
|
+
reasoningEffort: latest.reasoningEffort, permissionMode: latest.permissionMode,
|
|
4663
|
+
};
|
|
4664
|
+
const current = runtime.getSession(record.localSessionId);
|
|
4665
|
+
if (current && (forSettings || !this.settingsOnlySessions.has(record.localSessionId))) {
|
|
4666
|
+
return { record, runtime, session: current };
|
|
4667
|
+
}
|
|
4548
4668
|
if (record.lastTurnId) {
|
|
4549
4669
|
try {
|
|
4550
4670
|
await this.outbound.resumeDelivery(record.localSessionId, record.contextKey, record.lastTurnId);
|
|
@@ -4556,41 +4676,30 @@ export class ProxySessionController {
|
|
|
4556
4676
|
}
|
|
4557
4677
|
}
|
|
4558
4678
|
const permissionMode = record.permissionMode ?? "auto";
|
|
4559
|
-
|
|
4679
|
+
const prepare = forSettings && record.remoteSessionId && runtime.prepareSessionForSettings;
|
|
4680
|
+
if (record.remoteSessionId && !prepare) await this.assertSessionTurnOwnership(record, runtime);
|
|
4560
4681
|
let session: RuntimeSession;
|
|
4561
4682
|
if (record.remoteSessionId) {
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
&& !this.store.getForkHistorySource(record.localSessionId) && isMissingRolloutError(error))) throw error;
|
|
4583
|
-
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
4584
|
-
session = await runtime.createSession({
|
|
4585
|
-
localSessionId: record.localSessionId,
|
|
4586
|
-
agentName: record.agentName,
|
|
4587
|
-
cwd: record.cwd,
|
|
4588
|
-
title: record.title,
|
|
4589
|
-
modelProvider: record.modelProvider,
|
|
4590
|
-
model: record.model,
|
|
4591
|
-
reasoningEffort: record.reasoningEffort,
|
|
4592
|
-
permissionMode,
|
|
4593
|
-
});
|
|
4683
|
+
const input = {
|
|
4684
|
+
localSessionId: record.localSessionId,
|
|
4685
|
+
remoteSessionId: record.remoteSessionId,
|
|
4686
|
+
agentName: record.agentName,
|
|
4687
|
+
cwd: record.cwd,
|
|
4688
|
+
title: record.title,
|
|
4689
|
+
modelProvider: record.modelProvider,
|
|
4690
|
+
model: record.model,
|
|
4691
|
+
reasoningEffort: record.reasoningEffort,
|
|
4692
|
+
permissionMode,
|
|
4693
|
+
activeTurnId: agent.kind === "app-server" && record.status === "running" && record.lastTurnStatus === "running"
|
|
4694
|
+
? record.lastTurnId
|
|
4695
|
+
: undefined,
|
|
4696
|
+
lastTurnId: record.lastTurnId,
|
|
4697
|
+
lastTurnStatus: record.lastTurnStatus,
|
|
4698
|
+
};
|
|
4699
|
+
if (prepare) this.settingsOnlySessions.add(record.localSessionId);
|
|
4700
|
+
session = prepare ? await prepare.call(runtime, input) : await runtime.resumeSession(input);
|
|
4701
|
+
if (session.remoteSessionId !== record.remoteSessionId) {
|
|
4702
|
+
throw new Error("加载设置返回了不同的任务 ID,不允许替换任务或更改历史。");
|
|
4594
4703
|
}
|
|
4595
4704
|
} else {
|
|
4596
4705
|
session = await runtime.createSession({
|
|
@@ -4604,19 +4713,22 @@ export class ProxySessionController {
|
|
|
4604
4713
|
permissionMode,
|
|
4605
4714
|
});
|
|
4606
4715
|
}
|
|
4607
|
-
|
|
4716
|
+
if (!prepare) {
|
|
4717
|
+
this.settingsOnlySessions.delete(record.localSessionId);
|
|
4718
|
+
this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
|
|
4719
|
+
}
|
|
4608
4720
|
const saved = this.store.getSession(record.localSessionId) ?? record;
|
|
4609
4721
|
return {
|
|
4610
4722
|
record: { ...saved, contextKey: record.contextKey },
|
|
4611
4723
|
runtime,
|
|
4612
4724
|
session,
|
|
4613
4725
|
};
|
|
4614
|
-
})
|
|
4726
|
+
});
|
|
4615
4727
|
this.sessionLoads.set(record.localSessionId, loading);
|
|
4616
4728
|
try {
|
|
4617
4729
|
return await loading;
|
|
4618
4730
|
} finally {
|
|
4619
|
-
this.sessionLoads.delete(record.localSessionId);
|
|
4731
|
+
if (this.sessionLoads.get(record.localSessionId) === loading) this.sessionLoads.delete(record.localSessionId);
|
|
4620
4732
|
}
|
|
4621
4733
|
}
|
|
4622
4734
|
|
|
@@ -4638,6 +4750,11 @@ export class ProxySessionController {
|
|
|
4638
4750
|
|
|
4639
4751
|
private async handleRuntimeEvent(event: RuntimeEvent): Promise<void> {
|
|
4640
4752
|
if ("turnId" in event) {
|
|
4753
|
+
const recoveryAttempt = this.recoveryRetryStartedAt.size > 0
|
|
4754
|
+
? this.store.findIncompleteTurnAttemptByTurnId(event.turnId) : undefined;
|
|
4755
|
+
if (recoveryAttempt && this.recoveryRetryStartedAt.has(recoveryAttempt.attemptId)) {
|
|
4756
|
+
this.recoveredLiveAttempts.add(recoveryAttempt.attemptId);
|
|
4757
|
+
}
|
|
4641
4758
|
this.store.touchTurnAttempt(event.turnId);
|
|
4642
4759
|
const source = this.store.getSession(event.sessionId);
|
|
4643
4760
|
if (source?.remoteSessionId && this.isCodexSession(source)) {
|
|
@@ -4660,6 +4777,14 @@ export class ProxySessionController {
|
|
|
4660
4777
|
);
|
|
4661
4778
|
}
|
|
4662
4779
|
}
|
|
4780
|
+
if (event.type === "turn_completed" && event.approvedPlan) {
|
|
4781
|
+
if (this.planHandoffs.get(event.sessionId) === event.turnId) return;
|
|
4782
|
+
const attempt = this.store.findIncompleteTurnAttemptByTurnId(event.turnId);
|
|
4783
|
+
if (attempt) {
|
|
4784
|
+
await this.continueApprovedPlan(event, attempt);
|
|
4785
|
+
return;
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4663
4788
|
const failedAttempt = event.type === "turn_failed"
|
|
4664
4789
|
? this.store.findIncompleteTurnAttemptByTurnId(event.turnId)
|
|
4665
4790
|
: undefined;
|
|
@@ -4683,6 +4808,13 @@ export class ProxySessionController {
|
|
|
4683
4808
|
this.store.updateSession(event.sessionId, { status: "running" });
|
|
4684
4809
|
this.store.updateRuntimeSession(event.sessionId, { lastTurnId: event.turnId, lastTurnStatus: "running" });
|
|
4685
4810
|
} else if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
|
|
4811
|
+
const attempt = this.store.findIncompleteTurnAttemptByTurnId(event.turnId);
|
|
4812
|
+
if (attempt) {
|
|
4813
|
+
clearTimeout(this.recoveryRetryTimers.get(attempt.attemptId));
|
|
4814
|
+
this.recoveryRetryTimers.delete(attempt.attemptId);
|
|
4815
|
+
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
4816
|
+
this.recoveredLiveAttempts.delete(attempt.attemptId);
|
|
4817
|
+
}
|
|
4686
4818
|
this.store.markTurnAttemptTerminal(
|
|
4687
4819
|
event.turnId,
|
|
4688
4820
|
event.type === "turn_completed" ? "completed" : event.type === "turn_cancelled" ? "cancelled" : "failed",
|
|
@@ -4698,7 +4830,7 @@ export class ProxySessionController {
|
|
|
4698
4830
|
...event,
|
|
4699
4831
|
turnCount: event.turnCount ?? this.store.countTaskTurns(event.sessionId),
|
|
4700
4832
|
}
|
|
4701
|
-
: event;
|
|
4833
|
+
: event.type === "turn_completed" && event.approvedPlan ? { ...event, approvedPlan: false } : event;
|
|
4702
4834
|
const presentationEvent = event.type === "turn_failed"
|
|
4703
4835
|
&& isRetryableLlmTurnFailure(event.message)
|
|
4704
4836
|
&& (failedAttempt?.retryCount ?? 0) >= MAX_LLM_TURN_RETRIES
|
|
@@ -4736,6 +4868,7 @@ export class ProxySessionController {
|
|
|
4736
4868
|
: undefined;
|
|
4737
4869
|
if (latest?.lastTurnId === event.turnId && !activeTurnId) {
|
|
4738
4870
|
this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
|
|
4871
|
+
await this.applyPendingProviderWhenIdle(event.sessionId);
|
|
4739
4872
|
await this.scheduleNextQueuedPrompt(event.sessionId);
|
|
4740
4873
|
}
|
|
4741
4874
|
}
|
|
@@ -4743,6 +4876,80 @@ export class ProxySessionController {
|
|
|
4743
4876
|
if (presentationError) throw presentationError;
|
|
4744
4877
|
}
|
|
4745
4878
|
|
|
4879
|
+
private async continueApprovedPlan(
|
|
4880
|
+
event: Extract<RuntimeEvent, { type: "turn_completed" }>,
|
|
4881
|
+
attempt: TurnAttemptRecord,
|
|
4882
|
+
): Promise<void> {
|
|
4883
|
+
const { sessionId, turnId } = event;
|
|
4884
|
+
const source = this.store.getSession(sessionId);
|
|
4885
|
+
if (!source || source.status === "closed" || source.lastTurnId !== turnId) return;
|
|
4886
|
+
this.planHandoffs.set(sessionId, turnId);
|
|
4887
|
+
this.store.markTurnAttemptTerminal(turnId, "completed");
|
|
4888
|
+
this.store.updateRuntimeSession(sessionId, { lastTurnId: turnId, lastTurnStatus: "completed" });
|
|
4889
|
+
let continued = false;
|
|
4890
|
+
let failed = false;
|
|
4891
|
+
try {
|
|
4892
|
+
try {
|
|
4893
|
+
await this.outbound.onEvent(event);
|
|
4894
|
+
} catch (error) {
|
|
4895
|
+
this.logger.warn({ error, sessionId, turnId }, "Failed to finalize the approved plan card.");
|
|
4896
|
+
}
|
|
4897
|
+
await this.settingsOperations.run(sessionId, async () => {
|
|
4898
|
+
const current = this.store.getSessionForContext(sessionId, attempt.contextKey);
|
|
4899
|
+
if (this.planHandoffs.get(sessionId) !== turnId || this.sessionResets.has(sessionId)
|
|
4900
|
+
|| !current || current.status === "closed" || current.remoteSessionId !== source.remoteSessionId) return;
|
|
4901
|
+
const loaded = await this.loadSession(current);
|
|
4902
|
+
await this.assertSessionTurnOwnership(current, loaded.runtime);
|
|
4903
|
+
if (this.planHandoffs.get(sessionId) !== turnId || this.sessionResets.has(sessionId)) return;
|
|
4904
|
+
// Another client may already have started the implementation.
|
|
4905
|
+
let nextTurnId = loaded.session.activeTurnId;
|
|
4906
|
+
if (!nextTurnId) {
|
|
4907
|
+
if (current.lastTurnId !== turnId) return;
|
|
4908
|
+
await this.applyPendingProviderSettings(loaded);
|
|
4909
|
+
if (this.store.getPendingProviderSettings(sessionId)) throw new Error("Provider 切换尚未完成。");
|
|
4910
|
+
if (this.planHandoffs.get(sessionId) !== turnId || this.sessionResets.has(sessionId)) return;
|
|
4911
|
+
nextTurnId = await this.startTurnNow(
|
|
4912
|
+
loaded,
|
|
4913
|
+
"The proposed plan has been approved. Continue in the current task and workspace without asking for plan approval again; preserve the existing conversation and completed work. Implement the plan unless the user explicitly requested discussion or planning only, or prohibited implementation; those constraints still apply.",
|
|
4914
|
+
attempt.replyMessageId ? { messageId: attempt.replyMessageId, replyInThread: true } : undefined,
|
|
4915
|
+
undefined,
|
|
4916
|
+
{ messageId: attempt.messageId, displayPrompt: "方案已确认,继续执行", approvedPlanTurnId: turnId },
|
|
4917
|
+
);
|
|
4918
|
+
}
|
|
4919
|
+
continued = true;
|
|
4920
|
+
this.store.rebindPendingTurnMessages(sessionId, turnId, nextTurnId);
|
|
4921
|
+
const latest = this.store.getSession(sessionId);
|
|
4922
|
+
if (latest?.lastTurnId === nextTurnId && (latest.lastTurnStatus === "completed"
|
|
4923
|
+
|| latest.lastTurnStatus === "failed" || latest.lastTurnStatus === "cancelled")) {
|
|
4924
|
+
await this.finalizeTurnMessageReactions(nextTurnId, latest.lastTurnStatus);
|
|
4925
|
+
}
|
|
4926
|
+
this.store.audit(attempt.contextKey, "approved_plan_continued", { sessionId, turnId, nextTurnId });
|
|
4927
|
+
});
|
|
4928
|
+
} catch (error) {
|
|
4929
|
+
failed = true;
|
|
4930
|
+
this.logger.error({ error, sessionId, turnId }, "Failed to continue an approved plan.");
|
|
4931
|
+
await this.outbound.sendText(attempt.contextKey,
|
|
4932
|
+
"方案已确认,但后续执行启动失败:" + runtimeErrorMessage(error) + "。请发送消息继续。");
|
|
4933
|
+
} finally {
|
|
4934
|
+
if (this.planHandoffs.get(sessionId) === turnId) this.planHandoffs.delete(sessionId);
|
|
4935
|
+
if (!continued) {
|
|
4936
|
+
try {
|
|
4937
|
+
await this.outbound.onEvent(failed
|
|
4938
|
+
? { type: "turn_failed", sessionId, turnId, message: "方案已确认,但后续执行未能启动。" }
|
|
4939
|
+
: { type: "turn_cancelled", sessionId, turnId });
|
|
4940
|
+
} catch (error) {
|
|
4941
|
+
this.logger.warn({ error, sessionId, turnId }, "Failed to finish the plan handoff card.");
|
|
4942
|
+
}
|
|
4943
|
+
await this.finalizeTurnMessageReactions(turnId, failed ? "failed" : "cancelled");
|
|
4944
|
+
}
|
|
4945
|
+
const latest = this.store.getSession(sessionId);
|
|
4946
|
+
if (latest?.lastTurnId === turnId && latest.status !== "closed") {
|
|
4947
|
+
this.store.updateSession(sessionId, { status: failed ? "failed" : "ready" });
|
|
4948
|
+
}
|
|
4949
|
+
await this.scheduleNextQueuedPrompt(sessionId);
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4746
4953
|
private async retryFailedLlmTurn(
|
|
4747
4954
|
event: Extract<RuntimeEvent, { type: "turn_failed" }>,
|
|
4748
4955
|
attempt: TurnAttemptRecord,
|
|
@@ -4784,7 +4991,7 @@ export class ProxySessionController {
|
|
|
4784
4991
|
: undefined;
|
|
4785
4992
|
try {
|
|
4786
4993
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
4787
|
-
const loaded = await this.loadSession(current);
|
|
4994
|
+
const loaded = await this.loadSession(current, Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
4788
4995
|
const retryTurnId = await this.startTurn(
|
|
4789
4996
|
loaded,
|
|
4790
4997
|
llmRetryRuntimePrompt(attempt.promptText, retryNumber),
|
|
@@ -4832,6 +5039,7 @@ export class ProxySessionController {
|
|
|
4832
5039
|
}
|
|
4833
5040
|
|
|
4834
5041
|
private scheduleQueuedPromptRetry(sessionId: string): void {
|
|
5042
|
+
if (this.store.getPendingProviderSettings(sessionId)?.error) return;
|
|
4835
5043
|
if (this.queuedPromptRetryTimers.has(sessionId)) return;
|
|
4836
5044
|
if (this.store.countQueuedPrompts(sessionId) === 0) return;
|
|
4837
5045
|
const timer = setTimeout(() => {
|
|
@@ -4849,24 +5057,31 @@ export class ProxySessionController {
|
|
|
4849
5057
|
}
|
|
4850
5058
|
|
|
4851
5059
|
private async startNextQueuedPromptIfIdle(sessionId: string): Promise<void> {
|
|
5060
|
+
return this.settingsOperations.run(sessionId, () => this.startNextQueuedPromptIfIdleNow(sessionId));
|
|
5061
|
+
}
|
|
5062
|
+
|
|
5063
|
+
private async startNextQueuedPromptIfIdleNow(sessionId: string): Promise<void> {
|
|
5064
|
+
if (this.planHandoffs.has(sessionId) || this.sessionResets.has(sessionId)) return;
|
|
4852
5065
|
if (this.store.countQueuedPrompts(sessionId) === 0) return;
|
|
4853
5066
|
const baseRecord = this.store.getSession(sessionId);
|
|
4854
5067
|
if (!baseRecord || baseRecord.status === "closed") return;
|
|
4855
5068
|
let prompt: QueuedPromptRecord | undefined;
|
|
4856
5069
|
try {
|
|
4857
|
-
let loaded = await this.loadSession(baseRecord);
|
|
5070
|
+
let loaded = await this.loadSession(baseRecord, Boolean(this.store.getPendingProviderSettings(sessionId)));
|
|
4858
5071
|
const remoteActivity = await this.assertSessionTurnOwnership(loaded.record, loaded.runtime);
|
|
4859
5072
|
if (needsFullSessionSynchronization(loaded.record, loaded.session, remoteActivity)) {
|
|
4860
5073
|
loaded = { ...loaded, session: await loaded.runtime.synchronizeSession(sessionId) };
|
|
4861
5074
|
}
|
|
4862
5075
|
if (loaded.session.activeTurnId) return;
|
|
4863
5076
|
|
|
5077
|
+
await this.applyPendingProviderSettings(loaded);
|
|
5078
|
+
if (this.store.getPendingProviderSettings(sessionId)) return;
|
|
4864
5079
|
prompt = this.store.takeNextQueuedPrompt(sessionId);
|
|
4865
5080
|
if (!prompt) return;
|
|
4866
5081
|
const record = this.store.getSessionForContext(sessionId, prompt.contextKey) ?? baseRecord;
|
|
4867
5082
|
loaded = { ...loaded, record };
|
|
4868
5083
|
await this.refreshPromptQueueCards(sessionId);
|
|
4869
|
-
const turnId = await this.
|
|
5084
|
+
const turnId = await this.startTurnNow(
|
|
4870
5085
|
loaded,
|
|
4871
5086
|
prompt.text,
|
|
4872
5087
|
prompt.replyMessageId ? { messageId: prompt.replyMessageId, replyInThread: true } : undefined,
|
|
@@ -4937,6 +5152,7 @@ export class ProxySessionController {
|
|
|
4937
5152
|
});
|
|
4938
5153
|
const pending = {
|
|
4939
5154
|
contextKey: message.contextKey,
|
|
5155
|
+
userId: message.userId,
|
|
4940
5156
|
attachmentPromise,
|
|
4941
5157
|
resolveAttachment,
|
|
4942
5158
|
};
|
|
@@ -4945,14 +5161,15 @@ export class ProxySessionController {
|
|
|
4945
5161
|
}
|
|
4946
5162
|
|
|
4947
5163
|
private reserveForwardAttachment(message: IncomingMessage): ForwardAttachmentReservation | undefined {
|
|
4948
|
-
if (!message.parentMessageId) return undefined;
|
|
5164
|
+
if (!message.parentMessageId || isCommandMessage(message)) return undefined;
|
|
4949
5165
|
const candidates = [
|
|
4950
5166
|
{ sourceKind: "merged_forward" as const, registry: this.pendingMergedForwards },
|
|
4951
5167
|
{ sourceKind: "resource" as const, registry: this.pendingResourceForwards },
|
|
4952
5168
|
];
|
|
4953
5169
|
for (const candidate of candidates) {
|
|
4954
5170
|
const pending = candidate.registry.get(message.parentMessageId);
|
|
4955
|
-
if (!pending || pending.contextKey !== message.contextKey
|
|
5171
|
+
if (!pending || pending.contextKey !== message.contextKey
|
|
5172
|
+
|| pending.userId !== message.userId || pending.attachmentMessageId) continue;
|
|
4956
5173
|
pending.attachmentMessageId = message.messageId;
|
|
4957
5174
|
return {
|
|
4958
5175
|
sourceMessageId: message.parentMessageId,
|
|
@@ -4981,15 +5198,17 @@ export class ProxySessionController {
|
|
|
4981
5198
|
pending: PendingForwardAttachment,
|
|
4982
5199
|
registry: Map<string, PendingForwardAttachment>,
|
|
4983
5200
|
): Promise<IncomingMessage | undefined> {
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
5201
|
+
const timer = setTimeout(() => {
|
|
5202
|
+
// Arrival reserves the pair before acknowledgement. A slow Reaction must not split it.
|
|
5203
|
+
if (pending.attachmentMessageId) return;
|
|
5204
|
+
if (registry.get(messageId) === pending) registry.delete(messageId);
|
|
5205
|
+
pending.resolveAttachment(undefined);
|
|
5206
|
+
}, FORWARD_ATTACHMENT_WINDOW_MS);
|
|
5207
|
+
timer.unref?.();
|
|
4989
5208
|
try {
|
|
4990
|
-
return await
|
|
5209
|
+
return await pending.attachmentPromise;
|
|
4991
5210
|
} finally {
|
|
4992
|
-
|
|
5211
|
+
clearTimeout(timer);
|
|
4993
5212
|
if (registry.get(messageId) === pending) {
|
|
4994
5213
|
registry.delete(messageId);
|
|
4995
5214
|
}
|
|
@@ -5068,6 +5287,7 @@ export class ProxySessionController {
|
|
|
5068
5287
|
}
|
|
5069
5288
|
|
|
5070
5289
|
private async cancelSession(record: SessionRecord): Promise<void> {
|
|
5290
|
+
this.planHandoffs.delete(record.localSessionId);
|
|
5071
5291
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
5072
5292
|
if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
|
|
5073
5293
|
if (runtime.getGoal && runtime.setGoal) {
|
|
@@ -5146,6 +5366,7 @@ export class ProxySessionController {
|
|
|
5146
5366
|
}
|
|
5147
5367
|
|
|
5148
5368
|
private async runSessionReset(sessionId: string, operation: () => Promise<void>): Promise<void> {
|
|
5369
|
+
this.planHandoffs.delete(sessionId);
|
|
5149
5370
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
5150
5371
|
const reset = previous.catch(() => undefined).then(operation);
|
|
5151
5372
|
this.sessionResets.set(sessionId, reset);
|
|
@@ -5329,6 +5550,20 @@ export class ProxySessionController {
|
|
|
5329
5550
|
contextKey: string,
|
|
5330
5551
|
activeTab: ExecutionSettingsTab,
|
|
5331
5552
|
options: ExecutionSettingsCardOptions = {},
|
|
5553
|
+
): Promise<void> {
|
|
5554
|
+
const record = options.sessionId
|
|
5555
|
+
? this.requireSession(contextKey, options.sessionId)
|
|
5556
|
+
: this.currentSession(contextKey);
|
|
5557
|
+
if (!record) return this.openExecutionSettingsNow(contextKey, activeTab, options);
|
|
5558
|
+
return this.settingsOperations.run(record.localSessionId, () => this.openExecutionSettingsNow(
|
|
5559
|
+
contextKey, activeTab, { ...options, sessionId: record.localSessionId },
|
|
5560
|
+
));
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
private async openExecutionSettingsNow(
|
|
5564
|
+
contextKey: string,
|
|
5565
|
+
activeTab: ExecutionSettingsTab,
|
|
5566
|
+
options: ExecutionSettingsCardOptions = {},
|
|
5332
5567
|
): Promise<void> {
|
|
5333
5568
|
const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
|
|
5334
5569
|
const record = options.sessionId
|
|
@@ -5349,24 +5584,33 @@ export class ProxySessionController {
|
|
|
5349
5584
|
reasoningOptions: [],
|
|
5350
5585
|
};
|
|
5351
5586
|
if (record) {
|
|
5352
|
-
const loaded = await this.loadSession(record);
|
|
5587
|
+
const loaded = await this.loadSession(record, true);
|
|
5588
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
5589
|
+
const selected = pending?.settings ?? loaded.session;
|
|
5590
|
+
if (pending && options.updateMessageId) {
|
|
5591
|
+
this.store.savePendingProviderSettings({
|
|
5592
|
+
...pending, contextKey, messageId: options.updateMessageId, replyMessageId: options.updateMessageId, activeTab,
|
|
5593
|
+
});
|
|
5594
|
+
}
|
|
5353
5595
|
const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
|
|
5354
5596
|
const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
|
|
5355
|
-
const currentProvider =
|
|
5356
|
-
const { models, warning } = await this.modelOptions(loaded.runtime, currentProvider,
|
|
5357
|
-
const currentModel = models.find((model) => model.id ===
|
|
5597
|
+
const currentProvider = selected.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
|
|
5598
|
+
const { models, warning } = await this.modelOptions(loaded.runtime, currentProvider, selected.model);
|
|
5599
|
+
const currentModel = models.find((model) => model.id === selected.model)
|
|
5358
5600
|
?? models.find((model) => model.isDefault)
|
|
5359
5601
|
?? models[0];
|
|
5360
5602
|
const currentEffort = currentModel?.supportedReasoningEfforts.some(
|
|
5361
|
-
(option) => option.value ===
|
|
5603
|
+
(option) => option.value === selected.reasoningEffort,
|
|
5362
5604
|
)
|
|
5363
|
-
?
|
|
5364
|
-
: currentModel?.defaultReasoningEffort ??
|
|
5605
|
+
? selected.reasoningEffort
|
|
5606
|
+
: currentModel?.defaultReasoningEffort ?? selected.reasoningEffort;
|
|
5365
5607
|
runtimeSettings = {
|
|
5608
|
+
pendingNotice: pending ? this.pendingProviderNotice(pending) : undefined,
|
|
5609
|
+
pendingFailed: Boolean(pending?.error),
|
|
5366
5610
|
currentProvider,
|
|
5367
|
-
currentModel: currentModel?.id ??
|
|
5611
|
+
currentModel: pending?.settings.model ?? currentModel?.id ?? selected.model,
|
|
5368
5612
|
currentEffort,
|
|
5369
|
-
currentPermissionMode:
|
|
5613
|
+
currentPermissionMode: selected.permissionMode,
|
|
5370
5614
|
providers,
|
|
5371
5615
|
providerSupported,
|
|
5372
5616
|
models,
|
|
@@ -5382,7 +5626,10 @@ export class ProxySessionController {
|
|
|
5382
5626
|
agents,
|
|
5383
5627
|
runtimeSettingsAvailable: Boolean(record),
|
|
5384
5628
|
...runtimeSettings,
|
|
5629
|
+
modelPage: options.expectedProvider !== undefined && options.expectedProvider !== (runtimeSettings.currentProvider ?? "")
|
|
5630
|
+
? undefined : options.modelPage,
|
|
5385
5631
|
notice: options.notice,
|
|
5632
|
+
warning: [runtimeSettings.warning, options.warning].filter(Boolean).join("\n") || undefined,
|
|
5386
5633
|
});
|
|
5387
5634
|
if (options.updateMessageId) {
|
|
5388
5635
|
await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
|
|
@@ -5400,7 +5647,12 @@ export class ProxySessionController {
|
|
|
5400
5647
|
await this.openExecutionSettings(contextKey, "agent");
|
|
5401
5648
|
}
|
|
5402
5649
|
|
|
5403
|
-
private async selectProvider(
|
|
5650
|
+
private async selectProvider(contextKey: string, modelProvider: string, options: ModelCardOptions = {}): Promise<void> {
|
|
5651
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
5652
|
+
return this.settingsOperations.run(record.localSessionId, () => this.selectProviderNow(contextKey, modelProvider, { ...options, sessionId: record.localSessionId }));
|
|
5653
|
+
}
|
|
5654
|
+
|
|
5655
|
+
private async selectProviderNow(
|
|
5404
5656
|
contextKey: string,
|
|
5405
5657
|
modelProvider: string,
|
|
5406
5658
|
options: ModelCardOptions = {},
|
|
@@ -5408,19 +5660,19 @@ export class ProxySessionController {
|
|
|
5408
5660
|
const record = options.sessionId
|
|
5409
5661
|
? this.requireSession(contextKey, options.sessionId)
|
|
5410
5662
|
: this.requireCurrentSession(contextKey);
|
|
5411
|
-
await this.
|
|
5412
|
-
const
|
|
5663
|
+
const loaded = await this.loadSession(record, true);
|
|
5664
|
+
const selected = this.store.getPendingProviderSettings(record.localSessionId)?.settings ?? loaded.session;
|
|
5413
5665
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5414
|
-
const models = await loaded.runtime.listModels(modelProvider,
|
|
5415
|
-
const model = selectProviderModel(models,
|
|
5666
|
+
const models = await loaded.runtime.listModels(modelProvider, selected.model);
|
|
5667
|
+
const model = selectProviderModel(models, selected.model);
|
|
5416
5668
|
if (!model) throw new Error("当前运行时没有可用于 Provider 切换的模型。");
|
|
5417
|
-
const effort = selectProviderReasoningEffort(model,
|
|
5418
|
-
await this.
|
|
5669
|
+
const effort = selectProviderReasoningEffort(model, selected.reasoningEffort);
|
|
5670
|
+
await this.applyProviderSettingsNow(contextKey, {
|
|
5419
5671
|
provider: modelProvider,
|
|
5420
5672
|
model: model.id,
|
|
5421
5673
|
effort,
|
|
5422
|
-
mode:
|
|
5423
|
-
}, options
|
|
5674
|
+
mode: selected.permissionMode,
|
|
5675
|
+
}, options);
|
|
5424
5676
|
}
|
|
5425
5677
|
|
|
5426
5678
|
private async openProviderSelector(
|
|
@@ -5436,14 +5688,16 @@ export class ProxySessionController {
|
|
|
5436
5688
|
permissionMode: PermissionMode,
|
|
5437
5689
|
sessionId: string,
|
|
5438
5690
|
updateMessageId: string,
|
|
5691
|
+
modelPage?: number,
|
|
5439
5692
|
): Promise<void> {
|
|
5440
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5693
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
5441
5694
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5442
5695
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, loaded.session.model);
|
|
5443
5696
|
const card = this.cardRenderer.renderModelSelector({
|
|
5444
5697
|
sessionId,
|
|
5445
5698
|
contextKey,
|
|
5446
5699
|
currentModel: selectProviderModel(models, loaded.session.model)?.id,
|
|
5700
|
+
modelPage,
|
|
5447
5701
|
reasoningEffort: loaded.session.reasoningEffort,
|
|
5448
5702
|
models,
|
|
5449
5703
|
warning,
|
|
@@ -5462,7 +5716,7 @@ export class ProxySessionController {
|
|
|
5462
5716
|
sessionId: string,
|
|
5463
5717
|
updateMessageId: string,
|
|
5464
5718
|
): Promise<void> {
|
|
5465
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5719
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
5466
5720
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5467
5721
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
|
|
5468
5722
|
const selected = models.find((candidate) => candidate.id === model);
|
|
@@ -5498,7 +5752,7 @@ export class ProxySessionController {
|
|
|
5498
5752
|
sessionId: string,
|
|
5499
5753
|
updateMessageId: string,
|
|
5500
5754
|
): Promise<void> {
|
|
5501
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5755
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
5502
5756
|
await this.assertProviderModelSettings(loaded, modelProvider, model, effort);
|
|
5503
5757
|
await this.outbound.updateInteractiveCard(
|
|
5504
5758
|
contextKey,
|
|
@@ -5514,7 +5768,12 @@ export class ProxySessionController {
|
|
|
5514
5768
|
);
|
|
5515
5769
|
}
|
|
5516
5770
|
|
|
5517
|
-
private async applyProviderSettings(
|
|
5771
|
+
private async applyProviderSettings(contextKey: string, settings: { provider: string; model: string; effort: string; mode: PermissionMode }, options: ModelCardOptions = {}, announceStart = true): Promise<void> {
|
|
5772
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
5773
|
+
return this.settingsOperations.run(record.localSessionId, () => this.applyProviderSettingsNow(contextKey, settings, { ...options, sessionId: record.localSessionId }, announceStart));
|
|
5774
|
+
}
|
|
5775
|
+
|
|
5776
|
+
private async applyProviderSettingsNow(
|
|
5518
5777
|
contextKey: string,
|
|
5519
5778
|
settings: { provider: string; model: string; effort: string; mode: PermissionMode },
|
|
5520
5779
|
options: ModelCardOptions = {},
|
|
@@ -5523,17 +5782,38 @@ export class ProxySessionController {
|
|
|
5523
5782
|
const record = options.sessionId
|
|
5524
5783
|
? this.requireSession(contextKey, options.sessionId)
|
|
5525
5784
|
: this.requireCurrentSession(contextKey);
|
|
5526
|
-
|
|
5527
|
-
await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
5528
|
-
}
|
|
5529
|
-
const loaded = await this.loadSession(record);
|
|
5785
|
+
const loaded = await this.loadSession(record, true);
|
|
5530
5786
|
const warning = await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
5531
|
-
const
|
|
5532
|
-
modelProvider: settings.provider,
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5787
|
+
const next: RuntimeExecutionSettings = {
|
|
5788
|
+
modelProvider: settings.provider, model: settings.model,
|
|
5789
|
+
reasoningEffort: settings.effort, permissionMode: settings.mode,
|
|
5790
|
+
};
|
|
5791
|
+
await this.reconcilePreparedSettingsSession(loaded);
|
|
5792
|
+
if (loaded.session.activeTurnId || this.store.getPendingProviderSettings(record.localSessionId)) {
|
|
5793
|
+
await this.savePendingProviderSelection(loaded, next, contextKey, options, "provider");
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5796
|
+
if (announceStart) await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
5797
|
+
const previous = { ...loaded.session };
|
|
5798
|
+
let session: RuntimeSession;
|
|
5799
|
+
try {
|
|
5800
|
+
session = await this.changeProviderSettings(loaded, next);
|
|
5801
|
+
} catch (error) {
|
|
5802
|
+
if (!this.providerSettingsRestored(loaded, previous, error)) {
|
|
5803
|
+
this.store.savePendingProviderSettings({
|
|
5804
|
+
localSessionId: record.localSessionId, contextKey, settings: next, activeTab: "provider",
|
|
5805
|
+
messageId: options.updateMessageId,
|
|
5806
|
+
replyMessageId: options.updateMessageId ?? this.controlSessionReplyTarget(record)?.messageId,
|
|
5807
|
+
error: runtimeErrorMessage(error),
|
|
5808
|
+
});
|
|
5809
|
+
if (options.updateMessageId) {
|
|
5810
|
+
await this.openExecutionSettingsNow(contextKey, "provider", options).catch((presentationError: unknown) => {
|
|
5811
|
+
this.logger.warn({ error: presentationError, sessionId: record.localSessionId }, "Failed to refresh rejected Provider selection.");
|
|
5812
|
+
});
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
throw error;
|
|
5816
|
+
}
|
|
5537
5817
|
const notice = [
|
|
5538
5818
|
`Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
|
|
5539
5819
|
`模型 ${cardCode(session.model ?? settings.model)}`,
|
|
@@ -5542,7 +5822,7 @@ export class ProxySessionController {
|
|
|
5542
5822
|
`并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
5543
5823
|
].join(",");
|
|
5544
5824
|
if (options.updateMessageId) {
|
|
5545
|
-
await this.
|
|
5825
|
+
await this.openExecutionSettingsNow(contextKey, "provider", {
|
|
5546
5826
|
sessionId: loaded.record.localSessionId,
|
|
5547
5827
|
updateMessageId: options.updateMessageId,
|
|
5548
5828
|
notice,
|
|
@@ -5553,15 +5833,23 @@ export class ProxySessionController {
|
|
|
5553
5833
|
}
|
|
5554
5834
|
|
|
5555
5835
|
private async changeProviderSettings(loaded: LoadedSession, settings: RuntimeExecutionSettings): Promise<RuntimeSession> {
|
|
5836
|
+
return this.sessionHydrationOperations.run(loaded.record.localSessionId, () => this.changeProviderSettingsNow(loaded, settings));
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
private async changeProviderSettingsNow(loaded: LoadedSession, settings: RuntimeExecutionSettings): Promise<RuntimeSession> {
|
|
5556
5840
|
if (!loaded.runtime.setExecutionSettings) throw new Error("当前运行时不支持 Provider 设置。");
|
|
5557
|
-
|
|
5841
|
+
loaded.session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
|
|
5842
|
+
const remoteSessionId = loaded.session.remoteSessionId;
|
|
5843
|
+
const persist = (candidate: RuntimeSession): Promise<void> => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
|
|
5844
|
+
if (candidate.remoteSessionId !== remoteSessionId) {
|
|
5845
|
+
throw new Error("设置切换返回了不同的任务 ID,未保存设置;不允许替换任务或更改历史。");
|
|
5846
|
+
}
|
|
5558
5847
|
assertProviderSettingsApplied(candidate, settings);
|
|
5559
5848
|
const agent = this.ensureAgent(loaded.record.agentName);
|
|
5560
5849
|
const previousDefaults = agent.defaults;
|
|
5561
|
-
await this.
|
|
5850
|
+
await this.persistAgentExecutionDefaultsNow(loaded.record.localSessionId, candidate);
|
|
5562
5851
|
try {
|
|
5563
5852
|
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
5564
|
-
remoteSessionId: candidate.remoteSessionId,
|
|
5565
5853
|
modelProvider: candidate.modelProvider,
|
|
5566
5854
|
model: candidate.model,
|
|
5567
5855
|
reasoningEffort: candidate.reasoningEffort,
|
|
@@ -5572,12 +5860,145 @@ export class ProxySessionController {
|
|
|
5572
5860
|
agent.defaults = previousDefaults;
|
|
5573
5861
|
throw error;
|
|
5574
5862
|
}
|
|
5575
|
-
};
|
|
5863
|
+
});
|
|
5576
5864
|
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
5865
|
+
this.settingsOnlySessions.delete(loaded.record.localSessionId);
|
|
5577
5866
|
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
|
|
5578
5867
|
return session;
|
|
5579
5868
|
}
|
|
5580
5869
|
|
|
5870
|
+
private async reconcilePreparedSettingsSession(loaded: LoadedSession): Promise<void> {
|
|
5871
|
+
if (!this.settingsOnlySessions.has(loaded.record.localSessionId) || !loaded.session.activeTurnId) return;
|
|
5872
|
+
const activity = await this.assertSessionTurnOwnership(loaded.record, loaded.runtime);
|
|
5873
|
+
if (activity && !activity.active) {
|
|
5874
|
+
loaded.session = await loaded.runtime.synchronizeSession(loaded.record.localSessionId);
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
private providerSettingsRestored(loaded: LoadedSession, previous: RuntimeSession, error: unknown): boolean {
|
|
5879
|
+
const restored = loaded.runtime.getSession(loaded.record.localSessionId);
|
|
5880
|
+
const saved = this.store.getSession(loaded.record.localSessionId);
|
|
5881
|
+
const keys = ["remoteSessionId", "modelProvider", "model", "reasoningEffort", "permissionMode"] as const;
|
|
5882
|
+
return error instanceof ProviderSwitchError && error.previousSettingsRestored
|
|
5883
|
+
&& Boolean(restored && saved && keys.every((key) => restored[key] === previous[key] && saved[key] === previous[key]));
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5886
|
+
private pendingProviderNotice(pending: PendingProviderSettings): string {
|
|
5887
|
+
const target = `Provider ${cardCode(pending.settings.modelProvider)},模型 ${cardCode(pending.settings.model)}`;
|
|
5888
|
+
return pending.error
|
|
5889
|
+
? `切换失败:${pending.error}。目标为 ${target};后续轮次已暂停,请重新选择 Provider 或模型重试。`
|
|
5890
|
+
: `待生效:当前轮次不变,结束后切换到 ${target}。可继续选择模型,以最后选择为准。`;
|
|
5891
|
+
}
|
|
5892
|
+
|
|
5893
|
+
private async savePendingProviderSelection(
|
|
5894
|
+
loaded: LoadedSession, settings: RuntimeExecutionSettings, contextKey: string,
|
|
5895
|
+
options: ModelCardOptions, activeTab: ExecutionSettingsTab,
|
|
5896
|
+
): Promise<void> {
|
|
5897
|
+
const previous = this.store.getPendingProviderSettings(loaded.record.localSessionId);
|
|
5898
|
+
const pending: PendingProviderSettings = {
|
|
5899
|
+
localSessionId: loaded.record.localSessionId, contextKey, settings, activeTab,
|
|
5900
|
+
messageId: options.updateMessageId ?? previous?.messageId,
|
|
5901
|
+
replyMessageId: options.updateMessageId ?? previous?.replyMessageId
|
|
5902
|
+
?? this.controlSessionReplyTarget(loaded.record)?.messageId,
|
|
5903
|
+
};
|
|
5904
|
+
this.store.savePendingProviderSettings(pending);
|
|
5905
|
+
await this.reconcilePreparedSettingsSession(loaded);
|
|
5906
|
+
if (!loaded.session.activeTurnId) {
|
|
5907
|
+
await this.applyPendingProviderSettings(loaded);
|
|
5908
|
+
void this.scheduleNextQueuedPrompt(loaded.record.localSessionId).catch((error: unknown) => {
|
|
5909
|
+
this.logger.warn({ error }, "Failed to resume queue after Provider selection.");
|
|
5910
|
+
});
|
|
5911
|
+
return;
|
|
5912
|
+
}
|
|
5913
|
+
if (options.updateMessageId) {
|
|
5914
|
+
await this.openExecutionSettingsNow(contextKey, activeTab, options);
|
|
5915
|
+
} else {
|
|
5916
|
+
await this.outbound.sendText(contextKey, this.pendingProviderNotice(pending).replaceAll("`", ""));
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
|
|
5920
|
+
// Caller holds settingsOperations; only a verified rollback can unblock the next turn.
|
|
5921
|
+
private async applyPendingProviderSettings(loaded: LoadedSession): Promise<void> {
|
|
5922
|
+
const pending = this.store.getPendingProviderSettings(loaded.record.localSessionId);
|
|
5923
|
+
if (!pending) return;
|
|
5924
|
+
await this.reconcilePreparedSettingsSession(loaded);
|
|
5925
|
+
if (loaded.runtime.getSession(loaded.record.localSessionId)?.activeTurnId) return;
|
|
5926
|
+
if (pending.error) throw new Error(this.pendingProviderNotice(pending));
|
|
5927
|
+
const previous = { ...(loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session) };
|
|
5928
|
+
let notice: string;
|
|
5929
|
+
let failure: unknown;
|
|
5930
|
+
let rolledBack = false;
|
|
5931
|
+
try {
|
|
5932
|
+
loaded.session = await this.changeProviderSettings(loaded, pending.settings);
|
|
5933
|
+
this.store.deletePendingProviderSettings(pending.localSessionId);
|
|
5934
|
+
notice = `Provider 已切换为 ${cardCode(pending.settings.modelProvider)},模型 ${cardCode(pending.settings.model)},从下一轮生效。`;
|
|
5935
|
+
} catch (error) {
|
|
5936
|
+
const restored = loaded.runtime.getSession(pending.localSessionId);
|
|
5937
|
+
rolledBack = this.providerSettingsRestored(loaded, previous, error);
|
|
5938
|
+
if (rolledBack) {
|
|
5939
|
+
loaded.session = restored!;
|
|
5940
|
+
this.store.deletePendingProviderSettings(pending.localSessionId);
|
|
5941
|
+
this.outbound.updateSessionModel(pending.localSessionId, previous.model, previous.modelProvider);
|
|
5942
|
+
notice = `切换到 Provider ${cardCode(pending.settings.modelProvider)}、模型 ${cardCode(pending.settings.model)} 失败:${runtimeErrorMessage(error)} 已回退到 Provider ${cardCode(previous.modelProvider ?? "Agent 默认")}、模型 ${cardCode(previous.model ?? "默认")},后续轮次继续使用原设置。`;
|
|
5943
|
+
} else {
|
|
5944
|
+
failure = error;
|
|
5945
|
+
const failed = { ...pending, error: runtimeErrorMessage(error) };
|
|
5946
|
+
this.store.savePendingProviderSettings(failed);
|
|
5947
|
+
notice = this.pendingProviderNotice(failed);
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
try {
|
|
5951
|
+
await this.outbound.withReplyTarget(pending.contextKey, pending.replyMessageId
|
|
5952
|
+
? { messageId: pending.replyMessageId, replyInThread: true } : undefined, async () => {
|
|
5953
|
+
if (pending.messageId) {
|
|
5954
|
+
try {
|
|
5955
|
+
await this.openExecutionSettingsNow(pending.contextKey, pending.activeTab ?? "provider", {
|
|
5956
|
+
sessionId: pending.localSessionId, updateMessageId: pending.messageId,
|
|
5957
|
+
notice: failure || rolledBack ? undefined : notice,
|
|
5958
|
+
warning: rolledBack ? notice.replaceAll("`", "") : undefined,
|
|
5959
|
+
});
|
|
5960
|
+
return;
|
|
5961
|
+
} catch (error) {
|
|
5962
|
+
this.logger.warn({ error, sessionId: pending.localSessionId }, "Failed to refresh Provider settings card.");
|
|
5963
|
+
}
|
|
5964
|
+
}
|
|
5965
|
+
await this.outbound.sendText(pending.contextKey, notice.replaceAll("`", ""));
|
|
5966
|
+
});
|
|
5967
|
+
} catch (error) {
|
|
5968
|
+
this.logger.warn({ error, sessionId: pending.localSessionId }, "Failed to notify Provider settings result.");
|
|
5969
|
+
}
|
|
5970
|
+
if (failure) throw failure;
|
|
5971
|
+
}
|
|
5972
|
+
|
|
5973
|
+
private async applyPendingProviderWhenIdle(sessionId: string): Promise<void> {
|
|
5974
|
+
await this.settingsOperations.run(sessionId, async () => {
|
|
5975
|
+
if (this.recoveryClosed) return;
|
|
5976
|
+
const pending = this.store.getPendingProviderSettings(sessionId);
|
|
5977
|
+
if (!pending || pending.error) return;
|
|
5978
|
+
const record = this.store.getSession(sessionId);
|
|
5979
|
+
if (!record || record.status === "closed") {
|
|
5980
|
+
this.store.deletePendingProviderSettings(sessionId);
|
|
5981
|
+
return;
|
|
5982
|
+
}
|
|
5983
|
+
try {
|
|
5984
|
+
const loaded = await this.loadSession(record, true);
|
|
5985
|
+
await this.applyPendingProviderSettings(loaded);
|
|
5986
|
+
} catch (error) {
|
|
5987
|
+
this.logger.warn({ error, sessionId }, "Deferred Provider switch could not be applied.");
|
|
5988
|
+
if (!this.store.getPendingProviderSettings(sessionId)?.error) {
|
|
5989
|
+
const failed = { ...pending, error: runtimeErrorMessage(error) };
|
|
5990
|
+
this.store.savePendingProviderSettings(failed);
|
|
5991
|
+
await this.outbound.withReplyTarget(pending.contextKey, pending.replyMessageId
|
|
5992
|
+
? { messageId: pending.replyMessageId, replyInThread: true } : undefined,
|
|
5993
|
+
() => this.outbound.sendText(pending.contextKey, this.pendingProviderNotice(failed).replaceAll("`", "")))
|
|
5994
|
+
.catch((notificationError: unknown) => {
|
|
5995
|
+
this.logger.warn({ error: notificationError, sessionId }, "Failed to notify Provider recovery failure.");
|
|
5996
|
+
});
|
|
5997
|
+
}
|
|
5998
|
+
}
|
|
5999
|
+
});
|
|
6000
|
+
}
|
|
6001
|
+
|
|
5581
6002
|
private async assertProviderModelSettings(
|
|
5582
6003
|
loaded: LoadedSession,
|
|
5583
6004
|
modelProvider: string,
|
|
@@ -5622,36 +6043,52 @@ export class ProxySessionController {
|
|
|
5622
6043
|
}
|
|
5623
6044
|
|
|
5624
6045
|
private async model(contextKey: string, model?: string, options: ModelCardOptions = {}): Promise<void> {
|
|
6046
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
6047
|
+
return this.settingsOperations.run(record.localSessionId, () => this.modelNow(contextKey, model, { ...options, sessionId: record.localSessionId }));
|
|
6048
|
+
}
|
|
6049
|
+
|
|
6050
|
+
private async modelNow(contextKey: string, model?: string, options: ModelCardOptions = {}): Promise<void> {
|
|
5625
6051
|
if (!model) {
|
|
5626
|
-
await this.
|
|
6052
|
+
await this.openExecutionSettingsNow(contextKey, "model", options);
|
|
5627
6053
|
return;
|
|
5628
6054
|
}
|
|
5629
6055
|
const record = options.sessionId
|
|
5630
6056
|
? this.requireSession(contextKey, options.sessionId)
|
|
5631
6057
|
: this.requireCurrentSession(contextKey);
|
|
5632
|
-
const loaded = await this.loadSession(record);
|
|
5633
|
-
const
|
|
6058
|
+
const loaded = await this.loadSession(record, true);
|
|
6059
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
6060
|
+
const selectedSettings = pending?.settings ?? loaded.session;
|
|
6061
|
+
if (options.expectedProvider && options.expectedProvider !== selectedSettings.modelProvider) {
|
|
6062
|
+
throw new Error("Provider 已发生变化,请重新打开运行设置后选择。");
|
|
6063
|
+
}
|
|
6064
|
+
const models = await loaded.runtime.listModels(selectedSettings.modelProvider, selectedSettings.model);
|
|
5634
6065
|
const selected = models.find((item) => item.id === model);
|
|
5635
6066
|
if (!selected) throw new Error(`未知模型:${model}`);
|
|
5636
|
-
const currentEffort =
|
|
6067
|
+
const currentEffort = selectedSettings.reasoningEffort;
|
|
5637
6068
|
const compatible = currentEffort
|
|
5638
6069
|
? selected.supportedReasoningEfforts.length === 0
|
|
5639
6070
|
|| selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
|
|
5640
6071
|
: false;
|
|
5641
6072
|
const nextEffort = compatible ? currentEffort : selected.defaultReasoningEffort;
|
|
5642
6073
|
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
6074
|
+
if (pending) {
|
|
6075
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, model, reasoningEffort: selectProviderReasoningEffort(selected, nextEffort) }, contextKey, options, "model");
|
|
6076
|
+
return;
|
|
5646
6077
|
}
|
|
5647
|
-
this.
|
|
5648
|
-
|
|
5649
|
-
|
|
6078
|
+
await this.sessionHydrationOperations.run(loaded.record.localSessionId, async () => {
|
|
6079
|
+
await loaded.runtime.setModel(loaded.record.localSessionId, model);
|
|
6080
|
+
if (nextEffort && nextEffort !== currentEffort) {
|
|
6081
|
+
await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
|
|
6082
|
+
}
|
|
6083
|
+
this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
|
|
6084
|
+
this.outbound.updateSessionModel(loaded.record.localSessionId, model, loaded.session.modelProvider);
|
|
6085
|
+
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
6086
|
+
});
|
|
5650
6087
|
const effortMessage = nextEffort && nextEffort !== currentEffort
|
|
5651
6088
|
? `,思考强度已自动调整为 ${nextEffort}`
|
|
5652
6089
|
: "";
|
|
5653
6090
|
if (options.updateMessageId) {
|
|
5654
|
-
await this.
|
|
6091
|
+
await this.openExecutionSettingsNow(contextKey, "model", {
|
|
5655
6092
|
sessionId: loaded.record.localSessionId,
|
|
5656
6093
|
updateMessageId: options.updateMessageId,
|
|
5657
6094
|
notice: `模型已切换为 ${cardCode(model)}${effortMessage},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
@@ -5827,6 +6264,7 @@ export class ProxySessionController {
|
|
|
5827
6264
|
const created = await this.shellCommandJobs.createJob({ contextKey, sourceMessageId, command, cwd });
|
|
5828
6265
|
let cardMessageId: string | undefined;
|
|
5829
6266
|
const initialState: ShellCommandCardView = {
|
|
6267
|
+
terminalUrl: this.lifecycle?.terminalUrl?.(created.id),
|
|
5830
6268
|
jobId: created.id,
|
|
5831
6269
|
contextKey,
|
|
5832
6270
|
command,
|
|
@@ -5853,7 +6291,7 @@ export class ProxySessionController {
|
|
|
5853
6291
|
await this.outbound.updateInteractiveCard(
|
|
5854
6292
|
contextKey,
|
|
5855
6293
|
cardMessageId,
|
|
5856
|
-
this.cardRenderer.renderShellCommandCard(shellCommandJobCardView(failed)),
|
|
6294
|
+
this.cardRenderer.renderShellCommandCard({ ...shellCommandJobCardView(failed), terminalUrl: this.lifecycle?.terminalUrl?.(failed.id) }),
|
|
5857
6295
|
);
|
|
5858
6296
|
await this.shellCommandJobs.markPresented(created.id);
|
|
5859
6297
|
} catch (cardError) {
|
|
@@ -5911,7 +6349,7 @@ export class ProxySessionController {
|
|
|
5911
6349
|
): void {
|
|
5912
6350
|
if (!job.cardMessageId || this.shellCommandJobMonitors.has(job.id)) return;
|
|
5913
6351
|
const scheduler = new CardUpdateScheduler<ShellCommandCardView>({
|
|
5914
|
-
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
6352
|
+
render: (state) => this.cardRenderer.renderShellCommandCard({ ...state, terminalUrl: this.lifecycle?.terminalUrl?.(job.id) }),
|
|
5915
6353
|
write: (card) => this.outbound.updateInteractiveCard(job.contextKey, job.cardMessageId!, card),
|
|
5916
6354
|
onError: (error) => this.logger.warn(
|
|
5917
6355
|
{ error, jobId: job.id, contextKey: job.contextKey, messageId: job.cardMessageId },
|
|
@@ -6062,16 +6500,26 @@ export class ProxySessionController {
|
|
|
6062
6500
|
}
|
|
6063
6501
|
|
|
6064
6502
|
private async thinking(contextKey: string, effort?: string, options: ThinkingCardOptions = {}): Promise<void> {
|
|
6503
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
6504
|
+
return this.settingsOperations.run(record.localSessionId, () => this.thinkingNow(contextKey, effort, { ...options, sessionId: record.localSessionId }));
|
|
6505
|
+
}
|
|
6506
|
+
|
|
6507
|
+
private async thinkingNow(contextKey: string, effort?: string, options: ThinkingCardOptions = {}): Promise<void> {
|
|
6065
6508
|
if (!effort) {
|
|
6066
|
-
await this.
|
|
6509
|
+
await this.openExecutionSettingsNow(contextKey, "thinking", options);
|
|
6067
6510
|
return;
|
|
6068
6511
|
}
|
|
6069
6512
|
const record = options.sessionId
|
|
6070
6513
|
? this.requireSession(contextKey, options.sessionId)
|
|
6071
6514
|
: this.requireCurrentSession(contextKey);
|
|
6072
|
-
const loaded = await this.loadSession(record);
|
|
6073
|
-
const
|
|
6074
|
-
const
|
|
6515
|
+
const loaded = await this.loadSession(record, true);
|
|
6516
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
6517
|
+
const selectedSettings = pending?.settings ?? loaded.session;
|
|
6518
|
+
if (options.expectedProvider && options.expectedProvider !== selectedSettings.modelProvider) {
|
|
6519
|
+
throw new Error("Provider 已发生变化,请重新打开运行设置后选择。");
|
|
6520
|
+
}
|
|
6521
|
+
const models = await loaded.runtime.listModels(selectedSettings.modelProvider, selectedSettings.model);
|
|
6522
|
+
const currentModel = models.find((item) => item.id === selectedSettings.model)
|
|
6075
6523
|
?? models.find((item) => item.isDefault);
|
|
6076
6524
|
if (!currentModel) throw new Error("当前运行时没有可配置思考强度的模型。");
|
|
6077
6525
|
if (options.expectedModel && currentModel.id !== options.expectedModel) {
|
|
@@ -6083,11 +6531,17 @@ export class ProxySessionController {
|
|
|
6083
6531
|
const options = supported.map((option) => option.value).join("、") || "无";
|
|
6084
6532
|
throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
|
|
6085
6533
|
}
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6534
|
+
if (pending) {
|
|
6535
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, reasoningEffort: effort }, contextKey, options, "thinking");
|
|
6536
|
+
return;
|
|
6537
|
+
}
|
|
6538
|
+
await this.sessionHydrationOperations.run(loaded.record.localSessionId, async () => {
|
|
6539
|
+
await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, effort);
|
|
6540
|
+
this.store.updateRuntimeSession(loaded.record.localSessionId, { reasoningEffort: effort });
|
|
6541
|
+
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
6542
|
+
});
|
|
6089
6543
|
if (options.updateMessageId) {
|
|
6090
|
-
await this.
|
|
6544
|
+
await this.openExecutionSettingsNow(contextKey, "thinking", {
|
|
6091
6545
|
sessionId: loaded.record.localSessionId,
|
|
6092
6546
|
updateMessageId: options.updateMessageId,
|
|
6093
6547
|
notice: `思考强度已切换为 ${cardCode(effort)},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
@@ -6100,25 +6554,38 @@ export class ProxySessionController {
|
|
|
6100
6554
|
);
|
|
6101
6555
|
}
|
|
6102
6556
|
|
|
6103
|
-
private async permissions(
|
|
6557
|
+
private async permissions(contextKey: string, mode?: PermissionMode, options: ModelCardOptions = {}): Promise<void> {
|
|
6558
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
6559
|
+
return this.settingsOperations.run(record.localSessionId, () => this.permissionsNow(contextKey, mode, { ...options, sessionId: record.localSessionId }));
|
|
6560
|
+
}
|
|
6561
|
+
|
|
6562
|
+
private async permissionsNow(
|
|
6104
6563
|
contextKey: string,
|
|
6105
6564
|
mode?: PermissionMode,
|
|
6106
6565
|
options: ModelCardOptions = {},
|
|
6107
6566
|
): Promise<void> {
|
|
6108
6567
|
if (!mode) {
|
|
6109
|
-
await this.
|
|
6568
|
+
await this.openExecutionSettingsNow(contextKey, "permission", options);
|
|
6110
6569
|
return;
|
|
6111
6570
|
}
|
|
6112
6571
|
const record = options.sessionId
|
|
6113
6572
|
? this.requireSession(contextKey, options.sessionId)
|
|
6114
6573
|
: this.requireCurrentSession(contextKey);
|
|
6115
|
-
const loaded = await this.loadSession(record);
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6574
|
+
const loaded = await this.loadSession(record, true);
|
|
6575
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
6576
|
+
if (pending) {
|
|
6577
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, permissionMode: mode }, contextKey, options, "permission");
|
|
6578
|
+
return;
|
|
6579
|
+
}
|
|
6580
|
+
await this.sessionHydrationOperations.run(record.localSessionId, async () => {
|
|
6581
|
+
await loaded.runtime.setPermissionMode(record.localSessionId, mode);
|
|
6582
|
+
loaded.session = loaded.runtime.getSession(record.localSessionId) ?? loaded.session;
|
|
6583
|
+
loaded.session.permissionMode = mode;
|
|
6584
|
+
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: mode });
|
|
6585
|
+
await this.persistAgentExecutionDefaults(record.localSessionId);
|
|
6586
|
+
});
|
|
6120
6587
|
if (options.updateMessageId) {
|
|
6121
|
-
await this.
|
|
6588
|
+
await this.openExecutionSettingsNow(contextKey, "permission", {
|
|
6122
6589
|
sessionId: loaded.record.localSessionId,
|
|
6123
6590
|
updateMessageId: options.updateMessageId,
|
|
6124
6591
|
notice: mode === "auto"
|
|
@@ -6163,7 +6630,8 @@ export class ProxySessionController {
|
|
|
6163
6630
|
const existing = this.turnDetailHydrations.get(turnId);
|
|
6164
6631
|
if (existing) return existing;
|
|
6165
6632
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
6166
|
-
|
|
6633
|
+
const previewTurnId = snapshot?.previewTurnId ?? turnId;
|
|
6634
|
+
if ((!snapshot || snapshot.previewJournal) && this.store.previews.has(previewTurnId)) return this.store.previews.load(previewTurnId);
|
|
6167
6635
|
if (snapshot && !needsHistoricalDetails(snapshot)) return snapshot;
|
|
6168
6636
|
const origin = !snapshot ? this.store.getTurnRuntimeOrigin(turnId) : undefined;
|
|
6169
6637
|
if (!snapshot && (!origin || !this.runtimes.forAgent(origin.agentName).readRemoteTurn)) return undefined;
|
|
@@ -6189,7 +6657,7 @@ export class ProxySessionController {
|
|
|
6189
6657
|
}
|
|
6190
6658
|
// An Agent Bot turn may have arrived while the read-only request was in flight.
|
|
6191
6659
|
const latest = turnViewSnapshot(this.store.getTurnSnapshot(origin.turnId));
|
|
6192
|
-
if (latest) return latest.previewJournal ? this.store.previews.load(origin.turnId) ?? latest : latest;
|
|
6660
|
+
if (latest) return latest.previewJournal ? this.store.previews.load(latest.previewTurnId ?? origin.turnId) ?? latest : latest;
|
|
6193
6661
|
const state = hydrateTurnViewState(createTurnViewState(
|
|
6194
6662
|
origin.localSessionId, origin.turnId, details.startedAt ?? 0,
|
|
6195
6663
|
remote?.title, undefined, remote?.cwd, undefined, this.agentLabel(origin.agentName),
|
|
@@ -6754,20 +7222,28 @@ export class ProxySessionController {
|
|
|
6754
7222
|
const project = taskProjectInfo(entry);
|
|
6755
7223
|
const projectActionSource = projectActionSources.get(project.key) ?? entry;
|
|
6756
7224
|
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,
|
|
7225
|
+
const stopAction = entry.active ? bindCardAction({
|
|
7226
|
+
text: "Stop",
|
|
7227
|
+
type: "danger",
|
|
6763
7228
|
value: {
|
|
6764
|
-
action:
|
|
7229
|
+
action: "session_stop",
|
|
6765
7230
|
sessionId: entry.reference,
|
|
6766
7231
|
...(searchTerm ? { searchTerm } : {}),
|
|
6767
7232
|
page: String(page),
|
|
6768
7233
|
contextKey,
|
|
6769
7234
|
},
|
|
6770
|
-
})
|
|
7235
|
+
}) : undefined;
|
|
7236
|
+
const actions: TaskListCardAction[] = [];
|
|
7237
|
+
if (!entry.current && entry.status !== "外部执行中") actions.push(bindCardAction({
|
|
7238
|
+
text: "Switch",
|
|
7239
|
+
value: {
|
|
7240
|
+
action: "session_switch",
|
|
7241
|
+
sessionId: entry.reference,
|
|
7242
|
+
...(searchTerm ? { searchTerm } : {}),
|
|
7243
|
+
page: String(page),
|
|
7244
|
+
contextKey,
|
|
7245
|
+
},
|
|
7246
|
+
}));
|
|
6771
7247
|
actions.push(bindCardAction({
|
|
6772
7248
|
text: "SwitchGroup",
|
|
6773
7249
|
value: {
|
|
@@ -6833,7 +7309,7 @@ export class ProxySessionController {
|
|
|
6833
7309
|
contextKey,
|
|
6834
7310
|
},
|
|
6835
7311
|
}));
|
|
6836
|
-
actions.push(bindCardAction({
|
|
7312
|
+
if (!entry.active) actions.push(bindCardAction({
|
|
6837
7313
|
text: "Archive",
|
|
6838
7314
|
value: {
|
|
6839
7315
|
action: "session_archive",
|
|
@@ -6855,6 +7331,7 @@ export class ProxySessionController {
|
|
|
6855
7331
|
`**最新 Prompt**:${cardText(lastUserPrompt)}`,
|
|
6856
7332
|
`<font color='grey'>最近更新:${cardText(entry.updatedLabel)}</font>`,
|
|
6857
7333
|
],
|
|
7334
|
+
stopAction,
|
|
6858
7335
|
actions,
|
|
6859
7336
|
current: entry.current,
|
|
6860
7337
|
},
|
|
@@ -6884,7 +7361,7 @@ export class ProxySessionController {
|
|
|
6884
7361
|
`第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
|
|
6885
7362
|
"",
|
|
6886
7363
|
"> 项目菜单:**New** 新建任务,**NewGroup** 新建群。",
|
|
6887
|
-
"> 任务详情:**Switch**
|
|
7364
|
+
"> 任务详情:**Switch** 切换,运行中任务可用 **Stop** 发送停止信号,**Fork** / **ForkGroup** 创建分支,**Status** 查看状态,**Archive** 归档。",
|
|
6888
7365
|
],
|
|
6889
7366
|
[
|
|
6890
7367
|
...(hasPrevious ? [bindCardAction({
|
|
@@ -6959,7 +7436,7 @@ export class ProxySessionController {
|
|
|
6959
7436
|
|
|
6960
7437
|
private async refreshSessionsCardFromAction(
|
|
6961
7438
|
action: CardAction,
|
|
6962
|
-
options: {
|
|
7439
|
+
options: { page?: number } = {},
|
|
6963
7440
|
): Promise<void> {
|
|
6964
7441
|
if (!action.messageId) return;
|
|
6965
7442
|
const searchTerm = typeof action.value.searchTerm === "string" && action.value.searchTerm.trim()
|
|
@@ -6967,7 +7444,6 @@ export class ProxySessionController {
|
|
|
6967
7444
|
: undefined;
|
|
6968
7445
|
await this.listSessions(action.contextKey, searchTerm, {
|
|
6969
7446
|
updateMessageId: action.messageId,
|
|
6970
|
-
forceSwitchTaskId: options.forceSwitchTaskId,
|
|
6971
7447
|
page: options.page ?? parseSessionPage(action.value.page),
|
|
6972
7448
|
});
|
|
6973
7449
|
}
|
|
@@ -7156,7 +7632,7 @@ export class ProxySessionController {
|
|
|
7156
7632
|
const remoteReference = local
|
|
7157
7633
|
? remoteSessionReference(local.agentName, local.remoteSessionId!)
|
|
7158
7634
|
: taskId;
|
|
7159
|
-
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference);
|
|
7635
|
+
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference, { view: "metadata" });
|
|
7160
7636
|
if (isRemoteSessionActive(remote)) {
|
|
7161
7637
|
throw new Error("任务正在执行,无法归档。请先停止任务或等待本轮完成。");
|
|
7162
7638
|
}
|
|
@@ -7165,7 +7641,11 @@ export class ProxySessionController {
|
|
|
7165
7641
|
}
|
|
7166
7642
|
|
|
7167
7643
|
const wasCurrent = Boolean(local && this.store.getUserContext(contextKey)?.currentSessionId === local.localSessionId);
|
|
7168
|
-
|
|
7644
|
+
const allowUnmaterialized = Boolean(local && !local.lastTurnId && !local.lastTurnStatus
|
|
7645
|
+
&& !this.store.hasTurnAttemptsForSession(local.localSessionId)
|
|
7646
|
+
&& !this.store.getForkHistorySource(local.localSessionId)
|
|
7647
|
+
&& this.store.listTaskTurnGraphIndex(local.localSessionId).length === 0);
|
|
7648
|
+
await runtime.archiveRemoteSession(remote.id, { allowUnmaterialized });
|
|
7169
7649
|
const title = local?.title ?? remote.title ?? remote.preview ?? remote.id;
|
|
7170
7650
|
if (local) {
|
|
7171
7651
|
this.store.archiveSession(local.localSessionId);
|
|
@@ -7318,6 +7798,7 @@ export class ProxySessionController {
|
|
|
7318
7798
|
}
|
|
7319
7799
|
const localActiveTurnId = runtime.getSession(record.localSessionId)?.activeTurnId;
|
|
7320
7800
|
const runtimeOwnsActiveTurn = Boolean(localActiveTurnId)
|
|
7801
|
+
&& (!this.settingsOnlySessions.has(record.localSessionId) || Boolean(activity.activeTurnId))
|
|
7321
7802
|
&& (!activity.activeTurnId || activity.activeTurnId === localActiveTurnId);
|
|
7322
7803
|
const persistedTurnMatches = record.status === "running"
|
|
7323
7804
|
&& record.lastTurnStatus === "running"
|
|
@@ -7719,6 +8200,12 @@ export class ProxySessionController {
|
|
|
7719
8200
|
}
|
|
7720
8201
|
|
|
7721
8202
|
private async persistAgentExecutionDefaults(localSessionId: string, candidate?: RuntimeSession): Promise<void> {
|
|
8203
|
+
const record = this.store.getSession(localSessionId);
|
|
8204
|
+
if (!record) throw new Error(`找不到任务:${localSessionId}`);
|
|
8205
|
+
return this.agentDefaultsOperations.run(record.agentName, () => this.persistAgentExecutionDefaultsNow(localSessionId, candidate));
|
|
8206
|
+
}
|
|
8207
|
+
|
|
8208
|
+
private async persistAgentExecutionDefaultsNow(localSessionId: string, candidate?: RuntimeSession): Promise<void> {
|
|
7722
8209
|
const record = this.store.getSession(localSessionId);
|
|
7723
8210
|
if (!record) throw new Error(`找不到任务:${localSessionId}`);
|
|
7724
8211
|
const session = candidate ?? record;
|
|
@@ -8848,6 +9335,13 @@ function executionSettingsTabValue(value: unknown): ExecutionSettingsTab {
|
|
|
8848
9335
|
throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
|
|
8849
9336
|
}
|
|
8850
9337
|
|
|
9338
|
+
function modelCardPageValue(value: unknown): number | undefined {
|
|
9339
|
+
if (value === undefined) return undefined;
|
|
9340
|
+
const page = typeof value === "number" || (typeof value === "string" && value.trim()) ? Number(value) : NaN;
|
|
9341
|
+
if (!Number.isSafeInteger(page) || page < 0) throw new Error("模型卡片页码无效,请重新发送 /model。");
|
|
9342
|
+
return page;
|
|
9343
|
+
}
|
|
9344
|
+
|
|
8851
9345
|
function resetHistoryPageValue(value: unknown): number {
|
|
8852
9346
|
const page = Number(value);
|
|
8853
9347
|
if (!Number.isSafeInteger(page) || page < 0) throw new Error("历史轮次卡片页码无效,请重新发送 /turns。");
|
|
@@ -9255,10 +9749,18 @@ function defaultResourcePromptFromCounts(imageCount: number, fileCount: number):
|
|
|
9255
9749
|
return imageCount === 1 ? "请查看这张图片" : "请查看这些图片";
|
|
9256
9750
|
}
|
|
9257
9751
|
|
|
9258
|
-
function
|
|
9752
|
+
function isCommandMessage(message: IncomingMessage): boolean {
|
|
9753
|
+
const text = normalizeFeishuPostText(message.text).trimStart();
|
|
9754
|
+
return text.startsWith("/") || text.startsWith("!");
|
|
9755
|
+
}
|
|
9756
|
+
|
|
9757
|
+
function isForwardAttachmentSource(message: IncomingMessage): boolean {
|
|
9259
9758
|
return !message.mergedForwardMessageId
|
|
9260
|
-
&& message.
|
|
9261
|
-
&&
|
|
9759
|
+
&& !message.parentMessageId
|
|
9760
|
+
&& !message.contextKey.startsWith("console:")
|
|
9761
|
+
&& !isCommandMessage(message)
|
|
9762
|
+
&& (message.messageType === "post"
|
|
9763
|
+
|| (message.images?.length ?? 0) > 0 || (message.files?.length ?? 0) > 0);
|
|
9262
9764
|
}
|
|
9263
9765
|
|
|
9264
9766
|
function deduplicateImageReferences(
|
|
@@ -9335,7 +9837,8 @@ function isRetryableLlmTurnFailure(message: string): boolean {
|
|
|
9335
9837
|
const normalized = message.trim().toLowerCase();
|
|
9336
9838
|
if (!normalized) return false;
|
|
9337
9839
|
const permanentFailure = [
|
|
9338
|
-
|
|
9840
|
+
/\b(?:auth|authenticat\w*|unauthenticat\w*|authoriz\w*|unauthorized|forbidden)\b|invalid api key|incorrect api key/u,
|
|
9841
|
+
/(?:^|\D)(?:401|403)(?:\D|$)/u,
|
|
9339
9842
|
/insufficient[_ -]?quota|quota (?:exceeded|exhausted)|usage limit|billing|credit balance/u,
|
|
9340
9843
|
/context (?:length|window)|maximum context|too many tokens|token limit/u,
|
|
9341
9844
|
/invalid request|bad request|invalid argument|malformed|unsupported|not supported/u,
|