@keyou007/agent-bot 0.1.24-alpha.5 → 0.1.24-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +27 -3
- package/README.zh.md +27 -3
- package/SECURITY.md +12 -0
- package/config.example.yaml +5 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/serverStatus.js +1 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexRuntime.js +202 -115
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +12 -13
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/config/AgentExecutionDefaultsStore.js +4 -0
- package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +3 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +66 -8
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +1 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +121 -17
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +43 -6
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +51 -4
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +7 -2
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +41 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +615 -214
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/proxy/SessionOperationQueue.js +14 -0
- package/dist/proxy/SessionOperationQueue.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +4 -2
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/state/StateStore.js +22 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +19 -7
- package/dist/state/TurnPreviewJournal.js.map +1 -1
- package/dist/state/migrations.js +6 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +1 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +19 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/terminal/TerminalCards.js +13 -0
- package/dist/terminal/TerminalCards.js.map +1 -0
- package/dist/terminal/TerminalLinks.js +76 -0
- package/dist/terminal/TerminalLinks.js.map +1 -0
- package/dist/terminal/TerminalPage.js +212 -0
- package/dist/terminal/TerminalPage.js.map +1 -0
- package/dist/terminal/TerminalProcess.js +71 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +639 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +48 -0
- package/docs/technical-reference.zh.md +48 -0
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +9 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/CodexRuntime.ts +200 -123
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/config/AgentExecutionDefaultsStore.ts +2 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +5 -1
- package/src/feishu/CardRenderer.ts +81 -8
- package/src/feishu/FeishuConnector.ts +1 -0
- package/src/feishu/FeishuTurnPresenter.ts +108 -16
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +44 -7
- package/src/local-files/LocalFileViewerServer.ts +48 -4
- package/src/local-files/TurnPreviewPage.ts +7 -2
- package/src/presentation/TurnStateReducer.ts +46 -7
- package/src/presentation/turnViewTypes.ts +5 -0
- package/src/proxy/ProxySessionController.ts +625 -211
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +8 -3
- package/src/state/StateStore.ts +38 -1
- package/src/state/TurnPreviewJournal.ts +19 -7
- package/src/state/migrations.ts +6 -0
- package/src/supervision/SafeRestartNotifier.ts +1 -0
- package/src/supervision/SafeRestartScheduler.ts +18 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +71 -0
- package/src/terminal/TerminalServer.ts +513 -0
|
@@ -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";
|
|
@@ -53,7 +54,7 @@ const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
|
53
54
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
54
55
|
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
55
56
|
const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
|
|
56
|
-
const FORWARD_ATTACHMENT_WINDOW_MS =
|
|
57
|
+
const FORWARD_ATTACHMENT_WINDOW_MS = 1_500;
|
|
57
58
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
58
59
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
59
60
|
const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
|
|
@@ -227,9 +228,14 @@ export class ProxySessionController {
|
|
|
227
228
|
messageQueues = new Map();
|
|
228
229
|
switchGroupOperations = new Set();
|
|
229
230
|
sessionLoads = new Map();
|
|
231
|
+
settingsOnlySessions = new Set();
|
|
230
232
|
queuedPromptStarts = new Map();
|
|
231
233
|
queuedPromptCards = new Map();
|
|
232
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();
|
|
233
239
|
sessionResets = new Map();
|
|
234
240
|
resetHistoryOperations = new Map();
|
|
235
241
|
remoteTurnHistoryRefreshes = new Map();
|
|
@@ -239,6 +245,7 @@ export class ProxySessionController {
|
|
|
239
245
|
lastSessionListings = new Map();
|
|
240
246
|
threadInitializations = new Map();
|
|
241
247
|
llmRetryingSessions = new Set();
|
|
248
|
+
planHandoffs = new Map();
|
|
242
249
|
retriedFailureTurnIds = new Set();
|
|
243
250
|
queuedPromptRetryTimers = new Map();
|
|
244
251
|
recoveryRetryTimers = new Map();
|
|
@@ -304,7 +311,7 @@ export class ProxySessionController {
|
|
|
304
311
|
const pendingMergedForward = message.mergedForwardMessageId
|
|
305
312
|
? this.registerPendingForwardAttachment(message, this.pendingMergedForwards)
|
|
306
313
|
: undefined;
|
|
307
|
-
const pendingResourceForward = !attachmentReservation &&
|
|
314
|
+
const pendingResourceForward = !attachmentReservation && isForwardAttachmentSource(message)
|
|
308
315
|
? this.registerPendingForwardAttachment(message, this.pendingResourceForwards)
|
|
309
316
|
: undefined;
|
|
310
317
|
try {
|
|
@@ -342,7 +349,9 @@ export class ProxySessionController {
|
|
|
342
349
|
const attachment = await this.waitForForwardAttachment(message.messageId, pendingResourceForward, this.pendingResourceForwards);
|
|
343
350
|
if (attachment) {
|
|
344
351
|
coalescedForwardAttachment = true;
|
|
345
|
-
const
|
|
352
|
+
const sourceText = resourceMessage.text.trim();
|
|
353
|
+
const instruction = normalizeFeishuPostText(attachment.text)
|
|
354
|
+
|| (sourceText ? DEFAULT_REFERENCED_MESSAGE_INSTRUCTION : defaultResourcePrompt(resourceMessage));
|
|
346
355
|
forwardedImages = (resourceMessage.images ?? []).map((image) => ({
|
|
347
356
|
messageId: resourceMessage.messageId,
|
|
348
357
|
imageKey: image.imageKey,
|
|
@@ -354,7 +363,7 @@ export class ProxySessionController {
|
|
|
354
363
|
}));
|
|
355
364
|
message = {
|
|
356
365
|
...attachment,
|
|
357
|
-
text: instruction,
|
|
366
|
+
text: sourceText ? referencedMessagePrompt(instruction, sourceText) : instruction,
|
|
358
367
|
displayText: instruction,
|
|
359
368
|
};
|
|
360
369
|
this.relatedReactionMessageIds.set(message.messageId, [resourceMessage.messageId]);
|
|
@@ -623,7 +632,12 @@ export class ProxySessionController {
|
|
|
623
632
|
await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
|
|
624
633
|
try {
|
|
625
634
|
const kind = String(scopedAction.value.action ?? "");
|
|
626
|
-
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") {
|
|
627
641
|
if (!this.lifecycle?.cancelAutomaticUpdate)
|
|
628
642
|
throw new Error("自动更新功能未启用。");
|
|
629
643
|
await this.lifecycle.cancelAutomaticUpdate(scopedAction);
|
|
@@ -842,9 +856,11 @@ export class ProxySessionController {
|
|
|
842
856
|
await this.refreshStatusCardFromAction(scopedAction);
|
|
843
857
|
}
|
|
844
858
|
else if (kind === "settings_tab_open") {
|
|
859
|
+
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
845
860
|
await this.openExecutionSettings(contextKey, executionSettingsTabValue(scopedAction.value.tab), {
|
|
846
|
-
sessionId:
|
|
847
|
-
|
|
861
|
+
sessionId, updateMessageId: scopedAction.messageId,
|
|
862
|
+
modelPage: modelCardPageValue(scopedAction.value.page),
|
|
863
|
+
expectedProvider: typeof scopedAction.value.provider === "string" ? scopedAction.value.provider : undefined,
|
|
848
864
|
});
|
|
849
865
|
}
|
|
850
866
|
else if (kind === "settings_agent_select") {
|
|
@@ -861,12 +877,14 @@ export class ProxySessionController {
|
|
|
861
877
|
}
|
|
862
878
|
else if (kind === "settings_model_select") {
|
|
863
879
|
await this.model(contextKey, String(scopedAction.value.model ?? ""), {
|
|
880
|
+
expectedProvider: String(scopedAction.value.provider ?? ""),
|
|
864
881
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
865
882
|
updateMessageId: scopedAction.messageId,
|
|
866
883
|
});
|
|
867
884
|
}
|
|
868
885
|
else if (kind === "settings_thinking_select") {
|
|
869
886
|
await this.thinking(contextKey, String(scopedAction.value.effort ?? ""), {
|
|
887
|
+
expectedProvider: String(scopedAction.value.provider ?? ""),
|
|
870
888
|
sessionId: String(scopedAction.value.sessionId ?? ""),
|
|
871
889
|
updateMessageId: scopedAction.messageId,
|
|
872
890
|
expectedModel: String(scopedAction.value.model ?? ""),
|
|
@@ -891,6 +909,7 @@ export class ProxySessionController {
|
|
|
891
909
|
await this.model(contextKey, undefined, {
|
|
892
910
|
sessionId,
|
|
893
911
|
updateMessageId: scopedAction.messageId,
|
|
912
|
+
modelPage: modelCardPageValue(scopedAction.value.page),
|
|
894
913
|
});
|
|
895
914
|
}
|
|
896
915
|
else if (kind === "reasoning_select") {
|
|
@@ -910,7 +929,7 @@ export class ProxySessionController {
|
|
|
910
929
|
});
|
|
911
930
|
}
|
|
912
931
|
else if (kind === "provider_select" || kind === "provider_model_open") {
|
|
913
|
-
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));
|
|
914
933
|
}
|
|
915
934
|
else if (kind === "provider_model_select" || kind === "provider_reasoning_open") {
|
|
916
935
|
await this.openProviderReasoningSelector(contextKey, String(scopedAction.value.provider ?? ""), String(scopedAction.value.model ?? ""), permissionModeValue(scopedAction.value.permissionMode), String(scopedAction.value.sessionId ?? ""), requiredCardMessageId(scopedAction.messageId));
|
|
@@ -965,6 +984,7 @@ export class ProxySessionController {
|
|
|
965
984
|
this.queuedPromptCards.clear();
|
|
966
985
|
this.queuedPromptCardWrites.clear();
|
|
967
986
|
this.sessionResets.clear();
|
|
987
|
+
this.planHandoffs.clear();
|
|
968
988
|
this.resetHistoryOperations.clear();
|
|
969
989
|
this.remoteTurnHistoryRefreshes.clear();
|
|
970
990
|
this.forkHistoryCursors.clear();
|
|
@@ -1270,78 +1290,43 @@ export class ProxySessionController {
|
|
|
1270
1290
|
if (setting && !value?.trim())
|
|
1271
1291
|
throw new Error(`task ${setting} requires a value.`);
|
|
1272
1292
|
const nextValue = value?.trim();
|
|
1273
|
-
if (setting
|
|
1293
|
+
if (setting && nextValue) {
|
|
1274
1294
|
const contextKey = this.controlSessionContextKey(record);
|
|
1275
|
-
await this.outbound.withReplyTarget(contextKey, this.controlSessionReplyTarget(record), () =>
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
reasoningEffort: effort,
|
|
1289
|
-
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
|
+
}
|
|
1290
1308
|
});
|
|
1291
1309
|
}
|
|
1292
|
-
|
|
1293
|
-
const
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
const
|
|
1298
|
-
const
|
|
1299
|
-
|| selected.supportedReasoningEfforts.some((candidate) => candidate.value === currentEffort))
|
|
1300
|
-
? currentEffort
|
|
1301
|
-
: selected.defaultReasoningEffort;
|
|
1302
|
-
await loaded.runtime.setModel(record.localSessionId, nextValue);
|
|
1303
|
-
if (nextEffort && nextEffort !== currentEffort) {
|
|
1304
|
-
await loaded.runtime.setReasoningEffort(record.localSessionId, nextEffort);
|
|
1305
|
-
}
|
|
1306
|
-
this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
|
|
1307
|
-
this.outbound.updateSessionModel(record.localSessionId, nextValue, loaded.session.modelProvider);
|
|
1308
|
-
}
|
|
1309
|
-
else if (setting === "thinking" && nextValue) {
|
|
1310
|
-
const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
|
|
1311
|
-
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)
|
|
1312
1317
|
?? models.find((candidate) => candidate.isDefault);
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
await loaded.runtime.setPermissionMode(record.localSessionId, nextValue);
|
|
1327
|
-
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
|
|
1328
|
-
}
|
|
1329
|
-
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1330
|
-
if (setting && setting !== "provider")
|
|
1331
|
-
await this.persistAgentExecutionDefaults(session.localSessionId);
|
|
1332
|
-
const models = await loaded.runtime.listModels(session.modelProvider, session.model);
|
|
1333
|
-
const currentModel = models.find((candidate) => candidate.id === session.model)
|
|
1334
|
-
?? models.find((candidate) => candidate.isDefault);
|
|
1335
|
-
const providers = loaded.runtime.listModelProviders
|
|
1336
|
-
? await this.modelProviderOptions(loaded)
|
|
1337
|
-
: [];
|
|
1338
|
-
return {
|
|
1339
|
-
session,
|
|
1340
|
-
providers,
|
|
1341
|
-
models,
|
|
1342
|
-
reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
|
|
1343
|
-
permissionModes: ["auto", "confirm"],
|
|
1344
|
-
};
|
|
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
|
+
});
|
|
1345
1330
|
}
|
|
1346
1331
|
async controlTaskGoal(localSessionId, action, objective) {
|
|
1347
1332
|
const record = this.requireControlSession(localSessionId);
|
|
@@ -1746,6 +1731,9 @@ export class ProxySessionController {
|
|
|
1746
1731
|
this.recoveryRetryStartedAt.delete(attempt.attemptId);
|
|
1747
1732
|
}
|
|
1748
1733
|
}
|
|
1734
|
+
for (const pending of this.store.listPendingProviderSettings()) {
|
|
1735
|
+
await this.applyPendingProviderWhenIdle(pending.localSessionId);
|
|
1736
|
+
}
|
|
1749
1737
|
this.restorePersistedQueuedPrompts();
|
|
1750
1738
|
}
|
|
1751
1739
|
async finishPersistedCancellationRequests() {
|
|
@@ -2051,6 +2039,17 @@ export class ProxySessionController {
|
|
|
2051
2039
|
const pendingAttempt = this.store.getTurnAttempt(attempt.attemptId);
|
|
2052
2040
|
if (!pendingAttempt || !isIncompleteTurnAttemptStatus(pendingAttempt.status))
|
|
2053
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
|
+
}
|
|
2054
2053
|
const oldCardTurnId = attempt.pendingTurnId ?? attempt.turnId;
|
|
2055
2054
|
if (oldCardTurnId) {
|
|
2056
2055
|
await this.outbound.interruptTurnForRecovery(session.localSessionId, attempt.contextKey, oldCardTurnId, "Agent Bot 已重启,执行仍在继续;进度已转移到新的思考卡片。");
|
|
@@ -2091,7 +2090,7 @@ export class ProxySessionController {
|
|
|
2091
2090
|
});
|
|
2092
2091
|
}
|
|
2093
2092
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
2094
|
-
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)));
|
|
2095
2094
|
const recoveryText = recoveryRuntimePrompt(attempt.promptText);
|
|
2096
2095
|
const turnId = await this.startTurn(loaded, recoveryText, replyTarget, attempt.localImagePaths, {
|
|
2097
2096
|
attemptId: attempt.attemptId,
|
|
@@ -2130,7 +2129,7 @@ export class ProxySessionController {
|
|
|
2130
2129
|
await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt);
|
|
2131
2130
|
}
|
|
2132
2131
|
async promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt) {
|
|
2133
|
-
if (this.llmRetryingSessions.has(record.localSessionId)) {
|
|
2132
|
+
if (this.llmRetryingSessions.has(record.localSessionId) || this.planHandoffs.has(record.localSessionId)) {
|
|
2134
2133
|
const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
|
|
2135
2134
|
localImagePaths,
|
|
2136
2135
|
messageId,
|
|
@@ -2151,7 +2150,7 @@ export class ProxySessionController {
|
|
|
2151
2150
|
}
|
|
2152
2151
|
let loaded;
|
|
2153
2152
|
try {
|
|
2154
|
-
loaded = await this.loadSession(record);
|
|
2153
|
+
loaded = await this.loadSession(record, Boolean(this.store.getPendingProviderSettings(record.localSessionId)));
|
|
2155
2154
|
}
|
|
2156
2155
|
catch (error) {
|
|
2157
2156
|
if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
|
|
@@ -3127,6 +3126,22 @@ export class ProxySessionController {
|
|
|
3127
3126
|
};
|
|
3128
3127
|
}
|
|
3129
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 = {}) {
|
|
3130
3145
|
const currentRecord = this.store.getSession(loaded.record.localSessionId);
|
|
3131
3146
|
const title = currentRecord?.title ?? normalizeTaskTitle(options.displayPrompt ?? text);
|
|
3132
3147
|
if (!currentRecord?.title && title)
|
|
@@ -3172,7 +3187,13 @@ export class ProxySessionController {
|
|
|
3172
3187
|
}
|
|
3173
3188
|
this.store.updateTurnAttempt(attemptId, { pendingTurnId: pendingTurnId ?? null });
|
|
3174
3189
|
try {
|
|
3175
|
-
|
|
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));
|
|
3176
3197
|
break;
|
|
3177
3198
|
}
|
|
3178
3199
|
catch (error) {
|
|
@@ -3587,22 +3608,44 @@ export class ProxySessionController {
|
|
|
3587
3608
|
this.store.setBoundProjectCwd(groupContextKey, boundProjectCwd);
|
|
3588
3609
|
return { chatId: group.chatId, contextKey: groupContextKey, name: group.name };
|
|
3589
3610
|
}
|
|
3590
|
-
async loadSession(record) {
|
|
3611
|
+
async loadSession(record, forSettings = false) {
|
|
3591
3612
|
const agent = this.ensureAgent(record.agentName);
|
|
3592
3613
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
3593
3614
|
const existing = runtime.getSession(record.localSessionId);
|
|
3594
|
-
if (existing)
|
|
3615
|
+
if (existing && (forSettings || !this.settingsOnlySessions.has(record.localSessionId))) {
|
|
3595
3616
|
return { record, runtime, session: existing };
|
|
3617
|
+
}
|
|
3596
3618
|
const pending = this.sessionLoads.get(record.localSessionId);
|
|
3597
3619
|
if (pending) {
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
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
|
+
}
|
|
3603
3636
|
}
|
|
3604
3637
|
this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName), record.model, record.modelProvider);
|
|
3605
|
-
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
|
+
}
|
|
3606
3649
|
if (record.lastTurnId) {
|
|
3607
3650
|
try {
|
|
3608
3651
|
await this.outbound.resumeDelivery(record.localSessionId, record.contextKey, record.lastTurnId);
|
|
@@ -3612,45 +3655,32 @@ export class ProxySessionController {
|
|
|
3612
3655
|
}
|
|
3613
3656
|
}
|
|
3614
3657
|
const permissionMode = record.permissionMode ?? "auto";
|
|
3615
|
-
|
|
3658
|
+
const prepare = forSettings && record.remoteSessionId && runtime.prepareSessionForSettings;
|
|
3659
|
+
if (record.remoteSessionId && !prepare)
|
|
3616
3660
|
await this.assertSessionTurnOwnership(record, runtime);
|
|
3617
3661
|
let session;
|
|
3618
3662
|
if (record.remoteSessionId) {
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
if (!(agent.kind === "app-server" && !record.lastTurnId
|
|
3641
|
-
&& !this.store.getForkHistorySource(record.localSessionId) && isMissingRolloutError(error)))
|
|
3642
|
-
throw error;
|
|
3643
|
-
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
3644
|
-
session = await runtime.createSession({
|
|
3645
|
-
localSessionId: record.localSessionId,
|
|
3646
|
-
agentName: record.agentName,
|
|
3647
|
-
cwd: record.cwd,
|
|
3648
|
-
title: record.title,
|
|
3649
|
-
modelProvider: record.modelProvider,
|
|
3650
|
-
model: record.model,
|
|
3651
|
-
reasoningEffort: record.reasoningEffort,
|
|
3652
|
-
permissionMode,
|
|
3653
|
-
});
|
|
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,不允许替换任务或更改历史。");
|
|
3654
3684
|
}
|
|
3655
3685
|
}
|
|
3656
3686
|
else {
|
|
@@ -3665,20 +3695,24 @@ export class ProxySessionController {
|
|
|
3665
3695
|
permissionMode,
|
|
3666
3696
|
});
|
|
3667
3697
|
}
|
|
3668
|
-
|
|
3698
|
+
if (!prepare) {
|
|
3699
|
+
this.settingsOnlySessions.delete(record.localSessionId);
|
|
3700
|
+
this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
|
|
3701
|
+
}
|
|
3669
3702
|
const saved = this.store.getSession(record.localSessionId) ?? record;
|
|
3670
3703
|
return {
|
|
3671
3704
|
record: { ...saved, contextKey: record.contextKey },
|
|
3672
3705
|
runtime,
|
|
3673
3706
|
session,
|
|
3674
3707
|
};
|
|
3675
|
-
})
|
|
3708
|
+
});
|
|
3676
3709
|
this.sessionLoads.set(record.localSessionId, loading);
|
|
3677
3710
|
try {
|
|
3678
3711
|
return await loading;
|
|
3679
3712
|
}
|
|
3680
3713
|
finally {
|
|
3681
|
-
this.sessionLoads.
|
|
3714
|
+
if (this.sessionLoads.get(record.localSessionId) === loading)
|
|
3715
|
+
this.sessionLoads.delete(record.localSessionId);
|
|
3682
3716
|
}
|
|
3683
3717
|
}
|
|
3684
3718
|
persistRuntimeSession(record, session, status) {
|
|
@@ -3716,6 +3750,15 @@ export class ProxySessionController {
|
|
|
3716
3750
|
this.store.saveTurnParent(event.turnId, event.sessionId, this.completedParentTurnId(source));
|
|
3717
3751
|
}
|
|
3718
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
|
+
}
|
|
3719
3762
|
const failedAttempt = event.type === "turn_failed"
|
|
3720
3763
|
? this.store.findIncompleteTurnAttemptByTurnId(event.turnId)
|
|
3721
3764
|
: undefined;
|
|
@@ -3759,7 +3802,7 @@ export class ProxySessionController {
|
|
|
3759
3802
|
...event,
|
|
3760
3803
|
turnCount: event.turnCount ?? this.store.countTaskTurns(event.sessionId),
|
|
3761
3804
|
}
|
|
3762
|
-
: event;
|
|
3805
|
+
: event.type === "turn_completed" && event.approvedPlan ? { ...event, approvedPlan: false } : event;
|
|
3763
3806
|
const presentationEvent = event.type === "turn_failed"
|
|
3764
3807
|
&& isRetryableLlmTurnFailure(event.message)
|
|
3765
3808
|
&& (failedAttempt?.retryCount ?? 0) >= MAX_LLM_TURN_RETRIES
|
|
@@ -3794,12 +3837,87 @@ export class ProxySessionController {
|
|
|
3794
3837
|
: undefined;
|
|
3795
3838
|
if (latest?.lastTurnId === event.turnId && !activeTurnId) {
|
|
3796
3839
|
this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
|
|
3840
|
+
await this.applyPendingProviderWhenIdle(event.sessionId);
|
|
3797
3841
|
await this.scheduleNextQueuedPrompt(event.sessionId);
|
|
3798
3842
|
}
|
|
3799
3843
|
}
|
|
3800
3844
|
if (presentationError)
|
|
3801
3845
|
throw presentationError;
|
|
3802
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
|
+
}
|
|
3803
3921
|
async retryFailedLlmTurn(event, attempt) {
|
|
3804
3922
|
const retryNumber = attempt.retryCount;
|
|
3805
3923
|
const session = this.store.getSession(event.sessionId);
|
|
@@ -3833,7 +3951,7 @@ export class ProxySessionController {
|
|
|
3833
3951
|
: undefined;
|
|
3834
3952
|
try {
|
|
3835
3953
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
3836
|
-
const loaded = await this.loadSession(current);
|
|
3954
|
+
const loaded = await this.loadSession(current, Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
3837
3955
|
const retryTurnId = await this.startTurn(loaded, llmRetryRuntimePrompt(attempt.promptText, retryNumber), replyTarget, attempt.localImagePaths, {
|
|
3838
3956
|
attemptId: attempt.attemptId,
|
|
3839
3957
|
messageId: attempt.messageId,
|
|
@@ -3870,6 +3988,8 @@ export class ProxySessionController {
|
|
|
3870
3988
|
});
|
|
3871
3989
|
}
|
|
3872
3990
|
scheduleQueuedPromptRetry(sessionId) {
|
|
3991
|
+
if (this.store.getPendingProviderSettings(sessionId)?.error)
|
|
3992
|
+
return;
|
|
3873
3993
|
if (this.queuedPromptRetryTimers.has(sessionId))
|
|
3874
3994
|
return;
|
|
3875
3995
|
if (this.store.countQueuedPrompts(sessionId) === 0)
|
|
@@ -3885,6 +4005,11 @@ export class ProxySessionController {
|
|
|
3885
4005
|
this.queuedPromptRetryTimers.set(sessionId, timer);
|
|
3886
4006
|
}
|
|
3887
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;
|
|
3888
4013
|
if (this.store.countQueuedPrompts(sessionId) === 0)
|
|
3889
4014
|
return;
|
|
3890
4015
|
const baseRecord = this.store.getSession(sessionId);
|
|
@@ -3892,20 +4017,23 @@ export class ProxySessionController {
|
|
|
3892
4017
|
return;
|
|
3893
4018
|
let prompt;
|
|
3894
4019
|
try {
|
|
3895
|
-
let loaded = await this.loadSession(baseRecord);
|
|
4020
|
+
let loaded = await this.loadSession(baseRecord, Boolean(this.store.getPendingProviderSettings(sessionId)));
|
|
3896
4021
|
const remoteActivity = await this.assertSessionTurnOwnership(loaded.record, loaded.runtime);
|
|
3897
4022
|
if (needsFullSessionSynchronization(loaded.record, loaded.session, remoteActivity)) {
|
|
3898
4023
|
loaded = { ...loaded, session: await loaded.runtime.synchronizeSession(sessionId) };
|
|
3899
4024
|
}
|
|
3900
4025
|
if (loaded.session.activeTurnId)
|
|
3901
4026
|
return;
|
|
4027
|
+
await this.applyPendingProviderSettings(loaded);
|
|
4028
|
+
if (this.store.getPendingProviderSettings(sessionId))
|
|
4029
|
+
return;
|
|
3902
4030
|
prompt = this.store.takeNextQueuedPrompt(sessionId);
|
|
3903
4031
|
if (!prompt)
|
|
3904
4032
|
return;
|
|
3905
4033
|
const record = this.store.getSessionForContext(sessionId, prompt.contextKey) ?? baseRecord;
|
|
3906
4034
|
loaded = { ...loaded, record };
|
|
3907
4035
|
await this.refreshPromptQueueCards(sessionId);
|
|
3908
|
-
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 });
|
|
3909
4037
|
if (prompt.messageId)
|
|
3910
4038
|
await this.bindMessageReactionToTurn(prompt.messageId, sessionId, turnId);
|
|
3911
4039
|
}
|
|
@@ -3964,6 +4092,7 @@ export class ProxySessionController {
|
|
|
3964
4092
|
});
|
|
3965
4093
|
const pending = {
|
|
3966
4094
|
contextKey: message.contextKey,
|
|
4095
|
+
userId: message.userId,
|
|
3967
4096
|
attachmentPromise,
|
|
3968
4097
|
resolveAttachment,
|
|
3969
4098
|
};
|
|
@@ -3971,7 +4100,7 @@ export class ProxySessionController {
|
|
|
3971
4100
|
return pending;
|
|
3972
4101
|
}
|
|
3973
4102
|
reserveForwardAttachment(message) {
|
|
3974
|
-
if (!message.parentMessageId)
|
|
4103
|
+
if (!message.parentMessageId || isCommandMessage(message))
|
|
3975
4104
|
return undefined;
|
|
3976
4105
|
const candidates = [
|
|
3977
4106
|
{ sourceKind: "merged_forward", registry: this.pendingMergedForwards },
|
|
@@ -3979,7 +4108,8 @@ export class ProxySessionController {
|
|
|
3979
4108
|
];
|
|
3980
4109
|
for (const candidate of candidates) {
|
|
3981
4110
|
const pending = candidate.registry.get(message.parentMessageId);
|
|
3982
|
-
if (!pending || pending.contextKey !== message.contextKey
|
|
4111
|
+
if (!pending || pending.contextKey !== message.contextKey
|
|
4112
|
+
|| pending.userId !== message.userId || pending.attachmentMessageId)
|
|
3983
4113
|
continue;
|
|
3984
4114
|
pending.attachmentMessageId = message.messageId;
|
|
3985
4115
|
return {
|
|
@@ -4004,17 +4134,20 @@ export class ProxySessionController {
|
|
|
4004
4134
|
return true;
|
|
4005
4135
|
}
|
|
4006
4136
|
async waitForForwardAttachment(messageId, pending, registry) {
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
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?.();
|
|
4012
4146
|
try {
|
|
4013
|
-
return await
|
|
4147
|
+
return await pending.attachmentPromise;
|
|
4014
4148
|
}
|
|
4015
4149
|
finally {
|
|
4016
|
-
|
|
4017
|
-
clearTimeout(timer);
|
|
4150
|
+
clearTimeout(timer);
|
|
4018
4151
|
if (registry.get(messageId) === pending) {
|
|
4019
4152
|
registry.delete(messageId);
|
|
4020
4153
|
}
|
|
@@ -4075,6 +4208,7 @@ export class ProxySessionController {
|
|
|
4075
4208
|
await this.cancelSession(record);
|
|
4076
4209
|
}
|
|
4077
4210
|
async cancelSession(record) {
|
|
4211
|
+
this.planHandoffs.delete(record.localSessionId);
|
|
4078
4212
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
4079
4213
|
if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
|
|
4080
4214
|
if (runtime.getGoal && runtime.setGoal) {
|
|
@@ -4151,6 +4285,7 @@ export class ProxySessionController {
|
|
|
4151
4285
|
});
|
|
4152
4286
|
}
|
|
4153
4287
|
async runSessionReset(sessionId, operation) {
|
|
4288
|
+
this.planHandoffs.delete(sessionId);
|
|
4154
4289
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
4155
4290
|
const reset = previous.catch(() => undefined).then(operation);
|
|
4156
4291
|
this.sessionResets.set(sessionId, reset);
|
|
@@ -4295,6 +4430,14 @@ export class ProxySessionController {
|
|
|
4295
4430
|
await loaded.runtime.respondToApproval(sessionId, requestId, decision);
|
|
4296
4431
|
}
|
|
4297
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 = {}) {
|
|
4298
4441
|
const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
|
|
4299
4442
|
const record = options.sessionId
|
|
4300
4443
|
? this.requireSession(contextKey, options.sessionId)
|
|
@@ -4313,22 +4456,31 @@ export class ProxySessionController {
|
|
|
4313
4456
|
reasoningOptions: [],
|
|
4314
4457
|
};
|
|
4315
4458
|
if (record) {
|
|
4316
|
-
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
|
+
}
|
|
4317
4467
|
const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
|
|
4318
4468
|
const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
|
|
4319
|
-
const currentProvider =
|
|
4320
|
-
const { models, warning } = await this.modelOptions(loaded.runtime, currentProvider,
|
|
4321
|
-
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)
|
|
4322
4472
|
?? models.find((model) => model.isDefault)
|
|
4323
4473
|
?? models[0];
|
|
4324
|
-
const currentEffort = currentModel?.supportedReasoningEfforts.some((option) => option.value ===
|
|
4325
|
-
?
|
|
4326
|
-
: currentModel?.defaultReasoningEffort ??
|
|
4474
|
+
const currentEffort = currentModel?.supportedReasoningEfforts.some((option) => option.value === selected.reasoningEffort)
|
|
4475
|
+
? selected.reasoningEffort
|
|
4476
|
+
: currentModel?.defaultReasoningEffort ?? selected.reasoningEffort;
|
|
4327
4477
|
runtimeSettings = {
|
|
4478
|
+
pendingNotice: pending ? this.pendingProviderNotice(pending) : undefined,
|
|
4479
|
+
pendingFailed: Boolean(pending?.error),
|
|
4328
4480
|
currentProvider,
|
|
4329
|
-
currentModel: currentModel?.id ??
|
|
4481
|
+
currentModel: pending?.settings.model ?? currentModel?.id ?? selected.model,
|
|
4330
4482
|
currentEffort,
|
|
4331
|
-
currentPermissionMode:
|
|
4483
|
+
currentPermissionMode: selected.permissionMode,
|
|
4332
4484
|
providers,
|
|
4333
4485
|
providerSupported,
|
|
4334
4486
|
models,
|
|
@@ -4344,7 +4496,10 @@ export class ProxySessionController {
|
|
|
4344
4496
|
agents,
|
|
4345
4497
|
runtimeSettingsAvailable: Boolean(record),
|
|
4346
4498
|
...runtimeSettings,
|
|
4499
|
+
modelPage: options.expectedProvider !== undefined && options.expectedProvider !== (runtimeSettings.currentProvider ?? "")
|
|
4500
|
+
? undefined : options.modelPage,
|
|
4347
4501
|
notice: options.notice,
|
|
4502
|
+
warning: [runtimeSettings.warning, options.warning].filter(Boolean).join("\n") || undefined,
|
|
4348
4503
|
});
|
|
4349
4504
|
if (options.updateMessageId) {
|
|
4350
4505
|
await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
|
|
@@ -4362,35 +4517,40 @@ export class ProxySessionController {
|
|
|
4362
4517
|
await this.openExecutionSettings(contextKey, "agent");
|
|
4363
4518
|
}
|
|
4364
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 = {}) {
|
|
4365
4524
|
const record = options.sessionId
|
|
4366
4525
|
? this.requireSession(contextKey, options.sessionId)
|
|
4367
4526
|
: this.requireCurrentSession(contextKey);
|
|
4368
|
-
await this.
|
|
4369
|
-
const
|
|
4527
|
+
const loaded = await this.loadSession(record, true);
|
|
4528
|
+
const selected = this.store.getPendingProviderSettings(record.localSessionId)?.settings ?? loaded.session;
|
|
4370
4529
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4371
|
-
const models = await loaded.runtime.listModels(modelProvider,
|
|
4372
|
-
const model = selectProviderModel(models,
|
|
4530
|
+
const models = await loaded.runtime.listModels(modelProvider, selected.model);
|
|
4531
|
+
const model = selectProviderModel(models, selected.model);
|
|
4373
4532
|
if (!model)
|
|
4374
4533
|
throw new Error("当前运行时没有可用于 Provider 切换的模型。");
|
|
4375
|
-
const effort = selectProviderReasoningEffort(model,
|
|
4376
|
-
await this.
|
|
4534
|
+
const effort = selectProviderReasoningEffort(model, selected.reasoningEffort);
|
|
4535
|
+
await this.applyProviderSettingsNow(contextKey, {
|
|
4377
4536
|
provider: modelProvider,
|
|
4378
4537
|
model: model.id,
|
|
4379
4538
|
effort,
|
|
4380
|
-
mode:
|
|
4381
|
-
}, options
|
|
4539
|
+
mode: selected.permissionMode,
|
|
4540
|
+
}, options);
|
|
4382
4541
|
}
|
|
4383
4542
|
async openProviderSelector(contextKey, options = {}) {
|
|
4384
4543
|
await this.openExecutionSettings(contextKey, "provider", options);
|
|
4385
4544
|
}
|
|
4386
|
-
async openProviderModelSelector(contextKey, modelProvider, permissionMode, sessionId, updateMessageId) {
|
|
4387
|
-
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);
|
|
4388
4547
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4389
4548
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, loaded.session.model);
|
|
4390
4549
|
const card = this.cardRenderer.renderModelSelector({
|
|
4391
4550
|
sessionId,
|
|
4392
4551
|
contextKey,
|
|
4393
4552
|
currentModel: selectProviderModel(models, loaded.session.model)?.id,
|
|
4553
|
+
modelPage,
|
|
4394
4554
|
reasoningEffort: loaded.session.reasoningEffort,
|
|
4395
4555
|
models,
|
|
4396
4556
|
warning,
|
|
@@ -4401,7 +4561,7 @@ export class ProxySessionController {
|
|
|
4401
4561
|
await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
|
|
4402
4562
|
}
|
|
4403
4563
|
async openProviderReasoningSelector(contextKey, modelProvider, model, permissionMode, sessionId, updateMessageId) {
|
|
4404
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
4564
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
4405
4565
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4406
4566
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
|
|
4407
4567
|
const selected = models.find((candidate) => candidate.id === model);
|
|
@@ -4427,7 +4587,7 @@ export class ProxySessionController {
|
|
|
4427
4587
|
await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
|
|
4428
4588
|
}
|
|
4429
4589
|
async openProviderPermissionSelector(contextKey, modelProvider, model, effort, permissionMode, sessionId, updateMessageId) {
|
|
4430
|
-
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
4590
|
+
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId), true);
|
|
4431
4591
|
await this.assertProviderModelSettings(loaded, modelProvider, model, effort);
|
|
4432
4592
|
await this.outbound.updateInteractiveCard(contextKey, updateMessageId, this.cardRenderer.renderPermissionSelector({
|
|
4433
4593
|
sessionId,
|
|
@@ -4439,20 +4599,47 @@ export class ProxySessionController {
|
|
|
4439
4599
|
}));
|
|
4440
4600
|
}
|
|
4441
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) {
|
|
4442
4606
|
const record = options.sessionId
|
|
4443
4607
|
? this.requireSession(contextKey, options.sessionId)
|
|
4444
4608
|
: this.requireCurrentSession(contextKey);
|
|
4445
|
-
|
|
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)
|
|
4446
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;
|
|
4447
4642
|
}
|
|
4448
|
-
const loaded = await this.loadSession(record);
|
|
4449
|
-
const warning = await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
4450
|
-
const session = await this.changeProviderSettings(loaded, {
|
|
4451
|
-
modelProvider: settings.provider,
|
|
4452
|
-
model: settings.model,
|
|
4453
|
-
reasoningEffort: settings.effort,
|
|
4454
|
-
permissionMode: settings.mode,
|
|
4455
|
-
});
|
|
4456
4643
|
const notice = [
|
|
4457
4644
|
`Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
|
|
4458
4645
|
`模型 ${cardCode(session.model ?? settings.model)}`,
|
|
@@ -4461,7 +4648,7 @@ export class ProxySessionController {
|
|
|
4461
4648
|
`并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
4462
4649
|
].join(",");
|
|
4463
4650
|
if (options.updateMessageId) {
|
|
4464
|
-
await this.
|
|
4651
|
+
await this.openExecutionSettingsNow(contextKey, "provider", {
|
|
4465
4652
|
sessionId: loaded.record.localSessionId,
|
|
4466
4653
|
updateMessageId: options.updateMessageId,
|
|
4467
4654
|
notice,
|
|
@@ -4472,16 +4659,23 @@ export class ProxySessionController {
|
|
|
4472
4659
|
}
|
|
4473
4660
|
}
|
|
4474
4661
|
async changeProviderSettings(loaded, settings) {
|
|
4662
|
+
return this.sessionHydrationOperations.run(loaded.record.localSessionId, () => this.changeProviderSettingsNow(loaded, settings));
|
|
4663
|
+
}
|
|
4664
|
+
async changeProviderSettingsNow(loaded, settings) {
|
|
4475
4665
|
if (!loaded.runtime.setExecutionSettings)
|
|
4476
4666
|
throw new Error("当前运行时不支持 Provider 设置。");
|
|
4477
|
-
|
|
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
|
+
}
|
|
4478
4673
|
assertProviderSettingsApplied(candidate, settings);
|
|
4479
4674
|
const agent = this.ensureAgent(loaded.record.agentName);
|
|
4480
4675
|
const previousDefaults = agent.defaults;
|
|
4481
|
-
await this.
|
|
4676
|
+
await this.persistAgentExecutionDefaultsNow(loaded.record.localSessionId, candidate);
|
|
4482
4677
|
try {
|
|
4483
4678
|
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
4484
|
-
remoteSessionId: candidate.remoteSessionId,
|
|
4485
4679
|
modelProvider: candidate.modelProvider,
|
|
4486
4680
|
model: candidate.model,
|
|
4487
4681
|
reasoningEffort: candidate.reasoningEffort,
|
|
@@ -4493,11 +4687,147 @@ export class ProxySessionController {
|
|
|
4493
4687
|
agent.defaults = previousDefaults;
|
|
4494
4688
|
throw error;
|
|
4495
4689
|
}
|
|
4496
|
-
};
|
|
4690
|
+
});
|
|
4497
4691
|
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
4692
|
+
this.settingsOnlySessions.delete(loaded.record.localSessionId);
|
|
4498
4693
|
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
|
|
4499
4694
|
return session;
|
|
4500
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
|
+
}
|
|
4501
4831
|
async assertProviderModelSettings(loaded, modelProvider, model, effort) {
|
|
4502
4832
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4503
4833
|
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
|
|
@@ -4534,36 +4864,51 @@ export class ProxySessionController {
|
|
|
4534
4864
|
return providers;
|
|
4535
4865
|
}
|
|
4536
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 = {}) {
|
|
4537
4871
|
if (!model) {
|
|
4538
|
-
await this.
|
|
4872
|
+
await this.openExecutionSettingsNow(contextKey, "model", options);
|
|
4539
4873
|
return;
|
|
4540
4874
|
}
|
|
4541
4875
|
const record = options.sessionId
|
|
4542
4876
|
? this.requireSession(contextKey, options.sessionId)
|
|
4543
4877
|
: this.requireCurrentSession(contextKey);
|
|
4544
|
-
const loaded = await this.loadSession(record);
|
|
4545
|
-
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);
|
|
4546
4885
|
const selected = models.find((item) => item.id === model);
|
|
4547
4886
|
if (!selected)
|
|
4548
4887
|
throw new Error(`未知模型:${model}`);
|
|
4549
|
-
const currentEffort =
|
|
4888
|
+
const currentEffort = selectedSettings.reasoningEffort;
|
|
4550
4889
|
const compatible = currentEffort
|
|
4551
4890
|
? selected.supportedReasoningEfforts.length === 0
|
|
4552
4891
|
|| selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
|
|
4553
4892
|
: false;
|
|
4554
4893
|
const nextEffort = compatible ? currentEffort : selected.defaultReasoningEffort;
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4894
|
+
if (pending) {
|
|
4895
|
+
await this.savePendingProviderSelection(loaded, { ...pending.settings, model, reasoningEffort: selectProviderReasoningEffort(selected, nextEffort) }, contextKey, options, "model");
|
|
4896
|
+
return;
|
|
4558
4897
|
}
|
|
4559
|
-
this.
|
|
4560
|
-
|
|
4561
|
-
|
|
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
|
+
});
|
|
4562
4907
|
const effortMessage = nextEffort && nextEffort !== currentEffort
|
|
4563
4908
|
? `,思考强度已自动调整为 ${nextEffort}`
|
|
4564
4909
|
: "";
|
|
4565
4910
|
if (options.updateMessageId) {
|
|
4566
|
-
await this.
|
|
4911
|
+
await this.openExecutionSettingsNow(contextKey, "model", {
|
|
4567
4912
|
sessionId: loaded.record.localSessionId,
|
|
4568
4913
|
updateMessageId: options.updateMessageId,
|
|
4569
4914
|
notice: `模型已切换为 ${cardCode(model)}${effortMessage},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
@@ -4703,6 +5048,7 @@ export class ProxySessionController {
|
|
|
4703
5048
|
const created = await this.shellCommandJobs.createJob({ contextKey, sourceMessageId, command, cwd });
|
|
4704
5049
|
let cardMessageId;
|
|
4705
5050
|
const initialState = {
|
|
5051
|
+
terminalUrl: this.lifecycle?.terminalUrl?.(created.id),
|
|
4706
5052
|
jobId: created.id,
|
|
4707
5053
|
contextKey,
|
|
4708
5054
|
command,
|
|
@@ -4725,7 +5071,7 @@ export class ProxySessionController {
|
|
|
4725
5071
|
if (cardMessageId) {
|
|
4726
5072
|
try {
|
|
4727
5073
|
const failed = await this.shellCommandJobs.readJob(created.id);
|
|
4728
|
-
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) }));
|
|
4729
5075
|
await this.shellCommandJobs.markPresented(created.id);
|
|
4730
5076
|
}
|
|
4731
5077
|
catch (cardError) {
|
|
@@ -4776,7 +5122,7 @@ export class ProxySessionController {
|
|
|
4776
5122
|
if (!job.cardMessageId || this.shellCommandJobMonitors.has(job.id))
|
|
4777
5123
|
return;
|
|
4778
5124
|
const scheduler = new CardUpdateScheduler({
|
|
4779
|
-
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
5125
|
+
render: (state) => this.cardRenderer.renderShellCommandCard({ ...state, terminalUrl: this.lifecycle?.terminalUrl?.(job.id) }),
|
|
4780
5126
|
write: (card) => this.outbound.updateInteractiveCard(job.contextKey, job.cardMessageId, card),
|
|
4781
5127
|
onError: (error) => this.logger.warn({ error, jobId: job.id, contextKey: job.contextKey, messageId: job.cardMessageId }, "Failed to update a background shell command card."),
|
|
4782
5128
|
});
|
|
@@ -4925,16 +5271,25 @@ export class ProxySessionController {
|
|
|
4925
5271
|
});
|
|
4926
5272
|
}
|
|
4927
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 = {}) {
|
|
4928
5278
|
if (!effort) {
|
|
4929
|
-
await this.
|
|
5279
|
+
await this.openExecutionSettingsNow(contextKey, "thinking", options);
|
|
4930
5280
|
return;
|
|
4931
5281
|
}
|
|
4932
5282
|
const record = options.sessionId
|
|
4933
5283
|
? this.requireSession(contextKey, options.sessionId)
|
|
4934
5284
|
: this.requireCurrentSession(contextKey);
|
|
4935
|
-
const loaded = await this.loadSession(record);
|
|
4936
|
-
const
|
|
4937
|
-
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)
|
|
4938
5293
|
?? models.find((item) => item.isDefault);
|
|
4939
5294
|
if (!currentModel)
|
|
4940
5295
|
throw new Error("当前运行时没有可配置思考强度的模型。");
|
|
@@ -4946,11 +5301,17 @@ export class ProxySessionController {
|
|
|
4946
5301
|
const options = supported.map((option) => option.value).join("、") || "无";
|
|
4947
5302
|
throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
|
|
4948
5303
|
}
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
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
|
+
});
|
|
4952
5313
|
if (options.updateMessageId) {
|
|
4953
|
-
await this.
|
|
5314
|
+
await this.openExecutionSettingsNow(contextKey, "thinking", {
|
|
4954
5315
|
sessionId: loaded.record.localSessionId,
|
|
4955
5316
|
updateMessageId: options.updateMessageId,
|
|
4956
5317
|
notice: `思考强度已切换为 ${cardCode(effort)},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
|
|
@@ -4960,20 +5321,32 @@ export class ProxySessionController {
|
|
|
4960
5321
|
await this.outbound.sendText(contextKey, `思考强度已切换为 ${effort},并已保存为 ${loaded.record.agentName} Agent 的默认设置,从下一次请求生效。`);
|
|
4961
5322
|
}
|
|
4962
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 = {}) {
|
|
4963
5328
|
if (!mode) {
|
|
4964
|
-
await this.
|
|
5329
|
+
await this.openExecutionSettingsNow(contextKey, "permission", options);
|
|
4965
5330
|
return;
|
|
4966
5331
|
}
|
|
4967
5332
|
const record = options.sessionId
|
|
4968
5333
|
? this.requireSession(contextKey, options.sessionId)
|
|
4969
5334
|
: this.requireCurrentSession(contextKey);
|
|
4970
|
-
const loaded = await this.loadSession(record);
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
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
|
+
});
|
|
4975
5348
|
if (options.updateMessageId) {
|
|
4976
|
-
await this.
|
|
5349
|
+
await this.openExecutionSettingsNow(contextKey, "permission", {
|
|
4977
5350
|
sessionId: loaded.record.localSessionId,
|
|
4978
5351
|
updateMessageId: options.updateMessageId,
|
|
4979
5352
|
notice: mode === "auto"
|
|
@@ -5014,8 +5387,9 @@ export class ProxySessionController {
|
|
|
5014
5387
|
if (existing)
|
|
5015
5388
|
return existing;
|
|
5016
5389
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
5017
|
-
|
|
5018
|
-
|
|
5390
|
+
const previewTurnId = snapshot?.previewTurnId ?? turnId;
|
|
5391
|
+
if ((!snapshot || snapshot.previewJournal) && this.store.previews.has(previewTurnId))
|
|
5392
|
+
return this.store.previews.load(previewTurnId);
|
|
5019
5393
|
if (snapshot && !needsHistoricalDetails(snapshot))
|
|
5020
5394
|
return snapshot;
|
|
5021
5395
|
const origin = !snapshot ? this.store.getTurnRuntimeOrigin(turnId) : undefined;
|
|
@@ -5048,7 +5422,7 @@ export class ProxySessionController {
|
|
|
5048
5422
|
// An Agent Bot turn may have arrived while the read-only request was in flight.
|
|
5049
5423
|
const latest = turnViewSnapshot(this.store.getTurnSnapshot(origin.turnId));
|
|
5050
5424
|
if (latest)
|
|
5051
|
-
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;
|
|
5052
5426
|
const state = hydrateTurnViewState(createTurnViewState(origin.localSessionId, origin.turnId, details.startedAt ?? 0, remote?.title, undefined, remote?.cwd, undefined, this.agentLabel(origin.agentName)), details);
|
|
5053
5427
|
// Viewing external work must not create a task, a chat binding or a pending final reply.
|
|
5054
5428
|
this.store.previews.seed(state);
|
|
@@ -5951,7 +6325,7 @@ export class ProxySessionController {
|
|
|
5951
6325
|
const remoteReference = local
|
|
5952
6326
|
? remoteSessionReference(local.agentName, local.remoteSessionId)
|
|
5953
6327
|
: taskId;
|
|
5954
|
-
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference);
|
|
6328
|
+
const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference, { view: "metadata" });
|
|
5955
6329
|
if (isRemoteSessionActive(remote)) {
|
|
5956
6330
|
throw new Error("任务正在执行,无法归档。请先停止任务或等待本轮完成。");
|
|
5957
6331
|
}
|
|
@@ -5959,7 +6333,11 @@ export class ProxySessionController {
|
|
|
5959
6333
|
throw new Error(`Agent ${agentName} 不支持归档任务。`);
|
|
5960
6334
|
}
|
|
5961
6335
|
const wasCurrent = Boolean(local && this.store.getUserContext(contextKey)?.currentSessionId === local.localSessionId);
|
|
5962
|
-
|
|
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 });
|
|
5963
6341
|
const title = local?.title ?? remote.title ?? remote.preview ?? remote.id;
|
|
5964
6342
|
if (local) {
|
|
5965
6343
|
this.store.archiveSession(local.localSessionId);
|
|
@@ -6097,6 +6475,7 @@ export class ProxySessionController {
|
|
|
6097
6475
|
}
|
|
6098
6476
|
const localActiveTurnId = runtime.getSession(record.localSessionId)?.activeTurnId;
|
|
6099
6477
|
const runtimeOwnsActiveTurn = Boolean(localActiveTurnId)
|
|
6478
|
+
&& (!this.settingsOnlySessions.has(record.localSessionId) || Boolean(activity.activeTurnId))
|
|
6100
6479
|
&& (!activity.activeTurnId || activity.activeTurnId === localActiveTurnId);
|
|
6101
6480
|
const persistedTurnMatches = record.status === "running"
|
|
6102
6481
|
&& record.lastTurnStatus === "running"
|
|
@@ -6443,6 +6822,12 @@ export class ProxySessionController {
|
|
|
6443
6822
|
return agent;
|
|
6444
6823
|
}
|
|
6445
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) {
|
|
6446
6831
|
const record = this.store.getSession(localSessionId);
|
|
6447
6832
|
if (!record)
|
|
6448
6833
|
throw new Error(`找不到任务:${localSessionId}`);
|
|
@@ -7367,6 +7752,14 @@ function executionSettingsTabValue(value) {
|
|
|
7367
7752
|
return value;
|
|
7368
7753
|
throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
|
|
7369
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
|
+
}
|
|
7370
7763
|
function resetHistoryPageValue(value) {
|
|
7371
7764
|
const page = Number(value);
|
|
7372
7765
|
if (!Number.isSafeInteger(page) || page < 0)
|
|
@@ -7743,10 +8136,17 @@ function defaultResourcePromptFromCounts(imageCount, fileCount) {
|
|
|
7743
8136
|
return fileCount === 1 ? "请查看这个文件" : "请查看这些文件";
|
|
7744
8137
|
return imageCount === 1 ? "请查看这张图片" : "请查看这些图片";
|
|
7745
8138
|
}
|
|
7746
|
-
function
|
|
8139
|
+
function isCommandMessage(message) {
|
|
8140
|
+
const text = normalizeFeishuPostText(message.text).trimStart();
|
|
8141
|
+
return text.startsWith("/") || text.startsWith("!");
|
|
8142
|
+
}
|
|
8143
|
+
function isForwardAttachmentSource(message) {
|
|
7747
8144
|
return !message.mergedForwardMessageId
|
|
7748
|
-
&& message.
|
|
7749
|
-
&&
|
|
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);
|
|
7750
8150
|
}
|
|
7751
8151
|
function deduplicateImageReferences(images) {
|
|
7752
8152
|
const seen = new Set();
|
|
@@ -7813,7 +8213,8 @@ function isRetryableLlmTurnFailure(message) {
|
|
|
7813
8213
|
if (!normalized)
|
|
7814
8214
|
return false;
|
|
7815
8215
|
const permanentFailure = [
|
|
7816
|
-
|
|
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,
|
|
7817
8218
|
/insufficient[_ -]?quota|quota (?:exceeded|exhausted)|usage limit|billing|credit balance/u,
|
|
7818
8219
|
/context (?:length|window)|maximum context|too many tokens|token limit/u,
|
|
7819
8220
|
/invalid request|bad request|invalid argument|malformed|unsupported|not supported/u,
|