@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
|
@@ -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.",
|
|
@@ -2990,6 +3037,7 @@ export class ProxySessionController {
|
|
|
2990
3037
|
}
|
|
2991
3038
|
|
|
2992
3039
|
private async forkSessionReference(contextKey: string, reference?: string): Promise<void> {
|
|
3040
|
+
await this.outbound.sendText(contextKey, FORK_TASK_STARTED_MESSAGE);
|
|
2993
3041
|
const plan = await this.prepareForkSession(contextKey, reference);
|
|
2994
3042
|
const forked = await this.forkSessionIntoContext(contextKey, plan);
|
|
2995
3043
|
const forkSourceLabel = plan.forkedFromHistoricalTurn
|
|
@@ -3050,35 +3098,24 @@ export class ProxySessionController {
|
|
|
3050
3098
|
if (!runtime.readRemoteSession && !source) {
|
|
3051
3099
|
throw new Error("当前 App Server Agent 不支持读取指定任务。");
|
|
3052
3100
|
}
|
|
3053
|
-
const
|
|
3101
|
+
const persistedCompletedTurnId = source
|
|
3102
|
+
? this.store.findLatestCompletedTurnId(source.localSessionId)
|
|
3103
|
+
: undefined;
|
|
3054
3104
|
const sourceLastSnapshot = turnViewSnapshot(
|
|
3055
3105
|
source?.lastTurnId ? this.store.getTurnSnapshot(source.lastTurnId) : undefined,
|
|
3056
3106
|
);
|
|
3057
|
-
const
|
|
3058
|
-
|
|
3059
|
-
: source?.lastTurnId
|
|
3107
|
+
const localCompletedTurnId = persistedCompletedTurnId
|
|
3108
|
+
?? (source?.lastTurnId
|
|
3060
3109
|
&& (source.lastTurnStatus === "completed" || sourceLastSnapshot?.status === "completed")
|
|
3061
|
-
?
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
}]
|
|
3069
|
-
: [];
|
|
3070
|
-
if (!remote && runtime.readRemoteSession && (!source || persistedCompletedTurns.length === 0)) {
|
|
3071
|
-
try {
|
|
3072
|
-
remote = await runtime.readRemoteSession(remoteSessionId);
|
|
3073
|
-
} catch (error) {
|
|
3074
|
-
if (localCompletedTurns.length === 0) throw error;
|
|
3075
|
-
this.logger.warn(
|
|
3076
|
-
{ error, contextKey, remoteSessionId, sourceTurnId: localCompletedTurns.at(-1)?.id },
|
|
3077
|
-
"Failed to read the complete source task before Fork; using the locally persisted completed Turn.",
|
|
3078
|
-
);
|
|
3079
|
-
}
|
|
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);
|
|
3080
3117
|
}
|
|
3081
|
-
const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ??
|
|
3118
|
+
const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurnId;
|
|
3082
3119
|
const latestSnapshot = turnViewSnapshot(latestTurnId ? this.store.getTurnSnapshot(latestTurnId) : undefined);
|
|
3083
3120
|
const isRunning = remote
|
|
3084
3121
|
? isRemoteSessionActive(remote)
|
|
@@ -3089,7 +3126,7 @@ export class ProxySessionController {
|
|
|
3089
3126
|
);
|
|
3090
3127
|
const lastTurnId = remote?.lastCompletedTurnId
|
|
3091
3128
|
?? (remote?.lastTurnStatus === "completed" ? remote.lastTurnId : undefined)
|
|
3092
|
-
??
|
|
3129
|
+
?? localCompletedTurnId;
|
|
3093
3130
|
if (!lastTurnId) {
|
|
3094
3131
|
if (isRunning) {
|
|
3095
3132
|
throw new Error(`${sourceLabel}正在执行,且还没有已完成轮次可供 fork。请等待当前轮次完成后重试。`);
|
|
@@ -3098,8 +3135,7 @@ export class ProxySessionController {
|
|
|
3098
3135
|
}
|
|
3099
3136
|
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
|
|
3100
3137
|
const forkedFromHistoricalTurn = lastTurnId !== latestTurnId;
|
|
3101
|
-
const completedTurn =
|
|
3102
|
-
.find((turn) => turn.id === lastTurnId);
|
|
3138
|
+
const completedTurn = remote?.completedTurns?.find((turn) => turn.id === lastTurnId);
|
|
3103
3139
|
|
|
3104
3140
|
const cwd = remote?.cwd || source?.cwd;
|
|
3105
3141
|
if (!cwd) throw new Error("指定的 App Server 任务没有可用的工作目录,暂时不能 fork。");
|
|
@@ -3145,29 +3181,11 @@ export class ProxySessionController {
|
|
|
3145
3181
|
sourceTurnPrompt,
|
|
3146
3182
|
sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
|
|
3147
3183
|
sourceTurnCompletedAt,
|
|
3148
|
-
sourceCompletedTurns: remote?.completedTurns
|
|
3149
|
-
?? (persistedCompletedTurns.length > 0 ? [] : localCompletedTurns),
|
|
3150
3184
|
sourceWasRunning: isRunning,
|
|
3151
3185
|
forkedFromHistoricalTurn,
|
|
3152
3186
|
};
|
|
3153
3187
|
}
|
|
3154
3188
|
|
|
3155
|
-
private localForkCompletedTurns(source: SessionRecord): RemoteCompletedTurnSummary[] {
|
|
3156
|
-
return this.store.listTaskTurnGraph(source.localSessionId)
|
|
3157
|
-
.slice()
|
|
3158
|
-
.reverse()
|
|
3159
|
-
.map((record) => {
|
|
3160
|
-
const snapshot = turnViewSnapshot(record.snapshot);
|
|
3161
|
-
const startedAt = snapshot?.startedAt ?? parseIsoTimestamp(record.updatedAt);
|
|
3162
|
-
return {
|
|
3163
|
-
id: record.turnId,
|
|
3164
|
-
prompt: latestUserPromptFromTurnView(snapshot),
|
|
3165
|
-
startedAt,
|
|
3166
|
-
completedAt: snapshot?.completedAt ?? startedAt,
|
|
3167
|
-
};
|
|
3168
|
-
});
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
3189
|
private async prepareForkGroupSession(
|
|
3172
3190
|
contextKey: string,
|
|
3173
3191
|
requestedTitle: string | undefined,
|
|
@@ -3270,7 +3288,6 @@ export class ProxySessionController {
|
|
|
3270
3288
|
contextKey: string,
|
|
3271
3289
|
plan: ForkSessionPlan,
|
|
3272
3290
|
): Promise<ForkSessionResult> {
|
|
3273
|
-
const sourceCompletedTurns = await this.resolveForkSourceCompletedTurns(plan);
|
|
3274
3291
|
const localSessionId = createId("sess");
|
|
3275
3292
|
const record = this.store.createSession({
|
|
3276
3293
|
localSessionId,
|
|
@@ -3313,12 +3330,6 @@ export class ProxySessionController {
|
|
|
3313
3330
|
plan.cwd,
|
|
3314
3331
|
this.agentLabel(plan.agentName),
|
|
3315
3332
|
);
|
|
3316
|
-
const sourceTurnHistoryCount = this.persistForkSourceHistory(
|
|
3317
|
-
contextKey,
|
|
3318
|
-
localSessionId,
|
|
3319
|
-
plan,
|
|
3320
|
-
sourceCompletedTurns,
|
|
3321
|
-
);
|
|
3322
3333
|
this.store.audit(contextKey, "session_forked", {
|
|
3323
3334
|
sourceLocalSessionId: plan.source?.localSessionId,
|
|
3324
3335
|
sourceRemoteSessionId: plan.remoteSessionId,
|
|
@@ -3326,7 +3337,6 @@ export class ProxySessionController {
|
|
|
3326
3337
|
sourceTurnPrompt: plan.sourceTurnPrompt,
|
|
3327
3338
|
sourceTurnStartedAt: plan.sourceTurnStartedAt,
|
|
3328
3339
|
sourceTurnCompletedAt: plan.sourceTurnCompletedAt,
|
|
3329
|
-
sourceTurnHistoryCount,
|
|
3330
3340
|
sourceWasRunning: plan.sourceWasRunning,
|
|
3331
3341
|
forkedLocalSessionId: localSessionId,
|
|
3332
3342
|
forkedRemoteSessionId: forked.remoteSessionId,
|
|
@@ -3342,54 +3352,6 @@ export class ProxySessionController {
|
|
|
3342
3352
|
}
|
|
3343
3353
|
}
|
|
3344
3354
|
|
|
3345
|
-
private async resolveForkSourceCompletedTurns(
|
|
3346
|
-
plan: ForkSessionPlan,
|
|
3347
|
-
): Promise<RemoteCompletedTurnSummary[]> {
|
|
3348
|
-
let completedTurns = plan.sourceCompletedTurns;
|
|
3349
|
-
if (completedTurns === undefined && plan.runtime.readRemoteSession) {
|
|
3350
|
-
try {
|
|
3351
|
-
completedTurns = (await plan.runtime.readRemoteSession(plan.remoteSessionId)).completedTurns ?? [];
|
|
3352
|
-
} catch (error) {
|
|
3353
|
-
this.logger.warn(
|
|
3354
|
-
{ error, remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId },
|
|
3355
|
-
"Failed to read the source task Turn history before Fork; keeping the Fork anchor fallback.",
|
|
3356
|
-
);
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
if (!completedTurns?.length) return [];
|
|
3360
|
-
const anchorIndex = completedTurns.findIndex((turn) => turn.id === plan.lastTurnId);
|
|
3361
|
-
if (anchorIndex < 0) {
|
|
3362
|
-
this.logger.warn(
|
|
3363
|
-
{ remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId },
|
|
3364
|
-
"The source task Turn history did not include the Fork anchor; keeping the Fork anchor fallback.",
|
|
3365
|
-
);
|
|
3366
|
-
return [];
|
|
3367
|
-
}
|
|
3368
|
-
return completedTurns.slice(0, anchorIndex + 1);
|
|
3369
|
-
}
|
|
3370
|
-
|
|
3371
|
-
private persistForkSourceHistory(
|
|
3372
|
-
contextKey: string,
|
|
3373
|
-
forkedLocalSessionId: string,
|
|
3374
|
-
plan: ForkSessionPlan,
|
|
3375
|
-
completedTurns: RemoteCompletedTurnSummary[],
|
|
3376
|
-
): number {
|
|
3377
|
-
if (completedTurns.length === 0) return 0;
|
|
3378
|
-
const localSessionId = plan.source?.localSessionId ?? forkedLocalSessionId;
|
|
3379
|
-
const historyContextKey = plan.source?.contextKey ?? contextKey;
|
|
3380
|
-
const fallbackStart = plan.sourceTurnStartedAt
|
|
3381
|
-
?? plan.sourceTurnCompletedAt
|
|
3382
|
-
?? Date.now() - completedTurns.length;
|
|
3383
|
-
return this.persistCompletedTurnHistory({
|
|
3384
|
-
localSessionId,
|
|
3385
|
-
contextKey: historyContextKey,
|
|
3386
|
-
agentName: plan.agentName,
|
|
3387
|
-
remoteSessionId: plan.remoteSessionId,
|
|
3388
|
-
completedTurns,
|
|
3389
|
-
fallbackStart,
|
|
3390
|
-
});
|
|
3391
|
-
}
|
|
3392
|
-
|
|
3393
3355
|
private persistCompletedTurnHistory(input: {
|
|
3394
3356
|
localSessionId: string;
|
|
3395
3357
|
contextKey: string;
|
|
@@ -3397,6 +3359,7 @@ export class ProxySessionController {
|
|
|
3397
3359
|
remoteSessionId: string;
|
|
3398
3360
|
completedTurns: RemoteCompletedTurnSummary[];
|
|
3399
3361
|
fallbackStart: number;
|
|
3362
|
+
replaceParents?: boolean;
|
|
3400
3363
|
}): number {
|
|
3401
3364
|
try {
|
|
3402
3365
|
this.store.importCompletedTurnHistory({
|
|
@@ -3404,6 +3367,7 @@ export class ProxySessionController {
|
|
|
3404
3367
|
contextKey: input.contextKey,
|
|
3405
3368
|
agentName: input.agentName,
|
|
3406
3369
|
remoteSessionId: input.remoteSessionId,
|
|
3370
|
+
replaceParents: input.replaceParents,
|
|
3407
3371
|
turns: input.completedTurns.map((turn, index) => {
|
|
3408
3372
|
const startedAt = turn.startedAt ?? input.fallbackStart + index;
|
|
3409
3373
|
const completedAt = turn.completedAt ?? startedAt;
|
|
@@ -3437,82 +3401,202 @@ export class ProxySessionController {
|
|
|
3437
3401
|
}
|
|
3438
3402
|
}
|
|
3439
3403
|
|
|
3440
|
-
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(
|
|
3441
3427
|
contextKey: string,
|
|
3442
3428
|
current: SessionRecord,
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
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;
|
|
3448
3484
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
3449
|
-
if (runtime.kind !== "codex" || !runtime.
|
|
3485
|
+
if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries) return false;
|
|
3486
|
+
const seenCursors = new Set<string>();
|
|
3450
3487
|
try {
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
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) {
|
|
3470
3532
|
this.store.audit(contextKey, "fork_turn_history_imported", {
|
|
3471
3533
|
forkedLocalSessionId: current.localSessionId,
|
|
3472
3534
|
sourceRemoteSessionId: source.sourceRemoteSessionId,
|
|
3473
3535
|
sourceTurnId: source.sourceTurnId,
|
|
3474
|
-
importedTurnCount: imported,
|
|
3475
3536
|
});
|
|
3476
3537
|
}
|
|
3538
|
+
return !state.complete;
|
|
3477
3539
|
} catch (error) {
|
|
3540
|
+
if (error instanceof CodexVersionError) throw error;
|
|
3478
3541
|
this.logger.warn(
|
|
3479
3542
|
{ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId },
|
|
3480
|
-
"Failed to
|
|
3543
|
+
"Failed to load the requested Fork Turn history page; rendering the locally available history.",
|
|
3481
3544
|
);
|
|
3482
3545
|
}
|
|
3546
|
+
return true;
|
|
3483
3547
|
}
|
|
3484
3548
|
|
|
3485
3549
|
private async hydrateRemoteTurnHistory(
|
|
3486
3550
|
contextKey: string,
|
|
3487
3551
|
current: SessionRecord,
|
|
3488
|
-
|
|
3489
|
-
|
|
3552
|
+
requiredCount: number,
|
|
3553
|
+
): Promise<boolean> {
|
|
3554
|
+
if (!current.remoteSessionId) return false;
|
|
3555
|
+
const key = JSON.stringify([current.agentName, current.remoteSessionId, "turn-card"]);
|
|
3490
3556
|
const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
&& 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 };
|
|
3495
3560
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
3496
|
-
if (runtime.kind !== "codex" || !runtime.
|
|
3561
|
+
if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries) return false;
|
|
3562
|
+
const seen = new Set<string>();
|
|
3497
3563
|
try {
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
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;
|
|
3510
3592
|
} catch (error) {
|
|
3593
|
+
if (error instanceof CodexVersionError) throw error;
|
|
3511
3594
|
this.logger.warn(
|
|
3512
3595
|
{ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId },
|
|
3513
3596
|
"Failed to hydrate App Server Turn history; rendering the locally available history.",
|
|
3514
3597
|
);
|
|
3515
3598
|
}
|
|
3599
|
+
return true;
|
|
3516
3600
|
}
|
|
3517
3601
|
|
|
3518
3602
|
private async createProjectSessionFromReference(contextKey: string, reference: string): Promise<void> {
|
|
@@ -3559,7 +3643,7 @@ export class ProxySessionController {
|
|
|
3559
3643
|
agentName = source.agentName;
|
|
3560
3644
|
runtime = this.runtimes.forAgent(agentName);
|
|
3561
3645
|
} else {
|
|
3562
|
-
const resolved = await this.resolveRemoteCodexSession(taskId);
|
|
3646
|
+
const resolved = await this.resolveRemoteCodexSession(taskId, { view: "metadata" });
|
|
3563
3647
|
agentName = resolved.agentName;
|
|
3564
3648
|
runtime = resolved.runtime;
|
|
3565
3649
|
remote = resolved.remote;
|
|
@@ -3568,9 +3652,9 @@ export class ProxySessionController {
|
|
|
3568
3652
|
const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
|
|
3569
3653
|
if (!remote && runtime.readRemoteSession && remoteSessionId) {
|
|
3570
3654
|
try {
|
|
3571
|
-
remote = await runtime.readRemoteSession(remoteSessionId);
|
|
3655
|
+
remote = await runtime.readRemoteSession(remoteSessionId, "metadata");
|
|
3572
3656
|
} catch (error) {
|
|
3573
|
-
if (!source) throw error;
|
|
3657
|
+
if (!source || error instanceof CodexVersionError) throw error;
|
|
3574
3658
|
this.logger.warn(
|
|
3575
3659
|
{ error, contextKey, taskId: remoteSessionId },
|
|
3576
3660
|
"Failed to refresh the source task before creating a project task; using the local project path.",
|
|
@@ -3907,6 +3991,7 @@ export class ProxySessionController {
|
|
|
3907
3991
|
if (!userId?.startsWith("ou_")) {
|
|
3908
3992
|
throw new Error("/forkgroup 只能由具有 open_id 的飞书用户消息触发。");
|
|
3909
3993
|
}
|
|
3994
|
+
await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
|
|
3910
3995
|
const prepared = await this.prepareForkGroupSession(sourceContextKey, requestedTitle, incomingMessage);
|
|
3911
3996
|
await this.forkPreparedSessionToFeishuGroup(
|
|
3912
3997
|
sourceContextKey,
|
|
@@ -3924,6 +4009,7 @@ export class ProxySessionController {
|
|
|
3924
4009
|
if (!userId?.startsWith("ou_")) {
|
|
3925
4010
|
throw new Error("ForkGroup 只能由具有 open_id 的飞书用户触发。");
|
|
3926
4011
|
}
|
|
4012
|
+
await this.outbound.sendText(sourceContextKey, FORK_GROUP_STARTED_MESSAGE);
|
|
3927
4013
|
const plan = await this.prepareForkSession(sourceContextKey, reference);
|
|
3928
4014
|
await this.forkPreparedSessionToFeishuGroup(
|
|
3929
4015
|
sourceContextKey,
|
|
@@ -4076,7 +4162,8 @@ export class ProxySessionController {
|
|
|
4076
4162
|
});
|
|
4077
4163
|
} catch (error) {
|
|
4078
4164
|
if (activeWriterThreadId(error)) throw error;
|
|
4079
|
-
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;
|
|
4080
4167
|
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
4081
4168
|
session = await runtime.createSession({
|
|
4082
4169
|
localSessionId: record.localSessionId,
|
|
@@ -4554,6 +4641,7 @@ export class ProxySessionController {
|
|
|
4554
4641
|
|
|
4555
4642
|
private async cancel(contextKey: string): Promise<void> {
|
|
4556
4643
|
const record = this.requireCurrentSession(contextKey);
|
|
4644
|
+
await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
|
|
4557
4645
|
await this.cancelSession(record);
|
|
4558
4646
|
}
|
|
4559
4647
|
|
|
@@ -4566,6 +4654,7 @@ export class ProxySessionController {
|
|
|
4566
4654
|
const goal = await runtime.getGoal(record.localSessionId);
|
|
4567
4655
|
if (goal?.status === "active") await runtime.setGoal(record.localSessionId, { status: "paused" });
|
|
4568
4656
|
} catch (error) {
|
|
4657
|
+
if (error instanceof CodexVersionError) throw error;
|
|
4569
4658
|
this.logger.warn(
|
|
4570
4659
|
{ error, sessionId: record.localSessionId },
|
|
4571
4660
|
"Failed to pause the active Agent goal before interrupting its turn.",
|
|
@@ -4580,6 +4669,7 @@ export class ProxySessionController {
|
|
|
4580
4669
|
? remote.lastTurnId
|
|
4581
4670
|
: undefined;
|
|
4582
4671
|
} catch (error) {
|
|
4672
|
+
if (error instanceof CodexVersionError) throw error;
|
|
4583
4673
|
this.logger.warn(
|
|
4584
4674
|
{ error, sessionId: record.localSessionId, remoteSessionId: record.remoteSessionId },
|
|
4585
4675
|
"Failed to inspect the current App Server turn before interrupting; using the locally tracked turn.",
|
|
@@ -4627,9 +4717,15 @@ export class ProxySessionController {
|
|
|
4627
4717
|
turnId: string,
|
|
4628
4718
|
): Promise<void> {
|
|
4629
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> {
|
|
4630
4727
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
4631
|
-
const reset = previous.catch(() => undefined).then(
|
|
4632
|
-
this.performCurrentSessionReset(contextKey, sessionId, turnId));
|
|
4728
|
+
const reset = previous.catch(() => undefined).then(operation);
|
|
4633
4729
|
this.sessionResets.set(sessionId, reset);
|
|
4634
4730
|
try {
|
|
4635
4731
|
await reset;
|
|
@@ -4638,6 +4734,19 @@ export class ProxySessionController {
|
|
|
4638
4734
|
}
|
|
4639
4735
|
}
|
|
4640
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
|
+
|
|
4641
4750
|
private async resetFromHistoryCard(
|
|
4642
4751
|
contextKey: string,
|
|
4643
4752
|
sessionId: string,
|
|
@@ -4654,16 +4763,11 @@ export class ProxySessionController {
|
|
|
4654
4763
|
|
|
4655
4764
|
this.resetHistoryOperations.set(sessionId, turnId);
|
|
4656
4765
|
try {
|
|
4657
|
-
await this.
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
this.
|
|
4661
|
-
}
|
|
4662
|
-
throw error;
|
|
4663
|
-
}
|
|
4664
|
-
|
|
4665
|
-
try {
|
|
4666
|
-
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
|
+
});
|
|
4667
4771
|
} catch (error) {
|
|
4668
4772
|
if (this.resetHistoryOperations.get(sessionId) === turnId) {
|
|
4669
4773
|
this.resetHistoryOperations.delete(sessionId);
|
|
@@ -4707,9 +4811,9 @@ export class ProxySessionController {
|
|
|
4707
4811
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
4708
4812
|
}
|
|
4709
4813
|
|
|
4710
|
-
const historyTurn = this.store.
|
|
4814
|
+
const historyTurn = this.store.listTaskTurnGraphIndex(current.localSessionId)
|
|
4711
4815
|
.find((turn) => turn.turnId === turnId);
|
|
4712
|
-
const snapshot =
|
|
4816
|
+
const snapshot = historyTurn ? this.store.getTurnPromptSummary(turnId) : undefined;
|
|
4713
4817
|
if (!snapshot || snapshot.status !== "completed") {
|
|
4714
4818
|
throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
|
|
4715
4819
|
}
|
|
@@ -4893,6 +4997,7 @@ export class ProxySessionController {
|
|
|
4893
4997
|
const record = options.sessionId
|
|
4894
4998
|
? this.requireSession(contextKey, options.sessionId)
|
|
4895
4999
|
: this.requireCurrentSession(contextKey);
|
|
5000
|
+
await this.outbound.sendText(contextKey, `正在切换到 Provider ${modelProvider},请稍后。`);
|
|
4896
5001
|
const loaded = await this.loadSession(record);
|
|
4897
5002
|
await this.assertModelProvider(loaded, modelProvider);
|
|
4898
5003
|
const models = await loaded.runtime.listModels();
|
|
@@ -4910,7 +5015,7 @@ export class ProxySessionController {
|
|
|
4910
5015
|
model: model.id,
|
|
4911
5016
|
effort,
|
|
4912
5017
|
mode: loaded.session.permissionMode,
|
|
4913
|
-
}, options);
|
|
5018
|
+
}, options, false);
|
|
4914
5019
|
}
|
|
4915
5020
|
|
|
4916
5021
|
private async openProviderSelector(
|
|
@@ -5003,28 +5108,22 @@ export class ProxySessionController {
|
|
|
5003
5108
|
contextKey: string,
|
|
5004
5109
|
settings: { provider: string; model: string; effort: string; mode: PermissionMode },
|
|
5005
5110
|
options: ModelCardOptions = {},
|
|
5111
|
+
announceStart = true,
|
|
5006
5112
|
): Promise<void> {
|
|
5007
5113
|
const record = options.sessionId
|
|
5008
5114
|
? this.requireSession(contextKey, options.sessionId)
|
|
5009
5115
|
: this.requireCurrentSession(contextKey);
|
|
5116
|
+
if (announceStart) {
|
|
5117
|
+
await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
5118
|
+
}
|
|
5010
5119
|
const loaded = await this.loadSession(record);
|
|
5011
5120
|
await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
5012
|
-
|
|
5013
|
-
throw new Error("当前运行时不支持 Provider 设置。");
|
|
5014
|
-
}
|
|
5015
|
-
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
|
|
5121
|
+
const session = await this.changeProviderSettings(loaded, {
|
|
5016
5122
|
modelProvider: settings.provider,
|
|
5017
5123
|
model: settings.model,
|
|
5018
5124
|
reasoningEffort: settings.effort,
|
|
5019
5125
|
permissionMode: settings.mode,
|
|
5020
5126
|
});
|
|
5021
|
-
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
5022
|
-
modelProvider: session.modelProvider ?? settings.provider,
|
|
5023
|
-
model: session.model ?? settings.model,
|
|
5024
|
-
reasoningEffort: session.reasoningEffort ?? settings.effort,
|
|
5025
|
-
permissionMode: session.permissionMode,
|
|
5026
|
-
});
|
|
5027
|
-
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
5028
5127
|
const notice = [
|
|
5029
5128
|
`Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
|
|
5030
5129
|
`模型 ${cardCode(session.model ?? settings.model)}`,
|
|
@@ -5043,6 +5142,30 @@ export class ProxySessionController {
|
|
|
5043
5142
|
}
|
|
5044
5143
|
}
|
|
5045
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
|
+
|
|
5046
5169
|
private async assertProviderModelSettings(
|
|
5047
5170
|
loaded: LoadedSession,
|
|
5048
5171
|
modelProvider: string,
|
|
@@ -5463,7 +5586,9 @@ export class ProxySessionController {
|
|
|
5463
5586
|
private async setTitle(contextKey: string, title: string): Promise<void> {
|
|
5464
5587
|
const normalizedTitle = normalizeTaskTitle(title);
|
|
5465
5588
|
if (!normalizedTitle) throw new Error("任务标题不能为空。");
|
|
5466
|
-
const
|
|
5589
|
+
const current = this.requireCurrentSession(contextKey);
|
|
5590
|
+
await this.outbound.sendText(contextKey, CHANGE_TITLE_STARTED_MESSAGE);
|
|
5591
|
+
const loaded = await this.loadSession(current);
|
|
5467
5592
|
if (loaded.runtime.setTitle) await loaded.runtime.setTitle(loaded.record.localSessionId, normalizedTitle);
|
|
5468
5593
|
else loaded.session.title = normalizedTitle;
|
|
5469
5594
|
this.store.updateRuntimeSession(loaded.record.localSessionId, { title: normalizedTitle });
|
|
@@ -5596,9 +5721,9 @@ export class ProxySessionController {
|
|
|
5596
5721
|
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
5597
5722
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
5598
5723
|
}
|
|
5599
|
-
|
|
5600
|
-
await this.
|
|
5601
|
-
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);
|
|
5602
5727
|
const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
|
|
5603
5728
|
const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
|
|
5604
5729
|
&& !completedTurnIds.has(current.lastTurnId)
|
|
@@ -5640,13 +5765,13 @@ export class ProxySessionController {
|
|
|
5640
5765
|
parentTurnId: turn.parentTurnId,
|
|
5641
5766
|
})));
|
|
5642
5767
|
const total = allTurns.length;
|
|
5643
|
-
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);
|
|
5644
5769
|
const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
|
|
5645
5770
|
const offset = page * RESET_HISTORY_PAGE_SIZE;
|
|
5646
5771
|
const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
|
|
5647
5772
|
const resettingTurnId = this.resetHistoryOperations.get(current.localSessionId);
|
|
5648
5773
|
const entries: ResetHistoryCardEntry[] = turns.flatMap((turn, index) => {
|
|
5649
|
-
const snapshot =
|
|
5774
|
+
const snapshot = turn.turnId === runningTurnId ? runningSnapshot : this.store.getTurnPromptSummary(turn.turnId);
|
|
5650
5775
|
if (!snapshot) return [];
|
|
5651
5776
|
const graph = graphRows[offset + index]!;
|
|
5652
5777
|
const summary = truncateText(
|
|
@@ -5701,7 +5826,9 @@ export class ProxySessionController {
|
|
|
5701
5826
|
const card = this.cardRenderer.renderResetHistoryCard({
|
|
5702
5827
|
entries,
|
|
5703
5828
|
footerLines: [
|
|
5704
|
-
|
|
5829
|
+
hasMoreHistory
|
|
5830
|
+
? `第 ${page + 1} 页 · 已加载 ${total} 个 turn`
|
|
5831
|
+
: runningTurn
|
|
5705
5832
|
? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
|
|
5706
5833
|
: `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
|
|
5707
5834
|
...(resettingTurnId ? ["正在 Reset 到所选轮次,请稍候…"] : []),
|
|
@@ -5911,6 +6038,7 @@ export class ProxySessionController {
|
|
|
5911
6038
|
const record = this.requireCurrentSession(contextKey);
|
|
5912
6039
|
const filePath = resolveUserPath(requestedFilePath, record.cwd);
|
|
5913
6040
|
await this.assertSendableFile(filePath);
|
|
6041
|
+
await this.outbound.sendText(contextKey, SEND_FILE_STARTED_MESSAGE);
|
|
5914
6042
|
await this.outbound.sendFile(contextKey, filePath);
|
|
5915
6043
|
}
|
|
5916
6044
|
|
|
@@ -6215,7 +6343,7 @@ export class ProxySessionController {
|
|
|
6215
6343
|
|
|
6216
6344
|
private async resolveRemoteCodexSession(
|
|
6217
6345
|
reference: string,
|
|
6218
|
-
options: { forFork?: boolean } = {},
|
|
6346
|
+
options: { forFork?: boolean; view?: "metadata" | "latest" | "latest-full" } = {},
|
|
6219
6347
|
): Promise<AgentRemoteSession> {
|
|
6220
6348
|
const scoped = parseRemoteSessionReference(reference);
|
|
6221
6349
|
const candidates = scoped
|
|
@@ -6233,7 +6361,7 @@ export class ProxySessionController {
|
|
|
6233
6361
|
runtime,
|
|
6234
6362
|
remote: options.forFork && runtime.readRemoteForkSource
|
|
6235
6363
|
? await runtime.readRemoteForkSource(remoteSessionId)
|
|
6236
|
-
: await runtime.readRemoteSession(remoteSessionId),
|
|
6364
|
+
: await runtime.readRemoteSession(remoteSessionId, options.view ?? "latest"),
|
|
6237
6365
|
} satisfies AgentRemoteSession;
|
|
6238
6366
|
}));
|
|
6239
6367
|
const matches = reads
|
|
@@ -6259,6 +6387,7 @@ export class ProxySessionController {
|
|
|
6259
6387
|
if (direct?.status === "closed") {
|
|
6260
6388
|
throw new Error(`找不到任务:${taskId}`);
|
|
6261
6389
|
}
|
|
6390
|
+
await this.outbound.sendText(contextKey, SWITCH_TASK_STARTED_MESSAGE);
|
|
6262
6391
|
const existing = direct ?? this.findStoredSessionByReference(taskId);
|
|
6263
6392
|
if (existing) {
|
|
6264
6393
|
const runtime = this.runtimes.forAgent(existing.agentName);
|
|
@@ -6367,6 +6496,10 @@ export class ProxySessionController {
|
|
|
6367
6496
|
throw new Error("任务仍有排队消息,无法归档。请先处理或取消排队消息。");
|
|
6368
6497
|
}
|
|
6369
6498
|
|
|
6499
|
+
if (options.source !== "dismiss") {
|
|
6500
|
+
await this.outbound.sendText(contextKey, ARCHIVE_TASK_STARTED_MESSAGE);
|
|
6501
|
+
}
|
|
6502
|
+
|
|
6370
6503
|
const remoteReference = local
|
|
6371
6504
|
? remoteSessionReference(local.agentName, local.remoteSessionId!)
|
|
6372
6505
|
: taskId;
|
|
@@ -6598,27 +6731,28 @@ export class ProxySessionController {
|
|
|
6598
6731
|
current = context.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
|
|
6599
6732
|
}
|
|
6600
6733
|
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
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;
|
|
6606
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.");
|
|
6607
6744
|
}
|
|
6608
|
-
} catch (error) {
|
|
6609
|
-
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
|
|
6610
6745
|
}
|
|
6611
6746
|
}
|
|
6612
|
-
|
|
6613
6747
|
const localCurrent = current;
|
|
6614
|
-
let remote: RemoteSessionSummary | undefined;
|
|
6615
6748
|
let goal: RuntimeGoal | undefined;
|
|
6616
6749
|
if (current?.remoteSessionId) {
|
|
6617
6750
|
const runtime = this.runtimes.forAgent(current.agentName);
|
|
6618
|
-
if (runtime.readRemoteSession) {
|
|
6751
|
+
if (!remote && runtime.readRemoteSession) {
|
|
6619
6752
|
try {
|
|
6620
|
-
remote = await runtime.readRemoteSession(current.remoteSessionId);
|
|
6753
|
+
remote = await runtime.readRemoteSession(current.remoteSessionId, "latest-full");
|
|
6621
6754
|
} catch (error) {
|
|
6755
|
+
if (error instanceof CodexVersionError) throw error;
|
|
6622
6756
|
this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task status.");
|
|
6623
6757
|
}
|
|
6624
6758
|
}
|
|
@@ -6729,7 +6863,7 @@ export class ProxySessionController {
|
|
|
6729
6863
|
reference: string,
|
|
6730
6864
|
options: StatusCardOptions = {},
|
|
6731
6865
|
): Promise<void> {
|
|
6732
|
-
const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
|
|
6866
|
+
const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
|
|
6733
6867
|
const actionReference = remoteSessionReference(agentName, remote.id);
|
|
6734
6868
|
const sections: CardSection[] = [
|
|
6735
6869
|
{
|
|
@@ -6891,9 +7025,10 @@ export class ProxySessionController {
|
|
|
6891
7025
|
return agent;
|
|
6892
7026
|
}
|
|
6893
7027
|
|
|
6894
|
-
private async persistAgentExecutionDefaults(localSessionId: string): Promise<void> {
|
|
6895
|
-
const
|
|
6896
|
-
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;
|
|
6897
7032
|
const agent = this.ensureAgent(session.agentName);
|
|
6898
7033
|
const observed: AgentExecutionDefaults = {
|
|
6899
7034
|
...(session.modelProvider?.trim() ? { modelProvider: session.modelProvider } : {}),
|
|
@@ -7328,6 +7463,7 @@ export class ProxySessionController {
|
|
|
7328
7463
|
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
7329
7464
|
if (!lockPath) throw new Error("当前 Agent 不支持自动关闭占用程序。");
|
|
7330
7465
|
|
|
7466
|
+
await this.outbound.sendText(contextKey, CLOSE_WRITER_STARTED_MESSAGE);
|
|
7331
7467
|
let owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
7332
7468
|
const expectedOwner = owners.length === 1 && threadWriterMatches(
|
|
7333
7469
|
owners[0],
|
|
@@ -7787,11 +7923,6 @@ function runtimeErrorMessage(error: unknown): string {
|
|
|
7787
7923
|
return error instanceof Error ? error.message : String(error);
|
|
7788
7924
|
}
|
|
7789
7925
|
|
|
7790
|
-
function isMissingRolloutError(error: unknown): boolean {
|
|
7791
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7792
|
-
return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
|
|
7793
|
-
}
|
|
7794
|
-
|
|
7795
7926
|
function isUnmaterializedCodexThreadError(error: unknown): boolean {
|
|
7796
7927
|
const message = error instanceof Error ? error.message : String(error);
|
|
7797
7928
|
return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
|