@keyou007/agent-bot 0.1.24-alpha.5 → 0.1.24-alpha.7
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 +40 -0
- package/README.md +29 -3
- package/README.zh.md +29 -3
- 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/CodexEventMapper.js +1 -0
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +247 -120
- 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/TraePlanEntry.js +70 -0
- package/dist/codex/TraePlanEntry.js.map +1 -0
- 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 +92 -8
- 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 +57 -7
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +51 -4
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +7 -2
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +41 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +620 -214
- 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 +33 -1
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +22 -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/TerminalCommandTracker.js +74 -0
- package/dist/terminal/TerminalCommandTracker.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 +95 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +676 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +48 -0
- package/docs/technical-reference.zh.md +48 -0
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +11 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/CodexEventMapper.ts +1 -0
- package/src/codex/CodexRuntime.ts +244 -128
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/codex/TraePlanEntry.ts +63 -0
- 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 +109 -8
- 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 +56 -8
- package/src/local-files/LocalFileViewerServer.ts +48 -4
- package/src/local-files/TurnPreviewPage.ts +7 -2
- package/src/presentation/TurnStateReducer.ts +46 -7
- package/src/presentation/turnViewTypes.ts +5 -0
- package/src/proxy/ProxySessionController.ts +629 -211
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +8 -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 +35 -2
- package/src/supervision/SafeRestartScheduler.ts +25 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalCommandTracker.ts +74 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +93 -0
- package/src/terminal/TerminalServer.ts +551 -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;
|
|
@@ -198,7 +202,7 @@ const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
|
198
202
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
199
203
|
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
200
204
|
const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
|
|
201
|
-
const FORWARD_ATTACHMENT_WINDOW_MS =
|
|
205
|
+
const FORWARD_ATTACHMENT_WINDOW_MS = 1_500;
|
|
202
206
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
203
207
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
204
208
|
const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
|
|
@@ -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,9 @@ export interface ControlTaskReleaseResult {
|
|
|
506
513
|
}
|
|
507
514
|
|
|
508
515
|
export interface ProxyLifecycle {
|
|
516
|
+
terminalUrl?(jobId: string): string | undefined;
|
|
517
|
+
authorizeTerminal?(action: CardAction): Promise<string>;
|
|
518
|
+
stopSafeRestartTerminal?(action: CardAction): Promise<string>;
|
|
509
519
|
supervised?: boolean;
|
|
510
520
|
restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
|
|
511
521
|
cancelSafeRestart?(scheduleId: number): Promise<boolean>;
|
|
@@ -539,9 +549,14 @@ export class ProxySessionController {
|
|
|
539
549
|
private readonly messageQueues = new Map<string, Promise<void>>();
|
|
540
550
|
private readonly switchGroupOperations = new Set<string>();
|
|
541
551
|
private readonly sessionLoads = new Map<string, Promise<LoadedSession>>();
|
|
552
|
+
private readonly settingsOnlySessions = new Set<string>();
|
|
542
553
|
private readonly queuedPromptStarts = new Map<string, Promise<void>>();
|
|
543
554
|
private readonly queuedPromptCards = new Map<string, Map<string, string>>();
|
|
544
555
|
private readonly queuedPromptCardWrites = new Map<string, Promise<void>>();
|
|
556
|
+
private readonly settingsOperations = new SessionOperationQueue();
|
|
557
|
+
// Loads may run inside settingsOperations; never acquire that queue while hydrating.
|
|
558
|
+
private readonly sessionHydrationOperations = new SessionOperationQueue();
|
|
559
|
+
private readonly agentDefaultsOperations = new SessionOperationQueue();
|
|
545
560
|
private readonly sessionResets = new Map<string, Promise<void>>();
|
|
546
561
|
private readonly resetHistoryOperations = new Map<string, string>();
|
|
547
562
|
private readonly remoteTurnHistoryRefreshes = new Map<string, number>();
|
|
@@ -551,6 +566,7 @@ export class ProxySessionController {
|
|
|
551
566
|
private readonly lastSessionListings = new Map<string, string[]>();
|
|
552
567
|
private readonly threadInitializations = new Map<string, Promise<void>>();
|
|
553
568
|
private readonly llmRetryingSessions = new Set<string>();
|
|
569
|
+
private readonly planHandoffs = new Map<string, string>();
|
|
554
570
|
private readonly retriedFailureTurnIds = new Set<string>();
|
|
555
571
|
private readonly queuedPromptRetryTimers = new Map<string, NodeJS.Timeout>();
|
|
556
572
|
private readonly recoveryRetryTimers = new Map<string, NodeJS.Timeout>();
|
|
@@ -633,7 +649,7 @@ export class ProxySessionController {
|
|
|
633
649
|
const pendingMergedForward = message.mergedForwardMessageId
|
|
634
650
|
? this.registerPendingForwardAttachment(message, this.pendingMergedForwards)
|
|
635
651
|
: undefined;
|
|
636
|
-
const pendingResourceForward = !attachmentReservation &&
|
|
652
|
+
const pendingResourceForward = !attachmentReservation && isForwardAttachmentSource(message)
|
|
637
653
|
? this.registerPendingForwardAttachment(message, this.pendingResourceForwards)
|
|
638
654
|
: undefined;
|
|
639
655
|
try {
|
|
@@ -683,7 +699,9 @@ export class ProxySessionController {
|
|
|
683
699
|
);
|
|
684
700
|
if (attachment) {
|
|
685
701
|
coalescedForwardAttachment = true;
|
|
686
|
-
const
|
|
702
|
+
const sourceText = resourceMessage.text.trim();
|
|
703
|
+
const instruction = normalizeFeishuPostText(attachment.text)
|
|
704
|
+
|| (sourceText ? DEFAULT_REFERENCED_MESSAGE_INSTRUCTION : defaultResourcePrompt(resourceMessage));
|
|
687
705
|
forwardedImages = (resourceMessage.images ?? []).map((image) => ({
|
|
688
706
|
messageId: resourceMessage.messageId,
|
|
689
707
|
imageKey: image.imageKey,
|
|
@@ -695,7 +713,7 @@ export class ProxySessionController {
|
|
|
695
713
|
}));
|
|
696
714
|
message = {
|
|
697
715
|
...attachment,
|
|
698
|
-
text: instruction,
|
|
716
|
+
text: sourceText ? referencedMessagePrompt(instruction, sourceText) : instruction,
|
|
699
717
|
displayText: instruction,
|
|
700
718
|
};
|
|
701
719
|
this.relatedReactionMessageIds.set(message.messageId, [resourceMessage.messageId]);
|
|
@@ -1015,7 +1033,13 @@ export class ProxySessionController {
|
|
|
1015
1033
|
await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
|
|
1016
1034
|
try {
|
|
1017
1035
|
const kind = String(scopedAction.value.action ?? "");
|
|
1018
|
-
if (kind === "
|
|
1036
|
+
if (kind === "terminal_authorize") {
|
|
1037
|
+
if (!this.lifecycle?.authorizeTerminal) throw new Error("网页终端未启用。");
|
|
1038
|
+
await this.outbound.sendText(contextKey, await this.lifecycle.authorizeTerminal(scopedAction));
|
|
1039
|
+
} else if (kind === "safe_restart_terminal_stop") {
|
|
1040
|
+
if (!this.lifecycle?.stopSafeRestartTerminal) throw new Error("网页终端未启用。");
|
|
1041
|
+
await this.outbound.sendText(contextKey, await this.lifecycle.stopSafeRestartTerminal(scopedAction));
|
|
1042
|
+
} else if (kind === "agentbot_update_cancel") {
|
|
1019
1043
|
if (!this.lifecycle?.cancelAutomaticUpdate) throw new Error("自动更新功能未启用。");
|
|
1020
1044
|
await this.lifecycle.cancelAutomaticUpdate(scopedAction);
|
|
1021
1045
|
} else if (kind === "agentbot_update_select") {
|
|
@@ -1231,10 +1255,14 @@ export class ProxySessionController {
|
|
|
1231
1255
|
} else if (kind === "session_status_refresh") {
|
|
1232
1256
|
await this.refreshStatusCardFromAction(scopedAction);
|
|
1233
1257
|
} else if (kind === "settings_tab_open") {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1258
|
+
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
1259
|
+
await this.openExecutionSettings(
|
|
1260
|
+
contextKey, executionSettingsTabValue(scopedAction.value.tab), {
|
|
1261
|
+
sessionId, updateMessageId: scopedAction.messageId,
|
|
1262
|
+
modelPage: modelCardPageValue(scopedAction.value.page),
|
|
1263
|
+
expectedProvider: typeof scopedAction.value.provider === "string" ? scopedAction.value.provider : undefined,
|
|
1264
|
+
},
|
|
1265
|
+
);
|
|
1238
1266
|
} else if (kind === "settings_agent_select") {
|
|
1239
1267
|
await this.setDefaultAgent(contextKey, String(scopedAction.value.agent ?? ""), {
|
|
1240
1268
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
@@ -1247,11 +1275,13 @@ export class ProxySessionController {
|
|
|
1247
1275
|
});
|
|
1248
1276
|
} else if (kind === "settings_model_select") {
|
|
1249
1277
|
await this.model(contextKey, String(scopedAction.value.model ?? ""), {
|
|
1278
|
+
expectedProvider: String(scopedAction.value.provider ?? ""),
|
|
1250
1279
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
1251
1280
|
updateMessageId: scopedAction.messageId,
|
|
1252
1281
|
});
|
|
1253
1282
|
} else if (kind === "settings_thinking_select") {
|
|
1254
1283
|
await this.thinking(contextKey, String(scopedAction.value.effort ?? ""), {
|
|
1284
|
+
expectedProvider: String(scopedAction.value.provider ?? ""),
|
|
1255
1285
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
1256
1286
|
updateMessageId: scopedAction.messageId,
|
|
1257
1287
|
expectedModel: String(scopedAction.value.model ?? ""),
|
|
@@ -1273,6 +1303,7 @@ export class ProxySessionController {
|
|
|
1273
1303
|
await this.model(contextKey, undefined, {
|
|
1274
1304
|
sessionId,
|
|
1275
1305
|
updateMessageId: scopedAction.messageId,
|
|
1306
|
+
modelPage: modelCardPageValue(scopedAction.value.page),
|
|
1276
1307
|
});
|
|
1277
1308
|
} else if (kind === "reasoning_select") {
|
|
1278
1309
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
@@ -1295,6 +1326,7 @@ export class ProxySessionController {
|
|
|
1295
1326
|
permissionModeValue(scopedAction.value.permissionMode),
|
|
1296
1327
|
String(scopedAction.value.sessionId ?? ""),
|
|
1297
1328
|
requiredCardMessageId(scopedAction.messageId),
|
|
1329
|
+
modelCardPageValue(scopedAction.value.page),
|
|
1298
1330
|
);
|
|
1299
1331
|
} else if (kind === "provider_model_select" || kind === "provider_reasoning_open") {
|
|
1300
1332
|
await this.openProviderReasoningSelector(
|
|
@@ -1363,6 +1395,7 @@ export class ProxySessionController {
|
|
|
1363
1395
|
this.queuedPromptCards.clear();
|
|
1364
1396
|
this.queuedPromptCardWrites.clear();
|
|
1365
1397
|
this.sessionResets.clear();
|
|
1398
|
+
this.planHandoffs.clear();
|
|
1366
1399
|
this.resetHistoryOperations.clear();
|
|
1367
1400
|
this.remoteTurnHistoryRefreshes.clear();
|
|
1368
1401
|
this.forkHistoryCursors.clear();
|
|
@@ -1766,79 +1799,44 @@ export class ProxySessionController {
|
|
|
1766
1799
|
models: Awaited<ReturnType<AgentRuntime["listModels"]>>;
|
|
1767
1800
|
reasoningOptions: Awaited<ReturnType<AgentRuntime["listModels"]>>[number]["supportedReasoningEfforts"];
|
|
1768
1801
|
permissionModes: PermissionMode[];
|
|
1802
|
+
pendingProvider?: PendingProviderSettings;
|
|
1769
1803
|
}> {
|
|
1770
1804
|
const record = this.requireControlSession(localSessionId);
|
|
1771
1805
|
if (setting && !value?.trim()) throw new Error(`task ${setting} requires a value.`);
|
|
1772
1806
|
const nextValue = value?.trim();
|
|
1773
|
-
if (setting
|
|
1807
|
+
if (setting && nextValue) {
|
|
1774
1808
|
const contextKey = this.controlSessionContextKey(record);
|
|
1775
|
-
await this.outbound.withReplyTarget(
|
|
1776
|
-
|
|
1777
|
-
this.
|
|
1778
|
-
()
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
const models = await loaded.runtime.listModels(nextValue, loaded.session.model);
|
|
1785
|
-
const model = selectProviderModel(models, loaded.session.model);
|
|
1786
|
-
if (!model) throw new Error("The current runtime has no model available for a Provider change.");
|
|
1787
|
-
const effort = selectProviderReasoningEffort(model, loaded.session.reasoningEffort);
|
|
1788
|
-
await this.changeProviderSettings(loaded, {
|
|
1789
|
-
modelProvider: nextValue,
|
|
1790
|
-
model: model.id,
|
|
1791
|
-
reasoningEffort: effort,
|
|
1792
|
-
permissionMode: loaded.session.permissionMode,
|
|
1809
|
+
await this.outbound.withReplyTarget(contextKey, this.controlSessionReplyTarget(record), async () => {
|
|
1810
|
+
const options = { sessionId: record.localSessionId };
|
|
1811
|
+
if (setting === "provider") await this.selectProvider(contextKey, nextValue, options);
|
|
1812
|
+
else if (setting === "model") await this.model(contextKey, nextValue, options);
|
|
1813
|
+
else if (setting === "thinking") await this.thinking(contextKey, nextValue, options);
|
|
1814
|
+
else {
|
|
1815
|
+
if (nextValue !== "auto" && nextValue !== "confirm") throw new Error("task permissions accepts auto or confirm.");
|
|
1816
|
+
await this.permissions(contextKey, nextValue, options);
|
|
1817
|
+
}
|
|
1793
1818
|
});
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
const
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
: selected.defaultReasoningEffort;
|
|
1803
|
-
await loaded.runtime.setModel(record.localSessionId, nextValue);
|
|
1804
|
-
if (nextEffort && nextEffort !== currentEffort) {
|
|
1805
|
-
await loaded.runtime.setReasoningEffort(record.localSessionId, nextEffort);
|
|
1806
|
-
}
|
|
1807
|
-
this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
|
|
1808
|
-
this.outbound.updateSessionModel(record.localSessionId, nextValue, loaded.session.modelProvider);
|
|
1809
|
-
} else if (setting === "thinking" && nextValue) {
|
|
1810
|
-
const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
|
|
1811
|
-
const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
|
|
1819
|
+
}
|
|
1820
|
+
return this.settingsOperations.run(localSessionId, async () => {
|
|
1821
|
+
const loaded = await this.loadSession(this.store.getSession(localSessionId) ?? record, true);
|
|
1822
|
+
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1823
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
1824
|
+
const selected = pending?.settings ?? session;
|
|
1825
|
+
const models = await loaded.runtime.listModels(selected.modelProvider, selected.model);
|
|
1826
|
+
const currentModel = models.find((candidate) => candidate.id === selected.model)
|
|
1812
1827
|
?? models.find((candidate) => candidate.isDefault);
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
|
|
1826
|
-
}
|
|
1827
|
-
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1828
|
-
if (setting && setting !== "provider") await this.persistAgentExecutionDefaults(session.localSessionId);
|
|
1829
|
-
const models = await loaded.runtime.listModels(session.modelProvider, session.model);
|
|
1830
|
-
const currentModel = models.find((candidate) => candidate.id === session.model)
|
|
1831
|
-
?? models.find((candidate) => candidate.isDefault);
|
|
1832
|
-
const providers = loaded.runtime.listModelProviders
|
|
1833
|
-
? await this.modelProviderOptions(loaded)
|
|
1834
|
-
: [];
|
|
1835
|
-
return {
|
|
1836
|
-
session,
|
|
1837
|
-
providers,
|
|
1838
|
-
models,
|
|
1839
|
-
reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
|
|
1840
|
-
permissionModes: ["auto", "confirm"],
|
|
1841
|
-
};
|
|
1828
|
+
const providers = loaded.runtime.listModelProviders
|
|
1829
|
+
? await this.modelProviderOptions(loaded)
|
|
1830
|
+
: [];
|
|
1831
|
+
return {
|
|
1832
|
+
session,
|
|
1833
|
+
pendingProvider: pending,
|
|
1834
|
+
providers,
|
|
1835
|
+
models,
|
|
1836
|
+
reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
|
|
1837
|
+
permissionModes: ["auto", "confirm"],
|
|
1838
|
+
};
|
|
1839
|
+
});
|
|
1842
1840
|
}
|
|
1843
1841
|
|
|
1844
1842
|
async controlTaskGoal(
|
|
@@ -2309,6 +2307,9 @@ export class ProxySessionController {
|
|
|
2309
2307
|
if (!this.recoveryRetryTimers.has(attempt.attemptId)) this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
2310
2308
|
}
|
|
2311
2309
|
}
|
|
2310
|
+
for (const pending of this.store.listPendingProviderSettings()) {
|
|
2311
|
+
await this.applyPendingProviderWhenIdle(pending.localSessionId);
|
|
2312
|
+
}
|
|
2312
2313
|
this.restorePersistedQueuedPrompts();
|
|
2313
2314
|
}
|
|
2314
2315
|
|
|
@@ -2652,6 +2653,16 @@ export class ProxySessionController {
|
|
|
2652
2653
|
});
|
|
2653
2654
|
const pendingAttempt = this.store.getTurnAttempt(attempt.attemptId);
|
|
2654
2655
|
if (!pendingAttempt || !isIncompleteTurnAttemptStatus(pendingAttempt.status)) return;
|
|
2656
|
+
const continued = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
2657
|
+
if (continued?.continuedFromTurnId && this.store.getTurnDelivery(turnId)?.progressMessageId) {
|
|
2658
|
+
this.store.updateTurnAttempt(attempt.attemptId, { pendingTurnId: null, turnId, status: "running" });
|
|
2659
|
+
await this.outbound.onEvent({ type: "turn_started", sessionId: session.localSessionId,
|
|
2660
|
+
turnId, startedAt: continued.startedAt });
|
|
2661
|
+
const synchronized = await loaded.runtime.synchronizeSession(session.localSessionId);
|
|
2662
|
+
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
2663
|
+
if (synchronized.activeTurnId) this.recoveredLiveAttempts.add(attempt.attemptId);
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2655
2666
|
const oldCardTurnId = attempt.pendingTurnId ?? attempt.turnId;
|
|
2656
2667
|
if (oldCardTurnId) {
|
|
2657
2668
|
await this.outbound.interruptTurnForRecovery(
|
|
@@ -2717,7 +2728,8 @@ export class ProxySessionController {
|
|
|
2717
2728
|
});
|
|
2718
2729
|
}
|
|
2719
2730
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
2720
|
-
const loaded = await this.loadSession({ ...current, contextKey: attempt.contextKey }
|
|
2731
|
+
const loaded = await this.loadSession({ ...current, contextKey: attempt.contextKey },
|
|
2732
|
+
Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
2721
2733
|
const recoveryText = recoveryRuntimePrompt(attempt.promptText);
|
|
2722
2734
|
const turnId = await this.startTurn(
|
|
2723
2735
|
loaded,
|
|
@@ -2784,7 +2796,7 @@ export class ProxySessionController {
|
|
|
2784
2796
|
localImagePaths?: string[],
|
|
2785
2797
|
displayPrompt?: string,
|
|
2786
2798
|
): Promise<void> {
|
|
2787
|
-
if (this.llmRetryingSessions.has(record.localSessionId)) {
|
|
2799
|
+
if (this.llmRetryingSessions.has(record.localSessionId) || this.planHandoffs.has(record.localSessionId)) {
|
|
2788
2800
|
const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
|
|
2789
2801
|
localImagePaths,
|
|
2790
2802
|
messageId,
|
|
@@ -2820,7 +2832,7 @@ export class ProxySessionController {
|
|
|
2820
2832
|
}
|
|
2821
2833
|
let loaded: LoadedSession;
|
|
2822
2834
|
try {
|
|
2823
|
-
loaded = await this.loadSession(record);
|
|
2835
|
+
loaded = await this.loadSession(record, Boolean(this.store.getPendingProviderSettings(record.localSessionId)));
|
|
2824
2836
|
} catch (error) {
|
|
2825
2837
|
if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
|
|
2826
2838
|
throw new PresentedRequestError(error);
|
|
@@ -3960,6 +3972,25 @@ export class ProxySessionController {
|
|
|
3960
3972
|
}
|
|
3961
3973
|
|
|
3962
3974
|
private async startTurn(
|
|
3975
|
+
loaded: LoadedSession, text: string, replyTarget?: MessageReplyTarget,
|
|
3976
|
+
localImagePaths?: string[], options: StartTurnOptions = {},
|
|
3977
|
+
): Promise<string> {
|
|
3978
|
+
return this.settingsOperations.run(loaded.record.localSessionId, async () => {
|
|
3979
|
+
try {
|
|
3980
|
+
await this.applyPendingProviderSettings(loaded);
|
|
3981
|
+
} catch (error) {
|
|
3982
|
+
await this.outbound.failPendingTurn(loaded.record.localSessionId, runtimeErrorMessage(error));
|
|
3983
|
+
throw error;
|
|
3984
|
+
}
|
|
3985
|
+
if (this.store.getPendingProviderSettings(loaded.record.localSessionId)) {
|
|
3986
|
+
throw new Error("Provider 切换尚未完成,请稍后重试。");
|
|
3987
|
+
}
|
|
3988
|
+
loaded.session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
|
|
3989
|
+
return this.startTurnNow(loaded, text, replyTarget, localImagePaths, options);
|
|
3990
|
+
});
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
private async startTurnNow(
|
|
3963
3994
|
loaded: LoadedSession,
|
|
3964
3995
|
text: string,
|
|
3965
3996
|
replyTarget?: MessageReplyTarget,
|
|
@@ -4015,9 +4046,15 @@ export class ProxySessionController {
|
|
|
4015
4046
|
this.store.updateTurnAttempt(attemptId, { pendingTurnId: pendingTurnId ?? null });
|
|
4016
4047
|
|
|
4017
4048
|
try {
|
|
4049
|
+
if (options.approvedPlanTurnId && (this.planHandoffs.get(loaded.record.localSessionId) !== options.approvedPlanTurnId
|
|
4050
|
+
|| this.sessionResets.has(loaded.record.localSessionId))) {
|
|
4051
|
+
throw new Error("方案执行已取消。");
|
|
4052
|
+
}
|
|
4018
4053
|
turnId = await loaded.runtime.startTurn(
|
|
4019
4054
|
loaded.record.localSessionId,
|
|
4020
|
-
|
|
4055
|
+
options.approvedPlanTurnId
|
|
4056
|
+
? { text: runtimeText, collaborationMode: "default" }
|
|
4057
|
+
: runtimePrompt(runtimeText, localImagePaths),
|
|
4021
4058
|
);
|
|
4022
4059
|
break;
|
|
4023
4060
|
} catch (error) {
|
|
@@ -4590,18 +4627,27 @@ export class ProxySessionController {
|
|
|
4590
4627
|
return { chatId: group.chatId, contextKey: groupContextKey, name: group.name };
|
|
4591
4628
|
}
|
|
4592
4629
|
|
|
4593
|
-
private async loadSession(record: SessionRecord): Promise<LoadedSession> {
|
|
4630
|
+
private async loadSession(record: SessionRecord, forSettings = false): Promise<LoadedSession> {
|
|
4594
4631
|
const agent = this.ensureAgent(record.agentName);
|
|
4595
4632
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
4596
4633
|
const existing = runtime.getSession(record.localSessionId);
|
|
4597
|
-
if (existing
|
|
4634
|
+
if (existing && (forSettings || !this.settingsOnlySessions.has(record.localSessionId))) {
|
|
4635
|
+
return { record, runtime, session: existing };
|
|
4636
|
+
}
|
|
4598
4637
|
const pending = this.sessionLoads.get(record.localSessionId);
|
|
4599
4638
|
if (pending) {
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4639
|
+
try {
|
|
4640
|
+
const loaded = await pending;
|
|
4641
|
+
if (!forSettings && this.settingsOnlySessions.has(record.localSessionId)) {
|
|
4642
|
+
if (this.sessionLoads.get(record.localSessionId) === pending) this.sessionLoads.delete(record.localSessionId);
|
|
4643
|
+
return this.loadSession(this.store.getSession(record.localSessionId) ?? record);
|
|
4644
|
+
}
|
|
4645
|
+
return { ...loaded, record: { ...loaded.record, contextKey: record.contextKey } };
|
|
4646
|
+
} catch (error) {
|
|
4647
|
+
if (!forSettings || !runtime.prepareSessionForSettings) throw error;
|
|
4648
|
+
if (this.sessionLoads.get(record.localSessionId) === pending) this.sessionLoads.delete(record.localSessionId);
|
|
4649
|
+
return this.loadSession(record, forSettings);
|
|
4650
|
+
}
|
|
4605
4651
|
}
|
|
4606
4652
|
|
|
4607
4653
|
this.outbound.registerSession(
|
|
@@ -4613,7 +4659,16 @@ export class ProxySessionController {
|
|
|
4613
4659
|
record.model,
|
|
4614
4660
|
record.modelProvider,
|
|
4615
4661
|
);
|
|
4616
|
-
const loading = (async (): Promise<LoadedSession> => {
|
|
4662
|
+
const loading = this.sessionHydrationOperations.run(record.localSessionId, async (): Promise<LoadedSession> => {
|
|
4663
|
+
const latest = this.store.getSession(record.localSessionId);
|
|
4664
|
+
if (latest) record = {
|
|
4665
|
+
...record, modelProvider: latest.modelProvider, model: latest.model,
|
|
4666
|
+
reasoningEffort: latest.reasoningEffort, permissionMode: latest.permissionMode,
|
|
4667
|
+
};
|
|
4668
|
+
const current = runtime.getSession(record.localSessionId);
|
|
4669
|
+
if (current && (forSettings || !this.settingsOnlySessions.has(record.localSessionId))) {
|
|
4670
|
+
return { record, runtime, session: current };
|
|
4671
|
+
}
|
|
4617
4672
|
if (record.lastTurnId) {
|
|
4618
4673
|
try {
|
|
4619
4674
|
await this.outbound.resumeDelivery(record.localSessionId, record.contextKey, record.lastTurnId);
|
|
@@ -4625,41 +4680,30 @@ export class ProxySessionController {
|
|
|
4625
4680
|
}
|
|
4626
4681
|
}
|
|
4627
4682
|
const permissionMode = record.permissionMode ?? "auto";
|
|
4628
|
-
|
|
4683
|
+
const prepare = forSettings && record.remoteSessionId && runtime.prepareSessionForSettings;
|
|
4684
|
+
if (record.remoteSessionId && !prepare) await this.assertSessionTurnOwnership(record, runtime);
|
|
4629
4685
|
let session: RuntimeSession;
|
|
4630
4686
|
if (record.remoteSessionId) {
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
&& !this.store.getForkHistorySource(record.localSessionId) && isMissingRolloutError(error))) throw error;
|
|
4652
|
-
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
4653
|
-
session = await runtime.createSession({
|
|
4654
|
-
localSessionId: record.localSessionId,
|
|
4655
|
-
agentName: record.agentName,
|
|
4656
|
-
cwd: record.cwd,
|
|
4657
|
-
title: record.title,
|
|
4658
|
-
modelProvider: record.modelProvider,
|
|
4659
|
-
model: record.model,
|
|
4660
|
-
reasoningEffort: record.reasoningEffort,
|
|
4661
|
-
permissionMode,
|
|
4662
|
-
});
|
|
4687
|
+
const input = {
|
|
4688
|
+
localSessionId: record.localSessionId,
|
|
4689
|
+
remoteSessionId: record.remoteSessionId,
|
|
4690
|
+
agentName: record.agentName,
|
|
4691
|
+
cwd: record.cwd,
|
|
4692
|
+
title: record.title,
|
|
4693
|
+
modelProvider: record.modelProvider,
|
|
4694
|
+
model: record.model,
|
|
4695
|
+
reasoningEffort: record.reasoningEffort,
|
|
4696
|
+
permissionMode,
|
|
4697
|
+
activeTurnId: agent.kind === "app-server" && record.status === "running" && record.lastTurnStatus === "running"
|
|
4698
|
+
? record.lastTurnId
|
|
4699
|
+
: undefined,
|
|
4700
|
+
lastTurnId: record.lastTurnId,
|
|
4701
|
+
lastTurnStatus: record.lastTurnStatus,
|
|
4702
|
+
};
|
|
4703
|
+
if (prepare) this.settingsOnlySessions.add(record.localSessionId);
|
|
4704
|
+
session = prepare ? await prepare.call(runtime, input) : await runtime.resumeSession(input);
|
|
4705
|
+
if (session.remoteSessionId !== record.remoteSessionId) {
|
|
4706
|
+
throw new Error("加载设置返回了不同的任务 ID,不允许替换任务或更改历史。");
|
|
4663
4707
|
}
|
|
4664
4708
|
} else {
|
|
4665
4709
|
session = await runtime.createSession({
|
|
@@ -4673,19 +4717,22 @@ export class ProxySessionController {
|
|
|
4673
4717
|
permissionMode,
|
|
4674
4718
|
});
|
|
4675
4719
|
}
|
|
4676
|
-
|
|
4720
|
+
if (!prepare) {
|
|
4721
|
+
this.settingsOnlySessions.delete(record.localSessionId);
|
|
4722
|
+
this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
|
|
4723
|
+
}
|
|
4677
4724
|
const saved = this.store.getSession(record.localSessionId) ?? record;
|
|
4678
4725
|
return {
|
|
4679
4726
|
record: { ...saved, contextKey: record.contextKey },
|
|
4680
4727
|
runtime,
|
|
4681
4728
|
session,
|
|
4682
4729
|
};
|
|
4683
|
-
})
|
|
4730
|
+
});
|
|
4684
4731
|
this.sessionLoads.set(record.localSessionId, loading);
|
|
4685
4732
|
try {
|
|
4686
4733
|
return await loading;
|
|
4687
4734
|
} finally {
|
|
4688
|
-
this.sessionLoads.delete(record.localSessionId);
|
|
4735
|
+
if (this.sessionLoads.get(record.localSessionId) === loading) this.sessionLoads.delete(record.localSessionId);
|
|
4689
4736
|
}
|
|
4690
4737
|
}
|
|
4691
4738
|
|
|
@@ -4734,6 +4781,14 @@ export class ProxySessionController {
|
|
|
4734
4781
|
);
|
|
4735
4782
|
}
|
|
4736
4783
|
}
|
|
4784
|
+
if (event.type === "turn_completed" && event.approvedPlan) {
|
|
4785
|
+
if (this.planHandoffs.get(event.sessionId) === event.turnId) return;
|
|
4786
|
+
const attempt = this.store.findIncompleteTurnAttemptByTurnId(event.turnId);
|
|
4787
|
+
if (attempt) {
|
|
4788
|
+
await this.continueApprovedPlan(event, attempt);
|
|
4789
|
+
return;
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4737
4792
|
const failedAttempt = event.type === "turn_failed"
|
|
4738
4793
|
? this.store.findIncompleteTurnAttemptByTurnId(event.turnId)
|
|
4739
4794
|
: undefined;
|
|
@@ -4779,7 +4834,7 @@ export class ProxySessionController {
|
|
|
4779
4834
|
...event,
|
|
4780
4835
|
turnCount: event.turnCount ?? this.store.countTaskTurns(event.sessionId),
|
|
4781
4836
|
}
|
|
4782
|
-
: event;
|
|
4837
|
+
: event.type === "turn_completed" && event.approvedPlan ? { ...event, approvedPlan: false } : event;
|
|
4783
4838
|
const presentationEvent = event.type === "turn_failed"
|
|
4784
4839
|
&& isRetryableLlmTurnFailure(event.message)
|
|
4785
4840
|
&& (failedAttempt?.retryCount ?? 0) >= MAX_LLM_TURN_RETRIES
|
|
@@ -4817,6 +4872,7 @@ export class ProxySessionController {
|
|
|
4817
4872
|
: undefined;
|
|
4818
4873
|
if (latest?.lastTurnId === event.turnId && !activeTurnId) {
|
|
4819
4874
|
this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
|
|
4875
|
+
await this.applyPendingProviderWhenIdle(event.sessionId);
|
|
4820
4876
|
await this.scheduleNextQueuedPrompt(event.sessionId);
|
|
4821
4877
|
}
|
|
4822
4878
|
}
|
|
@@ -4824,6 +4880,80 @@ export class ProxySessionController {
|
|
|
4824
4880
|
if (presentationError) throw presentationError;
|
|
4825
4881
|
}
|
|
4826
4882
|
|
|
4883
|
+
private async continueApprovedPlan(
|
|
4884
|
+
event: Extract<RuntimeEvent, { type: "turn_completed" }>,
|
|
4885
|
+
attempt: TurnAttemptRecord,
|
|
4886
|
+
): Promise<void> {
|
|
4887
|
+
const { sessionId, turnId } = event;
|
|
4888
|
+
const source = this.store.getSession(sessionId);
|
|
4889
|
+
if (!source || source.status === "closed" || source.lastTurnId !== turnId) return;
|
|
4890
|
+
this.planHandoffs.set(sessionId, turnId);
|
|
4891
|
+
this.store.markTurnAttemptTerminal(turnId, "completed");
|
|
4892
|
+
this.store.updateRuntimeSession(sessionId, { lastTurnId: turnId, lastTurnStatus: "completed" });
|
|
4893
|
+
let continued = false;
|
|
4894
|
+
let failed = false;
|
|
4895
|
+
try {
|
|
4896
|
+
try {
|
|
4897
|
+
await this.outbound.onEvent(event);
|
|
4898
|
+
} catch (error) {
|
|
4899
|
+
this.logger.warn({ error, sessionId, turnId }, "Failed to finalize the approved plan card.");
|
|
4900
|
+
}
|
|
4901
|
+
await this.settingsOperations.run(sessionId, async () => {
|
|
4902
|
+
const current = this.store.getSessionForContext(sessionId, attempt.contextKey);
|
|
4903
|
+
if (this.planHandoffs.get(sessionId) !== turnId || this.sessionResets.has(sessionId)
|
|
4904
|
+
|| !current || current.status === "closed" || current.remoteSessionId !== source.remoteSessionId) return;
|
|
4905
|
+
const loaded = await this.loadSession(current);
|
|
4906
|
+
await this.assertSessionTurnOwnership(current, loaded.runtime);
|
|
4907
|
+
if (this.planHandoffs.get(sessionId) !== turnId || this.sessionResets.has(sessionId)) return;
|
|
4908
|
+
// Another client may already have started the implementation.
|
|
4909
|
+
let nextTurnId = loaded.session.activeTurnId;
|
|
4910
|
+
if (!nextTurnId) {
|
|
4911
|
+
if (current.lastTurnId !== turnId) return;
|
|
4912
|
+
await this.applyPendingProviderSettings(loaded);
|
|
4913
|
+
if (this.store.getPendingProviderSettings(sessionId)) throw new Error("Provider 切换尚未完成。");
|
|
4914
|
+
if (this.planHandoffs.get(sessionId) !== turnId || this.sessionResets.has(sessionId)) return;
|
|
4915
|
+
nextTurnId = await this.startTurnNow(
|
|
4916
|
+
loaded,
|
|
4917
|
+
"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.",
|
|
4918
|
+
attempt.replyMessageId ? { messageId: attempt.replyMessageId, replyInThread: true } : undefined,
|
|
4919
|
+
undefined,
|
|
4920
|
+
{ messageId: attempt.messageId, displayPrompt: "方案已确认,继续执行", approvedPlanTurnId: turnId },
|
|
4921
|
+
);
|
|
4922
|
+
}
|
|
4923
|
+
continued = true;
|
|
4924
|
+
this.store.rebindPendingTurnMessages(sessionId, turnId, nextTurnId);
|
|
4925
|
+
const latest = this.store.getSession(sessionId);
|
|
4926
|
+
if (latest?.lastTurnId === nextTurnId && (latest.lastTurnStatus === "completed"
|
|
4927
|
+
|| latest.lastTurnStatus === "failed" || latest.lastTurnStatus === "cancelled")) {
|
|
4928
|
+
await this.finalizeTurnMessageReactions(nextTurnId, latest.lastTurnStatus);
|
|
4929
|
+
}
|
|
4930
|
+
this.store.audit(attempt.contextKey, "approved_plan_continued", { sessionId, turnId, nextTurnId });
|
|
4931
|
+
});
|
|
4932
|
+
} catch (error) {
|
|
4933
|
+
failed = true;
|
|
4934
|
+
this.logger.error({ error, sessionId, turnId }, "Failed to continue an approved plan.");
|
|
4935
|
+
await this.outbound.sendText(attempt.contextKey,
|
|
4936
|
+
"方案已确认,但后续执行启动失败:" + runtimeErrorMessage(error) + "。请发送消息继续。");
|
|
4937
|
+
} finally {
|
|
4938
|
+
if (this.planHandoffs.get(sessionId) === turnId) this.planHandoffs.delete(sessionId);
|
|
4939
|
+
if (!continued) {
|
|
4940
|
+
try {
|
|
4941
|
+
await this.outbound.onEvent(failed
|
|
4942
|
+
? { type: "turn_failed", sessionId, turnId, message: "方案已确认,但后续执行未能启动。" }
|
|
4943
|
+
: { type: "turn_cancelled", sessionId, turnId });
|
|
4944
|
+
} catch (error) {
|
|
4945
|
+
this.logger.warn({ error, sessionId, turnId }, "Failed to finish the plan handoff card.");
|
|
4946
|
+
}
|
|
4947
|
+
await this.finalizeTurnMessageReactions(turnId, failed ? "failed" : "cancelled");
|
|
4948
|
+
}
|
|
4949
|
+
const latest = this.store.getSession(sessionId);
|
|
4950
|
+
if (latest?.lastTurnId === turnId && latest.status !== "closed") {
|
|
4951
|
+
this.store.updateSession(sessionId, { status: failed ? "failed" : "ready" });
|
|
4952
|
+
}
|
|
4953
|
+
await this.scheduleNextQueuedPrompt(sessionId);
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4827
4957
|
private async retryFailedLlmTurn(
|
|
4828
4958
|
event: Extract<RuntimeEvent, { type: "turn_failed" }>,
|
|
4829
4959
|
attempt: TurnAttemptRecord,
|
|
@@ -4865,7 +4995,7 @@ export class ProxySessionController {
|
|
|
4865
4995
|
: undefined;
|
|
4866
4996
|
try {
|
|
4867
4997
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
4868
|
-
const loaded = await this.loadSession(current);
|
|
4998
|
+
const loaded = await this.loadSession(current, Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
4869
4999
|
const retryTurnId = await this.startTurn(
|
|
4870
5000
|
loaded,
|
|
4871
5001
|
llmRetryRuntimePrompt(attempt.promptText, retryNumber),
|
|
@@ -4913,6 +5043,7 @@ export class ProxySessionController {
|
|
|
4913
5043
|
}
|
|
4914
5044
|
|
|
4915
5045
|
private scheduleQueuedPromptRetry(sessionId: string): void {
|
|
5046
|
+
if (this.store.getPendingProviderSettings(sessionId)?.error) return;
|
|
4916
5047
|
if (this.queuedPromptRetryTimers.has(sessionId)) return;
|
|
4917
5048
|
if (this.store.countQueuedPrompts(sessionId) === 0) return;
|
|
4918
5049
|
const timer = setTimeout(() => {
|
|
@@ -4930,24 +5061,31 @@ export class ProxySessionController {
|
|
|
4930
5061
|
}
|
|
4931
5062
|
|
|
4932
5063
|
private async startNextQueuedPromptIfIdle(sessionId: string): Promise<void> {
|
|
5064
|
+
return this.settingsOperations.run(sessionId, () => this.startNextQueuedPromptIfIdleNow(sessionId));
|
|
5065
|
+
}
|
|
5066
|
+
|
|
5067
|
+
private async startNextQueuedPromptIfIdleNow(sessionId: string): Promise<void> {
|
|
5068
|
+
if (this.planHandoffs.has(sessionId) || this.sessionResets.has(sessionId)) return;
|
|
4933
5069
|
if (this.store.countQueuedPrompts(sessionId) === 0) return;
|
|
4934
5070
|
const baseRecord = this.store.getSession(sessionId);
|
|
4935
5071
|
if (!baseRecord || baseRecord.status === "closed") return;
|
|
4936
5072
|
let prompt: QueuedPromptRecord | undefined;
|
|
4937
5073
|
try {
|
|
4938
|
-
let loaded = await this.loadSession(baseRecord);
|
|
5074
|
+
let loaded = await this.loadSession(baseRecord, Boolean(this.store.getPendingProviderSettings(sessionId)));
|
|
4939
5075
|
const remoteActivity = await this.assertSessionTurnOwnership(loaded.record, loaded.runtime);
|
|
4940
5076
|
if (needsFullSessionSynchronization(loaded.record, loaded.session, remoteActivity)) {
|
|
4941
5077
|
loaded = { ...loaded, session: await loaded.runtime.synchronizeSession(sessionId) };
|
|
4942
5078
|
}
|
|
4943
5079
|
if (loaded.session.activeTurnId) return;
|
|
4944
5080
|
|
|
5081
|
+
await this.applyPendingProviderSettings(loaded);
|
|
5082
|
+
if (this.store.getPendingProviderSettings(sessionId)) return;
|
|
4945
5083
|
prompt = this.store.takeNextQueuedPrompt(sessionId);
|
|
4946
5084
|
if (!prompt) return;
|
|
4947
5085
|
const record = this.store.getSessionForContext(sessionId, prompt.contextKey) ?? baseRecord;
|
|
4948
5086
|
loaded = { ...loaded, record };
|
|
4949
5087
|
await this.refreshPromptQueueCards(sessionId);
|
|
4950
|
-
const turnId = await this.
|
|
5088
|
+
const turnId = await this.startTurnNow(
|
|
4951
5089
|
loaded,
|
|
4952
5090
|
prompt.text,
|
|
4953
5091
|
prompt.replyMessageId ? { messageId: prompt.replyMessageId, replyInThread: true } : undefined,
|
|
@@ -5018,6 +5156,7 @@ export class ProxySessionController {
|
|
|
5018
5156
|
});
|
|
5019
5157
|
const pending = {
|
|
5020
5158
|
contextKey: message.contextKey,
|
|
5159
|
+
userId: message.userId,
|
|
5021
5160
|
attachmentPromise,
|
|
5022
5161
|
resolveAttachment,
|
|
5023
5162
|
};
|
|
@@ -5026,14 +5165,15 @@ export class ProxySessionController {
|
|
|
5026
5165
|
}
|
|
5027
5166
|
|
|
5028
5167
|
private reserveForwardAttachment(message: IncomingMessage): ForwardAttachmentReservation | undefined {
|
|
5029
|
-
if (!message.parentMessageId) return undefined;
|
|
5168
|
+
if (!message.parentMessageId || isCommandMessage(message)) return undefined;
|
|
5030
5169
|
const candidates = [
|
|
5031
5170
|
{ sourceKind: "merged_forward" as const, registry: this.pendingMergedForwards },
|
|
5032
5171
|
{ sourceKind: "resource" as const, registry: this.pendingResourceForwards },
|
|
5033
5172
|
];
|
|
5034
5173
|
for (const candidate of candidates) {
|
|
5035
5174
|
const pending = candidate.registry.get(message.parentMessageId);
|
|
5036
|
-
if (!pending || pending.contextKey !== message.contextKey
|
|
5175
|
+
if (!pending || pending.contextKey !== message.contextKey
|
|
5176
|
+
|| pending.userId !== message.userId || pending.attachmentMessageId) continue;
|
|
5037
5177
|
pending.attachmentMessageId = message.messageId;
|
|
5038
5178
|
return {
|
|
5039
5179
|
sourceMessageId: message.parentMessageId,
|
|
@@ -5062,15 +5202,17 @@ export class ProxySessionController {
|
|
|
5062
5202
|
pending: PendingForwardAttachment,
|
|
5063
5203
|
registry: Map<string, PendingForwardAttachment>,
|
|
5064
5204
|
): Promise<IncomingMessage | undefined> {
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5205
|
+
const timer = setTimeout(() => {
|
|
5206
|
+
// Arrival reserves the pair before acknowledgement. A slow Reaction must not split it.
|
|
5207
|
+
if (pending.attachmentMessageId) return;
|
|
5208
|
+
if (registry.get(messageId) === pending) registry.delete(messageId);
|
|
5209
|
+
pending.resolveAttachment(undefined);
|
|
5210
|
+
}, FORWARD_ATTACHMENT_WINDOW_MS);
|
|
5211
|
+
timer.unref?.();
|
|
5070
5212
|
try {
|
|
5071
|
-
return await
|
|
5213
|
+
return await pending.attachmentPromise;
|
|
5072
5214
|
} finally {
|
|
5073
|
-
|
|
5215
|
+
clearTimeout(timer);
|
|
5074
5216
|
if (registry.get(messageId) === pending) {
|
|
5075
5217
|
registry.delete(messageId);
|
|
5076
5218
|
}
|
|
@@ -5149,6 +5291,7 @@ export class ProxySessionController {
|
|
|
5149
5291
|
}
|
|
5150
5292
|
|
|
5151
5293
|
private async cancelSession(record: SessionRecord): Promise<void> {
|
|
5294
|
+
this.planHandoffs.delete(record.localSessionId);
|
|
5152
5295
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
5153
5296
|
if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
|
|
5154
5297
|
if (runtime.getGoal && runtime.setGoal) {
|
|
@@ -5227,6 +5370,7 @@ export class ProxySessionController {
|
|
|
5227
5370
|
}
|
|
5228
5371
|
|
|
5229
5372
|
private async runSessionReset(sessionId: string, operation: () => Promise<void>): Promise<void> {
|
|
5373
|
+
this.planHandoffs.delete(sessionId);
|
|
5230
5374
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
5231
5375
|
const reset = previous.catch(() => undefined).then(operation);
|
|
5232
5376
|
this.sessionResets.set(sessionId, reset);
|
|
@@ -5410,6 +5554,20 @@ export class ProxySessionController {
|
|
|
5410
5554
|
contextKey: string,
|
|
5411
5555
|
activeTab: ExecutionSettingsTab,
|
|
5412
5556
|
options: ExecutionSettingsCardOptions = {},
|
|
5557
|
+
): Promise<void> {
|
|
5558
|
+
const record = options.sessionId
|
|
5559
|
+
? this.requireSession(contextKey, options.sessionId)
|
|
5560
|
+
: this.currentSession(contextKey);
|
|
5561
|
+
if (!record) return this.openExecutionSettingsNow(contextKey, activeTab, options);
|
|
5562
|
+
return this.settingsOperations.run(record.localSessionId, () => this.openExecutionSettingsNow(
|
|
5563
|
+
contextKey, activeTab, { ...options, sessionId: record.localSessionId },
|
|
5564
|
+
));
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
private async openExecutionSettingsNow(
|
|
5568
|
+
contextKey: string,
|
|
5569
|
+
activeTab: ExecutionSettingsTab,
|
|
5570
|
+
options: ExecutionSettingsCardOptions = {},
|
|
5413
5571
|
): Promise<void> {
|
|
5414
5572
|
const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
|
|
5415
5573
|
const record = options.sessionId
|
|
@@ -5430,24 +5588,33 @@ export class ProxySessionController {
|
|
|
5430
5588
|
reasoningOptions: [],
|
|
5431
5589
|
};
|
|
5432
5590
|
if (record) {
|
|
5433
|
-
const loaded = await this.loadSession(record);
|
|
5591
|
+
const loaded = await this.loadSession(record, true);
|
|
5592
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
5593
|
+
const selected = pending?.settings ?? loaded.session;
|
|
5594
|
+
if (pending && options.updateMessageId) {
|
|
5595
|
+
this.store.savePendingProviderSettings({
|
|
5596
|
+
...pending, contextKey, messageId: options.updateMessageId, replyMessageId: options.updateMessageId, activeTab,
|
|
5597
|
+
});
|
|
5598
|
+
}
|
|
5434
5599
|
const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
|
|
5435
5600
|
const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
|
|
5436
|
-
const currentProvider =
|
|
5437
|
-
const { models, warning } = await this.modelOptions(loaded.runtime, currentProvider,
|
|
5438
|
-
const currentModel = models.find((model) => model.id ===
|
|
5601
|
+
const currentProvider = selected.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
|
|
5602
|
+
const { models, warning } = await this.modelOptions(loaded.runtime, currentProvider, selected.model);
|
|
5603
|
+
const currentModel = models.find((model) => model.id === selected.model)
|
|
5439
5604
|
?? models.find((model) => model.isDefault)
|
|
5440
5605
|
?? models[0];
|
|
5441
5606
|
const currentEffort = currentModel?.supportedReasoningEfforts.some(
|
|
5442
|
-
(option) => option.value ===
|
|
5607
|
+
(option) => option.value === selected.reasoningEffort,
|
|
5443
5608
|
)
|
|
5444
|
-
?
|
|
5445
|
-
: currentModel?.defaultReasoningEffort ??
|
|
5609
|
+
? selected.reasoningEffort
|
|
5610
|
+
: currentModel?.defaultReasoningEffort ?? selected.reasoningEffort;
|
|
5446
5611
|
runtimeSettings = {
|
|
5612
|
+
pendingNotice: pending ? this.pendingProviderNotice(pending) : undefined,
|
|
5613
|
+
pendingFailed: Boolean(pending?.error),
|
|
5447
5614
|
currentProvider,
|
|
5448
|
-
currentModel: currentModel?.id ??
|
|
5615
|
+
currentModel: pending?.settings.model ?? currentModel?.id ?? selected.model,
|
|
5449
5616
|
currentEffort,
|
|
5450
|
-
currentPermissionMode:
|
|
5617
|
+
currentPermissionMode: selected.permissionMode,
|
|
5451
5618
|
providers,
|
|
5452
5619
|
providerSupported,
|
|
5453
5620
|
models,
|
|
@@ -5463,7 +5630,10 @@ export class ProxySessionController {
|
|
|
5463
5630
|
agents,
|
|
5464
5631
|
runtimeSettingsAvailable: Boolean(record),
|
|
5465
5632
|
...runtimeSettings,
|
|
5633
|
+
modelPage: options.expectedProvider !== undefined && options.expectedProvider !== (runtimeSettings.currentProvider ?? "")
|
|
5634
|
+
? undefined : options.modelPage,
|
|
5466
5635
|
notice: options.notice,
|
|
5636
|
+
warning: [runtimeSettings.warning, options.warning].filter(Boolean).join("\n") || undefined,
|
|
5467
5637
|
});
|
|
5468
5638
|
if (options.updateMessageId) {
|
|
5469
5639
|
await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
|
|
@@ -5481,7 +5651,12 @@ export class ProxySessionController {
|
|
|
5481
5651
|
await this.openExecutionSettings(contextKey, "agent");
|
|
5482
5652
|
}
|
|
5483
5653
|
|
|
5484
|
-
private async selectProvider(
|
|
5654
|
+
private async selectProvider(contextKey: string, modelProvider: string, options: ModelCardOptions = {}): Promise<void> {
|
|
5655
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
5656
|
+
return this.settingsOperations.run(record.localSessionId, () => this.selectProviderNow(contextKey, modelProvider, { ...options, sessionId: record.localSessionId }));
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
private async selectProviderNow(
|
|
5485
5660
|
contextKey: string,
|
|
5486
5661
|
modelProvider: string,
|
|
5487
5662
|
options: ModelCardOptions = {},
|
|
@@ -5489,19 +5664,19 @@ export class ProxySessionController {
|
|
|
5489
5664
|
const record = options.sessionId
|
|
5490
5665
|
? this.requireSession(contextKey, options.sessionId)
|
|
5491
5666
|
: this.requireCurrentSession(contextKey);
|
|
5492
|
-
await this.
|
|
5493
|
-
const
|
|
5667
|
+
const loaded = await this.loadSession(record, true);
|
|
5668
|
+
const selected = this.store.getPendingProviderSettings(record.localSessionId)?.settings ?? loaded.session;
|
|
5494
5669
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5495
|
-
const models = await loaded.runtime.listModels(modelProvider,
|
|
5496
|
-
const model = selectProviderModel(models,
|
|
5670
|
+
const models = await loaded.runtime.listModels(modelProvider, selected.model);
|
|
5671
|
+
const model = selectProviderModel(models, selected.model);
|
|
5497
5672
|
if (!model) throw new Error("当前运行时没有可用于 Provider 切换的模型。");
|
|
5498
|
-
const effort = selectProviderReasoningEffort(model,
|
|
5499
|
-
await this.
|
|
5673
|
+
const effort = selectProviderReasoningEffort(model, selected.reasoningEffort);
|
|
5674
|
+
await this.applyProviderSettingsNow(contextKey, {
|
|
5500
5675
|
provider: modelProvider,
|
|
5501
5676
|
model: model.id,
|
|
5502
5677
|
effort,
|
|
5503
|
-
mode:
|
|
5504
|
-
}, options
|
|
5678
|
+
mode: selected.permissionMode,
|
|
5679
|
+
}, options);
|
|
5505
5680
|
}
|
|
5506
5681
|
|
|
5507
5682
|
private async openProviderSelector(
|
|
@@ -5517,14 +5692,16 @@ export class ProxySessionController {
|
|
|
5517
5692
|
permissionMode: PermissionMode,
|
|
5518
5693
|
sessionId: string,
|
|
5519
5694
|
updateMessageId: string,
|
|
5695
|
+
modelPage?: number,
|
|
5520
5696
|
): Promise<void> {
|
|
5521
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5697
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
5522
5698
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5523
5699
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, loaded.session.model);
|
|
5524
5700
|
const card = this.cardRenderer.renderModelSelector({
|
|
5525
5701
|
sessionId,
|
|
5526
5702
|
contextKey,
|
|
5527
5703
|
currentModel: selectProviderModel(models, loaded.session.model)?.id,
|
|
5704
|
+
modelPage,
|
|
5528
5705
|
reasoningEffort: loaded.session.reasoningEffort,
|
|
5529
5706
|
models,
|
|
5530
5707
|
warning,
|
|
@@ -5543,7 +5720,7 @@ export class ProxySessionController {
|
|
|
5543
5720
|
sessionId: string,
|
|
5544
5721
|
updateMessageId: string,
|
|
5545
5722
|
): Promise<void> {
|
|
5546
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5723
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
5547
5724
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5548
5725
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
|
|
5549
5726
|
const selected = models.find((candidate) => candidate.id === model);
|
|
@@ -5579,7 +5756,7 @@ export class ProxySessionController {
|
|
|
5579
5756
|
sessionId: string,
|
|
5580
5757
|
updateMessageId: string,
|
|
5581
5758
|
): Promise<void> {
|
|
5582
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5759
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
5583
5760
|
await this.assertProviderModelSettings(loaded, modelProvider, model, effort);
|
|
5584
5761
|
await this.outbound.updateInteractiveCard(
|
|
5585
5762
|
contextKey,
|
|
@@ -5595,7 +5772,12 @@ export class ProxySessionController {
|
|
|
5595
5772
|
);
|
|
5596
5773
|
}
|
|
5597
5774
|
|
|
5598
|
-
private async applyProviderSettings(
|
|
5775
|
+
private async applyProviderSettings(contextKey: string, settings: { provider: string; model: string; effort: string; mode: PermissionMode }, options: ModelCardOptions = {}, announceStart = true): Promise<void> {
|
|
5776
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
5777
|
+
return this.settingsOperations.run(record.localSessionId, () => this.applyProviderSettingsNow(contextKey, settings, { ...options, sessionId: record.localSessionId }, announceStart));
|
|
5778
|
+
}
|
|
5779
|
+
|
|
5780
|
+
private async applyProviderSettingsNow(
|
|
5599
5781
|
contextKey: string,
|
|
5600
5782
|
settings: { provider: string; model: string; effort: string; mode: PermissionMode },
|
|
5601
5783
|
options: ModelCardOptions = {},
|
|
@@ -5604,17 +5786,38 @@ export class ProxySessionController {
|
|
|
5604
5786
|
const record = options.sessionId
|
|
5605
5787
|
? this.requireSession(contextKey, options.sessionId)
|
|
5606
5788
|
: this.requireCurrentSession(contextKey);
|
|
5607
|
-
|
|
5608
|
-
await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
5609
|
-
}
|
|
5610
|
-
const loaded = await this.loadSession(record);
|
|
5789
|
+
const loaded = await this.loadSession(record, true);
|
|
5611
5790
|
const warning = await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
5612
|
-
const
|
|
5613
|
-
modelProvider: settings.provider,
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5791
|
+
const next: RuntimeExecutionSettings = {
|
|
5792
|
+
modelProvider: settings.provider, model: settings.model,
|
|
5793
|
+
reasoningEffort: settings.effort, permissionMode: settings.mode,
|
|
5794
|
+
};
|
|
5795
|
+
await this.reconcilePreparedSettingsSession(loaded);
|
|
5796
|
+
if (loaded.session.activeTurnId || this.store.getPendingProviderSettings(record.localSessionId)) {
|
|
5797
|
+
await this.savePendingProviderSelection(loaded, next, contextKey, options, "provider");
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5800
|
+
if (announceStart) await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
5801
|
+
const previous = { ...loaded.session };
|
|
5802
|
+
let session: RuntimeSession;
|
|
5803
|
+
try {
|
|
5804
|
+
session = await this.changeProviderSettings(loaded, next);
|
|
5805
|
+
} catch (error) {
|
|
5806
|
+
if (!this.providerSettingsRestored(loaded, previous, error)) {
|
|
5807
|
+
this.store.savePendingProviderSettings({
|
|
5808
|
+
localSessionId: record.localSessionId, contextKey, settings: next, activeTab: "provider",
|
|
5809
|
+
messageId: options.updateMessageId,
|
|
5810
|
+
replyMessageId: options.updateMessageId ?? this.controlSessionReplyTarget(record)?.messageId,
|
|
5811
|
+
error: runtimeErrorMessage(error),
|
|
5812
|
+
});
|
|
5813
|
+
if (options.updateMessageId) {
|
|
5814
|
+
await this.openExecutionSettingsNow(contextKey, "provider", options).catch((presentationError: unknown) => {
|
|
5815
|
+
this.logger.warn({ error: presentationError, sessionId: record.localSessionId }, "Failed to refresh rejected Provider selection.");
|
|
5816
|
+
});
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
5819
|
+
throw error;
|
|
5820
|
+
}
|
|
5618
5821
|
const notice = [
|
|
5619
5822
|
`Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
|
|
5620
5823
|
`模型 ${cardCode(session.model ?? settings.model)}`,
|
|
@@ -5623,7 +5826,7 @@ export class ProxySessionController {
|
|
|
5623
5826
|
`并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
5624
5827
|
].join(",");
|
|
5625
5828
|
if (options.updateMessageId) {
|
|
5626
|
-
await this.
|
|
5829
|
+
await this.openExecutionSettingsNow(contextKey, "provider", {
|
|
5627
5830
|
sessionId: loaded.record.localSessionId,
|
|
5628
5831
|
updateMessageId: options.updateMessageId,
|
|
5629
5832
|
notice,
|
|
@@ -5634,15 +5837,23 @@ export class ProxySessionController {
|
|
|
5634
5837
|
}
|
|
5635
5838
|
|
|
5636
5839
|
private async changeProviderSettings(loaded: LoadedSession, settings: RuntimeExecutionSettings): Promise<RuntimeSession> {
|
|
5840
|
+
return this.sessionHydrationOperations.run(loaded.record.localSessionId, () => this.changeProviderSettingsNow(loaded, settings));
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
private async changeProviderSettingsNow(loaded: LoadedSession, settings: RuntimeExecutionSettings): Promise<RuntimeSession> {
|
|
5637
5844
|
if (!loaded.runtime.setExecutionSettings) throw new Error("当前运行时不支持 Provider 设置。");
|
|
5638
|
-
|
|
5845
|
+
loaded.session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
|
|
5846
|
+
const remoteSessionId = loaded.session.remoteSessionId;
|
|
5847
|
+
const persist = (candidate: RuntimeSession): Promise<void> => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
|
|
5848
|
+
if (candidate.remoteSessionId !== remoteSessionId) {
|
|
5849
|
+
throw new Error("设置切换返回了不同的任务 ID,未保存设置;不允许替换任务或更改历史。");
|
|
5850
|
+
}
|
|
5639
5851
|
assertProviderSettingsApplied(candidate, settings);
|
|
5640
5852
|
const agent = this.ensureAgent(loaded.record.agentName);
|
|
5641
5853
|
const previousDefaults = agent.defaults;
|
|
5642
|
-
await this.
|
|
5854
|
+
await this.persistAgentExecutionDefaultsNow(loaded.record.localSessionId, candidate);
|
|
5643
5855
|
try {
|
|
5644
5856
|
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
5645
|
-
remoteSessionId: candidate.remoteSessionId,
|
|
5646
5857
|
modelProvider: candidate.modelProvider,
|
|
5647
5858
|
model: candidate.model,
|
|
5648
5859
|
reasoningEffort: candidate.reasoningEffort,
|
|
@@ -5653,12 +5864,145 @@ export class ProxySessionController {
|
|
|
5653
5864
|
agent.defaults = previousDefaults;
|
|
5654
5865
|
throw error;
|
|
5655
5866
|
}
|
|
5656
|
-
};
|
|
5867
|
+
});
|
|
5657
5868
|
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
5869
|
+
this.settingsOnlySessions.delete(loaded.record.localSessionId);
|
|
5658
5870
|
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
|
|
5659
5871
|
return session;
|
|
5660
5872
|
}
|
|
5661
5873
|
|
|
5874
|
+
private async reconcilePreparedSettingsSession(loaded: LoadedSession): Promise<void> {
|
|
5875
|
+
if (!this.settingsOnlySessions.has(loaded.record.localSessionId) || !loaded.session.activeTurnId) return;
|
|
5876
|
+
const activity = await this.assertSessionTurnOwnership(loaded.record, loaded.runtime);
|
|
5877
|
+
if (activity && !activity.active) {
|
|
5878
|
+
loaded.session = await loaded.runtime.synchronizeSession(loaded.record.localSessionId);
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
|
|
5882
|
+
private providerSettingsRestored(loaded: LoadedSession, previous: RuntimeSession, error: unknown): boolean {
|
|
5883
|
+
const restored = loaded.runtime.getSession(loaded.record.localSessionId);
|
|
5884
|
+
const saved = this.store.getSession(loaded.record.localSessionId);
|
|
5885
|
+
const keys = ["remoteSessionId", "modelProvider", "model", "reasoningEffort", "permissionMode"] as const;
|
|
5886
|
+
return error instanceof ProviderSwitchError && error.previousSettingsRestored
|
|
5887
|
+
&& Boolean(restored && saved && keys.every((key) => restored[key] === previous[key] && saved[key] === previous[key]));
|
|
5888
|
+
}
|
|
5889
|
+
|
|
5890
|
+
private pendingProviderNotice(pending: PendingProviderSettings): string {
|
|
5891
|
+
const target = `Provider ${cardCode(pending.settings.modelProvider)},模型 ${cardCode(pending.settings.model)}`;
|
|
5892
|
+
return pending.error
|
|
5893
|
+
? `切换失败:${pending.error}。目标为 ${target};后续轮次已暂停,请重新选择 Provider 或模型重试。`
|
|
5894
|
+
: `待生效:当前轮次不变,结束后切换到 ${target}。可继续选择模型,以最后选择为准。`;
|
|
5895
|
+
}
|
|
5896
|
+
|
|
5897
|
+
private async savePendingProviderSelection(
|
|
5898
|
+
loaded: LoadedSession, settings: RuntimeExecutionSettings, contextKey: string,
|
|
5899
|
+
options: ModelCardOptions, activeTab: ExecutionSettingsTab,
|
|
5900
|
+
): Promise<void> {
|
|
5901
|
+
const previous = this.store.getPendingProviderSettings(loaded.record.localSessionId);
|
|
5902
|
+
const pending: PendingProviderSettings = {
|
|
5903
|
+
localSessionId: loaded.record.localSessionId, contextKey, settings, activeTab,
|
|
5904
|
+
messageId: options.updateMessageId ?? previous?.messageId,
|
|
5905
|
+
replyMessageId: options.updateMessageId ?? previous?.replyMessageId
|
|
5906
|
+
?? this.controlSessionReplyTarget(loaded.record)?.messageId,
|
|
5907
|
+
};
|
|
5908
|
+
this.store.savePendingProviderSettings(pending);
|
|
5909
|
+
await this.reconcilePreparedSettingsSession(loaded);
|
|
5910
|
+
if (!loaded.session.activeTurnId) {
|
|
5911
|
+
await this.applyPendingProviderSettings(loaded);
|
|
5912
|
+
void this.scheduleNextQueuedPrompt(loaded.record.localSessionId).catch((error: unknown) => {
|
|
5913
|
+
this.logger.warn({ error }, "Failed to resume queue after Provider selection.");
|
|
5914
|
+
});
|
|
5915
|
+
return;
|
|
5916
|
+
}
|
|
5917
|
+
if (options.updateMessageId) {
|
|
5918
|
+
await this.openExecutionSettingsNow(contextKey, activeTab, options);
|
|
5919
|
+
} else {
|
|
5920
|
+
await this.outbound.sendText(contextKey, this.pendingProviderNotice(pending).replaceAll("`", ""));
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
|
|
5924
|
+
// Caller holds settingsOperations; only a verified rollback can unblock the next turn.
|
|
5925
|
+
private async applyPendingProviderSettings(loaded: LoadedSession): Promise<void> {
|
|
5926
|
+
const pending = this.store.getPendingProviderSettings(loaded.record.localSessionId);
|
|
5927
|
+
if (!pending) return;
|
|
5928
|
+
await this.reconcilePreparedSettingsSession(loaded);
|
|
5929
|
+
if (loaded.runtime.getSession(loaded.record.localSessionId)?.activeTurnId) return;
|
|
5930
|
+
if (pending.error) throw new Error(this.pendingProviderNotice(pending));
|
|
5931
|
+
const previous = { ...(loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session) };
|
|
5932
|
+
let notice: string;
|
|
5933
|
+
let failure: unknown;
|
|
5934
|
+
let rolledBack = false;
|
|
5935
|
+
try {
|
|
5936
|
+
loaded.session = await this.changeProviderSettings(loaded, pending.settings);
|
|
5937
|
+
this.store.deletePendingProviderSettings(pending.localSessionId);
|
|
5938
|
+
notice = `Provider 已切换为 ${cardCode(pending.settings.modelProvider)},模型 ${cardCode(pending.settings.model)},从下一轮生效。`;
|
|
5939
|
+
} catch (error) {
|
|
5940
|
+
const restored = loaded.runtime.getSession(pending.localSessionId);
|
|
5941
|
+
rolledBack = this.providerSettingsRestored(loaded, previous, error);
|
|
5942
|
+
if (rolledBack) {
|
|
5943
|
+
loaded.session = restored!;
|
|
5944
|
+
this.store.deletePendingProviderSettings(pending.localSessionId);
|
|
5945
|
+
this.outbound.updateSessionModel(pending.localSessionId, previous.model, previous.modelProvider);
|
|
5946
|
+
notice = `切换到 Provider ${cardCode(pending.settings.modelProvider)}、模型 ${cardCode(pending.settings.model)} 失败:${runtimeErrorMessage(error)} 已回退到 Provider ${cardCode(previous.modelProvider ?? "Agent 默认")}、模型 ${cardCode(previous.model ?? "默认")},后续轮次继续使用原设置。`;
|
|
5947
|
+
} else {
|
|
5948
|
+
failure = error;
|
|
5949
|
+
const failed = { ...pending, error: runtimeErrorMessage(error) };
|
|
5950
|
+
this.store.savePendingProviderSettings(failed);
|
|
5951
|
+
notice = this.pendingProviderNotice(failed);
|
|
5952
|
+
}
|
|
5953
|
+
}
|
|
5954
|
+
try {
|
|
5955
|
+
await this.outbound.withReplyTarget(pending.contextKey, pending.replyMessageId
|
|
5956
|
+
? { messageId: pending.replyMessageId, replyInThread: true } : undefined, async () => {
|
|
5957
|
+
if (pending.messageId) {
|
|
5958
|
+
try {
|
|
5959
|
+
await this.openExecutionSettingsNow(pending.contextKey, pending.activeTab ?? "provider", {
|
|
5960
|
+
sessionId: pending.localSessionId, updateMessageId: pending.messageId,
|
|
5961
|
+
notice: failure || rolledBack ? undefined : notice,
|
|
5962
|
+
warning: rolledBack ? notice.replaceAll("`", "") : undefined,
|
|
5963
|
+
});
|
|
5964
|
+
return;
|
|
5965
|
+
} catch (error) {
|
|
5966
|
+
this.logger.warn({ error, sessionId: pending.localSessionId }, "Failed to refresh Provider settings card.");
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
await this.outbound.sendText(pending.contextKey, notice.replaceAll("`", ""));
|
|
5970
|
+
});
|
|
5971
|
+
} catch (error) {
|
|
5972
|
+
this.logger.warn({ error, sessionId: pending.localSessionId }, "Failed to notify Provider settings result.");
|
|
5973
|
+
}
|
|
5974
|
+
if (failure) throw failure;
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5977
|
+
private async applyPendingProviderWhenIdle(sessionId: string): Promise<void> {
|
|
5978
|
+
await this.settingsOperations.run(sessionId, async () => {
|
|
5979
|
+
if (this.recoveryClosed) return;
|
|
5980
|
+
const pending = this.store.getPendingProviderSettings(sessionId);
|
|
5981
|
+
if (!pending || pending.error) return;
|
|
5982
|
+
const record = this.store.getSession(sessionId);
|
|
5983
|
+
if (!record || record.status === "closed") {
|
|
5984
|
+
this.store.deletePendingProviderSettings(sessionId);
|
|
5985
|
+
return;
|
|
5986
|
+
}
|
|
5987
|
+
try {
|
|
5988
|
+
const loaded = await this.loadSession(record, true);
|
|
5989
|
+
await this.applyPendingProviderSettings(loaded);
|
|
5990
|
+
} catch (error) {
|
|
5991
|
+
this.logger.warn({ error, sessionId }, "Deferred Provider switch could not be applied.");
|
|
5992
|
+
if (!this.store.getPendingProviderSettings(sessionId)?.error) {
|
|
5993
|
+
const failed = { ...pending, error: runtimeErrorMessage(error) };
|
|
5994
|
+
this.store.savePendingProviderSettings(failed);
|
|
5995
|
+
await this.outbound.withReplyTarget(pending.contextKey, pending.replyMessageId
|
|
5996
|
+
? { messageId: pending.replyMessageId, replyInThread: true } : undefined,
|
|
5997
|
+
() => this.outbound.sendText(pending.contextKey, this.pendingProviderNotice(failed).replaceAll("`", "")))
|
|
5998
|
+
.catch((notificationError: unknown) => {
|
|
5999
|
+
this.logger.warn({ error: notificationError, sessionId }, "Failed to notify Provider recovery failure.");
|
|
6000
|
+
});
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
6003
|
+
});
|
|
6004
|
+
}
|
|
6005
|
+
|
|
5662
6006
|
private async assertProviderModelSettings(
|
|
5663
6007
|
loaded: LoadedSession,
|
|
5664
6008
|
modelProvider: string,
|
|
@@ -5703,36 +6047,52 @@ export class ProxySessionController {
|
|
|
5703
6047
|
}
|
|
5704
6048
|
|
|
5705
6049
|
private async model(contextKey: string, model?: string, options: ModelCardOptions = {}): Promise<void> {
|
|
6050
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
6051
|
+
return this.settingsOperations.run(record.localSessionId, () => this.modelNow(contextKey, model, { ...options, sessionId: record.localSessionId }));
|
|
6052
|
+
}
|
|
6053
|
+
|
|
6054
|
+
private async modelNow(contextKey: string, model?: string, options: ModelCardOptions = {}): Promise<void> {
|
|
5706
6055
|
if (!model) {
|
|
5707
|
-
await this.
|
|
6056
|
+
await this.openExecutionSettingsNow(contextKey, "model", options);
|
|
5708
6057
|
return;
|
|
5709
6058
|
}
|
|
5710
6059
|
const record = options.sessionId
|
|
5711
6060
|
? this.requireSession(contextKey, options.sessionId)
|
|
5712
6061
|
: this.requireCurrentSession(contextKey);
|
|
5713
|
-
const loaded = await this.loadSession(record);
|
|
5714
|
-
const
|
|
6062
|
+
const loaded = await this.loadSession(record, true);
|
|
6063
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
6064
|
+
const selectedSettings = pending?.settings ?? loaded.session;
|
|
6065
|
+
if (options.expectedProvider && options.expectedProvider !== selectedSettings.modelProvider) {
|
|
6066
|
+
throw new Error("Provider 已发生变化,请重新打开运行设置后选择。");
|
|
6067
|
+
}
|
|
6068
|
+
const models = await loaded.runtime.listModels(selectedSettings.modelProvider, selectedSettings.model);
|
|
5715
6069
|
const selected = models.find((item) => item.id === model);
|
|
5716
6070
|
if (!selected) throw new Error(`未知模型:${model}`);
|
|
5717
|
-
const currentEffort =
|
|
6071
|
+
const currentEffort = selectedSettings.reasoningEffort;
|
|
5718
6072
|
const compatible = currentEffort
|
|
5719
6073
|
? selected.supportedReasoningEfforts.length === 0
|
|
5720
6074
|
|| selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
|
|
5721
6075
|
: false;
|
|
5722
6076
|
const nextEffort = compatible ? currentEffort : selected.defaultReasoningEffort;
|
|
5723
6077
|
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
6078
|
+
if (pending) {
|
|
6079
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, model, reasoningEffort: selectProviderReasoningEffort(selected, nextEffort) }, contextKey, options, "model");
|
|
6080
|
+
return;
|
|
5727
6081
|
}
|
|
5728
|
-
this.
|
|
5729
|
-
|
|
5730
|
-
|
|
6082
|
+
await this.sessionHydrationOperations.run(loaded.record.localSessionId, async () => {
|
|
6083
|
+
await loaded.runtime.setModel(loaded.record.localSessionId, model);
|
|
6084
|
+
if (nextEffort && nextEffort !== currentEffort) {
|
|
6085
|
+
await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
|
|
6086
|
+
}
|
|
6087
|
+
this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
|
|
6088
|
+
this.outbound.updateSessionModel(loaded.record.localSessionId, model, loaded.session.modelProvider);
|
|
6089
|
+
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
6090
|
+
});
|
|
5731
6091
|
const effortMessage = nextEffort && nextEffort !== currentEffort
|
|
5732
6092
|
? `,思考强度已自动调整为 ${nextEffort}`
|
|
5733
6093
|
: "";
|
|
5734
6094
|
if (options.updateMessageId) {
|
|
5735
|
-
await this.
|
|
6095
|
+
await this.openExecutionSettingsNow(contextKey, "model", {
|
|
5736
6096
|
sessionId: loaded.record.localSessionId,
|
|
5737
6097
|
updateMessageId: options.updateMessageId,
|
|
5738
6098
|
notice: `模型已切换为 ${cardCode(model)}${effortMessage},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
@@ -5908,6 +6268,7 @@ export class ProxySessionController {
|
|
|
5908
6268
|
const created = await this.shellCommandJobs.createJob({ contextKey, sourceMessageId, command, cwd });
|
|
5909
6269
|
let cardMessageId: string | undefined;
|
|
5910
6270
|
const initialState: ShellCommandCardView = {
|
|
6271
|
+
terminalUrl: this.lifecycle?.terminalUrl?.(created.id),
|
|
5911
6272
|
jobId: created.id,
|
|
5912
6273
|
contextKey,
|
|
5913
6274
|
command,
|
|
@@ -5934,7 +6295,7 @@ export class ProxySessionController {
|
|
|
5934
6295
|
await this.outbound.updateInteractiveCard(
|
|
5935
6296
|
contextKey,
|
|
5936
6297
|
cardMessageId,
|
|
5937
|
-
this.cardRenderer.renderShellCommandCard(shellCommandJobCardView(failed)),
|
|
6298
|
+
this.cardRenderer.renderShellCommandCard({ ...shellCommandJobCardView(failed), terminalUrl: this.lifecycle?.terminalUrl?.(failed.id) }),
|
|
5938
6299
|
);
|
|
5939
6300
|
await this.shellCommandJobs.markPresented(created.id);
|
|
5940
6301
|
} catch (cardError) {
|
|
@@ -5992,7 +6353,7 @@ export class ProxySessionController {
|
|
|
5992
6353
|
): void {
|
|
5993
6354
|
if (!job.cardMessageId || this.shellCommandJobMonitors.has(job.id)) return;
|
|
5994
6355
|
const scheduler = new CardUpdateScheduler<ShellCommandCardView>({
|
|
5995
|
-
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
6356
|
+
render: (state) => this.cardRenderer.renderShellCommandCard({ ...state, terminalUrl: this.lifecycle?.terminalUrl?.(job.id) }),
|
|
5996
6357
|
write: (card) => this.outbound.updateInteractiveCard(job.contextKey, job.cardMessageId!, card),
|
|
5997
6358
|
onError: (error) => this.logger.warn(
|
|
5998
6359
|
{ error, jobId: job.id, contextKey: job.contextKey, messageId: job.cardMessageId },
|
|
@@ -6143,16 +6504,26 @@ export class ProxySessionController {
|
|
|
6143
6504
|
}
|
|
6144
6505
|
|
|
6145
6506
|
private async thinking(contextKey: string, effort?: string, options: ThinkingCardOptions = {}): Promise<void> {
|
|
6507
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
6508
|
+
return this.settingsOperations.run(record.localSessionId, () => this.thinkingNow(contextKey, effort, { ...options, sessionId: record.localSessionId }));
|
|
6509
|
+
}
|
|
6510
|
+
|
|
6511
|
+
private async thinkingNow(contextKey: string, effort?: string, options: ThinkingCardOptions = {}): Promise<void> {
|
|
6146
6512
|
if (!effort) {
|
|
6147
|
-
await this.
|
|
6513
|
+
await this.openExecutionSettingsNow(contextKey, "thinking", options);
|
|
6148
6514
|
return;
|
|
6149
6515
|
}
|
|
6150
6516
|
const record = options.sessionId
|
|
6151
6517
|
? this.requireSession(contextKey, options.sessionId)
|
|
6152
6518
|
: this.requireCurrentSession(contextKey);
|
|
6153
|
-
const loaded = await this.loadSession(record);
|
|
6154
|
-
const
|
|
6155
|
-
const
|
|
6519
|
+
const loaded = await this.loadSession(record, true);
|
|
6520
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
6521
|
+
const selectedSettings = pending?.settings ?? loaded.session;
|
|
6522
|
+
if (options.expectedProvider && options.expectedProvider !== selectedSettings.modelProvider) {
|
|
6523
|
+
throw new Error("Provider 已发生变化,请重新打开运行设置后选择。");
|
|
6524
|
+
}
|
|
6525
|
+
const models = await loaded.runtime.listModels(selectedSettings.modelProvider, selectedSettings.model);
|
|
6526
|
+
const currentModel = models.find((item) => item.id === selectedSettings.model)
|
|
6156
6527
|
?? models.find((item) => item.isDefault);
|
|
6157
6528
|
if (!currentModel) throw new Error("当前运行时没有可配置思考强度的模型。");
|
|
6158
6529
|
if (options.expectedModel && currentModel.id !== options.expectedModel) {
|
|
@@ -6164,11 +6535,17 @@ export class ProxySessionController {
|
|
|
6164
6535
|
const options = supported.map((option) => option.value).join("、") || "无";
|
|
6165
6536
|
throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
|
|
6166
6537
|
}
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6538
|
+
if (pending) {
|
|
6539
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, reasoningEffort: effort }, contextKey, options, "thinking");
|
|
6540
|
+
return;
|
|
6541
|
+
}
|
|
6542
|
+
await this.sessionHydrationOperations.run(loaded.record.localSessionId, async () => {
|
|
6543
|
+
await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, effort);
|
|
6544
|
+
this.store.updateRuntimeSession(loaded.record.localSessionId, { reasoningEffort: effort });
|
|
6545
|
+
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
6546
|
+
});
|
|
6170
6547
|
if (options.updateMessageId) {
|
|
6171
|
-
await this.
|
|
6548
|
+
await this.openExecutionSettingsNow(contextKey, "thinking", {
|
|
6172
6549
|
sessionId: loaded.record.localSessionId,
|
|
6173
6550
|
updateMessageId: options.updateMessageId,
|
|
6174
6551
|
notice: `思考强度已切换为 ${cardCode(effort)},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
@@ -6181,25 +6558,38 @@ export class ProxySessionController {
|
|
|
6181
6558
|
);
|
|
6182
6559
|
}
|
|
6183
6560
|
|
|
6184
|
-
private async permissions(
|
|
6561
|
+
private async permissions(contextKey: string, mode?: PermissionMode, options: ModelCardOptions = {}): Promise<void> {
|
|
6562
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
6563
|
+
return this.settingsOperations.run(record.localSessionId, () => this.permissionsNow(contextKey, mode, { ...options, sessionId: record.localSessionId }));
|
|
6564
|
+
}
|
|
6565
|
+
|
|
6566
|
+
private async permissionsNow(
|
|
6185
6567
|
contextKey: string,
|
|
6186
6568
|
mode?: PermissionMode,
|
|
6187
6569
|
options: ModelCardOptions = {},
|
|
6188
6570
|
): Promise<void> {
|
|
6189
6571
|
if (!mode) {
|
|
6190
|
-
await this.
|
|
6572
|
+
await this.openExecutionSettingsNow(contextKey, "permission", options);
|
|
6191
6573
|
return;
|
|
6192
6574
|
}
|
|
6193
6575
|
const record = options.sessionId
|
|
6194
6576
|
? this.requireSession(contextKey, options.sessionId)
|
|
6195
6577
|
: this.requireCurrentSession(contextKey);
|
|
6196
|
-
const loaded = await this.loadSession(record);
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6578
|
+
const loaded = await this.loadSession(record, true);
|
|
6579
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
6580
|
+
if (pending) {
|
|
6581
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, permissionMode: mode }, contextKey, options, "permission");
|
|
6582
|
+
return;
|
|
6583
|
+
}
|
|
6584
|
+
await this.sessionHydrationOperations.run(record.localSessionId, async () => {
|
|
6585
|
+
await loaded.runtime.setPermissionMode(record.localSessionId, mode);
|
|
6586
|
+
loaded.session = loaded.runtime.getSession(record.localSessionId) ?? loaded.session;
|
|
6587
|
+
loaded.session.permissionMode = mode;
|
|
6588
|
+
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: mode });
|
|
6589
|
+
await this.persistAgentExecutionDefaults(record.localSessionId);
|
|
6590
|
+
});
|
|
6201
6591
|
if (options.updateMessageId) {
|
|
6202
|
-
await this.
|
|
6592
|
+
await this.openExecutionSettingsNow(contextKey, "permission", {
|
|
6203
6593
|
sessionId: loaded.record.localSessionId,
|
|
6204
6594
|
updateMessageId: options.updateMessageId,
|
|
6205
6595
|
notice: mode === "auto"
|
|
@@ -6244,7 +6634,8 @@ export class ProxySessionController {
|
|
|
6244
6634
|
const existing = this.turnDetailHydrations.get(turnId);
|
|
6245
6635
|
if (existing) return existing;
|
|
6246
6636
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
6247
|
-
|
|
6637
|
+
const previewTurnId = snapshot?.previewTurnId ?? turnId;
|
|
6638
|
+
if ((!snapshot || snapshot.previewJournal) && this.store.previews.has(previewTurnId)) return this.store.previews.load(previewTurnId);
|
|
6248
6639
|
if (snapshot && !needsHistoricalDetails(snapshot)) return snapshot;
|
|
6249
6640
|
const origin = !snapshot ? this.store.getTurnRuntimeOrigin(turnId) : undefined;
|
|
6250
6641
|
if (!snapshot && (!origin || !this.runtimes.forAgent(origin.agentName).readRemoteTurn)) return undefined;
|
|
@@ -6270,7 +6661,7 @@ export class ProxySessionController {
|
|
|
6270
6661
|
}
|
|
6271
6662
|
// An Agent Bot turn may have arrived while the read-only request was in flight.
|
|
6272
6663
|
const latest = turnViewSnapshot(this.store.getTurnSnapshot(origin.turnId));
|
|
6273
|
-
if (latest) return latest.previewJournal ? this.store.previews.load(origin.turnId) ?? latest : latest;
|
|
6664
|
+
if (latest) return latest.previewJournal ? this.store.previews.load(latest.previewTurnId ?? origin.turnId) ?? latest : latest;
|
|
6274
6665
|
const state = hydrateTurnViewState(createTurnViewState(
|
|
6275
6666
|
origin.localSessionId, origin.turnId, details.startedAt ?? 0,
|
|
6276
6667
|
remote?.title, undefined, remote?.cwd, undefined, this.agentLabel(origin.agentName),
|
|
@@ -7245,7 +7636,7 @@ export class ProxySessionController {
|
|
|
7245
7636
|
const remoteReference = local
|
|
7246
7637
|
? remoteSessionReference(local.agentName, local.remoteSessionId!)
|
|
7247
7638
|
: taskId;
|
|
7248
|
-
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference);
|
|
7639
|
+
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference, { view: "metadata" });
|
|
7249
7640
|
if (isRemoteSessionActive(remote)) {
|
|
7250
7641
|
throw new Error("任务正在执行,无法归档。请先停止任务或等待本轮完成。");
|
|
7251
7642
|
}
|
|
@@ -7254,7 +7645,11 @@ export class ProxySessionController {
|
|
|
7254
7645
|
}
|
|
7255
7646
|
|
|
7256
7647
|
const wasCurrent = Boolean(local && this.store.getUserContext(contextKey)?.currentSessionId === local.localSessionId);
|
|
7257
|
-
|
|
7648
|
+
const allowUnmaterialized = Boolean(local && !local.lastTurnId && !local.lastTurnStatus
|
|
7649
|
+
&& !this.store.hasTurnAttemptsForSession(local.localSessionId)
|
|
7650
|
+
&& !this.store.getForkHistorySource(local.localSessionId)
|
|
7651
|
+
&& this.store.listTaskTurnGraphIndex(local.localSessionId).length === 0);
|
|
7652
|
+
await runtime.archiveRemoteSession(remote.id, { allowUnmaterialized });
|
|
7258
7653
|
const title = local?.title ?? remote.title ?? remote.preview ?? remote.id;
|
|
7259
7654
|
if (local) {
|
|
7260
7655
|
this.store.archiveSession(local.localSessionId);
|
|
@@ -7407,6 +7802,7 @@ export class ProxySessionController {
|
|
|
7407
7802
|
}
|
|
7408
7803
|
const localActiveTurnId = runtime.getSession(record.localSessionId)?.activeTurnId;
|
|
7409
7804
|
const runtimeOwnsActiveTurn = Boolean(localActiveTurnId)
|
|
7805
|
+
&& (!this.settingsOnlySessions.has(record.localSessionId) || Boolean(activity.activeTurnId))
|
|
7410
7806
|
&& (!activity.activeTurnId || activity.activeTurnId === localActiveTurnId);
|
|
7411
7807
|
const persistedTurnMatches = record.status === "running"
|
|
7412
7808
|
&& record.lastTurnStatus === "running"
|
|
@@ -7808,6 +8204,12 @@ export class ProxySessionController {
|
|
|
7808
8204
|
}
|
|
7809
8205
|
|
|
7810
8206
|
private async persistAgentExecutionDefaults(localSessionId: string, candidate?: RuntimeSession): Promise<void> {
|
|
8207
|
+
const record = this.store.getSession(localSessionId);
|
|
8208
|
+
if (!record) throw new Error(`找不到任务:${localSessionId}`);
|
|
8209
|
+
return this.agentDefaultsOperations.run(record.agentName, () => this.persistAgentExecutionDefaultsNow(localSessionId, candidate));
|
|
8210
|
+
}
|
|
8211
|
+
|
|
8212
|
+
private async persistAgentExecutionDefaultsNow(localSessionId: string, candidate?: RuntimeSession): Promise<void> {
|
|
7811
8213
|
const record = this.store.getSession(localSessionId);
|
|
7812
8214
|
if (!record) throw new Error(`找不到任务:${localSessionId}`);
|
|
7813
8215
|
const session = candidate ?? record;
|
|
@@ -8937,6 +9339,13 @@ function executionSettingsTabValue(value: unknown): ExecutionSettingsTab {
|
|
|
8937
9339
|
throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
|
|
8938
9340
|
}
|
|
8939
9341
|
|
|
9342
|
+
function modelCardPageValue(value: unknown): number | undefined {
|
|
9343
|
+
if (value === undefined) return undefined;
|
|
9344
|
+
const page = typeof value === "number" || (typeof value === "string" && value.trim()) ? Number(value) : NaN;
|
|
9345
|
+
if (!Number.isSafeInteger(page) || page < 0) throw new Error("模型卡片页码无效,请重新发送 /model。");
|
|
9346
|
+
return page;
|
|
9347
|
+
}
|
|
9348
|
+
|
|
8940
9349
|
function resetHistoryPageValue(value: unknown): number {
|
|
8941
9350
|
const page = Number(value);
|
|
8942
9351
|
if (!Number.isSafeInteger(page) || page < 0) throw new Error("历史轮次卡片页码无效,请重新发送 /turns。");
|
|
@@ -9344,10 +9753,18 @@ function defaultResourcePromptFromCounts(imageCount: number, fileCount: number):
|
|
|
9344
9753
|
return imageCount === 1 ? "请查看这张图片" : "请查看这些图片";
|
|
9345
9754
|
}
|
|
9346
9755
|
|
|
9347
|
-
function
|
|
9756
|
+
function isCommandMessage(message: IncomingMessage): boolean {
|
|
9757
|
+
const text = normalizeFeishuPostText(message.text).trimStart();
|
|
9758
|
+
return text.startsWith("/") || text.startsWith("!");
|
|
9759
|
+
}
|
|
9760
|
+
|
|
9761
|
+
function isForwardAttachmentSource(message: IncomingMessage): boolean {
|
|
9348
9762
|
return !message.mergedForwardMessageId
|
|
9349
|
-
&& message.
|
|
9350
|
-
&&
|
|
9763
|
+
&& !message.parentMessageId
|
|
9764
|
+
&& !message.contextKey.startsWith("console:")
|
|
9765
|
+
&& !isCommandMessage(message)
|
|
9766
|
+
&& (message.messageType === "post"
|
|
9767
|
+
|| (message.images?.length ?? 0) > 0 || (message.files?.length ?? 0) > 0);
|
|
9351
9768
|
}
|
|
9352
9769
|
|
|
9353
9770
|
function deduplicateImageReferences(
|
|
@@ -9424,7 +9841,8 @@ function isRetryableLlmTurnFailure(message: string): boolean {
|
|
|
9424
9841
|
const normalized = message.trim().toLowerCase();
|
|
9425
9842
|
if (!normalized) return false;
|
|
9426
9843
|
const permanentFailure = [
|
|
9427
|
-
|
|
9844
|
+
/\b(?:auth|authenticat\w*|unauthenticat\w*|authoriz\w*|unauthorized|forbidden)\b|invalid api key|incorrect api key/u,
|
|
9845
|
+
/(?:^|\D)(?:401|403)(?:\D|$)/u,
|
|
9428
9846
|
/insufficient[_ -]?quota|quota (?:exceeded|exhausted)|usage limit|billing|credit balance/u,
|
|
9429
9847
|
/context (?:length|window)|maximum context|too many tokens|token limit/u,
|
|
9430
9848
|
/invalid request|bad request|invalid argument|malformed|unsupported|not supported/u,
|