@keyou007/agent-bot 0.1.24-alpha.4 → 0.1.24-alpha.6
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 +59 -0
- package/README.md +42 -6
- package/README.zh.md +42 -6
- 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/AppServerPermissions.js +34 -0
- package/dist/codex/AppServerPermissions.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +37 -5
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexRuntime.js +238 -127
- 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/ThreadWriterProcess.js +19 -5
- package/dist/codex/ThreadWriterProcess.js.map +1 -1
- 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 +146 -72
- 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 +43 -6
- package/dist/index.js.map +1 -1
- package/dist/local-files/CsvPreview.js +148 -0
- package/dist/local-files/CsvPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +104 -14
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +128 -12
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +80 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +767 -260
- 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 +1 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +19 -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/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 +71 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +639 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +53 -3
- package/docs/technical-reference.zh.md +52 -2
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +15 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/AppServerPermissions.ts +39 -0
- package/src/codex/CodexLocalActivityDetector.ts +34 -4
- package/src/codex/CodexRuntime.ts +243 -139
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/codex/ThreadWriterProcess.ts +17 -9
- 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 +159 -72
- 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 +44 -7
- package/src/local-files/CsvPreview.ts +132 -0
- package/src/local-files/LocalFileViewerServer.ts +102 -15
- package/src/local-files/TurnPreviewPage.ts +131 -13
- package/src/presentation/TurnStateReducer.ts +87 -7
- package/src/presentation/turnViewTypes.ts +9 -0
- package/src/proxy/ProxySessionController.ts +762 -259
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +10 -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 +1 -0
- package/src/supervision/SafeRestartScheduler.ts +18 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +71 -0
- package/src/terminal/TerminalServer.ts +513 -0
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
AgentRuntime,
|
|
5
5
|
AgentProcessInfo,
|
|
6
6
|
ApprovalDecision,
|
|
7
|
+
ApprovalRequest,
|
|
7
8
|
CreateRuntimeSessionInput,
|
|
8
9
|
ConversationTurn,
|
|
9
10
|
ForkRuntimeSessionInput,
|
|
@@ -30,11 +31,12 @@ import type {
|
|
|
30
31
|
import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
|
|
31
32
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
32
33
|
import { AppServerRequestError } from "./AppServerConnection.js";
|
|
34
|
+
import { workspaceSandboxFromConfig, workspaceSandboxFromResponse, type TurnSandboxPolicy, type WorkspaceSandboxPolicy } from "./AppServerPermissions.js";
|
|
33
35
|
import { formatCodexError, mapCodexNotification } from "./CodexEventMapper.js";
|
|
34
36
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
35
37
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
36
38
|
import { threadWriterLockPath } from "./ThreadWriterProcess.js";
|
|
37
|
-
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
39
|
+
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, PROVIDER_SWITCH_EMPTY, ProviderSwitchError, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
38
40
|
import { fetchProviderModels, type ProviderModelConfig } from "./ProviderModels.js";
|
|
39
41
|
|
|
40
42
|
const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
@@ -83,13 +85,14 @@ interface CodexSession extends RuntimeSession {
|
|
|
83
85
|
activeTurnStartedAt?: number;
|
|
84
86
|
terminalTurnIds: Set<string>;
|
|
85
87
|
modeChangeRequestIds: Set<string>;
|
|
88
|
+
planContinuationTurnId?: string;
|
|
86
89
|
finalText: string;
|
|
87
90
|
generatedImagePaths: string[];
|
|
88
91
|
messagePhases: Map<string, "commentary" | "final_answer">;
|
|
89
92
|
unphasedMessages: Map<string, string>;
|
|
90
93
|
unphasedFinalMessageId?: string;
|
|
91
94
|
needsResume: boolean;
|
|
92
|
-
|
|
95
|
+
workspaceSandboxPolicy?: WorkspaceSandboxPolicy;
|
|
93
96
|
settingsRecoveryError?: string;
|
|
94
97
|
rolloutPath?: string;
|
|
95
98
|
turnCount?: number;
|
|
@@ -108,6 +111,10 @@ interface PendingModeChange {
|
|
|
108
111
|
requestId: string;
|
|
109
112
|
targetMode: "plan" | "default";
|
|
110
113
|
responding: boolean;
|
|
114
|
+
decision?: ApprovalDecision;
|
|
115
|
+
automatic?: boolean;
|
|
116
|
+
continued?: boolean;
|
|
117
|
+
terminalEvent?: Extract<RuntimeEvent, { type: "turn_completed" }>;
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
export class CodexRuntime implements AgentRuntime {
|
|
@@ -138,6 +145,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
138
145
|
return this.sessions.get(localSessionId);
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
forgetSession(localSessionId: string): void {
|
|
149
|
+
this.sessions.delete(localSessionId);
|
|
150
|
+
}
|
|
151
|
+
|
|
141
152
|
getProcessInfo(): AgentProcessInfo {
|
|
142
153
|
return this.provider.getProcessInfo?.() ?? {};
|
|
143
154
|
}
|
|
@@ -178,6 +189,20 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
178
189
|
return this.runSessionOperation(input.localSessionId, () => this.resumeSessionNow(input));
|
|
179
190
|
}
|
|
180
191
|
|
|
192
|
+
async prepareSessionForSettings(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
193
|
+
return this.runSessionOperation(input.localSessionId, async () => {
|
|
194
|
+
const existing = this.sessions.get(input.localSessionId);
|
|
195
|
+
if (existing) return existing;
|
|
196
|
+
const session = this.makeSession(input, {
|
|
197
|
+
thread: { id: input.remoteSessionId, name: input.title },
|
|
198
|
+
modelProvider: input.modelProvider, model: input.model,
|
|
199
|
+
}, input.reasoningEffort);
|
|
200
|
+
session.needsResume = true;
|
|
201
|
+
this.sessions.set(input.localSessionId, session);
|
|
202
|
+
return session;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
181
206
|
private async resumeSessionNow(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
182
207
|
const client = await this.client();
|
|
183
208
|
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
@@ -189,8 +214,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
189
214
|
...threadLifecycleParams(input.cwd),
|
|
190
215
|
...permissionParams(input.permissionMode),
|
|
191
216
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
217
|
+
if (response.thread.id !== input.remoteSessionId) throw new Error("App Server 返回了不同的任务 ID,未恢复会话。");
|
|
192
218
|
const reasoningEffort = await this.resolveReasoningEffort(input, response);
|
|
193
219
|
const session = this.makeSession(input, response, reasoningEffort);
|
|
220
|
+
try {
|
|
221
|
+
assertProviderSettingsApplied(response, { modelProvider: input.modelProvider });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
// Keep the attachment available for an explicit settings repair, but never send to the wrong Provider.
|
|
224
|
+
session.settingsRecoveryError = providerSwitchFailure(error);
|
|
225
|
+
session.needsResume = true;
|
|
226
|
+
}
|
|
194
227
|
this.sessions.set(input.localSessionId, session);
|
|
195
228
|
return session;
|
|
196
229
|
}
|
|
@@ -237,15 +270,21 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
237
270
|
const session = this.requireSession(sessionId);
|
|
238
271
|
const client = await this.client();
|
|
239
272
|
await this.ensureSessionResumed(session, client);
|
|
240
|
-
|
|
241
|
-
const start = () => client.request<{ turn: { id: string } }>("turn/start", {
|
|
273
|
+
const start = async () => client.request<{ turn: { id: string } }>("turn/start", {
|
|
242
274
|
threadId: session.remoteSessionId,
|
|
243
275
|
input: codexUserInput(prompt),
|
|
244
276
|
cwd: session.cwd,
|
|
245
277
|
model: session.model,
|
|
246
278
|
effort: session.reasoningEffort,
|
|
279
|
+
...(typeof prompt !== "string" && prompt.collaborationMode ? {
|
|
280
|
+
collaborationMode: {
|
|
281
|
+
mode: prompt.collaborationMode,
|
|
282
|
+
settings: { model: session.model, reasoning_effort: session.reasoningEffort ?? null, developer_instructions: null },
|
|
283
|
+
},
|
|
284
|
+
} : {}),
|
|
247
285
|
summary: "auto",
|
|
248
286
|
approvalPolicy: session.permissionMode === "auto" ? "never" : "on-request",
|
|
287
|
+
sandboxPolicy: await this.turnSandboxPolicy(session, client),
|
|
249
288
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
250
289
|
let response: { turn: { id: string } };
|
|
251
290
|
try {
|
|
@@ -294,7 +333,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
294
333
|
return listed;
|
|
295
334
|
}
|
|
296
335
|
}));
|
|
297
|
-
const activeThreads = await this.localActivityDetector?.activeThreads(
|
|
336
|
+
const activeThreads = await this.localActivityDetector?.activeThreads(
|
|
337
|
+
sessions.filter((session) => session.status === "not_loaded").map((session) => session.id),
|
|
338
|
+
);
|
|
298
339
|
return {
|
|
299
340
|
sessions: activeThreads
|
|
300
341
|
? sessions.map((session) => markLocallyDetectedActive(session, activeThreads))
|
|
@@ -499,7 +540,8 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
499
540
|
remoteSessionId: string,
|
|
500
541
|
summary: RemoteSessionSummary,
|
|
501
542
|
): Promise<RemoteSessionSummary> {
|
|
502
|
-
const activeThreads =
|
|
543
|
+
const activeThreads = summary.status === "not_loaded"
|
|
544
|
+
? await this.localActivityDetector?.activeThreads([remoteSessionId]) : undefined;
|
|
503
545
|
const activeSummary = activeThreads ? markLocallyDetectedActive(summary, activeThreads) : summary;
|
|
504
546
|
const settings = await this.localActivityDetector?.threadSettings([remoteSessionId]);
|
|
505
547
|
return {
|
|
@@ -571,24 +613,23 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
571
613
|
}
|
|
572
614
|
|
|
573
615
|
async inspectRemoteSessionActivity(remoteSessionId: string): Promise<RemoteSessionActivity> {
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
)
|
|
580
|
-
this.localActivityDetector?.activeThreads([remoteSessionId]),
|
|
581
|
-
]);
|
|
616
|
+
const response = await (await this.client()).request<ThreadReadResponse>(
|
|
617
|
+
"thread/read", { threadId: remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS,
|
|
618
|
+
);
|
|
619
|
+
const status = remoteThreadStatus(response.thread.status?.type);
|
|
620
|
+
const activeThreads = status === "not_loaded"
|
|
621
|
+
? await this.localActivityDetector?.activeThreads([remoteSessionId]) : undefined;
|
|
582
622
|
const runtimeSession = [...this.sessions.values()]
|
|
583
623
|
.find((session) => session.remoteSessionId === remoteSessionId);
|
|
584
624
|
const detectedTurnId = activeThreads?.get(remoteSessionId);
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
625
|
+
// A turn started in this process is evidence; a restored activeTurnId alone is not.
|
|
626
|
+
const observedTurnId = runtimeSession?.activeTurnStartedAt !== undefined ? runtimeSession.activeTurnId : undefined;
|
|
627
|
+
const active = status === "active" || Boolean(activeThreads?.has(remoteSessionId))
|
|
628
|
+
|| Boolean(observedTurnId);
|
|
629
|
+
const activeTurnId = detectedTurnId ?? observedTurnId;
|
|
589
630
|
return {
|
|
590
631
|
active,
|
|
591
|
-
...(activeTurnId ? { activeTurnId } : {}),
|
|
632
|
+
...(active && activeTurnId ? { activeTurnId } : {}),
|
|
592
633
|
};
|
|
593
634
|
}
|
|
594
635
|
|
|
@@ -619,6 +660,12 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
619
660
|
}
|
|
620
661
|
|
|
621
662
|
async interruptRemoteTurn(remoteSessionId: string, turnId: string, sessionId?: string): Promise<void> {
|
|
663
|
+
for (const session of this.sessions.values()) {
|
|
664
|
+
if (session.remoteSessionId === remoteSessionId) session.planContinuationTurnId = undefined;
|
|
665
|
+
}
|
|
666
|
+
for (const pending of this.modeChanges.values()) {
|
|
667
|
+
if (pending.turnId === turnId) pending.decision = "cancel";
|
|
668
|
+
}
|
|
622
669
|
await (await this.client()).request(
|
|
623
670
|
"turn/interrupt",
|
|
624
671
|
{ threadId: remoteSessionId, turnId },
|
|
@@ -637,17 +684,51 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
637
684
|
});
|
|
638
685
|
}
|
|
639
686
|
|
|
640
|
-
async archiveRemoteSession(remoteSessionId: string): Promise<void> {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
{ threadId: remoteSessionId },
|
|
644
|
-
|
|
645
|
-
|
|
687
|
+
async archiveRemoteSession(remoteSessionId: string, options: { allowUnmaterialized?: boolean } = {}): Promise<void> {
|
|
688
|
+
const client = await this.client();
|
|
689
|
+
try {
|
|
690
|
+
await client.request("thread/archive", { threadId: remoteSessionId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (!options.allowUnmaterialized || !(error instanceof AppServerRequestError)
|
|
693
|
+
|| error.method !== "thread/archive" || error.code !== -32600
|
|
694
|
+
|| error.serverMessage !== `no rollout found for thread id ${remoteSessionId}`
|
|
695
|
+
|| !await this.isUnmaterializedThread(client, remoteSessionId)) throw error;
|
|
696
|
+
// Codex has nothing to move to its archive before the first user message.
|
|
697
|
+
const result = await client.request<{ status: string }>(
|
|
698
|
+
"thread/unsubscribe", { threadId: remoteSessionId }, CONTROL_REQUEST_TIMEOUT_MS,
|
|
699
|
+
);
|
|
700
|
+
if (!["unsubscribed", "notLoaded", "notSubscribed"].includes(result.status)) {
|
|
701
|
+
throw new Error(`Cannot release empty App Server task: ${result.status}`);
|
|
702
|
+
}
|
|
703
|
+
this.logger.info({ remoteSessionId }, "Released an empty App Server task without a materialized rollout.");
|
|
704
|
+
}
|
|
646
705
|
for (const [localSessionId, session] of this.sessions) {
|
|
647
706
|
if (session.remoteSessionId === remoteSessionId) this.sessions.delete(localSessionId);
|
|
648
707
|
}
|
|
649
708
|
}
|
|
650
709
|
|
|
710
|
+
private async isUnmaterializedThread(client: AppServerClient, remoteSessionId: string): Promise<boolean> {
|
|
711
|
+
const { thread } = await client.request<ThreadReadResponse>(
|
|
712
|
+
"thread/read", { threadId: remoteSessionId, includeTurns: false }, CONTROL_REQUEST_TIMEOUT_MS,
|
|
713
|
+
);
|
|
714
|
+
if (thread.id !== remoteSessionId || thread.preview !== "" || thread.forkedFromId !== null
|
|
715
|
+
|| !["idle", "notLoaded"].includes(thread.status?.type ?? "") || !thread.path
|
|
716
|
+
|| (thread.turns?.length ?? 0) > 0) return false;
|
|
717
|
+
for (const session of this.sessions.values()) {
|
|
718
|
+
if (session.remoteSessionId === remoteSessionId && (session.activeTurnId
|
|
719
|
+
|| session.knownTurnIds.size > 0 || (session.turnCount ?? 0) > 0)) return false;
|
|
720
|
+
}
|
|
721
|
+
const active = await this.localActivityDetector?.activeThreads([remoteSessionId]);
|
|
722
|
+
if (active?.has(remoteSessionId)) return false;
|
|
723
|
+
try {
|
|
724
|
+
statSync(thread.path);
|
|
725
|
+
return false;
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
651
732
|
async setTitle(sessionId: string, title: string): Promise<void> {
|
|
652
733
|
return this.runSessionOperation(sessionId, () => this.setTitleNow(sessionId, title));
|
|
653
734
|
}
|
|
@@ -688,7 +769,6 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
688
769
|
const session = this.requireSession(sessionId);
|
|
689
770
|
const client = await this.client();
|
|
690
771
|
await this.ensureSessionResumed(session, client);
|
|
691
|
-
session.canReplaceEmptyThread = false;
|
|
692
772
|
const response = await client.request<{ goal: RuntimeGoal }>(
|
|
693
773
|
"thread/goal/set",
|
|
694
774
|
{ threadId: session.remoteSessionId, ...update },
|
|
@@ -730,7 +810,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
730
810
|
settings: RuntimeExecutionSettings,
|
|
731
811
|
persist?: (session: RuntimeSession) => Promise<void>,
|
|
732
812
|
): Promise<RuntimeSession> {
|
|
733
|
-
return this.runSessionOperation(sessionId, () =>
|
|
813
|
+
return this.runSessionOperation(sessionId, async () => {
|
|
814
|
+
try {
|
|
815
|
+
return await this.setExecutionSettingsNow(sessionId, settings, persist);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
if (error instanceof ProviderSwitchError) throw error;
|
|
818
|
+
// Errors outside the mutation block are preflight failures; no settings were changed.
|
|
819
|
+
const session = this.sessions.get(sessionId);
|
|
820
|
+
throw new ProviderSwitchError(providerSwitchFailure(error), Boolean(session && !session.needsResume && !session.settingsRecoveryError), { cause: error });
|
|
821
|
+
}
|
|
822
|
+
});
|
|
734
823
|
}
|
|
735
824
|
|
|
736
825
|
private async setExecutionSettingsNow(
|
|
@@ -748,84 +837,58 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
748
837
|
if (metadata.thread.status?.type === "active" || localActive?.has(session.remoteSessionId) || session.activeTurnId) {
|
|
749
838
|
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
750
839
|
}
|
|
751
|
-
|
|
752
|
-
if (session.canReplaceEmptyThread && !metadata.thread.forkedFromId && !metadata.thread.preview?.trim()) {
|
|
840
|
+
if (session.turnCount === 0 && !metadata.thread.forkedFromId && !metadata.thread.preview?.trim()) {
|
|
753
841
|
try {
|
|
754
842
|
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "summary");
|
|
755
|
-
|
|
756
|
-
if (!empty) session.canReplaceEmptyThread = false;
|
|
843
|
+
if (turns.length === 0) throw new Error(PROVIDER_SWITCH_EMPTY);
|
|
757
844
|
} catch (error) {
|
|
758
845
|
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list"
|
|
759
846
|
|| !/missing source rollout|no rollout found/iu.test(error.serverMessage)) throw error;
|
|
760
|
-
|
|
847
|
+
throw new Error(PROVIDER_SWITCH_EMPTY, { cause: error });
|
|
761
848
|
}
|
|
762
849
|
}
|
|
763
850
|
const previous = { ...session };
|
|
764
|
-
|
|
851
|
+
const restoreProvider = previous.modelProvider?.trim()
|
|
852
|
+
|| (metadata.thread.id === previous.remoteSessionId ? metadata.thread.modelProvider?.trim() : undefined);
|
|
765
853
|
let detached = false;
|
|
766
854
|
try {
|
|
767
855
|
const params = {
|
|
768
856
|
cwd: session.cwd, modelProvider: settings.modelProvider, model: settings.model,
|
|
769
857
|
...threadLifecycleParams(session.cwd), ...permissionParams(settings.permissionMode),
|
|
770
858
|
};
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}
|
|
775
|
-
let response: ThreadResponse;
|
|
776
|
-
if (empty) {
|
|
777
|
-
response = await client.request<ThreadResponse>("thread/start", {
|
|
778
|
-
...params, threadSource: "user", allowProviderModelFallback: false,
|
|
779
|
-
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
780
|
-
replacementId = response.thread.id;
|
|
781
|
-
} else {
|
|
782
|
-
response = await client.request<ThreadResponse>("thread/resume", {
|
|
783
|
-
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
784
|
-
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
785
|
-
if (!providerSettingsMatch(response, settings)) {
|
|
786
|
-
this.logger.warn(
|
|
787
|
-
{
|
|
788
|
-
sessionId,
|
|
789
|
-
threadId: session.remoteSessionId,
|
|
790
|
-
requestedProvider: settings.modelProvider,
|
|
791
|
-
actualProvider: response.modelProvider,
|
|
792
|
-
requestedModel: settings.model,
|
|
793
|
-
actualModel: response.model,
|
|
794
|
-
},
|
|
795
|
-
"App Server did not apply Provider settings while resuming; forking the latest completed Turn.",
|
|
796
|
-
);
|
|
797
|
-
await this.detachThreadForSettings(client, session);
|
|
798
|
-
response = await this.forkThreadForSettings(client, session, settings);
|
|
799
|
-
replacementId = response.thread.id;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
859
|
+
await this.detachThreadForSettings(client, session, () => { detached = true; });
|
|
860
|
+
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
861
|
+
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
862
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
802
863
|
assertProviderSettingsApplied(response, settings);
|
|
803
|
-
if (
|
|
864
|
+
if (response.thread.id !== session.remoteSessionId) {
|
|
804
865
|
throw new Error("App Server 返回了不同的任务 ID,未保存设置。");
|
|
805
866
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
809
|
-
const candidate = { ...session, ...settings, remoteSessionId: response.thread.id,
|
|
867
|
+
const candidate = { ...session, ...settings,
|
|
868
|
+
workspaceSandboxPolicy: workspaceSandboxFromResponse(response.sandbox),
|
|
810
869
|
needsResume: false, settingsRecoveryError: undefined };
|
|
811
870
|
await persist?.(candidate);
|
|
812
|
-
Object.assign(session,
|
|
871
|
+
Object.assign(session, settings);
|
|
872
|
+
session.workspaceSandboxPolicy = candidate.workspaceSandboxPolicy;
|
|
873
|
+
session.needsResume = this.attachedClient !== client;
|
|
874
|
+
session.settingsRecoveryError = undefined;
|
|
813
875
|
} catch (error) {
|
|
814
|
-
if (replacementId) await this.discardUnusedThread(client, replacementId);
|
|
815
876
|
let recovery = "原任务设置未更改。";
|
|
816
877
|
if (detached) {
|
|
817
878
|
try {
|
|
879
|
+
if (!restoreProvider) throw new Error("无法确认切换前的 Provider,不能验证原设置已恢复。");
|
|
818
880
|
await this.detachThreadForSettings(client, previous);
|
|
819
881
|
const restored = await client.request<ThreadResponse>("thread/resume", {
|
|
820
882
|
threadId: previous.remoteSessionId, excludeTurns: true, cwd: previous.cwd,
|
|
821
|
-
modelProvider:
|
|
883
|
+
modelProvider: restoreProvider, model: previous.model,
|
|
822
884
|
...threadLifecycleParams(previous.cwd), ...permissionParams(previous.permissionMode),
|
|
823
885
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
824
|
-
|
|
825
|
-
modelProvider:
|
|
886
|
+
assertProviderSettingsApplied(restored, {
|
|
887
|
+
modelProvider: restoreProvider, model: previous.model,
|
|
826
888
|
});
|
|
827
889
|
if (restored.thread.id !== previous.remoteSessionId) throw new Error("恢复时返回了不同的任务 ID。");
|
|
828
890
|
session.needsResume = false;
|
|
891
|
+
session.workspaceSandboxPolicy = workspaceSandboxFromResponse(restored.sandbox);
|
|
829
892
|
session.settingsRecoveryError = undefined;
|
|
830
893
|
} catch (restoreError) {
|
|
831
894
|
this.logger.warn({ error: restoreError, sessionId }, "Failed to restore Provider after a rejected settings change.");
|
|
@@ -834,60 +897,42 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
834
897
|
session.needsResume = true;
|
|
835
898
|
}
|
|
836
899
|
}
|
|
837
|
-
throw new
|
|
900
|
+
throw new ProviderSwitchError(
|
|
901
|
+
`Provider 切换失败:${providerSwitchFailure(error)} ${recovery}`,
|
|
902
|
+
!session.needsResume && !session.settingsRecoveryError, { cause: error },
|
|
903
|
+
);
|
|
838
904
|
}
|
|
839
|
-
if (replacementId) await this.discardUnusedThread(client, previous.remoteSessionId);
|
|
840
905
|
return session;
|
|
841
906
|
}
|
|
842
907
|
|
|
843
|
-
private async detachThreadForSettings(client: AppServerClient, session: CodexSession): Promise<void> {
|
|
908
|
+
private async detachThreadForSettings(client: AppServerClient, session: CodexSession, onDetach?: () => void): Promise<void> {
|
|
844
909
|
const current = await client.request<ThreadReadResponse>("thread/read", {
|
|
845
910
|
threadId: session.remoteSessionId, includeTurns: false,
|
|
846
911
|
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
847
912
|
if (session.activeTurnId || current.thread.status?.type === "active") throw new Error(PROVIDER_SWITCH_BUSY);
|
|
913
|
+
// A timeout does not cancel the remote unsubscribe, so recovery is required once it is sent.
|
|
914
|
+
onDetach?.();
|
|
848
915
|
const result = await client.request<{ status: string }>("thread/unsubscribe", {
|
|
849
916
|
threadId: session.remoteSessionId,
|
|
850
917
|
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
851
|
-
if (
|
|
852
|
-
throw new Error(
|
|
918
|
+
if (!["unsubscribed", "notLoaded", "notSubscribed"].includes(result.status)) {
|
|
919
|
+
throw new Error(`App Server 返回了未知的取消订阅状态:${result.status}`);
|
|
853
920
|
}
|
|
854
921
|
}
|
|
855
922
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
): Promise<
|
|
861
|
-
|
|
862
|
-
const lastTurnId = turns.find((turn) => turn.status === "completed")?.id;
|
|
863
|
-
if (!lastTurnId) {
|
|
864
|
-
throw new Error("无法为 Provider 切换找到已完成的 Turn。请先完成一轮任务后重试。");
|
|
865
|
-
}
|
|
866
|
-
return client.request<ThreadResponse>("thread/fork", {
|
|
867
|
-
threadId: session.remoteSessionId,
|
|
868
|
-
lastTurnId,
|
|
869
|
-
cwd: session.cwd,
|
|
870
|
-
model: settings.model,
|
|
871
|
-
modelProvider: settings.modelProvider,
|
|
872
|
-
threadSource: "user",
|
|
873
|
-
excludeTurns: true,
|
|
874
|
-
...threadLifecycleParams(session.cwd),
|
|
875
|
-
...permissionParams(settings.permissionMode),
|
|
876
|
-
}, FORK_REQUEST_TIMEOUT_MS);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
private async discardUnusedThread(client: AppServerClient, threadId: string): Promise<void> {
|
|
880
|
-
try {
|
|
881
|
-
await client.request("thread/unsubscribe", { threadId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
882
|
-
} catch (error) {
|
|
883
|
-
this.logger.warn({ error, threadId }, "Failed to release an unused empty Thread after changing Provider.");
|
|
884
|
-
}
|
|
923
|
+
async respondToApproval(
|
|
924
|
+
sessionId: string,
|
|
925
|
+
requestId: string,
|
|
926
|
+
decision: ApprovalDecision,
|
|
927
|
+
): Promise<void> {
|
|
928
|
+
return this.resolveApproval(sessionId, requestId, decision);
|
|
885
929
|
}
|
|
886
930
|
|
|
887
|
-
async
|
|
931
|
+
private async resolveApproval(
|
|
888
932
|
sessionId: string,
|
|
889
933
|
requestId: string,
|
|
890
934
|
decision: ApprovalDecision,
|
|
935
|
+
automatic = false,
|
|
891
936
|
): Promise<void> {
|
|
892
937
|
const modeChange = this.modeChanges.get(requestId);
|
|
893
938
|
if (modeChange) {
|
|
@@ -900,6 +945,8 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
900
945
|
}
|
|
901
946
|
if (modeChange.responding) throw new Error("Approval response is already in progress.");
|
|
902
947
|
modeChange.responding = true;
|
|
948
|
+
modeChange.decision = decision;
|
|
949
|
+
modeChange.automatic = automatic;
|
|
903
950
|
try {
|
|
904
951
|
const client = await this.client();
|
|
905
952
|
if (this.modeChanges.get(requestId) !== modeChange) throw new Error("Approval request is no longer pending.");
|
|
@@ -909,11 +956,14 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
909
956
|
decision: decision === "accept" ? "approved" : decision === "decline" ? "rejected" : "cancelled",
|
|
910
957
|
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
911
958
|
if (this.modeChanges.get(requestId) === modeChange) {
|
|
959
|
+
this.confirmPlanContinuation(modeChange, decision);
|
|
912
960
|
this.modeChanges.delete(requestId);
|
|
913
|
-
this.emit({ type: "approval_resolved", sessionId, turnId: modeChange.turnId, requestId, decision
|
|
961
|
+
this.emit({ type: "approval_resolved", sessionId, turnId: modeChange.turnId, requestId, decision,
|
|
962
|
+
...(automatic ? { automatic: true } : {}) });
|
|
914
963
|
}
|
|
915
964
|
} finally {
|
|
916
965
|
modeChange.responding = false;
|
|
966
|
+
if (modeChange.terminalEvent) this.emit(modeChange.terminalEvent);
|
|
917
967
|
}
|
|
918
968
|
return;
|
|
919
969
|
}
|
|
@@ -1164,34 +1214,45 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1164
1214
|
const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
|
|
1165
1215
|
if (session.modeChangeRequestIds.has(requestId)) return;
|
|
1166
1216
|
session.modeChangeRequestIds.add(requestId);
|
|
1167
|
-
|
|
1217
|
+
const pending: PendingModeChange = {
|
|
1168
1218
|
sessionId, turnId: mapped.turnId, requestId: mapped.requestId,
|
|
1169
1219
|
targetMode: mapped.targetMode, responding: false,
|
|
1170
|
-
}
|
|
1171
|
-
this.
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1220
|
+
};
|
|
1221
|
+
this.modeChanges.set(requestId, pending);
|
|
1222
|
+
const automatic = session.permissionMode === "auto" && this.provider.getAgentFamily?.() === "traex";
|
|
1223
|
+
const request: ApprovalRequest = {
|
|
1224
|
+
id: requestId,
|
|
1225
|
+
kind: "mode_change",
|
|
1226
|
+
...(automatic ? { automatic: true } : {}),
|
|
1227
|
+
title: mapped.targetMode === "default" ? "确认方案并开始执行" : "确认进入规划模式",
|
|
1228
|
+
reason: [mapped.reason, ...(mapped.allowedPrompts ?? []).map((entry) => `- ${entry.tool}: ${entry.prompt}`)]
|
|
1229
|
+
.filter(Boolean).join("\n"),
|
|
1230
|
+
options: [
|
|
1231
|
+
{ id: "accept", label: mapped.targetMode === "default" ? "Approve Plan" : "Enter Plan Mode" },
|
|
1232
|
+
{ id: "decline", label: "Reject" },
|
|
1233
|
+
{ id: "cancel", label: "Cancel Request" },
|
|
1234
|
+
],
|
|
1235
|
+
};
|
|
1236
|
+
this.emit({ type: "approval_requested", sessionId, turnId: mapped.turnId, request });
|
|
1237
|
+
if (automatic) {
|
|
1238
|
+
void this.resolveApproval(sessionId, requestId, "accept", true).catch((error: unknown) => {
|
|
1239
|
+
this.logger.warn({ error, sessionId, requestId }, "Failed to automatically approve the TraeX mode change.");
|
|
1240
|
+
if (this.modeChanges.get(requestId) !== pending || session.activeTurnId !== mapped.turnId) return;
|
|
1241
|
+
this.emit({ type: "approval_requested", sessionId, turnId: mapped.turnId,
|
|
1242
|
+
request: { ...request, automatic: false, reason: ["自动同意失败,请手动确认。", request.reason].filter(Boolean).join("\n\n") } });
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1186
1245
|
} else if (mapped.kind === "mode_change_resolved") {
|
|
1187
1246
|
const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
|
|
1188
1247
|
const pending = this.modeChanges.get(requestId);
|
|
1189
1248
|
if (!pending || pending.sessionId !== sessionId || pending.turnId !== mapped.turnId
|
|
1190
1249
|
|| pending.targetMode !== mapped.targetMode) return;
|
|
1250
|
+
this.confirmPlanContinuation(pending, mapped.decision === "approved" ? "accept" : "decline");
|
|
1191
1251
|
this.modeChanges.delete(requestId);
|
|
1192
1252
|
this.emit({
|
|
1193
1253
|
type: "approval_resolved", sessionId, turnId: mapped.turnId, requestId,
|
|
1194
1254
|
decision: mapped.decision === "approved" ? "accept" : mapped.decision === "rejected" ? "decline" : "cancel",
|
|
1255
|
+
...(pending.automatic && mapped.decision === "approved" ? { automatic: true } : {}),
|
|
1195
1256
|
});
|
|
1196
1257
|
} else if (mapped.kind === "token_usage") {
|
|
1197
1258
|
this.emit({
|
|
@@ -1222,6 +1283,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1222
1283
|
if (mapped.phase === "final_answer") this.promoteUnphasedMessage(session, mapped.turnId, mapped.itemId);
|
|
1223
1284
|
else if (session.unphasedFinalMessageId === mapped.itemId) session.unphasedFinalMessageId = undefined;
|
|
1224
1285
|
} else if (mapped.kind === "agent_delta") {
|
|
1286
|
+
this.clearPlanContinuation(session, mapped.turnId);
|
|
1225
1287
|
const phase = session.messagePhases.get(mapped.itemId);
|
|
1226
1288
|
if (phase !== "final_answer") {
|
|
1227
1289
|
// Providers may omit phase. Keep their text in sequence until a tool or completion disambiguates it.
|
|
@@ -1273,6 +1335,8 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1273
1335
|
} else if (mapped.kind === "plan") {
|
|
1274
1336
|
this.emit({ type: "plan_updated", sessionId, turnId: mapped.turnId, steps: mapped.steps });
|
|
1275
1337
|
} else if (mapped.kind === "tool") {
|
|
1338
|
+
// A backend that resumes tool execution itself needs no client handoff.
|
|
1339
|
+
if (mapped.phase === "started") this.clearPlanContinuation(session, mapped.turnId);
|
|
1276
1340
|
if (mapped.phase === "started") session.unphasedFinalMessageId = undefined;
|
|
1277
1341
|
if (
|
|
1278
1342
|
mapped.phase === "updated"
|
|
@@ -1390,7 +1454,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1390
1454
|
}
|
|
1391
1455
|
|
|
1392
1456
|
private async resumeAppServerSession(session: CodexSession, client: AppServerClient): Promise<void> {
|
|
1393
|
-
await client.request("thread/resume", {
|
|
1457
|
+
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
1394
1458
|
threadId: session.remoteSessionId,
|
|
1395
1459
|
excludeTurns: true,
|
|
1396
1460
|
cwd: session.cwd,
|
|
@@ -1399,6 +1463,21 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1399
1463
|
...threadLifecycleParams(session.cwd),
|
|
1400
1464
|
...permissionParams(session.permissionMode),
|
|
1401
1465
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
1466
|
+
if (response.thread.id !== session.remoteSessionId) throw new Error("App Server 返回了不同的任务 ID,未恢复会话。");
|
|
1467
|
+
assertProviderSettingsApplied(response, { modelProvider: session.modelProvider });
|
|
1468
|
+
session.workspaceSandboxPolicy = workspaceSandboxFromResponse(response.sandbox);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
private async turnSandboxPolicy(session: CodexSession, client: AppServerClient): Promise<TurnSandboxPolicy> {
|
|
1472
|
+
if (session.permissionMode === "auto") return { type: "dangerFullAccess" };
|
|
1473
|
+
if (!session.workspaceSandboxPolicy) {
|
|
1474
|
+
// Resolve project-specific options once, without resuming or scanning history.
|
|
1475
|
+
const response = await client.request<{ config?: unknown }>("config/read", {
|
|
1476
|
+
cwd: session.cwd, includeLayers: false,
|
|
1477
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
1478
|
+
session.workspaceSandboxPolicy = workspaceSandboxFromConfig(response.config);
|
|
1479
|
+
}
|
|
1480
|
+
return session.workspaceSandboxPolicy;
|
|
1402
1481
|
}
|
|
1403
1482
|
|
|
1404
1483
|
private async synchronizeSessionNow(sessionId: string): Promise<RuntimeSession> {
|
|
@@ -1435,7 +1514,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1435
1514
|
}
|
|
1436
1515
|
if (latestInProgress) {
|
|
1437
1516
|
this.supersedeTurn(session, activeTurnId);
|
|
1438
|
-
|
|
1517
|
+
if (thread.status?.type === "active") {
|
|
1518
|
+
this.adoptTurn(session, latestInProgress.id, turnStartedAt(latestInProgress));
|
|
1519
|
+
}
|
|
1439
1520
|
return;
|
|
1440
1521
|
}
|
|
1441
1522
|
|
|
@@ -1462,13 +1543,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1462
1543
|
}
|
|
1463
1544
|
|
|
1464
1545
|
private adoptTurn(session: CodexSession, turnId: string, startedAt: number): void {
|
|
1465
|
-
session.canReplaceEmptyThread = false;
|
|
1466
1546
|
if (!session.knownTurnIds.has(turnId)) {
|
|
1467
1547
|
session.knownTurnIds.add(turnId);
|
|
1468
1548
|
if (session.turnCount !== undefined) session.turnCount += 1;
|
|
1469
1549
|
}
|
|
1470
1550
|
session.activeTurnId = turnId;
|
|
1471
1551
|
session.activeTurnStartedAt = startedAt;
|
|
1552
|
+
session.planContinuationTurnId = undefined;
|
|
1472
1553
|
session.modeChangeRequestIds.clear();
|
|
1473
1554
|
session.unphasedMessages.clear();
|
|
1474
1555
|
session.unphasedFinalMessageId = undefined;
|
|
@@ -1570,7 +1651,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1570
1651
|
messagePhases: new Map(),
|
|
1571
1652
|
unphasedMessages: new Map(),
|
|
1572
1653
|
needsResume: false,
|
|
1573
|
-
|
|
1654
|
+
workspaceSandboxPolicy: workspaceSandboxFromResponse(response.sandbox),
|
|
1574
1655
|
rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
|
|
1575
1656
|
turnCount: "remoteSessionId" in input ? undefined : 0,
|
|
1576
1657
|
knownTurnIds: new Set(),
|
|
@@ -1600,10 +1681,38 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1600
1681
|
return session;
|
|
1601
1682
|
}
|
|
1602
1683
|
|
|
1684
|
+
private clearPlanContinuation(session: CodexSession, turnId: string): void {
|
|
1685
|
+
session.planContinuationTurnId = undefined;
|
|
1686
|
+
for (const pending of this.modeChanges.values()) {
|
|
1687
|
+
if (pending.sessionId === session.localSessionId && pending.turnId === turnId && pending.responding) {
|
|
1688
|
+
pending.continued = true;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
private confirmPlanContinuation(pending: PendingModeChange, decision: ApprovalDecision): void {
|
|
1694
|
+
const session = this.sessions.get(pending.sessionId);
|
|
1695
|
+
// Do not start work approved in a different client.
|
|
1696
|
+
if (session && pending.responding && !pending.continued && pending.decision === "accept" && decision === "accept"
|
|
1697
|
+
&& pending.targetMode === "default") session.planContinuationTurnId = pending.turnId;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1603
1700
|
private emit(event: RuntimeEvent): void {
|
|
1604
1701
|
if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
|
|
1605
1702
|
for (const [requestId, pending] of this.modeChanges) {
|
|
1606
|
-
if (pending.sessionId
|
|
1703
|
+
if (pending.sessionId !== event.sessionId || pending.turnId !== event.turnId) continue;
|
|
1704
|
+
if (event.type === "turn_completed" && pending.responding && pending.decision === "accept"
|
|
1705
|
+
&& pending.targetMode === "default") {
|
|
1706
|
+
// The terminal notification may beat the approval RPC response.
|
|
1707
|
+
pending.terminalEvent = event;
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
this.modeChanges.delete(requestId);
|
|
1711
|
+
}
|
|
1712
|
+
const session = this.sessions.get(event.sessionId);
|
|
1713
|
+
if (session?.planContinuationTurnId === event.turnId) {
|
|
1714
|
+
session.planContinuationTurnId = undefined;
|
|
1715
|
+
if (event.type === "turn_completed") event = { ...event, approvedPlan: true };
|
|
1607
1716
|
}
|
|
1608
1717
|
}
|
|
1609
1718
|
for (const listener of this.listeners) listener(event);
|
|
@@ -1616,6 +1725,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1616
1725
|
this.modeChanges.clear();
|
|
1617
1726
|
for (const session of this.sessions.values()) {
|
|
1618
1727
|
session.needsResume = true;
|
|
1728
|
+
session.planContinuationTurnId = undefined;
|
|
1619
1729
|
const turnId = session.activeTurnId;
|
|
1620
1730
|
if (!turnId) continue;
|
|
1621
1731
|
session.activeTurnId = undefined;
|
|
@@ -1642,6 +1752,7 @@ interface ThreadResponse {
|
|
|
1642
1752
|
modelProvider?: string;
|
|
1643
1753
|
model?: string;
|
|
1644
1754
|
reasoningEffort?: string | null;
|
|
1755
|
+
sandbox?: unknown;
|
|
1645
1756
|
}
|
|
1646
1757
|
|
|
1647
1758
|
interface ThreadReadResponse {
|
|
@@ -2010,13 +2121,6 @@ function remoteThreadStatus(value: string | undefined): RemoteSessionSummary["st
|
|
|
2010
2121
|
return "not_loaded";
|
|
2011
2122
|
}
|
|
2012
2123
|
|
|
2013
|
-
function providerSettingsMatch(
|
|
2014
|
-
response: Pick<ThreadResponse, "modelProvider" | "model">,
|
|
2015
|
-
settings: RuntimeExecutionSettings,
|
|
2016
|
-
): boolean {
|
|
2017
|
-
return response.modelProvider === settings.modelProvider && response.model === settings.model;
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
2124
|
function codexSourceLabel(source: unknown): string {
|
|
2021
2125
|
if (typeof source === "string") return source;
|
|
2022
2126
|
if (isRecord(source)) {
|