@keyou007/agent-bot 0.1.24-alpha.5 → 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 +26 -0
- package/README.md +27 -3
- package/README.zh.md +27 -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/CodexRuntime.js +202 -115
- 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/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 +66 -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 +43 -6
- 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 +615 -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 +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 +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 +9 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/CodexRuntime.ts +200 -123
- package/src/codex/ProviderSwitch.ts +11 -12
- 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 +81 -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 +44 -7
- 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 +625 -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 +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;
|
|
@@ -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,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,6 +565,7 @@ 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>();
|
|
@@ -633,7 +648,7 @@ export class ProxySessionController {
|
|
|
633
648
|
const pendingMergedForward = message.mergedForwardMessageId
|
|
634
649
|
? this.registerPendingForwardAttachment(message, this.pendingMergedForwards)
|
|
635
650
|
: undefined;
|
|
636
|
-
const pendingResourceForward = !attachmentReservation &&
|
|
651
|
+
const pendingResourceForward = !attachmentReservation && isForwardAttachmentSource(message)
|
|
637
652
|
? this.registerPendingForwardAttachment(message, this.pendingResourceForwards)
|
|
638
653
|
: undefined;
|
|
639
654
|
try {
|
|
@@ -683,7 +698,9 @@ export class ProxySessionController {
|
|
|
683
698
|
);
|
|
684
699
|
if (attachment) {
|
|
685
700
|
coalescedForwardAttachment = true;
|
|
686
|
-
const
|
|
701
|
+
const sourceText = resourceMessage.text.trim();
|
|
702
|
+
const instruction = normalizeFeishuPostText(attachment.text)
|
|
703
|
+
|| (sourceText ? DEFAULT_REFERENCED_MESSAGE_INSTRUCTION : defaultResourcePrompt(resourceMessage));
|
|
687
704
|
forwardedImages = (resourceMessage.images ?? []).map((image) => ({
|
|
688
705
|
messageId: resourceMessage.messageId,
|
|
689
706
|
imageKey: image.imageKey,
|
|
@@ -695,7 +712,7 @@ export class ProxySessionController {
|
|
|
695
712
|
}));
|
|
696
713
|
message = {
|
|
697
714
|
...attachment,
|
|
698
|
-
text: instruction,
|
|
715
|
+
text: sourceText ? referencedMessagePrompt(instruction, sourceText) : instruction,
|
|
699
716
|
displayText: instruction,
|
|
700
717
|
};
|
|
701
718
|
this.relatedReactionMessageIds.set(message.messageId, [resourceMessage.messageId]);
|
|
@@ -1015,7 +1032,10 @@ export class ProxySessionController {
|
|
|
1015
1032
|
await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
|
|
1016
1033
|
try {
|
|
1017
1034
|
const kind = String(scopedAction.value.action ?? "");
|
|
1018
|
-
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") {
|
|
1019
1039
|
if (!this.lifecycle?.cancelAutomaticUpdate) throw new Error("自动更新功能未启用。");
|
|
1020
1040
|
await this.lifecycle.cancelAutomaticUpdate(scopedAction);
|
|
1021
1041
|
} else if (kind === "agentbot_update_select") {
|
|
@@ -1231,10 +1251,14 @@ export class ProxySessionController {
|
|
|
1231
1251
|
} else if (kind === "session_status_refresh") {
|
|
1232
1252
|
await this.refreshStatusCardFromAction(scopedAction);
|
|
1233
1253
|
} else if (kind === "settings_tab_open") {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
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
|
+
);
|
|
1238
1262
|
} else if (kind === "settings_agent_select") {
|
|
1239
1263
|
await this.setDefaultAgent(contextKey, String(scopedAction.value.agent ?? ""), {
|
|
1240
1264
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
@@ -1247,11 +1271,13 @@ export class ProxySessionController {
|
|
|
1247
1271
|
});
|
|
1248
1272
|
} else if (kind === "settings_model_select") {
|
|
1249
1273
|
await this.model(contextKey, String(scopedAction.value.model ?? ""), {
|
|
1274
|
+
expectedProvider: String(scopedAction.value.provider ?? ""),
|
|
1250
1275
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
1251
1276
|
updateMessageId: scopedAction.messageId,
|
|
1252
1277
|
});
|
|
1253
1278
|
} else if (kind === "settings_thinking_select") {
|
|
1254
1279
|
await this.thinking(contextKey, String(scopedAction.value.effort ?? ""), {
|
|
1280
|
+
expectedProvider: String(scopedAction.value.provider ?? ""),
|
|
1255
1281
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
1256
1282
|
updateMessageId: scopedAction.messageId,
|
|
1257
1283
|
expectedModel: String(scopedAction.value.model ?? ""),
|
|
@@ -1273,6 +1299,7 @@ export class ProxySessionController {
|
|
|
1273
1299
|
await this.model(contextKey, undefined, {
|
|
1274
1300
|
sessionId,
|
|
1275
1301
|
updateMessageId: scopedAction.messageId,
|
|
1302
|
+
modelPage: modelCardPageValue(scopedAction.value.page),
|
|
1276
1303
|
});
|
|
1277
1304
|
} else if (kind === "reasoning_select") {
|
|
1278
1305
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
@@ -1295,6 +1322,7 @@ export class ProxySessionController {
|
|
|
1295
1322
|
permissionModeValue(scopedAction.value.permissionMode),
|
|
1296
1323
|
String(scopedAction.value.sessionId ?? ""),
|
|
1297
1324
|
requiredCardMessageId(scopedAction.messageId),
|
|
1325
|
+
modelCardPageValue(scopedAction.value.page),
|
|
1298
1326
|
);
|
|
1299
1327
|
} else if (kind === "provider_model_select" || kind === "provider_reasoning_open") {
|
|
1300
1328
|
await this.openProviderReasoningSelector(
|
|
@@ -1363,6 +1391,7 @@ export class ProxySessionController {
|
|
|
1363
1391
|
this.queuedPromptCards.clear();
|
|
1364
1392
|
this.queuedPromptCardWrites.clear();
|
|
1365
1393
|
this.sessionResets.clear();
|
|
1394
|
+
this.planHandoffs.clear();
|
|
1366
1395
|
this.resetHistoryOperations.clear();
|
|
1367
1396
|
this.remoteTurnHistoryRefreshes.clear();
|
|
1368
1397
|
this.forkHistoryCursors.clear();
|
|
@@ -1766,79 +1795,44 @@ export class ProxySessionController {
|
|
|
1766
1795
|
models: Awaited<ReturnType<AgentRuntime["listModels"]>>;
|
|
1767
1796
|
reasoningOptions: Awaited<ReturnType<AgentRuntime["listModels"]>>[number]["supportedReasoningEfforts"];
|
|
1768
1797
|
permissionModes: PermissionMode[];
|
|
1798
|
+
pendingProvider?: PendingProviderSettings;
|
|
1769
1799
|
}> {
|
|
1770
1800
|
const record = this.requireControlSession(localSessionId);
|
|
1771
1801
|
if (setting && !value?.trim()) throw new Error(`task ${setting} requires a value.`);
|
|
1772
1802
|
const nextValue = value?.trim();
|
|
1773
|
-
if (setting
|
|
1803
|
+
if (setting && nextValue) {
|
|
1774
1804
|
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,
|
|
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
|
+
}
|
|
1793
1814
|
});
|
|
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)
|
|
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)
|
|
1812
1823
|
?? 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
|
-
};
|
|
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
|
+
});
|
|
1842
1836
|
}
|
|
1843
1837
|
|
|
1844
1838
|
async controlTaskGoal(
|
|
@@ -2309,6 +2303,9 @@ export class ProxySessionController {
|
|
|
2309
2303
|
if (!this.recoveryRetryTimers.has(attempt.attemptId)) this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
2310
2304
|
}
|
|
2311
2305
|
}
|
|
2306
|
+
for (const pending of this.store.listPendingProviderSettings()) {
|
|
2307
|
+
await this.applyPendingProviderWhenIdle(pending.localSessionId);
|
|
2308
|
+
}
|
|
2312
2309
|
this.restorePersistedQueuedPrompts();
|
|
2313
2310
|
}
|
|
2314
2311
|
|
|
@@ -2652,6 +2649,16 @@ export class ProxySessionController {
|
|
|
2652
2649
|
});
|
|
2653
2650
|
const pendingAttempt = this.store.getTurnAttempt(attempt.attemptId);
|
|
2654
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
|
+
}
|
|
2655
2662
|
const oldCardTurnId = attempt.pendingTurnId ?? attempt.turnId;
|
|
2656
2663
|
if (oldCardTurnId) {
|
|
2657
2664
|
await this.outbound.interruptTurnForRecovery(
|
|
@@ -2717,7 +2724,8 @@ export class ProxySessionController {
|
|
|
2717
2724
|
});
|
|
2718
2725
|
}
|
|
2719
2726
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
2720
|
-
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)));
|
|
2721
2729
|
const recoveryText = recoveryRuntimePrompt(attempt.promptText);
|
|
2722
2730
|
const turnId = await this.startTurn(
|
|
2723
2731
|
loaded,
|
|
@@ -2784,7 +2792,7 @@ export class ProxySessionController {
|
|
|
2784
2792
|
localImagePaths?: string[],
|
|
2785
2793
|
displayPrompt?: string,
|
|
2786
2794
|
): Promise<void> {
|
|
2787
|
-
if (this.llmRetryingSessions.has(record.localSessionId)) {
|
|
2795
|
+
if (this.llmRetryingSessions.has(record.localSessionId) || this.planHandoffs.has(record.localSessionId)) {
|
|
2788
2796
|
const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
|
|
2789
2797
|
localImagePaths,
|
|
2790
2798
|
messageId,
|
|
@@ -2820,7 +2828,7 @@ export class ProxySessionController {
|
|
|
2820
2828
|
}
|
|
2821
2829
|
let loaded: LoadedSession;
|
|
2822
2830
|
try {
|
|
2823
|
-
loaded = await this.loadSession(record);
|
|
2831
|
+
loaded = await this.loadSession(record, Boolean(this.store.getPendingProviderSettings(record.localSessionId)));
|
|
2824
2832
|
} catch (error) {
|
|
2825
2833
|
if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
|
|
2826
2834
|
throw new PresentedRequestError(error);
|
|
@@ -3960,6 +3968,25 @@ export class ProxySessionController {
|
|
|
3960
3968
|
}
|
|
3961
3969
|
|
|
3962
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(
|
|
3963
3990
|
loaded: LoadedSession,
|
|
3964
3991
|
text: string,
|
|
3965
3992
|
replyTarget?: MessageReplyTarget,
|
|
@@ -4015,9 +4042,15 @@ export class ProxySessionController {
|
|
|
4015
4042
|
this.store.updateTurnAttempt(attemptId, { pendingTurnId: pendingTurnId ?? null });
|
|
4016
4043
|
|
|
4017
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
|
+
}
|
|
4018
4049
|
turnId = await loaded.runtime.startTurn(
|
|
4019
4050
|
loaded.record.localSessionId,
|
|
4020
|
-
|
|
4051
|
+
options.approvedPlanTurnId
|
|
4052
|
+
? { text: runtimeText, collaborationMode: "default" }
|
|
4053
|
+
: runtimePrompt(runtimeText, localImagePaths),
|
|
4021
4054
|
);
|
|
4022
4055
|
break;
|
|
4023
4056
|
} catch (error) {
|
|
@@ -4590,18 +4623,27 @@ export class ProxySessionController {
|
|
|
4590
4623
|
return { chatId: group.chatId, contextKey: groupContextKey, name: group.name };
|
|
4591
4624
|
}
|
|
4592
4625
|
|
|
4593
|
-
private async loadSession(record: SessionRecord): Promise<LoadedSession> {
|
|
4626
|
+
private async loadSession(record: SessionRecord, forSettings = false): Promise<LoadedSession> {
|
|
4594
4627
|
const agent = this.ensureAgent(record.agentName);
|
|
4595
4628
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
4596
4629
|
const existing = runtime.getSession(record.localSessionId);
|
|
4597
|
-
if (existing
|
|
4630
|
+
if (existing && (forSettings || !this.settingsOnlySessions.has(record.localSessionId))) {
|
|
4631
|
+
return { record, runtime, session: existing };
|
|
4632
|
+
}
|
|
4598
4633
|
const pending = this.sessionLoads.get(record.localSessionId);
|
|
4599
4634
|
if (pending) {
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
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
|
+
}
|
|
4605
4647
|
}
|
|
4606
4648
|
|
|
4607
4649
|
this.outbound.registerSession(
|
|
@@ -4613,7 +4655,16 @@ export class ProxySessionController {
|
|
|
4613
4655
|
record.model,
|
|
4614
4656
|
record.modelProvider,
|
|
4615
4657
|
);
|
|
4616
|
-
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
|
+
}
|
|
4617
4668
|
if (record.lastTurnId) {
|
|
4618
4669
|
try {
|
|
4619
4670
|
await this.outbound.resumeDelivery(record.localSessionId, record.contextKey, record.lastTurnId);
|
|
@@ -4625,41 +4676,30 @@ export class ProxySessionController {
|
|
|
4625
4676
|
}
|
|
4626
4677
|
}
|
|
4627
4678
|
const permissionMode = record.permissionMode ?? "auto";
|
|
4628
|
-
|
|
4679
|
+
const prepare = forSettings && record.remoteSessionId && runtime.prepareSessionForSettings;
|
|
4680
|
+
if (record.remoteSessionId && !prepare) await this.assertSessionTurnOwnership(record, runtime);
|
|
4629
4681
|
let session: RuntimeSession;
|
|
4630
4682
|
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
|
-
});
|
|
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,不允许替换任务或更改历史。");
|
|
4663
4703
|
}
|
|
4664
4704
|
} else {
|
|
4665
4705
|
session = await runtime.createSession({
|
|
@@ -4673,19 +4713,22 @@ export class ProxySessionController {
|
|
|
4673
4713
|
permissionMode,
|
|
4674
4714
|
});
|
|
4675
4715
|
}
|
|
4676
|
-
|
|
4716
|
+
if (!prepare) {
|
|
4717
|
+
this.settingsOnlySessions.delete(record.localSessionId);
|
|
4718
|
+
this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
|
|
4719
|
+
}
|
|
4677
4720
|
const saved = this.store.getSession(record.localSessionId) ?? record;
|
|
4678
4721
|
return {
|
|
4679
4722
|
record: { ...saved, contextKey: record.contextKey },
|
|
4680
4723
|
runtime,
|
|
4681
4724
|
session,
|
|
4682
4725
|
};
|
|
4683
|
-
})
|
|
4726
|
+
});
|
|
4684
4727
|
this.sessionLoads.set(record.localSessionId, loading);
|
|
4685
4728
|
try {
|
|
4686
4729
|
return await loading;
|
|
4687
4730
|
} finally {
|
|
4688
|
-
this.sessionLoads.delete(record.localSessionId);
|
|
4731
|
+
if (this.sessionLoads.get(record.localSessionId) === loading) this.sessionLoads.delete(record.localSessionId);
|
|
4689
4732
|
}
|
|
4690
4733
|
}
|
|
4691
4734
|
|
|
@@ -4734,6 +4777,14 @@ export class ProxySessionController {
|
|
|
4734
4777
|
);
|
|
4735
4778
|
}
|
|
4736
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
|
+
}
|
|
4737
4788
|
const failedAttempt = event.type === "turn_failed"
|
|
4738
4789
|
? this.store.findIncompleteTurnAttemptByTurnId(event.turnId)
|
|
4739
4790
|
: undefined;
|
|
@@ -4779,7 +4830,7 @@ export class ProxySessionController {
|
|
|
4779
4830
|
...event,
|
|
4780
4831
|
turnCount: event.turnCount ?? this.store.countTaskTurns(event.sessionId),
|
|
4781
4832
|
}
|
|
4782
|
-
: event;
|
|
4833
|
+
: event.type === "turn_completed" && event.approvedPlan ? { ...event, approvedPlan: false } : event;
|
|
4783
4834
|
const presentationEvent = event.type === "turn_failed"
|
|
4784
4835
|
&& isRetryableLlmTurnFailure(event.message)
|
|
4785
4836
|
&& (failedAttempt?.retryCount ?? 0) >= MAX_LLM_TURN_RETRIES
|
|
@@ -4817,6 +4868,7 @@ export class ProxySessionController {
|
|
|
4817
4868
|
: undefined;
|
|
4818
4869
|
if (latest?.lastTurnId === event.turnId && !activeTurnId) {
|
|
4819
4870
|
this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
|
|
4871
|
+
await this.applyPendingProviderWhenIdle(event.sessionId);
|
|
4820
4872
|
await this.scheduleNextQueuedPrompt(event.sessionId);
|
|
4821
4873
|
}
|
|
4822
4874
|
}
|
|
@@ -4824,6 +4876,80 @@ export class ProxySessionController {
|
|
|
4824
4876
|
if (presentationError) throw presentationError;
|
|
4825
4877
|
}
|
|
4826
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
|
+
|
|
4827
4953
|
private async retryFailedLlmTurn(
|
|
4828
4954
|
event: Extract<RuntimeEvent, { type: "turn_failed" }>,
|
|
4829
4955
|
attempt: TurnAttemptRecord,
|
|
@@ -4865,7 +4991,7 @@ export class ProxySessionController {
|
|
|
4865
4991
|
: undefined;
|
|
4866
4992
|
try {
|
|
4867
4993
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
4868
|
-
const loaded = await this.loadSession(current);
|
|
4994
|
+
const loaded = await this.loadSession(current, Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
4869
4995
|
const retryTurnId = await this.startTurn(
|
|
4870
4996
|
loaded,
|
|
4871
4997
|
llmRetryRuntimePrompt(attempt.promptText, retryNumber),
|
|
@@ -4913,6 +5039,7 @@ export class ProxySessionController {
|
|
|
4913
5039
|
}
|
|
4914
5040
|
|
|
4915
5041
|
private scheduleQueuedPromptRetry(sessionId: string): void {
|
|
5042
|
+
if (this.store.getPendingProviderSettings(sessionId)?.error) return;
|
|
4916
5043
|
if (this.queuedPromptRetryTimers.has(sessionId)) return;
|
|
4917
5044
|
if (this.store.countQueuedPrompts(sessionId) === 0) return;
|
|
4918
5045
|
const timer = setTimeout(() => {
|
|
@@ -4930,24 +5057,31 @@ export class ProxySessionController {
|
|
|
4930
5057
|
}
|
|
4931
5058
|
|
|
4932
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;
|
|
4933
5065
|
if (this.store.countQueuedPrompts(sessionId) === 0) return;
|
|
4934
5066
|
const baseRecord = this.store.getSession(sessionId);
|
|
4935
5067
|
if (!baseRecord || baseRecord.status === "closed") return;
|
|
4936
5068
|
let prompt: QueuedPromptRecord | undefined;
|
|
4937
5069
|
try {
|
|
4938
|
-
let loaded = await this.loadSession(baseRecord);
|
|
5070
|
+
let loaded = await this.loadSession(baseRecord, Boolean(this.store.getPendingProviderSettings(sessionId)));
|
|
4939
5071
|
const remoteActivity = await this.assertSessionTurnOwnership(loaded.record, loaded.runtime);
|
|
4940
5072
|
if (needsFullSessionSynchronization(loaded.record, loaded.session, remoteActivity)) {
|
|
4941
5073
|
loaded = { ...loaded, session: await loaded.runtime.synchronizeSession(sessionId) };
|
|
4942
5074
|
}
|
|
4943
5075
|
if (loaded.session.activeTurnId) return;
|
|
4944
5076
|
|
|
5077
|
+
await this.applyPendingProviderSettings(loaded);
|
|
5078
|
+
if (this.store.getPendingProviderSettings(sessionId)) return;
|
|
4945
5079
|
prompt = this.store.takeNextQueuedPrompt(sessionId);
|
|
4946
5080
|
if (!prompt) return;
|
|
4947
5081
|
const record = this.store.getSessionForContext(sessionId, prompt.contextKey) ?? baseRecord;
|
|
4948
5082
|
loaded = { ...loaded, record };
|
|
4949
5083
|
await this.refreshPromptQueueCards(sessionId);
|
|
4950
|
-
const turnId = await this.
|
|
5084
|
+
const turnId = await this.startTurnNow(
|
|
4951
5085
|
loaded,
|
|
4952
5086
|
prompt.text,
|
|
4953
5087
|
prompt.replyMessageId ? { messageId: prompt.replyMessageId, replyInThread: true } : undefined,
|
|
@@ -5018,6 +5152,7 @@ export class ProxySessionController {
|
|
|
5018
5152
|
});
|
|
5019
5153
|
const pending = {
|
|
5020
5154
|
contextKey: message.contextKey,
|
|
5155
|
+
userId: message.userId,
|
|
5021
5156
|
attachmentPromise,
|
|
5022
5157
|
resolveAttachment,
|
|
5023
5158
|
};
|
|
@@ -5026,14 +5161,15 @@ export class ProxySessionController {
|
|
|
5026
5161
|
}
|
|
5027
5162
|
|
|
5028
5163
|
private reserveForwardAttachment(message: IncomingMessage): ForwardAttachmentReservation | undefined {
|
|
5029
|
-
if (!message.parentMessageId) return undefined;
|
|
5164
|
+
if (!message.parentMessageId || isCommandMessage(message)) return undefined;
|
|
5030
5165
|
const candidates = [
|
|
5031
5166
|
{ sourceKind: "merged_forward" as const, registry: this.pendingMergedForwards },
|
|
5032
5167
|
{ sourceKind: "resource" as const, registry: this.pendingResourceForwards },
|
|
5033
5168
|
];
|
|
5034
5169
|
for (const candidate of candidates) {
|
|
5035
5170
|
const pending = candidate.registry.get(message.parentMessageId);
|
|
5036
|
-
if (!pending || pending.contextKey !== message.contextKey
|
|
5171
|
+
if (!pending || pending.contextKey !== message.contextKey
|
|
5172
|
+
|| pending.userId !== message.userId || pending.attachmentMessageId) continue;
|
|
5037
5173
|
pending.attachmentMessageId = message.messageId;
|
|
5038
5174
|
return {
|
|
5039
5175
|
sourceMessageId: message.parentMessageId,
|
|
@@ -5062,15 +5198,17 @@ export class ProxySessionController {
|
|
|
5062
5198
|
pending: PendingForwardAttachment,
|
|
5063
5199
|
registry: Map<string, PendingForwardAttachment>,
|
|
5064
5200
|
): Promise<IncomingMessage | undefined> {
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
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?.();
|
|
5070
5208
|
try {
|
|
5071
|
-
return await
|
|
5209
|
+
return await pending.attachmentPromise;
|
|
5072
5210
|
} finally {
|
|
5073
|
-
|
|
5211
|
+
clearTimeout(timer);
|
|
5074
5212
|
if (registry.get(messageId) === pending) {
|
|
5075
5213
|
registry.delete(messageId);
|
|
5076
5214
|
}
|
|
@@ -5149,6 +5287,7 @@ export class ProxySessionController {
|
|
|
5149
5287
|
}
|
|
5150
5288
|
|
|
5151
5289
|
private async cancelSession(record: SessionRecord): Promise<void> {
|
|
5290
|
+
this.planHandoffs.delete(record.localSessionId);
|
|
5152
5291
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
5153
5292
|
if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
|
|
5154
5293
|
if (runtime.getGoal && runtime.setGoal) {
|
|
@@ -5227,6 +5366,7 @@ export class ProxySessionController {
|
|
|
5227
5366
|
}
|
|
5228
5367
|
|
|
5229
5368
|
private async runSessionReset(sessionId: string, operation: () => Promise<void>): Promise<void> {
|
|
5369
|
+
this.planHandoffs.delete(sessionId);
|
|
5230
5370
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
5231
5371
|
const reset = previous.catch(() => undefined).then(operation);
|
|
5232
5372
|
this.sessionResets.set(sessionId, reset);
|
|
@@ -5410,6 +5550,20 @@ export class ProxySessionController {
|
|
|
5410
5550
|
contextKey: string,
|
|
5411
5551
|
activeTab: ExecutionSettingsTab,
|
|
5412
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 = {},
|
|
5413
5567
|
): Promise<void> {
|
|
5414
5568
|
const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
|
|
5415
5569
|
const record = options.sessionId
|
|
@@ -5430,24 +5584,33 @@ export class ProxySessionController {
|
|
|
5430
5584
|
reasoningOptions: [],
|
|
5431
5585
|
};
|
|
5432
5586
|
if (record) {
|
|
5433
|
-
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
|
+
}
|
|
5434
5595
|
const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
|
|
5435
5596
|
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 ===
|
|
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)
|
|
5439
5600
|
?? models.find((model) => model.isDefault)
|
|
5440
5601
|
?? models[0];
|
|
5441
5602
|
const currentEffort = currentModel?.supportedReasoningEfforts.some(
|
|
5442
|
-
(option) => option.value ===
|
|
5603
|
+
(option) => option.value === selected.reasoningEffort,
|
|
5443
5604
|
)
|
|
5444
|
-
?
|
|
5445
|
-
: currentModel?.defaultReasoningEffort ??
|
|
5605
|
+
? selected.reasoningEffort
|
|
5606
|
+
: currentModel?.defaultReasoningEffort ?? selected.reasoningEffort;
|
|
5446
5607
|
runtimeSettings = {
|
|
5608
|
+
pendingNotice: pending ? this.pendingProviderNotice(pending) : undefined,
|
|
5609
|
+
pendingFailed: Boolean(pending?.error),
|
|
5447
5610
|
currentProvider,
|
|
5448
|
-
currentModel: currentModel?.id ??
|
|
5611
|
+
currentModel: pending?.settings.model ?? currentModel?.id ?? selected.model,
|
|
5449
5612
|
currentEffort,
|
|
5450
|
-
currentPermissionMode:
|
|
5613
|
+
currentPermissionMode: selected.permissionMode,
|
|
5451
5614
|
providers,
|
|
5452
5615
|
providerSupported,
|
|
5453
5616
|
models,
|
|
@@ -5463,7 +5626,10 @@ export class ProxySessionController {
|
|
|
5463
5626
|
agents,
|
|
5464
5627
|
runtimeSettingsAvailable: Boolean(record),
|
|
5465
5628
|
...runtimeSettings,
|
|
5629
|
+
modelPage: options.expectedProvider !== undefined && options.expectedProvider !== (runtimeSettings.currentProvider ?? "")
|
|
5630
|
+
? undefined : options.modelPage,
|
|
5466
5631
|
notice: options.notice,
|
|
5632
|
+
warning: [runtimeSettings.warning, options.warning].filter(Boolean).join("\n") || undefined,
|
|
5467
5633
|
});
|
|
5468
5634
|
if (options.updateMessageId) {
|
|
5469
5635
|
await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
|
|
@@ -5481,7 +5647,12 @@ export class ProxySessionController {
|
|
|
5481
5647
|
await this.openExecutionSettings(contextKey, "agent");
|
|
5482
5648
|
}
|
|
5483
5649
|
|
|
5484
|
-
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(
|
|
5485
5656
|
contextKey: string,
|
|
5486
5657
|
modelProvider: string,
|
|
5487
5658
|
options: ModelCardOptions = {},
|
|
@@ -5489,19 +5660,19 @@ export class ProxySessionController {
|
|
|
5489
5660
|
const record = options.sessionId
|
|
5490
5661
|
? this.requireSession(contextKey, options.sessionId)
|
|
5491
5662
|
: this.requireCurrentSession(contextKey);
|
|
5492
|
-
await this.
|
|
5493
|
-
const
|
|
5663
|
+
const loaded = await this.loadSession(record, true);
|
|
5664
|
+
const selected = this.store.getPendingProviderSettings(record.localSessionId)?.settings ?? loaded.session;
|
|
5494
5665
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5495
|
-
const models = await loaded.runtime.listModels(modelProvider,
|
|
5496
|
-
const model = selectProviderModel(models,
|
|
5666
|
+
const models = await loaded.runtime.listModels(modelProvider, selected.model);
|
|
5667
|
+
const model = selectProviderModel(models, selected.model);
|
|
5497
5668
|
if (!model) throw new Error("当前运行时没有可用于 Provider 切换的模型。");
|
|
5498
|
-
const effort = selectProviderReasoningEffort(model,
|
|
5499
|
-
await this.
|
|
5669
|
+
const effort = selectProviderReasoningEffort(model, selected.reasoningEffort);
|
|
5670
|
+
await this.applyProviderSettingsNow(contextKey, {
|
|
5500
5671
|
provider: modelProvider,
|
|
5501
5672
|
model: model.id,
|
|
5502
5673
|
effort,
|
|
5503
|
-
mode:
|
|
5504
|
-
}, options
|
|
5674
|
+
mode: selected.permissionMode,
|
|
5675
|
+
}, options);
|
|
5505
5676
|
}
|
|
5506
5677
|
|
|
5507
5678
|
private async openProviderSelector(
|
|
@@ -5517,14 +5688,16 @@ export class ProxySessionController {
|
|
|
5517
5688
|
permissionMode: PermissionMode,
|
|
5518
5689
|
sessionId: string,
|
|
5519
5690
|
updateMessageId: string,
|
|
5691
|
+
modelPage?: number,
|
|
5520
5692
|
): Promise<void> {
|
|
5521
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5693
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
5522
5694
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5523
5695
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, loaded.session.model);
|
|
5524
5696
|
const card = this.cardRenderer.renderModelSelector({
|
|
5525
5697
|
sessionId,
|
|
5526
5698
|
contextKey,
|
|
5527
5699
|
currentModel: selectProviderModel(models, loaded.session.model)?.id,
|
|
5700
|
+
modelPage,
|
|
5528
5701
|
reasoningEffort: loaded.session.reasoningEffort,
|
|
5529
5702
|
models,
|
|
5530
5703
|
warning,
|
|
@@ -5543,7 +5716,7 @@ export class ProxySessionController {
|
|
|
5543
5716
|
sessionId: string,
|
|
5544
5717
|
updateMessageId: string,
|
|
5545
5718
|
): Promise<void> {
|
|
5546
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5719
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
5547
5720
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5548
5721
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
|
|
5549
5722
|
const selected = models.find((candidate) => candidate.id === model);
|
|
@@ -5579,7 +5752,7 @@ export class ProxySessionController {
|
|
|
5579
5752
|
sessionId: string,
|
|
5580
5753
|
updateMessageId: string,
|
|
5581
5754
|
): Promise<void> {
|
|
5582
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5755
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
5583
5756
|
await this.assertProviderModelSettings(loaded, modelProvider, model, effort);
|
|
5584
5757
|
await this.outbound.updateInteractiveCard(
|
|
5585
5758
|
contextKey,
|
|
@@ -5595,7 +5768,12 @@ export class ProxySessionController {
|
|
|
5595
5768
|
);
|
|
5596
5769
|
}
|
|
5597
5770
|
|
|
5598
|
-
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(
|
|
5599
5777
|
contextKey: string,
|
|
5600
5778
|
settings: { provider: string; model: string; effort: string; mode: PermissionMode },
|
|
5601
5779
|
options: ModelCardOptions = {},
|
|
@@ -5604,17 +5782,38 @@ export class ProxySessionController {
|
|
|
5604
5782
|
const record = options.sessionId
|
|
5605
5783
|
? this.requireSession(contextKey, options.sessionId)
|
|
5606
5784
|
: this.requireCurrentSession(contextKey);
|
|
5607
|
-
|
|
5608
|
-
await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
5609
|
-
}
|
|
5610
|
-
const loaded = await this.loadSession(record);
|
|
5785
|
+
const loaded = await this.loadSession(record, true);
|
|
5611
5786
|
const warning = await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
5612
|
-
const
|
|
5613
|
-
modelProvider: settings.provider,
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
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
|
+
}
|
|
5618
5817
|
const notice = [
|
|
5619
5818
|
`Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
|
|
5620
5819
|
`模型 ${cardCode(session.model ?? settings.model)}`,
|
|
@@ -5623,7 +5822,7 @@ export class ProxySessionController {
|
|
|
5623
5822
|
`并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
5624
5823
|
].join(",");
|
|
5625
5824
|
if (options.updateMessageId) {
|
|
5626
|
-
await this.
|
|
5825
|
+
await this.openExecutionSettingsNow(contextKey, "provider", {
|
|
5627
5826
|
sessionId: loaded.record.localSessionId,
|
|
5628
5827
|
updateMessageId: options.updateMessageId,
|
|
5629
5828
|
notice,
|
|
@@ -5634,15 +5833,23 @@ export class ProxySessionController {
|
|
|
5634
5833
|
}
|
|
5635
5834
|
|
|
5636
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> {
|
|
5637
5840
|
if (!loaded.runtime.setExecutionSettings) throw new Error("当前运行时不支持 Provider 设置。");
|
|
5638
|
-
|
|
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
|
+
}
|
|
5639
5847
|
assertProviderSettingsApplied(candidate, settings);
|
|
5640
5848
|
const agent = this.ensureAgent(loaded.record.agentName);
|
|
5641
5849
|
const previousDefaults = agent.defaults;
|
|
5642
|
-
await this.
|
|
5850
|
+
await this.persistAgentExecutionDefaultsNow(loaded.record.localSessionId, candidate);
|
|
5643
5851
|
try {
|
|
5644
5852
|
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
5645
|
-
remoteSessionId: candidate.remoteSessionId,
|
|
5646
5853
|
modelProvider: candidate.modelProvider,
|
|
5647
5854
|
model: candidate.model,
|
|
5648
5855
|
reasoningEffort: candidate.reasoningEffort,
|
|
@@ -5653,12 +5860,145 @@ export class ProxySessionController {
|
|
|
5653
5860
|
agent.defaults = previousDefaults;
|
|
5654
5861
|
throw error;
|
|
5655
5862
|
}
|
|
5656
|
-
};
|
|
5863
|
+
});
|
|
5657
5864
|
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
5865
|
+
this.settingsOnlySessions.delete(loaded.record.localSessionId);
|
|
5658
5866
|
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
|
|
5659
5867
|
return session;
|
|
5660
5868
|
}
|
|
5661
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
|
+
|
|
5662
6002
|
private async assertProviderModelSettings(
|
|
5663
6003
|
loaded: LoadedSession,
|
|
5664
6004
|
modelProvider: string,
|
|
@@ -5703,36 +6043,52 @@ export class ProxySessionController {
|
|
|
5703
6043
|
}
|
|
5704
6044
|
|
|
5705
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> {
|
|
5706
6051
|
if (!model) {
|
|
5707
|
-
await this.
|
|
6052
|
+
await this.openExecutionSettingsNow(contextKey, "model", options);
|
|
5708
6053
|
return;
|
|
5709
6054
|
}
|
|
5710
6055
|
const record = options.sessionId
|
|
5711
6056
|
? this.requireSession(contextKey, options.sessionId)
|
|
5712
6057
|
: this.requireCurrentSession(contextKey);
|
|
5713
|
-
const loaded = await this.loadSession(record);
|
|
5714
|
-
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);
|
|
5715
6065
|
const selected = models.find((item) => item.id === model);
|
|
5716
6066
|
if (!selected) throw new Error(`未知模型:${model}`);
|
|
5717
|
-
const currentEffort =
|
|
6067
|
+
const currentEffort = selectedSettings.reasoningEffort;
|
|
5718
6068
|
const compatible = currentEffort
|
|
5719
6069
|
? selected.supportedReasoningEfforts.length === 0
|
|
5720
6070
|
|| selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
|
|
5721
6071
|
: false;
|
|
5722
6072
|
const nextEffort = compatible ? currentEffort : selected.defaultReasoningEffort;
|
|
5723
6073
|
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
6074
|
+
if (pending) {
|
|
6075
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, model, reasoningEffort: selectProviderReasoningEffort(selected, nextEffort) }, contextKey, options, "model");
|
|
6076
|
+
return;
|
|
5727
6077
|
}
|
|
5728
|
-
this.
|
|
5729
|
-
|
|
5730
|
-
|
|
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
|
+
});
|
|
5731
6087
|
const effortMessage = nextEffort && nextEffort !== currentEffort
|
|
5732
6088
|
? `,思考强度已自动调整为 ${nextEffort}`
|
|
5733
6089
|
: "";
|
|
5734
6090
|
if (options.updateMessageId) {
|
|
5735
|
-
await this.
|
|
6091
|
+
await this.openExecutionSettingsNow(contextKey, "model", {
|
|
5736
6092
|
sessionId: loaded.record.localSessionId,
|
|
5737
6093
|
updateMessageId: options.updateMessageId,
|
|
5738
6094
|
notice: `模型已切换为 ${cardCode(model)}${effortMessage},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
@@ -5908,6 +6264,7 @@ export class ProxySessionController {
|
|
|
5908
6264
|
const created = await this.shellCommandJobs.createJob({ contextKey, sourceMessageId, command, cwd });
|
|
5909
6265
|
let cardMessageId: string | undefined;
|
|
5910
6266
|
const initialState: ShellCommandCardView = {
|
|
6267
|
+
terminalUrl: this.lifecycle?.terminalUrl?.(created.id),
|
|
5911
6268
|
jobId: created.id,
|
|
5912
6269
|
contextKey,
|
|
5913
6270
|
command,
|
|
@@ -5934,7 +6291,7 @@ export class ProxySessionController {
|
|
|
5934
6291
|
await this.outbound.updateInteractiveCard(
|
|
5935
6292
|
contextKey,
|
|
5936
6293
|
cardMessageId,
|
|
5937
|
-
this.cardRenderer.renderShellCommandCard(shellCommandJobCardView(failed)),
|
|
6294
|
+
this.cardRenderer.renderShellCommandCard({ ...shellCommandJobCardView(failed), terminalUrl: this.lifecycle?.terminalUrl?.(failed.id) }),
|
|
5938
6295
|
);
|
|
5939
6296
|
await this.shellCommandJobs.markPresented(created.id);
|
|
5940
6297
|
} catch (cardError) {
|
|
@@ -5992,7 +6349,7 @@ export class ProxySessionController {
|
|
|
5992
6349
|
): void {
|
|
5993
6350
|
if (!job.cardMessageId || this.shellCommandJobMonitors.has(job.id)) return;
|
|
5994
6351
|
const scheduler = new CardUpdateScheduler<ShellCommandCardView>({
|
|
5995
|
-
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
6352
|
+
render: (state) => this.cardRenderer.renderShellCommandCard({ ...state, terminalUrl: this.lifecycle?.terminalUrl?.(job.id) }),
|
|
5996
6353
|
write: (card) => this.outbound.updateInteractiveCard(job.contextKey, job.cardMessageId!, card),
|
|
5997
6354
|
onError: (error) => this.logger.warn(
|
|
5998
6355
|
{ error, jobId: job.id, contextKey: job.contextKey, messageId: job.cardMessageId },
|
|
@@ -6143,16 +6500,26 @@ export class ProxySessionController {
|
|
|
6143
6500
|
}
|
|
6144
6501
|
|
|
6145
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> {
|
|
6146
6508
|
if (!effort) {
|
|
6147
|
-
await this.
|
|
6509
|
+
await this.openExecutionSettingsNow(contextKey, "thinking", options);
|
|
6148
6510
|
return;
|
|
6149
6511
|
}
|
|
6150
6512
|
const record = options.sessionId
|
|
6151
6513
|
? this.requireSession(contextKey, options.sessionId)
|
|
6152
6514
|
: this.requireCurrentSession(contextKey);
|
|
6153
|
-
const loaded = await this.loadSession(record);
|
|
6154
|
-
const
|
|
6155
|
-
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)
|
|
6156
6523
|
?? models.find((item) => item.isDefault);
|
|
6157
6524
|
if (!currentModel) throw new Error("当前运行时没有可配置思考强度的模型。");
|
|
6158
6525
|
if (options.expectedModel && currentModel.id !== options.expectedModel) {
|
|
@@ -6164,11 +6531,17 @@ export class ProxySessionController {
|
|
|
6164
6531
|
const options = supported.map((option) => option.value).join("、") || "无";
|
|
6165
6532
|
throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
|
|
6166
6533
|
}
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
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
|
+
});
|
|
6170
6543
|
if (options.updateMessageId) {
|
|
6171
|
-
await this.
|
|
6544
|
+
await this.openExecutionSettingsNow(contextKey, "thinking", {
|
|
6172
6545
|
sessionId: loaded.record.localSessionId,
|
|
6173
6546
|
updateMessageId: options.updateMessageId,
|
|
6174
6547
|
notice: `思考强度已切换为 ${cardCode(effort)},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
@@ -6181,25 +6554,38 @@ export class ProxySessionController {
|
|
|
6181
6554
|
);
|
|
6182
6555
|
}
|
|
6183
6556
|
|
|
6184
|
-
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(
|
|
6185
6563
|
contextKey: string,
|
|
6186
6564
|
mode?: PermissionMode,
|
|
6187
6565
|
options: ModelCardOptions = {},
|
|
6188
6566
|
): Promise<void> {
|
|
6189
6567
|
if (!mode) {
|
|
6190
|
-
await this.
|
|
6568
|
+
await this.openExecutionSettingsNow(contextKey, "permission", options);
|
|
6191
6569
|
return;
|
|
6192
6570
|
}
|
|
6193
6571
|
const record = options.sessionId
|
|
6194
6572
|
? this.requireSession(contextKey, options.sessionId)
|
|
6195
6573
|
: this.requireCurrentSession(contextKey);
|
|
6196
|
-
const loaded = await this.loadSession(record);
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
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
|
+
});
|
|
6201
6587
|
if (options.updateMessageId) {
|
|
6202
|
-
await this.
|
|
6588
|
+
await this.openExecutionSettingsNow(contextKey, "permission", {
|
|
6203
6589
|
sessionId: loaded.record.localSessionId,
|
|
6204
6590
|
updateMessageId: options.updateMessageId,
|
|
6205
6591
|
notice: mode === "auto"
|
|
@@ -6244,7 +6630,8 @@ export class ProxySessionController {
|
|
|
6244
6630
|
const existing = this.turnDetailHydrations.get(turnId);
|
|
6245
6631
|
if (existing) return existing;
|
|
6246
6632
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
6247
|
-
|
|
6633
|
+
const previewTurnId = snapshot?.previewTurnId ?? turnId;
|
|
6634
|
+
if ((!snapshot || snapshot.previewJournal) && this.store.previews.has(previewTurnId)) return this.store.previews.load(previewTurnId);
|
|
6248
6635
|
if (snapshot && !needsHistoricalDetails(snapshot)) return snapshot;
|
|
6249
6636
|
const origin = !snapshot ? this.store.getTurnRuntimeOrigin(turnId) : undefined;
|
|
6250
6637
|
if (!snapshot && (!origin || !this.runtimes.forAgent(origin.agentName).readRemoteTurn)) return undefined;
|
|
@@ -6270,7 +6657,7 @@ export class ProxySessionController {
|
|
|
6270
6657
|
}
|
|
6271
6658
|
// An Agent Bot turn may have arrived while the read-only request was in flight.
|
|
6272
6659
|
const latest = turnViewSnapshot(this.store.getTurnSnapshot(origin.turnId));
|
|
6273
|
-
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;
|
|
6274
6661
|
const state = hydrateTurnViewState(createTurnViewState(
|
|
6275
6662
|
origin.localSessionId, origin.turnId, details.startedAt ?? 0,
|
|
6276
6663
|
remote?.title, undefined, remote?.cwd, undefined, this.agentLabel(origin.agentName),
|
|
@@ -7245,7 +7632,7 @@ export class ProxySessionController {
|
|
|
7245
7632
|
const remoteReference = local
|
|
7246
7633
|
? remoteSessionReference(local.agentName, local.remoteSessionId!)
|
|
7247
7634
|
: taskId;
|
|
7248
|
-
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference);
|
|
7635
|
+
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference, { view: "metadata" });
|
|
7249
7636
|
if (isRemoteSessionActive(remote)) {
|
|
7250
7637
|
throw new Error("任务正在执行,无法归档。请先停止任务或等待本轮完成。");
|
|
7251
7638
|
}
|
|
@@ -7254,7 +7641,11 @@ export class ProxySessionController {
|
|
|
7254
7641
|
}
|
|
7255
7642
|
|
|
7256
7643
|
const wasCurrent = Boolean(local && this.store.getUserContext(contextKey)?.currentSessionId === local.localSessionId);
|
|
7257
|
-
|
|
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 });
|
|
7258
7649
|
const title = local?.title ?? remote.title ?? remote.preview ?? remote.id;
|
|
7259
7650
|
if (local) {
|
|
7260
7651
|
this.store.archiveSession(local.localSessionId);
|
|
@@ -7407,6 +7798,7 @@ export class ProxySessionController {
|
|
|
7407
7798
|
}
|
|
7408
7799
|
const localActiveTurnId = runtime.getSession(record.localSessionId)?.activeTurnId;
|
|
7409
7800
|
const runtimeOwnsActiveTurn = Boolean(localActiveTurnId)
|
|
7801
|
+
&& (!this.settingsOnlySessions.has(record.localSessionId) || Boolean(activity.activeTurnId))
|
|
7410
7802
|
&& (!activity.activeTurnId || activity.activeTurnId === localActiveTurnId);
|
|
7411
7803
|
const persistedTurnMatches = record.status === "running"
|
|
7412
7804
|
&& record.lastTurnStatus === "running"
|
|
@@ -7808,6 +8200,12 @@ export class ProxySessionController {
|
|
|
7808
8200
|
}
|
|
7809
8201
|
|
|
7810
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> {
|
|
7811
8209
|
const record = this.store.getSession(localSessionId);
|
|
7812
8210
|
if (!record) throw new Error(`找不到任务:${localSessionId}`);
|
|
7813
8211
|
const session = candidate ?? record;
|
|
@@ -8937,6 +9335,13 @@ function executionSettingsTabValue(value: unknown): ExecutionSettingsTab {
|
|
|
8937
9335
|
throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
|
|
8938
9336
|
}
|
|
8939
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
|
+
|
|
8940
9345
|
function resetHistoryPageValue(value: unknown): number {
|
|
8941
9346
|
const page = Number(value);
|
|
8942
9347
|
if (!Number.isSafeInteger(page) || page < 0) throw new Error("历史轮次卡片页码无效,请重新发送 /turns。");
|
|
@@ -9344,10 +9749,18 @@ function defaultResourcePromptFromCounts(imageCount: number, fileCount: number):
|
|
|
9344
9749
|
return imageCount === 1 ? "请查看这张图片" : "请查看这些图片";
|
|
9345
9750
|
}
|
|
9346
9751
|
|
|
9347
|
-
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 {
|
|
9348
9758
|
return !message.mergedForwardMessageId
|
|
9349
|
-
&& message.
|
|
9350
|
-
&&
|
|
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);
|
|
9351
9764
|
}
|
|
9352
9765
|
|
|
9353
9766
|
function deduplicateImageReferences(
|
|
@@ -9424,7 +9837,8 @@ function isRetryableLlmTurnFailure(message: string): boolean {
|
|
|
9424
9837
|
const normalized = message.trim().toLowerCase();
|
|
9425
9838
|
if (!normalized) return false;
|
|
9426
9839
|
const permanentFailure = [
|
|
9427
|
-
|
|
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,
|
|
9428
9842
|
/insufficient[_ -]?quota|quota (?:exceeded|exhausted)|usage limit|billing|credit balance/u,
|
|
9429
9843
|
/context (?:length|window)|maximum context|too many tokens|token limit/u,
|
|
9430
9844
|
/invalid request|bad request|invalid argument|malformed|unsupported|not supported/u,
|