@keyou007/agent-bot 0.1.24-alpha.4 → 0.1.24-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +42 -6
- package/README.zh.md +42 -6
- package/SECURITY.md +12 -0
- package/config.example.yaml +5 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/serverStatus.js +1 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex/AppServerPermissions.js +34 -0
- package/dist/codex/AppServerPermissions.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +37 -5
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexRuntime.js +238 -127
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +12 -13
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/codex/ThreadWriterProcess.js +19 -5
- package/dist/codex/ThreadWriterProcess.js.map +1 -1
- package/dist/config/AgentExecutionDefaultsStore.js +4 -0
- package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +3 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +146 -72
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +1 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +121 -17
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +43 -6
- package/dist/index.js.map +1 -1
- package/dist/local-files/CsvPreview.js +148 -0
- package/dist/local-files/CsvPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +104 -14
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +128 -12
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +80 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +767 -260
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/proxy/SessionOperationQueue.js +14 -0
- package/dist/proxy/SessionOperationQueue.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +4 -2
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/state/StateStore.js +22 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +19 -7
- package/dist/state/TurnPreviewJournal.js.map +1 -1
- package/dist/state/migrations.js +6 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +1 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +19 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/terminal/TerminalCards.js +13 -0
- package/dist/terminal/TerminalCards.js.map +1 -0
- package/dist/terminal/TerminalLinks.js +76 -0
- package/dist/terminal/TerminalLinks.js.map +1 -0
- package/dist/terminal/TerminalPage.js +212 -0
- package/dist/terminal/TerminalPage.js.map +1 -0
- package/dist/terminal/TerminalProcess.js +71 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +639 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +53 -3
- package/docs/technical-reference.zh.md +52 -2
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +15 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/AppServerPermissions.ts +39 -0
- package/src/codex/CodexLocalActivityDetector.ts +34 -4
- package/src/codex/CodexRuntime.ts +243 -139
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/codex/ThreadWriterProcess.ts +17 -9
- package/src/config/AgentExecutionDefaultsStore.ts +2 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +5 -1
- package/src/feishu/CardRenderer.ts +159 -72
- package/src/feishu/FeishuConnector.ts +1 -0
- package/src/feishu/FeishuTurnPresenter.ts +108 -16
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +44 -7
- package/src/local-files/CsvPreview.ts +132 -0
- package/src/local-files/LocalFileViewerServer.ts +102 -15
- package/src/local-files/TurnPreviewPage.ts +131 -13
- package/src/presentation/TurnStateReducer.ts +87 -7
- package/src/presentation/turnViewTypes.ts +9 -0
- package/src/proxy/ProxySessionController.ts +762 -259
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +10 -3
- package/src/state/StateStore.ts +38 -1
- package/src/state/TurnPreviewJournal.ts +19 -7
- package/src/state/migrations.ts +6 -0
- package/src/supervision/SafeRestartNotifier.ts +1 -0
- package/src/supervision/SafeRestartScheduler.ts +18 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +71 -0
- package/src/terminal/TerminalServer.ts +513 -0
|
@@ -4,7 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
6
6
|
import { CodexVersionError } from "../codex/CodexVersion.js";
|
|
7
|
-
import { assertProviderSettingsApplied,
|
|
7
|
+
import { assertProviderSettingsApplied, ProviderSwitchError } from "../codex/ProviderSwitch.js";
|
|
8
8
|
import { SystemThreadWriterProcessController, } from "../codex/ThreadWriterProcess.js";
|
|
9
9
|
import { resolveUserPath } from "../config/paths.js";
|
|
10
10
|
import { DEFAULT_GROUP_NAME_FORMAT, } from "../config/schema.js";
|
|
@@ -22,6 +22,7 @@ import { errorLogValue } from "../logging/errorLogValue.js";
|
|
|
22
22
|
import { createTurnViewState, hydrateTurnViewState } from "../presentation/TurnStateReducer.js";
|
|
23
23
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
24
24
|
import { isActiveShellCommandJob, ShellCommandJobManager, } from "../shell/ShellCommandJobManager.js";
|
|
25
|
+
import { SessionOperationQueue } from "./SessionOperationQueue.js";
|
|
25
26
|
import { createId } from "../utils/id.js";
|
|
26
27
|
import { formatStorageSize } from "../utils/formatStorageSize.js";
|
|
27
28
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
@@ -48,11 +49,12 @@ const REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS = 30_000;
|
|
|
48
49
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
49
50
|
const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
|
|
50
51
|
const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
|
|
52
|
+
const RECOVERY_RETRY_WINDOW_MS = 60_000;
|
|
51
53
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
52
54
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
53
55
|
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
54
56
|
const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
|
|
55
|
-
const FORWARD_ATTACHMENT_WINDOW_MS =
|
|
57
|
+
const FORWARD_ATTACHMENT_WINDOW_MS = 1_500;
|
|
56
58
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
57
59
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
58
60
|
const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
|
|
@@ -226,9 +228,14 @@ export class ProxySessionController {
|
|
|
226
228
|
messageQueues = new Map();
|
|
227
229
|
switchGroupOperations = new Set();
|
|
228
230
|
sessionLoads = new Map();
|
|
231
|
+
settingsOnlySessions = new Set();
|
|
229
232
|
queuedPromptStarts = new Map();
|
|
230
233
|
queuedPromptCards = new Map();
|
|
231
234
|
queuedPromptCardWrites = new Map();
|
|
235
|
+
settingsOperations = new SessionOperationQueue();
|
|
236
|
+
// Loads may run inside settingsOperations; never acquire that queue while hydrating.
|
|
237
|
+
sessionHydrationOperations = new SessionOperationQueue();
|
|
238
|
+
agentDefaultsOperations = new SessionOperationQueue();
|
|
232
239
|
sessionResets = new Map();
|
|
233
240
|
resetHistoryOperations = new Map();
|
|
234
241
|
remoteTurnHistoryRefreshes = new Map();
|
|
@@ -238,9 +245,13 @@ export class ProxySessionController {
|
|
|
238
245
|
lastSessionListings = new Map();
|
|
239
246
|
threadInitializations = new Map();
|
|
240
247
|
llmRetryingSessions = new Set();
|
|
248
|
+
planHandoffs = new Map();
|
|
241
249
|
retriedFailureTurnIds = new Set();
|
|
242
250
|
queuedPromptRetryTimers = new Map();
|
|
243
251
|
recoveryRetryTimers = new Map();
|
|
252
|
+
recoveryRetryStartedAt = new Map();
|
|
253
|
+
recoveredLiveAttempts = new Set();
|
|
254
|
+
recoveryClosed = false;
|
|
244
255
|
pendingMergedForwards = new Map();
|
|
245
256
|
pendingResourceForwards = new Map();
|
|
246
257
|
relatedReactionMessageIds = new Map();
|
|
@@ -300,7 +311,7 @@ export class ProxySessionController {
|
|
|
300
311
|
const pendingMergedForward = message.mergedForwardMessageId
|
|
301
312
|
? this.registerPendingForwardAttachment(message, this.pendingMergedForwards)
|
|
302
313
|
: undefined;
|
|
303
|
-
const pendingResourceForward = !attachmentReservation &&
|
|
314
|
+
const pendingResourceForward = !attachmentReservation && isForwardAttachmentSource(message)
|
|
304
315
|
? this.registerPendingForwardAttachment(message, this.pendingResourceForwards)
|
|
305
316
|
: undefined;
|
|
306
317
|
try {
|
|
@@ -338,7 +349,9 @@ export class ProxySessionController {
|
|
|
338
349
|
const attachment = await this.waitForForwardAttachment(message.messageId, pendingResourceForward, this.pendingResourceForwards);
|
|
339
350
|
if (attachment) {
|
|
340
351
|
coalescedForwardAttachment = true;
|
|
341
|
-
const
|
|
352
|
+
const sourceText = resourceMessage.text.trim();
|
|
353
|
+
const instruction = normalizeFeishuPostText(attachment.text)
|
|
354
|
+
|| (sourceText ? DEFAULT_REFERENCED_MESSAGE_INSTRUCTION : defaultResourcePrompt(resourceMessage));
|
|
342
355
|
forwardedImages = (resourceMessage.images ?? []).map((image) => ({
|
|
343
356
|
messageId: resourceMessage.messageId,
|
|
344
357
|
imageKey: image.imageKey,
|
|
@@ -350,7 +363,7 @@ export class ProxySessionController {
|
|
|
350
363
|
}));
|
|
351
364
|
message = {
|
|
352
365
|
...attachment,
|
|
353
|
-
text: instruction,
|
|
366
|
+
text: sourceText ? referencedMessagePrompt(instruction, sourceText) : instruction,
|
|
354
367
|
displayText: instruction,
|
|
355
368
|
};
|
|
356
369
|
this.relatedReactionMessageIds.set(message.messageId, [resourceMessage.messageId]);
|
|
@@ -619,7 +632,12 @@ export class ProxySessionController {
|
|
|
619
632
|
await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
|
|
620
633
|
try {
|
|
621
634
|
const kind = String(scopedAction.value.action ?? "");
|
|
622
|
-
if (kind === "
|
|
635
|
+
if (kind === "terminal_authorize") {
|
|
636
|
+
if (!this.lifecycle?.authorizeTerminal)
|
|
637
|
+
throw new Error("网页终端未启用。");
|
|
638
|
+
await this.outbound.sendText(contextKey, await this.lifecycle.authorizeTerminal(scopedAction));
|
|
639
|
+
}
|
|
640
|
+
else if (kind === "agentbot_update_cancel") {
|
|
623
641
|
if (!this.lifecycle?.cancelAutomaticUpdate)
|
|
624
642
|
throw new Error("自动更新功能未启用。");
|
|
625
643
|
await this.lifecycle.cancelAutomaticUpdate(scopedAction);
|
|
@@ -812,7 +830,7 @@ export class ProxySessionController {
|
|
|
812
830
|
if (scopedAction.value.cardView === "status")
|
|
813
831
|
await this.refreshStatusCardFromAction(scopedAction, sessionId);
|
|
814
832
|
else if (scopedAction.value.cardView !== "safe_restart") {
|
|
815
|
-
await this.refreshSessionsCardFromAction(scopedAction
|
|
833
|
+
await this.refreshSessionsCardFromAction(scopedAction);
|
|
816
834
|
}
|
|
817
835
|
}
|
|
818
836
|
else if (kind === "session_turns" || kind === "session_turns_page") {
|
|
@@ -838,9 +856,11 @@ export class ProxySessionController {
|
|
|
838
856
|
await this.refreshStatusCardFromAction(scopedAction);
|
|
839
857
|
}
|
|
840
858
|
else if (kind === "settings_tab_open") {
|
|
859
|
+
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
841
860
|
await this.openExecutionSettings(contextKey, executionSettingsTabValue(scopedAction.value.tab), {
|
|
842
|
-
sessionId:
|
|
843
|
-
|
|
861
|
+
sessionId, updateMessageId: scopedAction.messageId,
|
|
862
|
+
modelPage: modelCardPageValue(scopedAction.value.page),
|
|
863
|
+
expectedProvider: typeof scopedAction.value.provider === "string" ? scopedAction.value.provider : undefined,
|
|
844
864
|
});
|
|
845
865
|
}
|
|
846
866
|
else if (kind === "settings_agent_select") {
|
|
@@ -857,12 +877,14 @@ export class ProxySessionController {
|
|
|
857
877
|
}
|
|
858
878
|
else if (kind === "settings_model_select") {
|
|
859
879
|
await this.model(contextKey, String(scopedAction.value.model ?? ""), {
|
|
880
|
+
expectedProvider: String(scopedAction.value.provider ?? ""),
|
|
860
881
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
861
882
|
updateMessageId: scopedAction.messageId,
|
|
862
883
|
});
|
|
863
884
|
}
|
|
864
885
|
else if (kind === "settings_thinking_select") {
|
|
865
886
|
await this.thinking(contextKey, String(scopedAction.value.effort ?? ""), {
|
|
887
|
+
expectedProvider: String(scopedAction.value.provider ?? ""),
|
|
866
888
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
867
889
|
updateMessageId: scopedAction.messageId,
|
|
868
890
|
expectedModel: String(scopedAction.value.model ?? ""),
|
|
@@ -887,6 +909,7 @@ export class ProxySessionController {
|
|
|
887
909
|
await this.model(contextKey, undefined, {
|
|
888
910
|
sessionId,
|
|
889
911
|
updateMessageId: scopedAction.messageId,
|
|
912
|
+
modelPage: modelCardPageValue(scopedAction.value.page),
|
|
890
913
|
});
|
|
891
914
|
}
|
|
892
915
|
else if (kind === "reasoning_select") {
|
|
@@ -906,7 +929,7 @@ export class ProxySessionController {
|
|
|
906
929
|
});
|
|
907
930
|
}
|
|
908
931
|
else if (kind === "provider_select" || kind === "provider_model_open") {
|
|
909
|
-
await this.openProviderModelSelector(contextKey, String(scopedAction.value.provider ?? ""), permissionModeValue(scopedAction.value.permissionMode), String(scopedAction.value.sessionId ?? ""), requiredCardMessageId(scopedAction.messageId));
|
|
932
|
+
await this.openProviderModelSelector(contextKey, String(scopedAction.value.provider ?? ""), permissionModeValue(scopedAction.value.permissionMode), String(scopedAction.value.sessionId ?? ""), requiredCardMessageId(scopedAction.messageId), modelCardPageValue(scopedAction.value.page));
|
|
910
933
|
}
|
|
911
934
|
else if (kind === "provider_model_select" || kind === "provider_reasoning_open") {
|
|
912
935
|
await this.openProviderReasoningSelector(contextKey, String(scopedAction.value.provider ?? ""), String(scopedAction.value.model ?? ""), permissionModeValue(scopedAction.value.permissionMode), String(scopedAction.value.sessionId ?? ""), requiredCardMessageId(scopedAction.messageId));
|
|
@@ -953,6 +976,7 @@ export class ProxySessionController {
|
|
|
953
976
|
}
|
|
954
977
|
}
|
|
955
978
|
close() {
|
|
979
|
+
this.recoveryClosed = true;
|
|
956
980
|
for (const unsubscribe of this.unsubscribe)
|
|
957
981
|
unsubscribe();
|
|
958
982
|
this.unsubscribe.length = 0;
|
|
@@ -960,6 +984,7 @@ export class ProxySessionController {
|
|
|
960
984
|
this.queuedPromptCards.clear();
|
|
961
985
|
this.queuedPromptCardWrites.clear();
|
|
962
986
|
this.sessionResets.clear();
|
|
987
|
+
this.planHandoffs.clear();
|
|
963
988
|
this.resetHistoryOperations.clear();
|
|
964
989
|
this.remoteTurnHistoryRefreshes.clear();
|
|
965
990
|
this.forkHistoryCursors.clear();
|
|
@@ -977,6 +1002,8 @@ export class ProxySessionController {
|
|
|
977
1002
|
for (const timer of this.recoveryRetryTimers.values())
|
|
978
1003
|
clearTimeout(timer);
|
|
979
1004
|
this.recoveryRetryTimers.clear();
|
|
1005
|
+
this.recoveryRetryStartedAt.clear();
|
|
1006
|
+
this.recoveredLiveAttempts.clear();
|
|
980
1007
|
for (const schedule of this.appServerReleaseSchedules.values()) {
|
|
981
1008
|
if (schedule.timer)
|
|
982
1009
|
clearTimeout(schedule.timer);
|
|
@@ -1263,78 +1290,43 @@ export class ProxySessionController {
|
|
|
1263
1290
|
if (setting && !value?.trim())
|
|
1264
1291
|
throw new Error(`task ${setting} requires a value.`);
|
|
1265
1292
|
const nextValue = value?.trim();
|
|
1266
|
-
if (setting
|
|
1293
|
+
if (setting && nextValue) {
|
|
1267
1294
|
const contextKey = this.controlSessionContextKey(record);
|
|
1268
|
-
await this.outbound.withReplyTarget(contextKey, this.controlSessionReplyTarget(record), () =>
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
reasoningEffort: effort,
|
|
1282
|
-
permissionMode: loaded.session.permissionMode,
|
|
1295
|
+
await this.outbound.withReplyTarget(contextKey, this.controlSessionReplyTarget(record), async () => {
|
|
1296
|
+
const options = { sessionId: record.localSessionId };
|
|
1297
|
+
if (setting === "provider")
|
|
1298
|
+
await this.selectProvider(contextKey, nextValue, options);
|
|
1299
|
+
else if (setting === "model")
|
|
1300
|
+
await this.model(contextKey, nextValue, options);
|
|
1301
|
+
else if (setting === "thinking")
|
|
1302
|
+
await this.thinking(contextKey, nextValue, options);
|
|
1303
|
+
else {
|
|
1304
|
+
if (nextValue !== "auto" && nextValue !== "confirm")
|
|
1305
|
+
throw new Error("task permissions accepts auto or confirm.");
|
|
1306
|
+
await this.permissions(contextKey, nextValue, options);
|
|
1307
|
+
}
|
|
1283
1308
|
});
|
|
1284
1309
|
}
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
const
|
|
1291
|
-
const
|
|
1292
|
-
|| selected.supportedReasoningEfforts.some((candidate) => candidate.value === currentEffort))
|
|
1293
|
-
? currentEffort
|
|
1294
|
-
: selected.defaultReasoningEffort;
|
|
1295
|
-
await loaded.runtime.setModel(record.localSessionId, nextValue);
|
|
1296
|
-
if (nextEffort && nextEffort !== currentEffort) {
|
|
1297
|
-
await loaded.runtime.setReasoningEffort(record.localSessionId, nextEffort);
|
|
1298
|
-
}
|
|
1299
|
-
this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
|
|
1300
|
-
this.outbound.updateSessionModel(record.localSessionId, nextValue, loaded.session.modelProvider);
|
|
1301
|
-
}
|
|
1302
|
-
else if (setting === "thinking" && nextValue) {
|
|
1303
|
-
const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
|
|
1304
|
-
const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
|
|
1310
|
+
return this.settingsOperations.run(localSessionId, async () => {
|
|
1311
|
+
const loaded = await this.loadSession(this.store.getSession(localSessionId) ?? record, true);
|
|
1312
|
+
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1313
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
1314
|
+
const selected = pending?.settings ?? session;
|
|
1315
|
+
const models = await loaded.runtime.listModels(selected.modelProvider, selected.model);
|
|
1316
|
+
const currentModel = models.find((candidate) => candidate.id === selected.model)
|
|
1305
1317
|
?? models.find((candidate) => candidate.isDefault);
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
}
|
|
1319
|
-
await loaded.runtime.setPermissionMode(record.localSessionId, nextValue);
|
|
1320
|
-
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
|
|
1321
|
-
}
|
|
1322
|
-
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1323
|
-
if (setting && setting !== "provider")
|
|
1324
|
-
await this.persistAgentExecutionDefaults(session.localSessionId);
|
|
1325
|
-
const models = await loaded.runtime.listModels(session.modelProvider, session.model);
|
|
1326
|
-
const currentModel = models.find((candidate) => candidate.id === session.model)
|
|
1327
|
-
?? models.find((candidate) => candidate.isDefault);
|
|
1328
|
-
const providers = loaded.runtime.listModelProviders
|
|
1329
|
-
? await this.modelProviderOptions(loaded)
|
|
1330
|
-
: [];
|
|
1331
|
-
return {
|
|
1332
|
-
session,
|
|
1333
|
-
providers,
|
|
1334
|
-
models,
|
|
1335
|
-
reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
|
|
1336
|
-
permissionModes: ["auto", "confirm"],
|
|
1337
|
-
};
|
|
1318
|
+
const providers = loaded.runtime.listModelProviders
|
|
1319
|
+
? await this.modelProviderOptions(loaded)
|
|
1320
|
+
: [];
|
|
1321
|
+
return {
|
|
1322
|
+
session,
|
|
1323
|
+
pendingProvider: pending,
|
|
1324
|
+
providers,
|
|
1325
|
+
models,
|
|
1326
|
+
reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
|
|
1327
|
+
permissionModes: ["auto", "confirm"],
|
|
1328
|
+
};
|
|
1329
|
+
});
|
|
1338
1330
|
}
|
|
1339
1331
|
async controlTaskGoal(localSessionId, action, objective) {
|
|
1340
1332
|
const record = this.requireControlSession(localSessionId);
|
|
@@ -1727,14 +1719,21 @@ export class ProxySessionController {
|
|
|
1727
1719
|
}
|
|
1728
1720
|
if (!this.outbound.canRoute(attempt.contextKey))
|
|
1729
1721
|
continue;
|
|
1722
|
+
this.recoveryRetryStartedAt.set(attempt.attemptId, Date.now());
|
|
1730
1723
|
try {
|
|
1731
1724
|
await this.recoverTurnAttempt(attempt);
|
|
1732
1725
|
}
|
|
1733
1726
|
catch (error) {
|
|
1734
|
-
this.
|
|
1735
|
-
|
|
1727
|
+
await this.handleRecoveryError(attempt.attemptId, error);
|
|
1728
|
+
}
|
|
1729
|
+
finally {
|
|
1730
|
+
if (!this.recoveryRetryTimers.has(attempt.attemptId))
|
|
1731
|
+
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
1736
1732
|
}
|
|
1737
1733
|
}
|
|
1734
|
+
for (const pending of this.store.listPendingProviderSettings()) {
|
|
1735
|
+
await this.applyPendingProviderWhenIdle(pending.localSessionId);
|
|
1736
|
+
}
|
|
1738
1737
|
this.restorePersistedQueuedPrompts();
|
|
1739
1738
|
}
|
|
1740
1739
|
async finishPersistedCancellationRequests() {
|
|
@@ -1815,22 +1814,36 @@ export class ProxySessionController {
|
|
|
1815
1814
|
});
|
|
1816
1815
|
}
|
|
1817
1816
|
}
|
|
1818
|
-
async expireStaleTurnAttempt(attempt) {
|
|
1819
|
-
|
|
1817
|
+
async expireStaleTurnAttempt(attempt, message = "执行中断已超过 10 分钟,未自动恢复。") {
|
|
1818
|
+
clearTimeout(this.recoveryRetryTimers.get(attempt.attemptId));
|
|
1819
|
+
this.recoveryRetryTimers.delete(attempt.attemptId);
|
|
1820
|
+
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
1821
|
+
this.recoveredLiveAttempts.delete(attempt.attemptId);
|
|
1822
|
+
let session = this.store.getSession(attempt.localSessionId);
|
|
1823
|
+
this.store.updateTurnAttempt(attempt.attemptId, { status: "interrupted" });
|
|
1820
1824
|
const oldCardTurnId = attempt.pendingTurnId ?? attempt.turnId;
|
|
1821
1825
|
if (session && oldCardTurnId && this.outbound.canRoute(attempt.contextKey)) {
|
|
1822
|
-
await this.outbound.interruptTurnForRecovery(session.localSessionId, attempt.contextKey, oldCardTurnId,
|
|
1826
|
+
await this.outbound.interruptTurnForRecovery(session.localSessionId, attempt.contextKey, oldCardTurnId, message).catch((error) => {
|
|
1823
1827
|
this.logger.warn({ error, attemptId: attempt.attemptId, turnId: oldCardTurnId }, "Failed to close an expired interrupted thinking card.");
|
|
1824
1828
|
});
|
|
1825
1829
|
}
|
|
1826
|
-
this.store.
|
|
1830
|
+
session = this.store.getSession(attempt.localSessionId);
|
|
1827
1831
|
if (!session || session.status === "closed")
|
|
1828
1832
|
return;
|
|
1829
|
-
|
|
1833
|
+
const runtime = this.runtimes.forAgent(session.agentName);
|
|
1834
|
+
const activeTurnId = runtime.getSession(session.localSessionId)?.activeTurnId;
|
|
1835
|
+
const sameTurn = session.lastTurnId === attempt.turnId
|
|
1836
|
+
|| session.lastTurnId === attempt.recoveredFromTurnId
|
|
1837
|
+
|| !session.lastTurnId;
|
|
1838
|
+
if (sameTurn && session.lastTurnStatus !== "completed" && session.lastTurnStatus !== "failed"
|
|
1839
|
+
&& (!activeTurnId || activeTurnId === attempt.turnId)) {
|
|
1840
|
+
// Forget monitoring, not the remote task. Never cancel an unknown writer.
|
|
1841
|
+
if (activeTurnId)
|
|
1842
|
+
runtime.forgetSession?.(session.localSessionId);
|
|
1830
1843
|
this.store.updateRuntimeSession(session.localSessionId, { lastTurnStatus: "cancelled" });
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1844
|
+
if (session.status === "starting" || session.status === "running") {
|
|
1845
|
+
this.store.updateSession(session.localSessionId, { status: "ready" });
|
|
1846
|
+
}
|
|
1834
1847
|
}
|
|
1835
1848
|
if (attempt.turnId && this.outbound.canRoute(attempt.contextKey)) {
|
|
1836
1849
|
await this.finalizeTurnMessageReactions(attempt.turnId, "cancelled").catch((error) => {
|
|
@@ -1838,6 +1851,47 @@ export class ProxySessionController {
|
|
|
1838
1851
|
});
|
|
1839
1852
|
}
|
|
1840
1853
|
}
|
|
1854
|
+
async handleRecoveryError(attemptId, error, activeTurnId) {
|
|
1855
|
+
if (this.recoveryClosed)
|
|
1856
|
+
return;
|
|
1857
|
+
const attempt = this.store.getTurnAttempt(attemptId);
|
|
1858
|
+
if (!attempt || !isIncompleteTurnAttemptStatus(attempt.status))
|
|
1859
|
+
return;
|
|
1860
|
+
this.logger.warn({ error, attemptId, sessionId: attempt.localSessionId }, "Task recovery failed.");
|
|
1861
|
+
const session = this.store.getSession(attempt.localSessionId);
|
|
1862
|
+
const writerConflict = session?.remoteSessionId === activeWriterThreadId(error) && Boolean(session?.remoteSessionId);
|
|
1863
|
+
const startedAt = this.recoveryRetryStartedAt.get(attemptId) ?? Date.now();
|
|
1864
|
+
this.recoveryRetryStartedAt.set(attemptId, startedAt);
|
|
1865
|
+
const localActiveTurnId = session ? this.runtimes.forAgent(session.agentName).getSession(session.localSessionId)?.activeTurnId : undefined;
|
|
1866
|
+
const monitoredTurnId = activeTurnId ?? (localActiveTurnId === attempt.turnId ? localActiveTurnId : undefined);
|
|
1867
|
+
// Once a live turn was confirmed, a monitoring outage must not make safe restart
|
|
1868
|
+
// consider it idle. Keep reconciling without cancelling or replaying the prompt.
|
|
1869
|
+
if (monitoredTurnId && this.recoveredLiveAttempts.has(attemptId) && !writerConflict) {
|
|
1870
|
+
this.scheduleRecoveryRetry(attemptId, monitoredTurnId);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
if (writerConflict || error instanceof CodexVersionError || Date.now() - startedAt >= RECOVERY_RETRY_WINDOW_MS) {
|
|
1874
|
+
const message = writerConflict
|
|
1875
|
+
? "自动恢复未完成:任务仍被另一个 App Server 占用。已停止自动重试,请确认占用进程后释放任务再继续。"
|
|
1876
|
+
: `自动恢复未完成,已停止自动重试。请检查 Agent 状态后重新发送消息。\n${error instanceof Error ? error.message : String(error)}`;
|
|
1877
|
+
await this.expireStaleTurnAttempt(attempt, message);
|
|
1878
|
+
try {
|
|
1879
|
+
const replyTarget = attempt.replyMessageId
|
|
1880
|
+
? { messageId: attempt.replyMessageId, replyInThread: true } : undefined;
|
|
1881
|
+
await this.outbound.withReplyTarget(attempt.contextKey, replyTarget, async () => {
|
|
1882
|
+
await this.outbound.sendText(attempt.contextKey, message);
|
|
1883
|
+
if (writerConflict && session)
|
|
1884
|
+
await this.presentThreadWriterConflict(session, attempt.contextKey, error, false);
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
catch (notificationError) {
|
|
1888
|
+
this.logger.warn({ error: notificationError, attemptId }, "Failed to notify about stopped task recovery.");
|
|
1889
|
+
}
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
this.store.updateTurnAttempt(attemptId, { status: "recovering" });
|
|
1893
|
+
this.scheduleRecoveryRetry(attemptId, monitoredTurnId);
|
|
1894
|
+
}
|
|
1841
1895
|
persistActiveTurnHeartbeats() {
|
|
1842
1896
|
for (const attempt of this.store.listIncompleteTurnAttempts()) {
|
|
1843
1897
|
if (!attempt.turnId)
|
|
@@ -1851,6 +1905,8 @@ export class ProxySessionController {
|
|
|
1851
1905
|
}
|
|
1852
1906
|
}
|
|
1853
1907
|
async recoverTurnAttempt(attempt, announce = true) {
|
|
1908
|
+
if (this.recoveryClosed)
|
|
1909
|
+
return;
|
|
1854
1910
|
const session = this.store.getSession(attempt.localSessionId);
|
|
1855
1911
|
if (!session || session.status === "closed") {
|
|
1856
1912
|
this.store.updateTurnAttempt(attempt.attemptId, { status: "interrupted" });
|
|
@@ -1879,9 +1935,9 @@ export class ProxySessionController {
|
|
|
1879
1935
|
remote = await runtime.readRemoteSession(session.remoteSessionId);
|
|
1880
1936
|
}
|
|
1881
1937
|
catch (error) {
|
|
1882
|
-
|
|
1938
|
+
// A failed activity check is not proof of interruption. Do not replay a prompt blindly.
|
|
1939
|
+
if (sourceTurnId || !isUnmaterializedCodexThreadError(error))
|
|
1883
1940
|
throw error;
|
|
1884
|
-
this.logger.warn({ error, attemptId: attempt.attemptId, remoteSessionId: session.remoteSessionId }, "Failed to read the remote task during startup recovery; treating the old local execution as interrupted.");
|
|
1885
1941
|
}
|
|
1886
1942
|
}
|
|
1887
1943
|
const remoteTurnBelongsToAttempt = remoteTurnMatchesAttempt(remote, attempt);
|
|
@@ -1927,14 +1983,22 @@ export class ProxySessionController {
|
|
|
1927
1983
|
this.scheduleRecoveryRetry(attempt.attemptId, synchronized.activeTurnId);
|
|
1928
1984
|
}
|
|
1929
1985
|
scheduleRecoveryRetry(attemptId, activeTurnId) {
|
|
1930
|
-
if (this.recoveryRetryTimers.has(attemptId))
|
|
1986
|
+
if (this.recoveryClosed || this.recoveryRetryTimers.has(attemptId))
|
|
1931
1987
|
return;
|
|
1988
|
+
if (!this.recoveryRetryStartedAt.has(attemptId))
|
|
1989
|
+
this.recoveryRetryStartedAt.set(attemptId, Date.now());
|
|
1932
1990
|
const timer = setTimeout(() => {
|
|
1933
1991
|
this.recoveryRetryTimers.delete(attemptId);
|
|
1934
1992
|
const attempt = this.store.getTurnAttempt(attemptId);
|
|
1935
|
-
if (!attempt || !isIncompleteTurnAttemptStatus(attempt.status))
|
|
1993
|
+
if (!attempt || !isIncompleteTurnAttemptStatus(attempt.status)) {
|
|
1994
|
+
this.recoveryRetryStartedAt.delete(attemptId);
|
|
1995
|
+
this.recoveredLiveAttempts.delete(attemptId);
|
|
1936
1996
|
return;
|
|
1997
|
+
}
|
|
1937
1998
|
const retry = async () => {
|
|
1999
|
+
if (!activeTurnId && Date.now() - this.recoveryRetryStartedAt.get(attemptId) >= RECOVERY_RETRY_WINDOW_MS) {
|
|
2000
|
+
throw new Error("等待恢复超过 1 分钟,尚未确认执行已恢复。");
|
|
2001
|
+
}
|
|
1938
2002
|
let recoveryAttempt = attempt;
|
|
1939
2003
|
if (activeTurnId) {
|
|
1940
2004
|
const session = this.store.getSession(attempt.localSessionId);
|
|
@@ -1942,6 +2006,8 @@ export class ProxySessionController {
|
|
|
1942
2006
|
const runtime = this.runtimes.forAgent(session.agentName);
|
|
1943
2007
|
const synchronized = await runtime.synchronizeSession(session.localSessionId);
|
|
1944
2008
|
if (synchronized.activeTurnId === activeTurnId) {
|
|
2009
|
+
this.recoveredLiveAttempts.add(attemptId);
|
|
2010
|
+
this.recoveryRetryStartedAt.delete(attemptId);
|
|
1945
2011
|
this.scheduleRecoveryRetry(attemptId, activeTurnId);
|
|
1946
2012
|
return;
|
|
1947
2013
|
}
|
|
@@ -1952,16 +2018,38 @@ export class ProxySessionController {
|
|
|
1952
2018
|
}
|
|
1953
2019
|
}
|
|
1954
2020
|
await this.recoverTurnAttempt(recoveryAttempt, false);
|
|
2021
|
+
if (!this.recoveryRetryTimers.has(attemptId))
|
|
2022
|
+
this.recoveryRetryStartedAt.delete(attemptId);
|
|
1955
2023
|
};
|
|
1956
2024
|
void retry().catch((error) => {
|
|
1957
|
-
this.
|
|
1958
|
-
|
|
2025
|
+
return this.handleRecoveryError(attemptId, error, activeTurnId);
|
|
2026
|
+
}).catch((error) => {
|
|
2027
|
+
this.logger.error({ error, attemptId }, "Failed to finalize task recovery.");
|
|
1959
2028
|
});
|
|
1960
2029
|
}, 5_000);
|
|
1961
2030
|
timer.unref?.();
|
|
1962
2031
|
this.recoveryRetryTimers.set(attemptId, timer);
|
|
1963
2032
|
}
|
|
1964
2033
|
async reattachActiveRecoveredTurn(attempt, session, turnId, replyTarget) {
|
|
2034
|
+
// Confirm resume before replacing the old card or announcing a running turn.
|
|
2035
|
+
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
2036
|
+
const loaded = await this.loadSession({
|
|
2037
|
+
...current, contextKey: attempt.contextKey, status: "running", lastTurnId: turnId, lastTurnStatus: "running",
|
|
2038
|
+
});
|
|
2039
|
+
const pendingAttempt = this.store.getTurnAttempt(attempt.attemptId);
|
|
2040
|
+
if (!pendingAttempt || !isIncompleteTurnAttemptStatus(pendingAttempt.status))
|
|
2041
|
+
return;
|
|
2042
|
+
const continued = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
2043
|
+
if (continued?.continuedFromTurnId && this.store.getTurnDelivery(turnId)?.progressMessageId) {
|
|
2044
|
+
this.store.updateTurnAttempt(attempt.attemptId, { pendingTurnId: null, turnId, status: "running" });
|
|
2045
|
+
await this.outbound.onEvent({ type: "turn_started", sessionId: session.localSessionId,
|
|
2046
|
+
turnId, startedAt: continued.startedAt });
|
|
2047
|
+
const synchronized = await loaded.runtime.synchronizeSession(session.localSessionId);
|
|
2048
|
+
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
2049
|
+
if (synchronized.activeTurnId)
|
|
2050
|
+
this.recoveredLiveAttempts.add(attempt.attemptId);
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
1965
2053
|
const oldCardTurnId = attempt.pendingTurnId ?? attempt.turnId;
|
|
1966
2054
|
if (oldCardTurnId) {
|
|
1967
2055
|
await this.outbound.interruptTurnForRecovery(session.localSessionId, attempt.contextKey, oldCardTurnId, "Agent Bot 已重启,执行仍在继续;进度已转移到新的思考卡片。");
|
|
@@ -1979,17 +2067,12 @@ export class ProxySessionController {
|
|
|
1979
2067
|
startedAt: Date.now(),
|
|
1980
2068
|
});
|
|
1981
2069
|
this.store.updateTurnAttempt(attempt.attemptId, { pendingTurnId: null });
|
|
1982
|
-
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
1983
|
-
const loaded = await this.loadSession({
|
|
1984
|
-
...current,
|
|
1985
|
-
contextKey: attempt.contextKey,
|
|
1986
|
-
status: "running",
|
|
1987
|
-
lastTurnId: turnId,
|
|
1988
|
-
lastTurnStatus: "running",
|
|
1989
|
-
});
|
|
1990
2070
|
const synchronized = await loaded.runtime.synchronizeSession(session.localSessionId);
|
|
1991
|
-
|
|
2071
|
+
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
2072
|
+
if (synchronized.activeTurnId) {
|
|
2073
|
+
this.recoveredLiveAttempts.add(attempt.attemptId);
|
|
1992
2074
|
this.scheduleRecoveryRetry(attempt.attemptId, synchronized.activeTurnId);
|
|
2075
|
+
}
|
|
1993
2076
|
}
|
|
1994
2077
|
async continueInterruptedAttempt(attempt, session, sourceTurnId, replyTarget) {
|
|
1995
2078
|
const oldCardTurnId = attempt.pendingTurnId ?? sourceTurnId;
|
|
@@ -2007,7 +2090,7 @@ export class ProxySessionController {
|
|
|
2007
2090
|
});
|
|
2008
2091
|
}
|
|
2009
2092
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
2010
|
-
const loaded = await this.loadSession({ ...current, contextKey: attempt.contextKey });
|
|
2093
|
+
const loaded = await this.loadSession({ ...current, contextKey: attempt.contextKey }, Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
2011
2094
|
const recoveryText = recoveryRuntimePrompt(attempt.promptText);
|
|
2012
2095
|
const turnId = await this.startTurn(loaded, recoveryText, replyTarget, attempt.localImagePaths, {
|
|
2013
2096
|
attemptId: attempt.attemptId,
|
|
@@ -2046,7 +2129,7 @@ export class ProxySessionController {
|
|
|
2046
2129
|
await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt);
|
|
2047
2130
|
}
|
|
2048
2131
|
async promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt) {
|
|
2049
|
-
if (this.llmRetryingSessions.has(record.localSessionId)) {
|
|
2132
|
+
if (this.llmRetryingSessions.has(record.localSessionId) || this.planHandoffs.has(record.localSessionId)) {
|
|
2050
2133
|
const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
|
|
2051
2134
|
localImagePaths,
|
|
2052
2135
|
messageId,
|
|
@@ -2067,7 +2150,7 @@ export class ProxySessionController {
|
|
|
2067
2150
|
}
|
|
2068
2151
|
let loaded;
|
|
2069
2152
|
try {
|
|
2070
|
-
loaded = await this.loadSession(record);
|
|
2153
|
+
loaded = await this.loadSession(record, Boolean(this.store.getPendingProviderSettings(record.localSessionId)));
|
|
2071
2154
|
}
|
|
2072
2155
|
catch (error) {
|
|
2073
2156
|
if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
|
|
@@ -3043,6 +3126,22 @@ export class ProxySessionController {
|
|
|
3043
3126
|
};
|
|
3044
3127
|
}
|
|
3045
3128
|
async startTurn(loaded, text, replyTarget, localImagePaths, options = {}) {
|
|
3129
|
+
return this.settingsOperations.run(loaded.record.localSessionId, async () => {
|
|
3130
|
+
try {
|
|
3131
|
+
await this.applyPendingProviderSettings(loaded);
|
|
3132
|
+
}
|
|
3133
|
+
catch (error) {
|
|
3134
|
+
await this.outbound.failPendingTurn(loaded.record.localSessionId, runtimeErrorMessage(error));
|
|
3135
|
+
throw error;
|
|
3136
|
+
}
|
|
3137
|
+
if (this.store.getPendingProviderSettings(loaded.record.localSessionId)) {
|
|
3138
|
+
throw new Error("Provider 切换尚未完成,请稍后重试。");
|
|
3139
|
+
}
|
|
3140
|
+
loaded.session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
|
|
3141
|
+
return this.startTurnNow(loaded, text, replyTarget, localImagePaths, options);
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
async startTurnNow(loaded, text, replyTarget, localImagePaths, options = {}) {
|
|
3046
3145
|
const currentRecord = this.store.getSession(loaded.record.localSessionId);
|
|
3047
3146
|
const title = currentRecord?.title ?? normalizeTaskTitle(options.displayPrompt ?? text);
|
|
3048
3147
|
if (!currentRecord?.title && title)
|
|
@@ -3088,7 +3187,13 @@ export class ProxySessionController {
|
|
|
3088
3187
|
}
|
|
3089
3188
|
this.store.updateTurnAttempt(attemptId, { pendingTurnId: pendingTurnId ?? null });
|
|
3090
3189
|
try {
|
|
3091
|
-
|
|
3190
|
+
if (options.approvedPlanTurnId && (this.planHandoffs.get(loaded.record.localSessionId) !== options.approvedPlanTurnId
|
|
3191
|
+
|| this.sessionResets.has(loaded.record.localSessionId))) {
|
|
3192
|
+
throw new Error("方案执行已取消。");
|
|
3193
|
+
}
|
|
3194
|
+
turnId = await loaded.runtime.startTurn(loaded.record.localSessionId, options.approvedPlanTurnId
|
|
3195
|
+
? { text: runtimeText, collaborationMode: "default" }
|
|
3196
|
+
: runtimePrompt(runtimeText, localImagePaths));
|
|
3092
3197
|
break;
|
|
3093
3198
|
}
|
|
3094
3199
|
catch (error) {
|
|
@@ -3503,22 +3608,44 @@ export class ProxySessionController {
|
|
|
3503
3608
|
this.store.setBoundProjectCwd(groupContextKey, boundProjectCwd);
|
|
3504
3609
|
return { chatId: group.chatId, contextKey: groupContextKey, name: group.name };
|
|
3505
3610
|
}
|
|
3506
|
-
async loadSession(record) {
|
|
3611
|
+
async loadSession(record, forSettings = false) {
|
|
3507
3612
|
const agent = this.ensureAgent(record.agentName);
|
|
3508
3613
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
3509
3614
|
const existing = runtime.getSession(record.localSessionId);
|
|
3510
|
-
if (existing)
|
|
3615
|
+
if (existing && (forSettings || !this.settingsOnlySessions.has(record.localSessionId))) {
|
|
3511
3616
|
return { record, runtime, session: existing };
|
|
3617
|
+
}
|
|
3512
3618
|
const pending = this.sessionLoads.get(record.localSessionId);
|
|
3513
3619
|
if (pending) {
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3620
|
+
try {
|
|
3621
|
+
const loaded = await pending;
|
|
3622
|
+
if (!forSettings && this.settingsOnlySessions.has(record.localSessionId)) {
|
|
3623
|
+
if (this.sessionLoads.get(record.localSessionId) === pending)
|
|
3624
|
+
this.sessionLoads.delete(record.localSessionId);
|
|
3625
|
+
return this.loadSession(this.store.getSession(record.localSessionId) ?? record);
|
|
3626
|
+
}
|
|
3627
|
+
return { ...loaded, record: { ...loaded.record, contextKey: record.contextKey } };
|
|
3628
|
+
}
|
|
3629
|
+
catch (error) {
|
|
3630
|
+
if (!forSettings || !runtime.prepareSessionForSettings)
|
|
3631
|
+
throw error;
|
|
3632
|
+
if (this.sessionLoads.get(record.localSessionId) === pending)
|
|
3633
|
+
this.sessionLoads.delete(record.localSessionId);
|
|
3634
|
+
return this.loadSession(record, forSettings);
|
|
3635
|
+
}
|
|
3519
3636
|
}
|
|
3520
3637
|
this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName), record.model, record.modelProvider);
|
|
3521
|
-
const loading = (async () => {
|
|
3638
|
+
const loading = this.sessionHydrationOperations.run(record.localSessionId, async () => {
|
|
3639
|
+
const latest = this.store.getSession(record.localSessionId);
|
|
3640
|
+
if (latest)
|
|
3641
|
+
record = {
|
|
3642
|
+
...record, modelProvider: latest.modelProvider, model: latest.model,
|
|
3643
|
+
reasoningEffort: latest.reasoningEffort, permissionMode: latest.permissionMode,
|
|
3644
|
+
};
|
|
3645
|
+
const current = runtime.getSession(record.localSessionId);
|
|
3646
|
+
if (current && (forSettings || !this.settingsOnlySessions.has(record.localSessionId))) {
|
|
3647
|
+
return { record, runtime, session: current };
|
|
3648
|
+
}
|
|
3522
3649
|
if (record.lastTurnId) {
|
|
3523
3650
|
try {
|
|
3524
3651
|
await this.outbound.resumeDelivery(record.localSessionId, record.contextKey, record.lastTurnId);
|
|
@@ -3528,45 +3655,32 @@ export class ProxySessionController {
|
|
|
3528
3655
|
}
|
|
3529
3656
|
}
|
|
3530
3657
|
const permissionMode = record.permissionMode ?? "auto";
|
|
3531
|
-
|
|
3658
|
+
const prepare = forSettings && record.remoteSessionId && runtime.prepareSessionForSettings;
|
|
3659
|
+
if (record.remoteSessionId && !prepare)
|
|
3532
3660
|
await this.assertSessionTurnOwnership(record, runtime);
|
|
3533
3661
|
let session;
|
|
3534
3662
|
if (record.remoteSessionId) {
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
if (!(agent.kind === "app-server" && !record.lastTurnId
|
|
3557
|
-
&& !this.store.getForkHistorySource(record.localSessionId) && isMissingRolloutError(error)))
|
|
3558
|
-
throw error;
|
|
3559
|
-
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
3560
|
-
session = await runtime.createSession({
|
|
3561
|
-
localSessionId: record.localSessionId,
|
|
3562
|
-
agentName: record.agentName,
|
|
3563
|
-
cwd: record.cwd,
|
|
3564
|
-
title: record.title,
|
|
3565
|
-
modelProvider: record.modelProvider,
|
|
3566
|
-
model: record.model,
|
|
3567
|
-
reasoningEffort: record.reasoningEffort,
|
|
3568
|
-
permissionMode,
|
|
3569
|
-
});
|
|
3663
|
+
const input = {
|
|
3664
|
+
localSessionId: record.localSessionId,
|
|
3665
|
+
remoteSessionId: record.remoteSessionId,
|
|
3666
|
+
agentName: record.agentName,
|
|
3667
|
+
cwd: record.cwd,
|
|
3668
|
+
title: record.title,
|
|
3669
|
+
modelProvider: record.modelProvider,
|
|
3670
|
+
model: record.model,
|
|
3671
|
+
reasoningEffort: record.reasoningEffort,
|
|
3672
|
+
permissionMode,
|
|
3673
|
+
activeTurnId: agent.kind === "app-server" && record.status === "running" && record.lastTurnStatus === "running"
|
|
3674
|
+
? record.lastTurnId
|
|
3675
|
+
: undefined,
|
|
3676
|
+
lastTurnId: record.lastTurnId,
|
|
3677
|
+
lastTurnStatus: record.lastTurnStatus,
|
|
3678
|
+
};
|
|
3679
|
+
if (prepare)
|
|
3680
|
+
this.settingsOnlySessions.add(record.localSessionId);
|
|
3681
|
+
session = prepare ? await prepare.call(runtime, input) : await runtime.resumeSession(input);
|
|
3682
|
+
if (session.remoteSessionId !== record.remoteSessionId) {
|
|
3683
|
+
throw new Error("加载设置返回了不同的任务 ID,不允许替换任务或更改历史。");
|
|
3570
3684
|
}
|
|
3571
3685
|
}
|
|
3572
3686
|
else {
|
|
@@ -3581,20 +3695,24 @@ export class ProxySessionController {
|
|
|
3581
3695
|
permissionMode,
|
|
3582
3696
|
});
|
|
3583
3697
|
}
|
|
3584
|
-
|
|
3698
|
+
if (!prepare) {
|
|
3699
|
+
this.settingsOnlySessions.delete(record.localSessionId);
|
|
3700
|
+
this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
|
|
3701
|
+
}
|
|
3585
3702
|
const saved = this.store.getSession(record.localSessionId) ?? record;
|
|
3586
3703
|
return {
|
|
3587
3704
|
record: { ...saved, contextKey: record.contextKey },
|
|
3588
3705
|
runtime,
|
|
3589
3706
|
session,
|
|
3590
3707
|
};
|
|
3591
|
-
})
|
|
3708
|
+
});
|
|
3592
3709
|
this.sessionLoads.set(record.localSessionId, loading);
|
|
3593
3710
|
try {
|
|
3594
3711
|
return await loading;
|
|
3595
3712
|
}
|
|
3596
3713
|
finally {
|
|
3597
|
-
this.sessionLoads.
|
|
3714
|
+
if (this.sessionLoads.get(record.localSessionId) === loading)
|
|
3715
|
+
this.sessionLoads.delete(record.localSessionId);
|
|
3598
3716
|
}
|
|
3599
3717
|
}
|
|
3600
3718
|
persistRuntimeSession(record, session, status) {
|
|
@@ -3614,6 +3732,11 @@ export class ProxySessionController {
|
|
|
3614
3732
|
}
|
|
3615
3733
|
async handleRuntimeEvent(event) {
|
|
3616
3734
|
if ("turnId" in event) {
|
|
3735
|
+
const recoveryAttempt = this.recoveryRetryStartedAt.size > 0
|
|
3736
|
+
? this.store.findIncompleteTurnAttemptByTurnId(event.turnId) : undefined;
|
|
3737
|
+
if (recoveryAttempt && this.recoveryRetryStartedAt.has(recoveryAttempt.attemptId)) {
|
|
3738
|
+
this.recoveredLiveAttempts.add(recoveryAttempt.attemptId);
|
|
3739
|
+
}
|
|
3617
3740
|
this.store.touchTurnAttempt(event.turnId);
|
|
3618
3741
|
const source = this.store.getSession(event.sessionId);
|
|
3619
3742
|
if (source?.remoteSessionId && this.isCodexSession(source)) {
|
|
@@ -3627,6 +3750,15 @@ export class ProxySessionController {
|
|
|
3627
3750
|
this.store.saveTurnParent(event.turnId, event.sessionId, this.completedParentTurnId(source));
|
|
3628
3751
|
}
|
|
3629
3752
|
}
|
|
3753
|
+
if (event.type === "turn_completed" && event.approvedPlan) {
|
|
3754
|
+
if (this.planHandoffs.get(event.sessionId) === event.turnId)
|
|
3755
|
+
return;
|
|
3756
|
+
const attempt = this.store.findIncompleteTurnAttemptByTurnId(event.turnId);
|
|
3757
|
+
if (attempt) {
|
|
3758
|
+
await this.continueApprovedPlan(event, attempt);
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3630
3762
|
const failedAttempt = event.type === "turn_failed"
|
|
3631
3763
|
? this.store.findIncompleteTurnAttemptByTurnId(event.turnId)
|
|
3632
3764
|
: undefined;
|
|
@@ -3652,6 +3784,13 @@ export class ProxySessionController {
|
|
|
3652
3784
|
this.store.updateRuntimeSession(event.sessionId, { lastTurnId: event.turnId, lastTurnStatus: "running" });
|
|
3653
3785
|
}
|
|
3654
3786
|
else if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
|
|
3787
|
+
const attempt = this.store.findIncompleteTurnAttemptByTurnId(event.turnId);
|
|
3788
|
+
if (attempt) {
|
|
3789
|
+
clearTimeout(this.recoveryRetryTimers.get(attempt.attemptId));
|
|
3790
|
+
this.recoveryRetryTimers.delete(attempt.attemptId);
|
|
3791
|
+
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
3792
|
+
this.recoveredLiveAttempts.delete(attempt.attemptId);
|
|
3793
|
+
}
|
|
3655
3794
|
this.store.markTurnAttemptTerminal(event.turnId, event.type === "turn_completed" ? "completed" : event.type === "turn_cancelled" ? "cancelled" : "failed");
|
|
3656
3795
|
this.store.updateRuntimeSession(event.sessionId, {
|
|
3657
3796
|
lastTurnId: event.turnId,
|
|
@@ -3663,7 +3802,7 @@ export class ProxySessionController {
|
|
|
3663
3802
|
...event,
|
|
3664
3803
|
turnCount: event.turnCount ?? this.store.countTaskTurns(event.sessionId),
|
|
3665
3804
|
}
|
|
3666
|
-
: event;
|
|
3805
|
+
: event.type === "turn_completed" && event.approvedPlan ? { ...event, approvedPlan: false } : event;
|
|
3667
3806
|
const presentationEvent = event.type === "turn_failed"
|
|
3668
3807
|
&& isRetryableLlmTurnFailure(event.message)
|
|
3669
3808
|
&& (failedAttempt?.retryCount ?? 0) >= MAX_LLM_TURN_RETRIES
|
|
@@ -3698,12 +3837,87 @@ export class ProxySessionController {
|
|
|
3698
3837
|
: undefined;
|
|
3699
3838
|
if (latest?.lastTurnId === event.turnId && !activeTurnId) {
|
|
3700
3839
|
this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
|
|
3840
|
+
await this.applyPendingProviderWhenIdle(event.sessionId);
|
|
3701
3841
|
await this.scheduleNextQueuedPrompt(event.sessionId);
|
|
3702
3842
|
}
|
|
3703
3843
|
}
|
|
3704
3844
|
if (presentationError)
|
|
3705
3845
|
throw presentationError;
|
|
3706
3846
|
}
|
|
3847
|
+
async continueApprovedPlan(event, attempt) {
|
|
3848
|
+
const { sessionId, turnId } = event;
|
|
3849
|
+
const source = this.store.getSession(sessionId);
|
|
3850
|
+
if (!source || source.status === "closed" || source.lastTurnId !== turnId)
|
|
3851
|
+
return;
|
|
3852
|
+
this.planHandoffs.set(sessionId, turnId);
|
|
3853
|
+
this.store.markTurnAttemptTerminal(turnId, "completed");
|
|
3854
|
+
this.store.updateRuntimeSession(sessionId, { lastTurnId: turnId, lastTurnStatus: "completed" });
|
|
3855
|
+
let continued = false;
|
|
3856
|
+
let failed = false;
|
|
3857
|
+
try {
|
|
3858
|
+
try {
|
|
3859
|
+
await this.outbound.onEvent(event);
|
|
3860
|
+
}
|
|
3861
|
+
catch (error) {
|
|
3862
|
+
this.logger.warn({ error, sessionId, turnId }, "Failed to finalize the approved plan card.");
|
|
3863
|
+
}
|
|
3864
|
+
await this.settingsOperations.run(sessionId, async () => {
|
|
3865
|
+
const current = this.store.getSessionForContext(sessionId, attempt.contextKey);
|
|
3866
|
+
if (this.planHandoffs.get(sessionId) !== turnId || this.sessionResets.has(sessionId)
|
|
3867
|
+
|| !current || current.status === "closed" || current.remoteSessionId !== source.remoteSessionId)
|
|
3868
|
+
return;
|
|
3869
|
+
const loaded = await this.loadSession(current);
|
|
3870
|
+
await this.assertSessionTurnOwnership(current, loaded.runtime);
|
|
3871
|
+
if (this.planHandoffs.get(sessionId) !== turnId || this.sessionResets.has(sessionId))
|
|
3872
|
+
return;
|
|
3873
|
+
// Another client may already have started the implementation.
|
|
3874
|
+
let nextTurnId = loaded.session.activeTurnId;
|
|
3875
|
+
if (!nextTurnId) {
|
|
3876
|
+
if (current.lastTurnId !== turnId)
|
|
3877
|
+
return;
|
|
3878
|
+
await this.applyPendingProviderSettings(loaded);
|
|
3879
|
+
if (this.store.getPendingProviderSettings(sessionId))
|
|
3880
|
+
throw new Error("Provider 切换尚未完成。");
|
|
3881
|
+
if (this.planHandoffs.get(sessionId) !== turnId || this.sessionResets.has(sessionId))
|
|
3882
|
+
return;
|
|
3883
|
+
nextTurnId = await this.startTurnNow(loaded, "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.", attempt.replyMessageId ? { messageId: attempt.replyMessageId, replyInThread: true } : undefined, undefined, { messageId: attempt.messageId, displayPrompt: "方案已确认,继续执行", approvedPlanTurnId: turnId });
|
|
3884
|
+
}
|
|
3885
|
+
continued = true;
|
|
3886
|
+
this.store.rebindPendingTurnMessages(sessionId, turnId, nextTurnId);
|
|
3887
|
+
const latest = this.store.getSession(sessionId);
|
|
3888
|
+
if (latest?.lastTurnId === nextTurnId && (latest.lastTurnStatus === "completed"
|
|
3889
|
+
|| latest.lastTurnStatus === "failed" || latest.lastTurnStatus === "cancelled")) {
|
|
3890
|
+
await this.finalizeTurnMessageReactions(nextTurnId, latest.lastTurnStatus);
|
|
3891
|
+
}
|
|
3892
|
+
this.store.audit(attempt.contextKey, "approved_plan_continued", { sessionId, turnId, nextTurnId });
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
catch (error) {
|
|
3896
|
+
failed = true;
|
|
3897
|
+
this.logger.error({ error, sessionId, turnId }, "Failed to continue an approved plan.");
|
|
3898
|
+
await this.outbound.sendText(attempt.contextKey, "方案已确认,但后续执行启动失败:" + runtimeErrorMessage(error) + "。请发送消息继续。");
|
|
3899
|
+
}
|
|
3900
|
+
finally {
|
|
3901
|
+
if (this.planHandoffs.get(sessionId) === turnId)
|
|
3902
|
+
this.planHandoffs.delete(sessionId);
|
|
3903
|
+
if (!continued) {
|
|
3904
|
+
try {
|
|
3905
|
+
await this.outbound.onEvent(failed
|
|
3906
|
+
? { type: "turn_failed", sessionId, turnId, message: "方案已确认,但后续执行未能启动。" }
|
|
3907
|
+
: { type: "turn_cancelled", sessionId, turnId });
|
|
3908
|
+
}
|
|
3909
|
+
catch (error) {
|
|
3910
|
+
this.logger.warn({ error, sessionId, turnId }, "Failed to finish the plan handoff card.");
|
|
3911
|
+
}
|
|
3912
|
+
await this.finalizeTurnMessageReactions(turnId, failed ? "failed" : "cancelled");
|
|
3913
|
+
}
|
|
3914
|
+
const latest = this.store.getSession(sessionId);
|
|
3915
|
+
if (latest?.lastTurnId === turnId && latest.status !== "closed") {
|
|
3916
|
+
this.store.updateSession(sessionId, { status: failed ? "failed" : "ready" });
|
|
3917
|
+
}
|
|
3918
|
+
await this.scheduleNextQueuedPrompt(sessionId);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3707
3921
|
async retryFailedLlmTurn(event, attempt) {
|
|
3708
3922
|
const retryNumber = attempt.retryCount;
|
|
3709
3923
|
const session = this.store.getSession(event.sessionId);
|
|
@@ -3737,7 +3951,7 @@ export class ProxySessionController {
|
|
|
3737
3951
|
: undefined;
|
|
3738
3952
|
try {
|
|
3739
3953
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
3740
|
-
const loaded = await this.loadSession(current);
|
|
3954
|
+
const loaded = await this.loadSession(current, Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
3741
3955
|
const retryTurnId = await this.startTurn(loaded, llmRetryRuntimePrompt(attempt.promptText, retryNumber), replyTarget, attempt.localImagePaths, {
|
|
3742
3956
|
attemptId: attempt.attemptId,
|
|
3743
3957
|
messageId: attempt.messageId,
|
|
@@ -3774,6 +3988,8 @@ export class ProxySessionController {
|
|
|
3774
3988
|
});
|
|
3775
3989
|
}
|
|
3776
3990
|
scheduleQueuedPromptRetry(sessionId) {
|
|
3991
|
+
if (this.store.getPendingProviderSettings(sessionId)?.error)
|
|
3992
|
+
return;
|
|
3777
3993
|
if (this.queuedPromptRetryTimers.has(sessionId))
|
|
3778
3994
|
return;
|
|
3779
3995
|
if (this.store.countQueuedPrompts(sessionId) === 0)
|
|
@@ -3789,6 +4005,11 @@ export class ProxySessionController {
|
|
|
3789
4005
|
this.queuedPromptRetryTimers.set(sessionId, timer);
|
|
3790
4006
|
}
|
|
3791
4007
|
async startNextQueuedPromptIfIdle(sessionId) {
|
|
4008
|
+
return this.settingsOperations.run(sessionId, () => this.startNextQueuedPromptIfIdleNow(sessionId));
|
|
4009
|
+
}
|
|
4010
|
+
async startNextQueuedPromptIfIdleNow(sessionId) {
|
|
4011
|
+
if (this.planHandoffs.has(sessionId) || this.sessionResets.has(sessionId))
|
|
4012
|
+
return;
|
|
3792
4013
|
if (this.store.countQueuedPrompts(sessionId) === 0)
|
|
3793
4014
|
return;
|
|
3794
4015
|
const baseRecord = this.store.getSession(sessionId);
|
|
@@ -3796,20 +4017,23 @@ export class ProxySessionController {
|
|
|
3796
4017
|
return;
|
|
3797
4018
|
let prompt;
|
|
3798
4019
|
try {
|
|
3799
|
-
let loaded = await this.loadSession(baseRecord);
|
|
4020
|
+
let loaded = await this.loadSession(baseRecord, Boolean(this.store.getPendingProviderSettings(sessionId)));
|
|
3800
4021
|
const remoteActivity = await this.assertSessionTurnOwnership(loaded.record, loaded.runtime);
|
|
3801
4022
|
if (needsFullSessionSynchronization(loaded.record, loaded.session, remoteActivity)) {
|
|
3802
4023
|
loaded = { ...loaded, session: await loaded.runtime.synchronizeSession(sessionId) };
|
|
3803
4024
|
}
|
|
3804
4025
|
if (loaded.session.activeTurnId)
|
|
3805
4026
|
return;
|
|
4027
|
+
await this.applyPendingProviderSettings(loaded);
|
|
4028
|
+
if (this.store.getPendingProviderSettings(sessionId))
|
|
4029
|
+
return;
|
|
3806
4030
|
prompt = this.store.takeNextQueuedPrompt(sessionId);
|
|
3807
4031
|
if (!prompt)
|
|
3808
4032
|
return;
|
|
3809
4033
|
const record = this.store.getSessionForContext(sessionId, prompt.contextKey) ?? baseRecord;
|
|
3810
4034
|
loaded = { ...loaded, record };
|
|
3811
4035
|
await this.refreshPromptQueueCards(sessionId);
|
|
3812
|
-
const turnId = await this.
|
|
4036
|
+
const turnId = await this.startTurnNow(loaded, prompt.text, prompt.replyMessageId ? { messageId: prompt.replyMessageId, replyInThread: true } : undefined, prompt.localImagePaths, { messageId: prompt.messageId, displayPrompt: prompt.displayPrompt });
|
|
3813
4037
|
if (prompt.messageId)
|
|
3814
4038
|
await this.bindMessageReactionToTurn(prompt.messageId, sessionId, turnId);
|
|
3815
4039
|
}
|
|
@@ -3868,6 +4092,7 @@ export class ProxySessionController {
|
|
|
3868
4092
|
});
|
|
3869
4093
|
const pending = {
|
|
3870
4094
|
contextKey: message.contextKey,
|
|
4095
|
+
userId: message.userId,
|
|
3871
4096
|
attachmentPromise,
|
|
3872
4097
|
resolveAttachment,
|
|
3873
4098
|
};
|
|
@@ -3875,7 +4100,7 @@ export class ProxySessionController {
|
|
|
3875
4100
|
return pending;
|
|
3876
4101
|
}
|
|
3877
4102
|
reserveForwardAttachment(message) {
|
|
3878
|
-
if (!message.parentMessageId)
|
|
4103
|
+
if (!message.parentMessageId || isCommandMessage(message))
|
|
3879
4104
|
return undefined;
|
|
3880
4105
|
const candidates = [
|
|
3881
4106
|
{ sourceKind: "merged_forward", registry: this.pendingMergedForwards },
|
|
@@ -3883,7 +4108,8 @@ export class ProxySessionController {
|
|
|
3883
4108
|
];
|
|
3884
4109
|
for (const candidate of candidates) {
|
|
3885
4110
|
const pending = candidate.registry.get(message.parentMessageId);
|
|
3886
|
-
if (!pending || pending.contextKey !== message.contextKey
|
|
4111
|
+
if (!pending || pending.contextKey !== message.contextKey
|
|
4112
|
+
|| pending.userId !== message.userId || pending.attachmentMessageId)
|
|
3887
4113
|
continue;
|
|
3888
4114
|
pending.attachmentMessageId = message.messageId;
|
|
3889
4115
|
return {
|
|
@@ -3908,17 +4134,20 @@ export class ProxySessionController {
|
|
|
3908
4134
|
return true;
|
|
3909
4135
|
}
|
|
3910
4136
|
async waitForForwardAttachment(messageId, pending, registry) {
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
4137
|
+
const timer = setTimeout(() => {
|
|
4138
|
+
// Arrival reserves the pair before acknowledgement. A slow Reaction must not split it.
|
|
4139
|
+
if (pending.attachmentMessageId)
|
|
4140
|
+
return;
|
|
4141
|
+
if (registry.get(messageId) === pending)
|
|
4142
|
+
registry.delete(messageId);
|
|
4143
|
+
pending.resolveAttachment(undefined);
|
|
4144
|
+
}, FORWARD_ATTACHMENT_WINDOW_MS);
|
|
4145
|
+
timer.unref?.();
|
|
3916
4146
|
try {
|
|
3917
|
-
return await
|
|
4147
|
+
return await pending.attachmentPromise;
|
|
3918
4148
|
}
|
|
3919
4149
|
finally {
|
|
3920
|
-
|
|
3921
|
-
clearTimeout(timer);
|
|
4150
|
+
clearTimeout(timer);
|
|
3922
4151
|
if (registry.get(messageId) === pending) {
|
|
3923
4152
|
registry.delete(messageId);
|
|
3924
4153
|
}
|
|
@@ -3979,6 +4208,7 @@ export class ProxySessionController {
|
|
|
3979
4208
|
await this.cancelSession(record);
|
|
3980
4209
|
}
|
|
3981
4210
|
async cancelSession(record) {
|
|
4211
|
+
this.planHandoffs.delete(record.localSessionId);
|
|
3982
4212
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
3983
4213
|
if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
|
|
3984
4214
|
if (runtime.getGoal && runtime.setGoal) {
|
|
@@ -4055,6 +4285,7 @@ export class ProxySessionController {
|
|
|
4055
4285
|
});
|
|
4056
4286
|
}
|
|
4057
4287
|
async runSessionReset(sessionId, operation) {
|
|
4288
|
+
this.planHandoffs.delete(sessionId);
|
|
4058
4289
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
4059
4290
|
const reset = previous.catch(() => undefined).then(operation);
|
|
4060
4291
|
this.sessionResets.set(sessionId, reset);
|
|
@@ -4199,6 +4430,14 @@ export class ProxySessionController {
|
|
|
4199
4430
|
await loaded.runtime.respondToApproval(sessionId, requestId, decision);
|
|
4200
4431
|
}
|
|
4201
4432
|
async openExecutionSettings(contextKey, activeTab, options = {}) {
|
|
4433
|
+
const record = options.sessionId
|
|
4434
|
+
? this.requireSession(contextKey, options.sessionId)
|
|
4435
|
+
: this.currentSession(contextKey);
|
|
4436
|
+
if (!record)
|
|
4437
|
+
return this.openExecutionSettingsNow(contextKey, activeTab, options);
|
|
4438
|
+
return this.settingsOperations.run(record.localSessionId, () => this.openExecutionSettingsNow(contextKey, activeTab, { ...options, sessionId: record.localSessionId }));
|
|
4439
|
+
}
|
|
4440
|
+
async openExecutionSettingsNow(contextKey, activeTab, options = {}) {
|
|
4202
4441
|
const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
|
|
4203
4442
|
const record = options.sessionId
|
|
4204
4443
|
? this.requireSession(contextKey, options.sessionId)
|
|
@@ -4217,22 +4456,31 @@ export class ProxySessionController {
|
|
|
4217
4456
|
reasoningOptions: [],
|
|
4218
4457
|
};
|
|
4219
4458
|
if (record) {
|
|
4220
|
-
const loaded = await this.loadSession(record);
|
|
4459
|
+
const loaded = await this.loadSession(record, true);
|
|
4460
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
4461
|
+
const selected = pending?.settings ?? loaded.session;
|
|
4462
|
+
if (pending && options.updateMessageId) {
|
|
4463
|
+
this.store.savePendingProviderSettings({
|
|
4464
|
+
...pending, contextKey, messageId: options.updateMessageId, replyMessageId: options.updateMessageId, activeTab,
|
|
4465
|
+
});
|
|
4466
|
+
}
|
|
4221
4467
|
const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
|
|
4222
4468
|
const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
|
|
4223
|
-
const currentProvider =
|
|
4224
|
-
const { models, warning } = await this.modelOptions(loaded.runtime, currentProvider,
|
|
4225
|
-
const currentModel = models.find((model) => model.id ===
|
|
4469
|
+
const currentProvider = selected.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
|
|
4470
|
+
const { models, warning } = await this.modelOptions(loaded.runtime, currentProvider, selected.model);
|
|
4471
|
+
const currentModel = models.find((model) => model.id === selected.model)
|
|
4226
4472
|
?? models.find((model) => model.isDefault)
|
|
4227
4473
|
?? models[0];
|
|
4228
|
-
const currentEffort = currentModel?.supportedReasoningEfforts.some((option) => option.value ===
|
|
4229
|
-
?
|
|
4230
|
-
: currentModel?.defaultReasoningEffort ??
|
|
4474
|
+
const currentEffort = currentModel?.supportedReasoningEfforts.some((option) => option.value === selected.reasoningEffort)
|
|
4475
|
+
? selected.reasoningEffort
|
|
4476
|
+
: currentModel?.defaultReasoningEffort ?? selected.reasoningEffort;
|
|
4231
4477
|
runtimeSettings = {
|
|
4478
|
+
pendingNotice: pending ? this.pendingProviderNotice(pending) : undefined,
|
|
4479
|
+
pendingFailed: Boolean(pending?.error),
|
|
4232
4480
|
currentProvider,
|
|
4233
|
-
currentModel: currentModel?.id ??
|
|
4481
|
+
currentModel: pending?.settings.model ?? currentModel?.id ?? selected.model,
|
|
4234
4482
|
currentEffort,
|
|
4235
|
-
currentPermissionMode:
|
|
4483
|
+
currentPermissionMode: selected.permissionMode,
|
|
4236
4484
|
providers,
|
|
4237
4485
|
providerSupported,
|
|
4238
4486
|
models,
|
|
@@ -4248,7 +4496,10 @@ export class ProxySessionController {
|
|
|
4248
4496
|
agents,
|
|
4249
4497
|
runtimeSettingsAvailable: Boolean(record),
|
|
4250
4498
|
...runtimeSettings,
|
|
4499
|
+
modelPage: options.expectedProvider !== undefined && options.expectedProvider !== (runtimeSettings.currentProvider ?? "")
|
|
4500
|
+
? undefined : options.modelPage,
|
|
4251
4501
|
notice: options.notice,
|
|
4502
|
+
warning: [runtimeSettings.warning, options.warning].filter(Boolean).join("\n") || undefined,
|
|
4252
4503
|
});
|
|
4253
4504
|
if (options.updateMessageId) {
|
|
4254
4505
|
await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
|
|
@@ -4266,35 +4517,40 @@ export class ProxySessionController {
|
|
|
4266
4517
|
await this.openExecutionSettings(contextKey, "agent");
|
|
4267
4518
|
}
|
|
4268
4519
|
async selectProvider(contextKey, modelProvider, options = {}) {
|
|
4520
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
4521
|
+
return this.settingsOperations.run(record.localSessionId, () => this.selectProviderNow(contextKey, modelProvider, { ...options, sessionId: record.localSessionId }));
|
|
4522
|
+
}
|
|
4523
|
+
async selectProviderNow(contextKey, modelProvider, options = {}) {
|
|
4269
4524
|
const record = options.sessionId
|
|
4270
4525
|
? this.requireSession(contextKey, options.sessionId)
|
|
4271
4526
|
: this.requireCurrentSession(contextKey);
|
|
4272
|
-
await this.
|
|
4273
|
-
const
|
|
4527
|
+
const loaded = await this.loadSession(record, true);
|
|
4528
|
+
const selected = this.store.getPendingProviderSettings(record.localSessionId)?.settings ?? loaded.session;
|
|
4274
4529
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4275
|
-
const models = await loaded.runtime.listModels(modelProvider,
|
|
4276
|
-
const model = selectProviderModel(models,
|
|
4530
|
+
const models = await loaded.runtime.listModels(modelProvider, selected.model);
|
|
4531
|
+
const model = selectProviderModel(models, selected.model);
|
|
4277
4532
|
if (!model)
|
|
4278
4533
|
throw new Error("当前运行时没有可用于 Provider 切换的模型。");
|
|
4279
|
-
const effort = selectProviderReasoningEffort(model,
|
|
4280
|
-
await this.
|
|
4534
|
+
const effort = selectProviderReasoningEffort(model, selected.reasoningEffort);
|
|
4535
|
+
await this.applyProviderSettingsNow(contextKey, {
|
|
4281
4536
|
provider: modelProvider,
|
|
4282
4537
|
model: model.id,
|
|
4283
4538
|
effort,
|
|
4284
|
-
mode:
|
|
4285
|
-
}, options
|
|
4539
|
+
mode: selected.permissionMode,
|
|
4540
|
+
}, options);
|
|
4286
4541
|
}
|
|
4287
4542
|
async openProviderSelector(contextKey, options = {}) {
|
|
4288
4543
|
await this.openExecutionSettings(contextKey, "provider", options);
|
|
4289
4544
|
}
|
|
4290
|
-
async openProviderModelSelector(contextKey, modelProvider, permissionMode, sessionId, updateMessageId) {
|
|
4291
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
4545
|
+
async openProviderModelSelector(contextKey, modelProvider, permissionMode, sessionId, updateMessageId, modelPage) {
|
|
4546
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
4292
4547
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4293
4548
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, loaded.session.model);
|
|
4294
4549
|
const card = this.cardRenderer.renderModelSelector({
|
|
4295
4550
|
sessionId,
|
|
4296
4551
|
contextKey,
|
|
4297
4552
|
currentModel: selectProviderModel(models, loaded.session.model)?.id,
|
|
4553
|
+
modelPage,
|
|
4298
4554
|
reasoningEffort: loaded.session.reasoningEffort,
|
|
4299
4555
|
models,
|
|
4300
4556
|
warning,
|
|
@@ -4305,7 +4561,7 @@ export class ProxySessionController {
|
|
|
4305
4561
|
await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
|
|
4306
4562
|
}
|
|
4307
4563
|
async openProviderReasoningSelector(contextKey, modelProvider, model, permissionMode, sessionId, updateMessageId) {
|
|
4308
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
4564
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
4309
4565
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4310
4566
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
|
|
4311
4567
|
const selected = models.find((candidate) => candidate.id === model);
|
|
@@ -4331,7 +4587,7 @@ export class ProxySessionController {
|
|
|
4331
4587
|
await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
|
|
4332
4588
|
}
|
|
4333
4589
|
async openProviderPermissionSelector(contextKey, modelProvider, model, effort, permissionMode, sessionId, updateMessageId) {
|
|
4334
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
4590
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
4335
4591
|
await this.assertProviderModelSettings(loaded, modelProvider, model, effort);
|
|
4336
4592
|
await this.outbound.updateInteractiveCard(contextKey, updateMessageId, this.cardRenderer.renderPermissionSelector({
|
|
4337
4593
|
sessionId,
|
|
@@ -4343,20 +4599,47 @@ export class ProxySessionController {
|
|
|
4343
4599
|
}));
|
|
4344
4600
|
}
|
|
4345
4601
|
async applyProviderSettings(contextKey, settings, options = {}, announceStart = true) {
|
|
4602
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
4603
|
+
return this.settingsOperations.run(record.localSessionId, () => this.applyProviderSettingsNow(contextKey, settings, { ...options, sessionId: record.localSessionId }, announceStart));
|
|
4604
|
+
}
|
|
4605
|
+
async applyProviderSettingsNow(contextKey, settings, options = {}, announceStart = true) {
|
|
4346
4606
|
const record = options.sessionId
|
|
4347
4607
|
? this.requireSession(contextKey, options.sessionId)
|
|
4348
4608
|
: this.requireCurrentSession(contextKey);
|
|
4349
|
-
|
|
4609
|
+
const loaded = await this.loadSession(record, true);
|
|
4610
|
+
const warning = await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
4611
|
+
const next = {
|
|
4612
|
+
modelProvider: settings.provider, model: settings.model,
|
|
4613
|
+
reasoningEffort: settings.effort, permissionMode: settings.mode,
|
|
4614
|
+
};
|
|
4615
|
+
await this.reconcilePreparedSettingsSession(loaded);
|
|
4616
|
+
if (loaded.session.activeTurnId || this.store.getPendingProviderSettings(record.localSessionId)) {
|
|
4617
|
+
await this.savePendingProviderSelection(loaded, next, contextKey, options, "provider");
|
|
4618
|
+
return;
|
|
4619
|
+
}
|
|
4620
|
+
if (announceStart)
|
|
4350
4621
|
await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
4622
|
+
const previous = { ...loaded.session };
|
|
4623
|
+
let session;
|
|
4624
|
+
try {
|
|
4625
|
+
session = await this.changeProviderSettings(loaded, next);
|
|
4626
|
+
}
|
|
4627
|
+
catch (error) {
|
|
4628
|
+
if (!this.providerSettingsRestored(loaded, previous, error)) {
|
|
4629
|
+
this.store.savePendingProviderSettings({
|
|
4630
|
+
localSessionId: record.localSessionId, contextKey, settings: next, activeTab: "provider",
|
|
4631
|
+
messageId: options.updateMessageId,
|
|
4632
|
+
replyMessageId: options.updateMessageId ?? this.controlSessionReplyTarget(record)?.messageId,
|
|
4633
|
+
error: runtimeErrorMessage(error),
|
|
4634
|
+
});
|
|
4635
|
+
if (options.updateMessageId) {
|
|
4636
|
+
await this.openExecutionSettingsNow(contextKey, "provider", options).catch((presentationError) => {
|
|
4637
|
+
this.logger.warn({ error: presentationError, sessionId: record.localSessionId }, "Failed to refresh rejected Provider selection.");
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
throw error;
|
|
4351
4642
|
}
|
|
4352
|
-
const loaded = await this.loadSession(record);
|
|
4353
|
-
const warning = await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
4354
|
-
const session = await this.changeProviderSettings(loaded, {
|
|
4355
|
-
modelProvider: settings.provider,
|
|
4356
|
-
model: settings.model,
|
|
4357
|
-
reasoningEffort: settings.effort,
|
|
4358
|
-
permissionMode: settings.mode,
|
|
4359
|
-
});
|
|
4360
4643
|
const notice = [
|
|
4361
4644
|
`Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
|
|
4362
4645
|
`模型 ${cardCode(session.model ?? settings.model)}`,
|
|
@@ -4365,7 +4648,7 @@ export class ProxySessionController {
|
|
|
4365
4648
|
`并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
4366
4649
|
].join(",");
|
|
4367
4650
|
if (options.updateMessageId) {
|
|
4368
|
-
await this.
|
|
4651
|
+
await this.openExecutionSettingsNow(contextKey, "provider", {
|
|
4369
4652
|
sessionId: loaded.record.localSessionId,
|
|
4370
4653
|
updateMessageId: options.updateMessageId,
|
|
4371
4654
|
notice,
|
|
@@ -4376,16 +4659,23 @@ export class ProxySessionController {
|
|
|
4376
4659
|
}
|
|
4377
4660
|
}
|
|
4378
4661
|
async changeProviderSettings(loaded, settings) {
|
|
4662
|
+
return this.sessionHydrationOperations.run(loaded.record.localSessionId, () => this.changeProviderSettingsNow(loaded, settings));
|
|
4663
|
+
}
|
|
4664
|
+
async changeProviderSettingsNow(loaded, settings) {
|
|
4379
4665
|
if (!loaded.runtime.setExecutionSettings)
|
|
4380
4666
|
throw new Error("当前运行时不支持 Provider 设置。");
|
|
4381
|
-
|
|
4667
|
+
loaded.session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
|
|
4668
|
+
const remoteSessionId = loaded.session.remoteSessionId;
|
|
4669
|
+
const persist = (candidate) => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
|
|
4670
|
+
if (candidate.remoteSessionId !== remoteSessionId) {
|
|
4671
|
+
throw new Error("设置切换返回了不同的任务 ID,未保存设置;不允许替换任务或更改历史。");
|
|
4672
|
+
}
|
|
4382
4673
|
assertProviderSettingsApplied(candidate, settings);
|
|
4383
4674
|
const agent = this.ensureAgent(loaded.record.agentName);
|
|
4384
4675
|
const previousDefaults = agent.defaults;
|
|
4385
|
-
await this.
|
|
4676
|
+
await this.persistAgentExecutionDefaultsNow(loaded.record.localSessionId, candidate);
|
|
4386
4677
|
try {
|
|
4387
4678
|
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
4388
|
-
remoteSessionId: candidate.remoteSessionId,
|
|
4389
4679
|
modelProvider: candidate.modelProvider,
|
|
4390
4680
|
model: candidate.model,
|
|
4391
4681
|
reasoningEffort: candidate.reasoningEffort,
|
|
@@ -4397,11 +4687,147 @@ export class ProxySessionController {
|
|
|
4397
4687
|
agent.defaults = previousDefaults;
|
|
4398
4688
|
throw error;
|
|
4399
4689
|
}
|
|
4400
|
-
};
|
|
4690
|
+
});
|
|
4401
4691
|
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
4692
|
+
this.settingsOnlySessions.delete(loaded.record.localSessionId);
|
|
4402
4693
|
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
|
|
4403
4694
|
return session;
|
|
4404
4695
|
}
|
|
4696
|
+
async reconcilePreparedSettingsSession(loaded) {
|
|
4697
|
+
if (!this.settingsOnlySessions.has(loaded.record.localSessionId) || !loaded.session.activeTurnId)
|
|
4698
|
+
return;
|
|
4699
|
+
const activity = await this.assertSessionTurnOwnership(loaded.record, loaded.runtime);
|
|
4700
|
+
if (activity && !activity.active) {
|
|
4701
|
+
loaded.session = await loaded.runtime.synchronizeSession(loaded.record.localSessionId);
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
providerSettingsRestored(loaded, previous, error) {
|
|
4705
|
+
const restored = loaded.runtime.getSession(loaded.record.localSessionId);
|
|
4706
|
+
const saved = this.store.getSession(loaded.record.localSessionId);
|
|
4707
|
+
const keys = ["remoteSessionId", "modelProvider", "model", "reasoningEffort", "permissionMode"];
|
|
4708
|
+
return error instanceof ProviderSwitchError && error.previousSettingsRestored
|
|
4709
|
+
&& Boolean(restored && saved && keys.every((key) => restored[key] === previous[key] && saved[key] === previous[key]));
|
|
4710
|
+
}
|
|
4711
|
+
pendingProviderNotice(pending) {
|
|
4712
|
+
const target = `Provider ${cardCode(pending.settings.modelProvider)},模型 ${cardCode(pending.settings.model)}`;
|
|
4713
|
+
return pending.error
|
|
4714
|
+
? `切换失败:${pending.error}。目标为 ${target};后续轮次已暂停,请重新选择 Provider 或模型重试。`
|
|
4715
|
+
: `待生效:当前轮次不变,结束后切换到 ${target}。可继续选择模型,以最后选择为准。`;
|
|
4716
|
+
}
|
|
4717
|
+
async savePendingProviderSelection(loaded, settings, contextKey, options, activeTab) {
|
|
4718
|
+
const previous = this.store.getPendingProviderSettings(loaded.record.localSessionId);
|
|
4719
|
+
const pending = {
|
|
4720
|
+
localSessionId: loaded.record.localSessionId, contextKey, settings, activeTab,
|
|
4721
|
+
messageId: options.updateMessageId ?? previous?.messageId,
|
|
4722
|
+
replyMessageId: options.updateMessageId ?? previous?.replyMessageId
|
|
4723
|
+
?? this.controlSessionReplyTarget(loaded.record)?.messageId,
|
|
4724
|
+
};
|
|
4725
|
+
this.store.savePendingProviderSettings(pending);
|
|
4726
|
+
await this.reconcilePreparedSettingsSession(loaded);
|
|
4727
|
+
if (!loaded.session.activeTurnId) {
|
|
4728
|
+
await this.applyPendingProviderSettings(loaded);
|
|
4729
|
+
void this.scheduleNextQueuedPrompt(loaded.record.localSessionId).catch((error) => {
|
|
4730
|
+
this.logger.warn({ error }, "Failed to resume queue after Provider selection.");
|
|
4731
|
+
});
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
4734
|
+
if (options.updateMessageId) {
|
|
4735
|
+
await this.openExecutionSettingsNow(contextKey, activeTab, options);
|
|
4736
|
+
}
|
|
4737
|
+
else {
|
|
4738
|
+
await this.outbound.sendText(contextKey, this.pendingProviderNotice(pending).replaceAll("`", ""));
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
// Caller holds settingsOperations; only a verified rollback can unblock the next turn.
|
|
4742
|
+
async applyPendingProviderSettings(loaded) {
|
|
4743
|
+
const pending = this.store.getPendingProviderSettings(loaded.record.localSessionId);
|
|
4744
|
+
if (!pending)
|
|
4745
|
+
return;
|
|
4746
|
+
await this.reconcilePreparedSettingsSession(loaded);
|
|
4747
|
+
if (loaded.runtime.getSession(loaded.record.localSessionId)?.activeTurnId)
|
|
4748
|
+
return;
|
|
4749
|
+
if (pending.error)
|
|
4750
|
+
throw new Error(this.pendingProviderNotice(pending));
|
|
4751
|
+
const previous = { ...(loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session) };
|
|
4752
|
+
let notice;
|
|
4753
|
+
let failure;
|
|
4754
|
+
let rolledBack = false;
|
|
4755
|
+
try {
|
|
4756
|
+
loaded.session = await this.changeProviderSettings(loaded, pending.settings);
|
|
4757
|
+
this.store.deletePendingProviderSettings(pending.localSessionId);
|
|
4758
|
+
notice = `Provider 已切换为 ${cardCode(pending.settings.modelProvider)},模型 ${cardCode(pending.settings.model)},从下一轮生效。`;
|
|
4759
|
+
}
|
|
4760
|
+
catch (error) {
|
|
4761
|
+
const restored = loaded.runtime.getSession(pending.localSessionId);
|
|
4762
|
+
rolledBack = this.providerSettingsRestored(loaded, previous, error);
|
|
4763
|
+
if (rolledBack) {
|
|
4764
|
+
loaded.session = restored;
|
|
4765
|
+
this.store.deletePendingProviderSettings(pending.localSessionId);
|
|
4766
|
+
this.outbound.updateSessionModel(pending.localSessionId, previous.model, previous.modelProvider);
|
|
4767
|
+
notice = `切换到 Provider ${cardCode(pending.settings.modelProvider)}、模型 ${cardCode(pending.settings.model)} 失败:${runtimeErrorMessage(error)} 已回退到 Provider ${cardCode(previous.modelProvider ?? "Agent 默认")}、模型 ${cardCode(previous.model ?? "默认")},后续轮次继续使用原设置。`;
|
|
4768
|
+
}
|
|
4769
|
+
else {
|
|
4770
|
+
failure = error;
|
|
4771
|
+
const failed = { ...pending, error: runtimeErrorMessage(error) };
|
|
4772
|
+
this.store.savePendingProviderSettings(failed);
|
|
4773
|
+
notice = this.pendingProviderNotice(failed);
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
try {
|
|
4777
|
+
await this.outbound.withReplyTarget(pending.contextKey, pending.replyMessageId
|
|
4778
|
+
? { messageId: pending.replyMessageId, replyInThread: true } : undefined, async () => {
|
|
4779
|
+
if (pending.messageId) {
|
|
4780
|
+
try {
|
|
4781
|
+
await this.openExecutionSettingsNow(pending.contextKey, pending.activeTab ?? "provider", {
|
|
4782
|
+
sessionId: pending.localSessionId, updateMessageId: pending.messageId,
|
|
4783
|
+
notice: failure || rolledBack ? undefined : notice,
|
|
4784
|
+
warning: rolledBack ? notice.replaceAll("`", "") : undefined,
|
|
4785
|
+
});
|
|
4786
|
+
return;
|
|
4787
|
+
}
|
|
4788
|
+
catch (error) {
|
|
4789
|
+
this.logger.warn({ error, sessionId: pending.localSessionId }, "Failed to refresh Provider settings card.");
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
await this.outbound.sendText(pending.contextKey, notice.replaceAll("`", ""));
|
|
4793
|
+
});
|
|
4794
|
+
}
|
|
4795
|
+
catch (error) {
|
|
4796
|
+
this.logger.warn({ error, sessionId: pending.localSessionId }, "Failed to notify Provider settings result.");
|
|
4797
|
+
}
|
|
4798
|
+
if (failure)
|
|
4799
|
+
throw failure;
|
|
4800
|
+
}
|
|
4801
|
+
async applyPendingProviderWhenIdle(sessionId) {
|
|
4802
|
+
await this.settingsOperations.run(sessionId, async () => {
|
|
4803
|
+
if (this.recoveryClosed)
|
|
4804
|
+
return;
|
|
4805
|
+
const pending = this.store.getPendingProviderSettings(sessionId);
|
|
4806
|
+
if (!pending || pending.error)
|
|
4807
|
+
return;
|
|
4808
|
+
const record = this.store.getSession(sessionId);
|
|
4809
|
+
if (!record || record.status === "closed") {
|
|
4810
|
+
this.store.deletePendingProviderSettings(sessionId);
|
|
4811
|
+
return;
|
|
4812
|
+
}
|
|
4813
|
+
try {
|
|
4814
|
+
const loaded = await this.loadSession(record, true);
|
|
4815
|
+
await this.applyPendingProviderSettings(loaded);
|
|
4816
|
+
}
|
|
4817
|
+
catch (error) {
|
|
4818
|
+
this.logger.warn({ error, sessionId }, "Deferred Provider switch could not be applied.");
|
|
4819
|
+
if (!this.store.getPendingProviderSettings(sessionId)?.error) {
|
|
4820
|
+
const failed = { ...pending, error: runtimeErrorMessage(error) };
|
|
4821
|
+
this.store.savePendingProviderSettings(failed);
|
|
4822
|
+
await this.outbound.withReplyTarget(pending.contextKey, pending.replyMessageId
|
|
4823
|
+
? { messageId: pending.replyMessageId, replyInThread: true } : undefined, () => this.outbound.sendText(pending.contextKey, this.pendingProviderNotice(failed).replaceAll("`", "")))
|
|
4824
|
+
.catch((notificationError) => {
|
|
4825
|
+
this.logger.warn({ error: notificationError, sessionId }, "Failed to notify Provider recovery failure.");
|
|
4826
|
+
});
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4405
4831
|
async assertProviderModelSettings(loaded, modelProvider, model, effort) {
|
|
4406
4832
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4407
4833
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
|
|
@@ -4438,36 +4864,51 @@ export class ProxySessionController {
|
|
|
4438
4864
|
return providers;
|
|
4439
4865
|
}
|
|
4440
4866
|
async model(contextKey, model, options = {}) {
|
|
4867
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
4868
|
+
return this.settingsOperations.run(record.localSessionId, () => this.modelNow(contextKey, model, { ...options, sessionId: record.localSessionId }));
|
|
4869
|
+
}
|
|
4870
|
+
async modelNow(contextKey, model, options = {}) {
|
|
4441
4871
|
if (!model) {
|
|
4442
|
-
await this.
|
|
4872
|
+
await this.openExecutionSettingsNow(contextKey, "model", options);
|
|
4443
4873
|
return;
|
|
4444
4874
|
}
|
|
4445
4875
|
const record = options.sessionId
|
|
4446
4876
|
? this.requireSession(contextKey, options.sessionId)
|
|
4447
4877
|
: this.requireCurrentSession(contextKey);
|
|
4448
|
-
const loaded = await this.loadSession(record);
|
|
4449
|
-
const
|
|
4878
|
+
const loaded = await this.loadSession(record, true);
|
|
4879
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
4880
|
+
const selectedSettings = pending?.settings ?? loaded.session;
|
|
4881
|
+
if (options.expectedProvider && options.expectedProvider !== selectedSettings.modelProvider) {
|
|
4882
|
+
throw new Error("Provider 已发生变化,请重新打开运行设置后选择。");
|
|
4883
|
+
}
|
|
4884
|
+
const models = await loaded.runtime.listModels(selectedSettings.modelProvider, selectedSettings.model);
|
|
4450
4885
|
const selected = models.find((item) => item.id === model);
|
|
4451
4886
|
if (!selected)
|
|
4452
4887
|
throw new Error(`未知模型:${model}`);
|
|
4453
|
-
const currentEffort =
|
|
4888
|
+
const currentEffort = selectedSettings.reasoningEffort;
|
|
4454
4889
|
const compatible = currentEffort
|
|
4455
4890
|
? selected.supportedReasoningEfforts.length === 0
|
|
4456
4891
|
|| selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
|
|
4457
4892
|
: false;
|
|
4458
4893
|
const nextEffort = compatible ? currentEffort : selected.defaultReasoningEffort;
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4894
|
+
if (pending) {
|
|
4895
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, model, reasoningEffort: selectProviderReasoningEffort(selected, nextEffort) }, contextKey, options, "model");
|
|
4896
|
+
return;
|
|
4462
4897
|
}
|
|
4463
|
-
this.
|
|
4464
|
-
|
|
4465
|
-
|
|
4898
|
+
await this.sessionHydrationOperations.run(loaded.record.localSessionId, async () => {
|
|
4899
|
+
await loaded.runtime.setModel(loaded.record.localSessionId, model);
|
|
4900
|
+
if (nextEffort && nextEffort !== currentEffort) {
|
|
4901
|
+
await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
|
|
4902
|
+
}
|
|
4903
|
+
this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
|
|
4904
|
+
this.outbound.updateSessionModel(loaded.record.localSessionId, model, loaded.session.modelProvider);
|
|
4905
|
+
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
4906
|
+
});
|
|
4466
4907
|
const effortMessage = nextEffort && nextEffort !== currentEffort
|
|
4467
4908
|
? `,思考强度已自动调整为 ${nextEffort}`
|
|
4468
4909
|
: "";
|
|
4469
4910
|
if (options.updateMessageId) {
|
|
4470
|
-
await this.
|
|
4911
|
+
await this.openExecutionSettingsNow(contextKey, "model", {
|
|
4471
4912
|
sessionId: loaded.record.localSessionId,
|
|
4472
4913
|
updateMessageId: options.updateMessageId,
|
|
4473
4914
|
notice: `模型已切换为 ${cardCode(model)}${effortMessage},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
@@ -4607,6 +5048,7 @@ export class ProxySessionController {
|
|
|
4607
5048
|
const created = await this.shellCommandJobs.createJob({ contextKey, sourceMessageId, command, cwd });
|
|
4608
5049
|
let cardMessageId;
|
|
4609
5050
|
const initialState = {
|
|
5051
|
+
terminalUrl: this.lifecycle?.terminalUrl?.(created.id),
|
|
4610
5052
|
jobId: created.id,
|
|
4611
5053
|
contextKey,
|
|
4612
5054
|
command,
|
|
@@ -4629,7 +5071,7 @@ export class ProxySessionController {
|
|
|
4629
5071
|
if (cardMessageId) {
|
|
4630
5072
|
try {
|
|
4631
5073
|
const failed = await this.shellCommandJobs.readJob(created.id);
|
|
4632
|
-
await this.outbound.updateInteractiveCard(contextKey, cardMessageId, this.cardRenderer.renderShellCommandCard(shellCommandJobCardView(failed)));
|
|
5074
|
+
await this.outbound.updateInteractiveCard(contextKey, cardMessageId, this.cardRenderer.renderShellCommandCard({ ...shellCommandJobCardView(failed), terminalUrl: this.lifecycle?.terminalUrl?.(failed.id) }));
|
|
4633
5075
|
await this.shellCommandJobs.markPresented(created.id);
|
|
4634
5076
|
}
|
|
4635
5077
|
catch (cardError) {
|
|
@@ -4680,7 +5122,7 @@ export class ProxySessionController {
|
|
|
4680
5122
|
if (!job.cardMessageId || this.shellCommandJobMonitors.has(job.id))
|
|
4681
5123
|
return;
|
|
4682
5124
|
const scheduler = new CardUpdateScheduler({
|
|
4683
|
-
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
5125
|
+
render: (state) => this.cardRenderer.renderShellCommandCard({ ...state, terminalUrl: this.lifecycle?.terminalUrl?.(job.id) }),
|
|
4684
5126
|
write: (card) => this.outbound.updateInteractiveCard(job.contextKey, job.cardMessageId, card),
|
|
4685
5127
|
onError: (error) => this.logger.warn({ error, jobId: job.id, contextKey: job.contextKey, messageId: job.cardMessageId }, "Failed to update a background shell command card."),
|
|
4686
5128
|
});
|
|
@@ -4829,16 +5271,25 @@ export class ProxySessionController {
|
|
|
4829
5271
|
});
|
|
4830
5272
|
}
|
|
4831
5273
|
async thinking(contextKey, effort, options = {}) {
|
|
5274
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
5275
|
+
return this.settingsOperations.run(record.localSessionId, () => this.thinkingNow(contextKey, effort, { ...options, sessionId: record.localSessionId }));
|
|
5276
|
+
}
|
|
5277
|
+
async thinkingNow(contextKey, effort, options = {}) {
|
|
4832
5278
|
if (!effort) {
|
|
4833
|
-
await this.
|
|
5279
|
+
await this.openExecutionSettingsNow(contextKey, "thinking", options);
|
|
4834
5280
|
return;
|
|
4835
5281
|
}
|
|
4836
5282
|
const record = options.sessionId
|
|
4837
5283
|
? this.requireSession(contextKey, options.sessionId)
|
|
4838
5284
|
: this.requireCurrentSession(contextKey);
|
|
4839
|
-
const loaded = await this.loadSession(record);
|
|
4840
|
-
const
|
|
4841
|
-
const
|
|
5285
|
+
const loaded = await this.loadSession(record, true);
|
|
5286
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
5287
|
+
const selectedSettings = pending?.settings ?? loaded.session;
|
|
5288
|
+
if (options.expectedProvider && options.expectedProvider !== selectedSettings.modelProvider) {
|
|
5289
|
+
throw new Error("Provider 已发生变化,请重新打开运行设置后选择。");
|
|
5290
|
+
}
|
|
5291
|
+
const models = await loaded.runtime.listModels(selectedSettings.modelProvider, selectedSettings.model);
|
|
5292
|
+
const currentModel = models.find((item) => item.id === selectedSettings.model)
|
|
4842
5293
|
?? models.find((item) => item.isDefault);
|
|
4843
5294
|
if (!currentModel)
|
|
4844
5295
|
throw new Error("当前运行时没有可配置思考强度的模型。");
|
|
@@ -4850,11 +5301,17 @@ export class ProxySessionController {
|
|
|
4850
5301
|
const options = supported.map((option) => option.value).join("、") || "无";
|
|
4851
5302
|
throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
|
|
4852
5303
|
}
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
5304
|
+
if (pending) {
|
|
5305
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, reasoningEffort: effort }, contextKey, options, "thinking");
|
|
5306
|
+
return;
|
|
5307
|
+
}
|
|
5308
|
+
await this.sessionHydrationOperations.run(loaded.record.localSessionId, async () => {
|
|
5309
|
+
await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, effort);
|
|
5310
|
+
this.store.updateRuntimeSession(loaded.record.localSessionId, { reasoningEffort: effort });
|
|
5311
|
+
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
5312
|
+
});
|
|
4856
5313
|
if (options.updateMessageId) {
|
|
4857
|
-
await this.
|
|
5314
|
+
await this.openExecutionSettingsNow(contextKey, "thinking", {
|
|
4858
5315
|
sessionId: loaded.record.localSessionId,
|
|
4859
5316
|
updateMessageId: options.updateMessageId,
|
|
4860
5317
|
notice: `思考强度已切换为 ${cardCode(effort)},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
@@ -4864,20 +5321,32 @@ export class ProxySessionController {
|
|
|
4864
5321
|
await this.outbound.sendText(contextKey, `思考强度已切换为 ${effort},并已保存为 ${loaded.record.agentName} Agent 的默认设置,从下一次请求生效。`);
|
|
4865
5322
|
}
|
|
4866
5323
|
async permissions(contextKey, mode, options = {}) {
|
|
5324
|
+
const record = options.sessionId ? this.requireSession(contextKey, options.sessionId) : this.requireCurrentSession(contextKey);
|
|
5325
|
+
return this.settingsOperations.run(record.localSessionId, () => this.permissionsNow(contextKey, mode, { ...options, sessionId: record.localSessionId }));
|
|
5326
|
+
}
|
|
5327
|
+
async permissionsNow(contextKey, mode, options = {}) {
|
|
4867
5328
|
if (!mode) {
|
|
4868
|
-
await this.
|
|
5329
|
+
await this.openExecutionSettingsNow(contextKey, "permission", options);
|
|
4869
5330
|
return;
|
|
4870
5331
|
}
|
|
4871
5332
|
const record = options.sessionId
|
|
4872
5333
|
? this.requireSession(contextKey, options.sessionId)
|
|
4873
5334
|
: this.requireCurrentSession(contextKey);
|
|
4874
|
-
const loaded = await this.loadSession(record);
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
5335
|
+
const loaded = await this.loadSession(record, true);
|
|
5336
|
+
const pending = this.store.getPendingProviderSettings(record.localSessionId);
|
|
5337
|
+
if (pending) {
|
|
5338
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, permissionMode: mode }, contextKey, options, "permission");
|
|
5339
|
+
return;
|
|
5340
|
+
}
|
|
5341
|
+
await this.sessionHydrationOperations.run(record.localSessionId, async () => {
|
|
5342
|
+
await loaded.runtime.setPermissionMode(record.localSessionId, mode);
|
|
5343
|
+
loaded.session = loaded.runtime.getSession(record.localSessionId) ?? loaded.session;
|
|
5344
|
+
loaded.session.permissionMode = mode;
|
|
5345
|
+
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: mode });
|
|
5346
|
+
await this.persistAgentExecutionDefaults(record.localSessionId);
|
|
5347
|
+
});
|
|
4879
5348
|
if (options.updateMessageId) {
|
|
4880
|
-
await this.
|
|
5349
|
+
await this.openExecutionSettingsNow(contextKey, "permission", {
|
|
4881
5350
|
sessionId: loaded.record.localSessionId,
|
|
4882
5351
|
updateMessageId: options.updateMessageId,
|
|
4883
5352
|
notice: mode === "auto"
|
|
@@ -4918,8 +5387,9 @@ export class ProxySessionController {
|
|
|
4918
5387
|
if (existing)
|
|
4919
5388
|
return existing;
|
|
4920
5389
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
4921
|
-
|
|
4922
|
-
|
|
5390
|
+
const previewTurnId = snapshot?.previewTurnId ?? turnId;
|
|
5391
|
+
if ((!snapshot || snapshot.previewJournal) && this.store.previews.has(previewTurnId))
|
|
5392
|
+
return this.store.previews.load(previewTurnId);
|
|
4923
5393
|
if (snapshot && !needsHistoricalDetails(snapshot))
|
|
4924
5394
|
return snapshot;
|
|
4925
5395
|
const origin = !snapshot ? this.store.getTurnRuntimeOrigin(turnId) : undefined;
|
|
@@ -4952,7 +5422,7 @@ export class ProxySessionController {
|
|
|
4952
5422
|
// An Agent Bot turn may have arrived while the read-only request was in flight.
|
|
4953
5423
|
const latest = turnViewSnapshot(this.store.getTurnSnapshot(origin.turnId));
|
|
4954
5424
|
if (latest)
|
|
4955
|
-
return latest.previewJournal ? this.store.previews.load(origin.turnId) ?? latest : latest;
|
|
5425
|
+
return latest.previewJournal ? this.store.previews.load(latest.previewTurnId ?? origin.turnId) ?? latest : latest;
|
|
4956
5426
|
const state = hydrateTurnViewState(createTurnViewState(origin.localSessionId, origin.turnId, details.startedAt ?? 0, remote?.title, undefined, remote?.cwd, undefined, this.agentLabel(origin.agentName)), details);
|
|
4957
5427
|
// Viewing external work must not create a task, a chat binding or a pending final reply.
|
|
4958
5428
|
this.store.previews.seed(state);
|
|
@@ -5489,20 +5959,29 @@ export class ProxySessionController {
|
|
|
5489
5959
|
const project = taskProjectInfo(entry);
|
|
5490
5960
|
const projectActionSource = projectActionSources.get(project.key) ?? entry;
|
|
5491
5961
|
const marker = entry.current ? "✅ " : entry.active ? "🟢 " : "";
|
|
5492
|
-
const
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5962
|
+
const stopAction = entry.active ? bindCardAction({
|
|
5963
|
+
text: "Stop",
|
|
5964
|
+
type: "danger",
|
|
5965
|
+
value: {
|
|
5966
|
+
action: "session_stop",
|
|
5967
|
+
sessionId: entry.reference,
|
|
5968
|
+
...(searchTerm ? { searchTerm } : {}),
|
|
5969
|
+
page: String(page),
|
|
5970
|
+
contextKey,
|
|
5971
|
+
},
|
|
5972
|
+
}) : undefined;
|
|
5973
|
+
const actions = [];
|
|
5974
|
+
if (!entry.current && entry.status !== "外部执行中")
|
|
5975
|
+
actions.push(bindCardAction({
|
|
5976
|
+
text: "Switch",
|
|
5498
5977
|
value: {
|
|
5499
|
-
action:
|
|
5978
|
+
action: "session_switch",
|
|
5500
5979
|
sessionId: entry.reference,
|
|
5501
5980
|
...(searchTerm ? { searchTerm } : {}),
|
|
5502
5981
|
page: String(page),
|
|
5503
5982
|
contextKey,
|
|
5504
5983
|
},
|
|
5505
|
-
})
|
|
5984
|
+
}));
|
|
5506
5985
|
actions.push(bindCardAction({
|
|
5507
5986
|
text: "SwitchGroup",
|
|
5508
5987
|
value: {
|
|
@@ -5568,16 +6047,17 @@ export class ProxySessionController {
|
|
|
5568
6047
|
contextKey,
|
|
5569
6048
|
},
|
|
5570
6049
|
}));
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
6050
|
+
if (!entry.active)
|
|
6051
|
+
actions.push(bindCardAction({
|
|
6052
|
+
text: "Archive",
|
|
6053
|
+
value: {
|
|
6054
|
+
action: "session_archive",
|
|
6055
|
+
sessionId: entry.reference,
|
|
6056
|
+
...(searchTerm ? { searchTerm } : {}),
|
|
6057
|
+
page: String(page),
|
|
6058
|
+
contextKey,
|
|
6059
|
+
},
|
|
6060
|
+
}));
|
|
5581
6061
|
const title = truncateText(entry.title.replace(/\s+/gu, " ").trim() || "未命名任务", 56);
|
|
5582
6062
|
const lastUserPrompt = sessionPromptPreview(entry.lastUserPrompt);
|
|
5583
6063
|
return {
|
|
@@ -5590,6 +6070,7 @@ export class ProxySessionController {
|
|
|
5590
6070
|
`**最新 Prompt**:${cardText(lastUserPrompt)}`,
|
|
5591
6071
|
`<font color='grey'>最近更新:${cardText(entry.updatedLabel)}</font>`,
|
|
5592
6072
|
],
|
|
6073
|
+
stopAction,
|
|
5593
6074
|
actions,
|
|
5594
6075
|
current: entry.current,
|
|
5595
6076
|
},
|
|
@@ -5616,7 +6097,7 @@ export class ProxySessionController {
|
|
|
5616
6097
|
`第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
|
|
5617
6098
|
"",
|
|
5618
6099
|
"> 项目菜单:**New** 新建任务,**NewGroup** 新建群。",
|
|
5619
|
-
"> 任务详情:**Switch**
|
|
6100
|
+
"> 任务详情:**Switch** 切换,运行中任务可用 **Stop** 发送停止信号,**Fork** / **ForkGroup** 创建分支,**Status** 查看状态,**Archive** 归档。",
|
|
5620
6101
|
], [
|
|
5621
6102
|
...(hasPrevious ? [bindCardAction({
|
|
5622
6103
|
text: "Previous",
|
|
@@ -5690,7 +6171,6 @@ export class ProxySessionController {
|
|
|
5690
6171
|
: undefined;
|
|
5691
6172
|
await this.listSessions(action.contextKey, searchTerm, {
|
|
5692
6173
|
updateMessageId: action.messageId,
|
|
5693
|
-
forceSwitchTaskId: options.forceSwitchTaskId,
|
|
5694
6174
|
page: options.page ?? parseSessionPage(action.value.page),
|
|
5695
6175
|
});
|
|
5696
6176
|
}
|
|
@@ -5845,7 +6325,7 @@ export class ProxySessionController {
|
|
|
5845
6325
|
const remoteReference = local
|
|
5846
6326
|
? remoteSessionReference(local.agentName, local.remoteSessionId)
|
|
5847
6327
|
: taskId;
|
|
5848
|
-
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference);
|
|
6328
|
+
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference, { view: "metadata" });
|
|
5849
6329
|
if (isRemoteSessionActive(remote)) {
|
|
5850
6330
|
throw new Error("任务正在执行,无法归档。请先停止任务或等待本轮完成。");
|
|
5851
6331
|
}
|
|
@@ -5853,7 +6333,11 @@ export class ProxySessionController {
|
|
|
5853
6333
|
throw new Error(`Agent ${agentName} 不支持归档任务。`);
|
|
5854
6334
|
}
|
|
5855
6335
|
const wasCurrent = Boolean(local && this.store.getUserContext(contextKey)?.currentSessionId === local.localSessionId);
|
|
5856
|
-
|
|
6336
|
+
const allowUnmaterialized = Boolean(local && !local.lastTurnId && !local.lastTurnStatus
|
|
6337
|
+
&& !this.store.hasTurnAttemptsForSession(local.localSessionId)
|
|
6338
|
+
&& !this.store.getForkHistorySource(local.localSessionId)
|
|
6339
|
+
&& this.store.listTaskTurnGraphIndex(local.localSessionId).length === 0);
|
|
6340
|
+
await runtime.archiveRemoteSession(remote.id, { allowUnmaterialized });
|
|
5857
6341
|
const title = local?.title ?? remote.title ?? remote.preview ?? remote.id;
|
|
5858
6342
|
if (local) {
|
|
5859
6343
|
this.store.archiveSession(local.localSessionId);
|
|
@@ -5991,6 +6475,7 @@ export class ProxySessionController {
|
|
|
5991
6475
|
}
|
|
5992
6476
|
const localActiveTurnId = runtime.getSession(record.localSessionId)?.activeTurnId;
|
|
5993
6477
|
const runtimeOwnsActiveTurn = Boolean(localActiveTurnId)
|
|
6478
|
+
&& (!this.settingsOnlySessions.has(record.localSessionId) || Boolean(activity.activeTurnId))
|
|
5994
6479
|
&& (!activity.activeTurnId || activity.activeTurnId === localActiveTurnId);
|
|
5995
6480
|
const persistedTurnMatches = record.status === "running"
|
|
5996
6481
|
&& record.lastTurnStatus === "running"
|
|
@@ -6337,6 +6822,12 @@ export class ProxySessionController {
|
|
|
6337
6822
|
return agent;
|
|
6338
6823
|
}
|
|
6339
6824
|
async persistAgentExecutionDefaults(localSessionId, candidate) {
|
|
6825
|
+
const record = this.store.getSession(localSessionId);
|
|
6826
|
+
if (!record)
|
|
6827
|
+
throw new Error(`找不到任务:${localSessionId}`);
|
|
6828
|
+
return this.agentDefaultsOperations.run(record.agentName, () => this.persistAgentExecutionDefaultsNow(localSessionId, candidate));
|
|
6829
|
+
}
|
|
6830
|
+
async persistAgentExecutionDefaultsNow(localSessionId, candidate) {
|
|
6340
6831
|
const record = this.store.getSession(localSessionId);
|
|
6341
6832
|
if (!record)
|
|
6342
6833
|
throw new Error(`找不到任务:${localSessionId}`);
|
|
@@ -7261,6 +7752,14 @@ function executionSettingsTabValue(value) {
|
|
|
7261
7752
|
return value;
|
|
7262
7753
|
throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
|
|
7263
7754
|
}
|
|
7755
|
+
function modelCardPageValue(value) {
|
|
7756
|
+
if (value === undefined)
|
|
7757
|
+
return undefined;
|
|
7758
|
+
const page = typeof value === "number" || (typeof value === "string" && value.trim()) ? Number(value) : NaN;
|
|
7759
|
+
if (!Number.isSafeInteger(page) || page < 0)
|
|
7760
|
+
throw new Error("模型卡片页码无效,请重新发送 /model。");
|
|
7761
|
+
return page;
|
|
7762
|
+
}
|
|
7264
7763
|
function resetHistoryPageValue(value) {
|
|
7265
7764
|
const page = Number(value);
|
|
7266
7765
|
if (!Number.isSafeInteger(page) || page < 0)
|
|
@@ -7637,10 +8136,17 @@ function defaultResourcePromptFromCounts(imageCount, fileCount) {
|
|
|
7637
8136
|
return fileCount === 1 ? "请查看这个文件" : "请查看这些文件";
|
|
7638
8137
|
return imageCount === 1 ? "请查看这张图片" : "请查看这些图片";
|
|
7639
8138
|
}
|
|
7640
|
-
function
|
|
8139
|
+
function isCommandMessage(message) {
|
|
8140
|
+
const text = normalizeFeishuPostText(message.text).trimStart();
|
|
8141
|
+
return text.startsWith("/") || text.startsWith("!");
|
|
8142
|
+
}
|
|
8143
|
+
function isForwardAttachmentSource(message) {
|
|
7641
8144
|
return !message.mergedForwardMessageId
|
|
7642
|
-
&& message.
|
|
7643
|
-
&&
|
|
8145
|
+
&& !message.parentMessageId
|
|
8146
|
+
&& !message.contextKey.startsWith("console:")
|
|
8147
|
+
&& !isCommandMessage(message)
|
|
8148
|
+
&& (message.messageType === "post"
|
|
8149
|
+
|| (message.images?.length ?? 0) > 0 || (message.files?.length ?? 0) > 0);
|
|
7644
8150
|
}
|
|
7645
8151
|
function deduplicateImageReferences(images) {
|
|
7646
8152
|
const seen = new Set();
|
|
@@ -7707,7 +8213,8 @@ function isRetryableLlmTurnFailure(message) {
|
|
|
7707
8213
|
if (!normalized)
|
|
7708
8214
|
return false;
|
|
7709
8215
|
const permanentFailure = [
|
|
7710
|
-
|
|
8216
|
+
/\b(?:auth|authenticat\w*|unauthenticat\w*|authoriz\w*|unauthorized|forbidden)\b|invalid api key|incorrect api key/u,
|
|
8217
|
+
/(?:^|\D)(?:401|403)(?:\D|$)/u,
|
|
7711
8218
|
/insufficient[_ -]?quota|quota (?:exceeded|exhausted)|usage limit|billing|credit balance/u,
|
|
7712
8219
|
/context (?:length|window)|maximum context|too many tokens|token limit/u,
|
|
7713
8220
|
/invalid request|bad request|invalid argument|malformed|unsupported|not supported/u,
|