@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
|
@@ -4,6 +4,8 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import type { Logger } from "pino";
|
|
6
6
|
import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
7
|
+
import { CodexVersionError } from "../codex/CodexVersion.js";
|
|
8
|
+
import { assertProviderSettingsApplied, isMissingRolloutError } from "../codex/ProviderSwitch.js";
|
|
7
9
|
import {
|
|
8
10
|
SystemThreadWriterProcessController,
|
|
9
11
|
type ThreadWriterProcess,
|
|
@@ -64,6 +66,7 @@ import type {
|
|
|
64
66
|
RemoteSessionSummary,
|
|
65
67
|
RuntimeGoal,
|
|
66
68
|
RuntimeEvent,
|
|
69
|
+
RuntimeExecutionSettings,
|
|
67
70
|
RuntimePrompt,
|
|
68
71
|
RuntimeSession,
|
|
69
72
|
} from "../runtime/types.js";
|
|
@@ -187,6 +190,22 @@ const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
|
187
190
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
188
191
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
189
192
|
const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
|
|
193
|
+
const TURN_RESET_STARTED_MESSAGE = "Reset 正在进行,请稍后。期间发送的消息会自动排队。";
|
|
194
|
+
const CREATE_TASK_STARTED_MESSAGE = "正在创建任务,请稍后。";
|
|
195
|
+
const CREATE_GROUP_STARTED_MESSAGE = "正在创建群聊和任务,请稍后。";
|
|
196
|
+
const FORK_TASK_STARTED_MESSAGE = "正在创建分支任务,请稍后。";
|
|
197
|
+
const FORK_GROUP_STARTED_MESSAGE = "正在创建分支群聊,请稍后。";
|
|
198
|
+
const LIST_SESSIONS_STARTED_MESSAGE = "正在读取任务列表,请稍后。";
|
|
199
|
+
const LIST_TURNS_STARTED_MESSAGE = "正在读取历史轮次,请稍后。";
|
|
200
|
+
const SWITCH_TASK_STARTED_MESSAGE = "正在切换任务,请稍后。";
|
|
201
|
+
const STATUS_STARTED_MESSAGE = "正在读取任务状态,请稍后。";
|
|
202
|
+
const GOAL_STARTED_MESSAGE = "正在处理 Goal,请稍后。";
|
|
203
|
+
const ARCHIVE_TASK_STARTED_MESSAGE = "正在归档任务,请稍后。";
|
|
204
|
+
const DISMISS_GROUP_STARTED_MESSAGE = "正在归档任务并解散群聊,请稍后。";
|
|
205
|
+
const SEND_FILE_STARTED_MESSAGE = "正在发送文件,请稍后。";
|
|
206
|
+
const CHANGE_TITLE_STARTED_MESSAGE = "正在修改任务标题,请稍后。";
|
|
207
|
+
const STOP_TASK_STARTED_MESSAGE = "正在停止任务,请稍后。";
|
|
208
|
+
const CLOSE_WRITER_STARTED_MESSAGE = "正在检查并关闭占用进程,请稍后。";
|
|
190
209
|
const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
|
|
191
210
|
const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
|
|
192
211
|
const IMAGE_FILE_EXTENSIONS = new Set([
|
|
@@ -409,7 +428,6 @@ interface ForkSessionPlan {
|
|
|
409
428
|
sourceTurnPrompt?: string;
|
|
410
429
|
sourceTurnStartedAt?: number;
|
|
411
430
|
sourceTurnCompletedAt?: number;
|
|
412
|
-
sourceCompletedTurns?: RemoteCompletedTurnSummary[];
|
|
413
431
|
sourceWasRunning: boolean;
|
|
414
432
|
forkedFromHistoricalTurn: boolean;
|
|
415
433
|
}
|
|
@@ -419,6 +437,14 @@ interface ForkSessionResult {
|
|
|
419
437
|
session: RuntimeSession;
|
|
420
438
|
}
|
|
421
439
|
|
|
440
|
+
interface ForkHistoryCursor {
|
|
441
|
+
cursor?: string;
|
|
442
|
+
anchorFound: boolean;
|
|
443
|
+
tailTurnId?: string;
|
|
444
|
+
loadedCount?: number;
|
|
445
|
+
complete: boolean;
|
|
446
|
+
}
|
|
447
|
+
|
|
422
448
|
interface ForkGroupSessionPlan {
|
|
423
449
|
plan: ForkSessionPlan;
|
|
424
450
|
sourceDescription: string;
|
|
@@ -489,6 +515,8 @@ export class ProxySessionController {
|
|
|
489
515
|
private readonly sessionResets = new Map<string, Promise<void>>();
|
|
490
516
|
private readonly resetHistoryOperations = new Map<string, string>();
|
|
491
517
|
private readonly remoteTurnHistoryRefreshes = new Map<string, number>();
|
|
518
|
+
private readonly turnHistoryHydrations = new Map<string, Promise<boolean>>();
|
|
519
|
+
private readonly forkHistoryCursors = new Map<string, ForkHistoryCursor>();
|
|
492
520
|
private readonly lastSessionListings = new Map<string, string[]>();
|
|
493
521
|
private readonly threadInitializations = new Map<string, Promise<void>>();
|
|
494
522
|
private readonly llmRetryingSessions = new Set<string>();
|
|
@@ -589,6 +617,7 @@ export class ProxySessionController {
|
|
|
589
617
|
"Failed to acknowledge the incoming Feishu message with a reaction.",
|
|
590
618
|
);
|
|
591
619
|
}
|
|
620
|
+
await this.waitForCurrentSessionReset(message.contextKey);
|
|
592
621
|
if (attachmentReservation && this.completeForwardAttachment(message, attachmentReservation)) {
|
|
593
622
|
this.store.audit(message.contextKey, `${attachmentReservation.sourceKind}_attachment_coalesced`, {
|
|
594
623
|
messageId: message.messageId,
|
|
@@ -826,10 +855,11 @@ export class ProxySessionController {
|
|
|
826
855
|
"Processing Agent Bot command.",
|
|
827
856
|
);
|
|
828
857
|
}
|
|
829
|
-
// Operational and read-only commands
|
|
858
|
+
// Operational and read-only commands bypass the prompt queue, but still wait for a Reset binding change.
|
|
830
859
|
if (isQueueIndependentCommand(command)) {
|
|
831
860
|
await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
832
861
|
try {
|
|
862
|
+
await this.waitForCurrentSessionReset(message.contextKey);
|
|
833
863
|
if (command.type === "prompt") await this.ensureThreadFork(message);
|
|
834
864
|
await this.execute(
|
|
835
865
|
message.contextKey,
|
|
@@ -857,8 +887,9 @@ export class ProxySessionController {
|
|
|
857
887
|
}
|
|
858
888
|
|
|
859
889
|
const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
|
|
860
|
-
const next = previous.catch(() => undefined).then(() =>
|
|
861
|
-
this.
|
|
890
|
+
const next = previous.catch(() => undefined).then(async () => {
|
|
891
|
+
await this.waitForCurrentSessionReset(message.contextKey);
|
|
892
|
+
return this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
862
893
|
try {
|
|
863
894
|
let executableCommand = command;
|
|
864
895
|
let executableImagePaths = localImagePaths;
|
|
@@ -903,8 +934,8 @@ export class ProxySessionController {
|
|
|
903
934
|
phase: "command_execute",
|
|
904
935
|
});
|
|
905
936
|
}
|
|
906
|
-
})
|
|
907
|
-
);
|
|
937
|
+
});
|
|
938
|
+
});
|
|
908
939
|
this.messageQueues.set(message.contextKey, next);
|
|
909
940
|
await next;
|
|
910
941
|
if (this.messageQueues.get(message.contextKey) === next) this.messageQueues.delete(message.contextKey);
|
|
@@ -1022,6 +1053,7 @@ export class ProxySessionController {
|
|
|
1022
1053
|
} else if (kind === "directory_send_file") {
|
|
1023
1054
|
const filePath = directoryFileActionPath(scopedAction.value.filePath);
|
|
1024
1055
|
await this.assertSendableFile(filePath);
|
|
1056
|
+
await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
|
|
1025
1057
|
await this.outbound.sendFile(contextKey, filePath);
|
|
1026
1058
|
} else if (kind === "directory_new_folder_prompt") {
|
|
1027
1059
|
const directory = directoryActionPath(scopedAction.value.directory);
|
|
@@ -1077,10 +1109,12 @@ export class ProxySessionController {
|
|
|
1077
1109
|
await this.refreshSessionsCardFromAction(scopedAction);
|
|
1078
1110
|
} else if (kind === "session_new") {
|
|
1079
1111
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
1112
|
+
await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
|
|
1080
1113
|
await this.createProjectSessionFromReference(contextKey, sessionId);
|
|
1081
1114
|
await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
|
|
1082
1115
|
} else if (kind === "session_new_group") {
|
|
1083
1116
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
1117
|
+
await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
|
|
1084
1118
|
await this.createFeishuGroupFromReference(contextKey, sessionId, scopedAction.userId);
|
|
1085
1119
|
await this.refreshSessionsCardFromAction(scopedAction);
|
|
1086
1120
|
} else if (kind === "session_archive") {
|
|
@@ -1099,6 +1133,7 @@ export class ProxySessionController {
|
|
|
1099
1133
|
);
|
|
1100
1134
|
} else if (kind === "group_dismiss_confirm") {
|
|
1101
1135
|
this.assertDismissGroupRequester(scopedAction);
|
|
1136
|
+
await this.outbound.sendText(contextKey, DISMISS_GROUP_STARTED_MESSAGE);
|
|
1102
1137
|
await this.dismissGroupAndArchiveTask(
|
|
1103
1138
|
contextKey,
|
|
1104
1139
|
String(scopedAction.value.sessionId ?? ""),
|
|
@@ -1106,10 +1141,12 @@ export class ProxySessionController {
|
|
|
1106
1141
|
);
|
|
1107
1142
|
} else if (kind === "session_stop") {
|
|
1108
1143
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
1144
|
+
await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
|
|
1109
1145
|
await this.stopSessionReference(contextKey, sessionId);
|
|
1110
1146
|
if (scopedAction.value.cardView === "status") await this.refreshStatusCardFromAction(scopedAction, sessionId);
|
|
1111
1147
|
else await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
|
|
1112
1148
|
} else if (kind === "session_status") {
|
|
1149
|
+
await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
|
|
1113
1150
|
await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
|
|
1114
1151
|
} else if (kind === "session_status_refresh") {
|
|
1115
1152
|
await this.refreshStatusCardFromAction(scopedAction);
|
|
@@ -1247,6 +1284,8 @@ export class ProxySessionController {
|
|
|
1247
1284
|
this.sessionResets.clear();
|
|
1248
1285
|
this.resetHistoryOperations.clear();
|
|
1249
1286
|
this.remoteTurnHistoryRefreshes.clear();
|
|
1287
|
+
this.forkHistoryCursors.clear();
|
|
1288
|
+
this.turnHistoryHydrations.clear();
|
|
1250
1289
|
for (const pending of this.pendingMergedForwards.values()) pending.resolveAttachment(undefined);
|
|
1251
1290
|
this.pendingMergedForwards.clear();
|
|
1252
1291
|
for (const pending of this.pendingResourceForwards.values()) pending.resolveAttachment(undefined);
|
|
@@ -1326,8 +1365,9 @@ export class ProxySessionController {
|
|
|
1326
1365
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
1327
1366
|
if (runtime.readRemoteSession) {
|
|
1328
1367
|
try {
|
|
1329
|
-
remote = await runtime.readRemoteSession(record.remoteSessionId);
|
|
1368
|
+
remote = await runtime.readRemoteSession(record.remoteSessionId, "latest-full");
|
|
1330
1369
|
} catch (error) {
|
|
1370
|
+
if (error instanceof CodexVersionError) throw error;
|
|
1331
1371
|
this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to inspect App Server task status for CLI.");
|
|
1332
1372
|
}
|
|
1333
1373
|
}
|
|
@@ -1627,9 +1667,17 @@ export class ProxySessionController {
|
|
|
1627
1667
|
permissionModes: PermissionMode[];
|
|
1628
1668
|
}> {
|
|
1629
1669
|
const record = this.requireControlSession(localSessionId);
|
|
1630
|
-
const loaded = await this.loadSession(record);
|
|
1631
1670
|
if (setting && !value?.trim()) throw new Error(`task ${setting} requires a value.`);
|
|
1632
1671
|
const nextValue = value?.trim();
|
|
1672
|
+
if (setting === "provider" && nextValue) {
|
|
1673
|
+
const contextKey = this.controlSessionContextKey(record);
|
|
1674
|
+
await this.outbound.withReplyTarget(
|
|
1675
|
+
contextKey,
|
|
1676
|
+
this.controlSessionReplyTarget(record),
|
|
1677
|
+
() => this.outbound.sendText(contextKey, `正在切换到 Provider ${nextValue},请稍后。`),
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
const loaded = await this.loadSession(record);
|
|
1633
1681
|
if (setting === "provider" && nextValue) {
|
|
1634
1682
|
await this.assertModelProvider(loaded, nextValue);
|
|
1635
1683
|
const models = await loaded.runtime.listModels();
|
|
@@ -1642,19 +1690,12 @@ export class ProxySessionController {
|
|
|
1642
1690
|
? loaded.session.reasoningEffort
|
|
1643
1691
|
: model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
|
|
1644
1692
|
if (!effort) throw new Error(`Model ${model.id} has no reasoning effort available for a Provider change.`);
|
|
1645
|
-
|
|
1646
|
-
const updated = await loaded.runtime.setExecutionSettings(record.localSessionId, {
|
|
1693
|
+
await this.changeProviderSettings(loaded, {
|
|
1647
1694
|
modelProvider: nextValue,
|
|
1648
1695
|
model: model.id,
|
|
1649
1696
|
reasoningEffort: effort,
|
|
1650
1697
|
permissionMode: loaded.session.permissionMode,
|
|
1651
1698
|
});
|
|
1652
|
-
this.store.updateRuntimeSession(record.localSessionId, {
|
|
1653
|
-
modelProvider: updated.modelProvider ?? nextValue,
|
|
1654
|
-
model: updated.model ?? model.id,
|
|
1655
|
-
reasoningEffort: updated.reasoningEffort ?? effort,
|
|
1656
|
-
permissionMode: updated.permissionMode,
|
|
1657
|
-
});
|
|
1658
1699
|
} else if (setting === "model" && nextValue) {
|
|
1659
1700
|
const models = await loaded.runtime.listModels();
|
|
1660
1701
|
const selected = models.find((candidate) => candidate.id === nextValue);
|
|
@@ -1689,12 +1730,12 @@ export class ProxySessionController {
|
|
|
1689
1730
|
this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
|
|
1690
1731
|
}
|
|
1691
1732
|
const session = this.store.getSession(record.localSessionId) ?? record;
|
|
1692
|
-
if (setting) await this.persistAgentExecutionDefaults(session.localSessionId);
|
|
1733
|
+
if (setting && setting !== "provider") await this.persistAgentExecutionDefaults(session.localSessionId);
|
|
1693
1734
|
const models = await loaded.runtime.listModels();
|
|
1694
1735
|
const currentModel = models.find((candidate) => candidate.id === session.model)
|
|
1695
1736
|
?? models.find((candidate) => candidate.isDefault);
|
|
1696
1737
|
const providers = loaded.runtime.listModelProviders
|
|
1697
|
-
? await
|
|
1738
|
+
? await this.modelProviderOptions(loaded)
|
|
1698
1739
|
: [];
|
|
1699
1740
|
return {
|
|
1700
1741
|
session,
|
|
@@ -1738,7 +1779,7 @@ export class ProxySessionController {
|
|
|
1738
1779
|
};
|
|
1739
1780
|
}
|
|
1740
1781
|
|
|
1741
|
-
controlListTaskTurns(localSessionId: string): {
|
|
1782
|
+
async controlListTaskTurns(localSessionId: string): Promise<{
|
|
1742
1783
|
session: SessionRecord;
|
|
1743
1784
|
turns: Array<{
|
|
1744
1785
|
sequence: number;
|
|
@@ -1749,17 +1790,18 @@ export class ProxySessionController {
|
|
|
1749
1790
|
completedAt?: number;
|
|
1750
1791
|
current: boolean;
|
|
1751
1792
|
}>;
|
|
1752
|
-
} {
|
|
1793
|
+
}> {
|
|
1753
1794
|
const session = this.requireControlSession(localSessionId);
|
|
1754
1795
|
if (!session.remoteSessionId || !this.isCodexSession(session)) {
|
|
1755
1796
|
throw new Error("The task is not an App Server task that can be reset.");
|
|
1756
1797
|
}
|
|
1757
|
-
|
|
1798
|
+
await this.hydrateTaskTurnHistory(session);
|
|
1799
|
+
const rows = this.store.listTaskTurnGraphIndex(session.localSessionId);
|
|
1758
1800
|
const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
|
|
1759
1801
|
return {
|
|
1760
1802
|
session,
|
|
1761
1803
|
turns: rows.flatMap((turn, index) => {
|
|
1762
|
-
const snapshot =
|
|
1804
|
+
const snapshot = this.store.getTurnPromptSummary(turn.turnId);
|
|
1763
1805
|
if (!snapshot) return [];
|
|
1764
1806
|
return [{
|
|
1765
1807
|
sequence: graph[index]!.sequence,
|
|
@@ -1777,15 +1819,10 @@ export class ProxySessionController {
|
|
|
1777
1819
|
async controlResetTaskToTurn(localSessionId: string, turnId: string): Promise<SessionRecord> {
|
|
1778
1820
|
const record = this.requireControlSession(localSessionId);
|
|
1779
1821
|
const contextKey = this.controlSessionContextKey(record);
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
this.performSessionReset(contextKey, record, turnId, false)
|
|
1783
|
-
|
|
1784
|
-
try {
|
|
1785
|
-
await reset;
|
|
1786
|
-
} finally {
|
|
1787
|
-
if (this.sessionResets.get(record.localSessionId) === reset) this.sessionResets.delete(record.localSessionId);
|
|
1788
|
-
}
|
|
1822
|
+
await this.runSessionReset(record.localSessionId, async () => {
|
|
1823
|
+
await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
|
|
1824
|
+
await this.performSessionReset(contextKey, record, turnId, false);
|
|
1825
|
+
});
|
|
1789
1826
|
return this.store.getSession(record.localSessionId) ?? record;
|
|
1790
1827
|
}
|
|
1791
1828
|
|
|
@@ -1942,6 +1979,7 @@ export class ProxySessionController {
|
|
|
1942
1979
|
if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
|
|
1943
1980
|
throw new Error("/new --nodir 仅支持 App Server Agent。");
|
|
1944
1981
|
}
|
|
1982
|
+
await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
|
|
1945
1983
|
await this.createSession(
|
|
1946
1984
|
contextKey,
|
|
1947
1985
|
context.defaultAgent,
|
|
@@ -1962,6 +2000,10 @@ export class ProxySessionController {
|
|
|
1962
2000
|
if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
|
|
1963
2001
|
throw new Error("/newgroup --nodir 仅支持 App Server Agent。");
|
|
1964
2002
|
}
|
|
2003
|
+
if (!userId?.startsWith("ou_")) {
|
|
2004
|
+
throw new Error("/newgroup 只能由具有 open_id 的飞书用户消息触发。");
|
|
2005
|
+
}
|
|
2006
|
+
await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
|
|
1965
2007
|
await this.createFeishuGroup(
|
|
1966
2008
|
contextKey,
|
|
1967
2009
|
context.defaultAgent,
|
|
@@ -1994,6 +2036,7 @@ export class ProxySessionController {
|
|
|
1994
2036
|
await this.enqueueNoSteerPrompt(contextKey, command.text, messageId, replyTarget);
|
|
1995
2037
|
return;
|
|
1996
2038
|
case "sessions":
|
|
2039
|
+
await this.outbound.sendText(contextKey, LIST_SESSIONS_STARTED_MESSAGE);
|
|
1997
2040
|
await this.listSessions(contextKey, command.searchTerm);
|
|
1998
2041
|
return;
|
|
1999
2042
|
case "switch":
|
|
@@ -2016,9 +2059,11 @@ export class ProxySessionController {
|
|
|
2016
2059
|
await this.openDismissGroupCard(contextKey, userId);
|
|
2017
2060
|
return;
|
|
2018
2061
|
case "status":
|
|
2062
|
+
await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
|
|
2019
2063
|
await this.status(contextKey, command.sessionId);
|
|
2020
2064
|
return;
|
|
2021
2065
|
case "goal":
|
|
2066
|
+
await this.outbound.sendText(contextKey, GOAL_STARTED_MESSAGE);
|
|
2022
2067
|
await this.goal(contextKey, command);
|
|
2023
2068
|
return;
|
|
2024
2069
|
case "restart":
|
|
@@ -2032,6 +2077,7 @@ export class ProxySessionController {
|
|
|
2032
2077
|
await this.setGroupMute(contextKey, command.enabled);
|
|
2033
2078
|
return;
|
|
2034
2079
|
case "turns":
|
|
2080
|
+
await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
|
|
2035
2081
|
await this.openResetHistory(contextKey);
|
|
2036
2082
|
return;
|
|
2037
2083
|
case "model":
|
|
@@ -2309,6 +2355,7 @@ export class ProxySessionController {
|
|
|
2309
2355
|
try {
|
|
2310
2356
|
remote = await runtime.readRemoteSession(session.remoteSessionId);
|
|
2311
2357
|
} catch (error) {
|
|
2358
|
+
if (error instanceof CodexVersionError) throw error;
|
|
2312
2359
|
this.logger.warn(
|
|
2313
2360
|
{ error, attemptId: attempt.attemptId, remoteSessionId: session.remoteSessionId },
|
|
2314
2361
|
"Failed to read the remote task during startup recovery; treating the old local execution as interrupted.",
|
|
@@ -2884,8 +2931,7 @@ export class ProxySessionController {
|
|
|
2884
2931
|
const anchor = this.store.findTurnAnchorByMessageId(messageId);
|
|
2885
2932
|
if (!anchor) return false;
|
|
2886
2933
|
if (record.lastTurnId === anchor.turnId) return true;
|
|
2887
|
-
return this.store.
|
|
2888
|
-
.some((turn) => turn.turnId === anchor.turnId);
|
|
2934
|
+
return this.store.hasTaskHistoryTurn(record.localSessionId, anchor.turnId);
|
|
2889
2935
|
}
|
|
2890
2936
|
|
|
2891
2937
|
private async forkThreadSession(message: IncomingMessage): Promise<void> {
|
|
@@ -2991,6 +3037,7 @@ export class ProxySessionController {
|
|
|
2991
3037
|
}
|
|
2992
3038
|
|
|
2993
3039
|
private async forkSessionReference(contextKey: string, reference?: string): Promise<void> {
|
|
3040
|
+
await this.outbound.sendText(contextKey, FORK_TASK_STARTED_MESSAGE);
|
|
2994
3041
|
const plan = await this.prepareForkSession(contextKey, reference);
|
|
2995
3042
|
const forked = await this.forkSessionIntoContext(contextKey, plan);
|
|
2996
3043
|
const forkSourceLabel = plan.forkedFromHistoricalTurn
|
|
@@ -3051,35 +3098,24 @@ export class ProxySessionController {
|
|
|
3051
3098
|
if (!runtime.readRemoteSession && !source) {
|
|
3052
3099
|
throw new Error("当前 App Server Agent 不支持读取指定任务。");
|
|
3053
3100
|
}
|
|
3054
|
-
const
|
|
3101
|
+
const persistedCompletedTurnId = source
|
|
3102
|
+
? this.store.findLatestCompletedTurnId(source.localSessionId)
|
|
3103
|
+
: undefined;
|
|
3055
3104
|
const sourceLastSnapshot = turnViewSnapshot(
|
|
3056
3105
|
source?.lastTurnId ? this.store.getTurnSnapshot(source.lastTurnId) : undefined,
|
|
3057
3106
|
);
|
|
3058
|
-
const
|
|
3059
|
-
|
|
3060
|
-
: source?.lastTurnId
|
|
3107
|
+
const localCompletedTurnId = persistedCompletedTurnId
|
|
3108
|
+
?? (source?.lastTurnId
|
|
3061
3109
|
&& (source.lastTurnStatus === "completed" || sourceLastSnapshot?.status === "completed")
|
|
3062
|
-
?
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
}]
|
|
3070
|
-
: [];
|
|
3071
|
-
if (!remote && runtime.readRemoteSession && (!source || persistedCompletedTurns.length === 0)) {
|
|
3072
|
-
try {
|
|
3073
|
-
remote = await runtime.readRemoteSession(remoteSessionId);
|
|
3074
|
-
} catch (error) {
|
|
3075
|
-
if (localCompletedTurns.length === 0) throw error;
|
|
3076
|
-
this.logger.warn(
|
|
3077
|
-
{ error, contextKey, remoteSessionId, sourceTurnId: localCompletedTurns.at(-1)?.id },
|
|
3078
|
-
"Failed to read the complete source task before Fork; using the locally persisted completed Turn.",
|
|
3079
|
-
);
|
|
3080
|
-
}
|
|
3110
|
+
? source.lastTurnId
|
|
3111
|
+
: undefined)
|
|
3112
|
+
?? (source ? this.store.getForkHistorySource(source.localSessionId)?.sourceTurnId : undefined);
|
|
3113
|
+
if (!remote && !localCompletedTurnId) {
|
|
3114
|
+
remote = runtime.readRemoteForkSource
|
|
3115
|
+
? await runtime.readRemoteForkSource(remoteSessionId)
|
|
3116
|
+
: await runtime.readRemoteSession?.(remoteSessionId);
|
|
3081
3117
|
}
|
|
3082
|
-
const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ??
|
|
3118
|
+
const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurnId;
|
|
3083
3119
|
const latestSnapshot = turnViewSnapshot(latestTurnId ? this.store.getTurnSnapshot(latestTurnId) : undefined);
|
|
3084
3120
|
const isRunning = remote
|
|
3085
3121
|
? isRemoteSessionActive(remote)
|
|
@@ -3090,7 +3126,7 @@ export class ProxySessionController {
|
|
|
3090
3126
|
);
|
|
3091
3127
|
const lastTurnId = remote?.lastCompletedTurnId
|
|
3092
3128
|
?? (remote?.lastTurnStatus === "completed" ? remote.lastTurnId : undefined)
|
|
3093
|
-
??
|
|
3129
|
+
?? localCompletedTurnId;
|
|
3094
3130
|
if (!lastTurnId) {
|
|
3095
3131
|
if (isRunning) {
|
|
3096
3132
|
throw new Error(`${sourceLabel}正在执行,且还没有已完成轮次可供 fork。请等待当前轮次完成后重试。`);
|
|
@@ -3099,8 +3135,7 @@ export class ProxySessionController {
|
|
|
3099
3135
|
}
|
|
3100
3136
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
|
|
3101
3137
|
const forkedFromHistoricalTurn = lastTurnId !== latestTurnId;
|
|
3102
|
-
const completedTurn =
|
|
3103
|
-
.find((turn) => turn.id === lastTurnId);
|
|
3138
|
+
const completedTurn = remote?.completedTurns?.find((turn) => turn.id === lastTurnId);
|
|
3104
3139
|
|
|
3105
3140
|
const cwd = remote?.cwd || source?.cwd;
|
|
3106
3141
|
if (!cwd) throw new Error("指定的 App Server 任务没有可用的工作目录,暂时不能 fork。");
|
|
@@ -3146,29 +3181,11 @@ export class ProxySessionController {
|
|
|
3146
3181
|
sourceTurnPrompt,
|
|
3147
3182
|
sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
|
|
3148
3183
|
sourceTurnCompletedAt,
|
|
3149
|
-
sourceCompletedTurns: remote?.completedTurns
|
|
3150
|
-
?? (persistedCompletedTurns.length > 0 ? [] : localCompletedTurns),
|
|
3151
3184
|
sourceWasRunning: isRunning,
|
|
3152
3185
|
forkedFromHistoricalTurn,
|
|
3153
3186
|
};
|
|
3154
3187
|
}
|
|
3155
3188
|
|
|
3156
|
-
private localForkCompletedTurns(source: SessionRecord): RemoteCompletedTurnSummary[] {
|
|
3157
|
-
return this.store.listTaskTurnGraph(source.localSessionId)
|
|
3158
|
-
.slice()
|
|
3159
|
-
.reverse()
|
|
3160
|
-
.map((record) => {
|
|
3161
|
-
const snapshot = turnViewSnapshot(record.snapshot);
|
|
3162
|
-
const startedAt = snapshot?.startedAt ?? parseIsoTimestamp(record.updatedAt);
|
|
3163
|
-
return {
|
|
3164
|
-
id: record.turnId,
|
|
3165
|
-
prompt: latestUserPromptFromTurnView(snapshot),
|
|
3166
|
-
startedAt,
|
|
3167
|
-
completedAt: snapshot?.completedAt ?? startedAt,
|
|
3168
|
-
};
|
|
3169
|
-
});
|
|
3170
|
-
}
|
|
3171
|
-
|
|
3172
3189
|
private async prepareForkGroupSession(
|
|
3173
3190
|
contextKey: string,
|
|
3174
3191
|
requestedTitle: string | undefined,
|
|
@@ -3271,7 +3288,6 @@ export class ProxySessionController {
|
|
|
3271
3288
|
contextKey: string,
|
|
3272
3289
|
plan: ForkSessionPlan,
|
|
3273
3290
|
): Promise<ForkSessionResult> {
|
|
3274
|
-
const sourceCompletedTurns = await this.resolveForkSourceCompletedTurns(plan);
|
|
3275
3291
|
const localSessionId = createId("sess");
|
|
3276
3292
|
const record = this.store.createSession({
|
|
3277
3293
|
localSessionId,
|
|
@@ -3314,12 +3330,6 @@ export class ProxySessionController {
|
|
|
3314
3330
|
plan.cwd,
|
|
3315
3331
|
this.agentLabel(plan.agentName),
|
|
3316
3332
|
);
|
|
3317
|
-
const sourceTurnHistoryCount = this.persistForkSourceHistory(
|
|
3318
|
-
contextKey,
|
|
3319
|
-
localSessionId,
|
|
3320
|
-
plan,
|
|
3321
|
-
sourceCompletedTurns,
|
|
3322
|
-
);
|
|
3323
3333
|
this.store.audit(contextKey, "session_forked", {
|
|
3324
3334
|
sourceLocalSessionId: plan.source?.localSessionId,
|
|
3325
3335
|
sourceRemoteSessionId: plan.remoteSessionId,
|
|
@@ -3327,7 +3337,6 @@ export class ProxySessionController {
|
|
|
3327
3337
|
sourceTurnPrompt: plan.sourceTurnPrompt,
|
|
3328
3338
|
sourceTurnStartedAt: plan.sourceTurnStartedAt,
|
|
3329
3339
|
sourceTurnCompletedAt: plan.sourceTurnCompletedAt,
|
|
3330
|
-
sourceTurnHistoryCount,
|
|
3331
3340
|
sourceWasRunning: plan.sourceWasRunning,
|
|
3332
3341
|
forkedLocalSessionId: localSessionId,
|
|
3333
3342
|
forkedRemoteSessionId: forked.remoteSessionId,
|
|
@@ -3343,54 +3352,6 @@ export class ProxySessionController {
|
|
|
3343
3352
|
}
|
|
3344
3353
|
}
|
|
3345
3354
|
|
|
3346
|
-
private async resolveForkSourceCompletedTurns(
|
|
3347
|
-
plan: ForkSessionPlan,
|
|
3348
|
-
): Promise<RemoteCompletedTurnSummary[]> {
|
|
3349
|
-
let completedTurns = plan.sourceCompletedTurns;
|
|
3350
|
-
if (completedTurns === undefined && plan.runtime.readRemoteSession) {
|
|
3351
|
-
try {
|
|
3352
|
-
completedTurns = (await plan.runtime.readRemoteSession(plan.remoteSessionId)).completedTurns ?? [];
|
|
3353
|
-
} catch (error) {
|
|
3354
|
-
this.logger.warn(
|
|
3355
|
-
{ error, remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId },
|
|
3356
|
-
"Failed to read the source task Turn history before Fork; keeping the Fork anchor fallback.",
|
|
3357
|
-
);
|
|
3358
|
-
}
|
|
3359
|
-
}
|
|
3360
|
-
if (!completedTurns?.length) return [];
|
|
3361
|
-
const anchorIndex = completedTurns.findIndex((turn) => turn.id === plan.lastTurnId);
|
|
3362
|
-
if (anchorIndex < 0) {
|
|
3363
|
-
this.logger.warn(
|
|
3364
|
-
{ remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId },
|
|
3365
|
-
"The source task Turn history did not include the Fork anchor; keeping the Fork anchor fallback.",
|
|
3366
|
-
);
|
|
3367
|
-
return [];
|
|
3368
|
-
}
|
|
3369
|
-
return completedTurns.slice(0, anchorIndex + 1);
|
|
3370
|
-
}
|
|
3371
|
-
|
|
3372
|
-
private persistForkSourceHistory(
|
|
3373
|
-
contextKey: string,
|
|
3374
|
-
forkedLocalSessionId: string,
|
|
3375
|
-
plan: ForkSessionPlan,
|
|
3376
|
-
completedTurns: RemoteCompletedTurnSummary[],
|
|
3377
|
-
): number {
|
|
3378
|
-
if (completedTurns.length === 0) return 0;
|
|
3379
|
-
const localSessionId = plan.source?.localSessionId ?? forkedLocalSessionId;
|
|
3380
|
-
const historyContextKey = plan.source?.contextKey ?? contextKey;
|
|
3381
|
-
const fallbackStart = plan.sourceTurnStartedAt
|
|
3382
|
-
?? plan.sourceTurnCompletedAt
|
|
3383
|
-
?? Date.now() - completedTurns.length;
|
|
3384
|
-
return this.persistCompletedTurnHistory({
|
|
3385
|
-
localSessionId,
|
|
3386
|
-
contextKey: historyContextKey,
|
|
3387
|
-
agentName: plan.agentName,
|
|
3388
|
-
remoteSessionId: plan.remoteSessionId,
|
|
3389
|
-
completedTurns,
|
|
3390
|
-
fallbackStart,
|
|
3391
|
-
});
|
|
3392
|
-
}
|
|
3393
|
-
|
|
3394
3355
|
private persistCompletedTurnHistory(input: {
|
|
3395
3356
|
localSessionId: string;
|
|
3396
3357
|
contextKey: string;
|
|
@@ -3398,6 +3359,7 @@ export class ProxySessionController {
|
|
|
3398
3359
|
remoteSessionId: string;
|
|
3399
3360
|
completedTurns: RemoteCompletedTurnSummary[];
|
|
3400
3361
|
fallbackStart: number;
|
|
3362
|
+
replaceParents?: boolean;
|
|
3401
3363
|
}): number {
|
|
3402
3364
|
try {
|
|
3403
3365
|
this.store.importCompletedTurnHistory({
|
|
@@ -3405,6 +3367,7 @@ export class ProxySessionController {
|
|
|
3405
3367
|
contextKey: input.contextKey,
|
|
3406
3368
|
agentName: input.agentName,
|
|
3407
3369
|
remoteSessionId: input.remoteSessionId,
|
|
3370
|
+
replaceParents: input.replaceParents,
|
|
3408
3371
|
turns: input.completedTurns.map((turn, index) => {
|
|
3409
3372
|
const startedAt = turn.startedAt ?? input.fallbackStart + index;
|
|
3410
3373
|
const completedAt = turn.completedAt ?? startedAt;
|
|
@@ -3438,82 +3401,202 @@ export class ProxySessionController {
|
|
|
3438
3401
|
}
|
|
3439
3402
|
}
|
|
3440
3403
|
|
|
3441
|
-
private async
|
|
3404
|
+
private async hydrateTaskTurnHistory(
|
|
3405
|
+
current: SessionRecord,
|
|
3406
|
+
requiredCount = RESET_HISTORY_PAGE_SIZE,
|
|
3407
|
+
): Promise<boolean> {
|
|
3408
|
+
const existing = this.turnHistoryHydrations.get(current.localSessionId);
|
|
3409
|
+
if (existing) await existing;
|
|
3410
|
+
const hydration = (async () => {
|
|
3411
|
+
if (this.store.getForkHistorySource(current.localSessionId)) {
|
|
3412
|
+
return this.hydrateForkTurnHistory(current.contextKey, current, requiredCount);
|
|
3413
|
+
}
|
|
3414
|
+
return this.hydrateRemoteTurnHistory(current.contextKey, current, requiredCount);
|
|
3415
|
+
})();
|
|
3416
|
+
this.turnHistoryHydrations.set(current.localSessionId, hydration);
|
|
3417
|
+
try {
|
|
3418
|
+
return await hydration;
|
|
3419
|
+
} finally {
|
|
3420
|
+
if (this.turnHistoryHydrations.get(current.localSessionId) === hydration) {
|
|
3421
|
+
this.turnHistoryHydrations.delete(current.localSessionId);
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
private async hydrateForkTurnHistory(
|
|
3442
3427
|
contextKey: string,
|
|
3443
3428
|
current: SessionRecord,
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3429
|
+
requiredCount: number,
|
|
3430
|
+
visited = new Set<string>(),
|
|
3431
|
+
): Promise<boolean> {
|
|
3432
|
+
if (visited.has(current.localSessionId)) return false;
|
|
3433
|
+
visited.add(current.localSessionId);
|
|
3434
|
+
let source = this.store.getForkHistorySource(current.localSessionId);
|
|
3435
|
+
if (!source) return false;
|
|
3436
|
+
const completedHead = current.lastTurnStatus === "completed"
|
|
3437
|
+
? current.lastTurnId
|
|
3438
|
+
: this.store.findLatestCompletedTurnId(current.localSessionId);
|
|
3439
|
+
if (completedHead && completedHead !== source.sourceTurnId && current.remoteSessionId) {
|
|
3440
|
+
// Once the branch has its own completed work, its remote history is the
|
|
3441
|
+
// authority for missing local turns, including work continued in Desktop.
|
|
3442
|
+
source = {
|
|
3443
|
+
sourceLocalSessionId: current.localSessionId,
|
|
3444
|
+
sourceRemoteSessionId: current.remoteSessionId,
|
|
3445
|
+
sourceTurnId: completedHead,
|
|
3446
|
+
createdAt: current.createdAt,
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
if (this.store.hasImportedForkTurnHistory(current.localSessionId, source.sourceRemoteSessionId, source.sourceTurnId)) return false;
|
|
3450
|
+
const localTurns = this.store.listTaskTurnGraphIndex(current.localSessionId);
|
|
3451
|
+
const availableCount = localTurns.length;
|
|
3452
|
+
const key = JSON.stringify([current.agentName, source.sourceRemoteSessionId, source.sourceTurnId]);
|
|
3453
|
+
let state = this.forkHistoryCursors.get(key) ?? { anchorFound: false, complete: false };
|
|
3454
|
+
const localAnchorIndex = localTurns.findIndex((turn) => turn.turnId === source.sourceTurnId);
|
|
3455
|
+
const ownTurnCount = source.sourceLocalSessionId === current.localSessionId
|
|
3456
|
+
? Math.max(0, localAnchorIndex)
|
|
3457
|
+
: localAnchorIndex >= 0 ? localAnchorIndex : availableCount;
|
|
3458
|
+
const neededInheritedCount = Math.max(0, requiredCount - ownTurnCount);
|
|
3459
|
+
if (localAnchorIndex >= 0 && availableCount >= requiredCount
|
|
3460
|
+
&& (!state.anchorFound || (state.loadedCount ?? 0) >= neededInheritedCount)) return !state.complete;
|
|
3461
|
+
const sourceSession = source.sourceLocalSessionId
|
|
3462
|
+
? this.store.getSession(source.sourceLocalSessionId)
|
|
3463
|
+
: undefined;
|
|
3464
|
+
// A branch with no new turns can reuse its ancestor's history, even before
|
|
3465
|
+
// the Agent materializes that intermediate branch.
|
|
3466
|
+
if (sourceSession && sourceSession.localSessionId !== current.localSessionId
|
|
3467
|
+
&& this.store.getForkHistorySource(sourceSession.localSessionId)?.sourceTurnId === source.sourceTurnId) {
|
|
3468
|
+
const hasMore = await this.hydrateForkTurnHistory(
|
|
3469
|
+
sourceSession.contextKey, sourceSession,
|
|
3470
|
+
this.store.listTaskTurnGraphIndex(sourceSession.localSessionId).length + requiredCount - availableCount,
|
|
3471
|
+
visited,
|
|
3472
|
+
);
|
|
3473
|
+
if (!hasMore && this.store.hasImportedForkTurnHistory(sourceSession.localSessionId)) {
|
|
3474
|
+
this.store.audit(contextKey, "fork_turn_history_imported", {
|
|
3475
|
+
forkedLocalSessionId: current.localSessionId,
|
|
3476
|
+
sourceRemoteSessionId: source.sourceRemoteSessionId,
|
|
3477
|
+
sourceTurnId: source.sourceTurnId,
|
|
3478
|
+
reusedLocalSessionId: sourceSession.localSessionId,
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
return hasMore;
|
|
3482
|
+
}
|
|
3483
|
+
if (!source.sourceRemoteSessionId) return false;
|
|
3449
3484
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
3450
|
-
if (runtime.kind !== "codex" || !runtime.
|
|
3485
|
+
if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries) return false;
|
|
3486
|
+
const seenCursors = new Set<string>();
|
|
3451
3487
|
try {
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3488
|
+
while (!state.complete && (state.loadedCount ?? 0) < neededInheritedCount) {
|
|
3489
|
+
const remaining = neededInheritedCount - (state.loadedCount ?? 0);
|
|
3490
|
+
const page = await runtime.listRemoteTurnSummaries(source.sourceRemoteSessionId, {
|
|
3491
|
+
cursor: state.cursor, limit: Math.min(RESET_HISTORY_PAGE_SIZE, remaining),
|
|
3492
|
+
});
|
|
3493
|
+
if (page.nextCursor && (page.nextCursor === state.cursor || seenCursors.has(page.nextCursor))) {
|
|
3494
|
+
throw new Error("App Server repeated a Turn history cursor.");
|
|
3495
|
+
}
|
|
3496
|
+
const anchorIndex = state.anchorFound ? 0 : page.turns.findIndex((turn) => turn.id === source.sourceTurnId);
|
|
3497
|
+
const inheritedTurns = anchorIndex >= 0 ? page.turns.slice(anchorIndex) : [];
|
|
3498
|
+
const anchorFound = state.anchorFound || anchorIndex >= 0;
|
|
3499
|
+
if (!anchorFound && !page.nextCursor) {
|
|
3500
|
+
this.forkHistoryCursors.delete(key);
|
|
3501
|
+
throw new Error("The Fork source Turn was not found in the available history.");
|
|
3502
|
+
}
|
|
3503
|
+
if (inheritedTurns.length > 0) {
|
|
3504
|
+
const ownerId = sourceSession?.localSessionId ?? current.localSessionId;
|
|
3505
|
+
const imported = this.persistCompletedTurnHistory({
|
|
3506
|
+
localSessionId: ownerId,
|
|
3507
|
+
contextKey: sourceSession?.contextKey ?? contextKey,
|
|
3508
|
+
agentName: current.agentName,
|
|
3509
|
+
remoteSessionId: source.sourceRemoteSessionId,
|
|
3510
|
+
completedTurns: inheritedTurns.slice().reverse(),
|
|
3511
|
+
// Adjacent remote summaries repair gaps in locally inferred ancestry.
|
|
3512
|
+
replaceParents: true,
|
|
3513
|
+
fallbackStart: (source.sourceTurnStartedAt ?? source.sourceTurnCompletedAt
|
|
3514
|
+
?? parseIsoTimestamp(source.createdAt) ?? 0) - (state.loadedCount ?? 0) - inheritedTurns.length,
|
|
3515
|
+
});
|
|
3516
|
+
if (imported === 0) throw new Error("Failed to persist the requested Turn history page.");
|
|
3517
|
+
if (state.tailTurnId) {
|
|
3518
|
+
this.store.saveTurnParent(state.tailTurnId, ownerId, inheritedTurns[0]!.id, true);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
state = {
|
|
3522
|
+
cursor: page.nextCursor,
|
|
3523
|
+
anchorFound,
|
|
3524
|
+
tailTurnId: inheritedTurns.at(-1)?.id ?? state.tailTurnId,
|
|
3525
|
+
loadedCount: (state.loadedCount ?? 0) + inheritedTurns.length,
|
|
3526
|
+
complete: !page.nextCursor,
|
|
3527
|
+
};
|
|
3528
|
+
this.forkHistoryCursors.set(key, state);
|
|
3529
|
+
if (page.nextCursor) seenCursors.add(page.nextCursor);
|
|
3530
|
+
}
|
|
3531
|
+
if (state.complete) {
|
|
3471
3532
|
this.store.audit(contextKey, "fork_turn_history_imported", {
|
|
3472
3533
|
forkedLocalSessionId: current.localSessionId,
|
|
3473
3534
|
sourceRemoteSessionId: source.sourceRemoteSessionId,
|
|
3474
3535
|
sourceTurnId: source.sourceTurnId,
|
|
3475
|
-
importedTurnCount: imported,
|
|
3476
3536
|
});
|
|
3477
3537
|
}
|
|
3538
|
+
return !state.complete;
|
|
3478
3539
|
} catch (error) {
|
|
3540
|
+
if (error instanceof CodexVersionError) throw error;
|
|
3479
3541
|
this.logger.warn(
|
|
3480
3542
|
{ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId },
|
|
3481
|
-
"Failed to
|
|
3543
|
+
"Failed to load the requested Fork Turn history page; rendering the locally available history.",
|
|
3482
3544
|
);
|
|
3483
3545
|
}
|
|
3546
|
+
return true;
|
|
3484
3547
|
}
|
|
3485
3548
|
|
|
3486
3549
|
private async hydrateRemoteTurnHistory(
|
|
3487
3550
|
contextKey: string,
|
|
3488
3551
|
current: SessionRecord,
|
|
3489
|
-
|
|
3490
|
-
|
|
3552
|
+
requiredCount: number,
|
|
3553
|
+
): Promise<boolean> {
|
|
3554
|
+
if (!current.remoteSessionId) return false;
|
|
3555
|
+
const key = JSON.stringify([current.agentName, current.remoteSessionId, "turn-card"]);
|
|
3491
3556
|
const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
&& this.store.hasImportedForkTurnHistory(current.localSessionId)) return;
|
|
3557
|
+
let state = this.forkHistoryCursors.get(key);
|
|
3558
|
+
if (refreshedAt === undefined || Date.now() - refreshedAt >= REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS) state = undefined;
|
|
3559
|
+
state ??= { anchorFound: true, complete: false };
|
|
3496
3560
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
3497
|
-
if (runtime.kind !== "codex" || !runtime.
|
|
3561
|
+
if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries) return false;
|
|
3562
|
+
const seen = new Set<string>();
|
|
3498
3563
|
try {
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3564
|
+
while (!state.complete && (state.loadedCount ?? 0) < requiredCount) {
|
|
3565
|
+
const page = await runtime.listRemoteTurnSummaries(current.remoteSessionId, {
|
|
3566
|
+
cursor: state.cursor,
|
|
3567
|
+
limit: Math.min(RESET_HISTORY_PAGE_SIZE, requiredCount - (state.loadedCount ?? 0)),
|
|
3568
|
+
});
|
|
3569
|
+
if (page.nextCursor && (page.nextCursor === state.cursor || seen.has(page.nextCursor))) {
|
|
3570
|
+
throw new Error("App Server repeated a Turn history cursor.");
|
|
3571
|
+
}
|
|
3572
|
+
if (page.turns.length) {
|
|
3573
|
+
const imported = this.persistCompletedTurnHistory({
|
|
3574
|
+
localSessionId: current.localSessionId, contextKey, agentName: current.agentName,
|
|
3575
|
+
remoteSessionId: current.remoteSessionId,
|
|
3576
|
+
completedTurns: page.turns.slice().reverse(), replaceParents: true,
|
|
3577
|
+
fallbackStart: (parseIsoTimestamp(current.createdAt) ?? 0) - (state.loadedCount ?? 0) - page.turns.length,
|
|
3578
|
+
});
|
|
3579
|
+
if (!imported) throw new Error("Failed to persist the requested Turn history page.");
|
|
3580
|
+
if (state.tailTurnId) this.store.saveTurnParent(state.tailTurnId, current.localSessionId, page.turns[0]!.id, true);
|
|
3581
|
+
}
|
|
3582
|
+
state = {
|
|
3583
|
+
anchorFound: true, cursor: page.nextCursor, complete: !page.nextCursor,
|
|
3584
|
+
loadedCount: (state.loadedCount ?? 0) + page.turns.length,
|
|
3585
|
+
tailTurnId: page.turns.at(-1)?.id ?? state.tailTurnId,
|
|
3586
|
+
};
|
|
3587
|
+
this.forkHistoryCursors.set(key, state);
|
|
3588
|
+
this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
|
|
3589
|
+
if (page.nextCursor) seen.add(page.nextCursor);
|
|
3590
|
+
}
|
|
3591
|
+
return !state.complete;
|
|
3511
3592
|
} catch (error) {
|
|
3593
|
+
if (error instanceof CodexVersionError) throw error;
|
|
3512
3594
|
this.logger.warn(
|
|
3513
3595
|
{ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId },
|
|
3514
3596
|
"Failed to hydrate App Server Turn history; rendering the locally available history.",
|
|
3515
3597
|
);
|
|
3516
3598
|
}
|
|
3599
|
+
return true;
|
|
3517
3600
|
}
|
|
3518
3601
|
|
|
3519
3602
|
private async createProjectSessionFromReference(contextKey: string, reference: string): Promise<void> {
|
|
@@ -3560,7 +3643,7 @@ export class ProxySessionController {
|
|
|
3560
3643
|
agentName = source.agentName;
|
|
3561
3644
|
runtime = this.runtimes.forAgent(agentName);
|
|
3562
3645
|
} else {
|
|
3563
|
-
const resolved = await this.resolveRemoteCodexSession(taskId);
|
|
3646
|
+
const resolved = await this.resolveRemoteCodexSession(taskId, { view: "metadata" });
|
|
3564
3647
|
agentName = resolved.agentName;
|
|
3565
3648
|
runtime = resolved.runtime;
|
|
3566
3649
|
remote = resolved.remote;
|
|
@@ -3569,9 +3652,9 @@ export class ProxySessionController {
|
|
|
3569
3652
|
const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
|
|
3570
3653
|
if (!remote && runtime.readRemoteSession && remoteSessionId) {
|
|
3571
3654
|
try {
|
|
3572
|
-
remote = await runtime.readRemoteSession(remoteSessionId);
|
|
3655
|
+
remote = await runtime.readRemoteSession(remoteSessionId, "metadata");
|
|
3573
3656
|
} catch (error) {
|
|
3574
|
-
if (!source) throw error;
|
|
3657
|
+
if (!source || error instanceof CodexVersionError) throw error;
|
|
3575
3658
|
this.logger.warn(
|
|
3576
3659
|
{ error, contextKey, taskId: remoteSessionId },
|
|
3577
3660
|
"Failed to refresh the source task before creating a project task; using the local project path.",
|
|
@@ -3908,6 +3991,7 @@ export class ProxySessionController {
|
|
|
3908
3991
|
if (!userId?.startsWith("ou_")) {
|
|
3909
3992
|
throw new Error("/forkgroup 只能由具有 open_id 的飞书用户消息触发。");
|
|
3910
3993
|
}
|
|
3994
|
+
await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
|
|
3911
3995
|
const prepared = await this.prepareForkGroupSession(sourceContextKey, requestedTitle, incomingMessage);
|
|
3912
3996
|
await this.forkPreparedSessionToFeishuGroup(
|
|
3913
3997
|
sourceContextKey,
|
|
@@ -3925,6 +4009,7 @@ export class ProxySessionController {
|
|
|
3925
4009
|
if (!userId?.startsWith("ou_")) {
|
|
3926
4010
|
throw new Error("ForkGroup 只能由具有 open_id 的飞书用户触发。");
|
|
3927
4011
|
}
|
|
4012
|
+
await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
|
|
3928
4013
|
const plan = await this.prepareForkSession(sourceContextKey, reference);
|
|
3929
4014
|
await this.forkPreparedSessionToFeishuGroup(
|
|
3930
4015
|
sourceContextKey,
|
|
@@ -4077,7 +4162,8 @@ export class ProxySessionController {
|
|
|
4077
4162
|
});
|
|
4078
4163
|
} catch (error) {
|
|
4079
4164
|
if (activeWriterThreadId(error)) throw error;
|
|
4080
|
-
if (!(agent.kind === "app-server" && !record.lastTurnId
|
|
4165
|
+
if (!(agent.kind === "app-server" && !record.lastTurnId
|
|
4166
|
+
&& !this.store.getForkHistorySource(record.localSessionId) && isMissingRolloutError(error))) throw error;
|
|
4081
4167
|
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
4082
4168
|
session = await runtime.createSession({
|
|
4083
4169
|
localSessionId: record.localSessionId,
|
|
@@ -4555,6 +4641,7 @@ export class ProxySessionController {
|
|
|
4555
4641
|
|
|
4556
4642
|
private async cancel(contextKey: string): Promise<void> {
|
|
4557
4643
|
const record = this.requireCurrentSession(contextKey);
|
|
4644
|
+
await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
|
|
4558
4645
|
await this.cancelSession(record);
|
|
4559
4646
|
}
|
|
4560
4647
|
|
|
@@ -4567,6 +4654,7 @@ export class ProxySessionController {
|
|
|
4567
4654
|
const goal = await runtime.getGoal(record.localSessionId);
|
|
4568
4655
|
if (goal?.status === "active") await runtime.setGoal(record.localSessionId, { status: "paused" });
|
|
4569
4656
|
} catch (error) {
|
|
4657
|
+
if (error instanceof CodexVersionError) throw error;
|
|
4570
4658
|
this.logger.warn(
|
|
4571
4659
|
{ error, sessionId: record.localSessionId },
|
|
4572
4660
|
"Failed to pause the active Agent goal before interrupting its turn.",
|
|
@@ -4581,6 +4669,7 @@ export class ProxySessionController {
|
|
|
4581
4669
|
? remote.lastTurnId
|
|
4582
4670
|
: undefined;
|
|
4583
4671
|
} catch (error) {
|
|
4672
|
+
if (error instanceof CodexVersionError) throw error;
|
|
4584
4673
|
this.logger.warn(
|
|
4585
4674
|
{ error, sessionId: record.localSessionId, remoteSessionId: record.remoteSessionId },
|
|
4586
4675
|
"Failed to inspect the current App Server turn before interrupting; using the locally tracked turn.",
|
|
@@ -4628,9 +4717,15 @@ export class ProxySessionController {
|
|
|
4628
4717
|
turnId: string,
|
|
4629
4718
|
): Promise<void> {
|
|
4630
4719
|
if (!sessionId || !turnId) throw new Error("无效的 Reset 请求。请使用最新的思考卡片重试。");
|
|
4720
|
+
await this.runSessionReset(sessionId, async () => {
|
|
4721
|
+
await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
|
|
4722
|
+
await this.performCurrentSessionReset(contextKey, sessionId, turnId);
|
|
4723
|
+
});
|
|
4724
|
+
}
|
|
4725
|
+
|
|
4726
|
+
private async runSessionReset(sessionId: string, operation: () => Promise<void>): Promise<void> {
|
|
4631
4727
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
4632
|
-
const reset = previous.catch(() => undefined).then(
|
|
4633
|
-
this.performCurrentSessionReset(contextKey, sessionId, turnId));
|
|
4728
|
+
const reset = previous.catch(() => undefined).then(operation);
|
|
4634
4729
|
this.sessionResets.set(sessionId, reset);
|
|
4635
4730
|
try {
|
|
4636
4731
|
await reset;
|
|
@@ -4639,6 +4734,19 @@ export class ProxySessionController {
|
|
|
4639
4734
|
}
|
|
4640
4735
|
}
|
|
4641
4736
|
|
|
4737
|
+
private async waitForCurrentSessionReset(contextKey: string): Promise<void> {
|
|
4738
|
+
const sessionId = this.currentSession(contextKey)?.localSessionId;
|
|
4739
|
+
if (!sessionId) return;
|
|
4740
|
+
|
|
4741
|
+
let reset = this.sessionResets.get(sessionId);
|
|
4742
|
+
while (reset) {
|
|
4743
|
+
await reset.catch(() => undefined);
|
|
4744
|
+
const next = this.sessionResets.get(sessionId);
|
|
4745
|
+
if (!next || next === reset) return;
|
|
4746
|
+
reset = next;
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4642
4750
|
private async resetFromHistoryCard(
|
|
4643
4751
|
contextKey: string,
|
|
4644
4752
|
sessionId: string,
|
|
@@ -4655,16 +4763,11 @@ export class ProxySessionController {
|
|
|
4655
4763
|
|
|
4656
4764
|
this.resetHistoryOperations.set(sessionId, turnId);
|
|
4657
4765
|
try {
|
|
4658
|
-
await this.
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
this.
|
|
4662
|
-
}
|
|
4663
|
-
throw error;
|
|
4664
|
-
}
|
|
4665
|
-
|
|
4666
|
-
try {
|
|
4667
|
-
await this.resetCurrentSessionToTurn(contextKey, sessionId, turnId);
|
|
4766
|
+
await this.runSessionReset(sessionId, async () => {
|
|
4767
|
+
await this.outbound.sendText(contextKey, TURN_RESET_STARTED_MESSAGE);
|
|
4768
|
+
await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
|
|
4769
|
+
await this.performCurrentSessionReset(contextKey, sessionId, turnId);
|
|
4770
|
+
});
|
|
4668
4771
|
} catch (error) {
|
|
4669
4772
|
if (this.resetHistoryOperations.get(sessionId) === turnId) {
|
|
4670
4773
|
this.resetHistoryOperations.delete(sessionId);
|
|
@@ -4708,9 +4811,9 @@ export class ProxySessionController {
|
|
|
4708
4811
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
4709
4812
|
}
|
|
4710
4813
|
|
|
4711
|
-
const historyTurn = this.store.
|
|
4814
|
+
const historyTurn = this.store.listTaskTurnGraphIndex(current.localSessionId)
|
|
4712
4815
|
.find((turn) => turn.turnId === turnId);
|
|
4713
|
-
const snapshot =
|
|
4816
|
+
const snapshot = historyTurn ? this.store.getTurnPromptSummary(turnId) : undefined;
|
|
4714
4817
|
if (!snapshot || snapshot.status !== "completed") {
|
|
4715
4818
|
throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
|
|
4716
4819
|
}
|
|
@@ -4894,6 +4997,7 @@ export class ProxySessionController {
|
|
|
4894
4997
|
const record = options.sessionId
|
|
4895
4998
|
? this.requireSession(contextKey, options.sessionId)
|
|
4896
4999
|
: this.requireCurrentSession(contextKey);
|
|
5000
|
+
await this.outbound.sendText(contextKey, `正在切换到 Provider ${modelProvider},请稍后。`);
|
|
4897
5001
|
const loaded = await this.loadSession(record);
|
|
4898
5002
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4899
5003
|
const models = await loaded.runtime.listModels();
|
|
@@ -4911,7 +5015,7 @@ export class ProxySessionController {
|
|
|
4911
5015
|
model: model.id,
|
|
4912
5016
|
effort,
|
|
4913
5017
|
mode: loaded.session.permissionMode,
|
|
4914
|
-
}, options);
|
|
5018
|
+
}, options, false);
|
|
4915
5019
|
}
|
|
4916
5020
|
|
|
4917
5021
|
private async openProviderSelector(
|
|
@@ -5004,28 +5108,22 @@ export class ProxySessionController {
|
|
|
5004
5108
|
contextKey: string,
|
|
5005
5109
|
settings: { provider: string; model: string; effort: string; mode: PermissionMode },
|
|
5006
5110
|
options: ModelCardOptions = {},
|
|
5111
|
+
announceStart = true,
|
|
5007
5112
|
): Promise<void> {
|
|
5008
5113
|
const record = options.sessionId
|
|
5009
5114
|
? this.requireSession(contextKey, options.sessionId)
|
|
5010
5115
|
: this.requireCurrentSession(contextKey);
|
|
5116
|
+
if (announceStart) {
|
|
5117
|
+
await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
5118
|
+
}
|
|
5011
5119
|
const loaded = await this.loadSession(record);
|
|
5012
5120
|
await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
5013
|
-
|
|
5014
|
-
throw new Error("当前运行时不支持 Provider 设置。");
|
|
5015
|
-
}
|
|
5016
|
-
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
|
|
5121
|
+
const session = await this.changeProviderSettings(loaded, {
|
|
5017
5122
|
modelProvider: settings.provider,
|
|
5018
5123
|
model: settings.model,
|
|
5019
5124
|
reasoningEffort: settings.effort,
|
|
5020
5125
|
permissionMode: settings.mode,
|
|
5021
5126
|
});
|
|
5022
|
-
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
5023
|
-
modelProvider: session.modelProvider ?? settings.provider,
|
|
5024
|
-
model: session.model ?? settings.model,
|
|
5025
|
-
reasoningEffort: session.reasoningEffort ?? settings.effort,
|
|
5026
|
-
permissionMode: session.permissionMode,
|
|
5027
|
-
});
|
|
5028
|
-
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
5029
5127
|
const notice = [
|
|
5030
5128
|
`Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
|
|
5031
5129
|
`模型 ${cardCode(session.model ?? settings.model)}`,
|
|
@@ -5044,6 +5142,30 @@ export class ProxySessionController {
|
|
|
5044
5142
|
}
|
|
5045
5143
|
}
|
|
5046
5144
|
|
|
5145
|
+
private async changeProviderSettings(loaded: LoadedSession, settings: RuntimeExecutionSettings): Promise<RuntimeSession> {
|
|
5146
|
+
if (!loaded.runtime.setExecutionSettings) throw new Error("当前运行时不支持 Provider 设置。");
|
|
5147
|
+
const persist = async (candidate: RuntimeSession): Promise<void> => {
|
|
5148
|
+
assertProviderSettingsApplied(candidate, settings);
|
|
5149
|
+
const agent = this.ensureAgent(loaded.record.agentName);
|
|
5150
|
+
const previousDefaults = agent.defaults;
|
|
5151
|
+
await this.persistAgentExecutionDefaults(loaded.record.localSessionId, candidate);
|
|
5152
|
+
try {
|
|
5153
|
+
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
5154
|
+
remoteSessionId: candidate.remoteSessionId,
|
|
5155
|
+
modelProvider: candidate.modelProvider,
|
|
5156
|
+
model: candidate.model,
|
|
5157
|
+
reasoningEffort: candidate.reasoningEffort,
|
|
5158
|
+
permissionMode: candidate.permissionMode,
|
|
5159
|
+
});
|
|
5160
|
+
} catch (error) {
|
|
5161
|
+
await this.lifecycle?.persistAgentExecutionDefaults?.(loaded.record.agentName, previousDefaults ?? {});
|
|
5162
|
+
agent.defaults = previousDefaults;
|
|
5163
|
+
throw error;
|
|
5164
|
+
}
|
|
5165
|
+
};
|
|
5166
|
+
return loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
5167
|
+
}
|
|
5168
|
+
|
|
5047
5169
|
private async assertProviderModelSettings(
|
|
5048
5170
|
loaded: LoadedSession,
|
|
5049
5171
|
modelProvider: string,
|
|
@@ -5464,7 +5586,9 @@ export class ProxySessionController {
|
|
|
5464
5586
|
private async setTitle(contextKey: string, title: string): Promise<void> {
|
|
5465
5587
|
const normalizedTitle = normalizeTaskTitle(title);
|
|
5466
5588
|
if (!normalizedTitle) throw new Error("任务标题不能为空。");
|
|
5467
|
-
const
|
|
5589
|
+
const current = this.requireCurrentSession(contextKey);
|
|
5590
|
+
await this.outbound.sendText(contextKey, CHANGE_TITLE_STARTED_MESSAGE);
|
|
5591
|
+
const loaded = await this.loadSession(current);
|
|
5468
5592
|
if (loaded.runtime.setTitle) await loaded.runtime.setTitle(loaded.record.localSessionId, normalizedTitle);
|
|
5469
5593
|
else loaded.session.title = normalizedTitle;
|
|
5470
5594
|
this.store.updateRuntimeSession(loaded.record.localSessionId, { title: normalizedTitle });
|
|
@@ -5597,9 +5721,9 @@ export class ProxySessionController {
|
|
|
5597
5721
|
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
5598
5722
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
5599
5723
|
}
|
|
5600
|
-
|
|
5601
|
-
await this.
|
|
5602
|
-
const completedTurns = this.store.
|
|
5724
|
+
const requestedPage = Math.max(0, Math.trunc(options.page ?? 0));
|
|
5725
|
+
const hasMoreHistory = await this.hydrateTaskTurnHistory(current, (requestedPage + 1) * RESET_HISTORY_PAGE_SIZE);
|
|
5726
|
+
const completedTurns = this.store.listTaskTurnGraphIndex(current.localSessionId);
|
|
5603
5727
|
const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
|
|
5604
5728
|
const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
|
|
5605
5729
|
&& !completedTurnIds.has(current.lastTurnId)
|
|
@@ -5641,13 +5765,13 @@ export class ProxySessionController {
|
|
|
5641
5765
|
parentTurnId: turn.parentTurnId,
|
|
5642
5766
|
})));
|
|
5643
5767
|
const total = allTurns.length;
|
|
5644
|
-
const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE));
|
|
5768
|
+
const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE)) + (hasMoreHistory ? 1 : 0);
|
|
5645
5769
|
const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
|
|
5646
5770
|
const offset = page * RESET_HISTORY_PAGE_SIZE;
|
|
5647
5771
|
const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
|
|
5648
5772
|
const resettingTurnId = this.resetHistoryOperations.get(current.localSessionId);
|
|
5649
5773
|
const entries: ResetHistoryCardEntry[] = turns.flatMap((turn, index) => {
|
|
5650
|
-
const snapshot =
|
|
5774
|
+
const snapshot = turn.turnId === runningTurnId ? runningSnapshot : this.store.getTurnPromptSummary(turn.turnId);
|
|
5651
5775
|
if (!snapshot) return [];
|
|
5652
5776
|
const graph = graphRows[offset + index]!;
|
|
5653
5777
|
const summary = truncateText(
|
|
@@ -5702,7 +5826,9 @@ export class ProxySessionController {
|
|
|
5702
5826
|
const card = this.cardRenderer.renderResetHistoryCard({
|
|
5703
5827
|
entries,
|
|
5704
5828
|
footerLines: [
|
|
5705
|
-
|
|
5829
|
+
hasMoreHistory
|
|
5830
|
+
? `第 ${page + 1} 页 · 已加载 ${total} 个 turn`
|
|
5831
|
+
: runningTurn
|
|
5706
5832
|
? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
|
|
5707
5833
|
: `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
|
|
5708
5834
|
...(resettingTurnId ? ["正在 Reset 到所选轮次,请稍候…"] : []),
|
|
@@ -5912,6 +6038,7 @@ export class ProxySessionController {
|
|
|
5912
6038
|
const record = this.requireCurrentSession(contextKey);
|
|
5913
6039
|
const filePath = resolveUserPath(requestedFilePath, record.cwd);
|
|
5914
6040
|
await this.assertSendableFile(filePath);
|
|
6041
|
+
await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
|
|
5915
6042
|
await this.outbound.sendFile(contextKey, filePath);
|
|
5916
6043
|
}
|
|
5917
6044
|
|
|
@@ -6216,7 +6343,7 @@ export class ProxySessionController {
|
|
|
6216
6343
|
|
|
6217
6344
|
private async resolveRemoteCodexSession(
|
|
6218
6345
|
reference: string,
|
|
6219
|
-
options: { forFork?: boolean } = {},
|
|
6346
|
+
options: { forFork?: boolean; view?: "metadata" | "latest" | "latest-full" } = {},
|
|
6220
6347
|
): Promise<AgentRemoteSession> {
|
|
6221
6348
|
const scoped = parseRemoteSessionReference(reference);
|
|
6222
6349
|
const candidates = scoped
|
|
@@ -6234,7 +6361,7 @@ export class ProxySessionController {
|
|
|
6234
6361
|
runtime,
|
|
6235
6362
|
remote: options.forFork && runtime.readRemoteForkSource
|
|
6236
6363
|
? await runtime.readRemoteForkSource(remoteSessionId)
|
|
6237
|
-
: await runtime.readRemoteSession(remoteSessionId),
|
|
6364
|
+
: await runtime.readRemoteSession(remoteSessionId, options.view ?? "latest"),
|
|
6238
6365
|
} satisfies AgentRemoteSession;
|
|
6239
6366
|
}));
|
|
6240
6367
|
const matches = reads
|
|
@@ -6260,6 +6387,7 @@ export class ProxySessionController {
|
|
|
6260
6387
|
if (direct?.status === "closed") {
|
|
6261
6388
|
throw new Error(`找不到任务:${taskId}`);
|
|
6262
6389
|
}
|
|
6390
|
+
await this.outbound.sendText(contextKey, SWITCH_TASK_STARTED_MESSAGE);
|
|
6263
6391
|
const existing = direct ?? this.findStoredSessionByReference(taskId);
|
|
6264
6392
|
if (existing) {
|
|
6265
6393
|
const runtime = this.runtimes.forAgent(existing.agentName);
|
|
@@ -6368,6 +6496,10 @@ export class ProxySessionController {
|
|
|
6368
6496
|
throw new Error("任务仍有排队消息,无法归档。请先处理或取消排队消息。");
|
|
6369
6497
|
}
|
|
6370
6498
|
|
|
6499
|
+
if (options.source !== "dismiss") {
|
|
6500
|
+
await this.outbound.sendText(contextKey, ARCHIVE_TASK_STARTED_MESSAGE);
|
|
6501
|
+
}
|
|
6502
|
+
|
|
6371
6503
|
const remoteReference = local
|
|
6372
6504
|
? remoteSessionReference(local.agentName, local.remoteSessionId!)
|
|
6373
6505
|
: taskId;
|
|
@@ -6599,27 +6731,28 @@ export class ProxySessionController {
|
|
|
6599
6731
|
current = context.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
|
|
6600
6732
|
}
|
|
6601
6733
|
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6734
|
+
let remote: RemoteSessionSummary | undefined;
|
|
6735
|
+
if (current?.status === "running") {
|
|
6736
|
+
const runtime = this.runtimes.forAgent(current.agentName);
|
|
6737
|
+
if (runtime.getSession(current.localSessionId)) {
|
|
6738
|
+
try {
|
|
6739
|
+
remote = (await runtime.synchronizeSession(current.localSessionId)).remoteSummary;
|
|
6607
6740
|
current = this.store.getSession(current.localSessionId) ?? current;
|
|
6741
|
+
} catch (error) {
|
|
6742
|
+
if (error instanceof CodexVersionError) throw error;
|
|
6743
|
+
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
|
|
6608
6744
|
}
|
|
6609
|
-
} catch (error) {
|
|
6610
|
-
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
|
|
6611
6745
|
}
|
|
6612
6746
|
}
|
|
6613
|
-
|
|
6614
6747
|
const localCurrent = current;
|
|
6615
|
-
let remote: RemoteSessionSummary | undefined;
|
|
6616
6748
|
let goal: RuntimeGoal | undefined;
|
|
6617
6749
|
if (current?.remoteSessionId) {
|
|
6618
6750
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
6619
|
-
if (runtime.readRemoteSession) {
|
|
6751
|
+
if (!remote && runtime.readRemoteSession) {
|
|
6620
6752
|
try {
|
|
6621
|
-
remote = await runtime.readRemoteSession(current.remoteSessionId);
|
|
6753
|
+
remote = await runtime.readRemoteSession(current.remoteSessionId, "latest-full");
|
|
6622
6754
|
} catch (error) {
|
|
6755
|
+
if (error instanceof CodexVersionError) throw error;
|
|
6623
6756
|
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task status.");
|
|
6624
6757
|
}
|
|
6625
6758
|
}
|
|
@@ -6730,7 +6863,7 @@ export class ProxySessionController {
|
|
|
6730
6863
|
reference: string,
|
|
6731
6864
|
options: StatusCardOptions = {},
|
|
6732
6865
|
): Promise<void> {
|
|
6733
|
-
const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
|
|
6866
|
+
const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
|
|
6734
6867
|
const actionReference = remoteSessionReference(agentName, remote.id);
|
|
6735
6868
|
const sections: CardSection[] = [
|
|
6736
6869
|
{
|
|
@@ -6892,9 +7025,10 @@ export class ProxySessionController {
|
|
|
6892
7025
|
return agent;
|
|
6893
7026
|
}
|
|
6894
7027
|
|
|
6895
|
-
private async persistAgentExecutionDefaults(localSessionId: string): Promise<void> {
|
|
6896
|
-
const
|
|
6897
|
-
if (!
|
|
7028
|
+
private async persistAgentExecutionDefaults(localSessionId: string, candidate?: RuntimeSession): Promise<void> {
|
|
7029
|
+
const record = this.store.getSession(localSessionId);
|
|
7030
|
+
if (!record) throw new Error(`找不到任务:${localSessionId}`);
|
|
7031
|
+
const session = candidate ?? record;
|
|
6898
7032
|
const agent = this.ensureAgent(session.agentName);
|
|
6899
7033
|
const observed: AgentExecutionDefaults = {
|
|
6900
7034
|
...(session.modelProvider?.trim() ? { modelProvider: session.modelProvider } : {}),
|
|
@@ -7329,6 +7463,7 @@ export class ProxySessionController {
|
|
|
7329
7463
|
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
7330
7464
|
if (!lockPath) throw new Error("当前 Agent 不支持自动关闭占用程序。");
|
|
7331
7465
|
|
|
7466
|
+
await this.outbound.sendText(contextKey, CLOSE_WRITER_STARTED_MESSAGE);
|
|
7332
7467
|
let owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
7333
7468
|
const expectedOwner = owners.length === 1 && threadWriterMatches(
|
|
7334
7469
|
owners[0],
|
|
@@ -7788,11 +7923,6 @@ function runtimeErrorMessage(error: unknown): string {
|
|
|
7788
7923
|
return error instanceof Error ? error.message : String(error);
|
|
7789
7924
|
}
|
|
7790
7925
|
|
|
7791
|
-
function isMissingRolloutError(error: unknown): boolean {
|
|
7792
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7793
|
-
return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
|
|
7794
|
-
}
|
|
7795
|
-
|
|
7796
7926
|
function isUnmaterializedCodexThreadError(error: unknown): boolean {
|
|
7797
7927
|
const message = error instanceof Error ? error.message : String(error);
|
|
7798
7928
|
return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
|