@keyou007/agent-bot 0.1.24-alpha.5 → 0.1.24-alpha.7
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 +40 -0
- package/README.md +29 -3
- package/README.zh.md +29 -3
- package/SECURITY.md +12 -0
- package/config.example.yaml +5 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/serverStatus.js +1 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +1 -0
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +247 -120
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +12 -13
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/codex/TraePlanEntry.js +70 -0
- package/dist/codex/TraePlanEntry.js.map +1 -0
- package/dist/config/AgentExecutionDefaultsStore.js +4 -0
- package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +3 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +92 -8
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +1 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +121 -17
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +57 -7
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +51 -4
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +7 -2
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +41 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +620 -214
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/proxy/SessionOperationQueue.js +14 -0
- package/dist/proxy/SessionOperationQueue.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +4 -2
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/state/StateStore.js +22 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +19 -7
- package/dist/state/TurnPreviewJournal.js.map +1 -1
- package/dist/state/migrations.js +6 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +33 -1
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +22 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/terminal/TerminalCards.js +13 -0
- package/dist/terminal/TerminalCards.js.map +1 -0
- package/dist/terminal/TerminalCommandTracker.js +74 -0
- package/dist/terminal/TerminalCommandTracker.js.map +1 -0
- package/dist/terminal/TerminalLinks.js +76 -0
- package/dist/terminal/TerminalLinks.js.map +1 -0
- package/dist/terminal/TerminalPage.js +212 -0
- package/dist/terminal/TerminalPage.js.map +1 -0
- package/dist/terminal/TerminalProcess.js +95 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +676 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +48 -0
- package/docs/technical-reference.zh.md +48 -0
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +11 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/CodexEventMapper.ts +1 -0
- package/src/codex/CodexRuntime.ts +244 -128
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/codex/TraePlanEntry.ts +63 -0
- package/src/config/AgentExecutionDefaultsStore.ts +2 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +5 -1
- package/src/feishu/CardRenderer.ts +109 -8
- package/src/feishu/FeishuConnector.ts +1 -0
- package/src/feishu/FeishuTurnPresenter.ts +108 -16
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +56 -8
- package/src/local-files/LocalFileViewerServer.ts +48 -4
- package/src/local-files/TurnPreviewPage.ts +7 -2
- package/src/presentation/TurnStateReducer.ts +46 -7
- package/src/presentation/turnViewTypes.ts +5 -0
- package/src/proxy/ProxySessionController.ts +629 -211
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +8 -3
- package/src/state/StateStore.ts +38 -1
- package/src/state/TurnPreviewJournal.ts +19 -7
- package/src/state/migrations.ts +6 -0
- package/src/supervision/SafeRestartNotifier.ts +35 -2
- package/src/supervision/SafeRestartScheduler.ts +25 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalCommandTracker.ts +74 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +93 -0
- package/src/terminal/TerminalServer.ts +551 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
3
|
import type { Logger } from "pino";
|
|
3
4
|
import type {
|
|
4
5
|
AgentRuntime,
|
|
5
6
|
AgentProcessInfo,
|
|
6
7
|
ApprovalDecision,
|
|
8
|
+
ApprovalRequest,
|
|
7
9
|
CreateRuntimeSessionInput,
|
|
8
10
|
ConversationTurn,
|
|
9
11
|
ForkRuntimeSessionInput,
|
|
@@ -33,9 +35,10 @@ import { AppServerRequestError } from "./AppServerConnection.js";
|
|
|
33
35
|
import { workspaceSandboxFromConfig, workspaceSandboxFromResponse, type TurnSandboxPolicy, type WorkspaceSandboxPolicy } from "./AppServerPermissions.js";
|
|
34
36
|
import { formatCodexError, mapCodexNotification } from "./CodexEventMapper.js";
|
|
35
37
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
38
|
+
import { hasCompletedTraePlanEntry } from "./TraePlanEntry.js";
|
|
36
39
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
37
40
|
import { threadWriterLockPath } from "./ThreadWriterProcess.js";
|
|
38
|
-
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
41
|
+
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, PROVIDER_SWITCH_EMPTY, ProviderSwitchError, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
39
42
|
import { fetchProviderModels, type ProviderModelConfig } from "./ProviderModels.js";
|
|
40
43
|
|
|
41
44
|
const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
@@ -84,6 +87,7 @@ interface CodexSession extends RuntimeSession {
|
|
|
84
87
|
activeTurnStartedAt?: number;
|
|
85
88
|
terminalTurnIds: Set<string>;
|
|
86
89
|
modeChangeRequestIds: Set<string>;
|
|
90
|
+
planContinuationTurnId?: string;
|
|
87
91
|
finalText: string;
|
|
88
92
|
generatedImagePaths: string[];
|
|
89
93
|
messagePhases: Map<string, "commentary" | "final_answer">;
|
|
@@ -91,7 +95,6 @@ interface CodexSession extends RuntimeSession {
|
|
|
91
95
|
unphasedFinalMessageId?: string;
|
|
92
96
|
needsResume: boolean;
|
|
93
97
|
workspaceSandboxPolicy?: WorkspaceSandboxPolicy;
|
|
94
|
-
canReplaceEmptyThread: boolean;
|
|
95
98
|
settingsRecoveryError?: string;
|
|
96
99
|
rolloutPath?: string;
|
|
97
100
|
turnCount?: number;
|
|
@@ -109,7 +112,12 @@ interface PendingModeChange {
|
|
|
109
112
|
turnId: string;
|
|
110
113
|
requestId: string;
|
|
111
114
|
targetMode: "plan" | "default";
|
|
115
|
+
sourceToolCallId?: string;
|
|
112
116
|
responding: boolean;
|
|
117
|
+
decision?: ApprovalDecision;
|
|
118
|
+
automatic?: boolean;
|
|
119
|
+
continued?: boolean;
|
|
120
|
+
terminalEvent?: Extract<RuntimeEvent, { type: "turn_completed" }>;
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
export class CodexRuntime implements AgentRuntime {
|
|
@@ -184,6 +192,20 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
184
192
|
return this.runSessionOperation(input.localSessionId, () => this.resumeSessionNow(input));
|
|
185
193
|
}
|
|
186
194
|
|
|
195
|
+
async prepareSessionForSettings(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
196
|
+
return this.runSessionOperation(input.localSessionId, async () => {
|
|
197
|
+
const existing = this.sessions.get(input.localSessionId);
|
|
198
|
+
if (existing) return existing;
|
|
199
|
+
const session = this.makeSession(input, {
|
|
200
|
+
thread: { id: input.remoteSessionId, name: input.title },
|
|
201
|
+
modelProvider: input.modelProvider, model: input.model,
|
|
202
|
+
}, input.reasoningEffort);
|
|
203
|
+
session.needsResume = true;
|
|
204
|
+
this.sessions.set(input.localSessionId, session);
|
|
205
|
+
return session;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
187
209
|
private async resumeSessionNow(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
188
210
|
const client = await this.client();
|
|
189
211
|
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
@@ -195,8 +217,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
195
217
|
...threadLifecycleParams(input.cwd),
|
|
196
218
|
...permissionParams(input.permissionMode),
|
|
197
219
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
220
|
+
if (response.thread.id !== input.remoteSessionId) throw new Error("App Server 返回了不同的任务 ID,未恢复会话。");
|
|
198
221
|
const reasoningEffort = await this.resolveReasoningEffort(input, response);
|
|
199
222
|
const session = this.makeSession(input, response, reasoningEffort);
|
|
223
|
+
try {
|
|
224
|
+
assertProviderSettingsApplied(response, { modelProvider: input.modelProvider });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
// Keep the attachment available for an explicit settings repair, but never send to the wrong Provider.
|
|
227
|
+
session.settingsRecoveryError = providerSwitchFailure(error);
|
|
228
|
+
session.needsResume = true;
|
|
229
|
+
}
|
|
200
230
|
this.sessions.set(input.localSessionId, session);
|
|
201
231
|
return session;
|
|
202
232
|
}
|
|
@@ -243,13 +273,18 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
243
273
|
const session = this.requireSession(sessionId);
|
|
244
274
|
const client = await this.client();
|
|
245
275
|
await this.ensureSessionResumed(session, client);
|
|
246
|
-
session.canReplaceEmptyThread = false;
|
|
247
276
|
const start = async () => client.request<{ turn: { id: string } }>("turn/start", {
|
|
248
277
|
threadId: session.remoteSessionId,
|
|
249
278
|
input: codexUserInput(prompt),
|
|
250
279
|
cwd: session.cwd,
|
|
251
280
|
model: session.model,
|
|
252
281
|
effort: session.reasoningEffort,
|
|
282
|
+
...(typeof prompt !== "string" && prompt.collaborationMode ? {
|
|
283
|
+
collaborationMode: {
|
|
284
|
+
mode: prompt.collaborationMode,
|
|
285
|
+
settings: { model: session.model, reasoning_effort: session.reasoningEffort ?? null, developer_instructions: null },
|
|
286
|
+
},
|
|
287
|
+
} : {}),
|
|
253
288
|
summary: "auto",
|
|
254
289
|
approvalPolicy: session.permissionMode === "auto" ? "never" : "on-request",
|
|
255
290
|
sandboxPolicy: await this.turnSandboxPolicy(session, client),
|
|
@@ -591,9 +626,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
591
626
|
.find((session) => session.remoteSessionId === remoteSessionId);
|
|
592
627
|
const detectedTurnId = activeThreads?.get(remoteSessionId);
|
|
593
628
|
// A turn started in this process is evidence; a restored activeTurnId alone is not.
|
|
629
|
+
const observedTurnId = runtimeSession?.activeTurnStartedAt !== undefined ? runtimeSession.activeTurnId : undefined;
|
|
594
630
|
const active = status === "active" || Boolean(activeThreads?.has(remoteSessionId))
|
|
595
|
-
|| Boolean(
|
|
596
|
-
const activeTurnId = detectedTurnId ??
|
|
631
|
+
|| Boolean(observedTurnId);
|
|
632
|
+
const activeTurnId = detectedTurnId ?? observedTurnId;
|
|
597
633
|
return {
|
|
598
634
|
active,
|
|
599
635
|
...(active && activeTurnId ? { activeTurnId } : {}),
|
|
@@ -627,6 +663,12 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
627
663
|
}
|
|
628
664
|
|
|
629
665
|
async interruptRemoteTurn(remoteSessionId: string, turnId: string, sessionId?: string): Promise<void> {
|
|
666
|
+
for (const session of this.sessions.values()) {
|
|
667
|
+
if (session.remoteSessionId === remoteSessionId) session.planContinuationTurnId = undefined;
|
|
668
|
+
}
|
|
669
|
+
for (const pending of this.modeChanges.values()) {
|
|
670
|
+
if (pending.turnId === turnId) pending.decision = "cancel";
|
|
671
|
+
}
|
|
630
672
|
await (await this.client()).request(
|
|
631
673
|
"turn/interrupt",
|
|
632
674
|
{ threadId: remoteSessionId, turnId },
|
|
@@ -645,17 +687,51 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
645
687
|
});
|
|
646
688
|
}
|
|
647
689
|
|
|
648
|
-
async archiveRemoteSession(remoteSessionId: string): Promise<void> {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
{ threadId: remoteSessionId },
|
|
652
|
-
|
|
653
|
-
|
|
690
|
+
async archiveRemoteSession(remoteSessionId: string, options: { allowUnmaterialized?: boolean } = {}): Promise<void> {
|
|
691
|
+
const client = await this.client();
|
|
692
|
+
try {
|
|
693
|
+
await client.request("thread/archive", { threadId: remoteSessionId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (!options.allowUnmaterialized || !(error instanceof AppServerRequestError)
|
|
696
|
+
|| error.method !== "thread/archive" || error.code !== -32600
|
|
697
|
+
|| error.serverMessage !== `no rollout found for thread id ${remoteSessionId}`
|
|
698
|
+
|| !await this.isUnmaterializedThread(client, remoteSessionId)) throw error;
|
|
699
|
+
// Codex has nothing to move to its archive before the first user message.
|
|
700
|
+
const result = await client.request<{ status: string }>(
|
|
701
|
+
"thread/unsubscribe", { threadId: remoteSessionId }, CONTROL_REQUEST_TIMEOUT_MS,
|
|
702
|
+
);
|
|
703
|
+
if (!["unsubscribed", "notLoaded", "notSubscribed"].includes(result.status)) {
|
|
704
|
+
throw new Error(`Cannot release empty App Server task: ${result.status}`);
|
|
705
|
+
}
|
|
706
|
+
this.logger.info({ remoteSessionId }, "Released an empty App Server task without a materialized rollout.");
|
|
707
|
+
}
|
|
654
708
|
for (const [localSessionId, session] of this.sessions) {
|
|
655
709
|
if (session.remoteSessionId === remoteSessionId) this.sessions.delete(localSessionId);
|
|
656
710
|
}
|
|
657
711
|
}
|
|
658
712
|
|
|
713
|
+
private async isUnmaterializedThread(client: AppServerClient, remoteSessionId: string): Promise<boolean> {
|
|
714
|
+
const { thread } = await client.request<ThreadReadResponse>(
|
|
715
|
+
"thread/read", { threadId: remoteSessionId, includeTurns: false }, CONTROL_REQUEST_TIMEOUT_MS,
|
|
716
|
+
);
|
|
717
|
+
if (thread.id !== remoteSessionId || thread.preview !== "" || thread.forkedFromId !== null
|
|
718
|
+
|| !["idle", "notLoaded"].includes(thread.status?.type ?? "") || !thread.path
|
|
719
|
+
|| (thread.turns?.length ?? 0) > 0) return false;
|
|
720
|
+
for (const session of this.sessions.values()) {
|
|
721
|
+
if (session.remoteSessionId === remoteSessionId && (session.activeTurnId
|
|
722
|
+
|| session.knownTurnIds.size > 0 || (session.turnCount ?? 0) > 0)) return false;
|
|
723
|
+
}
|
|
724
|
+
const active = await this.localActivityDetector?.activeThreads([remoteSessionId]);
|
|
725
|
+
if (active?.has(remoteSessionId)) return false;
|
|
726
|
+
try {
|
|
727
|
+
statSync(thread.path);
|
|
728
|
+
return false;
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
659
735
|
async setTitle(sessionId: string, title: string): Promise<void> {
|
|
660
736
|
return this.runSessionOperation(sessionId, () => this.setTitleNow(sessionId, title));
|
|
661
737
|
}
|
|
@@ -696,7 +772,6 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
696
772
|
const session = this.requireSession(sessionId);
|
|
697
773
|
const client = await this.client();
|
|
698
774
|
await this.ensureSessionResumed(session, client);
|
|
699
|
-
session.canReplaceEmptyThread = false;
|
|
700
775
|
const response = await client.request<{ goal: RuntimeGoal }>(
|
|
701
776
|
"thread/goal/set",
|
|
702
777
|
{ threadId: session.remoteSessionId, ...update },
|
|
@@ -738,7 +813,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
738
813
|
settings: RuntimeExecutionSettings,
|
|
739
814
|
persist?: (session: RuntimeSession) => Promise<void>,
|
|
740
815
|
): Promise<RuntimeSession> {
|
|
741
|
-
return this.runSessionOperation(sessionId, () =>
|
|
816
|
+
return this.runSessionOperation(sessionId, async () => {
|
|
817
|
+
try {
|
|
818
|
+
return await this.setExecutionSettingsNow(sessionId, settings, persist);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
if (error instanceof ProviderSwitchError) throw error;
|
|
821
|
+
// Errors outside the mutation block are preflight failures; no settings were changed.
|
|
822
|
+
const session = this.sessions.get(sessionId);
|
|
823
|
+
throw new ProviderSwitchError(providerSwitchFailure(error), Boolean(session && !session.needsResume && !session.settingsRecoveryError), { cause: error });
|
|
824
|
+
}
|
|
825
|
+
});
|
|
742
826
|
}
|
|
743
827
|
|
|
744
828
|
private async setExecutionSettingsNow(
|
|
@@ -756,82 +840,54 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
756
840
|
if (metadata.thread.status?.type === "active" || localActive?.has(session.remoteSessionId) || session.activeTurnId) {
|
|
757
841
|
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
758
842
|
}
|
|
759
|
-
|
|
760
|
-
if (session.canReplaceEmptyThread && !metadata.thread.forkedFromId && !metadata.thread.preview?.trim()) {
|
|
843
|
+
if (session.turnCount === 0 && !metadata.thread.forkedFromId && !metadata.thread.preview?.trim()) {
|
|
761
844
|
try {
|
|
762
845
|
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "summary");
|
|
763
|
-
|
|
764
|
-
if (!empty) session.canReplaceEmptyThread = false;
|
|
846
|
+
if (turns.length === 0) throw new Error(PROVIDER_SWITCH_EMPTY);
|
|
765
847
|
} catch (error) {
|
|
766
848
|
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list"
|
|
767
849
|
|| !/missing source rollout|no rollout found/iu.test(error.serverMessage)) throw error;
|
|
768
|
-
|
|
850
|
+
throw new Error(PROVIDER_SWITCH_EMPTY, { cause: error });
|
|
769
851
|
}
|
|
770
852
|
}
|
|
771
853
|
const previous = { ...session };
|
|
772
|
-
|
|
854
|
+
const restoreProvider = previous.modelProvider?.trim()
|
|
855
|
+
|| (metadata.thread.id === previous.remoteSessionId ? metadata.thread.modelProvider?.trim() : undefined);
|
|
773
856
|
let detached = false;
|
|
774
857
|
try {
|
|
775
858
|
const params = {
|
|
776
859
|
cwd: session.cwd, modelProvider: settings.modelProvider, model: settings.model,
|
|
777
860
|
...threadLifecycleParams(session.cwd), ...permissionParams(settings.permissionMode),
|
|
778
861
|
};
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
let response: ThreadResponse;
|
|
784
|
-
if (empty) {
|
|
785
|
-
response = await client.request<ThreadResponse>("thread/start", {
|
|
786
|
-
...params, threadSource: "user", allowProviderModelFallback: false,
|
|
787
|
-
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
788
|
-
replacementId = response.thread.id;
|
|
789
|
-
} else {
|
|
790
|
-
response = await client.request<ThreadResponse>("thread/resume", {
|
|
791
|
-
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
792
|
-
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
793
|
-
if (!providerSettingsMatch(response, settings)) {
|
|
794
|
-
this.logger.warn(
|
|
795
|
-
{
|
|
796
|
-
sessionId,
|
|
797
|
-
threadId: session.remoteSessionId,
|
|
798
|
-
requestedProvider: settings.modelProvider,
|
|
799
|
-
actualProvider: response.modelProvider,
|
|
800
|
-
requestedModel: settings.model,
|
|
801
|
-
actualModel: response.model,
|
|
802
|
-
},
|
|
803
|
-
"App Server did not apply Provider settings while resuming; forking the latest completed Turn.",
|
|
804
|
-
);
|
|
805
|
-
await this.detachThreadForSettings(client, session);
|
|
806
|
-
response = await this.forkThreadForSettings(client, session, settings);
|
|
807
|
-
replacementId = response.thread.id;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
862
|
+
await this.detachThreadForSettings(client, session, () => { detached = true; });
|
|
863
|
+
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
864
|
+
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
865
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
810
866
|
assertProviderSettingsApplied(response, settings);
|
|
811
|
-
if (
|
|
867
|
+
if (response.thread.id !== session.remoteSessionId) {
|
|
812
868
|
throw new Error("App Server 返回了不同的任务 ID,未保存设置。");
|
|
813
869
|
}
|
|
814
|
-
|
|
815
|
-
threadId: response.thread.id, name: session.title,
|
|
816
|
-
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
817
|
-
const candidate = { ...session, ...settings, remoteSessionId: response.thread.id,
|
|
870
|
+
const candidate = { ...session, ...settings,
|
|
818
871
|
workspaceSandboxPolicy: workspaceSandboxFromResponse(response.sandbox),
|
|
819
872
|
needsResume: false, settingsRecoveryError: undefined };
|
|
820
873
|
await persist?.(candidate);
|
|
821
|
-
Object.assign(session,
|
|
874
|
+
Object.assign(session, settings);
|
|
875
|
+
session.workspaceSandboxPolicy = candidate.workspaceSandboxPolicy;
|
|
876
|
+
session.needsResume = this.attachedClient !== client;
|
|
877
|
+
session.settingsRecoveryError = undefined;
|
|
822
878
|
} catch (error) {
|
|
823
|
-
if (replacementId) await this.discardUnusedThread(client, replacementId);
|
|
824
879
|
let recovery = "原任务设置未更改。";
|
|
825
880
|
if (detached) {
|
|
826
881
|
try {
|
|
882
|
+
if (!restoreProvider) throw new Error("无法确认切换前的 Provider,不能验证原设置已恢复。");
|
|
827
883
|
await this.detachThreadForSettings(client, previous);
|
|
828
884
|
const restored = await client.request<ThreadResponse>("thread/resume", {
|
|
829
885
|
threadId: previous.remoteSessionId, excludeTurns: true, cwd: previous.cwd,
|
|
830
|
-
modelProvider:
|
|
886
|
+
modelProvider: restoreProvider, model: previous.model,
|
|
831
887
|
...threadLifecycleParams(previous.cwd), ...permissionParams(previous.permissionMode),
|
|
832
888
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
833
|
-
|
|
834
|
-
modelProvider:
|
|
889
|
+
assertProviderSettingsApplied(restored, {
|
|
890
|
+
modelProvider: restoreProvider, model: previous.model,
|
|
835
891
|
});
|
|
836
892
|
if (restored.thread.id !== previous.remoteSessionId) throw new Error("恢复时返回了不同的任务 ID。");
|
|
837
893
|
session.needsResume = false;
|
|
@@ -844,60 +900,42 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
844
900
|
session.needsResume = true;
|
|
845
901
|
}
|
|
846
902
|
}
|
|
847
|
-
throw new
|
|
903
|
+
throw new ProviderSwitchError(
|
|
904
|
+
`Provider 切换失败:${providerSwitchFailure(error)} ${recovery}`,
|
|
905
|
+
!session.needsResume && !session.settingsRecoveryError, { cause: error },
|
|
906
|
+
);
|
|
848
907
|
}
|
|
849
|
-
if (replacementId) await this.discardUnusedThread(client, previous.remoteSessionId);
|
|
850
908
|
return session;
|
|
851
909
|
}
|
|
852
910
|
|
|
853
|
-
private async detachThreadForSettings(client: AppServerClient, session: CodexSession): Promise<void> {
|
|
911
|
+
private async detachThreadForSettings(client: AppServerClient, session: CodexSession, onDetach?: () => void): Promise<void> {
|
|
854
912
|
const current = await client.request<ThreadReadResponse>("thread/read", {
|
|
855
913
|
threadId: session.remoteSessionId, includeTurns: false,
|
|
856
914
|
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
857
915
|
if (session.activeTurnId || current.thread.status?.type === "active") throw new Error(PROVIDER_SWITCH_BUSY);
|
|
916
|
+
// A timeout does not cancel the remote unsubscribe, so recovery is required once it is sent.
|
|
917
|
+
onDetach?.();
|
|
858
918
|
const result = await client.request<{ status: string }>("thread/unsubscribe", {
|
|
859
919
|
threadId: session.remoteSessionId,
|
|
860
920
|
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
861
|
-
if (
|
|
862
|
-
throw new Error(
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
private async forkThreadForSettings(
|
|
867
|
-
client: AppServerClient,
|
|
868
|
-
session: CodexSession,
|
|
869
|
-
settings: RuntimeExecutionSettings,
|
|
870
|
-
): Promise<ThreadResponse> {
|
|
871
|
-
const turns = await this.listRecentThreadTurnsThroughCompleted(client, session.remoteSessionId);
|
|
872
|
-
const lastTurnId = turns.find((turn) => turn.status === "completed")?.id;
|
|
873
|
-
if (!lastTurnId) {
|
|
874
|
-
throw new Error("无法为 Provider 切换找到已完成的 Turn。请先完成一轮任务后重试。");
|
|
921
|
+
if (!["unsubscribed", "notLoaded", "notSubscribed"].includes(result.status)) {
|
|
922
|
+
throw new Error(`App Server 返回了未知的取消订阅状态:${result.status}`);
|
|
875
923
|
}
|
|
876
|
-
return client.request<ThreadResponse>("thread/fork", {
|
|
877
|
-
threadId: session.remoteSessionId,
|
|
878
|
-
lastTurnId,
|
|
879
|
-
cwd: session.cwd,
|
|
880
|
-
model: settings.model,
|
|
881
|
-
modelProvider: settings.modelProvider,
|
|
882
|
-
threadSource: "user",
|
|
883
|
-
excludeTurns: true,
|
|
884
|
-
...threadLifecycleParams(session.cwd),
|
|
885
|
-
...permissionParams(settings.permissionMode),
|
|
886
|
-
}, FORK_REQUEST_TIMEOUT_MS);
|
|
887
924
|
}
|
|
888
925
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
926
|
+
async respondToApproval(
|
|
927
|
+
sessionId: string,
|
|
928
|
+
requestId: string,
|
|
929
|
+
decision: ApprovalDecision,
|
|
930
|
+
): Promise<void> {
|
|
931
|
+
return this.resolveApproval(sessionId, requestId, decision);
|
|
895
932
|
}
|
|
896
933
|
|
|
897
|
-
async
|
|
934
|
+
private async resolveApproval(
|
|
898
935
|
sessionId: string,
|
|
899
936
|
requestId: string,
|
|
900
937
|
decision: ApprovalDecision,
|
|
938
|
+
automatic = false,
|
|
901
939
|
): Promise<void> {
|
|
902
940
|
const modeChange = this.modeChanges.get(requestId);
|
|
903
941
|
if (modeChange) {
|
|
@@ -910,20 +948,29 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
910
948
|
}
|
|
911
949
|
if (modeChange.responding) throw new Error("Approval response is already in progress.");
|
|
912
950
|
modeChange.responding = true;
|
|
951
|
+
modeChange.decision = decision;
|
|
952
|
+
modeChange.automatic = automatic;
|
|
913
953
|
try {
|
|
914
954
|
const client = await this.client();
|
|
915
955
|
if (this.modeChanges.get(requestId) !== modeChange) throw new Error("Approval request is no longer pending.");
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
956
|
+
try {
|
|
957
|
+
await client.request("mode/change/respond", {
|
|
958
|
+
threadId: this.requireSession(sessionId).remoteSessionId,
|
|
959
|
+
requestId: modeChange.requestId,
|
|
960
|
+
decision: decision === "accept" ? "approved" : decision === "decline" ? "rejected" : "cancelled",
|
|
961
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
962
|
+
} catch (error) {
|
|
963
|
+
if (!await this.reconcileCompletedPlanEntry(requestId, modeChange, error)) throw error;
|
|
964
|
+
}
|
|
921
965
|
if (this.modeChanges.get(requestId) === modeChange) {
|
|
966
|
+
this.confirmPlanContinuation(modeChange, decision);
|
|
922
967
|
this.modeChanges.delete(requestId);
|
|
923
|
-
this.emit({ type: "approval_resolved", sessionId, turnId: modeChange.turnId, requestId, decision
|
|
968
|
+
this.emit({ type: "approval_resolved", sessionId, turnId: modeChange.turnId, requestId, decision,
|
|
969
|
+
...(automatic ? { automatic: true } : {}) });
|
|
924
970
|
}
|
|
925
971
|
} finally {
|
|
926
972
|
modeChange.responding = false;
|
|
973
|
+
if (modeChange.terminalEvent) this.emit(modeChange.terminalEvent);
|
|
927
974
|
}
|
|
928
975
|
return;
|
|
929
976
|
}
|
|
@@ -934,6 +981,36 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
934
981
|
this.emit({ type: "approval_resolved", sessionId, turnId: pending.turnId, requestId, decision });
|
|
935
982
|
}
|
|
936
983
|
|
|
984
|
+
private async reconcileCompletedPlanEntry(
|
|
985
|
+
requestId: string, pending: PendingModeChange, error: unknown,
|
|
986
|
+
): Promise<boolean> {
|
|
987
|
+
const session = this.sessions.get(pending.sessionId);
|
|
988
|
+
if (!pending.automatic || pending.decision !== "accept" || pending.targetMode !== "plan"
|
|
989
|
+
|| this.provider.getAgentFamily?.() !== "traex" || !session?.rolloutPath
|
|
990
|
+
|| !(error instanceof AppServerRequestError) || error.method !== "mode/change/respond" || error.code !== -32600
|
|
991
|
+
|| error.serverMessage !== `failed to respond to mode change request: No pending mode change found for request_id: ${pending.requestId}`) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
const toolCallId = pending.sourceToolCallId ?? pending.requestId;
|
|
995
|
+
for (const waitMs of [0, 100, 200]) {
|
|
996
|
+
if (waitMs) await delay(waitMs);
|
|
997
|
+
if (this.modeChanges.get(requestId) !== pending || this.sessions.get(pending.sessionId) !== session
|
|
998
|
+
|| session.activeTurnId !== pending.turnId) return false;
|
|
999
|
+
try {
|
|
1000
|
+
if (!await hasCompletedTraePlanEntry(session.rolloutPath, pending.turnId, toolCallId)) continue;
|
|
1001
|
+
if (this.modeChanges.get(requestId) !== pending || this.sessions.get(pending.sessionId) !== session
|
|
1002
|
+
|| session.activeTurnId !== pending.turnId) return false;
|
|
1003
|
+
this.logger.info({ sessionId: pending.sessionId, turnId: pending.turnId, requestId, toolCallId },
|
|
1004
|
+
"Verified that TraeX already completed the automatic plan-mode entry.");
|
|
1005
|
+
return true;
|
|
1006
|
+
} catch (verificationError) {
|
|
1007
|
+
this.logger.debug({ error: verificationError, sessionId: pending.sessionId, requestId },
|
|
1008
|
+
"Could not verify TraeX plan-mode entry from the rollout tail.");
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
937
1014
|
async listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]> {
|
|
938
1015
|
return (await this.listModelsWithStatus(modelProvider, fallbackModel)).models;
|
|
939
1016
|
}
|
|
@@ -1174,34 +1251,47 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1174
1251
|
const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
|
|
1175
1252
|
if (session.modeChangeRequestIds.has(requestId)) return;
|
|
1176
1253
|
session.modeChangeRequestIds.add(requestId);
|
|
1177
|
-
|
|
1254
|
+
const pending: PendingModeChange = {
|
|
1178
1255
|
sessionId, turnId: mapped.turnId, requestId: mapped.requestId,
|
|
1179
1256
|
targetMode: mapped.targetMode, responding: false,
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1257
|
+
sourceToolCallId: mapped.sourceToolCallId ?? undefined,
|
|
1258
|
+
};
|
|
1259
|
+
this.modeChanges.set(requestId, pending);
|
|
1260
|
+
const automatic = session.permissionMode === "auto" && this.provider.getAgentFamily?.() === "traex";
|
|
1261
|
+
const request: ApprovalRequest = {
|
|
1262
|
+
id: requestId,
|
|
1263
|
+
kind: "mode_change",
|
|
1264
|
+
...(automatic ? { automatic: true } : {}),
|
|
1265
|
+
title: mapped.targetMode === "default" ? "确认方案并开始执行" : "确认进入规划模式",
|
|
1266
|
+
reason: [mapped.reason, ...(mapped.allowedPrompts ?? []).map((entry) => `- ${entry.tool}: ${entry.prompt}`)]
|
|
1267
|
+
.filter(Boolean).join("\n"),
|
|
1268
|
+
options: [
|
|
1269
|
+
{ id: "accept", label: mapped.targetMode === "default" ? "Approve Plan" : "Enter Plan Mode" },
|
|
1270
|
+
{ id: "decline", label: "Reject" },
|
|
1271
|
+
{ id: "cancel", label: "Cancel Request" },
|
|
1272
|
+
],
|
|
1273
|
+
};
|
|
1274
|
+
this.emit({ type: "approval_requested", sessionId, turnId: mapped.turnId, request });
|
|
1275
|
+
if (automatic) {
|
|
1276
|
+
void this.resolveApproval(sessionId, requestId, "accept", true).catch((error: unknown) => {
|
|
1277
|
+
if (this.modeChanges.get(requestId) !== pending || session.activeTurnId !== mapped.turnId) return;
|
|
1278
|
+
this.logger.warn({ error, sessionId, requestId, turnId: mapped.turnId, targetMode: mapped.targetMode,
|
|
1279
|
+
sourceToolCallId: pending.sourceToolCallId }, "Failed to automatically approve the TraeX mode change.");
|
|
1280
|
+
this.emit({ type: "approval_requested", sessionId, turnId: mapped.turnId,
|
|
1281
|
+
request: { ...request, automatic: false, reason: ["自动同意失败,请手动确认。", request.reason].filter(Boolean).join("\n\n") } });
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1196
1284
|
} else if (mapped.kind === "mode_change_resolved") {
|
|
1197
1285
|
const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
|
|
1198
1286
|
const pending = this.modeChanges.get(requestId);
|
|
1199
1287
|
if (!pending || pending.sessionId !== sessionId || pending.turnId !== mapped.turnId
|
|
1200
1288
|
|| pending.targetMode !== mapped.targetMode) return;
|
|
1289
|
+
this.confirmPlanContinuation(pending, mapped.decision === "approved" ? "accept" : "decline");
|
|
1201
1290
|
this.modeChanges.delete(requestId);
|
|
1202
1291
|
this.emit({
|
|
1203
1292
|
type: "approval_resolved", sessionId, turnId: mapped.turnId, requestId,
|
|
1204
1293
|
decision: mapped.decision === "approved" ? "accept" : mapped.decision === "rejected" ? "decline" : "cancel",
|
|
1294
|
+
...(pending.automatic && mapped.decision === "approved" ? { automatic: true } : {}),
|
|
1205
1295
|
});
|
|
1206
1296
|
} else if (mapped.kind === "token_usage") {
|
|
1207
1297
|
this.emit({
|
|
@@ -1232,6 +1322,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1232
1322
|
if (mapped.phase === "final_answer") this.promoteUnphasedMessage(session, mapped.turnId, mapped.itemId);
|
|
1233
1323
|
else if (session.unphasedFinalMessageId === mapped.itemId) session.unphasedFinalMessageId = undefined;
|
|
1234
1324
|
} else if (mapped.kind === "agent_delta") {
|
|
1325
|
+
this.clearPlanContinuation(session, mapped.turnId);
|
|
1235
1326
|
const phase = session.messagePhases.get(mapped.itemId);
|
|
1236
1327
|
if (phase !== "final_answer") {
|
|
1237
1328
|
// Providers may omit phase. Keep their text in sequence until a tool or completion disambiguates it.
|
|
@@ -1283,6 +1374,8 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1283
1374
|
} else if (mapped.kind === "plan") {
|
|
1284
1375
|
this.emit({ type: "plan_updated", sessionId, turnId: mapped.turnId, steps: mapped.steps });
|
|
1285
1376
|
} else if (mapped.kind === "tool") {
|
|
1377
|
+
// A backend that resumes tool execution itself needs no client handoff.
|
|
1378
|
+
if (mapped.phase === "started") this.clearPlanContinuation(session, mapped.turnId);
|
|
1286
1379
|
if (mapped.phase === "started") session.unphasedFinalMessageId = undefined;
|
|
1287
1380
|
if (
|
|
1288
1381
|
mapped.phase === "updated"
|
|
@@ -1409,6 +1502,8 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1409
1502
|
...threadLifecycleParams(session.cwd),
|
|
1410
1503
|
...permissionParams(session.permissionMode),
|
|
1411
1504
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
1505
|
+
if (response.thread.id !== session.remoteSessionId) throw new Error("App Server 返回了不同的任务 ID,未恢复会话。");
|
|
1506
|
+
assertProviderSettingsApplied(response, { modelProvider: session.modelProvider });
|
|
1412
1507
|
session.workspaceSandboxPolicy = workspaceSandboxFromResponse(response.sandbox);
|
|
1413
1508
|
}
|
|
1414
1509
|
|
|
@@ -1487,13 +1582,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1487
1582
|
}
|
|
1488
1583
|
|
|
1489
1584
|
private adoptTurn(session: CodexSession, turnId: string, startedAt: number): void {
|
|
1490
|
-
session.canReplaceEmptyThread = false;
|
|
1491
1585
|
if (!session.knownTurnIds.has(turnId)) {
|
|
1492
1586
|
session.knownTurnIds.add(turnId);
|
|
1493
1587
|
if (session.turnCount !== undefined) session.turnCount += 1;
|
|
1494
1588
|
}
|
|
1495
1589
|
session.activeTurnId = turnId;
|
|
1496
1590
|
session.activeTurnStartedAt = startedAt;
|
|
1591
|
+
session.planContinuationTurnId = undefined;
|
|
1497
1592
|
session.modeChangeRequestIds.clear();
|
|
1498
1593
|
session.unphasedMessages.clear();
|
|
1499
1594
|
session.unphasedFinalMessageId = undefined;
|
|
@@ -1596,7 +1691,6 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1596
1691
|
unphasedMessages: new Map(),
|
|
1597
1692
|
needsResume: false,
|
|
1598
1693
|
workspaceSandboxPolicy: workspaceSandboxFromResponse(response.sandbox),
|
|
1599
|
-
canReplaceEmptyThread: !("remoteSessionId" in input),
|
|
1600
1694
|
rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
|
|
1601
1695
|
turnCount: "remoteSessionId" in input ? undefined : 0,
|
|
1602
1696
|
knownTurnIds: new Set(),
|
|
@@ -1626,10 +1720,38 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1626
1720
|
return session;
|
|
1627
1721
|
}
|
|
1628
1722
|
|
|
1723
|
+
private clearPlanContinuation(session: CodexSession, turnId: string): void {
|
|
1724
|
+
session.planContinuationTurnId = undefined;
|
|
1725
|
+
for (const pending of this.modeChanges.values()) {
|
|
1726
|
+
if (pending.sessionId === session.localSessionId && pending.turnId === turnId && pending.responding) {
|
|
1727
|
+
pending.continued = true;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
private confirmPlanContinuation(pending: PendingModeChange, decision: ApprovalDecision): void {
|
|
1733
|
+
const session = this.sessions.get(pending.sessionId);
|
|
1734
|
+
// Do not start work approved in a different client.
|
|
1735
|
+
if (session && pending.responding && !pending.continued && pending.decision === "accept" && decision === "accept"
|
|
1736
|
+
&& pending.targetMode === "default") session.planContinuationTurnId = pending.turnId;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1629
1739
|
private emit(event: RuntimeEvent): void {
|
|
1630
1740
|
if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
|
|
1631
1741
|
for (const [requestId, pending] of this.modeChanges) {
|
|
1632
|
-
if (pending.sessionId
|
|
1742
|
+
if (pending.sessionId !== event.sessionId || pending.turnId !== event.turnId) continue;
|
|
1743
|
+
if (event.type === "turn_completed" && pending.responding && pending.decision === "accept"
|
|
1744
|
+
&& pending.targetMode === "default") {
|
|
1745
|
+
// The terminal notification may beat the approval RPC response.
|
|
1746
|
+
pending.terminalEvent = event;
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
this.modeChanges.delete(requestId);
|
|
1750
|
+
}
|
|
1751
|
+
const session = this.sessions.get(event.sessionId);
|
|
1752
|
+
if (session?.planContinuationTurnId === event.turnId) {
|
|
1753
|
+
session.planContinuationTurnId = undefined;
|
|
1754
|
+
if (event.type === "turn_completed") event = { ...event, approvedPlan: true };
|
|
1633
1755
|
}
|
|
1634
1756
|
}
|
|
1635
1757
|
for (const listener of this.listeners) listener(event);
|
|
@@ -1642,6 +1764,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1642
1764
|
this.modeChanges.clear();
|
|
1643
1765
|
for (const session of this.sessions.values()) {
|
|
1644
1766
|
session.needsResume = true;
|
|
1767
|
+
session.planContinuationTurnId = undefined;
|
|
1645
1768
|
const turnId = session.activeTurnId;
|
|
1646
1769
|
if (!turnId) continue;
|
|
1647
1770
|
session.activeTurnId = undefined;
|
|
@@ -2037,13 +2160,6 @@ function remoteThreadStatus(value: string | undefined): RemoteSessionSummary["st
|
|
|
2037
2160
|
return "not_loaded";
|
|
2038
2161
|
}
|
|
2039
2162
|
|
|
2040
|
-
function providerSettingsMatch(
|
|
2041
|
-
response: Pick<ThreadResponse, "modelProvider" | "model">,
|
|
2042
|
-
settings: RuntimeExecutionSettings,
|
|
2043
|
-
): boolean {
|
|
2044
|
-
return response.modelProvider === settings.modelProvider && response.model === settings.model;
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
2163
|
function codexSourceLabel(source: unknown): string {
|
|
2048
2164
|
if (typeof source === "string") return source;
|
|
2049
2165
|
if (isRecord(source)) {
|