@keyou007/agent-bot 0.1.20-alpha.1 → 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 +11 -0
- package/README.md +13 -0
- 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/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 +14 -4
- 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 +399 -257
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +58 -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/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 +14 -4
- package/src/local-files/MarkdownFileLinks.ts +64 -0
- package/src/proxy/ProxySessionController.ts +369 -238
- package/src/runtime/types.ts +15 -2
- package/src/state/StateStore.ts +77 -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
|
}
|
|
@@ -2284,6 +2321,7 @@ export class ProxySessionController {
|
|
|
2284
2321
|
return { anchor, source, snapshot };
|
|
2285
2322
|
}
|
|
2286
2323
|
async forkSessionReference(contextKey, reference) {
|
|
2324
|
+
await this.outbound.sendText(contextKey, FORK_TASK_STARTED_MESSAGE);
|
|
2287
2325
|
const plan = await this.prepareForkSession(contextKey, reference);
|
|
2288
2326
|
const forked = await this.forkSessionIntoContext(contextKey, plan);
|
|
2289
2327
|
const forkSourceLabel = plan.forkedFromHistoricalTurn
|
|
@@ -2335,32 +2373,22 @@ export class ProxySessionController {
|
|
|
2335
2373
|
if (!runtime.readRemoteSession && !source) {
|
|
2336
2374
|
throw new Error("当前 App Server Agent 不支持读取指定任务。");
|
|
2337
2375
|
}
|
|
2338
|
-
const
|
|
2376
|
+
const persistedCompletedTurnId = source
|
|
2377
|
+
? this.store.findLatestCompletedTurnId(source.localSessionId)
|
|
2378
|
+
: undefined;
|
|
2339
2379
|
const sourceLastSnapshot = turnViewSnapshot(source?.lastTurnId ? this.store.getTurnSnapshot(source.lastTurnId) : undefined);
|
|
2340
|
-
const
|
|
2341
|
-
|
|
2342
|
-
: source?.lastTurnId
|
|
2380
|
+
const localCompletedTurnId = persistedCompletedTurnId
|
|
2381
|
+
?? (source?.lastTurnId
|
|
2343
2382
|
&& (source.lastTurnStatus === "completed" || sourceLastSnapshot?.status === "completed")
|
|
2344
|
-
?
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
}]
|
|
2352
|
-
: [];
|
|
2353
|
-
if (!remote && runtime.readRemoteSession && (!source || persistedCompletedTurns.length === 0)) {
|
|
2354
|
-
try {
|
|
2355
|
-
remote = await runtime.readRemoteSession(remoteSessionId);
|
|
2356
|
-
}
|
|
2357
|
-
catch (error) {
|
|
2358
|
-
if (localCompletedTurns.length === 0)
|
|
2359
|
-
throw error;
|
|
2360
|
-
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.");
|
|
2361
|
-
}
|
|
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);
|
|
2362
2390
|
}
|
|
2363
|
-
const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ??
|
|
2391
|
+
const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurnId;
|
|
2364
2392
|
const latestSnapshot = turnViewSnapshot(latestTurnId ? this.store.getTurnSnapshot(latestTurnId) : undefined);
|
|
2365
2393
|
const isRunning = remote
|
|
2366
2394
|
? isRemoteSessionActive(remote)
|
|
@@ -2369,7 +2397,7 @@ export class ProxySessionController {
|
|
|
2369
2397
|
|| isTurnStillRunning(latestSnapshot?.status));
|
|
2370
2398
|
const lastTurnId = remote?.lastCompletedTurnId
|
|
2371
2399
|
?? (remote?.lastTurnStatus === "completed" ? remote.lastTurnId : undefined)
|
|
2372
|
-
??
|
|
2400
|
+
?? localCompletedTurnId;
|
|
2373
2401
|
if (!lastTurnId) {
|
|
2374
2402
|
if (isRunning) {
|
|
2375
2403
|
throw new Error(`${sourceLabel}正在执行,且还没有已完成轮次可供 fork。请等待当前轮次完成后重试。`);
|
|
@@ -2378,8 +2406,7 @@ export class ProxySessionController {
|
|
|
2378
2406
|
}
|
|
2379
2407
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
|
|
2380
2408
|
const forkedFromHistoricalTurn = lastTurnId !== latestTurnId;
|
|
2381
|
-
const completedTurn =
|
|
2382
|
-
.find((turn) => turn.id === lastTurnId);
|
|
2409
|
+
const completedTurn = remote?.completedTurns?.find((turn) => turn.id === lastTurnId);
|
|
2383
2410
|
const cwd = remote?.cwd || source?.cwd;
|
|
2384
2411
|
if (!cwd)
|
|
2385
2412
|
throw new Error("指定的 App Server 任务没有可用的工作目录,暂时不能 fork。");
|
|
@@ -2421,27 +2448,10 @@ export class ProxySessionController {
|
|
|
2421
2448
|
sourceTurnPrompt,
|
|
2422
2449
|
sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
|
|
2423
2450
|
sourceTurnCompletedAt,
|
|
2424
|
-
sourceCompletedTurns: remote?.completedTurns
|
|
2425
|
-
?? (persistedCompletedTurns.length > 0 ? [] : localCompletedTurns),
|
|
2426
2451
|
sourceWasRunning: isRunning,
|
|
2427
2452
|
forkedFromHistoricalTurn,
|
|
2428
2453
|
};
|
|
2429
2454
|
}
|
|
2430
|
-
localForkCompletedTurns(source) {
|
|
2431
|
-
return this.store.listTaskTurnGraph(source.localSessionId)
|
|
2432
|
-
.slice()
|
|
2433
|
-
.reverse()
|
|
2434
|
-
.map((record) => {
|
|
2435
|
-
const snapshot = turnViewSnapshot(record.snapshot);
|
|
2436
|
-
const startedAt = snapshot?.startedAt ?? parseIsoTimestamp(record.updatedAt);
|
|
2437
|
-
return {
|
|
2438
|
-
id: record.turnId,
|
|
2439
|
-
prompt: latestUserPromptFromTurnView(snapshot),
|
|
2440
|
-
startedAt,
|
|
2441
|
-
completedAt: snapshot?.completedAt ?? startedAt,
|
|
2442
|
-
};
|
|
2443
|
-
});
|
|
2444
|
-
}
|
|
2445
2455
|
async prepareForkGroupSession(contextKey, requestedTitle, incomingMessage) {
|
|
2446
2456
|
if (!incomingMessage?.threadContext) {
|
|
2447
2457
|
const plan = await this.prepareForkSession(contextKey, undefined, requestedTitle);
|
|
@@ -2511,7 +2521,6 @@ export class ProxySessionController {
|
|
|
2511
2521
|
};
|
|
2512
2522
|
}
|
|
2513
2523
|
async forkSessionIntoContext(contextKey, plan) {
|
|
2514
|
-
const sourceCompletedTurns = await this.resolveForkSourceCompletedTurns(plan);
|
|
2515
2524
|
const localSessionId = createId("sess");
|
|
2516
2525
|
const record = this.store.createSession({
|
|
2517
2526
|
localSessionId,
|
|
@@ -2547,7 +2556,6 @@ export class ProxySessionController {
|
|
|
2547
2556
|
lastTurnStatus: plan.lastTurnStatus,
|
|
2548
2557
|
});
|
|
2549
2558
|
this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName));
|
|
2550
|
-
const sourceTurnHistoryCount = this.persistForkSourceHistory(contextKey, localSessionId, plan, sourceCompletedTurns);
|
|
2551
2559
|
this.store.audit(contextKey, "session_forked", {
|
|
2552
2560
|
sourceLocalSessionId: plan.source?.localSessionId,
|
|
2553
2561
|
sourceRemoteSessionId: plan.remoteSessionId,
|
|
@@ -2555,7 +2563,6 @@ export class ProxySessionController {
|
|
|
2555
2563
|
sourceTurnPrompt: plan.sourceTurnPrompt,
|
|
2556
2564
|
sourceTurnStartedAt: plan.sourceTurnStartedAt,
|
|
2557
2565
|
sourceTurnCompletedAt: plan.sourceTurnCompletedAt,
|
|
2558
|
-
sourceTurnHistoryCount,
|
|
2559
2566
|
sourceWasRunning: plan.sourceWasRunning,
|
|
2560
2567
|
forkedLocalSessionId: localSessionId,
|
|
2561
2568
|
forkedRemoteSessionId: forked.remoteSessionId,
|
|
@@ -2571,42 +2578,6 @@ export class ProxySessionController {
|
|
|
2571
2578
|
throw error;
|
|
2572
2579
|
}
|
|
2573
2580
|
}
|
|
2574
|
-
async resolveForkSourceCompletedTurns(plan) {
|
|
2575
|
-
let completedTurns = plan.sourceCompletedTurns;
|
|
2576
|
-
if (completedTurns === undefined && plan.runtime.readRemoteSession) {
|
|
2577
|
-
try {
|
|
2578
|
-
completedTurns = (await plan.runtime.readRemoteSession(plan.remoteSessionId)).completedTurns ?? [];
|
|
2579
|
-
}
|
|
2580
|
-
catch (error) {
|
|
2581
|
-
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.");
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
2584
|
-
if (!completedTurns?.length)
|
|
2585
|
-
return [];
|
|
2586
|
-
const anchorIndex = completedTurns.findIndex((turn) => turn.id === plan.lastTurnId);
|
|
2587
|
-
if (anchorIndex < 0) {
|
|
2588
|
-
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.");
|
|
2589
|
-
return [];
|
|
2590
|
-
}
|
|
2591
|
-
return completedTurns.slice(0, anchorIndex + 1);
|
|
2592
|
-
}
|
|
2593
|
-
persistForkSourceHistory(contextKey, forkedLocalSessionId, plan, completedTurns) {
|
|
2594
|
-
if (completedTurns.length === 0)
|
|
2595
|
-
return 0;
|
|
2596
|
-
const localSessionId = plan.source?.localSessionId ?? forkedLocalSessionId;
|
|
2597
|
-
const historyContextKey = plan.source?.contextKey ?? contextKey;
|
|
2598
|
-
const fallbackStart = plan.sourceTurnStartedAt
|
|
2599
|
-
?? plan.sourceTurnCompletedAt
|
|
2600
|
-
?? Date.now() - completedTurns.length;
|
|
2601
|
-
return this.persistCompletedTurnHistory({
|
|
2602
|
-
localSessionId,
|
|
2603
|
-
contextKey: historyContextKey,
|
|
2604
|
-
agentName: plan.agentName,
|
|
2605
|
-
remoteSessionId: plan.remoteSessionId,
|
|
2606
|
-
completedTurns,
|
|
2607
|
-
fallbackStart,
|
|
2608
|
-
});
|
|
2609
|
-
}
|
|
2610
2581
|
persistCompletedTurnHistory(input) {
|
|
2611
2582
|
try {
|
|
2612
2583
|
this.store.importCompletedTurnHistory({
|
|
@@ -2614,6 +2585,7 @@ export class ProxySessionController {
|
|
|
2614
2585
|
contextKey: input.contextKey,
|
|
2615
2586
|
agentName: input.agentName,
|
|
2616
2587
|
remoteSessionId: input.remoteSessionId,
|
|
2588
|
+
replaceParents: input.replaceParents,
|
|
2617
2589
|
turns: input.completedTurns.map((turn, index) => {
|
|
2618
2590
|
const startedAt = turn.startedAt ?? input.fallbackStart + index;
|
|
2619
2591
|
const completedAt = turn.completedAt ?? startedAt;
|
|
@@ -2644,80 +2616,198 @@ export class ProxySessionController {
|
|
|
2644
2616
|
return 0;
|
|
2645
2617
|
}
|
|
2646
2618
|
}
|
|
2647
|
-
async
|
|
2648
|
-
const
|
|
2649
|
-
if (
|
|
2650
|
-
|
|
2651
|
-
|
|
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
|
+
}
|
|
2652
2691
|
if (!source.sourceRemoteSessionId)
|
|
2653
|
-
return;
|
|
2692
|
+
return false;
|
|
2654
2693
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
2655
|
-
if (runtime.kind !== "codex" || !runtime.
|
|
2656
|
-
return;
|
|
2694
|
+
if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries)
|
|
2695
|
+
return false;
|
|
2696
|
+
const seenCursors = new Set();
|
|
2657
2697
|
try {
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
:
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
??
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
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) {
|
|
2678
2744
|
this.store.audit(contextKey, "fork_turn_history_imported", {
|
|
2679
2745
|
forkedLocalSessionId: current.localSessionId,
|
|
2680
2746
|
sourceRemoteSessionId: source.sourceRemoteSessionId,
|
|
2681
2747
|
sourceTurnId: source.sourceTurnId,
|
|
2682
|
-
importedTurnCount: imported,
|
|
2683
2748
|
});
|
|
2684
2749
|
}
|
|
2750
|
+
return !state.complete;
|
|
2685
2751
|
}
|
|
2686
2752
|
catch (error) {
|
|
2687
|
-
|
|
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.");
|
|
2688
2756
|
}
|
|
2757
|
+
return true;
|
|
2689
2758
|
}
|
|
2690
|
-
async hydrateRemoteTurnHistory(contextKey, current) {
|
|
2759
|
+
async hydrateRemoteTurnHistory(contextKey, current, requiredCount) {
|
|
2691
2760
|
if (!current.remoteSessionId)
|
|
2692
|
-
return;
|
|
2761
|
+
return false;
|
|
2762
|
+
const key = JSON.stringify([current.agentName, current.remoteSessionId, "turn-card"]);
|
|
2693
2763
|
const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
&& this.store.hasImportedForkTurnHistory(current.localSessionId))
|
|
2699
|
-
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 };
|
|
2700
2768
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
2701
|
-
if (runtime.kind !== "codex" || !runtime.
|
|
2702
|
-
return;
|
|
2769
|
+
if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries)
|
|
2770
|
+
return false;
|
|
2771
|
+
const seen = new Set();
|
|
2703
2772
|
try {
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
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;
|
|
2717
2804
|
}
|
|
2718
2805
|
catch (error) {
|
|
2806
|
+
if (error instanceof CodexVersionError)
|
|
2807
|
+
throw error;
|
|
2719
2808
|
this.logger.warn({ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId }, "Failed to hydrate App Server Turn history; rendering the locally available history.");
|
|
2720
2809
|
}
|
|
2810
|
+
return true;
|
|
2721
2811
|
}
|
|
2722
2812
|
async createProjectSessionFromReference(contextKey, reference) {
|
|
2723
2813
|
const resolved = await this.resolveProjectSessionReference(contextKey, reference);
|
|
@@ -2749,7 +2839,7 @@ export class ProxySessionController {
|
|
|
2749
2839
|
runtime = this.runtimes.forAgent(agentName);
|
|
2750
2840
|
}
|
|
2751
2841
|
else {
|
|
2752
|
-
const resolved = await this.resolveRemoteCodexSession(taskId);
|
|
2842
|
+
const resolved = await this.resolveRemoteCodexSession(taskId, { view: "metadata" });
|
|
2753
2843
|
agentName = resolved.agentName;
|
|
2754
2844
|
runtime = resolved.runtime;
|
|
2755
2845
|
remote = resolved.remote;
|
|
@@ -2757,10 +2847,10 @@ export class ProxySessionController {
|
|
|
2757
2847
|
const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
|
|
2758
2848
|
if (!remote && runtime.readRemoteSession && remoteSessionId) {
|
|
2759
2849
|
try {
|
|
2760
|
-
remote = await runtime.readRemoteSession(remoteSessionId);
|
|
2850
|
+
remote = await runtime.readRemoteSession(remoteSessionId, "metadata");
|
|
2761
2851
|
}
|
|
2762
2852
|
catch (error) {
|
|
2763
|
-
if (!source)
|
|
2853
|
+
if (!source || error instanceof CodexVersionError)
|
|
2764
2854
|
throw error;
|
|
2765
2855
|
this.logger.warn({ error, contextKey, taskId: remoteSessionId }, "Failed to refresh the source task before creating a project task; using the local project path.");
|
|
2766
2856
|
}
|
|
@@ -2991,6 +3081,7 @@ export class ProxySessionController {
|
|
|
2991
3081
|
if (!userId?.startsWith("ou_")) {
|
|
2992
3082
|
throw new Error("/forkgroup 只能由具有 open_id 的飞书用户消息触发。");
|
|
2993
3083
|
}
|
|
3084
|
+
await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
|
|
2994
3085
|
const prepared = await this.prepareForkGroupSession(sourceContextKey, requestedTitle, incomingMessage);
|
|
2995
3086
|
await this.forkPreparedSessionToFeishuGroup(sourceContextKey, prepared, userId, incomingMessage?.threadContext ? prepared.sourceDescription : "当前任务");
|
|
2996
3087
|
}
|
|
@@ -2998,6 +3089,7 @@ export class ProxySessionController {
|
|
|
2998
3089
|
if (!userId?.startsWith("ou_")) {
|
|
2999
3090
|
throw new Error("ForkGroup 只能由具有 open_id 的飞书用户触发。");
|
|
3000
3091
|
}
|
|
3092
|
+
await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
|
|
3001
3093
|
const plan = await this.prepareForkSession(sourceContextKey, reference);
|
|
3002
3094
|
await this.forkPreparedSessionToFeishuGroup(sourceContextKey, {
|
|
3003
3095
|
plan,
|
|
@@ -3106,7 +3198,8 @@ export class ProxySessionController {
|
|
|
3106
3198
|
catch (error) {
|
|
3107
3199
|
if (activeWriterThreadId(error))
|
|
3108
3200
|
throw error;
|
|
3109
|
-
if (!(agent.kind === "app-server" && !record.lastTurnId
|
|
3201
|
+
if (!(agent.kind === "app-server" && !record.lastTurnId
|
|
3202
|
+
&& !this.store.getForkHistorySource(record.localSessionId) && isMissingRolloutError(error)))
|
|
3110
3203
|
throw error;
|
|
3111
3204
|
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
3112
3205
|
session = await runtime.createSession({
|
|
@@ -3521,6 +3614,7 @@ export class ProxySessionController {
|
|
|
3521
3614
|
}
|
|
3522
3615
|
async cancel(contextKey) {
|
|
3523
3616
|
const record = this.requireCurrentSession(contextKey);
|
|
3617
|
+
await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
|
|
3524
3618
|
await this.cancelSession(record);
|
|
3525
3619
|
}
|
|
3526
3620
|
async cancelSession(record) {
|
|
@@ -3535,6 +3629,8 @@ export class ProxySessionController {
|
|
|
3535
3629
|
await runtime.setGoal(record.localSessionId, { status: "paused" });
|
|
3536
3630
|
}
|
|
3537
3631
|
catch (error) {
|
|
3632
|
+
if (error instanceof CodexVersionError)
|
|
3633
|
+
throw error;
|
|
3538
3634
|
this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to pause the active Agent goal before interrupting its turn.");
|
|
3539
3635
|
}
|
|
3540
3636
|
}
|
|
@@ -3547,6 +3643,8 @@ export class ProxySessionController {
|
|
|
3547
3643
|
: undefined;
|
|
3548
3644
|
}
|
|
3549
3645
|
catch (error) {
|
|
3646
|
+
if (error instanceof CodexVersionError)
|
|
3647
|
+
throw error;
|
|
3550
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.");
|
|
3551
3649
|
}
|
|
3552
3650
|
}
|
|
@@ -3590,8 +3688,14 @@ export class ProxySessionController {
|
|
|
3590
3688
|
async resetCurrentSessionToTurn(contextKey, sessionId, turnId) {
|
|
3591
3689
|
if (!sessionId || !turnId)
|
|
3592
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) {
|
|
3593
3697
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
3594
|
-
const reset = previous.catch(() => undefined).then(
|
|
3698
|
+
const reset = previous.catch(() => undefined).then(operation);
|
|
3595
3699
|
this.sessionResets.set(sessionId, reset);
|
|
3596
3700
|
try {
|
|
3597
3701
|
await reset;
|
|
@@ -3601,6 +3705,19 @@ export class ProxySessionController {
|
|
|
3601
3705
|
this.sessionResets.delete(sessionId);
|
|
3602
3706
|
}
|
|
3603
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
|
+
}
|
|
3604
3721
|
async resetFromHistoryCard(contextKey, sessionId, turnId, updateMessageId, page) {
|
|
3605
3722
|
if (!sessionId || !turnId)
|
|
3606
3723
|
throw new Error("无效的 Reset 请求。请使用最新的历史轮次卡片重试。");
|
|
@@ -3611,16 +3728,11 @@ export class ProxySessionController {
|
|
|
3611
3728
|
}
|
|
3612
3729
|
this.resetHistoryOperations.set(sessionId, turnId);
|
|
3613
3730
|
try {
|
|
3614
|
-
await this.
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
}
|
|
3620
|
-
throw error;
|
|
3621
|
-
}
|
|
3622
|
-
try {
|
|
3623
|
-
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
|
+
});
|
|
3624
3736
|
}
|
|
3625
3737
|
catch (error) {
|
|
3626
3738
|
if (this.resetHistoryOperations.get(sessionId) === turnId) {
|
|
@@ -3650,9 +3762,9 @@ export class ProxySessionController {
|
|
|
3650
3762
|
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
3651
3763
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
3652
3764
|
}
|
|
3653
|
-
const historyTurn = this.store.
|
|
3765
|
+
const historyTurn = this.store.listTaskTurnGraphIndex(current.localSessionId)
|
|
3654
3766
|
.find((turn) => turn.turnId === turnId);
|
|
3655
|
-
const snapshot =
|
|
3767
|
+
const snapshot = historyTurn ? this.store.getTurnPromptSummary(turnId) : undefined;
|
|
3656
3768
|
if (!snapshot || snapshot.status !== "completed") {
|
|
3657
3769
|
throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
|
|
3658
3770
|
}
|
|
@@ -3808,6 +3920,7 @@ export class ProxySessionController {
|
|
|
3808
3920
|
const record = options.sessionId
|
|
3809
3921
|
? this.requireSession(contextKey, options.sessionId)
|
|
3810
3922
|
: this.requireCurrentSession(contextKey);
|
|
3923
|
+
await this.outbound.sendText(contextKey, `正在切换到 Provider ${modelProvider},请稍后。`);
|
|
3811
3924
|
const loaded = await this.loadSession(record);
|
|
3812
3925
|
await this.assertModelProvider(loaded, modelProvider);
|
|
3813
3926
|
const models = await loaded.runtime.listModels();
|
|
@@ -3825,7 +3938,7 @@ export class ProxySessionController {
|
|
|
3825
3938
|
model: model.id,
|
|
3826
3939
|
effort,
|
|
3827
3940
|
mode: loaded.session.permissionMode,
|
|
3828
|
-
}, options);
|
|
3941
|
+
}, options, false);
|
|
3829
3942
|
}
|
|
3830
3943
|
async openProviderSelector(contextKey, options = {}) {
|
|
3831
3944
|
await this.openExecutionSettings(contextKey, "provider", options);
|
|
@@ -3880,28 +3993,21 @@ export class ProxySessionController {
|
|
|
3880
3993
|
currentMode: permissionMode,
|
|
3881
3994
|
}));
|
|
3882
3995
|
}
|
|
3883
|
-
async applyProviderSettings(contextKey, settings, options = {}) {
|
|
3996
|
+
async applyProviderSettings(contextKey, settings, options = {}, announceStart = true) {
|
|
3884
3997
|
const record = options.sessionId
|
|
3885
3998
|
? this.requireSession(contextKey, options.sessionId)
|
|
3886
3999
|
: this.requireCurrentSession(contextKey);
|
|
4000
|
+
if (announceStart) {
|
|
4001
|
+
await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
4002
|
+
}
|
|
3887
4003
|
const loaded = await this.loadSession(record);
|
|
3888
4004
|
await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
3889
|
-
|
|
3890
|
-
throw new Error("当前运行时不支持 Provider 设置。");
|
|
3891
|
-
}
|
|
3892
|
-
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
|
|
4005
|
+
const session = await this.changeProviderSettings(loaded, {
|
|
3893
4006
|
modelProvider: settings.provider,
|
|
3894
4007
|
model: settings.model,
|
|
3895
4008
|
reasoningEffort: settings.effort,
|
|
3896
4009
|
permissionMode: settings.mode,
|
|
3897
4010
|
});
|
|
3898
|
-
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
3899
|
-
modelProvider: session.modelProvider ?? settings.provider,
|
|
3900
|
-
model: session.model ?? settings.model,
|
|
3901
|
-
reasoningEffort: session.reasoningEffort ?? settings.effort,
|
|
3902
|
-
permissionMode: session.permissionMode,
|
|
3903
|
-
});
|
|
3904
|
-
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
3905
4011
|
const notice = [
|
|
3906
4012
|
`Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
|
|
3907
4013
|
`模型 ${cardCode(session.model ?? settings.model)}`,
|
|
@@ -3920,6 +4026,31 @@ export class ProxySessionController {
|
|
|
3920
4026
|
await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
|
|
3921
4027
|
}
|
|
3922
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
|
+
}
|
|
3923
4054
|
async assertProviderModelSettings(loaded, modelProvider, model, effort) {
|
|
3924
4055
|
await this.assertModelProvider(loaded, modelProvider);
|
|
3925
4056
|
const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
|
|
@@ -4282,7 +4413,9 @@ export class ProxySessionController {
|
|
|
4282
4413
|
const normalizedTitle = normalizeTaskTitle(title);
|
|
4283
4414
|
if (!normalizedTitle)
|
|
4284
4415
|
throw new Error("任务标题不能为空。");
|
|
4285
|
-
const
|
|
4416
|
+
const current = this.requireCurrentSession(contextKey);
|
|
4417
|
+
await this.outbound.sendText(contextKey, CHANGE_TITLE_STARTED_MESSAGE);
|
|
4418
|
+
const loaded = await this.loadSession(current);
|
|
4286
4419
|
if (loaded.runtime.setTitle)
|
|
4287
4420
|
await loaded.runtime.setTitle(loaded.record.localSessionId, normalizedTitle);
|
|
4288
4421
|
else
|
|
@@ -4402,9 +4535,9 @@ export class ProxySessionController {
|
|
|
4402
4535
|
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
4403
4536
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
4404
4537
|
}
|
|
4405
|
-
|
|
4406
|
-
await this.
|
|
4407
|
-
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);
|
|
4408
4541
|
const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
|
|
4409
4542
|
const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
|
|
4410
4543
|
&& !completedTurnIds.has(current.lastTurnId)
|
|
@@ -4446,13 +4579,13 @@ export class ProxySessionController {
|
|
|
4446
4579
|
parentTurnId: turn.parentTurnId,
|
|
4447
4580
|
})));
|
|
4448
4581
|
const total = allTurns.length;
|
|
4449
|
-
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);
|
|
4450
4583
|
const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
|
|
4451
4584
|
const offset = page * RESET_HISTORY_PAGE_SIZE;
|
|
4452
4585
|
const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
|
|
4453
4586
|
const resettingTurnId = this.resetHistoryOperations.get(current.localSessionId);
|
|
4454
4587
|
const entries = turns.flatMap((turn, index) => {
|
|
4455
|
-
const snapshot =
|
|
4588
|
+
const snapshot = turn.turnId === runningTurnId ? runningSnapshot : this.store.getTurnPromptSummary(turn.turnId);
|
|
4456
4589
|
if (!snapshot)
|
|
4457
4590
|
return [];
|
|
4458
4591
|
const graph = graphRows[offset + index];
|
|
@@ -4505,9 +4638,11 @@ export class ProxySessionController {
|
|
|
4505
4638
|
const card = this.cardRenderer.renderResetHistoryCard({
|
|
4506
4639
|
entries,
|
|
4507
4640
|
footerLines: [
|
|
4508
|
-
|
|
4509
|
-
? `第 ${page + 1}
|
|
4510
|
-
:
|
|
4641
|
+
hasMoreHistory
|
|
4642
|
+
? `第 ${page + 1} 页 · 已加载 ${total} 个 turn`
|
|
4643
|
+
: runningTurn
|
|
4644
|
+
? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
|
|
4645
|
+
: `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
|
|
4511
4646
|
...(resettingTurnId ? ["正在 Reset 到所选轮次,请稍候…"] : []),
|
|
4512
4647
|
],
|
|
4513
4648
|
pageActions,
|
|
@@ -4706,6 +4841,7 @@ export class ProxySessionController {
|
|
|
4706
4841
|
const record = this.requireCurrentSession(contextKey);
|
|
4707
4842
|
const filePath = resolveUserPath(requestedFilePath, record.cwd);
|
|
4708
4843
|
await this.assertSendableFile(filePath);
|
|
4844
|
+
await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
|
|
4709
4845
|
await this.outbound.sendFile(contextKey, filePath);
|
|
4710
4846
|
}
|
|
4711
4847
|
async listSessions(contextKey, searchTerm, options = {}) {
|
|
@@ -4996,7 +5132,7 @@ export class ProxySessionController {
|
|
|
4996
5132
|
runtime,
|
|
4997
5133
|
remote: options.forFork && runtime.readRemoteForkSource
|
|
4998
5134
|
? await runtime.readRemoteForkSource(remoteSessionId)
|
|
4999
|
-
: await runtime.readRemoteSession(remoteSessionId),
|
|
5135
|
+
: await runtime.readRemoteSession(remoteSessionId, options.view ?? "latest"),
|
|
5000
5136
|
};
|
|
5001
5137
|
}));
|
|
5002
5138
|
const matches = reads
|
|
@@ -5022,6 +5158,7 @@ export class ProxySessionController {
|
|
|
5022
5158
|
if (direct?.status === "closed") {
|
|
5023
5159
|
throw new Error(`找不到任务:${taskId}`);
|
|
5024
5160
|
}
|
|
5161
|
+
await this.outbound.sendText(contextKey, SWITCH_TASK_STARTED_MESSAGE);
|
|
5025
5162
|
const existing = direct ?? this.findStoredSessionByReference(taskId);
|
|
5026
5163
|
if (existing) {
|
|
5027
5164
|
const runtime = this.runtimes.forAgent(existing.agentName);
|
|
@@ -5106,6 +5243,9 @@ export class ProxySessionController {
|
|
|
5106
5243
|
if (local && this.store.countQueuedPrompts(local.localSessionId) > 0) {
|
|
5107
5244
|
throw new Error("任务仍有排队消息,无法归档。请先处理或取消排队消息。");
|
|
5108
5245
|
}
|
|
5246
|
+
if (options.source !== "dismiss") {
|
|
5247
|
+
await this.outbound.sendText(contextKey, ARCHIVE_TASK_STARTED_MESSAGE);
|
|
5248
|
+
}
|
|
5109
5249
|
const remoteReference = local
|
|
5110
5250
|
? remoteSessionReference(local.agentName, local.remoteSessionId)
|
|
5111
5251
|
: taskId;
|
|
@@ -5313,28 +5453,32 @@ export class ProxySessionController {
|
|
|
5313
5453
|
else {
|
|
5314
5454
|
current = context.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
|
|
5315
5455
|
}
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
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;
|
|
5321
5462
|
current = this.store.getSession(current.localSessionId) ?? current;
|
|
5322
5463
|
}
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
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
|
+
}
|
|
5326
5469
|
}
|
|
5327
5470
|
}
|
|
5328
5471
|
const localCurrent = current;
|
|
5329
|
-
let remote;
|
|
5330
5472
|
let goal;
|
|
5331
5473
|
if (current?.remoteSessionId) {
|
|
5332
5474
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
5333
|
-
if (runtime.readRemoteSession) {
|
|
5475
|
+
if (!remote && runtime.readRemoteSession) {
|
|
5334
5476
|
try {
|
|
5335
|
-
remote = await runtime.readRemoteSession(current.remoteSessionId);
|
|
5477
|
+
remote = await runtime.readRemoteSession(current.remoteSessionId, "latest-full");
|
|
5336
5478
|
}
|
|
5337
5479
|
catch (error) {
|
|
5480
|
+
if (error instanceof CodexVersionError)
|
|
5481
|
+
throw error;
|
|
5338
5482
|
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task status.");
|
|
5339
5483
|
}
|
|
5340
5484
|
}
|
|
@@ -5430,7 +5574,7 @@ export class ProxySessionController {
|
|
|
5430
5574
|
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
5431
5575
|
}
|
|
5432
5576
|
async statusForCodexTask(contextKey, reference, options = {}) {
|
|
5433
|
-
const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
|
|
5577
|
+
const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
|
|
5434
5578
|
const actionReference = remoteSessionReference(agentName, remote.id);
|
|
5435
5579
|
const sections = [
|
|
5436
5580
|
{
|
|
@@ -5561,10 +5705,11 @@ export class ProxySessionController {
|
|
|
5561
5705
|
throw new Error(`未知 agent:${agentName}`);
|
|
5562
5706
|
return agent;
|
|
5563
5707
|
}
|
|
5564
|
-
async persistAgentExecutionDefaults(localSessionId) {
|
|
5565
|
-
const
|
|
5566
|
-
if (!
|
|
5708
|
+
async persistAgentExecutionDefaults(localSessionId, candidate) {
|
|
5709
|
+
const record = this.store.getSession(localSessionId);
|
|
5710
|
+
if (!record)
|
|
5567
5711
|
throw new Error(`找不到任务:${localSessionId}`);
|
|
5712
|
+
const session = candidate ?? record;
|
|
5568
5713
|
const agent = this.ensureAgent(session.agentName);
|
|
5569
5714
|
const observed = {
|
|
5570
5715
|
...(session.modelProvider?.trim() ? { modelProvider: session.modelProvider } : {}),
|
|
@@ -5934,6 +6079,7 @@ export class ProxySessionController {
|
|
|
5934
6079
|
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
5935
6080
|
if (!lockPath)
|
|
5936
6081
|
throw new Error("当前 Agent 不支持自动关闭占用程序。");
|
|
6082
|
+
await this.outbound.sendText(contextKey, CLOSE_WRITER_STARTED_MESSAGE);
|
|
5937
6083
|
let owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
5938
6084
|
const expectedOwner = owners.length === 1 && threadWriterMatches(owners[0], expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt) ? owners[0] : undefined;
|
|
5939
6085
|
if (!expectedOwner) {
|
|
@@ -6259,10 +6405,6 @@ function bindSessionCardAction(action, bindings) {
|
|
|
6259
6405
|
function runtimeErrorMessage(error) {
|
|
6260
6406
|
return error instanceof Error ? error.message : String(error);
|
|
6261
6407
|
}
|
|
6262
|
-
function isMissingRolloutError(error) {
|
|
6263
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6264
|
-
return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
|
|
6265
|
-
}
|
|
6266
6408
|
function isUnmaterializedCodexThreadError(error) {
|
|
6267
6409
|
const message = error instanceof Error ? error.message : String(error);
|
|
6268
6410
|
return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
|