@keyou007/agent-bot 0.1.20-alpha.0 → 0.1.20
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 +17 -0
- package/README.md +18 -1
- package/README.zh.md +9 -0
- 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/CodexLocalActivityDetector.js +32 -10
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +53 -7
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +225 -164
- 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/ProviderSwitch.js +32 -0
- package/dist/codex/ProviderSwitch.js.map +1 -0
- package/dist/config/AgentExecutionDefaultsStore.js +1 -1
- package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
- package/dist/feishu/CardRenderer.js +36 -11
- 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/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 +1 -1
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +25 -7
- 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/proxy/ProxySessionController.js +400 -259
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +99 -65
- package/dist/state/StateStore.js.map +1 -1
- package/dist/utils/semanticVersion.js +39 -0
- package/dist/utils/semanticVersion.js.map +1 -0
- package/docs/technical-reference.md +5 -3
- package/docs/technical-reference.zh.md +5 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +7 -1
- package/src/cli/AgentPrerequisites.ts +14 -40
- package/src/cli/DefaultAgentSelection.ts +1 -1
- package/src/cli.ts +13 -0
- package/src/codex/CodexLocalActivityDetector.ts +41 -9
- package/src/codex/CodexProcessManager.ts +44 -7
- package/src/codex/CodexRuntime.ts +244 -195
- package/src/codex/CodexVersion.ts +18 -0
- package/src/codex/ProviderSwitch.ts +36 -0
- package/src/config/AgentExecutionDefaultsStore.ts +1 -1
- package/src/feishu/CardRenderer.ts +37 -27
- package/src/feishu/FeishuConnector.ts +15 -5
- package/src/feishu/InboundText.ts +11 -0
- package/src/feishu/MergedForwardMessage.ts +15 -5
- package/src/index.ts +1 -1
- package/src/local-files/LocalFileViewerServer.ts +26 -7
- package/src/local-files/MarkdownFileLinks.ts +64 -0
- package/src/proxy/ProxySessionController.ts +370 -240
- package/src/runtime/types.ts +15 -2
- package/src/state/StateStore.ts +117 -90
- package/src/utils/semanticVersion.ts +28 -0
|
@@ -3,6 +3,8 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
6
|
+
import { CodexVersionError } from "../codex/CodexVersion.js";
|
|
7
|
+
import { assertProviderSettingsApplied, isMissingRolloutError } from "../codex/ProviderSwitch.js";
|
|
6
8
|
import { SystemThreadWriterProcessController, } from "../codex/ThreadWriterProcess.js";
|
|
7
9
|
import { resolveUserPath } from "../config/paths.js";
|
|
8
10
|
import { DEFAULT_GROUP_NAME_FORMAT, } from "../config/schema.js";
|
|
@@ -50,6 +52,22 @@ const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
|
50
52
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
51
53
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
52
54
|
const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
|
|
55
|
+
const TURN_RESET_STARTED_MESSAGE = "Reset 正在进行,请稍后。期间发送的消息会自动排队。";
|
|
56
|
+
const CREATE_TASK_STARTED_MESSAGE = "正在创建任务,请稍后。";
|
|
57
|
+
const CREATE_GROUP_STARTED_MESSAGE = "正在创建群聊和任务,请稍后。";
|
|
58
|
+
const FORK_TASK_STARTED_MESSAGE = "正在创建分支任务,请稍后。";
|
|
59
|
+
const FORK_GROUP_STARTED_MESSAGE = "正在创建分支群聊,请稍后。";
|
|
60
|
+
const LIST_SESSIONS_STARTED_MESSAGE = "正在读取任务列表,请稍后。";
|
|
61
|
+
const LIST_TURNS_STARTED_MESSAGE = "正在读取历史轮次,请稍后。";
|
|
62
|
+
const SWITCH_TASK_STARTED_MESSAGE = "正在切换任务,请稍后。";
|
|
63
|
+
const STATUS_STARTED_MESSAGE = "正在读取任务状态,请稍后。";
|
|
64
|
+
const GOAL_STARTED_MESSAGE = "正在处理 Goal,请稍后。";
|
|
65
|
+
const ARCHIVE_TASK_STARTED_MESSAGE = "正在归档任务,请稍后。";
|
|
66
|
+
const DISMISS_GROUP_STARTED_MESSAGE = "正在归档任务并解散群聊,请稍后。";
|
|
67
|
+
const SEND_FILE_STARTED_MESSAGE = "正在发送文件,请稍后。";
|
|
68
|
+
const CHANGE_TITLE_STARTED_MESSAGE = "正在修改任务标题,请稍后。";
|
|
69
|
+
const STOP_TASK_STARTED_MESSAGE = "正在停止任务,请稍后。";
|
|
70
|
+
const CLOSE_WRITER_STARTED_MESSAGE = "正在检查并关闭占用进程,请稍后。";
|
|
53
71
|
const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
|
|
54
72
|
const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
|
|
55
73
|
const IMAGE_FILE_EXTENSIONS = new Set([
|
|
@@ -205,6 +223,8 @@ export class ProxySessionController {
|
|
|
205
223
|
sessionResets = new Map();
|
|
206
224
|
resetHistoryOperations = new Map();
|
|
207
225
|
remoteTurnHistoryRefreshes = new Map();
|
|
226
|
+
turnHistoryHydrations = new Map();
|
|
227
|
+
forkHistoryCursors = new Map();
|
|
208
228
|
lastSessionListings = new Map();
|
|
209
229
|
threadInitializations = new Map();
|
|
210
230
|
llmRetryingSessions = new Set();
|
|
@@ -282,6 +302,7 @@ export class ProxySessionController {
|
|
|
282
302
|
catch (error) {
|
|
283
303
|
this.logger.warn({ error, messageId: message.messageId, contextKey: message.contextKey }, "Failed to acknowledge the incoming Feishu message with a reaction.");
|
|
284
304
|
}
|
|
305
|
+
await this.waitForCurrentSessionReset(message.contextKey);
|
|
285
306
|
if (attachmentReservation && this.completeForwardAttachment(message, attachmentReservation)) {
|
|
286
307
|
this.store.audit(message.contextKey, `${attachmentReservation.sourceKind}_attachment_coalesced`, {
|
|
287
308
|
messageId: message.messageId,
|
|
@@ -478,10 +499,11 @@ export class ProxySessionController {
|
|
|
478
499
|
command: command.type,
|
|
479
500
|
}, "Processing Agent Bot command.");
|
|
480
501
|
}
|
|
481
|
-
// Operational and read-only commands
|
|
502
|
+
// Operational and read-only commands bypass the prompt queue, but still wait for a Reset binding change.
|
|
482
503
|
if (isQueueIndependentCommand(command)) {
|
|
483
504
|
await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
484
505
|
try {
|
|
506
|
+
await this.waitForCurrentSessionReset(message.contextKey);
|
|
485
507
|
if (command.type === "prompt")
|
|
486
508
|
await this.ensureThreadFork(message);
|
|
487
509
|
await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
|
|
@@ -502,45 +524,48 @@ export class ProxySessionController {
|
|
|
502
524
|
return;
|
|
503
525
|
}
|
|
504
526
|
const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
|
|
505
|
-
const next = previous.catch(() => undefined).then(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
this.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
527
|
+
const next = previous.catch(() => undefined).then(async () => {
|
|
528
|
+
await this.waitForCurrentSessionReset(message.contextKey);
|
|
529
|
+
return this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
530
|
+
try {
|
|
531
|
+
let executableCommand = command;
|
|
532
|
+
let executableImagePaths = localImagePaths;
|
|
533
|
+
let executableMessage = message;
|
|
534
|
+
let injectedRootMessageId;
|
|
535
|
+
if (command.type === "prompt") {
|
|
536
|
+
await this.ensureThreadFork(message);
|
|
537
|
+
const prepared = await this.prepareTopicRootPrompt(message, command.text, localImagePaths);
|
|
538
|
+
executableCommand = { ...command, text: prepared.text };
|
|
539
|
+
executableImagePaths = prepared.localImagePaths;
|
|
540
|
+
executableMessage = { ...message, displayText: prepared.displayPrompt };
|
|
541
|
+
injectedRootMessageId = prepared.injectedRootMessageId;
|
|
542
|
+
}
|
|
543
|
+
await this.execute(message.contextKey, executableCommand, message.messageId, replyTarget, executableImagePaths, message.userId, executableMessage);
|
|
544
|
+
if (injectedRootMessageId) {
|
|
545
|
+
const current = this.currentSession(message.contextKey);
|
|
546
|
+
if (current) {
|
|
547
|
+
this.store.audit(message.contextKey, "topic_root_injected", {
|
|
548
|
+
localSessionId: current.localSessionId,
|
|
549
|
+
rootMessageId: injectedRootMessageId,
|
|
550
|
+
promptMessageId: message.messageId,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (!commandDefersReactionFinalization(command)) {
|
|
555
|
+
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
528
556
|
}
|
|
529
557
|
}
|
|
530
|
-
|
|
531
|
-
await this.finalizeStandaloneMessageReaction(message.messageId, "
|
|
558
|
+
catch (error) {
|
|
559
|
+
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
560
|
+
await this.sendError(message.contextKey, error, {
|
|
561
|
+
requestId: message.messageId,
|
|
562
|
+
messageId: message.messageId,
|
|
563
|
+
command: command.type,
|
|
564
|
+
phase: "command_execute",
|
|
565
|
+
});
|
|
532
566
|
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
536
|
-
await this.sendError(message.contextKey, error, {
|
|
537
|
-
requestId: message.messageId,
|
|
538
|
-
messageId: message.messageId,
|
|
539
|
-
command: command.type,
|
|
540
|
-
phase: "command_execute",
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
}));
|
|
567
|
+
});
|
|
568
|
+
});
|
|
544
569
|
this.messageQueues.set(message.contextKey, next);
|
|
545
570
|
await next;
|
|
546
571
|
if (this.messageQueues.get(message.contextKey) === next)
|
|
@@ -653,6 +678,7 @@ export class ProxySessionController {
|
|
|
653
678
|
else if (kind === "directory_send_file") {
|
|
654
679
|
const filePath = directoryFileActionPath(scopedAction.value.filePath);
|
|
655
680
|
await this.assertSendableFile(filePath);
|
|
681
|
+
await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
|
|
656
682
|
await this.outbound.sendFile(contextKey, filePath);
|
|
657
683
|
}
|
|
658
684
|
else if (kind === "directory_new_folder_prompt") {
|
|
@@ -704,11 +730,13 @@ export class ProxySessionController {
|
|
|
704
730
|
}
|
|
705
731
|
else if (kind === "session_new") {
|
|
706
732
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
733
|
+
await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
|
|
707
734
|
await this.createProjectSessionFromReference(contextKey, sessionId);
|
|
708
735
|
await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
|
|
709
736
|
}
|
|
710
737
|
else if (kind === "session_new_group") {
|
|
711
738
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
739
|
+
await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
|
|
712
740
|
await this.createFeishuGroupFromReference(contextKey, sessionId, scopedAction.userId);
|
|
713
741
|
await this.refreshSessionsCardFromAction(scopedAction);
|
|
714
742
|
}
|
|
@@ -726,10 +754,12 @@ export class ProxySessionController {
|
|
|
726
754
|
}
|
|
727
755
|
else if (kind === "group_dismiss_confirm") {
|
|
728
756
|
this.assertDismissGroupRequester(scopedAction);
|
|
757
|
+
await this.outbound.sendText(contextKey, DISMISS_GROUP_STARTED_MESSAGE);
|
|
729
758
|
await this.dismissGroupAndArchiveTask(contextKey, String(scopedAction.value.sessionId ?? ""), "command");
|
|
730
759
|
}
|
|
731
760
|
else if (kind === "session_stop") {
|
|
732
761
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
762
|
+
await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
|
|
733
763
|
await this.stopSessionReference(contextKey, sessionId);
|
|
734
764
|
if (scopedAction.value.cardView === "status")
|
|
735
765
|
await this.refreshStatusCardFromAction(scopedAction, sessionId);
|
|
@@ -737,6 +767,7 @@ export class ProxySessionController {
|
|
|
737
767
|
await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
|
|
738
768
|
}
|
|
739
769
|
else if (kind === "session_status") {
|
|
770
|
+
await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
|
|
740
771
|
await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
|
|
741
772
|
}
|
|
742
773
|
else if (kind === "session_status_refresh") {
|
|
@@ -867,6 +898,8 @@ export class ProxySessionController {
|
|
|
867
898
|
this.sessionResets.clear();
|
|
868
899
|
this.resetHistoryOperations.clear();
|
|
869
900
|
this.remoteTurnHistoryRefreshes.clear();
|
|
901
|
+
this.forkHistoryCursors.clear();
|
|
902
|
+
this.turnHistoryHydrations.clear();
|
|
870
903
|
for (const pending of this.pendingMergedForwards.values())
|
|
871
904
|
pending.resolveAttachment(undefined);
|
|
872
905
|
this.pendingMergedForwards.clear();
|
|
@@ -930,9 +963,11 @@ export class ProxySessionController {
|
|
|
930
963
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
931
964
|
if (runtime.readRemoteSession) {
|
|
932
965
|
try {
|
|
933
|
-
remote = await runtime.readRemoteSession(record.remoteSessionId);
|
|
966
|
+
remote = await runtime.readRemoteSession(record.remoteSessionId, "latest-full");
|
|
934
967
|
}
|
|
935
968
|
catch (error) {
|
|
969
|
+
if (error instanceof CodexVersionError)
|
|
970
|
+
throw error;
|
|
936
971
|
this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to inspect App Server task status for CLI.");
|
|
937
972
|
}
|
|
938
973
|
}
|
|
@@ -1155,10 +1190,14 @@ export class ProxySessionController {
|
|
|
1155
1190
|
}
|
|
1156
1191
|
async controlTaskSettings(localSessionId, setting, value) {
|
|
1157
1192
|
const record = this.requireControlSession(localSessionId);
|
|
1158
|
-
const loaded = await this.loadSession(record);
|
|
1159
1193
|
if (setting && !value?.trim())
|
|
1160
1194
|
throw new Error(`task ${setting} requires a value.`);
|
|
1161
1195
|
const nextValue = value?.trim();
|
|
1196
|
+
if (setting === "provider" && nextValue) {
|
|
1197
|
+
const contextKey = this.controlSessionContextKey(record);
|
|
1198
|
+
await this.outbound.withReplyTarget(contextKey, this.controlSessionReplyTarget(record), () => this.outbound.sendText(contextKey, `正在切换到 Provider ${nextValue},请稍后。`));
|
|
1199
|
+
}
|
|
1200
|
+
const loaded = await this.loadSession(record);
|
|
1162
1201
|
if (setting === "provider" && nextValue) {
|
|
1163
1202
|
await this.assertModelProvider(loaded, nextValue);
|
|
1164
1203
|
const models = await loaded.runtime.listModels();
|
|
@@ -1171,20 +1210,12 @@ export class ProxySessionController {
|
|
|
1171
1210
|
: model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
|
|
1172
1211
|
if (!effort)
|
|
1173
1212
|
throw new Error(`Model ${model.id} has no reasoning effort available for a Provider change.`);
|
|
1174
|
-
|
|
1175
|
-
throw new Error("The current runtime does not support Provider settings.");
|
|
1176
|
-
const updated = await loaded.runtime.setExecutionSettings(record.localSessionId, {
|
|
1213
|
+
await this.changeProviderSettings(loaded, {
|
|
1177
1214
|
modelProvider: nextValue,
|
|
1178
1215
|
model: model.id,
|
|
1179
1216
|
reasoningEffort: effort,
|
|
1180
1217
|
permissionMode: loaded.session.permissionMode,
|
|
1181
1218
|
});
|
|
1182
|
-
this.store.updateRuntimeSession(record.localSessionId, {
|
|
1183
|
-
modelProvider: updated.modelProvider ?? nextValue,
|
|
1184
|
-
model: updated.model ?? model.id,
|
|
1185
|
-
reasoningEffort: updated.reasoningEffort ?? effort,
|
|
1186
|
-
permissionMode: updated.permissionMode,
|
|
1187
|
-
});
|
|
1188
1219
|
}
|
|
1189
1220
|
else if (setting === "model" && nextValue) {
|
|
1190
1221
|
const models = await loaded.runtime.listModels();
|
|
@@ -1222,13 +1253,13 @@ export class ProxySessionController {
|
|
|
1222
1253
|
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
|
|
1223
1254
|
}
|
|
1224
1255
|
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1225
|
-
if (setting)
|
|
1256
|
+
if (setting && setting !== "provider")
|
|
1226
1257
|
await this.persistAgentExecutionDefaults(session.localSessionId);
|
|
1227
1258
|
const models = await loaded.runtime.listModels();
|
|
1228
1259
|
const currentModel = models.find((candidate) => candidate.id === session.model)
|
|
1229
1260
|
?? models.find((candidate) => candidate.isDefault);
|
|
1230
1261
|
const providers = loaded.runtime.listModelProviders
|
|
1231
|
-
? await
|
|
1262
|
+
? await this.modelProviderOptions(loaded)
|
|
1232
1263
|
: [];
|
|
1233
1264
|
return {
|
|
1234
1265
|
session,
|
|
@@ -1271,17 +1302,18 @@ export class ProxySessionController {
|
|
|
1271
1302
|
}),
|
|
1272
1303
|
};
|
|
1273
1304
|
}
|
|
1274
|
-
controlListTaskTurns(localSessionId) {
|
|
1305
|
+
async controlListTaskTurns(localSessionId) {
|
|
1275
1306
|
const session = this.requireControlSession(localSessionId);
|
|
1276
1307
|
if (!session.remoteSessionId || !this.isCodexSession(session)) {
|
|
1277
1308
|
throw new Error("The task is not an App Server task that can be reset.");
|
|
1278
1309
|
}
|
|
1279
|
-
|
|
1310
|
+
await this.hydrateTaskTurnHistory(session);
|
|
1311
|
+
const rows = this.store.listTaskTurnGraphIndex(session.localSessionId);
|
|
1280
1312
|
const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
|
|
1281
1313
|
return {
|
|
1282
1314
|
session,
|
|
1283
1315
|
turns: rows.flatMap((turn, index) => {
|
|
1284
|
-
const snapshot =
|
|
1316
|
+
const snapshot = this.store.getTurnPromptSummary(turn.turnId);
|
|
1285
1317
|
if (!snapshot)
|
|
1286
1318
|
return [];
|
|
1287
1319
|
return [{
|
|
@@ -1299,16 +1331,10 @@ export class ProxySessionController {
|
|
|
1299
1331
|
async controlResetTaskToTurn(localSessionId, turnId) {
|
|
1300
1332
|
const record = this.requireControlSession(localSessionId);
|
|
1301
1333
|
const contextKey = this.controlSessionContextKey(record);
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
await reset;
|
|
1307
|
-
}
|
|
1308
|
-
finally {
|
|
1309
|
-
if (this.sessionResets.get(record.localSessionId) === reset)
|
|
1310
|
-
this.sessionResets.delete(record.localSessionId);
|
|
1311
|
-
}
|
|
1334
|
+
await this.runSessionReset(record.localSessionId, async () => {
|
|
1335
|
+
await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
|
|
1336
|
+
await this.performSessionReset(contextKey, record, turnId, false);
|
|
1337
|
+
});
|
|
1312
1338
|
return this.store.getSession(record.localSessionId) ?? record;
|
|
1313
1339
|
}
|
|
1314
1340
|
controlTaskMute(localSessionId, enabled) {
|
|
@@ -1438,6 +1464,7 @@ export class ProxySessionController {
|
|
|
1438
1464
|
if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
|
|
1439
1465
|
throw new Error("/new --nodir 仅支持 App Server Agent。");
|
|
1440
1466
|
}
|
|
1467
|
+
await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
|
|
1441
1468
|
await this.createSession(contextKey, context.defaultAgent, command.projectless
|
|
1442
1469
|
? undefined
|
|
1443
1470
|
: command.cwd === undefined
|
|
@@ -1448,6 +1475,10 @@ export class ProxySessionController {
|
|
|
1448
1475
|
if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
|
|
1449
1476
|
throw new Error("/newgroup --nodir 仅支持 App Server Agent。");
|
|
1450
1477
|
}
|
|
1478
|
+
if (!userId?.startsWith("ou_")) {
|
|
1479
|
+
throw new Error("/newgroup 只能由具有 open_id 的飞书用户消息触发。");
|
|
1480
|
+
}
|
|
1481
|
+
await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
|
|
1451
1482
|
await this.createFeishuGroup(contextKey, context.defaultAgent, command.title, userId, command.cwd === undefined ? undefined : resolveUserPath(command.cwd), command.projectless === true);
|
|
1452
1483
|
return;
|
|
1453
1484
|
case "forkgroup":
|
|
@@ -1466,6 +1497,7 @@ export class ProxySessionController {
|
|
|
1466
1497
|
await this.enqueueNoSteerPrompt(contextKey, command.text, messageId, replyTarget);
|
|
1467
1498
|
return;
|
|
1468
1499
|
case "sessions":
|
|
1500
|
+
await this.outbound.sendText(contextKey, LIST_SESSIONS_STARTED_MESSAGE);
|
|
1469
1501
|
await this.listSessions(contextKey, command.searchTerm);
|
|
1470
1502
|
return;
|
|
1471
1503
|
case "switch":
|
|
@@ -1491,9 +1523,11 @@ export class ProxySessionController {
|
|
|
1491
1523
|
await this.openDismissGroupCard(contextKey, userId);
|
|
1492
1524
|
return;
|
|
1493
1525
|
case "status":
|
|
1526
|
+
await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
|
|
1494
1527
|
await this.status(contextKey, command.sessionId);
|
|
1495
1528
|
return;
|
|
1496
1529
|
case "goal":
|
|
1530
|
+
await this.outbound.sendText(contextKey, GOAL_STARTED_MESSAGE);
|
|
1497
1531
|
await this.goal(contextKey, command);
|
|
1498
1532
|
return;
|
|
1499
1533
|
case "restart":
|
|
@@ -1508,6 +1542,7 @@ export class ProxySessionController {
|
|
|
1508
1542
|
await this.setGroupMute(contextKey, command.enabled);
|
|
1509
1543
|
return;
|
|
1510
1544
|
case "turns":
|
|
1545
|
+
await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
|
|
1511
1546
|
await this.openResetHistory(contextKey);
|
|
1512
1547
|
return;
|
|
1513
1548
|
case "model":
|
|
@@ -1747,6 +1782,8 @@ export class ProxySessionController {
|
|
|
1747
1782
|
remote = await runtime.readRemoteSession(session.remoteSessionId);
|
|
1748
1783
|
}
|
|
1749
1784
|
catch (error) {
|
|
1785
|
+
if (error instanceof CodexVersionError)
|
|
1786
|
+
throw error;
|
|
1750
1787
|
this.logger.warn({ error, attemptId: attempt.attemptId, remoteSessionId: session.remoteSessionId }, "Failed to read the remote task during startup recovery; treating the old local execution as interrupted.");
|
|
1751
1788
|
}
|
|
1752
1789
|
}
|
|
@@ -2191,8 +2228,7 @@ export class ProxySessionController {
|
|
|
2191
2228
|
return false;
|
|
2192
2229
|
if (record.lastTurnId === anchor.turnId)
|
|
2193
2230
|
return true;
|
|
2194
|
-
return this.store.
|
|
2195
|
-
.some((turn) => turn.turnId === anchor.turnId);
|
|
2231
|
+
return this.store.hasTaskHistoryTurn(record.localSessionId, anchor.turnId);
|
|
2196
2232
|
}
|
|
2197
2233
|
async forkThreadSession(message) {
|
|
2198
2234
|
const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
|
|
@@ -2285,6 +2321,7 @@ export class ProxySessionController {
|
|
|
2285
2321
|
return { anchor, source, snapshot };
|
|
2286
2322
|
}
|
|
2287
2323
|
async forkSessionReference(contextKey, reference) {
|
|
2324
|
+
await this.outbound.sendText(contextKey, FORK_TASK_STARTED_MESSAGE);
|
|
2288
2325
|
const plan = await this.prepareForkSession(contextKey, reference);
|
|
2289
2326
|
const forked = await this.forkSessionIntoContext(contextKey, plan);
|
|
2290
2327
|
const forkSourceLabel = plan.forkedFromHistoricalTurn
|
|
@@ -2336,32 +2373,22 @@ export class ProxySessionController {
|
|
|
2336
2373
|
if (!runtime.readRemoteSession && !source) {
|
|
2337
2374
|
throw new Error("当前 App Server Agent 不支持读取指定任务。");
|
|
2338
2375
|
}
|
|
2339
|
-
const
|
|
2376
|
+
const persistedCompletedTurnId = source
|
|
2377
|
+
? this.store.findLatestCompletedTurnId(source.localSessionId)
|
|
2378
|
+
: undefined;
|
|
2340
2379
|
const sourceLastSnapshot = turnViewSnapshot(source?.lastTurnId ? this.store.getTurnSnapshot(source.lastTurnId) : undefined);
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2343
|
-
: source?.lastTurnId
|
|
2380
|
+
const localCompletedTurnId = persistedCompletedTurnId
|
|
2381
|
+
?? (source?.lastTurnId
|
|
2344
2382
|
&& (source.lastTurnStatus === "completed" || sourceLastSnapshot?.status === "completed")
|
|
2345
|
-
?
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
}]
|
|
2353
|
-
: [];
|
|
2354
|
-
if (!remote && runtime.readRemoteSession && (!source || persistedCompletedTurns.length === 0)) {
|
|
2355
|
-
try {
|
|
2356
|
-
remote = await runtime.readRemoteSession(remoteSessionId);
|
|
2357
|
-
}
|
|
2358
|
-
catch (error) {
|
|
2359
|
-
if (localCompletedTurns.length === 0)
|
|
2360
|
-
throw error;
|
|
2361
|
-
this.logger.warn({ error, contextKey, remoteSessionId, sourceTurnId: localCompletedTurns.at(-1)?.id }, "Failed to read the complete source task before Fork; using the locally persisted completed Turn.");
|
|
2362
|
-
}
|
|
2383
|
+
? source.lastTurnId
|
|
2384
|
+
: undefined)
|
|
2385
|
+
?? (source ? this.store.getForkHistorySource(source.localSessionId)?.sourceTurnId : undefined);
|
|
2386
|
+
if (!remote && !localCompletedTurnId) {
|
|
2387
|
+
remote = runtime.readRemoteForkSource
|
|
2388
|
+
? await runtime.readRemoteForkSource(remoteSessionId)
|
|
2389
|
+
: await runtime.readRemoteSession?.(remoteSessionId);
|
|
2363
2390
|
}
|
|
2364
|
-
const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ??
|
|
2391
|
+
const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurnId;
|
|
2365
2392
|
const latestSnapshot = turnViewSnapshot(latestTurnId ? this.store.getTurnSnapshot(latestTurnId) : undefined);
|
|
2366
2393
|
const isRunning = remote
|
|
2367
2394
|
? isRemoteSessionActive(remote)
|
|
@@ -2370,7 +2397,7 @@ export class ProxySessionController {
|
|
|
2370
2397
|
|| isTurnStillRunning(latestSnapshot?.status));
|
|
2371
2398
|
const lastTurnId = remote?.lastCompletedTurnId
|
|
2372
2399
|
?? (remote?.lastTurnStatus === "completed" ? remote.lastTurnId : undefined)
|
|
2373
|
-
??
|
|
2400
|
+
?? localCompletedTurnId;
|
|
2374
2401
|
if (!lastTurnId) {
|
|
2375
2402
|
if (isRunning) {
|
|
2376
2403
|
throw new Error(`${sourceLabel}正在执行,且还没有已完成轮次可供 fork。请等待当前轮次完成后重试。`);
|
|
@@ -2379,8 +2406,7 @@ export class ProxySessionController {
|
|
|
2379
2406
|
}
|
|
2380
2407
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
|
|
2381
2408
|
const forkedFromHistoricalTurn = lastTurnId !== latestTurnId;
|
|
2382
|
-
const completedTurn =
|
|
2383
|
-
.find((turn) => turn.id === lastTurnId);
|
|
2409
|
+
const completedTurn = remote?.completedTurns?.find((turn) => turn.id === lastTurnId);
|
|
2384
2410
|
const cwd = remote?.cwd || source?.cwd;
|
|
2385
2411
|
if (!cwd)
|
|
2386
2412
|
throw new Error("指定的 App Server 任务没有可用的工作目录,暂时不能 fork。");
|
|
@@ -2422,27 +2448,10 @@ export class ProxySessionController {
|
|
|
2422
2448
|
sourceTurnPrompt,
|
|
2423
2449
|
sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
|
|
2424
2450
|
sourceTurnCompletedAt,
|
|
2425
|
-
sourceCompletedTurns: remote?.completedTurns
|
|
2426
|
-
?? (persistedCompletedTurns.length > 0 ? [] : localCompletedTurns),
|
|
2427
2451
|
sourceWasRunning: isRunning,
|
|
2428
2452
|
forkedFromHistoricalTurn,
|
|
2429
2453
|
};
|
|
2430
2454
|
}
|
|
2431
|
-
localForkCompletedTurns(source) {
|
|
2432
|
-
return this.store.listTaskTurnGraph(source.localSessionId)
|
|
2433
|
-
.slice()
|
|
2434
|
-
.reverse()
|
|
2435
|
-
.map((record) => {
|
|
2436
|
-
const snapshot = turnViewSnapshot(record.snapshot);
|
|
2437
|
-
const startedAt = snapshot?.startedAt ?? parseIsoTimestamp(record.updatedAt);
|
|
2438
|
-
return {
|
|
2439
|
-
id: record.turnId,
|
|
2440
|
-
prompt: latestUserPromptFromTurnView(snapshot),
|
|
2441
|
-
startedAt,
|
|
2442
|
-
completedAt: snapshot?.completedAt ?? startedAt,
|
|
2443
|
-
};
|
|
2444
|
-
});
|
|
2445
|
-
}
|
|
2446
2455
|
async prepareForkGroupSession(contextKey, requestedTitle, incomingMessage) {
|
|
2447
2456
|
if (!incomingMessage?.threadContext) {
|
|
2448
2457
|
const plan = await this.prepareForkSession(contextKey, undefined, requestedTitle);
|
|
@@ -2512,7 +2521,6 @@ export class ProxySessionController {
|
|
|
2512
2521
|
};
|
|
2513
2522
|
}
|
|
2514
2523
|
async forkSessionIntoContext(contextKey, plan) {
|
|
2515
|
-
const sourceCompletedTurns = await this.resolveForkSourceCompletedTurns(plan);
|
|
2516
2524
|
const localSessionId = createId("sess");
|
|
2517
2525
|
const record = this.store.createSession({
|
|
2518
2526
|
localSessionId,
|
|
@@ -2548,7 +2556,6 @@ export class ProxySessionController {
|
|
|
2548
2556
|
lastTurnStatus: plan.lastTurnStatus,
|
|
2549
2557
|
});
|
|
2550
2558
|
this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName));
|
|
2551
|
-
const sourceTurnHistoryCount = this.persistForkSourceHistory(contextKey, localSessionId, plan, sourceCompletedTurns);
|
|
2552
2559
|
this.store.audit(contextKey, "session_forked", {
|
|
2553
2560
|
sourceLocalSessionId: plan.source?.localSessionId,
|
|
2554
2561
|
sourceRemoteSessionId: plan.remoteSessionId,
|
|
@@ -2556,7 +2563,6 @@ export class ProxySessionController {
|
|
|
2556
2563
|
sourceTurnPrompt: plan.sourceTurnPrompt,
|
|
2557
2564
|
sourceTurnStartedAt: plan.sourceTurnStartedAt,
|
|
2558
2565
|
sourceTurnCompletedAt: plan.sourceTurnCompletedAt,
|
|
2559
|
-
sourceTurnHistoryCount,
|
|
2560
2566
|
sourceWasRunning: plan.sourceWasRunning,
|
|
2561
2567
|
forkedLocalSessionId: localSessionId,
|
|
2562
2568
|
forkedRemoteSessionId: forked.remoteSessionId,
|
|
@@ -2572,42 +2578,6 @@ export class ProxySessionController {
|
|
|
2572
2578
|
throw error;
|
|
2573
2579
|
}
|
|
2574
2580
|
}
|
|
2575
|
-
async resolveForkSourceCompletedTurns(plan) {
|
|
2576
|
-
let completedTurns = plan.sourceCompletedTurns;
|
|
2577
|
-
if (completedTurns === undefined && plan.runtime.readRemoteSession) {
|
|
2578
|
-
try {
|
|
2579
|
-
completedTurns = (await plan.runtime.readRemoteSession(plan.remoteSessionId)).completedTurns ?? [];
|
|
2580
|
-
}
|
|
2581
|
-
catch (error) {
|
|
2582
|
-
this.logger.warn({ error, remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId }, "Failed to read the source task Turn history before Fork; keeping the Fork anchor fallback.");
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
if (!completedTurns?.length)
|
|
2586
|
-
return [];
|
|
2587
|
-
const anchorIndex = completedTurns.findIndex((turn) => turn.id === plan.lastTurnId);
|
|
2588
|
-
if (anchorIndex < 0) {
|
|
2589
|
-
this.logger.warn({ remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId }, "The source task Turn history did not include the Fork anchor; keeping the Fork anchor fallback.");
|
|
2590
|
-
return [];
|
|
2591
|
-
}
|
|
2592
|
-
return completedTurns.slice(0, anchorIndex + 1);
|
|
2593
|
-
}
|
|
2594
|
-
persistForkSourceHistory(contextKey, forkedLocalSessionId, plan, completedTurns) {
|
|
2595
|
-
if (completedTurns.length === 0)
|
|
2596
|
-
return 0;
|
|
2597
|
-
const localSessionId = plan.source?.localSessionId ?? forkedLocalSessionId;
|
|
2598
|
-
const historyContextKey = plan.source?.contextKey ?? contextKey;
|
|
2599
|
-
const fallbackStart = plan.sourceTurnStartedAt
|
|
2600
|
-
?? plan.sourceTurnCompletedAt
|
|
2601
|
-
?? Date.now() - completedTurns.length;
|
|
2602
|
-
return this.persistCompletedTurnHistory({
|
|
2603
|
-
localSessionId,
|
|
2604
|
-
contextKey: historyContextKey,
|
|
2605
|
-
agentName: plan.agentName,
|
|
2606
|
-
remoteSessionId: plan.remoteSessionId,
|
|
2607
|
-
completedTurns,
|
|
2608
|
-
fallbackStart,
|
|
2609
|
-
});
|
|
2610
|
-
}
|
|
2611
2581
|
persistCompletedTurnHistory(input) {
|
|
2612
2582
|
try {
|
|
2613
2583
|
this.store.importCompletedTurnHistory({
|
|
@@ -2615,6 +2585,7 @@ export class ProxySessionController {
|
|
|
2615
2585
|
contextKey: input.contextKey,
|
|
2616
2586
|
agentName: input.agentName,
|
|
2617
2587
|
remoteSessionId: input.remoteSessionId,
|
|
2588
|
+
replaceParents: input.replaceParents,
|
|
2618
2589
|
turns: input.completedTurns.map((turn, index) => {
|
|
2619
2590
|
const startedAt = turn.startedAt ?? input.fallbackStart + index;
|
|
2620
2591
|
const completedAt = turn.completedAt ?? startedAt;
|
|
@@ -2645,80 +2616,198 @@ export class ProxySessionController {
|
|
|
2645
2616
|
return 0;
|
|
2646
2617
|
}
|
|
2647
2618
|
}
|
|
2648
|
-
async
|
|
2649
|
-
const
|
|
2650
|
-
if (
|
|
2651
|
-
|
|
2652
|
-
|
|
2619
|
+
async hydrateTaskTurnHistory(current, requiredCount = RESET_HISTORY_PAGE_SIZE) {
|
|
2620
|
+
const existing = this.turnHistoryHydrations.get(current.localSessionId);
|
|
2621
|
+
if (existing)
|
|
2622
|
+
await existing;
|
|
2623
|
+
const hydration = (async () => {
|
|
2624
|
+
if (this.store.getForkHistorySource(current.localSessionId)) {
|
|
2625
|
+
return this.hydrateForkTurnHistory(current.contextKey, current, requiredCount);
|
|
2626
|
+
}
|
|
2627
|
+
return this.hydrateRemoteTurnHistory(current.contextKey, current, requiredCount);
|
|
2628
|
+
})();
|
|
2629
|
+
this.turnHistoryHydrations.set(current.localSessionId, hydration);
|
|
2630
|
+
try {
|
|
2631
|
+
return await hydration;
|
|
2632
|
+
}
|
|
2633
|
+
finally {
|
|
2634
|
+
if (this.turnHistoryHydrations.get(current.localSessionId) === hydration) {
|
|
2635
|
+
this.turnHistoryHydrations.delete(current.localSessionId);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
async hydrateForkTurnHistory(contextKey, current, requiredCount, visited = new Set()) {
|
|
2640
|
+
if (visited.has(current.localSessionId))
|
|
2641
|
+
return false;
|
|
2642
|
+
visited.add(current.localSessionId);
|
|
2643
|
+
let source = this.store.getForkHistorySource(current.localSessionId);
|
|
2644
|
+
if (!source)
|
|
2645
|
+
return false;
|
|
2646
|
+
const completedHead = current.lastTurnStatus === "completed"
|
|
2647
|
+
? current.lastTurnId
|
|
2648
|
+
: this.store.findLatestCompletedTurnId(current.localSessionId);
|
|
2649
|
+
if (completedHead && completedHead !== source.sourceTurnId && current.remoteSessionId) {
|
|
2650
|
+
// Once the branch has its own completed work, its remote history is the
|
|
2651
|
+
// authority for missing local turns, including work continued in Desktop.
|
|
2652
|
+
source = {
|
|
2653
|
+
sourceLocalSessionId: current.localSessionId,
|
|
2654
|
+
sourceRemoteSessionId: current.remoteSessionId,
|
|
2655
|
+
sourceTurnId: completedHead,
|
|
2656
|
+
createdAt: current.createdAt,
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
if (this.store.hasImportedForkTurnHistory(current.localSessionId, source.sourceRemoteSessionId, source.sourceTurnId))
|
|
2660
|
+
return false;
|
|
2661
|
+
const localTurns = this.store.listTaskTurnGraphIndex(current.localSessionId);
|
|
2662
|
+
const availableCount = localTurns.length;
|
|
2663
|
+
const key = JSON.stringify([current.agentName, source.sourceRemoteSessionId, source.sourceTurnId]);
|
|
2664
|
+
let state = this.forkHistoryCursors.get(key) ?? { anchorFound: false, complete: false };
|
|
2665
|
+
const localAnchorIndex = localTurns.findIndex((turn) => turn.turnId === source.sourceTurnId);
|
|
2666
|
+
const ownTurnCount = source.sourceLocalSessionId === current.localSessionId
|
|
2667
|
+
? Math.max(0, localAnchorIndex)
|
|
2668
|
+
: localAnchorIndex >= 0 ? localAnchorIndex : availableCount;
|
|
2669
|
+
const neededInheritedCount = Math.max(0, requiredCount - ownTurnCount);
|
|
2670
|
+
if (localAnchorIndex >= 0 && availableCount >= requiredCount
|
|
2671
|
+
&& (!state.anchorFound || (state.loadedCount ?? 0) >= neededInheritedCount))
|
|
2672
|
+
return !state.complete;
|
|
2673
|
+
const sourceSession = source.sourceLocalSessionId
|
|
2674
|
+
? this.store.getSession(source.sourceLocalSessionId)
|
|
2675
|
+
: undefined;
|
|
2676
|
+
// A branch with no new turns can reuse its ancestor's history, even before
|
|
2677
|
+
// the Agent materializes that intermediate branch.
|
|
2678
|
+
if (sourceSession && sourceSession.localSessionId !== current.localSessionId
|
|
2679
|
+
&& this.store.getForkHistorySource(sourceSession.localSessionId)?.sourceTurnId === source.sourceTurnId) {
|
|
2680
|
+
const hasMore = await this.hydrateForkTurnHistory(sourceSession.contextKey, sourceSession, this.store.listTaskTurnGraphIndex(sourceSession.localSessionId).length + requiredCount - availableCount, visited);
|
|
2681
|
+
if (!hasMore && this.store.hasImportedForkTurnHistory(sourceSession.localSessionId)) {
|
|
2682
|
+
this.store.audit(contextKey, "fork_turn_history_imported", {
|
|
2683
|
+
forkedLocalSessionId: current.localSessionId,
|
|
2684
|
+
sourceRemoteSessionId: source.sourceRemoteSessionId,
|
|
2685
|
+
sourceTurnId: source.sourceTurnId,
|
|
2686
|
+
reusedLocalSessionId: sourceSession.localSessionId,
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
return hasMore;
|
|
2690
|
+
}
|
|
2653
2691
|
if (!source.sourceRemoteSessionId)
|
|
2654
|
-
return;
|
|
2692
|
+
return false;
|
|
2655
2693
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
2656
|
-
if (runtime.kind !== "codex" || !runtime.
|
|
2657
|
-
return;
|
|
2694
|
+
if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries)
|
|
2695
|
+
return false;
|
|
2696
|
+
const seenCursors = new Set();
|
|
2658
2697
|
try {
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
:
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
??
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2698
|
+
while (!state.complete && (state.loadedCount ?? 0) < neededInheritedCount) {
|
|
2699
|
+
const remaining = neededInheritedCount - (state.loadedCount ?? 0);
|
|
2700
|
+
const page = await runtime.listRemoteTurnSummaries(source.sourceRemoteSessionId, {
|
|
2701
|
+
cursor: state.cursor, limit: Math.min(RESET_HISTORY_PAGE_SIZE, remaining),
|
|
2702
|
+
});
|
|
2703
|
+
if (page.nextCursor && (page.nextCursor === state.cursor || seenCursors.has(page.nextCursor))) {
|
|
2704
|
+
throw new Error("App Server repeated a Turn history cursor.");
|
|
2705
|
+
}
|
|
2706
|
+
const anchorIndex = state.anchorFound ? 0 : page.turns.findIndex((turn) => turn.id === source.sourceTurnId);
|
|
2707
|
+
const inheritedTurns = anchorIndex >= 0 ? page.turns.slice(anchorIndex) : [];
|
|
2708
|
+
const anchorFound = state.anchorFound || anchorIndex >= 0;
|
|
2709
|
+
if (!anchorFound && !page.nextCursor) {
|
|
2710
|
+
this.forkHistoryCursors.delete(key);
|
|
2711
|
+
throw new Error("The Fork source Turn was not found in the available history.");
|
|
2712
|
+
}
|
|
2713
|
+
if (inheritedTurns.length > 0) {
|
|
2714
|
+
const ownerId = sourceSession?.localSessionId ?? current.localSessionId;
|
|
2715
|
+
const imported = this.persistCompletedTurnHistory({
|
|
2716
|
+
localSessionId: ownerId,
|
|
2717
|
+
contextKey: sourceSession?.contextKey ?? contextKey,
|
|
2718
|
+
agentName: current.agentName,
|
|
2719
|
+
remoteSessionId: source.sourceRemoteSessionId,
|
|
2720
|
+
completedTurns: inheritedTurns.slice().reverse(),
|
|
2721
|
+
// Adjacent remote summaries repair gaps in locally inferred ancestry.
|
|
2722
|
+
replaceParents: true,
|
|
2723
|
+
fallbackStart: (source.sourceTurnStartedAt ?? source.sourceTurnCompletedAt
|
|
2724
|
+
?? parseIsoTimestamp(source.createdAt) ?? 0) - (state.loadedCount ?? 0) - inheritedTurns.length,
|
|
2725
|
+
});
|
|
2726
|
+
if (imported === 0)
|
|
2727
|
+
throw new Error("Failed to persist the requested Turn history page.");
|
|
2728
|
+
if (state.tailTurnId) {
|
|
2729
|
+
this.store.saveTurnParent(state.tailTurnId, ownerId, inheritedTurns[0].id, true);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
state = {
|
|
2733
|
+
cursor: page.nextCursor,
|
|
2734
|
+
anchorFound,
|
|
2735
|
+
tailTurnId: inheritedTurns.at(-1)?.id ?? state.tailTurnId,
|
|
2736
|
+
loadedCount: (state.loadedCount ?? 0) + inheritedTurns.length,
|
|
2737
|
+
complete: !page.nextCursor,
|
|
2738
|
+
};
|
|
2739
|
+
this.forkHistoryCursors.set(key, state);
|
|
2740
|
+
if (page.nextCursor)
|
|
2741
|
+
seenCursors.add(page.nextCursor);
|
|
2742
|
+
}
|
|
2743
|
+
if (state.complete) {
|
|
2679
2744
|
this.store.audit(contextKey, "fork_turn_history_imported", {
|
|
2680
2745
|
forkedLocalSessionId: current.localSessionId,
|
|
2681
2746
|
sourceRemoteSessionId: source.sourceRemoteSessionId,
|
|
2682
2747
|
sourceTurnId: source.sourceTurnId,
|
|
2683
|
-
importedTurnCount: imported,
|
|
2684
2748
|
});
|
|
2685
2749
|
}
|
|
2750
|
+
return !state.complete;
|
|
2686
2751
|
}
|
|
2687
2752
|
catch (error) {
|
|
2688
|
-
|
|
2753
|
+
if (error instanceof CodexVersionError)
|
|
2754
|
+
throw error;
|
|
2755
|
+
this.logger.warn({ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId }, "Failed to load the requested Fork Turn history page; rendering the locally available history.");
|
|
2689
2756
|
}
|
|
2757
|
+
return true;
|
|
2690
2758
|
}
|
|
2691
|
-
async hydrateRemoteTurnHistory(contextKey, current) {
|
|
2759
|
+
async hydrateRemoteTurnHistory(contextKey, current, requiredCount) {
|
|
2692
2760
|
if (!current.remoteSessionId)
|
|
2693
|
-
return;
|
|
2761
|
+
return false;
|
|
2762
|
+
const key = JSON.stringify([current.agentName, current.remoteSessionId, "turn-card"]);
|
|
2694
2763
|
const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
&& this.store.hasImportedForkTurnHistory(current.localSessionId))
|
|
2700
|
-
return;
|
|
2764
|
+
let state = this.forkHistoryCursors.get(key);
|
|
2765
|
+
if (refreshedAt === undefined || Date.now() - refreshedAt >= REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS)
|
|
2766
|
+
state = undefined;
|
|
2767
|
+
state ??= { anchorFound: true, complete: false };
|
|
2701
2768
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
2702
|
-
if (runtime.kind !== "codex" || !runtime.
|
|
2703
|
-
return;
|
|
2769
|
+
if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries)
|
|
2770
|
+
return false;
|
|
2771
|
+
const seen = new Set();
|
|
2704
2772
|
try {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2773
|
+
while (!state.complete && (state.loadedCount ?? 0) < requiredCount) {
|
|
2774
|
+
const page = await runtime.listRemoteTurnSummaries(current.remoteSessionId, {
|
|
2775
|
+
cursor: state.cursor,
|
|
2776
|
+
limit: Math.min(RESET_HISTORY_PAGE_SIZE, requiredCount - (state.loadedCount ?? 0)),
|
|
2777
|
+
});
|
|
2778
|
+
if (page.nextCursor && (page.nextCursor === state.cursor || seen.has(page.nextCursor))) {
|
|
2779
|
+
throw new Error("App Server repeated a Turn history cursor.");
|
|
2780
|
+
}
|
|
2781
|
+
if (page.turns.length) {
|
|
2782
|
+
const imported = this.persistCompletedTurnHistory({
|
|
2783
|
+
localSessionId: current.localSessionId, contextKey, agentName: current.agentName,
|
|
2784
|
+
remoteSessionId: current.remoteSessionId,
|
|
2785
|
+
completedTurns: page.turns.slice().reverse(), replaceParents: true,
|
|
2786
|
+
fallbackStart: (parseIsoTimestamp(current.createdAt) ?? 0) - (state.loadedCount ?? 0) - page.turns.length,
|
|
2787
|
+
});
|
|
2788
|
+
if (!imported)
|
|
2789
|
+
throw new Error("Failed to persist the requested Turn history page.");
|
|
2790
|
+
if (state.tailTurnId)
|
|
2791
|
+
this.store.saveTurnParent(state.tailTurnId, current.localSessionId, page.turns[0].id, true);
|
|
2792
|
+
}
|
|
2793
|
+
state = {
|
|
2794
|
+
anchorFound: true, cursor: page.nextCursor, complete: !page.nextCursor,
|
|
2795
|
+
loadedCount: (state.loadedCount ?? 0) + page.turns.length,
|
|
2796
|
+
tailTurnId: page.turns.at(-1)?.id ?? state.tailTurnId,
|
|
2797
|
+
};
|
|
2798
|
+
this.forkHistoryCursors.set(key, state);
|
|
2799
|
+
this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
|
|
2800
|
+
if (page.nextCursor)
|
|
2801
|
+
seen.add(page.nextCursor);
|
|
2802
|
+
}
|
|
2803
|
+
return !state.complete;
|
|
2718
2804
|
}
|
|
2719
2805
|
catch (error) {
|
|
2806
|
+
if (error instanceof CodexVersionError)
|
|
2807
|
+
throw error;
|
|
2720
2808
|
this.logger.warn({ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId }, "Failed to hydrate App Server Turn history; rendering the locally available history.");
|
|
2721
2809
|
}
|
|
2810
|
+
return true;
|
|
2722
2811
|
}
|
|
2723
2812
|
async createProjectSessionFromReference(contextKey, reference) {
|
|
2724
2813
|
const resolved = await this.resolveProjectSessionReference(contextKey, reference);
|
|
@@ -2750,7 +2839,7 @@ export class ProxySessionController {
|
|
|
2750
2839
|
runtime = this.runtimes.forAgent(agentName);
|
|
2751
2840
|
}
|
|
2752
2841
|
else {
|
|
2753
|
-
const resolved = await this.resolveRemoteCodexSession(taskId);
|
|
2842
|
+
const resolved = await this.resolveRemoteCodexSession(taskId, { view: "metadata" });
|
|
2754
2843
|
agentName = resolved.agentName;
|
|
2755
2844
|
runtime = resolved.runtime;
|
|
2756
2845
|
remote = resolved.remote;
|
|
@@ -2758,10 +2847,10 @@ export class ProxySessionController {
|
|
|
2758
2847
|
const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
|
|
2759
2848
|
if (!remote && runtime.readRemoteSession && remoteSessionId) {
|
|
2760
2849
|
try {
|
|
2761
|
-
remote = await runtime.readRemoteSession(remoteSessionId);
|
|
2850
|
+
remote = await runtime.readRemoteSession(remoteSessionId, "metadata");
|
|
2762
2851
|
}
|
|
2763
2852
|
catch (error) {
|
|
2764
|
-
if (!source)
|
|
2853
|
+
if (!source || error instanceof CodexVersionError)
|
|
2765
2854
|
throw error;
|
|
2766
2855
|
this.logger.warn({ error, contextKey, taskId: remoteSessionId }, "Failed to refresh the source task before creating a project task; using the local project path.");
|
|
2767
2856
|
}
|
|
@@ -2992,6 +3081,7 @@ export class ProxySessionController {
|
|
|
2992
3081
|
if (!userId?.startsWith("ou_")) {
|
|
2993
3082
|
throw new Error("/forkgroup 只能由具有 open_id 的飞书用户消息触发。");
|
|
2994
3083
|
}
|
|
3084
|
+
await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
|
|
2995
3085
|
const prepared = await this.prepareForkGroupSession(sourceContextKey, requestedTitle, incomingMessage);
|
|
2996
3086
|
await this.forkPreparedSessionToFeishuGroup(sourceContextKey, prepared, userId, incomingMessage?.threadContext ? prepared.sourceDescription : "当前任务");
|
|
2997
3087
|
}
|
|
@@ -2999,6 +3089,7 @@ export class ProxySessionController {
|
|
|
2999
3089
|
if (!userId?.startsWith("ou_")) {
|
|
3000
3090
|
throw new Error("ForkGroup 只能由具有 open_id 的飞书用户触发。");
|
|
3001
3091
|
}
|
|
3092
|
+
await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
|
|
3002
3093
|
const plan = await this.prepareForkSession(sourceContextKey, reference);
|
|
3003
3094
|
await this.forkPreparedSessionToFeishuGroup(sourceContextKey, {
|
|
3004
3095
|
plan,
|
|
@@ -3107,7 +3198,8 @@ export class ProxySessionController {
|
|
|
3107
3198
|
catch (error) {
|
|
3108
3199
|
if (activeWriterThreadId(error))
|
|
3109
3200
|
throw error;
|
|
3110
|
-
if (!(agent.kind === "app-server" && !record.lastTurnId
|
|
3201
|
+
if (!(agent.kind === "app-server" && !record.lastTurnId
|
|
3202
|
+
&& !this.store.getForkHistorySource(record.localSessionId) && isMissingRolloutError(error)))
|
|
3111
3203
|
throw error;
|
|
3112
3204
|
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
3113
3205
|
session = await runtime.createSession({
|
|
@@ -3522,6 +3614,7 @@ export class ProxySessionController {
|
|
|
3522
3614
|
}
|
|
3523
3615
|
async cancel(contextKey) {
|
|
3524
3616
|
const record = this.requireCurrentSession(contextKey);
|
|
3617
|
+
await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
|
|
3525
3618
|
await this.cancelSession(record);
|
|
3526
3619
|
}
|
|
3527
3620
|
async cancelSession(record) {
|
|
@@ -3536,6 +3629,8 @@ export class ProxySessionController {
|
|
|
3536
3629
|
await runtime.setGoal(record.localSessionId, { status: "paused" });
|
|
3537
3630
|
}
|
|
3538
3631
|
catch (error) {
|
|
3632
|
+
if (error instanceof CodexVersionError)
|
|
3633
|
+
throw error;
|
|
3539
3634
|
this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to pause the active Agent goal before interrupting its turn.");
|
|
3540
3635
|
}
|
|
3541
3636
|
}
|
|
@@ -3548,6 +3643,8 @@ export class ProxySessionController {
|
|
|
3548
3643
|
: undefined;
|
|
3549
3644
|
}
|
|
3550
3645
|
catch (error) {
|
|
3646
|
+
if (error instanceof CodexVersionError)
|
|
3647
|
+
throw error;
|
|
3551
3648
|
this.logger.warn({ error, sessionId: record.localSessionId, remoteSessionId: record.remoteSessionId }, "Failed to inspect the current App Server turn before interrupting; using the locally tracked turn.");
|
|
3552
3649
|
}
|
|
3553
3650
|
}
|
|
@@ -3591,8 +3688,14 @@ export class ProxySessionController {
|
|
|
3591
3688
|
async resetCurrentSessionToTurn(contextKey, sessionId, turnId) {
|
|
3592
3689
|
if (!sessionId || !turnId)
|
|
3593
3690
|
throw new Error("无效的 Reset 请求。请使用最新的思考卡片重试。");
|
|
3691
|
+
await this.runSessionReset(sessionId, async () => {
|
|
3692
|
+
await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
|
|
3693
|
+
await this.performCurrentSessionReset(contextKey, sessionId, turnId);
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
async runSessionReset(sessionId, operation) {
|
|
3594
3697
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
3595
|
-
const reset = previous.catch(() => undefined).then(
|
|
3698
|
+
const reset = previous.catch(() => undefined).then(operation);
|
|
3596
3699
|
this.sessionResets.set(sessionId, reset);
|
|
3597
3700
|
try {
|
|
3598
3701
|
await reset;
|
|
@@ -3602,6 +3705,19 @@ export class ProxySessionController {
|
|
|
3602
3705
|
this.sessionResets.delete(sessionId);
|
|
3603
3706
|
}
|
|
3604
3707
|
}
|
|
3708
|
+
async waitForCurrentSessionReset(contextKey) {
|
|
3709
|
+
const sessionId = this.currentSession(contextKey)?.localSessionId;
|
|
3710
|
+
if (!sessionId)
|
|
3711
|
+
return;
|
|
3712
|
+
let reset = this.sessionResets.get(sessionId);
|
|
3713
|
+
while (reset) {
|
|
3714
|
+
await reset.catch(() => undefined);
|
|
3715
|
+
const next = this.sessionResets.get(sessionId);
|
|
3716
|
+
if (!next || next === reset)
|
|
3717
|
+
return;
|
|
3718
|
+
reset = next;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3605
3721
|
async resetFromHistoryCard(contextKey, sessionId, turnId, updateMessageId, page) {
|
|
3606
3722
|
if (!sessionId || !turnId)
|
|
3607
3723
|
throw new Error("无效的 Reset 请求。请使用最新的历史轮次卡片重试。");
|
|
@@ -3612,16 +3728,11 @@ export class ProxySessionController {
|
|
|
3612
3728
|
}
|
|
3613
3729
|
this.resetHistoryOperations.set(sessionId, turnId);
|
|
3614
3730
|
try {
|
|
3615
|
-
await this.
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
}
|
|
3621
|
-
throw error;
|
|
3622
|
-
}
|
|
3623
|
-
try {
|
|
3624
|
-
await this.resetCurrentSessionToTurn(contextKey, sessionId, turnId);
|
|
3731
|
+
await this.runSessionReset(sessionId, async () => {
|
|
3732
|
+
await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
|
|
3733
|
+
await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
|
|
3734
|
+
await this.performCurrentSessionReset(contextKey, sessionId, turnId);
|
|
3735
|
+
});
|
|
3625
3736
|
}
|
|
3626
3737
|
catch (error) {
|
|
3627
3738
|
if (this.resetHistoryOperations.get(sessionId) === turnId) {
|
|
@@ -3651,9 +3762,9 @@ export class ProxySessionController {
|
|
|
3651
3762
|
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
3652
3763
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
3653
3764
|
}
|
|
3654
|
-
const historyTurn = this.store.
|
|
3765
|
+
const historyTurn = this.store.listTaskTurnGraphIndex(current.localSessionId)
|
|
3655
3766
|
.find((turn) => turn.turnId === turnId);
|
|
3656
|
-
const snapshot =
|
|
3767
|
+
const snapshot = historyTurn ? this.store.getTurnPromptSummary(turnId) : undefined;
|
|
3657
3768
|
if (!snapshot || snapshot.status !== "completed") {
|
|
3658
3769
|
throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
|
|
3659
3770
|
}
|
|
@@ -3809,6 +3920,7 @@ export class ProxySessionController {
|
|
|
3809
3920
|
const record = options.sessionId
|
|
3810
3921
|
? this.requireSession(contextKey, options.sessionId)
|
|
3811
3922
|
: this.requireCurrentSession(contextKey);
|
|
3923
|
+
await this.outbound.sendText(contextKey, `正在切换到 Provider ${modelProvider},请稍后。`);
|
|
3812
3924
|
const loaded = await this.loadSession(record);
|
|
3813
3925
|
await this.assertModelProvider(loaded, modelProvider);
|
|
3814
3926
|
const models = await loaded.runtime.listModels();
|
|
@@ -3826,7 +3938,7 @@ export class ProxySessionController {
|
|
|
3826
3938
|
model: model.id,
|
|
3827
3939
|
effort,
|
|
3828
3940
|
mode: loaded.session.permissionMode,
|
|
3829
|
-
}, options);
|
|
3941
|
+
}, options, false);
|
|
3830
3942
|
}
|
|
3831
3943
|
async openProviderSelector(contextKey, options = {}) {
|
|
3832
3944
|
await this.openExecutionSettings(contextKey, "provider", options);
|
|
@@ -3881,28 +3993,21 @@ export class ProxySessionController {
|
|
|
3881
3993
|
currentMode: permissionMode,
|
|
3882
3994
|
}));
|
|
3883
3995
|
}
|
|
3884
|
-
async applyProviderSettings(contextKey, settings, options = {}) {
|
|
3996
|
+
async applyProviderSettings(contextKey, settings, options = {}, announceStart = true) {
|
|
3885
3997
|
const record = options.sessionId
|
|
3886
3998
|
? this.requireSession(contextKey, options.sessionId)
|
|
3887
3999
|
: this.requireCurrentSession(contextKey);
|
|
4000
|
+
if (announceStart) {
|
|
4001
|
+
await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
4002
|
+
}
|
|
3888
4003
|
const loaded = await this.loadSession(record);
|
|
3889
4004
|
await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
3890
|
-
|
|
3891
|
-
throw new Error("当前运行时不支持 Provider 设置。");
|
|
3892
|
-
}
|
|
3893
|
-
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
|
|
4005
|
+
const session = await this.changeProviderSettings(loaded, {
|
|
3894
4006
|
modelProvider: settings.provider,
|
|
3895
4007
|
model: settings.model,
|
|
3896
4008
|
reasoningEffort: settings.effort,
|
|
3897
4009
|
permissionMode: settings.mode,
|
|
3898
4010
|
});
|
|
3899
|
-
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
3900
|
-
modelProvider: session.modelProvider ?? settings.provider,
|
|
3901
|
-
model: session.model ?? settings.model,
|
|
3902
|
-
reasoningEffort: session.reasoningEffort ?? settings.effort,
|
|
3903
|
-
permissionMode: session.permissionMode,
|
|
3904
|
-
});
|
|
3905
|
-
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
3906
4011
|
const notice = [
|
|
3907
4012
|
`Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
|
|
3908
4013
|
`模型 ${cardCode(session.model ?? settings.model)}`,
|
|
@@ -3921,6 +4026,31 @@ export class ProxySessionController {
|
|
|
3921
4026
|
await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
|
|
3922
4027
|
}
|
|
3923
4028
|
}
|
|
4029
|
+
async changeProviderSettings(loaded, settings) {
|
|
4030
|
+
if (!loaded.runtime.setExecutionSettings)
|
|
4031
|
+
throw new Error("当前运行时不支持 Provider 设置。");
|
|
4032
|
+
const persist = async (candidate) => {
|
|
4033
|
+
assertProviderSettingsApplied(candidate, settings);
|
|
4034
|
+
const agent = this.ensureAgent(loaded.record.agentName);
|
|
4035
|
+
const previousDefaults = agent.defaults;
|
|
4036
|
+
await this.persistAgentExecutionDefaults(loaded.record.localSessionId, candidate);
|
|
4037
|
+
try {
|
|
4038
|
+
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
4039
|
+
remoteSessionId: candidate.remoteSessionId,
|
|
4040
|
+
modelProvider: candidate.modelProvider,
|
|
4041
|
+
model: candidate.model,
|
|
4042
|
+
reasoningEffort: candidate.reasoningEffort,
|
|
4043
|
+
permissionMode: candidate.permissionMode,
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
4046
|
+
catch (error) {
|
|
4047
|
+
await this.lifecycle?.persistAgentExecutionDefaults?.(loaded.record.agentName, previousDefaults ?? {});
|
|
4048
|
+
agent.defaults = previousDefaults;
|
|
4049
|
+
throw error;
|
|
4050
|
+
}
|
|
4051
|
+
};
|
|
4052
|
+
return loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
4053
|
+
}
|
|
3924
4054
|
async assertProviderModelSettings(loaded, modelProvider, model, effort) {
|
|
3925
4055
|
await this.assertModelProvider(loaded, modelProvider);
|
|
3926
4056
|
const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
|
|
@@ -4283,7 +4413,9 @@ export class ProxySessionController {
|
|
|
4283
4413
|
const normalizedTitle = normalizeTaskTitle(title);
|
|
4284
4414
|
if (!normalizedTitle)
|
|
4285
4415
|
throw new Error("任务标题不能为空。");
|
|
4286
|
-
const
|
|
4416
|
+
const current = this.requireCurrentSession(contextKey);
|
|
4417
|
+
await this.outbound.sendText(contextKey, CHANGE_TITLE_STARTED_MESSAGE);
|
|
4418
|
+
const loaded = await this.loadSession(current);
|
|
4287
4419
|
if (loaded.runtime.setTitle)
|
|
4288
4420
|
await loaded.runtime.setTitle(loaded.record.localSessionId, normalizedTitle);
|
|
4289
4421
|
else
|
|
@@ -4403,9 +4535,9 @@ export class ProxySessionController {
|
|
|
4403
4535
|
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
4404
4536
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
4405
4537
|
}
|
|
4406
|
-
|
|
4407
|
-
await this.
|
|
4408
|
-
const completedTurns = this.store.
|
|
4538
|
+
const requestedPage = Math.max(0, Math.trunc(options.page ?? 0));
|
|
4539
|
+
const hasMoreHistory = await this.hydrateTaskTurnHistory(current, (requestedPage + 1) * RESET_HISTORY_PAGE_SIZE);
|
|
4540
|
+
const completedTurns = this.store.listTaskTurnGraphIndex(current.localSessionId);
|
|
4409
4541
|
const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
|
|
4410
4542
|
const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
|
|
4411
4543
|
&& !completedTurnIds.has(current.lastTurnId)
|
|
@@ -4447,13 +4579,13 @@ export class ProxySessionController {
|
|
|
4447
4579
|
parentTurnId: turn.parentTurnId,
|
|
4448
4580
|
})));
|
|
4449
4581
|
const total = allTurns.length;
|
|
4450
|
-
const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE));
|
|
4582
|
+
const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE)) + (hasMoreHistory ? 1 : 0);
|
|
4451
4583
|
const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
|
|
4452
4584
|
const offset = page * RESET_HISTORY_PAGE_SIZE;
|
|
4453
4585
|
const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
|
|
4454
4586
|
const resettingTurnId = this.resetHistoryOperations.get(current.localSessionId);
|
|
4455
4587
|
const entries = turns.flatMap((turn, index) => {
|
|
4456
|
-
const snapshot =
|
|
4588
|
+
const snapshot = turn.turnId === runningTurnId ? runningSnapshot : this.store.getTurnPromptSummary(turn.turnId);
|
|
4457
4589
|
if (!snapshot)
|
|
4458
4590
|
return [];
|
|
4459
4591
|
const graph = graphRows[offset + index];
|
|
@@ -4506,9 +4638,11 @@ export class ProxySessionController {
|
|
|
4506
4638
|
const card = this.cardRenderer.renderResetHistoryCard({
|
|
4507
4639
|
entries,
|
|
4508
4640
|
footerLines: [
|
|
4509
|
-
|
|
4510
|
-
? `第 ${page + 1}
|
|
4511
|
-
:
|
|
4641
|
+
hasMoreHistory
|
|
4642
|
+
? `第 ${page + 1} 页 · 已加载 ${total} 个 turn`
|
|
4643
|
+
: runningTurn
|
|
4644
|
+
? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
|
|
4645
|
+
: `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
|
|
4512
4646
|
...(resettingTurnId ? ["正在 Reset 到所选轮次,请稍候…"] : []),
|
|
4513
4647
|
],
|
|
4514
4648
|
pageActions,
|
|
@@ -4707,6 +4841,7 @@ export class ProxySessionController {
|
|
|
4707
4841
|
const record = this.requireCurrentSession(contextKey);
|
|
4708
4842
|
const filePath = resolveUserPath(requestedFilePath, record.cwd);
|
|
4709
4843
|
await this.assertSendableFile(filePath);
|
|
4844
|
+
await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
|
|
4710
4845
|
await this.outbound.sendFile(contextKey, filePath);
|
|
4711
4846
|
}
|
|
4712
4847
|
async listSessions(contextKey, searchTerm, options = {}) {
|
|
@@ -4997,7 +5132,7 @@ export class ProxySessionController {
|
|
|
4997
5132
|
runtime,
|
|
4998
5133
|
remote: options.forFork && runtime.readRemoteForkSource
|
|
4999
5134
|
? await runtime.readRemoteForkSource(remoteSessionId)
|
|
5000
|
-
: await runtime.readRemoteSession(remoteSessionId),
|
|
5135
|
+
: await runtime.readRemoteSession(remoteSessionId, options.view ?? "latest"),
|
|
5001
5136
|
};
|
|
5002
5137
|
}));
|
|
5003
5138
|
const matches = reads
|
|
@@ -5023,6 +5158,7 @@ export class ProxySessionController {
|
|
|
5023
5158
|
if (direct?.status === "closed") {
|
|
5024
5159
|
throw new Error(`找不到任务:${taskId}`);
|
|
5025
5160
|
}
|
|
5161
|
+
await this.outbound.sendText(contextKey, SWITCH_TASK_STARTED_MESSAGE);
|
|
5026
5162
|
const existing = direct ?? this.findStoredSessionByReference(taskId);
|
|
5027
5163
|
if (existing) {
|
|
5028
5164
|
const runtime = this.runtimes.forAgent(existing.agentName);
|
|
@@ -5107,6 +5243,9 @@ export class ProxySessionController {
|
|
|
5107
5243
|
if (local && this.store.countQueuedPrompts(local.localSessionId) > 0) {
|
|
5108
5244
|
throw new Error("任务仍有排队消息,无法归档。请先处理或取消排队消息。");
|
|
5109
5245
|
}
|
|
5246
|
+
if (options.source !== "dismiss") {
|
|
5247
|
+
await this.outbound.sendText(contextKey, ARCHIVE_TASK_STARTED_MESSAGE);
|
|
5248
|
+
}
|
|
5110
5249
|
const remoteReference = local
|
|
5111
5250
|
? remoteSessionReference(local.agentName, local.remoteSessionId)
|
|
5112
5251
|
: taskId;
|
|
@@ -5314,28 +5453,32 @@ export class ProxySessionController {
|
|
|
5314
5453
|
else {
|
|
5315
5454
|
current = context.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
|
|
5316
5455
|
}
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5456
|
+
let remote;
|
|
5457
|
+
if (current?.status === "running") {
|
|
5458
|
+
const runtime = this.runtimes.forAgent(current.agentName);
|
|
5459
|
+
if (runtime.getSession(current.localSessionId)) {
|
|
5460
|
+
try {
|
|
5461
|
+
remote = (await runtime.synchronizeSession(current.localSessionId)).remoteSummary;
|
|
5322
5462
|
current = this.store.getSession(current.localSessionId) ?? current;
|
|
5323
5463
|
}
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5464
|
+
catch (error) {
|
|
5465
|
+
if (error instanceof CodexVersionError)
|
|
5466
|
+
throw error;
|
|
5467
|
+
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
|
|
5468
|
+
}
|
|
5327
5469
|
}
|
|
5328
5470
|
}
|
|
5329
5471
|
const localCurrent = current;
|
|
5330
|
-
let remote;
|
|
5331
5472
|
let goal;
|
|
5332
5473
|
if (current?.remoteSessionId) {
|
|
5333
5474
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
5334
|
-
if (runtime.readRemoteSession) {
|
|
5475
|
+
if (!remote && runtime.readRemoteSession) {
|
|
5335
5476
|
try {
|
|
5336
|
-
remote = await runtime.readRemoteSession(current.remoteSessionId);
|
|
5477
|
+
remote = await runtime.readRemoteSession(current.remoteSessionId, "latest-full");
|
|
5337
5478
|
}
|
|
5338
5479
|
catch (error) {
|
|
5480
|
+
if (error instanceof CodexVersionError)
|
|
5481
|
+
throw error;
|
|
5339
5482
|
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task status.");
|
|
5340
5483
|
}
|
|
5341
5484
|
}
|
|
@@ -5431,7 +5574,7 @@ export class ProxySessionController {
|
|
|
5431
5574
|
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
5432
5575
|
}
|
|
5433
5576
|
async statusForCodexTask(contextKey, reference, options = {}) {
|
|
5434
|
-
const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
|
|
5577
|
+
const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
|
|
5435
5578
|
const actionReference = remoteSessionReference(agentName, remote.id);
|
|
5436
5579
|
const sections = [
|
|
5437
5580
|
{
|
|
@@ -5562,10 +5705,11 @@ export class ProxySessionController {
|
|
|
5562
5705
|
throw new Error(`未知 agent:${agentName}`);
|
|
5563
5706
|
return agent;
|
|
5564
5707
|
}
|
|
5565
|
-
async persistAgentExecutionDefaults(localSessionId) {
|
|
5566
|
-
const
|
|
5567
|
-
if (!
|
|
5708
|
+
async persistAgentExecutionDefaults(localSessionId, candidate) {
|
|
5709
|
+
const record = this.store.getSession(localSessionId);
|
|
5710
|
+
if (!record)
|
|
5568
5711
|
throw new Error(`找不到任务:${localSessionId}`);
|
|
5712
|
+
const session = candidate ?? record;
|
|
5569
5713
|
const agent = this.ensureAgent(session.agentName);
|
|
5570
5714
|
const observed = {
|
|
5571
5715
|
...(session.modelProvider?.trim() ? { modelProvider: session.modelProvider } : {}),
|
|
@@ -5935,6 +6079,7 @@ export class ProxySessionController {
|
|
|
5935
6079
|
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
5936
6080
|
if (!lockPath)
|
|
5937
6081
|
throw new Error("当前 Agent 不支持自动关闭占用程序。");
|
|
6082
|
+
await this.outbound.sendText(contextKey, CLOSE_WRITER_STARTED_MESSAGE);
|
|
5938
6083
|
let owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
5939
6084
|
const expectedOwner = owners.length === 1 && threadWriterMatches(owners[0], expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt) ? owners[0] : undefined;
|
|
5940
6085
|
if (!expectedOwner) {
|
|
@@ -6260,10 +6405,6 @@ function bindSessionCardAction(action, bindings) {
|
|
|
6260
6405
|
function runtimeErrorMessage(error) {
|
|
6261
6406
|
return error instanceof Error ? error.message : String(error);
|
|
6262
6407
|
}
|
|
6263
|
-
function isMissingRolloutError(error) {
|
|
6264
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6265
|
-
return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
|
|
6266
|
-
}
|
|
6267
6408
|
function isUnmaterializedCodexThreadError(error) {
|
|
6268
6409
|
const message = error instanceof Error ? error.message : String(error);
|
|
6269
6410
|
return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
|