@keyou007/agent-bot 0.1.20-alpha.1 → 0.1.21
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 +21 -0
- package/README.md +27 -4
- package/README.zh.md +23 -4
- 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/CodexEventMapper.js +21 -1
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexLocalActivityDetector.js +32 -10
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +56 -7
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +361 -168
- 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/ProviderModels.js +159 -0
- package/dist/codex/ProviderModels.js.map +1 -0
- package/dist/codex/ProviderSwitch.js +32 -0
- package/dist/codex/ProviderSwitch.js.map +1 -0
- package/dist/commands/CommandRouter.js +8 -3
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +2 -1
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +82 -27
- 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/FeishuTurnPresenter.js +34 -8
- package/dist/feishu/FeishuTurnPresenter.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 +3 -1
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +164 -19
- 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/local-files/TurnPreviewPage.js +725 -0
- package/dist/local-files/TurnPreviewPage.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +6 -1
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +142 -3
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +573 -325
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +65 -65
- package/dist/state/StateStore.js.map +1 -1
- package/dist/utils/formatStorageSize.js +15 -0
- package/dist/utils/formatStorageSize.js.map +1 -0
- package/dist/utils/semanticVersion.js +39 -0
- package/dist/utils/semanticVersion.js.map +1 -0
- package/docs/technical-reference.md +11 -3
- package/docs/technical-reference.zh.md +11 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +11 -1
- package/src/cli/AgentPrerequisites.ts +14 -40
- package/src/cli/DefaultAgentSelection.ts +1 -1
- package/src/cli.ts +13 -0
- package/src/codex/CodexEventMapper.ts +31 -1
- package/src/codex/CodexLocalActivityDetector.ts +41 -9
- package/src/codex/CodexProcessManager.ts +48 -7
- package/src/codex/CodexRuntime.ts +397 -204
- package/src/codex/CodexVersion.ts +18 -0
- package/src/codex/ProviderModels.ts +185 -0
- package/src/codex/ProviderSwitch.ts +36 -0
- package/src/commands/CommandRouter.ts +7 -2
- package/src/commands/commandTypes.ts +1 -1
- package/src/console/ConsoleTurnPresenter.ts +3 -0
- package/src/feishu/CardRenderer.ts +84 -43
- package/src/feishu/FeishuConnector.ts +15 -5
- package/src/feishu/FeishuTurnPresenter.ts +46 -4
- package/src/feishu/InboundText.ts +11 -0
- package/src/feishu/MergedForwardMessage.ts +15 -5
- package/src/index.ts +3 -1
- package/src/local-files/LocalFileViewerServer.ts +179 -20
- package/src/local-files/MarkdownFileLinks.ts +64 -0
- package/src/local-files/TurnPreviewPage.ts +792 -0
- package/src/presentation/OutboundRouter.ts +8 -0
- package/src/presentation/TurnStateReducer.ts +175 -2
- package/src/presentation/turnViewTypes.ts +13 -0
- package/src/proxy/ProxySessionController.ts +547 -297
- package/src/runtime/types.ts +33 -3
- package/src/state/StateStore.ts +86 -90
- package/src/utils/formatStorageSize.ts +13 -0
- package/src/utils/semanticVersion.ts +28 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
1
2
|
import type { Logger } from "pino";
|
|
2
3
|
import type {
|
|
3
4
|
AgentRuntime,
|
|
@@ -9,8 +10,10 @@ import type {
|
|
|
9
10
|
ModelProviderOption,
|
|
10
11
|
PermissionMode,
|
|
11
12
|
RemoteSessionActivity,
|
|
13
|
+
RemoteSessionMetrics,
|
|
12
14
|
RemoteSessionPage,
|
|
13
15
|
RemoteSessionSummary,
|
|
16
|
+
RemoteTurnPage,
|
|
14
17
|
ResumeRuntimeSessionInput,
|
|
15
18
|
RuntimeGoal,
|
|
16
19
|
RuntimeGoalUpdate,
|
|
@@ -28,6 +31,8 @@ import { mapCodexNotification } from "./CodexEventMapper.js";
|
|
|
28
31
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
29
32
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
30
33
|
import { threadWriterLockPath } from "./ThreadWriterProcess.js";
|
|
34
|
+
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
35
|
+
import { fetchProviderModels, type ProviderModelConfig } from "./ProviderModels.js";
|
|
31
36
|
|
|
32
37
|
const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
33
38
|
"When capturing any screenshot on Windows, use one fresh process and make it Per-Monitor DPI Aware V2 before loading System.Windows.Forms, System.Drawing, or UI Automation, and before calling any screen, window, or bounds API.",
|
|
@@ -41,14 +46,13 @@ const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
|
41
46
|
const SESSION_REQUEST_TIMEOUT_MS = 60_000;
|
|
42
47
|
const CONTROL_REQUEST_TIMEOUT_MS = 10_000;
|
|
43
48
|
const SYNC_REQUEST_TIMEOUT_MS = 5_000;
|
|
44
|
-
const THREAD_TURN_PAGE_SIZE = 100;
|
|
45
49
|
const FORK_SOURCE_TURN_PAGE_SIZE = 20;
|
|
46
50
|
const FORK_SOURCE_REQUEST_TIMEOUT_MS = 15_000;
|
|
47
51
|
const USER_RESUMABLE_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer"] as const;
|
|
52
|
+
const BUILT_IN_CODEX_PROVIDER_ID = "openai";
|
|
48
53
|
// A timed-out fork keeps running in App Server and can create an orphan thread.
|
|
49
54
|
// Wait for its response; connection closure still rejects the request.
|
|
50
55
|
const FORK_REQUEST_TIMEOUT_MS = 0;
|
|
51
|
-
type ThreadListSortKey = "recency_at" | "updated_at";
|
|
52
56
|
|
|
53
57
|
export interface AppServerClient {
|
|
54
58
|
request<T = unknown>(method: string, params?: unknown, timeoutMs?: number): Promise<T>;
|
|
@@ -62,8 +66,10 @@ export interface AppServerClient {
|
|
|
62
66
|
|
|
63
67
|
export interface AppServerClientProvider {
|
|
64
68
|
getClient(): Promise<AppServerClient>;
|
|
69
|
+
getAgentFamily?(): "codex" | "traex" | undefined;
|
|
65
70
|
getProcessInfo?(): AgentProcessInfo;
|
|
66
71
|
getCodexHome?(): string;
|
|
72
|
+
getEnvironmentVariable?(name: string): string | undefined;
|
|
67
73
|
onDisconnect?(listener: (error: Error) => void): () => void;
|
|
68
74
|
release?(): Promise<void>;
|
|
69
75
|
close(): void;
|
|
@@ -76,6 +82,11 @@ interface CodexSession extends RuntimeSession {
|
|
|
76
82
|
generatedImagePaths: string[];
|
|
77
83
|
messagePhases: Map<string, "commentary" | "final_answer">;
|
|
78
84
|
needsResume: boolean;
|
|
85
|
+
canReplaceEmptyThread: boolean;
|
|
86
|
+
settingsRecoveryError?: string;
|
|
87
|
+
rolloutPath?: string;
|
|
88
|
+
turnCount?: number;
|
|
89
|
+
knownTurnIds: Set<string>;
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
interface PendingApproval {
|
|
@@ -93,9 +104,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
93
104
|
private unsubscribe?: () => void;
|
|
94
105
|
private readonly unsubscribeDisconnect?: () => void;
|
|
95
106
|
private readonly sessionSyncs = new Map<string, Promise<RuntimeSession>>();
|
|
107
|
+
private readonly sessionOperations = new Map<string, Promise<unknown>>();
|
|
96
108
|
private readonly localActivityDetector?: CodexLocalActivityDetector;
|
|
97
109
|
private readonly codexHome?: string;
|
|
98
|
-
private threadListSortKey: ThreadListSortKey = "recency_at";
|
|
99
110
|
private releaseInFlight?: Promise<RuntimeReleaseResult>;
|
|
100
111
|
|
|
101
112
|
constructor(
|
|
@@ -120,6 +131,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
120
131
|
}
|
|
121
132
|
|
|
122
133
|
async createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
|
|
134
|
+
return this.runSessionOperation(input.localSessionId, () => this.createSessionNow(input));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async createSessionNow(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
|
|
123
138
|
const client = await this.client();
|
|
124
139
|
const response = await client.request<ThreadResponse>("thread/start", {
|
|
125
140
|
cwd: input.cwd,
|
|
@@ -144,6 +159,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
144
159
|
}
|
|
145
160
|
|
|
146
161
|
async resumeSession(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
162
|
+
return this.runSessionOperation(input.localSessionId, () => this.resumeSessionNow(input));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private async resumeSessionNow(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
147
166
|
const client = await this.client();
|
|
148
167
|
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
149
168
|
threadId: input.remoteSessionId,
|
|
@@ -161,6 +180,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
161
180
|
}
|
|
162
181
|
|
|
163
182
|
async forkSession(input: ForkRuntimeSessionInput): Promise<RuntimeSession> {
|
|
183
|
+
return this.runSessionOperation(input.localSessionId, () => this.forkSessionNow(input));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async forkSessionNow(input: ForkRuntimeSessionInput): Promise<RuntimeSession> {
|
|
164
187
|
const client = await this.client();
|
|
165
188
|
const forkParams = {
|
|
166
189
|
threadId: input.remoteSessionId,
|
|
@@ -172,20 +195,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
172
195
|
...threadLifecycleParams(input.cwd),
|
|
173
196
|
...permissionParams(input.permissionMode),
|
|
174
197
|
};
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
excludeTurns: true,
|
|
180
|
-
}, FORK_REQUEST_TIMEOUT_MS);
|
|
181
|
-
} catch (error) {
|
|
182
|
-
if (!isUnsupportedExcludeTurnsError(error)) throw error;
|
|
183
|
-
this.logger.warn(
|
|
184
|
-
{ error },
|
|
185
|
-
"App Server does not support thread/fork excludeTurns; retrying without it.",
|
|
186
|
-
);
|
|
187
|
-
response = await client.request<ThreadResponse>("thread/fork", forkParams, FORK_REQUEST_TIMEOUT_MS);
|
|
188
|
-
}
|
|
198
|
+
const response = await client.request<ThreadResponse>("thread/fork", {
|
|
199
|
+
...forkParams,
|
|
200
|
+
excludeTurns: true,
|
|
201
|
+
}, FORK_REQUEST_TIMEOUT_MS);
|
|
189
202
|
const requestedTitle = normalizeTaskTitle(input.title);
|
|
190
203
|
if (requestedTitle) {
|
|
191
204
|
await client.request("thread/name/set", {
|
|
@@ -201,9 +214,14 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
201
214
|
}
|
|
202
215
|
|
|
203
216
|
async startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string> {
|
|
217
|
+
return this.runSessionOperation(sessionId, () => this.startTurnNow(sessionId, prompt));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private async startTurnNow(sessionId: string, prompt: RuntimePrompt): Promise<string> {
|
|
204
221
|
const session = this.requireSession(sessionId);
|
|
205
222
|
const client = await this.client();
|
|
206
223
|
await this.ensureSessionResumed(session, client);
|
|
224
|
+
session.canReplaceEmptyThread = false;
|
|
207
225
|
const start = () => client.request<{ turn: { id: string } }>("turn/start", {
|
|
208
226
|
threadId: session.remoteSessionId,
|
|
209
227
|
input: codexUserInput(prompt),
|
|
@@ -249,28 +267,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
249
267
|
limit?: number;
|
|
250
268
|
} = {}): Promise<RemoteSessionPage> {
|
|
251
269
|
const client = await this.client();
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
response = await this.requestThreadList(client, input, this.threadListSortKey);
|
|
255
|
-
} catch (error) {
|
|
256
|
-
if (this.threadListSortKey !== "recency_at" || !isUnsupportedRecencySortError(error)) throw error;
|
|
257
|
-
this.threadListSortKey = "updated_at";
|
|
258
|
-
this.logger.warn(
|
|
259
|
-
{ error },
|
|
260
|
-
"App Server does not support thread/list recency_at sorting; retrying with updated_at.",
|
|
261
|
-
);
|
|
262
|
-
response = await this.requestThreadList(client, input, this.threadListSortKey);
|
|
263
|
-
}
|
|
270
|
+
const response = await this.requestThreadList(client, input);
|
|
264
271
|
const sessions = await Promise.all(response.data.map(async (thread) => {
|
|
265
272
|
const listed = remoteSessionSummary(thread);
|
|
266
273
|
if (listed.status === "active" || listed.lastTurnStatus === "inProgress") return listed;
|
|
267
274
|
try {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
{ threadId: thread.id, includeTurns: true },
|
|
271
|
-
SYNC_REQUEST_TIMEOUT_MS,
|
|
272
|
-
);
|
|
273
|
-
return mergeRemoteSessionSummary(listed, remoteSessionSummary(detail.thread));
|
|
275
|
+
const turns = await this.readLatestThreadTurns(client, thread.id, "summary");
|
|
276
|
+
return mergeRemoteSessionSummary(listed, remoteSessionSummary({ ...thread, turns }));
|
|
274
277
|
} catch {
|
|
275
278
|
return listed;
|
|
276
279
|
}
|
|
@@ -287,15 +290,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
287
290
|
private requestThreadList(
|
|
288
291
|
client: AppServerClient,
|
|
289
292
|
input: { searchTerm?: string; cursor?: string; limit?: number },
|
|
290
|
-
sortKey: ThreadListSortKey,
|
|
291
293
|
): Promise<ThreadListResponse> {
|
|
292
294
|
return client.request<ThreadListResponse>(
|
|
293
295
|
"thread/list",
|
|
294
296
|
{
|
|
295
297
|
cursor: input.cursor,
|
|
296
298
|
limit: input.limit ?? 20,
|
|
297
|
-
sortKey,
|
|
299
|
+
sortKey: this.provider.getAgentFamily?.() === "traex" ? "updated_at" : "recency_at",
|
|
298
300
|
sortDirection: "desc",
|
|
301
|
+
// Omitting this filter only lists the App Server's current Provider.
|
|
302
|
+
modelProviders: [],
|
|
299
303
|
sourceKinds: USER_RESUMABLE_THREAD_SOURCE_KINDS,
|
|
300
304
|
archived: false,
|
|
301
305
|
searchTerm: input.searchTerm,
|
|
@@ -304,10 +308,79 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
304
308
|
);
|
|
305
309
|
}
|
|
306
310
|
|
|
307
|
-
async readRemoteSession(
|
|
311
|
+
async readRemoteSession(
|
|
312
|
+
remoteSessionId: string,
|
|
313
|
+
view: "metadata" | "latest" | "latest-full" = "latest",
|
|
314
|
+
): Promise<RemoteSessionSummary> {
|
|
315
|
+
const client = await this.client();
|
|
316
|
+
const response = await client.request<ThreadReadResponse>(
|
|
317
|
+
"thread/read", { threadId: remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS,
|
|
318
|
+
);
|
|
319
|
+
const turns = view === "metadata" ? []
|
|
320
|
+
: await this.readLatestThreadTurns(client, remoteSessionId, view === "latest-full" ? "full" : "summary");
|
|
321
|
+
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary({ ...response.thread, turns }));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async readRemoteSessionMetrics(remoteSessionId: string): Promise<RemoteSessionMetrics> {
|
|
308
325
|
const client = await this.client();
|
|
309
|
-
const
|
|
310
|
-
|
|
326
|
+
const metadata = await client.request<ThreadReadResponse>(
|
|
327
|
+
"thread/read", { threadId: remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS,
|
|
328
|
+
);
|
|
329
|
+
const { turnCount, turnIds } = await this.countRemoteTurns(client, remoteSessionId);
|
|
330
|
+
const rolloutPath = stringValue(metadata.thread.path)?.trim() || undefined;
|
|
331
|
+
const loaded = [...this.sessions.values()].filter((session) => session.remoteSessionId === remoteSessionId);
|
|
332
|
+
for (const session of loaded) {
|
|
333
|
+
session.rolloutPath = rolloutPath ?? session.rolloutPath;
|
|
334
|
+
const knownTurnIds = new Set(turnIds);
|
|
335
|
+
if (session.activeTurnId && !knownTurnIds.has(session.activeTurnId)) knownTurnIds.add(session.activeTurnId);
|
|
336
|
+
session.knownTurnIds = knownTurnIds;
|
|
337
|
+
session.turnCount = knownTurnIds.size;
|
|
338
|
+
}
|
|
339
|
+
const effectivePath = rolloutPath ?? loaded[0]?.rolloutPath;
|
|
340
|
+
return {
|
|
341
|
+
turnCount: loaded[0]?.turnCount ?? turnCount,
|
|
342
|
+
storageBytes: rolloutStorageBytes(effectivePath),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async countRemoteTurns(
|
|
347
|
+
client: AppServerClient,
|
|
348
|
+
remoteSessionId: string,
|
|
349
|
+
): Promise<{ turnCount: number; turnIds: Set<string> }> {
|
|
350
|
+
const turnIds = new Set<string>();
|
|
351
|
+
const seenCursors = new Set<string>();
|
|
352
|
+
let cursor: string | undefined;
|
|
353
|
+
do {
|
|
354
|
+
const page = await this.readTurnPage(client, remoteSessionId, {
|
|
355
|
+
cursor,
|
|
356
|
+
limit: 100,
|
|
357
|
+
itemsView: "summary",
|
|
358
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
359
|
+
for (const turn of page.data) turnIds.add(turn.id);
|
|
360
|
+
const nextCursor = page.nextCursor ?? undefined;
|
|
361
|
+
if (!nextCursor) break;
|
|
362
|
+
if (nextCursor === cursor || seenCursors.has(nextCursor)) {
|
|
363
|
+
throw new Error("App Server repeated a Turn history cursor while counting task Turns.");
|
|
364
|
+
}
|
|
365
|
+
seenCursors.add(nextCursor);
|
|
366
|
+
cursor = nextCursor;
|
|
367
|
+
} while (cursor);
|
|
368
|
+
return { turnCount: turnIds.size, turnIds };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async listRemoteTurnSummaries(
|
|
372
|
+
remoteSessionId: string,
|
|
373
|
+
input: { cursor?: string; limit: number },
|
|
374
|
+
): Promise<RemoteTurnPage> {
|
|
375
|
+
const response = await this.readTurnPage(await this.client(), remoteSessionId, {
|
|
376
|
+
cursor: input.cursor,
|
|
377
|
+
limit: Math.max(1, Math.min(100, Math.trunc(input.limit))),
|
|
378
|
+
itemsView: "summary",
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
turns: remoteSessionSummary({ id: remoteSessionId, turns: response.data }).completedTurns ?? [],
|
|
382
|
+
nextCursor: response.nextCursor ?? undefined,
|
|
383
|
+
};
|
|
311
384
|
}
|
|
312
385
|
|
|
313
386
|
async readRemoteForkSource(remoteSessionId: string): Promise<RemoteSessionSummary> {
|
|
@@ -317,17 +390,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
317
390
|
{ threadId: remoteSessionId, includeTurns: false },
|
|
318
391
|
FORK_SOURCE_REQUEST_TIMEOUT_MS,
|
|
319
392
|
);
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
turns = await this.listRecentThreadTurnsThroughCompleted(client, remoteSessionId);
|
|
323
|
-
} catch (error) {
|
|
324
|
-
if (!isUnsupportedThreadTurnsListError(error)) throw error;
|
|
325
|
-
this.logger.warn(
|
|
326
|
-
{ error, remoteSessionId },
|
|
327
|
-
"App Server does not support lightweight Fork source reads; falling back to complete thread history.",
|
|
328
|
-
);
|
|
329
|
-
return this.readRemoteSession(remoteSessionId);
|
|
330
|
-
}
|
|
393
|
+
const turns = await this.listRecentThreadTurnsThroughCompleted(client, remoteSessionId);
|
|
331
394
|
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary({
|
|
332
395
|
...metadata.thread,
|
|
333
396
|
turns,
|
|
@@ -355,17 +418,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
355
418
|
const seenCursors = new Set<string>();
|
|
356
419
|
let cursor: string | undefined;
|
|
357
420
|
do {
|
|
358
|
-
const response = await
|
|
359
|
-
"
|
|
360
|
-
|
|
361
|
-
threadId: remoteSessionId,
|
|
362
|
-
...(cursor ? { cursor } : {}),
|
|
363
|
-
limit: FORK_SOURCE_TURN_PAGE_SIZE,
|
|
364
|
-
sortDirection: "desc",
|
|
365
|
-
itemsView: "summary",
|
|
366
|
-
},
|
|
367
|
-
FORK_SOURCE_REQUEST_TIMEOUT_MS,
|
|
368
|
-
);
|
|
421
|
+
const response = await this.readTurnPage(client, remoteSessionId, {
|
|
422
|
+
cursor, limit: FORK_SOURCE_TURN_PAGE_SIZE, itemsView: "summary",
|
|
423
|
+
}, FORK_SOURCE_REQUEST_TIMEOUT_MS);
|
|
369
424
|
turns.push(...response.data);
|
|
370
425
|
const completedIndex = turns.findIndex((turn) => turn.status === "completed");
|
|
371
426
|
if (completedIndex >= 0) {
|
|
@@ -387,69 +442,34 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
387
442
|
return turns.reverse();
|
|
388
443
|
}
|
|
389
444
|
|
|
390
|
-
private async
|
|
445
|
+
private async readLatestThreadTurns(
|
|
391
446
|
client: AppServerClient,
|
|
392
447
|
remoteSessionId: string,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
{ threadId: remoteSessionId, includeTurns: true },
|
|
398
|
-
SYNC_REQUEST_TIMEOUT_MS,
|
|
399
|
-
);
|
|
400
|
-
} catch (error) {
|
|
401
|
-
const paginated = isPaginatedThreadReadError(error);
|
|
402
|
-
if (!paginated && !isUnmaterializedThreadReadError(error)) throw error;
|
|
403
|
-
|
|
404
|
-
const metadata = await client.request<ThreadReadResponse>(
|
|
405
|
-
"thread/read",
|
|
406
|
-
{ threadId: remoteSessionId, includeTurns: false },
|
|
407
|
-
SYNC_REQUEST_TIMEOUT_MS,
|
|
408
|
-
);
|
|
409
|
-
if (!paginated) return { thread: { ...metadata.thread, turns: [] } };
|
|
410
|
-
|
|
411
|
-
return {
|
|
412
|
-
thread: {
|
|
413
|
-
...metadata.thread,
|
|
414
|
-
turns: await this.listThreadTurns(client, remoteSessionId),
|
|
415
|
-
},
|
|
416
|
-
};
|
|
417
|
-
}
|
|
448
|
+
itemsView: "summary" | "full",
|
|
449
|
+
): Promise<CodexTurnSnapshot[]> {
|
|
450
|
+
const response = await this.readTurnPage(client, remoteSessionId, { limit: 1, itemsView });
|
|
451
|
+
return response.data;
|
|
418
452
|
}
|
|
419
453
|
|
|
420
|
-
private async
|
|
454
|
+
private async readTurnPage(
|
|
421
455
|
client: AppServerClient,
|
|
422
456
|
remoteSessionId: string,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
},
|
|
437
|
-
SYNC_REQUEST_TIMEOUT_MS,
|
|
438
|
-
);
|
|
439
|
-
turns.push(...response.data);
|
|
440
|
-
const nextCursor = response.nextCursor ?? undefined;
|
|
441
|
-
if (!nextCursor) break;
|
|
442
|
-
if (seenCursors.has(nextCursor)) {
|
|
443
|
-
this.logger.warn(
|
|
444
|
-
{ remoteSessionId, cursor: nextCursor },
|
|
445
|
-
"App Server repeated a thread Turn pagination cursor; stopping pagination.",
|
|
446
|
-
);
|
|
447
|
-
break;
|
|
457
|
+
input: { cursor?: string; limit: number; itemsView: "summary" | "full" },
|
|
458
|
+
timeoutMs = SYNC_REQUEST_TIMEOUT_MS,
|
|
459
|
+
): Promise<ThreadTurnsListResponse> {
|
|
460
|
+
try {
|
|
461
|
+
return await client.request<ThreadTurnsListResponse>("thread/turns/list", {
|
|
462
|
+
threadId: remoteSessionId,
|
|
463
|
+
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
464
|
+
limit: input.limit, sortDirection: "desc", itemsView: input.itemsView,
|
|
465
|
+
}, timeoutMs);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (error instanceof AppServerRequestError && error.method === "thread/turns/list"
|
|
468
|
+
&& /not materialized yet; thread\/turns\/list is unavailable before first user message/i.test(error.serverMessage)) {
|
|
469
|
+
return { data: [], nextCursor: null };
|
|
448
470
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
} while (cursor);
|
|
452
|
-
return turns;
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
453
473
|
}
|
|
454
474
|
|
|
455
475
|
async inspectRemoteSessionActivity(remoteSessionId: string): Promise<RemoteSessionActivity> {
|
|
@@ -477,7 +497,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
477
497
|
async synchronizeSession(sessionId: string): Promise<RuntimeSession> {
|
|
478
498
|
const existing = this.sessionSyncs.get(sessionId);
|
|
479
499
|
if (existing) return existing;
|
|
480
|
-
const synchronization = this.synchronizeSessionNow(sessionId);
|
|
500
|
+
const synchronization = this.runSessionOperation(sessionId, () => this.synchronizeSessionNow(sessionId));
|
|
481
501
|
this.sessionSyncs.set(sessionId, synchronization);
|
|
482
502
|
try {
|
|
483
503
|
return await synchronization;
|
|
@@ -513,8 +533,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
513
533
|
}
|
|
514
534
|
|
|
515
535
|
async closeSession(sessionId: string): Promise<void> {
|
|
516
|
-
|
|
517
|
-
|
|
536
|
+
await this.runSessionOperation(sessionId, async () => {
|
|
537
|
+
const session = this.requireSession(sessionId);
|
|
538
|
+
await this.archiveRemoteSession(session.remoteSessionId);
|
|
539
|
+
});
|
|
518
540
|
}
|
|
519
541
|
|
|
520
542
|
async archiveRemoteSession(remoteSessionId: string): Promise<void> {
|
|
@@ -529,6 +551,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
529
551
|
}
|
|
530
552
|
|
|
531
553
|
async setTitle(sessionId: string, title: string): Promise<void> {
|
|
554
|
+
return this.runSessionOperation(sessionId, () => this.setTitleNow(sessionId, title));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private async setTitleNow(sessionId: string, title: string): Promise<void> {
|
|
532
558
|
const session = this.requireSession(sessionId);
|
|
533
559
|
const normalizedTitle = normalizeTaskTitle(title);
|
|
534
560
|
if (!normalizedTitle) throw new Error("任务标题不能为空。");
|
|
@@ -541,6 +567,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
541
567
|
}
|
|
542
568
|
|
|
543
569
|
async getGoal(sessionId: string): Promise<RuntimeGoal | undefined> {
|
|
570
|
+
return this.runSessionOperation(sessionId, () => this.getGoalNow(sessionId));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private async getGoalNow(sessionId: string): Promise<RuntimeGoal | undefined> {
|
|
544
574
|
const session = this.requireSession(sessionId);
|
|
545
575
|
const client = await this.client();
|
|
546
576
|
await this.ensureSessionResumed(session, client);
|
|
@@ -553,9 +583,14 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
553
583
|
}
|
|
554
584
|
|
|
555
585
|
async setGoal(sessionId: string, update: RuntimeGoalUpdate): Promise<RuntimeGoal> {
|
|
586
|
+
return this.runSessionOperation(sessionId, () => this.setGoalNow(sessionId, update));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private async setGoalNow(sessionId: string, update: RuntimeGoalUpdate): Promise<RuntimeGoal> {
|
|
556
590
|
const session = this.requireSession(sessionId);
|
|
557
591
|
const client = await this.client();
|
|
558
592
|
await this.ensureSessionResumed(session, client);
|
|
593
|
+
session.canReplaceEmptyThread = false;
|
|
559
594
|
const response = await client.request<{ goal: RuntimeGoal }>(
|
|
560
595
|
"thread/goal/set",
|
|
561
596
|
{ threadId: session.remoteSessionId, ...update },
|
|
@@ -565,6 +600,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
565
600
|
}
|
|
566
601
|
|
|
567
602
|
async clearGoal(sessionId: string): Promise<boolean> {
|
|
603
|
+
return this.runSessionOperation(sessionId, () => this.clearGoalNow(sessionId));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private async clearGoalNow(sessionId: string): Promise<boolean> {
|
|
568
607
|
const session = this.requireSession(sessionId);
|
|
569
608
|
const client = await this.client();
|
|
570
609
|
await this.ensureSessionResumed(session, client);
|
|
@@ -577,39 +616,132 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
577
616
|
}
|
|
578
617
|
|
|
579
618
|
async setModel(sessionId: string, model: string): Promise<void> {
|
|
580
|
-
this.requireSession(sessionId).model = model;
|
|
619
|
+
await this.runSessionOperation(sessionId, async () => { this.requireSession(sessionId).model = model; });
|
|
581
620
|
}
|
|
582
621
|
|
|
583
622
|
async setReasoningEffort(sessionId: string, effort: string): Promise<void> {
|
|
584
|
-
this.requireSession(sessionId).reasoningEffort = effort;
|
|
623
|
+
await this.runSessionOperation(sessionId, async () => { this.requireSession(sessionId).reasoningEffort = effort; });
|
|
585
624
|
}
|
|
586
625
|
|
|
587
626
|
async setPermissionMode(sessionId: string, mode: PermissionMode): Promise<void> {
|
|
588
|
-
this.requireSession(sessionId).permissionMode = mode;
|
|
627
|
+
await this.runSessionOperation(sessionId, async () => { this.requireSession(sessionId).permissionMode = mode; });
|
|
589
628
|
}
|
|
590
629
|
|
|
591
630
|
async setExecutionSettings(
|
|
592
631
|
sessionId: string,
|
|
593
632
|
settings: RuntimeExecutionSettings,
|
|
633
|
+
persist?: (session: RuntimeSession) => Promise<void>,
|
|
634
|
+
): Promise<RuntimeSession> {
|
|
635
|
+
return this.runSessionOperation(sessionId, () => this.setExecutionSettingsNow(sessionId, settings, persist));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private async setExecutionSettingsNow(
|
|
639
|
+
sessionId: string,
|
|
640
|
+
settings: RuntimeExecutionSettings,
|
|
641
|
+
persist?: (session: RuntimeSession) => Promise<void>,
|
|
594
642
|
): Promise<RuntimeSession> {
|
|
595
643
|
const session = this.requireSession(sessionId);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
session.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
644
|
+
if (session.activeTurnId) throw new Error(PROVIDER_SWITCH_BUSY);
|
|
645
|
+
const client = await this.client();
|
|
646
|
+
const metadata = await client.request<ThreadReadResponse>("thread/read", {
|
|
647
|
+
threadId: session.remoteSessionId, includeTurns: false,
|
|
648
|
+
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
649
|
+
const localActive = await this.localActivityDetector?.activeThreads([session.remoteSessionId]);
|
|
650
|
+
if (metadata.thread.status?.type === "active" || localActive?.has(session.remoteSessionId) || session.activeTurnId) {
|
|
651
|
+
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
652
|
+
}
|
|
653
|
+
let empty = false;
|
|
654
|
+
if (session.canReplaceEmptyThread && !metadata.thread.forkedFromId && !metadata.thread.preview?.trim()) {
|
|
655
|
+
try {
|
|
656
|
+
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "summary");
|
|
657
|
+
empty = turns.length === 0;
|
|
658
|
+
if (!empty) session.canReplaceEmptyThread = false;
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list"
|
|
661
|
+
|| !/missing source rollout|no rollout found/iu.test(error.serverMessage)) throw error;
|
|
662
|
+
empty = true;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const previous = { ...session };
|
|
666
|
+
let replacementId: string | undefined;
|
|
667
|
+
let detached = false;
|
|
668
|
+
try {
|
|
669
|
+
const params = {
|
|
670
|
+
cwd: session.cwd, modelProvider: settings.modelProvider, model: settings.model,
|
|
671
|
+
...threadLifecycleParams(session.cwd), ...permissionParams(settings.permissionMode),
|
|
672
|
+
};
|
|
673
|
+
if (!empty) {
|
|
674
|
+
await this.detachThreadForSettings(client, session);
|
|
675
|
+
detached = true;
|
|
676
|
+
}
|
|
677
|
+
const response = empty
|
|
678
|
+
? await client.request<ThreadResponse>("thread/start", {
|
|
679
|
+
...params, threadSource: "user", allowProviderModelFallback: false,
|
|
680
|
+
}, SESSION_REQUEST_TIMEOUT_MS)
|
|
681
|
+
: await client.request<ThreadResponse>("thread/resume", {
|
|
682
|
+
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
683
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
684
|
+
if (empty) replacementId = response.thread.id;
|
|
685
|
+
assertProviderSettingsApplied(response, settings);
|
|
686
|
+
if (!empty && response.thread.id !== session.remoteSessionId) throw new Error("App Server 返回了不同的任务 ID,未保存设置。");
|
|
687
|
+
if (empty && session.title) await client.request("thread/name/set", {
|
|
688
|
+
threadId: response.thread.id, name: session.title,
|
|
689
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
690
|
+
const candidate = { ...session, ...settings, remoteSessionId: response.thread.id,
|
|
691
|
+
needsResume: false, settingsRecoveryError: undefined };
|
|
692
|
+
await persist?.(candidate);
|
|
693
|
+
Object.assign(session, candidate);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (replacementId) await this.discardUnusedThread(client, replacementId);
|
|
696
|
+
let recovery = "原任务设置未更改。";
|
|
697
|
+
if (detached) {
|
|
698
|
+
try {
|
|
699
|
+
await this.detachThreadForSettings(client, previous);
|
|
700
|
+
const restored = await client.request<ThreadResponse>("thread/resume", {
|
|
701
|
+
threadId: previous.remoteSessionId, excludeTurns: true, cwd: previous.cwd,
|
|
702
|
+
modelProvider: previous.modelProvider, model: previous.model,
|
|
703
|
+
...threadLifecycleParams(previous.cwd), ...permissionParams(previous.permissionMode),
|
|
704
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
705
|
+
if (previous.modelProvider && previous.model) assertProviderSettingsApplied(restored, {
|
|
706
|
+
modelProvider: previous.modelProvider, model: previous.model,
|
|
707
|
+
});
|
|
708
|
+
if (restored.thread.id !== previous.remoteSessionId) throw new Error("恢复时返回了不同的任务 ID。");
|
|
709
|
+
session.needsResume = false;
|
|
710
|
+
session.settingsRecoveryError = undefined;
|
|
711
|
+
} catch (restoreError) {
|
|
712
|
+
this.logger.warn({ error: restoreError, sessionId }, "Failed to restore Provider after a rejected settings change.");
|
|
713
|
+
recovery = "原设置仍保留,但远端恢复失败;请重新切换 Provider,确认成功后再发送消息。";
|
|
714
|
+
session.settingsRecoveryError = recovery;
|
|
715
|
+
session.needsResume = true;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
throw new Error(`Provider 切换失败:${providerSwitchFailure(error)} ${recovery}`, { cause: error });
|
|
719
|
+
}
|
|
720
|
+
if (replacementId) await this.discardUnusedThread(client, previous.remoteSessionId);
|
|
610
721
|
return session;
|
|
611
722
|
}
|
|
612
723
|
|
|
724
|
+
private async detachThreadForSettings(client: AppServerClient, session: CodexSession): Promise<void> {
|
|
725
|
+
const current = await client.request<ThreadReadResponse>("thread/read", {
|
|
726
|
+
threadId: session.remoteSessionId, includeTurns: false,
|
|
727
|
+
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
728
|
+
if (session.activeTurnId || current.thread.status?.type === "active") throw new Error(PROVIDER_SWITCH_BUSY);
|
|
729
|
+
const result = await client.request<{ status: string }>("thread/unsubscribe", {
|
|
730
|
+
threadId: session.remoteSessionId,
|
|
731
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
732
|
+
if (result.status !== "unsubscribed" && result.status !== "notLoaded") {
|
|
733
|
+
throw new Error("当前任务仍由其他客户端加载,无法安全切换 Provider。请先在原客户端释放任务。");
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private async discardUnusedThread(client: AppServerClient, threadId: string): Promise<void> {
|
|
738
|
+
try {
|
|
739
|
+
await client.request("thread/unsubscribe", { threadId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
this.logger.warn({ error, threadId }, "Failed to release an unused empty Thread after changing Provider.");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
613
745
|
async respondToApproval(
|
|
614
746
|
sessionId: string,
|
|
615
747
|
requestId: string,
|
|
@@ -622,7 +754,43 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
622
754
|
this.emit({ type: "approval_resolved", sessionId, turnId: pending.turnId, requestId, decision });
|
|
623
755
|
}
|
|
624
756
|
|
|
625
|
-
async listModels(): Promise<ModelOption[]> {
|
|
757
|
+
async listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]> {
|
|
758
|
+
const requestedProvider = modelProvider?.trim();
|
|
759
|
+
if (!requestedProvider
|
|
760
|
+
|| (requestedProvider === BUILT_IN_CODEX_PROVIDER_ID && this.provider.getAgentFamily?.() !== "traex")) {
|
|
761
|
+
return this.listCatalogModels();
|
|
762
|
+
}
|
|
763
|
+
const config = await this.readConfig();
|
|
764
|
+
const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
|
|
765
|
+
const providerConfig = configuredProviders[requestedProvider];
|
|
766
|
+
let catalog: ModelOption[] = [];
|
|
767
|
+
try {
|
|
768
|
+
catalog = await this.listCatalogModels();
|
|
769
|
+
} catch (error) {
|
|
770
|
+
this.logger.warn({ error, modelProvider: requestedProvider },
|
|
771
|
+
"Failed to read the App Server model catalog while discovering Provider models.");
|
|
772
|
+
}
|
|
773
|
+
const configuredDefaultModel = stringValue(config.model_provider)?.trim() === requestedProvider
|
|
774
|
+
? stringValue(config.model)?.trim()
|
|
775
|
+
: undefined;
|
|
776
|
+
try {
|
|
777
|
+
return await fetchProviderModels({
|
|
778
|
+
providerId: requestedProvider,
|
|
779
|
+
config: (isRecord(providerConfig) ? providerConfig : {}) as ProviderModelConfig,
|
|
780
|
+
catalog,
|
|
781
|
+
configuredDefaultModel,
|
|
782
|
+
environmentValue: (name) => this.provider.getEnvironmentVariable?.(name) ?? process.env[name],
|
|
783
|
+
});
|
|
784
|
+
} catch (error) {
|
|
785
|
+
const fallback = fallbackProviderModel(catalog, fallbackModel, configuredDefaultModel);
|
|
786
|
+
if (!fallback) throw error;
|
|
787
|
+
this.logger.warn({ error, modelProvider: requestedProvider, fallbackModel: fallback.id },
|
|
788
|
+
"Provider model discovery failed; using the current or configured model as a fallback.");
|
|
789
|
+
return [fallback];
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private async listCatalogModels(): Promise<ModelOption[]> {
|
|
626
794
|
const response = await (await this.client()).request<{
|
|
627
795
|
data: Array<{
|
|
628
796
|
id: string;
|
|
@@ -648,12 +816,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
648
816
|
}
|
|
649
817
|
|
|
650
818
|
async listModelProviders(): Promise<ModelProviderOption[]> {
|
|
651
|
-
const
|
|
652
|
-
"config/read",
|
|
653
|
-
{},
|
|
654
|
-
CONTROL_REQUEST_TIMEOUT_MS,
|
|
655
|
-
);
|
|
656
|
-
const config = response.config ?? {};
|
|
819
|
+
const config = await this.readConfig();
|
|
657
820
|
const defaultProvider = stringValue(config.model_provider)?.trim();
|
|
658
821
|
const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
|
|
659
822
|
const providers = new Map<string, ModelProviderOption>();
|
|
@@ -666,6 +829,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
666
829
|
...(id === defaultProvider ? { isDefault: true } : {}),
|
|
667
830
|
});
|
|
668
831
|
}
|
|
832
|
+
if (this.provider.getAgentFamily?.() === "codex" && !providers.has(BUILT_IN_CODEX_PROVIDER_ID)) {
|
|
833
|
+
providers.set(BUILT_IN_CODEX_PROVIDER_ID, {
|
|
834
|
+
id: BUILT_IN_CODEX_PROVIDER_ID,
|
|
835
|
+
displayName: "OpenAI",
|
|
836
|
+
...(!defaultProvider ? { isDefault: true } : {}),
|
|
837
|
+
});
|
|
838
|
+
}
|
|
669
839
|
if (defaultProvider && !providers.has(defaultProvider)) {
|
|
670
840
|
providers.set(defaultProvider, { id: defaultProvider, isDefault: true });
|
|
671
841
|
}
|
|
@@ -674,6 +844,15 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
674
844
|
);
|
|
675
845
|
}
|
|
676
846
|
|
|
847
|
+
private async readConfig(): Promise<Record<string, unknown>> {
|
|
848
|
+
const response = await (await this.client()).request<{ config?: Record<string, unknown> }>(
|
|
849
|
+
"config/read",
|
|
850
|
+
{},
|
|
851
|
+
CONTROL_REQUEST_TIMEOUT_MS,
|
|
852
|
+
);
|
|
853
|
+
return response.config ?? {};
|
|
854
|
+
}
|
|
855
|
+
|
|
677
856
|
onEvent(listener: (event: RuntimeEvent) => void): () => void {
|
|
678
857
|
this.listeners.add(listener);
|
|
679
858
|
return () => this.listeners.delete(listener);
|
|
@@ -682,7 +861,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
682
861
|
async release(options: { force?: boolean } = {}): Promise<RuntimeReleaseResult> {
|
|
683
862
|
if (this.releaseInFlight) return this.releaseInFlight;
|
|
684
863
|
const activeSessionIds = [...this.sessions.values()]
|
|
685
|
-
.filter((session) => Boolean(session.activeTurnId))
|
|
864
|
+
.filter((session) => Boolean(session.activeTurnId) || this.sessionOperations.has(session.localSessionId))
|
|
686
865
|
.map((session) => session.localSessionId);
|
|
687
866
|
if (activeSessionIds.length > 0 && options.force !== true) {
|
|
688
867
|
return { status: "busy", activeSessionIds };
|
|
@@ -807,6 +986,18 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
807
986
|
turnId: mapped.turnId,
|
|
808
987
|
lastTokens: mapped.lastTokens,
|
|
809
988
|
cumulativeTokens: mapped.cumulativeTokens,
|
|
989
|
+
contextTokens: mapped.contextTokens,
|
|
990
|
+
});
|
|
991
|
+
} else if (mapped.kind === "context_compaction") {
|
|
992
|
+
this.emit({
|
|
993
|
+
type: "context_compaction",
|
|
994
|
+
sessionId,
|
|
995
|
+
turnId: mapped.turnId,
|
|
996
|
+
phase: mapped.phase,
|
|
997
|
+
compactionId: mapped.compactionId,
|
|
998
|
+
timestampMs: mapped.timestampMs,
|
|
999
|
+
turnCount: session.turnCount,
|
|
1000
|
+
storageBytes: rolloutStorageBytes(session.rolloutPath),
|
|
810
1001
|
});
|
|
811
1002
|
} else if (mapped.kind === "agent_message_phase") {
|
|
812
1003
|
session.messagePhases.set(mapped.itemId, mapped.phase);
|
|
@@ -922,12 +1113,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
922
1113
|
if (input.reasoningEffort) return input.reasoningEffort;
|
|
923
1114
|
if (response.reasoningEffort) return response.reasoningEffort;
|
|
924
1115
|
const model = input.model ?? response.model;
|
|
925
|
-
const models = await this.listModels();
|
|
1116
|
+
const models = await this.listModels(input.modelProvider ?? response.modelProvider, model);
|
|
926
1117
|
return models.find((item) => item.id === model)?.defaultReasoningEffort
|
|
927
1118
|
?? models.find((item) => item.isDefault)?.defaultReasoningEffort;
|
|
928
1119
|
}
|
|
929
1120
|
|
|
930
1121
|
private async ensureSessionResumed(session: CodexSession, client: AppServerClient): Promise<void> {
|
|
1122
|
+
if (session.settingsRecoveryError) throw new Error(session.settingsRecoveryError);
|
|
931
1123
|
if (!session.needsResume) return;
|
|
932
1124
|
await this.resumeAppServerSession(session, client);
|
|
933
1125
|
session.needsResume = false;
|
|
@@ -947,13 +1139,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
947
1139
|
|
|
948
1140
|
private async synchronizeSessionNow(sessionId: string): Promise<RuntimeSession> {
|
|
949
1141
|
const session = this.requireSession(sessionId);
|
|
950
|
-
const
|
|
1142
|
+
const client = await this.client();
|
|
1143
|
+
const response = await client.request<ThreadReadResponse>(
|
|
951
1144
|
"thread/read",
|
|
952
|
-
{ threadId: session.remoteSessionId, includeTurns:
|
|
1145
|
+
{ threadId: session.remoteSessionId, includeTurns: false },
|
|
953
1146
|
SYNC_REQUEST_TIMEOUT_MS,
|
|
954
1147
|
);
|
|
955
|
-
this.
|
|
956
|
-
|
|
1148
|
+
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "full");
|
|
1149
|
+
const thread = { ...response.thread, turns };
|
|
1150
|
+
this.reconcileThreadSnapshot(session, thread);
|
|
1151
|
+
return { ...session, remoteSummary: await this.decorateRemoteSession(session.remoteSessionId, remoteSessionSummary(thread)) };
|
|
957
1152
|
}
|
|
958
1153
|
|
|
959
1154
|
private reconcileThreadSnapshot(session: CodexSession, thread: ThreadReadResponse["thread"]): void {
|
|
@@ -1003,6 +1198,11 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1003
1198
|
}
|
|
1004
1199
|
|
|
1005
1200
|
private adoptTurn(session: CodexSession, turnId: string, startedAt: number): void {
|
|
1201
|
+
session.canReplaceEmptyThread = false;
|
|
1202
|
+
if (!session.knownTurnIds.has(turnId)) {
|
|
1203
|
+
session.knownTurnIds.add(turnId);
|
|
1204
|
+
if (session.turnCount !== undefined) session.turnCount += 1;
|
|
1205
|
+
}
|
|
1006
1206
|
session.activeTurnId = turnId;
|
|
1007
1207
|
session.activeTurnStartedAt = startedAt;
|
|
1008
1208
|
session.finalText = "";
|
|
@@ -1094,6 +1294,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1094
1294
|
generatedImagePaths: [],
|
|
1095
1295
|
messagePhases: new Map(),
|
|
1096
1296
|
needsResume: false,
|
|
1297
|
+
canReplaceEmptyThread: !("remoteSessionId" in input),
|
|
1298
|
+
rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
|
|
1299
|
+
turnCount: "remoteSessionId" in input ? undefined : 0,
|
|
1300
|
+
knownTurnIds: new Set(),
|
|
1097
1301
|
};
|
|
1098
1302
|
}
|
|
1099
1303
|
|
|
@@ -1103,6 +1307,17 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1103
1307
|
return session;
|
|
1104
1308
|
}
|
|
1105
1309
|
|
|
1310
|
+
private async runSessionOperation<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
|
|
1311
|
+
const previous = this.sessionOperations.get(sessionId);
|
|
1312
|
+
const current = (previous ?? Promise.resolve()).catch(() => undefined).then(operation);
|
|
1313
|
+
this.sessionOperations.set(sessionId, current);
|
|
1314
|
+
try {
|
|
1315
|
+
return await current;
|
|
1316
|
+
} finally {
|
|
1317
|
+
if (this.sessionOperations.get(sessionId) === current) this.sessionOperations.delete(sessionId);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1106
1321
|
private requireActiveTurn(sessionId: string, turnId: string): CodexSession {
|
|
1107
1322
|
const session = this.requireSession(sessionId);
|
|
1108
1323
|
if (session.activeTurnId !== turnId) throw new Error(`App Server turn is no longer active: ${turnId}`);
|
|
@@ -1141,7 +1356,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1141
1356
|
}
|
|
1142
1357
|
|
|
1143
1358
|
interface ThreadResponse {
|
|
1144
|
-
thread: { id: string; name?: string | null; preview?: string };
|
|
1359
|
+
thread: { id: string; name?: string | null; preview?: string; path?: string | null };
|
|
1145
1360
|
modelProvider?: string;
|
|
1146
1361
|
model?: string;
|
|
1147
1362
|
reasoningEffort?: string | null;
|
|
@@ -1172,6 +1387,8 @@ interface CodexThreadSnapshot {
|
|
|
1172
1387
|
updatedAt?: number;
|
|
1173
1388
|
recencyAt?: number | null;
|
|
1174
1389
|
status?: { type?: string };
|
|
1390
|
+
forkedFromId?: string | null;
|
|
1391
|
+
path?: string | null;
|
|
1175
1392
|
turns?: CodexTurnSnapshot[];
|
|
1176
1393
|
}
|
|
1177
1394
|
|
|
@@ -1240,6 +1457,34 @@ function stringValue(value: unknown): string | undefined {
|
|
|
1240
1457
|
return typeof value === "string" ? value : undefined;
|
|
1241
1458
|
}
|
|
1242
1459
|
|
|
1460
|
+
function rolloutStorageBytes(filePath: string | undefined): number | undefined {
|
|
1461
|
+
if (!filePath) return undefined;
|
|
1462
|
+
try {
|
|
1463
|
+
const stats = statSync(filePath);
|
|
1464
|
+
return stats.isFile() ? stats.size : undefined;
|
|
1465
|
+
} catch {
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function fallbackProviderModel(
|
|
1471
|
+
catalog: ModelOption[],
|
|
1472
|
+
requestedModel?: string,
|
|
1473
|
+
configuredDefaultModel?: string,
|
|
1474
|
+
): ModelOption | undefined {
|
|
1475
|
+
const id = requestedModel?.trim()
|
|
1476
|
+
|| configuredDefaultModel?.trim()
|
|
1477
|
+
|| catalog.find((model) => model.isDefault)?.id
|
|
1478
|
+
|| catalog[0]?.id;
|
|
1479
|
+
if (!id) return undefined;
|
|
1480
|
+
const catalogModel = catalog.find((model) => model.id === id);
|
|
1481
|
+
return {
|
|
1482
|
+
...(catalogModel ?? { id, supportedReasoningEfforts: [] }),
|
|
1483
|
+
id,
|
|
1484
|
+
isDefault: true,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1243
1488
|
function turnStartedAt(turn: CodexTurnSnapshot): number {
|
|
1244
1489
|
return typeof turn.startedAt === "number" ? turn.startedAt * 1_000 : Date.now();
|
|
1245
1490
|
}
|
|
@@ -1265,64 +1510,12 @@ function uniqueStrings(values: string[]): string[] {
|
|
|
1265
1510
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
1266
1511
|
}
|
|
1267
1512
|
|
|
1268
|
-
function isUnsupportedExcludeTurnsError(error: unknown): boolean {
|
|
1269
|
-
if (!(error instanceof AppServerRequestError) || error.method !== "thread/fork") return false;
|
|
1270
|
-
const data = typeof error.data === "string"
|
|
1271
|
-
? error.data
|
|
1272
|
-
: error.data === undefined
|
|
1273
|
-
? ""
|
|
1274
|
-
: JSON.stringify(error.data);
|
|
1275
|
-
const details = `${error.serverMessage} ${data}`;
|
|
1276
|
-
if (!details.toLowerCase().replaceAll(/[^a-z]/g, "").includes("excludeturns")) return false;
|
|
1277
|
-
return error.code === -32602
|
|
1278
|
-
|| /unknown|unrecognized|unexpected|unsupported|not supported|experimental/i.test(details);
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
1513
|
function isInvalidWorkingDirectoryError(error: unknown): boolean {
|
|
1282
1514
|
return error instanceof AppServerRequestError
|
|
1283
1515
|
&& error.method === "turn/start"
|
|
1284
1516
|
&& /invalid cwd/i.test(`${error.serverMessage} ${error.data === undefined ? "" : JSON.stringify(error.data)}`);
|
|
1285
1517
|
}
|
|
1286
1518
|
|
|
1287
|
-
function isUnsupportedRecencySortError(error: unknown): boolean {
|
|
1288
|
-
if (!(error instanceof AppServerRequestError) || error.method !== "thread/list") return false;
|
|
1289
|
-
const data = typeof error.data === "string"
|
|
1290
|
-
? error.data
|
|
1291
|
-
: error.data === undefined
|
|
1292
|
-
? ""
|
|
1293
|
-
: JSON.stringify(error.data);
|
|
1294
|
-
const details = `${error.serverMessage} ${data}`;
|
|
1295
|
-
return /recency_at/i.test(details)
|
|
1296
|
-
&& /unknown|unrecognized|unexpected|unsupported|not supported|invalid/i.test(details);
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
function isPaginatedThreadReadError(error: unknown): boolean {
|
|
1300
|
-
const details = appServerErrorDetails(error);
|
|
1301
|
-
return /paginated threads?[^\n]*do(?:es)? not support[^\n]*thread\/read[^\n]*includeTurns\s*=\s*true/i.test(details);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
function isUnsupportedThreadTurnsListError(error: unknown): boolean {
|
|
1305
|
-
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list") return false;
|
|
1306
|
-
return error.code === -32601
|
|
1307
|
-
|| /method not found|unknown method|unsupported|not supported/i.test(appServerErrorDetails(error));
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
function isUnmaterializedThreadReadError(error: unknown): boolean {
|
|
1311
|
-
const details = appServerErrorDetails(error);
|
|
1312
|
-
return /thread[^\n]*(?:not materialized|not loaded)[^\n]*includeTurns[^\n]*(?:unavailable|before first user message)/i
|
|
1313
|
-
.test(details);
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
function appServerErrorDetails(error: unknown): string {
|
|
1317
|
-
if (!(error instanceof AppServerRequestError)) return error instanceof Error ? error.message : String(error);
|
|
1318
|
-
const data = typeof error.data === "string"
|
|
1319
|
-
? error.data
|
|
1320
|
-
: error.data === undefined
|
|
1321
|
-
? ""
|
|
1322
|
-
: JSON.stringify(error.data);
|
|
1323
|
-
return `${error.message} ${error.serverMessage} ${data}`;
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
1519
|
function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary {
|
|
1327
1520
|
const lastTurn = thread.turns?.at(-1);
|
|
1328
1521
|
const lastCompletedTurn = [...(thread.turns ?? [])]
|