@keyou007/agent-bot 0.1.20-alpha.1 → 0.1.21
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 +21 -0
- package/README.md +27 -4
- package/README.zh.md +23 -4
- package/dist/cli/AgentPrerequisites.js +11 -47
- package/dist/cli/AgentPrerequisites.js.map +1 -1
- package/dist/cli/DefaultAgentSelection.js +1 -1
- package/dist/cli/DefaultAgentSelection.js.map +1 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +21 -1
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexLocalActivityDetector.js +32 -10
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +56 -7
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +361 -168
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/CodexVersion.js +18 -0
- package/dist/codex/CodexVersion.js.map +1 -0
- package/dist/codex/ProviderModels.js +159 -0
- package/dist/codex/ProviderModels.js.map +1 -0
- package/dist/codex/ProviderSwitch.js +32 -0
- package/dist/codex/ProviderSwitch.js.map +1 -0
- package/dist/commands/CommandRouter.js +8 -3
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +2 -1
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +82 -27
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +17 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +34 -8
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/InboundText.js +11 -0
- package/dist/feishu/InboundText.js.map +1 -1
- package/dist/feishu/MergedForwardMessage.js +17 -5
- package/dist/feishu/MergedForwardMessage.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +164 -19
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/MarkdownFileLinks.js +65 -0
- package/dist/local-files/MarkdownFileLinks.js.map +1 -0
- package/dist/local-files/TurnPreviewPage.js +725 -0
- package/dist/local-files/TurnPreviewPage.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +6 -1
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +142 -3
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +573 -325
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +65 -65
- package/dist/state/StateStore.js.map +1 -1
- package/dist/utils/formatStorageSize.js +15 -0
- package/dist/utils/formatStorageSize.js.map +1 -0
- package/dist/utils/semanticVersion.js +39 -0
- package/dist/utils/semanticVersion.js.map +1 -0
- package/docs/technical-reference.md +11 -3
- package/docs/technical-reference.zh.md +11 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +11 -1
- package/src/cli/AgentPrerequisites.ts +14 -40
- package/src/cli/DefaultAgentSelection.ts +1 -1
- package/src/cli.ts +13 -0
- package/src/codex/CodexEventMapper.ts +31 -1
- package/src/codex/CodexLocalActivityDetector.ts +41 -9
- package/src/codex/CodexProcessManager.ts +48 -7
- package/src/codex/CodexRuntime.ts +397 -204
- package/src/codex/CodexVersion.ts +18 -0
- package/src/codex/ProviderModels.ts +185 -0
- package/src/codex/ProviderSwitch.ts +36 -0
- package/src/commands/CommandRouter.ts +7 -2
- package/src/commands/commandTypes.ts +1 -1
- package/src/console/ConsoleTurnPresenter.ts +3 -0
- package/src/feishu/CardRenderer.ts +84 -43
- package/src/feishu/FeishuConnector.ts +15 -5
- package/src/feishu/FeishuTurnPresenter.ts +46 -4
- package/src/feishu/InboundText.ts +11 -0
- package/src/feishu/MergedForwardMessage.ts +15 -5
- package/src/index.ts +3 -1
- package/src/local-files/LocalFileViewerServer.ts +179 -20
- package/src/local-files/MarkdownFileLinks.ts +64 -0
- package/src/local-files/TurnPreviewPage.ts +792 -0
- package/src/presentation/OutboundRouter.ts +8 -0
- package/src/presentation/TurnStateReducer.ts +175 -2
- package/src/presentation/turnViewTypes.ts +13 -0
- package/src/proxy/ProxySessionController.ts +547 -297
- package/src/runtime/types.ts +33 -3
- package/src/state/StateStore.ts +86 -90
- package/src/utils/formatStorageSize.ts +13 -0
- package/src/utils/semanticVersion.ts +28 -0
|
@@ -4,6 +4,8 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import type { Logger } from "pino";
|
|
6
6
|
import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
7
|
+
import { CodexVersionError } from "../codex/CodexVersion.js";
|
|
8
|
+
import { assertProviderSettingsApplied, isMissingRolloutError } from "../codex/ProviderSwitch.js";
|
|
7
9
|
import {
|
|
8
10
|
SystemThreadWriterProcessController,
|
|
9
11
|
type ThreadWriterProcess,
|
|
@@ -57,13 +59,16 @@ import type { AgentRuntimeRegistry } from "../runtime/AgentRuntimeRegistry.js";
|
|
|
57
59
|
import type {
|
|
58
60
|
AgentRuntime,
|
|
59
61
|
ApprovalDecision,
|
|
62
|
+
ModelOption,
|
|
60
63
|
PermissionMode,
|
|
61
64
|
RemoteCompletedTurnSummary,
|
|
62
65
|
RemoteSessionActivity,
|
|
66
|
+
RemoteSessionMetrics,
|
|
63
67
|
RemoteSessionPage,
|
|
64
68
|
RemoteSessionSummary,
|
|
65
69
|
RuntimeGoal,
|
|
66
70
|
RuntimeEvent,
|
|
71
|
+
RuntimeExecutionSettings,
|
|
67
72
|
RuntimePrompt,
|
|
68
73
|
RuntimeSession,
|
|
69
74
|
} from "../runtime/types.js";
|
|
@@ -84,6 +89,7 @@ import {
|
|
|
84
89
|
type TurnAnchorRecord,
|
|
85
90
|
} from "../state/StateStore.js";
|
|
86
91
|
import { createId } from "../utils/id.js";
|
|
92
|
+
import { formatStorageSize } from "../utils/formatStorageSize.js";
|
|
87
93
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
88
94
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
89
95
|
import {
|
|
@@ -187,6 +193,22 @@ const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
|
187
193
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
188
194
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
189
195
|
const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
|
|
196
|
+
const TURN_RESET_STARTED_MESSAGE = "Reset 正在进行,请稍后。期间发送的消息会自动排队。";
|
|
197
|
+
const CREATE_TASK_STARTED_MESSAGE = "正在创建任务,请稍后。";
|
|
198
|
+
const CREATE_GROUP_STARTED_MESSAGE = "正在创建群聊和任务,请稍后。";
|
|
199
|
+
const FORK_TASK_STARTED_MESSAGE = "正在创建分支任务,请稍后。";
|
|
200
|
+
const FORK_GROUP_STARTED_MESSAGE = "正在创建分支群聊,请稍后。";
|
|
201
|
+
const LIST_SESSIONS_STARTED_MESSAGE = "正在读取任务列表,请稍后。";
|
|
202
|
+
const LIST_TURNS_STARTED_MESSAGE = "正在读取历史轮次,请稍后。";
|
|
203
|
+
const SWITCH_TASK_STARTED_MESSAGE = "正在切换任务,请稍后。";
|
|
204
|
+
const STATUS_STARTED_MESSAGE = "正在读取任务状态,请稍后。";
|
|
205
|
+
const GOAL_STARTED_MESSAGE = "正在处理 Goal,请稍后。";
|
|
206
|
+
const ARCHIVE_TASK_STARTED_MESSAGE = "正在归档任务,请稍后。";
|
|
207
|
+
const DISMISS_GROUP_STARTED_MESSAGE = "正在归档任务并解散群聊,请稍后。";
|
|
208
|
+
const SEND_FILE_STARTED_MESSAGE = "正在发送文件,请稍后。";
|
|
209
|
+
const CHANGE_TITLE_STARTED_MESSAGE = "正在修改任务标题,请稍后。";
|
|
210
|
+
const STOP_TASK_STARTED_MESSAGE = "正在停止任务,请稍后。";
|
|
211
|
+
const CLOSE_WRITER_STARTED_MESSAGE = "正在检查并关闭占用进程,请稍后。";
|
|
190
212
|
const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
|
|
191
213
|
const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
|
|
192
214
|
const IMAGE_FILE_EXTENSIONS = new Set([
|
|
@@ -238,7 +260,7 @@ const HELP_COMMAND_SECTIONS: Array<{
|
|
|
238
260
|
usage: "[序号或任务 ID]",
|
|
239
261
|
description: "从当前或指定任务最近完成轮次创建分支",
|
|
240
262
|
},
|
|
241
|
-
{ command: "/turns", description: "浏览历史轮次,并可 Reset
|
|
263
|
+
{ command: "/turns", usage: "[Turn ID 或序号]", description: "浏览历史轮次,并可 Reset 对话上下文;带参数查看运行详情" },
|
|
242
264
|
{
|
|
243
265
|
command: "/title",
|
|
244
266
|
usage: "<新标题>",
|
|
@@ -409,7 +431,6 @@ interface ForkSessionPlan {
|
|
|
409
431
|
sourceTurnPrompt?: string;
|
|
410
432
|
sourceTurnStartedAt?: number;
|
|
411
433
|
sourceTurnCompletedAt?: number;
|
|
412
|
-
sourceCompletedTurns?: RemoteCompletedTurnSummary[];
|
|
413
434
|
sourceWasRunning: boolean;
|
|
414
435
|
forkedFromHistoricalTurn: boolean;
|
|
415
436
|
}
|
|
@@ -419,6 +440,14 @@ interface ForkSessionResult {
|
|
|
419
440
|
session: RuntimeSession;
|
|
420
441
|
}
|
|
421
442
|
|
|
443
|
+
interface ForkHistoryCursor {
|
|
444
|
+
cursor?: string;
|
|
445
|
+
anchorFound: boolean;
|
|
446
|
+
tailTurnId?: string;
|
|
447
|
+
loadedCount?: number;
|
|
448
|
+
complete: boolean;
|
|
449
|
+
}
|
|
450
|
+
|
|
422
451
|
interface ForkGroupSessionPlan {
|
|
423
452
|
plan: ForkSessionPlan;
|
|
424
453
|
sourceDescription: string;
|
|
@@ -489,6 +518,8 @@ export class ProxySessionController {
|
|
|
489
518
|
private readonly sessionResets = new Map<string, Promise<void>>();
|
|
490
519
|
private readonly resetHistoryOperations = new Map<string, string>();
|
|
491
520
|
private readonly remoteTurnHistoryRefreshes = new Map<string, number>();
|
|
521
|
+
private readonly turnHistoryHydrations = new Map<string, Promise<boolean>>();
|
|
522
|
+
private readonly forkHistoryCursors = new Map<string, ForkHistoryCursor>();
|
|
492
523
|
private readonly lastSessionListings = new Map<string, string[]>();
|
|
493
524
|
private readonly threadInitializations = new Map<string, Promise<void>>();
|
|
494
525
|
private readonly llmRetryingSessions = new Set<string>();
|
|
@@ -589,6 +620,7 @@ export class ProxySessionController {
|
|
|
589
620
|
"Failed to acknowledge the incoming Feishu message with a reaction.",
|
|
590
621
|
);
|
|
591
622
|
}
|
|
623
|
+
await this.waitForCurrentSessionReset(message.contextKey);
|
|
592
624
|
if (attachmentReservation && this.completeForwardAttachment(message, attachmentReservation)) {
|
|
593
625
|
this.store.audit(message.contextKey, `${attachmentReservation.sourceKind}_attachment_coalesced`, {
|
|
594
626
|
messageId: message.messageId,
|
|
@@ -826,10 +858,11 @@ export class ProxySessionController {
|
|
|
826
858
|
"Processing Agent Bot command.",
|
|
827
859
|
);
|
|
828
860
|
}
|
|
829
|
-
// Operational and read-only commands
|
|
861
|
+
// Operational and read-only commands bypass the prompt queue, but still wait for a Reset binding change.
|
|
830
862
|
if (isQueueIndependentCommand(command)) {
|
|
831
863
|
await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
832
864
|
try {
|
|
865
|
+
await this.waitForCurrentSessionReset(message.contextKey);
|
|
833
866
|
if (command.type === "prompt") await this.ensureThreadFork(message);
|
|
834
867
|
await this.execute(
|
|
835
868
|
message.contextKey,
|
|
@@ -857,8 +890,9 @@ export class ProxySessionController {
|
|
|
857
890
|
}
|
|
858
891
|
|
|
859
892
|
const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
|
|
860
|
-
const next = previous.catch(() => undefined).then(() =>
|
|
861
|
-
this.
|
|
893
|
+
const next = previous.catch(() => undefined).then(async () => {
|
|
894
|
+
await this.waitForCurrentSessionReset(message.contextKey);
|
|
895
|
+
return this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
862
896
|
try {
|
|
863
897
|
let executableCommand = command;
|
|
864
898
|
let executableImagePaths = localImagePaths;
|
|
@@ -903,8 +937,8 @@ export class ProxySessionController {
|
|
|
903
937
|
phase: "command_execute",
|
|
904
938
|
});
|
|
905
939
|
}
|
|
906
|
-
})
|
|
907
|
-
);
|
|
940
|
+
});
|
|
941
|
+
});
|
|
908
942
|
this.messageQueues.set(message.contextKey, next);
|
|
909
943
|
await next;
|
|
910
944
|
if (this.messageQueues.get(message.contextKey) === next) this.messageQueues.delete(message.contextKey);
|
|
@@ -1022,6 +1056,7 @@ export class ProxySessionController {
|
|
|
1022
1056
|
} else if (kind === "directory_send_file") {
|
|
1023
1057
|
const filePath = directoryFileActionPath(scopedAction.value.filePath);
|
|
1024
1058
|
await this.assertSendableFile(filePath);
|
|
1059
|
+
await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
|
|
1025
1060
|
await this.outbound.sendFile(contextKey, filePath);
|
|
1026
1061
|
} else if (kind === "directory_new_folder_prompt") {
|
|
1027
1062
|
const directory = directoryActionPath(scopedAction.value.directory);
|
|
@@ -1077,10 +1112,12 @@ export class ProxySessionController {
|
|
|
1077
1112
|
await this.refreshSessionsCardFromAction(scopedAction);
|
|
1078
1113
|
} else if (kind === "session_new") {
|
|
1079
1114
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
1115
|
+
await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
|
|
1080
1116
|
await this.createProjectSessionFromReference(contextKey, sessionId);
|
|
1081
1117
|
await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
|
|
1082
1118
|
} else if (kind === "session_new_group") {
|
|
1083
1119
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
1120
|
+
await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
|
|
1084
1121
|
await this.createFeishuGroupFromReference(contextKey, sessionId, scopedAction.userId);
|
|
1085
1122
|
await this.refreshSessionsCardFromAction(scopedAction);
|
|
1086
1123
|
} else if (kind === "session_archive") {
|
|
@@ -1099,6 +1136,7 @@ export class ProxySessionController {
|
|
|
1099
1136
|
);
|
|
1100
1137
|
} else if (kind === "group_dismiss_confirm") {
|
|
1101
1138
|
this.assertDismissGroupRequester(scopedAction);
|
|
1139
|
+
await this.outbound.sendText(contextKey, DISMISS_GROUP_STARTED_MESSAGE);
|
|
1102
1140
|
await this.dismissGroupAndArchiveTask(
|
|
1103
1141
|
contextKey,
|
|
1104
1142
|
String(scopedAction.value.sessionId ?? ""),
|
|
@@ -1106,10 +1144,12 @@ export class ProxySessionController {
|
|
|
1106
1144
|
);
|
|
1107
1145
|
} else if (kind === "session_stop") {
|
|
1108
1146
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
1147
|
+
await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
|
|
1109
1148
|
await this.stopSessionReference(contextKey, sessionId);
|
|
1110
1149
|
if (scopedAction.value.cardView === "status") await this.refreshStatusCardFromAction(scopedAction, sessionId);
|
|
1111
1150
|
else await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
|
|
1112
1151
|
} else if (kind === "session_status") {
|
|
1152
|
+
await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
|
|
1113
1153
|
await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
|
|
1114
1154
|
} else if (kind === "session_status_refresh") {
|
|
1115
1155
|
await this.refreshStatusCardFromAction(scopedAction);
|
|
@@ -1247,6 +1287,8 @@ export class ProxySessionController {
|
|
|
1247
1287
|
this.sessionResets.clear();
|
|
1248
1288
|
this.resetHistoryOperations.clear();
|
|
1249
1289
|
this.remoteTurnHistoryRefreshes.clear();
|
|
1290
|
+
this.forkHistoryCursors.clear();
|
|
1291
|
+
this.turnHistoryHydrations.clear();
|
|
1250
1292
|
for (const pending of this.pendingMergedForwards.values()) pending.resolveAttachment(undefined);
|
|
1251
1293
|
this.pendingMergedForwards.clear();
|
|
1252
1294
|
for (const pending of this.pendingResourceForwards.values()) pending.resolveAttachment(undefined);
|
|
@@ -1326,8 +1368,9 @@ export class ProxySessionController {
|
|
|
1326
1368
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
1327
1369
|
if (runtime.readRemoteSession) {
|
|
1328
1370
|
try {
|
|
1329
|
-
remote = await runtime.readRemoteSession(record.remoteSessionId);
|
|
1371
|
+
remote = await runtime.readRemoteSession(record.remoteSessionId, "latest-full");
|
|
1330
1372
|
} catch (error) {
|
|
1373
|
+
if (error instanceof CodexVersionError) throw error;
|
|
1331
1374
|
this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to inspect App Server task status for CLI.");
|
|
1332
1375
|
}
|
|
1333
1376
|
}
|
|
@@ -1393,6 +1436,7 @@ export class ProxySessionController {
|
|
|
1393
1436
|
scopedRecord.title,
|
|
1394
1437
|
scopedRecord.cwd,
|
|
1395
1438
|
this.agentLabel(scopedRecord.agentName),
|
|
1439
|
+
scopedRecord.model,
|
|
1396
1440
|
);
|
|
1397
1441
|
const promptMessageId = await this.outbound.withReplyTarget(
|
|
1398
1442
|
responseContextKey,
|
|
@@ -1627,42 +1671,36 @@ export class ProxySessionController {
|
|
|
1627
1671
|
permissionModes: PermissionMode[];
|
|
1628
1672
|
}> {
|
|
1629
1673
|
const record = this.requireControlSession(localSessionId);
|
|
1630
|
-
const loaded = await this.loadSession(record);
|
|
1631
1674
|
if (setting && !value?.trim()) throw new Error(`task ${setting} requires a value.`);
|
|
1632
1675
|
const nextValue = value?.trim();
|
|
1676
|
+
if (setting === "provider" && nextValue) {
|
|
1677
|
+
const contextKey = this.controlSessionContextKey(record);
|
|
1678
|
+
await this.outbound.withReplyTarget(
|
|
1679
|
+
contextKey,
|
|
1680
|
+
this.controlSessionReplyTarget(record),
|
|
1681
|
+
() => this.outbound.sendText(contextKey, `正在切换到 Provider ${nextValue},请稍后。`),
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
const loaded = await this.loadSession(record);
|
|
1633
1685
|
if (setting === "provider" && nextValue) {
|
|
1634
1686
|
await this.assertModelProvider(loaded, nextValue);
|
|
1635
|
-
const models = await loaded.runtime.listModels();
|
|
1636
|
-
const model = models
|
|
1637
|
-
?? models.find((candidate) => candidate.isDefault);
|
|
1687
|
+
const models = await loaded.runtime.listModels(nextValue, loaded.session.model);
|
|
1688
|
+
const model = selectProviderModel(models, loaded.session.model);
|
|
1638
1689
|
if (!model) throw new Error("The current runtime has no model available for a Provider change.");
|
|
1639
|
-
const effort = model.
|
|
1640
|
-
|
|
1641
|
-
)
|
|
1642
|
-
? loaded.session.reasoningEffort
|
|
1643
|
-
: model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
|
|
1644
|
-
if (!effort) throw new Error(`Model ${model.id} has no reasoning effort available for a Provider change.`);
|
|
1645
|
-
if (!loaded.runtime.setExecutionSettings) throw new Error("The current runtime does not support Provider settings.");
|
|
1646
|
-
const updated = await loaded.runtime.setExecutionSettings(record.localSessionId, {
|
|
1690
|
+
const effort = selectProviderReasoningEffort(model, loaded.session.reasoningEffort);
|
|
1691
|
+
await this.changeProviderSettings(loaded, {
|
|
1647
1692
|
modelProvider: nextValue,
|
|
1648
1693
|
model: model.id,
|
|
1649
1694
|
reasoningEffort: effort,
|
|
1650
1695
|
permissionMode: loaded.session.permissionMode,
|
|
1651
1696
|
});
|
|
1652
|
-
this.store.updateRuntimeSession(record.localSessionId, {
|
|
1653
|
-
modelProvider: updated.modelProvider ?? nextValue,
|
|
1654
|
-
model: updated.model ?? model.id,
|
|
1655
|
-
reasoningEffort: updated.reasoningEffort ?? effort,
|
|
1656
|
-
permissionMode: updated.permissionMode,
|
|
1657
|
-
});
|
|
1658
1697
|
} else if (setting === "model" && nextValue) {
|
|
1659
|
-
const models = await loaded.runtime.listModels();
|
|
1698
|
+
const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
|
|
1660
1699
|
const selected = models.find((candidate) => candidate.id === nextValue);
|
|
1661
1700
|
if (!selected) throw new Error(`Unknown model: ${nextValue}`);
|
|
1662
1701
|
const currentEffort = loaded.session.reasoningEffort;
|
|
1663
|
-
const nextEffort = currentEffort && selected.supportedReasoningEfforts.
|
|
1664
|
-
(candidate) => candidate.value === currentEffort
|
|
1665
|
-
)
|
|
1702
|
+
const nextEffort = currentEffort && (selected.supportedReasoningEfforts.length === 0
|
|
1703
|
+
|| selected.supportedReasoningEfforts.some((candidate) => candidate.value === currentEffort))
|
|
1666
1704
|
? currentEffort
|
|
1667
1705
|
: selected.defaultReasoningEffort;
|
|
1668
1706
|
await loaded.runtime.setModel(record.localSessionId, nextValue);
|
|
@@ -1671,7 +1709,7 @@ export class ProxySessionController {
|
|
|
1671
1709
|
}
|
|
1672
1710
|
this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
|
|
1673
1711
|
} else if (setting === "thinking" && nextValue) {
|
|
1674
|
-
const models = await loaded.runtime.listModels();
|
|
1712
|
+
const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
|
|
1675
1713
|
const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
|
|
1676
1714
|
?? models.find((candidate) => candidate.isDefault);
|
|
1677
1715
|
if (!currentModel) throw new Error("The current runtime has no model with configurable reasoning effort.");
|
|
@@ -1689,12 +1727,12 @@ export class ProxySessionController {
|
|
|
1689
1727
|
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
|
|
1690
1728
|
}
|
|
1691
1729
|
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1692
|
-
if (setting) await this.persistAgentExecutionDefaults(session.localSessionId);
|
|
1693
|
-
const models = await loaded.runtime.listModels();
|
|
1730
|
+
if (setting && setting !== "provider") await this.persistAgentExecutionDefaults(session.localSessionId);
|
|
1731
|
+
const models = await loaded.runtime.listModels(session.modelProvider, session.model);
|
|
1694
1732
|
const currentModel = models.find((candidate) => candidate.id === session.model)
|
|
1695
1733
|
?? models.find((candidate) => candidate.isDefault);
|
|
1696
1734
|
const providers = loaded.runtime.listModelProviders
|
|
1697
|
-
? await
|
|
1735
|
+
? await this.modelProviderOptions(loaded)
|
|
1698
1736
|
: [];
|
|
1699
1737
|
return {
|
|
1700
1738
|
session,
|
|
@@ -1738,7 +1776,7 @@ export class ProxySessionController {
|
|
|
1738
1776
|
};
|
|
1739
1777
|
}
|
|
1740
1778
|
|
|
1741
|
-
controlListTaskTurns(localSessionId: string): {
|
|
1779
|
+
async controlListTaskTurns(localSessionId: string): Promise<{
|
|
1742
1780
|
session: SessionRecord;
|
|
1743
1781
|
turns: Array<{
|
|
1744
1782
|
sequence: number;
|
|
@@ -1749,17 +1787,18 @@ export class ProxySessionController {
|
|
|
1749
1787
|
completedAt?: number;
|
|
1750
1788
|
current: boolean;
|
|
1751
1789
|
}>;
|
|
1752
|
-
} {
|
|
1790
|
+
}> {
|
|
1753
1791
|
const session = this.requireControlSession(localSessionId);
|
|
1754
1792
|
if (!session.remoteSessionId || !this.isCodexSession(session)) {
|
|
1755
1793
|
throw new Error("The task is not an App Server task that can be reset.");
|
|
1756
1794
|
}
|
|
1757
|
-
|
|
1795
|
+
await this.hydrateTaskTurnHistory(session);
|
|
1796
|
+
const rows = this.store.listTaskTurnGraphIndex(session.localSessionId);
|
|
1758
1797
|
const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
|
|
1759
1798
|
return {
|
|
1760
1799
|
session,
|
|
1761
1800
|
turns: rows.flatMap((turn, index) => {
|
|
1762
|
-
const snapshot =
|
|
1801
|
+
const snapshot = this.store.getTurnPromptSummary(turn.turnId);
|
|
1763
1802
|
if (!snapshot) return [];
|
|
1764
1803
|
return [{
|
|
1765
1804
|
sequence: graph[index]!.sequence,
|
|
@@ -1777,15 +1816,10 @@ export class ProxySessionController {
|
|
|
1777
1816
|
async controlResetTaskToTurn(localSessionId: string, turnId: string): Promise<SessionRecord> {
|
|
1778
1817
|
const record = this.requireControlSession(localSessionId);
|
|
1779
1818
|
const contextKey = this.controlSessionContextKey(record);
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
this.performSessionReset(contextKey, record, turnId, false)
|
|
1783
|
-
|
|
1784
|
-
try {
|
|
1785
|
-
await reset;
|
|
1786
|
-
} finally {
|
|
1787
|
-
if (this.sessionResets.get(record.localSessionId) === reset) this.sessionResets.delete(record.localSessionId);
|
|
1788
|
-
}
|
|
1819
|
+
await this.runSessionReset(record.localSessionId, async () => {
|
|
1820
|
+
await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
|
|
1821
|
+
await this.performSessionReset(contextKey, record, turnId, false);
|
|
1822
|
+
});
|
|
1789
1823
|
return this.store.getSession(record.localSessionId) ?? record;
|
|
1790
1824
|
}
|
|
1791
1825
|
|
|
@@ -1942,6 +1976,7 @@ export class ProxySessionController {
|
|
|
1942
1976
|
if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
|
|
1943
1977
|
throw new Error("/new --nodir 仅支持 App Server Agent。");
|
|
1944
1978
|
}
|
|
1979
|
+
await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
|
|
1945
1980
|
await this.createSession(
|
|
1946
1981
|
contextKey,
|
|
1947
1982
|
context.defaultAgent,
|
|
@@ -1962,6 +1997,10 @@ export class ProxySessionController {
|
|
|
1962
1997
|
if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
|
|
1963
1998
|
throw new Error("/newgroup --nodir 仅支持 App Server Agent。");
|
|
1964
1999
|
}
|
|
2000
|
+
if (!userId?.startsWith("ou_")) {
|
|
2001
|
+
throw new Error("/newgroup 只能由具有 open_id 的飞书用户消息触发。");
|
|
2002
|
+
}
|
|
2003
|
+
await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
|
|
1965
2004
|
await this.createFeishuGroup(
|
|
1966
2005
|
contextKey,
|
|
1967
2006
|
context.defaultAgent,
|
|
@@ -1994,6 +2033,7 @@ export class ProxySessionController {
|
|
|
1994
2033
|
await this.enqueueNoSteerPrompt(contextKey, command.text, messageId, replyTarget);
|
|
1995
2034
|
return;
|
|
1996
2035
|
case "sessions":
|
|
2036
|
+
await this.outbound.sendText(contextKey, LIST_SESSIONS_STARTED_MESSAGE);
|
|
1997
2037
|
await this.listSessions(contextKey, command.searchTerm);
|
|
1998
2038
|
return;
|
|
1999
2039
|
case "switch":
|
|
@@ -2016,9 +2056,11 @@ export class ProxySessionController {
|
|
|
2016
2056
|
await this.openDismissGroupCard(contextKey, userId);
|
|
2017
2057
|
return;
|
|
2018
2058
|
case "status":
|
|
2059
|
+
await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
|
|
2019
2060
|
await this.status(contextKey, command.sessionId);
|
|
2020
2061
|
return;
|
|
2021
2062
|
case "goal":
|
|
2063
|
+
await this.outbound.sendText(contextKey, GOAL_STARTED_MESSAGE);
|
|
2022
2064
|
await this.goal(contextKey, command);
|
|
2023
2065
|
return;
|
|
2024
2066
|
case "restart":
|
|
@@ -2032,7 +2074,13 @@ export class ProxySessionController {
|
|
|
2032
2074
|
await this.setGroupMute(contextKey, command.enabled);
|
|
2033
2075
|
return;
|
|
2034
2076
|
case "turns":
|
|
2035
|
-
|
|
2077
|
+
if (command.turnReference) {
|
|
2078
|
+
await this.outbound.sendText(contextKey, "正在读取轮次运行信息,请稍后。");
|
|
2079
|
+
await this.openTurnDetails(contextKey, command.turnReference);
|
|
2080
|
+
} else {
|
|
2081
|
+
await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
|
|
2082
|
+
await this.openResetHistory(contextKey);
|
|
2083
|
+
}
|
|
2036
2084
|
return;
|
|
2037
2085
|
case "model":
|
|
2038
2086
|
await this.openExecutionSettings(contextKey, "model");
|
|
@@ -2070,6 +2118,7 @@ export class ProxySessionController {
|
|
|
2070
2118
|
session.title,
|
|
2071
2119
|
session.cwd,
|
|
2072
2120
|
this.agentLabel(session.agentName),
|
|
2121
|
+
session.model,
|
|
2073
2122
|
);
|
|
2074
2123
|
if (!session.lastTurnId) continue;
|
|
2075
2124
|
void this.outbound.resumeDelivery(session.localSessionId, turnContextKey, session.lastTurnId).catch((error: unknown) => {
|
|
@@ -2156,6 +2205,7 @@ export class ProxySessionController {
|
|
|
2156
2205
|
session.title,
|
|
2157
2206
|
session.cwd,
|
|
2158
2207
|
this.agentLabel(session.agentName),
|
|
2208
|
+
session.model,
|
|
2159
2209
|
);
|
|
2160
2210
|
}
|
|
2161
2211
|
try {
|
|
@@ -2284,6 +2334,7 @@ export class ProxySessionController {
|
|
|
2284
2334
|
session.title,
|
|
2285
2335
|
session.cwd,
|
|
2286
2336
|
this.agentLabel(session.agentName),
|
|
2337
|
+
session.model,
|
|
2287
2338
|
);
|
|
2288
2339
|
if (announce) {
|
|
2289
2340
|
await this.outbound.withReplyTarget(contextKey, replyTarget, () =>
|
|
@@ -2309,6 +2360,7 @@ export class ProxySessionController {
|
|
|
2309
2360
|
try {
|
|
2310
2361
|
remote = await runtime.readRemoteSession(session.remoteSessionId);
|
|
2311
2362
|
} catch (error) {
|
|
2363
|
+
if (error instanceof CodexVersionError) throw error;
|
|
2312
2364
|
this.logger.warn(
|
|
2313
2365
|
{ error, attemptId: attempt.attemptId, remoteSessionId: session.remoteSessionId },
|
|
2314
2366
|
"Failed to read the remote task during startup recovery; treating the old local execution as interrupted.",
|
|
@@ -2564,6 +2616,7 @@ export class ProxySessionController {
|
|
|
2564
2616
|
record.title,
|
|
2565
2617
|
record.cwd,
|
|
2566
2618
|
this.agentLabel(record.agentName),
|
|
2619
|
+
record.model,
|
|
2567
2620
|
);
|
|
2568
2621
|
await this.outbound.startPendingTurn(
|
|
2569
2622
|
record.localSessionId,
|
|
@@ -2922,6 +2975,7 @@ export class ProxySessionController {
|
|
|
2922
2975
|
forkTitle,
|
|
2923
2976
|
source.cwd,
|
|
2924
2977
|
this.agentLabel(source.agentName),
|
|
2978
|
+
source.model,
|
|
2925
2979
|
);
|
|
2926
2980
|
|
|
2927
2981
|
try {
|
|
@@ -2990,6 +3044,7 @@ export class ProxySessionController {
|
|
|
2990
3044
|
}
|
|
2991
3045
|
|
|
2992
3046
|
private async forkSessionReference(contextKey: string, reference?: string): Promise<void> {
|
|
3047
|
+
await this.outbound.sendText(contextKey, FORK_TASK_STARTED_MESSAGE);
|
|
2993
3048
|
const plan = await this.prepareForkSession(contextKey, reference);
|
|
2994
3049
|
const forked = await this.forkSessionIntoContext(contextKey, plan);
|
|
2995
3050
|
const forkSourceLabel = plan.forkedFromHistoricalTurn
|
|
@@ -3050,35 +3105,24 @@ export class ProxySessionController {
|
|
|
3050
3105
|
if (!runtime.readRemoteSession && !source) {
|
|
3051
3106
|
throw new Error("当前 App Server Agent 不支持读取指定任务。");
|
|
3052
3107
|
}
|
|
3053
|
-
const
|
|
3108
|
+
const persistedCompletedTurnId = source
|
|
3109
|
+
? this.store.findLatestCompletedTurnId(source.localSessionId)
|
|
3110
|
+
: undefined;
|
|
3054
3111
|
const sourceLastSnapshot = turnViewSnapshot(
|
|
3055
3112
|
source?.lastTurnId ? this.store.getTurnSnapshot(source.lastTurnId) : undefined,
|
|
3056
3113
|
);
|
|
3057
|
-
const
|
|
3058
|
-
|
|
3059
|
-
: source?.lastTurnId
|
|
3114
|
+
const localCompletedTurnId = persistedCompletedTurnId
|
|
3115
|
+
?? (source?.lastTurnId
|
|
3060
3116
|
&& (source.lastTurnStatus === "completed" || sourceLastSnapshot?.status === "completed")
|
|
3061
|
-
?
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
}]
|
|
3069
|
-
: [];
|
|
3070
|
-
if (!remote && runtime.readRemoteSession && (!source || persistedCompletedTurns.length === 0)) {
|
|
3071
|
-
try {
|
|
3072
|
-
remote = await runtime.readRemoteSession(remoteSessionId);
|
|
3073
|
-
} catch (error) {
|
|
3074
|
-
if (localCompletedTurns.length === 0) throw error;
|
|
3075
|
-
this.logger.warn(
|
|
3076
|
-
{ error, contextKey, remoteSessionId, sourceTurnId: localCompletedTurns.at(-1)?.id },
|
|
3077
|
-
"Failed to read the complete source task before Fork; using the locally persisted completed Turn.",
|
|
3078
|
-
);
|
|
3079
|
-
}
|
|
3117
|
+
? source.lastTurnId
|
|
3118
|
+
: undefined)
|
|
3119
|
+
?? (source ? this.store.getForkHistorySource(source.localSessionId)?.sourceTurnId : undefined);
|
|
3120
|
+
if (!remote && !localCompletedTurnId) {
|
|
3121
|
+
remote = runtime.readRemoteForkSource
|
|
3122
|
+
? await runtime.readRemoteForkSource(remoteSessionId)
|
|
3123
|
+
: await runtime.readRemoteSession?.(remoteSessionId);
|
|
3080
3124
|
}
|
|
3081
|
-
const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ??
|
|
3125
|
+
const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurnId;
|
|
3082
3126
|
const latestSnapshot = turnViewSnapshot(latestTurnId ? this.store.getTurnSnapshot(latestTurnId) : undefined);
|
|
3083
3127
|
const isRunning = remote
|
|
3084
3128
|
? isRemoteSessionActive(remote)
|
|
@@ -3089,7 +3133,7 @@ export class ProxySessionController {
|
|
|
3089
3133
|
);
|
|
3090
3134
|
const lastTurnId = remote?.lastCompletedTurnId
|
|
3091
3135
|
?? (remote?.lastTurnStatus === "completed" ? remote.lastTurnId : undefined)
|
|
3092
|
-
??
|
|
3136
|
+
?? localCompletedTurnId;
|
|
3093
3137
|
if (!lastTurnId) {
|
|
3094
3138
|
if (isRunning) {
|
|
3095
3139
|
throw new Error(`${sourceLabel}正在执行,且还没有已完成轮次可供 fork。请等待当前轮次完成后重试。`);
|
|
@@ -3098,8 +3142,7 @@ export class ProxySessionController {
|
|
|
3098
3142
|
}
|
|
3099
3143
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
|
|
3100
3144
|
const forkedFromHistoricalTurn = lastTurnId !== latestTurnId;
|
|
3101
|
-
const completedTurn =
|
|
3102
|
-
.find((turn) => turn.id === lastTurnId);
|
|
3145
|
+
const completedTurn = remote?.completedTurns?.find((turn) => turn.id === lastTurnId);
|
|
3103
3146
|
|
|
3104
3147
|
const cwd = remote?.cwd || source?.cwd;
|
|
3105
3148
|
if (!cwd) throw new Error("指定的 App Server 任务没有可用的工作目录,暂时不能 fork。");
|
|
@@ -3145,29 +3188,11 @@ export class ProxySessionController {
|
|
|
3145
3188
|
sourceTurnPrompt,
|
|
3146
3189
|
sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
|
|
3147
3190
|
sourceTurnCompletedAt,
|
|
3148
|
-
sourceCompletedTurns: remote?.completedTurns
|
|
3149
|
-
?? (persistedCompletedTurns.length > 0 ? [] : localCompletedTurns),
|
|
3150
3191
|
sourceWasRunning: isRunning,
|
|
3151
3192
|
forkedFromHistoricalTurn,
|
|
3152
3193
|
};
|
|
3153
3194
|
}
|
|
3154
3195
|
|
|
3155
|
-
private localForkCompletedTurns(source: SessionRecord): RemoteCompletedTurnSummary[] {
|
|
3156
|
-
return this.store.listTaskTurnGraph(source.localSessionId)
|
|
3157
|
-
.slice()
|
|
3158
|
-
.reverse()
|
|
3159
|
-
.map((record) => {
|
|
3160
|
-
const snapshot = turnViewSnapshot(record.snapshot);
|
|
3161
|
-
const startedAt = snapshot?.startedAt ?? parseIsoTimestamp(record.updatedAt);
|
|
3162
|
-
return {
|
|
3163
|
-
id: record.turnId,
|
|
3164
|
-
prompt: latestUserPromptFromTurnView(snapshot),
|
|
3165
|
-
startedAt,
|
|
3166
|
-
completedAt: snapshot?.completedAt ?? startedAt,
|
|
3167
|
-
};
|
|
3168
|
-
});
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
3196
|
private async prepareForkGroupSession(
|
|
3172
3197
|
contextKey: string,
|
|
3173
3198
|
requestedTitle: string | undefined,
|
|
@@ -3270,7 +3295,6 @@ export class ProxySessionController {
|
|
|
3270
3295
|
contextKey: string,
|
|
3271
3296
|
plan: ForkSessionPlan,
|
|
3272
3297
|
): Promise<ForkSessionResult> {
|
|
3273
|
-
const sourceCompletedTurns = await this.resolveForkSourceCompletedTurns(plan);
|
|
3274
3298
|
const localSessionId = createId("sess");
|
|
3275
3299
|
const record = this.store.createSession({
|
|
3276
3300
|
localSessionId,
|
|
@@ -3312,12 +3336,7 @@ export class ProxySessionController {
|
|
|
3312
3336
|
forked.title ?? plan.forkTitle,
|
|
3313
3337
|
plan.cwd,
|
|
3314
3338
|
this.agentLabel(plan.agentName),
|
|
3315
|
-
|
|
3316
|
-
const sourceTurnHistoryCount = this.persistForkSourceHistory(
|
|
3317
|
-
contextKey,
|
|
3318
|
-
localSessionId,
|
|
3319
|
-
plan,
|
|
3320
|
-
sourceCompletedTurns,
|
|
3339
|
+
plan.model,
|
|
3321
3340
|
);
|
|
3322
3341
|
this.store.audit(contextKey, "session_forked", {
|
|
3323
3342
|
sourceLocalSessionId: plan.source?.localSessionId,
|
|
@@ -3326,7 +3345,6 @@ export class ProxySessionController {
|
|
|
3326
3345
|
sourceTurnPrompt: plan.sourceTurnPrompt,
|
|
3327
3346
|
sourceTurnStartedAt: plan.sourceTurnStartedAt,
|
|
3328
3347
|
sourceTurnCompletedAt: plan.sourceTurnCompletedAt,
|
|
3329
|
-
sourceTurnHistoryCount,
|
|
3330
3348
|
sourceWasRunning: plan.sourceWasRunning,
|
|
3331
3349
|
forkedLocalSessionId: localSessionId,
|
|
3332
3350
|
forkedRemoteSessionId: forked.remoteSessionId,
|
|
@@ -3342,54 +3360,6 @@ export class ProxySessionController {
|
|
|
3342
3360
|
}
|
|
3343
3361
|
}
|
|
3344
3362
|
|
|
3345
|
-
private async resolveForkSourceCompletedTurns(
|
|
3346
|
-
plan: ForkSessionPlan,
|
|
3347
|
-
): Promise<RemoteCompletedTurnSummary[]> {
|
|
3348
|
-
let completedTurns = plan.sourceCompletedTurns;
|
|
3349
|
-
if (completedTurns === undefined && plan.runtime.readRemoteSession) {
|
|
3350
|
-
try {
|
|
3351
|
-
completedTurns = (await plan.runtime.readRemoteSession(plan.remoteSessionId)).completedTurns ?? [];
|
|
3352
|
-
} catch (error) {
|
|
3353
|
-
this.logger.warn(
|
|
3354
|
-
{ error, remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId },
|
|
3355
|
-
"Failed to read the source task Turn history before Fork; keeping the Fork anchor fallback.",
|
|
3356
|
-
);
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
if (!completedTurns?.length) return [];
|
|
3360
|
-
const anchorIndex = completedTurns.findIndex((turn) => turn.id === plan.lastTurnId);
|
|
3361
|
-
if (anchorIndex < 0) {
|
|
3362
|
-
this.logger.warn(
|
|
3363
|
-
{ remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId },
|
|
3364
|
-
"The source task Turn history did not include the Fork anchor; keeping the Fork anchor fallback.",
|
|
3365
|
-
);
|
|
3366
|
-
return [];
|
|
3367
|
-
}
|
|
3368
|
-
return completedTurns.slice(0, anchorIndex + 1);
|
|
3369
|
-
}
|
|
3370
|
-
|
|
3371
|
-
private persistForkSourceHistory(
|
|
3372
|
-
contextKey: string,
|
|
3373
|
-
forkedLocalSessionId: string,
|
|
3374
|
-
plan: ForkSessionPlan,
|
|
3375
|
-
completedTurns: RemoteCompletedTurnSummary[],
|
|
3376
|
-
): number {
|
|
3377
|
-
if (completedTurns.length === 0) return 0;
|
|
3378
|
-
const localSessionId = plan.source?.localSessionId ?? forkedLocalSessionId;
|
|
3379
|
-
const historyContextKey = plan.source?.contextKey ?? contextKey;
|
|
3380
|
-
const fallbackStart = plan.sourceTurnStartedAt
|
|
3381
|
-
?? plan.sourceTurnCompletedAt
|
|
3382
|
-
?? Date.now() - completedTurns.length;
|
|
3383
|
-
return this.persistCompletedTurnHistory({
|
|
3384
|
-
localSessionId,
|
|
3385
|
-
contextKey: historyContextKey,
|
|
3386
|
-
agentName: plan.agentName,
|
|
3387
|
-
remoteSessionId: plan.remoteSessionId,
|
|
3388
|
-
completedTurns,
|
|
3389
|
-
fallbackStart,
|
|
3390
|
-
});
|
|
3391
|
-
}
|
|
3392
|
-
|
|
3393
3363
|
private persistCompletedTurnHistory(input: {
|
|
3394
3364
|
localSessionId: string;
|
|
3395
3365
|
contextKey: string;
|
|
@@ -3397,6 +3367,7 @@ export class ProxySessionController {
|
|
|
3397
3367
|
remoteSessionId: string;
|
|
3398
3368
|
completedTurns: RemoteCompletedTurnSummary[];
|
|
3399
3369
|
fallbackStart: number;
|
|
3370
|
+
replaceParents?: boolean;
|
|
3400
3371
|
}): number {
|
|
3401
3372
|
try {
|
|
3402
3373
|
this.store.importCompletedTurnHistory({
|
|
@@ -3404,6 +3375,7 @@ export class ProxySessionController {
|
|
|
3404
3375
|
contextKey: input.contextKey,
|
|
3405
3376
|
agentName: input.agentName,
|
|
3406
3377
|
remoteSessionId: input.remoteSessionId,
|
|
3378
|
+
replaceParents: input.replaceParents,
|
|
3407
3379
|
turns: input.completedTurns.map((turn, index) => {
|
|
3408
3380
|
const startedAt = turn.startedAt ?? input.fallbackStart + index;
|
|
3409
3381
|
const completedAt = turn.completedAt ?? startedAt;
|
|
@@ -3437,82 +3409,210 @@ export class ProxySessionController {
|
|
|
3437
3409
|
}
|
|
3438
3410
|
}
|
|
3439
3411
|
|
|
3440
|
-
private async
|
|
3412
|
+
private async hydrateTaskTurnHistory(
|
|
3413
|
+
current: SessionRecord,
|
|
3414
|
+
requiredCount = RESET_HISTORY_PAGE_SIZE,
|
|
3415
|
+
targetTurnId?: string,
|
|
3416
|
+
): Promise<boolean> {
|
|
3417
|
+
const existing = this.turnHistoryHydrations.get(current.localSessionId);
|
|
3418
|
+
if (existing) await existing;
|
|
3419
|
+
const hydration = (async () => {
|
|
3420
|
+
if (this.store.getForkHistorySource(current.localSessionId)) {
|
|
3421
|
+
return this.hydrateForkTurnHistory(current.contextKey, current, requiredCount, new Set(), targetTurnId);
|
|
3422
|
+
}
|
|
3423
|
+
return this.hydrateRemoteTurnHistory(current.contextKey, current, requiredCount, targetTurnId);
|
|
3424
|
+
})();
|
|
3425
|
+
this.turnHistoryHydrations.set(current.localSessionId, hydration);
|
|
3426
|
+
try {
|
|
3427
|
+
return await hydration;
|
|
3428
|
+
} finally {
|
|
3429
|
+
if (this.turnHistoryHydrations.get(current.localSessionId) === hydration) {
|
|
3430
|
+
this.turnHistoryHydrations.delete(current.localSessionId);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
private async hydrateForkTurnHistory(
|
|
3441
3436
|
contextKey: string,
|
|
3442
3437
|
current: SessionRecord,
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
if (
|
|
3438
|
+
requiredCount: number,
|
|
3439
|
+
visited = new Set<string>(),
|
|
3440
|
+
targetTurnId?: string,
|
|
3441
|
+
): Promise<boolean> {
|
|
3442
|
+
if (visited.has(current.localSessionId)) return false;
|
|
3443
|
+
visited.add(current.localSessionId);
|
|
3444
|
+
let source = this.store.getForkHistorySource(current.localSessionId);
|
|
3445
|
+
if (!source) return false;
|
|
3446
|
+
const completedHead = current.lastTurnStatus === "completed"
|
|
3447
|
+
? current.lastTurnId
|
|
3448
|
+
: this.store.findLatestCompletedTurnId(current.localSessionId);
|
|
3449
|
+
if (completedHead && completedHead !== source.sourceTurnId && current.remoteSessionId) {
|
|
3450
|
+
// Once the branch has its own completed work, its remote history is the
|
|
3451
|
+
// authority for missing local turns, including work continued in Desktop.
|
|
3452
|
+
source = {
|
|
3453
|
+
sourceLocalSessionId: current.localSessionId,
|
|
3454
|
+
sourceRemoteSessionId: current.remoteSessionId,
|
|
3455
|
+
sourceTurnId: completedHead,
|
|
3456
|
+
createdAt: current.createdAt,
|
|
3457
|
+
};
|
|
3458
|
+
}
|
|
3459
|
+
if (this.store.hasImportedForkTurnHistory(current.localSessionId, source.sourceRemoteSessionId, source.sourceTurnId)) return false;
|
|
3460
|
+
const localTurns = this.store.listTaskTurnGraphIndex(current.localSessionId);
|
|
3461
|
+
const availableCount = localTurns.length;
|
|
3462
|
+
const key = JSON.stringify([current.agentName, source.sourceRemoteSessionId, source.sourceTurnId]);
|
|
3463
|
+
let state = this.forkHistoryCursors.get(key) ?? { anchorFound: false, complete: false };
|
|
3464
|
+
const localAnchorIndex = localTurns.findIndex((turn) => turn.turnId === source.sourceTurnId);
|
|
3465
|
+
const ownTurnCount = source.sourceLocalSessionId === current.localSessionId
|
|
3466
|
+
? Math.max(0, localAnchorIndex)
|
|
3467
|
+
: localAnchorIndex >= 0 ? localAnchorIndex : availableCount;
|
|
3468
|
+
const neededInheritedCount = Math.max(0, requiredCount - ownTurnCount);
|
|
3469
|
+
if (!targetTurnId && localAnchorIndex >= 0 && availableCount >= requiredCount
|
|
3470
|
+
&& (!state.anchorFound || (state.loadedCount ?? 0) >= neededInheritedCount)) return !state.complete;
|
|
3471
|
+
const sourceSession = source.sourceLocalSessionId
|
|
3472
|
+
? this.store.getSession(source.sourceLocalSessionId)
|
|
3473
|
+
: undefined;
|
|
3474
|
+
// A branch with no new turns can reuse its ancestor's history, even before
|
|
3475
|
+
// the Agent materializes that intermediate branch.
|
|
3476
|
+
if (sourceSession && sourceSession.localSessionId !== current.localSessionId
|
|
3477
|
+
&& this.store.getForkHistorySource(sourceSession.localSessionId)?.sourceTurnId === source.sourceTurnId) {
|
|
3478
|
+
const hasMore = await this.hydrateForkTurnHistory(
|
|
3479
|
+
sourceSession.contextKey, sourceSession,
|
|
3480
|
+
this.store.listTaskTurnGraphIndex(sourceSession.localSessionId).length + requiredCount - availableCount,
|
|
3481
|
+
visited,
|
|
3482
|
+
targetTurnId,
|
|
3483
|
+
);
|
|
3484
|
+
if (!hasMore && this.store.hasImportedForkTurnHistory(sourceSession.localSessionId)) {
|
|
3485
|
+
this.store.audit(contextKey, "fork_turn_history_imported", {
|
|
3486
|
+
forkedLocalSessionId: current.localSessionId,
|
|
3487
|
+
sourceRemoteSessionId: source.sourceRemoteSessionId,
|
|
3488
|
+
sourceTurnId: source.sourceTurnId,
|
|
3489
|
+
reusedLocalSessionId: sourceSession.localSessionId,
|
|
3490
|
+
});
|
|
3491
|
+
}
|
|
3492
|
+
return hasMore;
|
|
3493
|
+
}
|
|
3494
|
+
if (!source.sourceRemoteSessionId) return false;
|
|
3448
3495
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
3449
|
-
if (runtime.kind !== "codex" || !runtime.
|
|
3496
|
+
if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries) return false;
|
|
3497
|
+
const seenCursors = new Set<string>();
|
|
3450
3498
|
try {
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3499
|
+
while (!state.complete && (targetTurnId
|
|
3500
|
+
? !this.store.hasTaskHistoryTurn(current.localSessionId, targetTurnId)
|
|
3501
|
+
: (state.loadedCount ?? 0) < neededInheritedCount)) {
|
|
3502
|
+
const remaining = targetTurnId ? RESET_HISTORY_PAGE_SIZE : neededInheritedCount - (state.loadedCount ?? 0);
|
|
3503
|
+
const page = await runtime.listRemoteTurnSummaries(source.sourceRemoteSessionId, {
|
|
3504
|
+
cursor: state.cursor, limit: Math.min(RESET_HISTORY_PAGE_SIZE, remaining),
|
|
3505
|
+
});
|
|
3506
|
+
if (page.nextCursor && (page.nextCursor === state.cursor || seenCursors.has(page.nextCursor))) {
|
|
3507
|
+
throw new Error("App Server repeated a Turn history cursor.");
|
|
3508
|
+
}
|
|
3509
|
+
const anchorIndex = state.anchorFound ? 0 : page.turns.findIndex((turn) => turn.id === source.sourceTurnId);
|
|
3510
|
+
const inheritedTurns = anchorIndex >= 0 ? page.turns.slice(anchorIndex) : [];
|
|
3511
|
+
const anchorFound = state.anchorFound || anchorIndex >= 0;
|
|
3512
|
+
if (!anchorFound && !page.nextCursor) {
|
|
3513
|
+
this.forkHistoryCursors.delete(key);
|
|
3514
|
+
throw new Error("The Fork source Turn was not found in the available history.");
|
|
3515
|
+
}
|
|
3516
|
+
if (inheritedTurns.length > 0) {
|
|
3517
|
+
const ownerId = sourceSession?.localSessionId ?? current.localSessionId;
|
|
3518
|
+
const imported = this.persistCompletedTurnHistory({
|
|
3519
|
+
localSessionId: ownerId,
|
|
3520
|
+
contextKey: sourceSession?.contextKey ?? contextKey,
|
|
3521
|
+
agentName: current.agentName,
|
|
3522
|
+
remoteSessionId: source.sourceRemoteSessionId,
|
|
3523
|
+
completedTurns: inheritedTurns.slice().reverse(),
|
|
3524
|
+
// Adjacent remote summaries repair gaps in locally inferred ancestry.
|
|
3525
|
+
replaceParents: true,
|
|
3526
|
+
fallbackStart: (source.sourceTurnStartedAt ?? source.sourceTurnCompletedAt
|
|
3527
|
+
?? parseIsoTimestamp(source.createdAt) ?? 0) - (state.loadedCount ?? 0) - inheritedTurns.length,
|
|
3528
|
+
});
|
|
3529
|
+
if (imported === 0) throw new Error("Failed to persist the requested Turn history page.");
|
|
3530
|
+
if (state.tailTurnId) {
|
|
3531
|
+
this.store.saveTurnParent(state.tailTurnId, ownerId, inheritedTurns[0]!.id, true);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
state = {
|
|
3535
|
+
cursor: page.nextCursor,
|
|
3536
|
+
anchorFound,
|
|
3537
|
+
tailTurnId: inheritedTurns.at(-1)?.id ?? state.tailTurnId,
|
|
3538
|
+
loadedCount: (state.loadedCount ?? 0) + inheritedTurns.length,
|
|
3539
|
+
complete: !page.nextCursor,
|
|
3540
|
+
};
|
|
3541
|
+
this.forkHistoryCursors.set(key, state);
|
|
3542
|
+
if (page.nextCursor) seenCursors.add(page.nextCursor);
|
|
3543
|
+
}
|
|
3544
|
+
if (state.complete) {
|
|
3470
3545
|
this.store.audit(contextKey, "fork_turn_history_imported", {
|
|
3471
3546
|
forkedLocalSessionId: current.localSessionId,
|
|
3472
3547
|
sourceRemoteSessionId: source.sourceRemoteSessionId,
|
|
3473
3548
|
sourceTurnId: source.sourceTurnId,
|
|
3474
|
-
importedTurnCount: imported,
|
|
3475
3549
|
});
|
|
3476
3550
|
}
|
|
3551
|
+
return !state.complete;
|
|
3477
3552
|
} catch (error) {
|
|
3553
|
+
if (error instanceof CodexVersionError) throw error;
|
|
3478
3554
|
this.logger.warn(
|
|
3479
3555
|
{ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId },
|
|
3480
|
-
"Failed to
|
|
3556
|
+
"Failed to load the requested Fork Turn history page; rendering the locally available history.",
|
|
3481
3557
|
);
|
|
3482
3558
|
}
|
|
3559
|
+
return true;
|
|
3483
3560
|
}
|
|
3484
3561
|
|
|
3485
3562
|
private async hydrateRemoteTurnHistory(
|
|
3486
3563
|
contextKey: string,
|
|
3487
3564
|
current: SessionRecord,
|
|
3488
|
-
|
|
3489
|
-
|
|
3565
|
+
requiredCount: number,
|
|
3566
|
+
targetTurnId?: string,
|
|
3567
|
+
): Promise<boolean> {
|
|
3568
|
+
if (!current.remoteSessionId) return false;
|
|
3569
|
+
const key = JSON.stringify([current.agentName, current.remoteSessionId, "turn-card"]);
|
|
3490
3570
|
const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
&& this.store.hasImportedForkTurnHistory(current.localSessionId)) return;
|
|
3571
|
+
let state = this.forkHistoryCursors.get(key);
|
|
3572
|
+
if (refreshedAt === undefined || Date.now() - refreshedAt >= REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS) state = undefined;
|
|
3573
|
+
state ??= { anchorFound: true, complete: false };
|
|
3495
3574
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
3496
|
-
if (runtime.kind !== "codex" || !runtime.
|
|
3575
|
+
if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries) return false;
|
|
3576
|
+
const seen = new Set<string>();
|
|
3497
3577
|
try {
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3578
|
+
while (!state.complete && (targetTurnId
|
|
3579
|
+
? !this.store.hasTaskHistoryTurn(current.localSessionId, targetTurnId)
|
|
3580
|
+
: (state.loadedCount ?? 0) < requiredCount)) {
|
|
3581
|
+
const page = await runtime.listRemoteTurnSummaries(current.remoteSessionId, {
|
|
3582
|
+
cursor: state.cursor,
|
|
3583
|
+
limit: targetTurnId ? RESET_HISTORY_PAGE_SIZE : Math.min(RESET_HISTORY_PAGE_SIZE, requiredCount - (state.loadedCount ?? 0)),
|
|
3584
|
+
});
|
|
3585
|
+
if (page.nextCursor && (page.nextCursor === state.cursor || seen.has(page.nextCursor))) {
|
|
3586
|
+
throw new Error("App Server repeated a Turn history cursor.");
|
|
3587
|
+
}
|
|
3588
|
+
if (page.turns.length) {
|
|
3589
|
+
const imported = this.persistCompletedTurnHistory({
|
|
3590
|
+
localSessionId: current.localSessionId, contextKey, agentName: current.agentName,
|
|
3591
|
+
remoteSessionId: current.remoteSessionId,
|
|
3592
|
+
completedTurns: page.turns.slice().reverse(), replaceParents: true,
|
|
3593
|
+
fallbackStart: (parseIsoTimestamp(current.createdAt) ?? 0) - (state.loadedCount ?? 0) - page.turns.length,
|
|
3594
|
+
});
|
|
3595
|
+
if (!imported) throw new Error("Failed to persist the requested Turn history page.");
|
|
3596
|
+
if (state.tailTurnId) this.store.saveTurnParent(state.tailTurnId, current.localSessionId, page.turns[0]!.id, true);
|
|
3597
|
+
}
|
|
3598
|
+
state = {
|
|
3599
|
+
anchorFound: true, cursor: page.nextCursor, complete: !page.nextCursor,
|
|
3600
|
+
loadedCount: (state.loadedCount ?? 0) + page.turns.length,
|
|
3601
|
+
tailTurnId: page.turns.at(-1)?.id ?? state.tailTurnId,
|
|
3602
|
+
};
|
|
3603
|
+
this.forkHistoryCursors.set(key, state);
|
|
3604
|
+
this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
|
|
3605
|
+
if (page.nextCursor) seen.add(page.nextCursor);
|
|
3606
|
+
}
|
|
3607
|
+
return !state.complete;
|
|
3510
3608
|
} catch (error) {
|
|
3609
|
+
if (error instanceof CodexVersionError) throw error;
|
|
3511
3610
|
this.logger.warn(
|
|
3512
3611
|
{ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId },
|
|
3513
3612
|
"Failed to hydrate App Server Turn history; rendering the locally available history.",
|
|
3514
3613
|
);
|
|
3515
3614
|
}
|
|
3615
|
+
return true;
|
|
3516
3616
|
}
|
|
3517
3617
|
|
|
3518
3618
|
private async createProjectSessionFromReference(contextKey: string, reference: string): Promise<void> {
|
|
@@ -3559,7 +3659,7 @@ export class ProxySessionController {
|
|
|
3559
3659
|
agentName = source.agentName;
|
|
3560
3660
|
runtime = this.runtimes.forAgent(agentName);
|
|
3561
3661
|
} else {
|
|
3562
|
-
const resolved = await this.resolveRemoteCodexSession(taskId);
|
|
3662
|
+
const resolved = await this.resolveRemoteCodexSession(taskId, { view: "metadata" });
|
|
3563
3663
|
agentName = resolved.agentName;
|
|
3564
3664
|
runtime = resolved.runtime;
|
|
3565
3665
|
remote = resolved.remote;
|
|
@@ -3568,9 +3668,9 @@ export class ProxySessionController {
|
|
|
3568
3668
|
const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
|
|
3569
3669
|
if (!remote && runtime.readRemoteSession && remoteSessionId) {
|
|
3570
3670
|
try {
|
|
3571
|
-
remote = await runtime.readRemoteSession(remoteSessionId);
|
|
3671
|
+
remote = await runtime.readRemoteSession(remoteSessionId, "metadata");
|
|
3572
3672
|
} catch (error) {
|
|
3573
|
-
if (!source) throw error;
|
|
3673
|
+
if (!source || error instanceof CodexVersionError) throw error;
|
|
3574
3674
|
this.logger.warn(
|
|
3575
3675
|
{ error, contextKey, taskId: remoteSessionId },
|
|
3576
3676
|
"Failed to refresh the source task before creating a project task; using the local project path.",
|
|
@@ -3754,6 +3854,7 @@ export class ProxySessionController {
|
|
|
3754
3854
|
initialTitle,
|
|
3755
3855
|
sessionCwd,
|
|
3756
3856
|
this.agentLabel(agentName),
|
|
3857
|
+
resolvedExecutionSettings.model,
|
|
3757
3858
|
);
|
|
3758
3859
|
const runtime = this.runtimes.forAgent(agentName);
|
|
3759
3860
|
try {
|
|
@@ -3771,6 +3872,7 @@ export class ProxySessionController {
|
|
|
3771
3872
|
permissionMode: resolvedExecutionSettings.permissionMode ?? "auto",
|
|
3772
3873
|
});
|
|
3773
3874
|
this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
|
|
3875
|
+
this.outbound.updateSessionModel(localSessionId, session.model);
|
|
3774
3876
|
const saved = this.store.getSession(localSessionId) ?? record;
|
|
3775
3877
|
if (announce) {
|
|
3776
3878
|
const task = initialTitle ? `${initialTitle}(${session.remoteSessionId})` : session.remoteSessionId;
|
|
@@ -3907,6 +4009,7 @@ export class ProxySessionController {
|
|
|
3907
4009
|
if (!userId?.startsWith("ou_")) {
|
|
3908
4010
|
throw new Error("/forkgroup 只能由具有 open_id 的飞书用户消息触发。");
|
|
3909
4011
|
}
|
|
4012
|
+
await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
|
|
3910
4013
|
const prepared = await this.prepareForkGroupSession(sourceContextKey, requestedTitle, incomingMessage);
|
|
3911
4014
|
await this.forkPreparedSessionToFeishuGroup(
|
|
3912
4015
|
sourceContextKey,
|
|
@@ -3924,6 +4027,7 @@ export class ProxySessionController {
|
|
|
3924
4027
|
if (!userId?.startsWith("ou_")) {
|
|
3925
4028
|
throw new Error("ForkGroup 只能由具有 open_id 的飞书用户触发。");
|
|
3926
4029
|
}
|
|
4030
|
+
await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
|
|
3927
4031
|
const plan = await this.prepareForkSession(sourceContextKey, reference);
|
|
3928
4032
|
await this.forkPreparedSessionToFeishuGroup(
|
|
3929
4033
|
sourceContextKey,
|
|
@@ -4041,6 +4145,7 @@ export class ProxySessionController {
|
|
|
4041
4145
|
record.title,
|
|
4042
4146
|
record.cwd,
|
|
4043
4147
|
this.agentLabel(record.agentName),
|
|
4148
|
+
record.model,
|
|
4044
4149
|
);
|
|
4045
4150
|
const loading = (async (): Promise<LoadedSession> => {
|
|
4046
4151
|
if (record.lastTurnId) {
|
|
@@ -4076,7 +4181,8 @@ export class ProxySessionController {
|
|
|
4076
4181
|
});
|
|
4077
4182
|
} catch (error) {
|
|
4078
4183
|
if (activeWriterThreadId(error)) throw error;
|
|
4079
|
-
if (!(agent.kind === "app-server" && !record.lastTurnId
|
|
4184
|
+
if (!(agent.kind === "app-server" && !record.lastTurnId
|
|
4185
|
+
&& !this.store.getForkHistorySource(record.localSessionId) && isMissingRolloutError(error))) throw error;
|
|
4080
4186
|
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
4081
4187
|
session = await runtime.createSession({
|
|
4082
4188
|
localSessionId: record.localSessionId,
|
|
@@ -4190,14 +4296,20 @@ export class ProxySessionController {
|
|
|
4190
4296
|
});
|
|
4191
4297
|
}
|
|
4192
4298
|
|
|
4299
|
+
const enrichedEvent: RuntimeEvent = event.type === "context_compaction"
|
|
4300
|
+
? {
|
|
4301
|
+
...event,
|
|
4302
|
+
turnCount: event.turnCount ?? this.store.countTaskTurns(event.sessionId),
|
|
4303
|
+
}
|
|
4304
|
+
: event;
|
|
4193
4305
|
const presentationEvent = event.type === "turn_failed"
|
|
4194
4306
|
&& isRetryableLlmTurnFailure(event.message)
|
|
4195
4307
|
&& (failedAttempt?.retryCount ?? 0) >= MAX_LLM_TURN_RETRIES
|
|
4196
4308
|
? {
|
|
4197
|
-
...
|
|
4309
|
+
...enrichedEvent,
|
|
4198
4310
|
message: `${event.message}\n\n已自动重试 ${MAX_LLM_TURN_RETRIES} 次,仍未成功。`,
|
|
4199
4311
|
}
|
|
4200
|
-
:
|
|
4312
|
+
: enrichedEvent;
|
|
4201
4313
|
let presentationError: unknown;
|
|
4202
4314
|
try {
|
|
4203
4315
|
await this.outbound.onEvent(presentationEvent);
|
|
@@ -4554,6 +4666,7 @@ export class ProxySessionController {
|
|
|
4554
4666
|
|
|
4555
4667
|
private async cancel(contextKey: string): Promise<void> {
|
|
4556
4668
|
const record = this.requireCurrentSession(contextKey);
|
|
4669
|
+
await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
|
|
4557
4670
|
await this.cancelSession(record);
|
|
4558
4671
|
}
|
|
4559
4672
|
|
|
@@ -4566,6 +4679,7 @@ export class ProxySessionController {
|
|
|
4566
4679
|
const goal = await runtime.getGoal(record.localSessionId);
|
|
4567
4680
|
if (goal?.status === "active") await runtime.setGoal(record.localSessionId, { status: "paused" });
|
|
4568
4681
|
} catch (error) {
|
|
4682
|
+
if (error instanceof CodexVersionError) throw error;
|
|
4569
4683
|
this.logger.warn(
|
|
4570
4684
|
{ error, sessionId: record.localSessionId },
|
|
4571
4685
|
"Failed to pause the active Agent goal before interrupting its turn.",
|
|
@@ -4580,6 +4694,7 @@ export class ProxySessionController {
|
|
|
4580
4694
|
? remote.lastTurnId
|
|
4581
4695
|
: undefined;
|
|
4582
4696
|
} catch (error) {
|
|
4697
|
+
if (error instanceof CodexVersionError) throw error;
|
|
4583
4698
|
this.logger.warn(
|
|
4584
4699
|
{ error, sessionId: record.localSessionId, remoteSessionId: record.remoteSessionId },
|
|
4585
4700
|
"Failed to inspect the current App Server turn before interrupting; using the locally tracked turn.",
|
|
@@ -4627,9 +4742,15 @@ export class ProxySessionController {
|
|
|
4627
4742
|
turnId: string,
|
|
4628
4743
|
): Promise<void> {
|
|
4629
4744
|
if (!sessionId || !turnId) throw new Error("无效的 Reset 请求。请使用最新的思考卡片重试。");
|
|
4745
|
+
await this.runSessionReset(sessionId, async () => {
|
|
4746
|
+
await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
|
|
4747
|
+
await this.performCurrentSessionReset(contextKey, sessionId, turnId);
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
private async runSessionReset(sessionId: string, operation: () => Promise<void>): Promise<void> {
|
|
4630
4752
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
4631
|
-
const reset = previous.catch(() => undefined).then(
|
|
4632
|
-
this.performCurrentSessionReset(contextKey, sessionId, turnId));
|
|
4753
|
+
const reset = previous.catch(() => undefined).then(operation);
|
|
4633
4754
|
this.sessionResets.set(sessionId, reset);
|
|
4634
4755
|
try {
|
|
4635
4756
|
await reset;
|
|
@@ -4638,6 +4759,19 @@ export class ProxySessionController {
|
|
|
4638
4759
|
}
|
|
4639
4760
|
}
|
|
4640
4761
|
|
|
4762
|
+
private async waitForCurrentSessionReset(contextKey: string): Promise<void> {
|
|
4763
|
+
const sessionId = this.currentSession(contextKey)?.localSessionId;
|
|
4764
|
+
if (!sessionId) return;
|
|
4765
|
+
|
|
4766
|
+
let reset = this.sessionResets.get(sessionId);
|
|
4767
|
+
while (reset) {
|
|
4768
|
+
await reset.catch(() => undefined);
|
|
4769
|
+
const next = this.sessionResets.get(sessionId);
|
|
4770
|
+
if (!next || next === reset) return;
|
|
4771
|
+
reset = next;
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
|
|
4641
4775
|
private async resetFromHistoryCard(
|
|
4642
4776
|
contextKey: string,
|
|
4643
4777
|
sessionId: string,
|
|
@@ -4654,16 +4788,11 @@ export class ProxySessionController {
|
|
|
4654
4788
|
|
|
4655
4789
|
this.resetHistoryOperations.set(sessionId, turnId);
|
|
4656
4790
|
try {
|
|
4657
|
-
await this.
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
this.
|
|
4661
|
-
}
|
|
4662
|
-
throw error;
|
|
4663
|
-
}
|
|
4664
|
-
|
|
4665
|
-
try {
|
|
4666
|
-
await this.resetCurrentSessionToTurn(contextKey, sessionId, turnId);
|
|
4791
|
+
await this.runSessionReset(sessionId, async () => {
|
|
4792
|
+
await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
|
|
4793
|
+
await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
|
|
4794
|
+
await this.performCurrentSessionReset(contextKey, sessionId, turnId);
|
|
4795
|
+
});
|
|
4667
4796
|
} catch (error) {
|
|
4668
4797
|
if (this.resetHistoryOperations.get(sessionId) === turnId) {
|
|
4669
4798
|
this.resetHistoryOperations.delete(sessionId);
|
|
@@ -4707,9 +4836,9 @@ export class ProxySessionController {
|
|
|
4707
4836
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
4708
4837
|
}
|
|
4709
4838
|
|
|
4710
|
-
const historyTurn = this.store.
|
|
4839
|
+
const historyTurn = this.store.listTaskTurnGraphIndex(current.localSessionId)
|
|
4711
4840
|
.find((turn) => turn.turnId === turnId);
|
|
4712
|
-
const snapshot =
|
|
4841
|
+
const snapshot = historyTurn ? this.store.getTurnPromptSummary(turnId) : undefined;
|
|
4713
4842
|
if (!snapshot || snapshot.status !== "completed") {
|
|
4714
4843
|
throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
|
|
4715
4844
|
}
|
|
@@ -4759,6 +4888,7 @@ export class ProxySessionController {
|
|
|
4759
4888
|
forked.title ?? current.title,
|
|
4760
4889
|
current.cwd,
|
|
4761
4890
|
this.agentLabel(current.agentName),
|
|
4891
|
+
current.model,
|
|
4762
4892
|
);
|
|
4763
4893
|
this.store.audit(contextKey, "session_reset_to_turn", {
|
|
4764
4894
|
localSessionId: current.localSessionId,
|
|
@@ -4822,12 +4952,13 @@ export class ProxySessionController {
|
|
|
4822
4952
|
};
|
|
4823
4953
|
if (record) {
|
|
4824
4954
|
const loaded = await this.loadSession(record);
|
|
4825
|
-
const models = await loaded.runtime.listModels();
|
|
4826
|
-
const currentModel = models.find((model) => model.id === loaded.session.model)
|
|
4827
|
-
?? models.find((model) => model.isDefault);
|
|
4828
4955
|
const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
|
|
4829
4956
|
const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
|
|
4830
4957
|
const currentProvider = loaded.session.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
|
|
4958
|
+
const models = await loaded.runtime.listModels(currentProvider, loaded.session.model);
|
|
4959
|
+
const currentModel = models.find((model) => model.id === loaded.session.model)
|
|
4960
|
+
?? models.find((model) => model.isDefault)
|
|
4961
|
+
?? models[0];
|
|
4831
4962
|
const currentEffort = currentModel?.supportedReasoningEfforts.some(
|
|
4832
4963
|
(option) => option.value === loaded.session.reasoningEffort,
|
|
4833
4964
|
)
|
|
@@ -4893,24 +5024,19 @@ export class ProxySessionController {
|
|
|
4893
5024
|
const record = options.sessionId
|
|
4894
5025
|
? this.requireSession(contextKey, options.sessionId)
|
|
4895
5026
|
: this.requireCurrentSession(contextKey);
|
|
5027
|
+
await this.outbound.sendText(contextKey, `正在切换到 Provider ${modelProvider},请稍后。`);
|
|
4896
5028
|
const loaded = await this.loadSession(record);
|
|
4897
5029
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4898
|
-
const models = await loaded.runtime.listModels();
|
|
4899
|
-
const model = models
|
|
4900
|
-
?? models.find((candidate) => candidate.isDefault);
|
|
5030
|
+
const models = await loaded.runtime.listModels(modelProvider, loaded.session.model);
|
|
5031
|
+
const model = selectProviderModel(models, loaded.session.model);
|
|
4901
5032
|
if (!model) throw new Error("当前运行时没有可用于 Provider 切换的模型。");
|
|
4902
|
-
const effort = model.
|
|
4903
|
-
(candidate) => candidate.value === loaded.session.reasoningEffort,
|
|
4904
|
-
)
|
|
4905
|
-
? loaded.session.reasoningEffort
|
|
4906
|
-
: model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
|
|
4907
|
-
if (!effort) throw new Error(`模型 ${model.id} 没有可用于 Provider 切换的思考强度。`);
|
|
5033
|
+
const effort = selectProviderReasoningEffort(model, loaded.session.reasoningEffort);
|
|
4908
5034
|
await this.applyProviderSettings(contextKey, {
|
|
4909
5035
|
provider: modelProvider,
|
|
4910
5036
|
model: model.id,
|
|
4911
5037
|
effort,
|
|
4912
5038
|
mode: loaded.session.permissionMode,
|
|
4913
|
-
}, options);
|
|
5039
|
+
}, options, false);
|
|
4914
5040
|
}
|
|
4915
5041
|
|
|
4916
5042
|
private async openProviderSelector(
|
|
@@ -4929,11 +5055,11 @@ export class ProxySessionController {
|
|
|
4929
5055
|
): Promise<void> {
|
|
4930
5056
|
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
4931
5057
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4932
|
-
const models = await loaded.runtime.listModels();
|
|
5058
|
+
const models = await loaded.runtime.listModels(modelProvider, loaded.session.model);
|
|
4933
5059
|
const card = this.cardRenderer.renderModelSelector({
|
|
4934
5060
|
sessionId,
|
|
4935
5061
|
contextKey,
|
|
4936
|
-
currentModel: loaded.session.model
|
|
5062
|
+
currentModel: selectProviderModel(models, loaded.session.model)?.id,
|
|
4937
5063
|
reasoningEffort: loaded.session.reasoningEffort,
|
|
4938
5064
|
models,
|
|
4939
5065
|
modelProvider,
|
|
@@ -4953,21 +5079,24 @@ export class ProxySessionController {
|
|
|
4953
5079
|
): Promise<void> {
|
|
4954
5080
|
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
4955
5081
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4956
|
-
const models = await loaded.runtime.listModels();
|
|
5082
|
+
const models = await loaded.runtime.listModels(modelProvider, model);
|
|
4957
5083
|
const selected = models.find((candidate) => candidate.id === model);
|
|
4958
5084
|
if (!selected) throw new Error(`未知模型:${model}`);
|
|
4959
|
-
const
|
|
5085
|
+
const options = selected.supportedReasoningEfforts.length > 0
|
|
5086
|
+
? selected.supportedReasoningEfforts
|
|
5087
|
+
: [{ value: selectProviderReasoningEffort(selected, loaded.session.reasoningEffort) }];
|
|
5088
|
+
const currentEffort = options.some(
|
|
4960
5089
|
(option) => option.value === loaded.session.reasoningEffort,
|
|
4961
5090
|
)
|
|
4962
5091
|
? loaded.session.reasoningEffort
|
|
4963
|
-
: selected.defaultReasoningEffort;
|
|
5092
|
+
: selected.defaultReasoningEffort ?? options[0]?.value;
|
|
4964
5093
|
const card = this.cardRenderer.renderReasoningSelector({
|
|
4965
5094
|
sessionId,
|
|
4966
5095
|
contextKey,
|
|
4967
5096
|
modelProvider,
|
|
4968
5097
|
model,
|
|
4969
5098
|
currentEffort,
|
|
4970
|
-
options
|
|
5099
|
+
options,
|
|
4971
5100
|
permissionMode,
|
|
4972
5101
|
unifiedSettings: true,
|
|
4973
5102
|
});
|
|
@@ -5003,28 +5132,22 @@ export class ProxySessionController {
|
|
|
5003
5132
|
contextKey: string,
|
|
5004
5133
|
settings: { provider: string; model: string; effort: string; mode: PermissionMode },
|
|
5005
5134
|
options: ModelCardOptions = {},
|
|
5135
|
+
announceStart = true,
|
|
5006
5136
|
): Promise<void> {
|
|
5007
5137
|
const record = options.sessionId
|
|
5008
5138
|
? this.requireSession(contextKey, options.sessionId)
|
|
5009
5139
|
: this.requireCurrentSession(contextKey);
|
|
5140
|
+
if (announceStart) {
|
|
5141
|
+
await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
5142
|
+
}
|
|
5010
5143
|
const loaded = await this.loadSession(record);
|
|
5011
5144
|
await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
5012
|
-
|
|
5013
|
-
throw new Error("当前运行时不支持 Provider 设置。");
|
|
5014
|
-
}
|
|
5015
|
-
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
|
|
5145
|
+
const session = await this.changeProviderSettings(loaded, {
|
|
5016
5146
|
modelProvider: settings.provider,
|
|
5017
5147
|
model: settings.model,
|
|
5018
5148
|
reasoningEffort: settings.effort,
|
|
5019
5149
|
permissionMode: settings.mode,
|
|
5020
5150
|
});
|
|
5021
|
-
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
5022
|
-
modelProvider: session.modelProvider ?? settings.provider,
|
|
5023
|
-
model: session.model ?? settings.model,
|
|
5024
|
-
reasoningEffort: session.reasoningEffort ?? settings.effort,
|
|
5025
|
-
permissionMode: session.permissionMode,
|
|
5026
|
-
});
|
|
5027
|
-
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
5028
5151
|
const notice = [
|
|
5029
5152
|
`Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
|
|
5030
5153
|
`模型 ${cardCode(session.model ?? settings.model)}`,
|
|
@@ -5043,6 +5166,30 @@ export class ProxySessionController {
|
|
|
5043
5166
|
}
|
|
5044
5167
|
}
|
|
5045
5168
|
|
|
5169
|
+
private async changeProviderSettings(loaded: LoadedSession, settings: RuntimeExecutionSettings): Promise<RuntimeSession> {
|
|
5170
|
+
if (!loaded.runtime.setExecutionSettings) throw new Error("当前运行时不支持 Provider 设置。");
|
|
5171
|
+
const persist = async (candidate: RuntimeSession): Promise<void> => {
|
|
5172
|
+
assertProviderSettingsApplied(candidate, settings);
|
|
5173
|
+
const agent = this.ensureAgent(loaded.record.agentName);
|
|
5174
|
+
const previousDefaults = agent.defaults;
|
|
5175
|
+
await this.persistAgentExecutionDefaults(loaded.record.localSessionId, candidate);
|
|
5176
|
+
try {
|
|
5177
|
+
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
5178
|
+
remoteSessionId: candidate.remoteSessionId,
|
|
5179
|
+
modelProvider: candidate.modelProvider,
|
|
5180
|
+
model: candidate.model,
|
|
5181
|
+
reasoningEffort: candidate.reasoningEffort,
|
|
5182
|
+
permissionMode: candidate.permissionMode,
|
|
5183
|
+
});
|
|
5184
|
+
} catch (error) {
|
|
5185
|
+
await this.lifecycle?.persistAgentExecutionDefaults?.(loaded.record.agentName, previousDefaults ?? {});
|
|
5186
|
+
agent.defaults = previousDefaults;
|
|
5187
|
+
throw error;
|
|
5188
|
+
}
|
|
5189
|
+
};
|
|
5190
|
+
return loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5046
5193
|
private async assertProviderModelSettings(
|
|
5047
5194
|
loaded: LoadedSession,
|
|
5048
5195
|
modelProvider: string,
|
|
@@ -5050,9 +5197,10 @@ export class ProxySessionController {
|
|
|
5050
5197
|
effort: string,
|
|
5051
5198
|
): Promise<void> {
|
|
5052
5199
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5053
|
-
const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
|
|
5200
|
+
const selected = (await loaded.runtime.listModels(modelProvider, model)).find((candidate) => candidate.id === model);
|
|
5054
5201
|
if (!selected) throw new Error(`未知模型:${model}`);
|
|
5055
|
-
if (
|
|
5202
|
+
if (selected.supportedReasoningEfforts.length > 0
|
|
5203
|
+
&& !selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
|
|
5056
5204
|
const supported = selected.supportedReasoningEfforts.map((option) => option.value).join("、") || "无";
|
|
5057
5205
|
throw new Error(`模型 ${model} 不支持思考强度 ${effort}。支持的强度:${supported}`);
|
|
5058
5206
|
}
|
|
@@ -5086,12 +5234,13 @@ export class ProxySessionController {
|
|
|
5086
5234
|
? this.requireSession(contextKey, options.sessionId)
|
|
5087
5235
|
: this.requireCurrentSession(contextKey);
|
|
5088
5236
|
const loaded = await this.loadSession(record);
|
|
5089
|
-
const models = await loaded.runtime.listModels();
|
|
5237
|
+
const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
|
|
5090
5238
|
const selected = models.find((item) => item.id === model);
|
|
5091
5239
|
if (!selected) throw new Error(`未知模型:${model}`);
|
|
5092
5240
|
const currentEffort = loaded.session.reasoningEffort;
|
|
5093
5241
|
const compatible = currentEffort
|
|
5094
|
-
? selected.supportedReasoningEfforts.
|
|
5242
|
+
? selected.supportedReasoningEfforts.length === 0
|
|
5243
|
+
|| selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
|
|
5095
5244
|
: false;
|
|
5096
5245
|
const nextEffort = compatible ? currentEffort : selected.defaultReasoningEffort;
|
|
5097
5246
|
|
|
@@ -5463,7 +5612,9 @@ export class ProxySessionController {
|
|
|
5463
5612
|
private async setTitle(contextKey: string, title: string): Promise<void> {
|
|
5464
5613
|
const normalizedTitle = normalizeTaskTitle(title);
|
|
5465
5614
|
if (!normalizedTitle) throw new Error("任务标题不能为空。");
|
|
5466
|
-
const
|
|
5615
|
+
const current = this.requireCurrentSession(contextKey);
|
|
5616
|
+
await this.outbound.sendText(contextKey, CHANGE_TITLE_STARTED_MESSAGE);
|
|
5617
|
+
const loaded = await this.loadSession(current);
|
|
5467
5618
|
if (loaded.runtime.setTitle) await loaded.runtime.setTitle(loaded.record.localSessionId, normalizedTitle);
|
|
5468
5619
|
else loaded.session.title = normalizedTitle;
|
|
5469
5620
|
this.store.updateRuntimeSession(loaded.record.localSessionId, { title: normalizedTitle });
|
|
@@ -5520,7 +5671,7 @@ export class ProxySessionController {
|
|
|
5520
5671
|
? this.requireSession(contextKey, options.sessionId)
|
|
5521
5672
|
: this.requireCurrentSession(contextKey);
|
|
5522
5673
|
const loaded = await this.loadSession(record);
|
|
5523
|
-
const models = await loaded.runtime.listModels();
|
|
5674
|
+
const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
|
|
5524
5675
|
const currentModel = models.find((item) => item.id === loaded.session.model)
|
|
5525
5676
|
?? models.find((item) => item.isDefault);
|
|
5526
5677
|
if (!currentModel) throw new Error("当前运行时没有可配置思考强度的模型。");
|
|
@@ -5585,6 +5736,71 @@ export class ProxySessionController {
|
|
|
5585
5736
|
);
|
|
5586
5737
|
}
|
|
5587
5738
|
|
|
5739
|
+
private taskTurnIds(current: SessionRecord): string[] {
|
|
5740
|
+
const ids = this.store.listTaskTurnGraphIndex(current.localSessionId).map((turn) => turn.turnId);
|
|
5741
|
+
if (current.lastTurnStatus === "running" && current.lastTurnId && !ids.includes(current.lastTurnId)) {
|
|
5742
|
+
ids.unshift(current.lastTurnId);
|
|
5743
|
+
}
|
|
5744
|
+
return ids;
|
|
5745
|
+
}
|
|
5746
|
+
|
|
5747
|
+
private runningTurnSnapshot(current: SessionRecord, turnId: string): TurnViewState {
|
|
5748
|
+
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
5749
|
+
if (snapshot) return snapshot;
|
|
5750
|
+
const attempt = this.store.findIncompleteTurnAttemptByTurnId(turnId);
|
|
5751
|
+
const startedAt = Date.parse(attempt?.createdAt ?? "");
|
|
5752
|
+
return {
|
|
5753
|
+
sessionId: current.localSessionId,
|
|
5754
|
+
turnId,
|
|
5755
|
+
taskTitle: current.title,
|
|
5756
|
+
prompt: attempt?.promptText ?? current.title,
|
|
5757
|
+
status: "running",
|
|
5758
|
+
startedAt: Number.isFinite(startedAt) ? startedAt : Date.now(),
|
|
5759
|
+
assistantText: "", plan: [], activities: [], completedTools: [], failedTools: [], fileSummary: [],
|
|
5760
|
+
};
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5763
|
+
private async openTurnDetails(contextKey: string, reference: string): Promise<void> {
|
|
5764
|
+
const current = this.requireCurrentSession(contextKey);
|
|
5765
|
+
let turnId: string | undefined;
|
|
5766
|
+
if (/^\d+$/.test(reference)) {
|
|
5767
|
+
const index = Number(reference);
|
|
5768
|
+
const runningCount = current.lastTurnStatus === "running" && current.lastTurnId ? 1 : 0;
|
|
5769
|
+
const hasMore = index > runningCount && this.isCodexSession(current)
|
|
5770
|
+
? await this.hydrateTaskTurnHistory(current, index - runningCount)
|
|
5771
|
+
: false;
|
|
5772
|
+
turnId = this.taskTurnIds(current)[index - 1];
|
|
5773
|
+
if (!turnId) {
|
|
5774
|
+
throw new Error(hasMore
|
|
5775
|
+
? "轮次历史暂未加载完成,请稍后重试。"
|
|
5776
|
+
: `未找到序号为 ${reference} 的轮次。请发送 /turn 查看当前轮次序号。`);
|
|
5777
|
+
}
|
|
5778
|
+
} else {
|
|
5779
|
+
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(reference));
|
|
5780
|
+
if (snapshot && (snapshot.sessionId === current.localSessionId
|
|
5781
|
+
|| this.store.hasTaskHistoryTurn(current.localSessionId, reference))) {
|
|
5782
|
+
turnId = reference;
|
|
5783
|
+
} else if (current.lastTurnStatus === "running" && current.lastTurnId === reference) {
|
|
5784
|
+
turnId = reference;
|
|
5785
|
+
} else if (this.isCodexSession(current) && (!snapshot || this.store.getForkHistorySource(current.localSessionId))) {
|
|
5786
|
+
const hasMore = await this.hydrateTaskTurnHistory(current, 0, reference);
|
|
5787
|
+
if (this.store.hasTaskHistoryTurn(current.localSessionId, reference)) turnId = reference;
|
|
5788
|
+
else if (hasMore) throw new Error("轮次历史暂未加载完成,请稍后重试。");
|
|
5789
|
+
}
|
|
5790
|
+
if (!turnId && snapshot) throw new Error("这个 Turn 不属于当前任务的历史。请先切换到对应任务。");
|
|
5791
|
+
if (!turnId) throw new Error(`未找到当前任务的 Turn:${reference}`);
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5794
|
+
if (current.lastTurnStatus === "running" && current.lastTurnId === turnId
|
|
5795
|
+
&& !this.store.getTurnPromptSummary(turnId)) {
|
|
5796
|
+
await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderTurnDetails(
|
|
5797
|
+
this.runningTurnSnapshot(current, turnId),
|
|
5798
|
+
));
|
|
5799
|
+
return;
|
|
5800
|
+
}
|
|
5801
|
+
await this.outbound.showDetails(contextKey, turnId);
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5588
5804
|
private async openResetHistory(
|
|
5589
5805
|
contextKey: string,
|
|
5590
5806
|
options: ResetHistoryCardOptions = {},
|
|
@@ -5596,35 +5812,19 @@ export class ProxySessionController {
|
|
|
5596
5812
|
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
5597
5813
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
5598
5814
|
}
|
|
5599
|
-
|
|
5600
|
-
await this.
|
|
5601
|
-
const completedTurns = this.store.
|
|
5815
|
+
const requestedPage = Math.max(0, Math.trunc(options.page ?? 0));
|
|
5816
|
+
const hasMoreHistory = await this.hydrateTaskTurnHistory(current, (requestedPage + 1) * RESET_HISTORY_PAGE_SIZE);
|
|
5817
|
+
const completedTurns = this.store.listTaskTurnGraphIndex(current.localSessionId);
|
|
5602
5818
|
const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
|
|
5603
5819
|
const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
|
|
5604
5820
|
&& !completedTurnIds.has(current.lastTurnId)
|
|
5605
5821
|
? current.lastTurnId
|
|
5606
5822
|
: undefined;
|
|
5607
|
-
const persistedRunningSnapshot = runningTurnId
|
|
5608
|
-
? turnViewSnapshot(this.store.getTurnSnapshot(runningTurnId))
|
|
5609
|
-
: undefined;
|
|
5610
5823
|
const runningAttempt = runningTurnId
|
|
5611
5824
|
? this.store.findIncompleteTurnAttemptByTurnId(runningTurnId)
|
|
5612
5825
|
: undefined;
|
|
5613
|
-
const attemptStartedAt = Date.parse(runningAttempt?.createdAt ?? "");
|
|
5614
5826
|
const runningSnapshot: TurnViewState | undefined = runningTurnId
|
|
5615
|
-
?
|
|
5616
|
-
sessionId: current.localSessionId,
|
|
5617
|
-
turnId: runningTurnId,
|
|
5618
|
-
prompt: runningAttempt?.promptText ?? current.title,
|
|
5619
|
-
status: "running",
|
|
5620
|
-
startedAt: Number.isFinite(attemptStartedAt) ? attemptStartedAt : Date.now(),
|
|
5621
|
-
assistantText: "",
|
|
5622
|
-
plan: [],
|
|
5623
|
-
activities: [],
|
|
5624
|
-
completedTools: [],
|
|
5625
|
-
failedTools: [],
|
|
5626
|
-
fileSummary: [],
|
|
5627
|
-
}
|
|
5827
|
+
? this.runningTurnSnapshot(current, runningTurnId)
|
|
5628
5828
|
: undefined;
|
|
5629
5829
|
const runningTurn = runningTurnId && runningSnapshot
|
|
5630
5830
|
? {
|
|
@@ -5640,13 +5840,13 @@ export class ProxySessionController {
|
|
|
5640
5840
|
parentTurnId: turn.parentTurnId,
|
|
5641
5841
|
})));
|
|
5642
5842
|
const total = allTurns.length;
|
|
5643
|
-
const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE));
|
|
5843
|
+
const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE)) + (hasMoreHistory ? 1 : 0);
|
|
5644
5844
|
const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
|
|
5645
5845
|
const offset = page * RESET_HISTORY_PAGE_SIZE;
|
|
5646
5846
|
const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
|
|
5647
5847
|
const resettingTurnId = this.resetHistoryOperations.get(current.localSessionId);
|
|
5648
5848
|
const entries: ResetHistoryCardEntry[] = turns.flatMap((turn, index) => {
|
|
5649
|
-
const snapshot =
|
|
5849
|
+
const snapshot = turn.turnId === runningTurnId ? runningSnapshot : this.store.getTurnPromptSummary(turn.turnId);
|
|
5650
5850
|
if (!snapshot) return [];
|
|
5651
5851
|
const graph = graphRows[offset + index]!;
|
|
5652
5852
|
const summary = truncateText(
|
|
@@ -5701,7 +5901,9 @@ export class ProxySessionController {
|
|
|
5701
5901
|
const card = this.cardRenderer.renderResetHistoryCard({
|
|
5702
5902
|
entries,
|
|
5703
5903
|
footerLines: [
|
|
5704
|
-
|
|
5904
|
+
hasMoreHistory
|
|
5905
|
+
? `第 ${page + 1} 页 · 已加载 ${total} 个 turn`
|
|
5906
|
+
: runningTurn
|
|
5705
5907
|
? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
|
|
5706
5908
|
: `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
|
|
5707
5909
|
...(resettingTurnId ? ["正在 Reset 到所选轮次,请稍候…"] : []),
|
|
@@ -5911,6 +6113,7 @@ export class ProxySessionController {
|
|
|
5911
6113
|
const record = this.requireCurrentSession(contextKey);
|
|
5912
6114
|
const filePath = resolveUserPath(requestedFilePath, record.cwd);
|
|
5913
6115
|
await this.assertSendableFile(filePath);
|
|
6116
|
+
await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
|
|
5914
6117
|
await this.outbound.sendFile(contextKey, filePath);
|
|
5915
6118
|
}
|
|
5916
6119
|
|
|
@@ -6215,7 +6418,7 @@ export class ProxySessionController {
|
|
|
6215
6418
|
|
|
6216
6419
|
private async resolveRemoteCodexSession(
|
|
6217
6420
|
reference: string,
|
|
6218
|
-
options: { forFork?: boolean } = {},
|
|
6421
|
+
options: { forFork?: boolean; view?: "metadata" | "latest" | "latest-full" } = {},
|
|
6219
6422
|
): Promise<AgentRemoteSession> {
|
|
6220
6423
|
const scoped = parseRemoteSessionReference(reference);
|
|
6221
6424
|
const candidates = scoped
|
|
@@ -6233,7 +6436,7 @@ export class ProxySessionController {
|
|
|
6233
6436
|
runtime,
|
|
6234
6437
|
remote: options.forFork && runtime.readRemoteForkSource
|
|
6235
6438
|
? await runtime.readRemoteForkSource(remoteSessionId)
|
|
6236
|
-
: await runtime.readRemoteSession(remoteSessionId),
|
|
6439
|
+
: await runtime.readRemoteSession(remoteSessionId, options.view ?? "latest"),
|
|
6237
6440
|
} satisfies AgentRemoteSession;
|
|
6238
6441
|
}));
|
|
6239
6442
|
const matches = reads
|
|
@@ -6259,6 +6462,7 @@ export class ProxySessionController {
|
|
|
6259
6462
|
if (direct?.status === "closed") {
|
|
6260
6463
|
throw new Error(`找不到任务:${taskId}`);
|
|
6261
6464
|
}
|
|
6465
|
+
await this.outbound.sendText(contextKey, SWITCH_TASK_STARTED_MESSAGE);
|
|
6262
6466
|
const existing = direct ?? this.findStoredSessionByReference(taskId);
|
|
6263
6467
|
if (existing) {
|
|
6264
6468
|
const runtime = this.runtimes.forAgent(existing.agentName);
|
|
@@ -6271,6 +6475,7 @@ export class ProxySessionController {
|
|
|
6271
6475
|
existing.title,
|
|
6272
6476
|
existing.cwd,
|
|
6273
6477
|
this.agentLabel(existing.agentName),
|
|
6478
|
+
existing.model,
|
|
6274
6479
|
);
|
|
6275
6480
|
await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
|
|
6276
6481
|
return;
|
|
@@ -6304,6 +6509,7 @@ export class ProxySessionController {
|
|
|
6304
6509
|
remote.title ?? remote.preview,
|
|
6305
6510
|
remote.cwd,
|
|
6306
6511
|
this.agentLabel(agentName),
|
|
6512
|
+
remote.model,
|
|
6307
6513
|
);
|
|
6308
6514
|
await this.outbound.sendText(
|
|
6309
6515
|
contextKey,
|
|
@@ -6367,6 +6573,10 @@ export class ProxySessionController {
|
|
|
6367
6573
|
throw new Error("任务仍有排队消息,无法归档。请先处理或取消排队消息。");
|
|
6368
6574
|
}
|
|
6369
6575
|
|
|
6576
|
+
if (options.source !== "dismiss") {
|
|
6577
|
+
await this.outbound.sendText(contextKey, ARCHIVE_TASK_STARTED_MESSAGE);
|
|
6578
|
+
}
|
|
6579
|
+
|
|
6370
6580
|
const remoteReference = local
|
|
6371
6581
|
? remoteSessionReference(local.agentName, local.remoteSessionId!)
|
|
6372
6582
|
: taskId;
|
|
@@ -6598,27 +6808,29 @@ export class ProxySessionController {
|
|
|
6598
6808
|
current = context.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
|
|
6599
6809
|
}
|
|
6600
6810
|
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6811
|
+
let remote: RemoteSessionSummary | undefined;
|
|
6812
|
+
let metrics: RemoteSessionMetrics | undefined;
|
|
6813
|
+
if (current?.status === "running") {
|
|
6814
|
+
const runtime = this.runtimes.forAgent(current.agentName);
|
|
6815
|
+
if (runtime.getSession(current.localSessionId)) {
|
|
6816
|
+
try {
|
|
6817
|
+
remote = (await runtime.synchronizeSession(current.localSessionId)).remoteSummary;
|
|
6606
6818
|
current = this.store.getSession(current.localSessionId) ?? current;
|
|
6819
|
+
} catch (error) {
|
|
6820
|
+
if (error instanceof CodexVersionError) throw error;
|
|
6821
|
+
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
|
|
6607
6822
|
}
|
|
6608
|
-
} catch (error) {
|
|
6609
|
-
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
|
|
6610
6823
|
}
|
|
6611
6824
|
}
|
|
6612
|
-
|
|
6613
6825
|
const localCurrent = current;
|
|
6614
|
-
let remote: RemoteSessionSummary | undefined;
|
|
6615
6826
|
let goal: RuntimeGoal | undefined;
|
|
6616
6827
|
if (current?.remoteSessionId) {
|
|
6617
6828
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
6618
|
-
if (runtime.readRemoteSession) {
|
|
6829
|
+
if (!remote && runtime.readRemoteSession) {
|
|
6619
6830
|
try {
|
|
6620
|
-
remote = await runtime.readRemoteSession(current.remoteSessionId);
|
|
6831
|
+
remote = await runtime.readRemoteSession(current.remoteSessionId, "latest-full");
|
|
6621
6832
|
} catch (error) {
|
|
6833
|
+
if (error instanceof CodexVersionError) throw error;
|
|
6622
6834
|
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task status.");
|
|
6623
6835
|
}
|
|
6624
6836
|
}
|
|
@@ -6630,6 +6842,13 @@ export class ProxySessionController {
|
|
|
6630
6842
|
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect Agent goal status.");
|
|
6631
6843
|
}
|
|
6632
6844
|
}
|
|
6845
|
+
if (runtime.readRemoteSessionMetrics) {
|
|
6846
|
+
try {
|
|
6847
|
+
metrics = await runtime.readRemoteSessionMetrics(current.remoteSessionId);
|
|
6848
|
+
} catch (error) {
|
|
6849
|
+
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task metrics.");
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
6633
6852
|
}
|
|
6634
6853
|
|
|
6635
6854
|
if (current) current = mergeRemoteTaskStatus(current, remote);
|
|
@@ -6664,6 +6883,7 @@ export class ProxySessionController {
|
|
|
6664
6883
|
`**Agent**:${cardCode(agent.title)}`,
|
|
6665
6884
|
`**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`,
|
|
6666
6885
|
`**App Server 任务 ID**:${cardCode(current.remoteSessionId ?? "尚未创建")}`,
|
|
6886
|
+
statusTaskMetricsLine(metrics?.turnCount ?? this.store.countTaskTurns(current.localSessionId), metrics?.storageBytes),
|
|
6667
6887
|
`**当前执行 / 排队消息**:${activeTurnId ? cardCode(activeTurnId) : "无"} / ${queued} 条`,
|
|
6668
6888
|
`**创建时间 / 最近活动**:${formatStatusTime(current.createdAt)} / ${formatStatusTime(current.updatedAt)}`,
|
|
6669
6889
|
);
|
|
@@ -6729,7 +6949,16 @@ export class ProxySessionController {
|
|
|
6729
6949
|
reference: string,
|
|
6730
6950
|
options: StatusCardOptions = {},
|
|
6731
6951
|
): Promise<void> {
|
|
6732
|
-
const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
|
|
6952
|
+
const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
|
|
6953
|
+
const runtime = this.runtimes.forAgent(agentName);
|
|
6954
|
+
let metrics: RemoteSessionMetrics | undefined;
|
|
6955
|
+
if (runtime.readRemoteSessionMetrics) {
|
|
6956
|
+
try {
|
|
6957
|
+
metrics = await runtime.readRemoteSessionMetrics(remote.id);
|
|
6958
|
+
} catch (error) {
|
|
6959
|
+
this.logger.warn({ error, remoteSessionId: remote.id }, "Failed to inspect App Server task metrics.");
|
|
6960
|
+
}
|
|
6961
|
+
}
|
|
6733
6962
|
const actionReference = remoteSessionReference(agentName, remote.id);
|
|
6734
6963
|
const sections: CardSection[] = [
|
|
6735
6964
|
{
|
|
@@ -6740,6 +6969,7 @@ export class ProxySessionController {
|
|
|
6740
6969
|
`**状态 / 当前任务**:${remoteSessionDetailStatus(remote)} / 未切换`,
|
|
6741
6970
|
`**Agent**:${cardCode(agentName)}`,
|
|
6742
6971
|
`**App Server 任务 ID**:${cardCode(remote.id)}`,
|
|
6972
|
+
statusTaskMetricsLine(metrics?.turnCount, metrics?.storageBytes),
|
|
6743
6973
|
`**最近回合**:${cardCode(remote.lastTurnId ?? "无")} ${remoteTurnStatusLabel(remote.lastTurnStatus)}`,
|
|
6744
6974
|
`**创建时间 / 最近活动**:${formatRemoteTime(remote.createdAt)} / ${formatRemoteTime(latestRemoteTimestamp(remote.recencyAt, remote.updatedAt))}`,
|
|
6745
6975
|
],
|
|
@@ -6891,9 +7121,10 @@ export class ProxySessionController {
|
|
|
6891
7121
|
return agent;
|
|
6892
7122
|
}
|
|
6893
7123
|
|
|
6894
|
-
private async persistAgentExecutionDefaults(localSessionId: string): Promise<void> {
|
|
6895
|
-
const
|
|
6896
|
-
if (!
|
|
7124
|
+
private async persistAgentExecutionDefaults(localSessionId: string, candidate?: RuntimeSession): Promise<void> {
|
|
7125
|
+
const record = this.store.getSession(localSessionId);
|
|
7126
|
+
if (!record) throw new Error(`找不到任务:${localSessionId}`);
|
|
7127
|
+
const session = candidate ?? record;
|
|
6897
7128
|
const agent = this.ensureAgent(session.agentName);
|
|
6898
7129
|
const observed: AgentExecutionDefaults = {
|
|
6899
7130
|
...(session.modelProvider?.trim() ? { modelProvider: session.modelProvider } : {}),
|
|
@@ -7328,6 +7559,7 @@ export class ProxySessionController {
|
|
|
7328
7559
|
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
7329
7560
|
if (!lockPath) throw new Error("当前 Agent 不支持自动关闭占用程序。");
|
|
7330
7561
|
|
|
7562
|
+
await this.outbound.sendText(contextKey, CLOSE_WRITER_STARTED_MESSAGE);
|
|
7331
7563
|
let owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
7332
7564
|
const expectedOwner = owners.length === 1 && threadWriterMatches(
|
|
7333
7565
|
owners[0],
|
|
@@ -7787,11 +8019,6 @@ function runtimeErrorMessage(error: unknown): string {
|
|
|
7787
8019
|
return error instanceof Error ? error.message : String(error);
|
|
7788
8020
|
}
|
|
7789
8021
|
|
|
7790
|
-
function isMissingRolloutError(error: unknown): boolean {
|
|
7791
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7792
|
-
return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
|
|
7793
|
-
}
|
|
7794
|
-
|
|
7795
8022
|
function isUnmaterializedCodexThreadError(error: unknown): boolean {
|
|
7796
8023
|
const message = error instanceof Error ? error.message : String(error);
|
|
7797
8024
|
return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
|
|
@@ -7970,6 +8197,12 @@ function formatDuration(milliseconds: number): string {
|
|
|
7970
8197
|
return milliseconds < 1_000 ? `${milliseconds}ms` : `${(milliseconds / 1_000).toFixed(1)}s`;
|
|
7971
8198
|
}
|
|
7972
8199
|
|
|
8200
|
+
function statusTaskMetricsLine(turnCount: number | undefined, storageBytes: number | undefined): string {
|
|
8201
|
+
const turns = turnCount === undefined ? "未知" : `${Math.max(0, Math.round(turnCount)).toLocaleString("en-US")} 轮`;
|
|
8202
|
+
const storage = storageBytes === undefined ? "未知" : formatStorageSize(storageBytes);
|
|
8203
|
+
return `**已执行轮次 / 磁盘占用**:${turns} / ${storage}`;
|
|
8204
|
+
}
|
|
8205
|
+
|
|
7973
8206
|
function formatStatusTime(value: string): string {
|
|
7974
8207
|
const date = new Date(value);
|
|
7975
8208
|
if (Number.isNaN(date.getTime())) return value;
|
|
@@ -8443,6 +8676,23 @@ function deduplicateFileReferences(
|
|
|
8443
8676
|
});
|
|
8444
8677
|
}
|
|
8445
8678
|
|
|
8679
|
+
function selectProviderModel(models: ModelOption[], previousModel?: string): ModelOption | undefined {
|
|
8680
|
+
return models.find((model) => model.id === previousModel)
|
|
8681
|
+
?? models.find((model) => model.isDefault)
|
|
8682
|
+
?? models[0];
|
|
8683
|
+
}
|
|
8684
|
+
|
|
8685
|
+
function selectProviderReasoningEffort(model: ModelOption, previousEffort?: string): string {
|
|
8686
|
+
if (previousEffort && (model.supportedReasoningEfforts.length === 0
|
|
8687
|
+
|| model.supportedReasoningEfforts.some((option) => option.value === previousEffort))) {
|
|
8688
|
+
return previousEffort;
|
|
8689
|
+
}
|
|
8690
|
+
return model.defaultReasoningEffort
|
|
8691
|
+
?? model.supportedReasoningEfforts[0]?.value
|
|
8692
|
+
?? previousEffort
|
|
8693
|
+
?? "medium";
|
|
8694
|
+
}
|
|
8695
|
+
|
|
8446
8696
|
function recoveryCardPrompt(prompt: string): string {
|
|
8447
8697
|
return `恢复重启前的任务:${prompt}`;
|
|
8448
8698
|
}
|