@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 { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
|
|
2
3
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
3
4
|
import { AppServerRequestError } from "./AppServerConnection.js";
|
|
@@ -5,6 +6,8 @@ import { mapCodexNotification } from "./CodexEventMapper.js";
|
|
|
5
6
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
6
7
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
7
8
|
import { threadWriterLockPath } from "./ThreadWriterProcess.js";
|
|
9
|
+
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
10
|
+
import { fetchProviderModels } from "./ProviderModels.js";
|
|
8
11
|
const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
9
12
|
"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.",
|
|
10
13
|
"Call user32!SetProcessDpiAwarenessContext((IntPtr)-4) in that same process and verify that it succeeded or that the process is already PMv2 before continuing.",
|
|
@@ -16,10 +19,10 @@ const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
|
16
19
|
const SESSION_REQUEST_TIMEOUT_MS = 60_000;
|
|
17
20
|
const CONTROL_REQUEST_TIMEOUT_MS = 10_000;
|
|
18
21
|
const SYNC_REQUEST_TIMEOUT_MS = 5_000;
|
|
19
|
-
const THREAD_TURN_PAGE_SIZE = 100;
|
|
20
22
|
const FORK_SOURCE_TURN_PAGE_SIZE = 20;
|
|
21
23
|
const FORK_SOURCE_REQUEST_TIMEOUT_MS = 15_000;
|
|
22
24
|
const USER_RESUMABLE_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer"];
|
|
25
|
+
const BUILT_IN_CODEX_PROVIDER_ID = "openai";
|
|
23
26
|
// A timed-out fork keeps running in App Server and can create an orphan thread.
|
|
24
27
|
// Wait for its response; connection closure still rejects the request.
|
|
25
28
|
const FORK_REQUEST_TIMEOUT_MS = 0;
|
|
@@ -34,9 +37,9 @@ export class CodexRuntime {
|
|
|
34
37
|
unsubscribe;
|
|
35
38
|
unsubscribeDisconnect;
|
|
36
39
|
sessionSyncs = new Map();
|
|
40
|
+
sessionOperations = new Map();
|
|
37
41
|
localActivityDetector;
|
|
38
42
|
codexHome;
|
|
39
|
-
threadListSortKey = "recency_at";
|
|
40
43
|
releaseInFlight;
|
|
41
44
|
constructor(provider, logger) {
|
|
42
45
|
this.provider = provider;
|
|
@@ -56,6 +59,9 @@ export class CodexRuntime {
|
|
|
56
59
|
return this.codexHome ? threadWriterLockPath(this.codexHome, remoteSessionId) : undefined;
|
|
57
60
|
}
|
|
58
61
|
async createSession(input) {
|
|
62
|
+
return this.runSessionOperation(input.localSessionId, () => this.createSessionNow(input));
|
|
63
|
+
}
|
|
64
|
+
async createSessionNow(input) {
|
|
59
65
|
const client = await this.client();
|
|
60
66
|
const response = await client.request("thread/start", {
|
|
61
67
|
cwd: input.cwd,
|
|
@@ -80,6 +86,9 @@ export class CodexRuntime {
|
|
|
80
86
|
return session;
|
|
81
87
|
}
|
|
82
88
|
async resumeSession(input) {
|
|
89
|
+
return this.runSessionOperation(input.localSessionId, () => this.resumeSessionNow(input));
|
|
90
|
+
}
|
|
91
|
+
async resumeSessionNow(input) {
|
|
83
92
|
const client = await this.client();
|
|
84
93
|
const response = await client.request("thread/resume", {
|
|
85
94
|
threadId: input.remoteSessionId,
|
|
@@ -96,6 +105,9 @@ export class CodexRuntime {
|
|
|
96
105
|
return session;
|
|
97
106
|
}
|
|
98
107
|
async forkSession(input) {
|
|
108
|
+
return this.runSessionOperation(input.localSessionId, () => this.forkSessionNow(input));
|
|
109
|
+
}
|
|
110
|
+
async forkSessionNow(input) {
|
|
99
111
|
const client = await this.client();
|
|
100
112
|
const forkParams = {
|
|
101
113
|
threadId: input.remoteSessionId,
|
|
@@ -107,19 +119,10 @@ export class CodexRuntime {
|
|
|
107
119
|
...threadLifecycleParams(input.cwd),
|
|
108
120
|
...permissionParams(input.permissionMode),
|
|
109
121
|
};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
excludeTurns: true,
|
|
115
|
-
}, FORK_REQUEST_TIMEOUT_MS);
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
if (!isUnsupportedExcludeTurnsError(error))
|
|
119
|
-
throw error;
|
|
120
|
-
this.logger.warn({ error }, "App Server does not support thread/fork excludeTurns; retrying without it.");
|
|
121
|
-
response = await client.request("thread/fork", forkParams, FORK_REQUEST_TIMEOUT_MS);
|
|
122
|
-
}
|
|
122
|
+
const response = await client.request("thread/fork", {
|
|
123
|
+
...forkParams,
|
|
124
|
+
excludeTurns: true,
|
|
125
|
+
}, FORK_REQUEST_TIMEOUT_MS);
|
|
123
126
|
const requestedTitle = normalizeTaskTitle(input.title);
|
|
124
127
|
if (requestedTitle) {
|
|
125
128
|
await client.request("thread/name/set", {
|
|
@@ -135,9 +138,13 @@ export class CodexRuntime {
|
|
|
135
138
|
return session;
|
|
136
139
|
}
|
|
137
140
|
async startTurn(sessionId, prompt) {
|
|
141
|
+
return this.runSessionOperation(sessionId, () => this.startTurnNow(sessionId, prompt));
|
|
142
|
+
}
|
|
143
|
+
async startTurnNow(sessionId, prompt) {
|
|
138
144
|
const session = this.requireSession(sessionId);
|
|
139
145
|
const client = await this.client();
|
|
140
146
|
await this.ensureSessionResumed(session, client);
|
|
147
|
+
session.canReplaceEmptyThread = false;
|
|
141
148
|
const start = () => client.request("turn/start", {
|
|
142
149
|
threadId: session.remoteSessionId,
|
|
143
150
|
input: codexUserInput(prompt),
|
|
@@ -172,24 +179,14 @@ export class CodexRuntime {
|
|
|
172
179
|
}
|
|
173
180
|
async listRemoteSessions(input = {}) {
|
|
174
181
|
const client = await this.client();
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
response = await this.requestThreadList(client, input, this.threadListSortKey);
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
if (this.threadListSortKey !== "recency_at" || !isUnsupportedRecencySortError(error))
|
|
181
|
-
throw error;
|
|
182
|
-
this.threadListSortKey = "updated_at";
|
|
183
|
-
this.logger.warn({ error }, "App Server does not support thread/list recency_at sorting; retrying with updated_at.");
|
|
184
|
-
response = await this.requestThreadList(client, input, this.threadListSortKey);
|
|
185
|
-
}
|
|
182
|
+
const response = await this.requestThreadList(client, input);
|
|
186
183
|
const sessions = await Promise.all(response.data.map(async (thread) => {
|
|
187
184
|
const listed = remoteSessionSummary(thread);
|
|
188
185
|
if (listed.status === "active" || listed.lastTurnStatus === "inProgress")
|
|
189
186
|
return listed;
|
|
190
187
|
try {
|
|
191
|
-
const
|
|
192
|
-
return mergeRemoteSessionSummary(listed, remoteSessionSummary(
|
|
188
|
+
const turns = await this.readLatestThreadTurns(client, thread.id, "summary");
|
|
189
|
+
return mergeRemoteSessionSummary(listed, remoteSessionSummary({ ...thread, turns }));
|
|
193
190
|
}
|
|
194
191
|
catch {
|
|
195
192
|
return listed;
|
|
@@ -203,35 +200,84 @@ export class CodexRuntime {
|
|
|
203
200
|
nextCursor: response.nextCursor ?? undefined,
|
|
204
201
|
};
|
|
205
202
|
}
|
|
206
|
-
requestThreadList(client, input
|
|
203
|
+
requestThreadList(client, input) {
|
|
207
204
|
return client.request("thread/list", {
|
|
208
205
|
cursor: input.cursor,
|
|
209
206
|
limit: input.limit ?? 20,
|
|
210
|
-
sortKey,
|
|
207
|
+
sortKey: this.provider.getAgentFamily?.() === "traex" ? "updated_at" : "recency_at",
|
|
211
208
|
sortDirection: "desc",
|
|
209
|
+
// Omitting this filter only lists the App Server's current Provider.
|
|
210
|
+
modelProviders: [],
|
|
212
211
|
sourceKinds: USER_RESUMABLE_THREAD_SOURCE_KINDS,
|
|
213
212
|
archived: false,
|
|
214
213
|
searchTerm: input.searchTerm,
|
|
215
214
|
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
216
215
|
}
|
|
217
|
-
async readRemoteSession(remoteSessionId) {
|
|
216
|
+
async readRemoteSession(remoteSessionId, view = "latest") {
|
|
217
|
+
const client = await this.client();
|
|
218
|
+
const response = await client.request("thread/read", { threadId: remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS);
|
|
219
|
+
const turns = view === "metadata" ? []
|
|
220
|
+
: await this.readLatestThreadTurns(client, remoteSessionId, view === "latest-full" ? "full" : "summary");
|
|
221
|
+
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary({ ...response.thread, turns }));
|
|
222
|
+
}
|
|
223
|
+
async readRemoteSessionMetrics(remoteSessionId) {
|
|
218
224
|
const client = await this.client();
|
|
219
|
-
const
|
|
220
|
-
|
|
225
|
+
const metadata = await client.request("thread/read", { threadId: remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS);
|
|
226
|
+
const { turnCount, turnIds } = await this.countRemoteTurns(client, remoteSessionId);
|
|
227
|
+
const rolloutPath = stringValue(metadata.thread.path)?.trim() || undefined;
|
|
228
|
+
const loaded = [...this.sessions.values()].filter((session) => session.remoteSessionId === remoteSessionId);
|
|
229
|
+
for (const session of loaded) {
|
|
230
|
+
session.rolloutPath = rolloutPath ?? session.rolloutPath;
|
|
231
|
+
const knownTurnIds = new Set(turnIds);
|
|
232
|
+
if (session.activeTurnId && !knownTurnIds.has(session.activeTurnId))
|
|
233
|
+
knownTurnIds.add(session.activeTurnId);
|
|
234
|
+
session.knownTurnIds = knownTurnIds;
|
|
235
|
+
session.turnCount = knownTurnIds.size;
|
|
236
|
+
}
|
|
237
|
+
const effectivePath = rolloutPath ?? loaded[0]?.rolloutPath;
|
|
238
|
+
return {
|
|
239
|
+
turnCount: loaded[0]?.turnCount ?? turnCount,
|
|
240
|
+
storageBytes: rolloutStorageBytes(effectivePath),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async countRemoteTurns(client, remoteSessionId) {
|
|
244
|
+
const turnIds = new Set();
|
|
245
|
+
const seenCursors = new Set();
|
|
246
|
+
let cursor;
|
|
247
|
+
do {
|
|
248
|
+
const page = await this.readTurnPage(client, remoteSessionId, {
|
|
249
|
+
cursor,
|
|
250
|
+
limit: 100,
|
|
251
|
+
itemsView: "summary",
|
|
252
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
253
|
+
for (const turn of page.data)
|
|
254
|
+
turnIds.add(turn.id);
|
|
255
|
+
const nextCursor = page.nextCursor ?? undefined;
|
|
256
|
+
if (!nextCursor)
|
|
257
|
+
break;
|
|
258
|
+
if (nextCursor === cursor || seenCursors.has(nextCursor)) {
|
|
259
|
+
throw new Error("App Server repeated a Turn history cursor while counting task Turns.");
|
|
260
|
+
}
|
|
261
|
+
seenCursors.add(nextCursor);
|
|
262
|
+
cursor = nextCursor;
|
|
263
|
+
} while (cursor);
|
|
264
|
+
return { turnCount: turnIds.size, turnIds };
|
|
265
|
+
}
|
|
266
|
+
async listRemoteTurnSummaries(remoteSessionId, input) {
|
|
267
|
+
const response = await this.readTurnPage(await this.client(), remoteSessionId, {
|
|
268
|
+
cursor: input.cursor,
|
|
269
|
+
limit: Math.max(1, Math.min(100, Math.trunc(input.limit))),
|
|
270
|
+
itemsView: "summary",
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
turns: remoteSessionSummary({ id: remoteSessionId, turns: response.data }).completedTurns ?? [],
|
|
274
|
+
nextCursor: response.nextCursor ?? undefined,
|
|
275
|
+
};
|
|
221
276
|
}
|
|
222
277
|
async readRemoteForkSource(remoteSessionId) {
|
|
223
278
|
const client = await this.client();
|
|
224
279
|
const metadata = await client.request("thread/read", { threadId: remoteSessionId, includeTurns: false }, FORK_SOURCE_REQUEST_TIMEOUT_MS);
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
turns = await this.listRecentThreadTurnsThroughCompleted(client, remoteSessionId);
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
if (!isUnsupportedThreadTurnsListError(error))
|
|
231
|
-
throw error;
|
|
232
|
-
this.logger.warn({ error, remoteSessionId }, "App Server does not support lightweight Fork source reads; falling back to complete thread history.");
|
|
233
|
-
return this.readRemoteSession(remoteSessionId);
|
|
234
|
-
}
|
|
280
|
+
const turns = await this.listRecentThreadTurnsThroughCompleted(client, remoteSessionId);
|
|
235
281
|
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary({
|
|
236
282
|
...metadata.thread,
|
|
237
283
|
turns,
|
|
@@ -251,12 +297,8 @@ export class CodexRuntime {
|
|
|
251
297
|
const seenCursors = new Set();
|
|
252
298
|
let cursor;
|
|
253
299
|
do {
|
|
254
|
-
const response = await
|
|
255
|
-
|
|
256
|
-
...(cursor ? { cursor } : {}),
|
|
257
|
-
limit: FORK_SOURCE_TURN_PAGE_SIZE,
|
|
258
|
-
sortDirection: "desc",
|
|
259
|
-
itemsView: "summary",
|
|
300
|
+
const response = await this.readTurnPage(client, remoteSessionId, {
|
|
301
|
+
cursor, limit: FORK_SOURCE_TURN_PAGE_SIZE, itemsView: "summary",
|
|
260
302
|
}, FORK_SOURCE_REQUEST_TIMEOUT_MS);
|
|
261
303
|
turns.push(...response.data);
|
|
262
304
|
const completedIndex = turns.findIndex((turn) => turn.status === "completed");
|
|
@@ -276,49 +318,25 @@ export class CodexRuntime {
|
|
|
276
318
|
} while (cursor);
|
|
277
319
|
return turns.reverse();
|
|
278
320
|
}
|
|
279
|
-
async
|
|
321
|
+
async readLatestThreadTurns(client, remoteSessionId, itemsView) {
|
|
322
|
+
const response = await this.readTurnPage(client, remoteSessionId, { limit: 1, itemsView });
|
|
323
|
+
return response.data;
|
|
324
|
+
}
|
|
325
|
+
async readTurnPage(client, remoteSessionId, input, timeoutMs = SYNC_REQUEST_TIMEOUT_MS) {
|
|
280
326
|
try {
|
|
281
|
-
return await client.request("thread/
|
|
327
|
+
return await client.request("thread/turns/list", {
|
|
328
|
+
threadId: remoteSessionId,
|
|
329
|
+
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
330
|
+
limit: input.limit, sortDirection: "desc", itemsView: input.itemsView,
|
|
331
|
+
}, timeoutMs);
|
|
282
332
|
}
|
|
283
333
|
catch (error) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const metadata = await client.request("thread/read", { threadId: remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS);
|
|
288
|
-
if (!paginated)
|
|
289
|
-
return { thread: { ...metadata.thread, turns: [] } };
|
|
290
|
-
return {
|
|
291
|
-
thread: {
|
|
292
|
-
...metadata.thread,
|
|
293
|
-
turns: await this.listThreadTurns(client, remoteSessionId),
|
|
294
|
-
},
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
async listThreadTurns(client, remoteSessionId) {
|
|
299
|
-
const turns = [];
|
|
300
|
-
const seenCursors = new Set();
|
|
301
|
-
let cursor;
|
|
302
|
-
do {
|
|
303
|
-
const response = await client.request("thread/turns/list", {
|
|
304
|
-
threadId: remoteSessionId,
|
|
305
|
-
...(cursor ? { cursor } : {}),
|
|
306
|
-
limit: THREAD_TURN_PAGE_SIZE,
|
|
307
|
-
sortDirection: "asc",
|
|
308
|
-
itemsView: "full",
|
|
309
|
-
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
310
|
-
turns.push(...response.data);
|
|
311
|
-
const nextCursor = response.nextCursor ?? undefined;
|
|
312
|
-
if (!nextCursor)
|
|
313
|
-
break;
|
|
314
|
-
if (seenCursors.has(nextCursor)) {
|
|
315
|
-
this.logger.warn({ remoteSessionId, cursor: nextCursor }, "App Server repeated a thread Turn pagination cursor; stopping pagination.");
|
|
316
|
-
break;
|
|
334
|
+
if (error instanceof AppServerRequestError && error.method === "thread/turns/list"
|
|
335
|
+
&& /not materialized yet; thread\/turns\/list is unavailable before first user message/i.test(error.serverMessage)) {
|
|
336
|
+
return { data: [], nextCursor: null };
|
|
317
337
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
} while (cursor);
|
|
321
|
-
return turns;
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
322
340
|
}
|
|
323
341
|
async inspectRemoteSessionActivity(remoteSessionId) {
|
|
324
342
|
const [response, activeThreads] = await Promise.all([
|
|
@@ -341,7 +359,7 @@ export class CodexRuntime {
|
|
|
341
359
|
const existing = this.sessionSyncs.get(sessionId);
|
|
342
360
|
if (existing)
|
|
343
361
|
return existing;
|
|
344
|
-
const synchronization = this.synchronizeSessionNow(sessionId);
|
|
362
|
+
const synchronization = this.runSessionOperation(sessionId, () => this.synchronizeSessionNow(sessionId));
|
|
345
363
|
this.sessionSyncs.set(sessionId, synchronization);
|
|
346
364
|
try {
|
|
347
365
|
return await synchronization;
|
|
@@ -368,8 +386,10 @@ export class CodexRuntime {
|
|
|
368
386
|
this.logger.info({ ...(sessionId ? { sessionId } : {}), threadId: remoteSessionId, turnId }, "App Server accepted the turn interrupt request.");
|
|
369
387
|
}
|
|
370
388
|
async closeSession(sessionId) {
|
|
371
|
-
|
|
372
|
-
|
|
389
|
+
await this.runSessionOperation(sessionId, async () => {
|
|
390
|
+
const session = this.requireSession(sessionId);
|
|
391
|
+
await this.archiveRemoteSession(session.remoteSessionId);
|
|
392
|
+
});
|
|
373
393
|
}
|
|
374
394
|
async archiveRemoteSession(remoteSessionId) {
|
|
375
395
|
await (await this.client()).request("thread/archive", { threadId: remoteSessionId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
@@ -379,6 +399,9 @@ export class CodexRuntime {
|
|
|
379
399
|
}
|
|
380
400
|
}
|
|
381
401
|
async setTitle(sessionId, title) {
|
|
402
|
+
return this.runSessionOperation(sessionId, () => this.setTitleNow(sessionId, title));
|
|
403
|
+
}
|
|
404
|
+
async setTitleNow(sessionId, title) {
|
|
382
405
|
const session = this.requireSession(sessionId);
|
|
383
406
|
const normalizedTitle = normalizeTaskTitle(title);
|
|
384
407
|
if (!normalizedTitle)
|
|
@@ -387,6 +410,9 @@ export class CodexRuntime {
|
|
|
387
410
|
session.title = normalizedTitle;
|
|
388
411
|
}
|
|
389
412
|
async getGoal(sessionId) {
|
|
413
|
+
return this.runSessionOperation(sessionId, () => this.getGoalNow(sessionId));
|
|
414
|
+
}
|
|
415
|
+
async getGoalNow(sessionId) {
|
|
390
416
|
const session = this.requireSession(sessionId);
|
|
391
417
|
const client = await this.client();
|
|
392
418
|
await this.ensureSessionResumed(session, client);
|
|
@@ -394,13 +420,20 @@ export class CodexRuntime {
|
|
|
394
420
|
return response.goal ?? undefined;
|
|
395
421
|
}
|
|
396
422
|
async setGoal(sessionId, update) {
|
|
423
|
+
return this.runSessionOperation(sessionId, () => this.setGoalNow(sessionId, update));
|
|
424
|
+
}
|
|
425
|
+
async setGoalNow(sessionId, update) {
|
|
397
426
|
const session = this.requireSession(sessionId);
|
|
398
427
|
const client = await this.client();
|
|
399
428
|
await this.ensureSessionResumed(session, client);
|
|
429
|
+
session.canReplaceEmptyThread = false;
|
|
400
430
|
const response = await client.request("thread/goal/set", { threadId: session.remoteSessionId, ...update }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
401
431
|
return response.goal;
|
|
402
432
|
}
|
|
403
433
|
async clearGoal(sessionId) {
|
|
434
|
+
return this.runSessionOperation(sessionId, () => this.clearGoalNow(sessionId));
|
|
435
|
+
}
|
|
436
|
+
async clearGoalNow(sessionId) {
|
|
404
437
|
const session = this.requireSession(sessionId);
|
|
405
438
|
const client = await this.client();
|
|
406
439
|
await this.ensureSessionResumed(session, client);
|
|
@@ -408,32 +441,132 @@ export class CodexRuntime {
|
|
|
408
441
|
return response.cleared;
|
|
409
442
|
}
|
|
410
443
|
async setModel(sessionId, model) {
|
|
411
|
-
this.requireSession(sessionId).model = model;
|
|
444
|
+
await this.runSessionOperation(sessionId, async () => { this.requireSession(sessionId).model = model; });
|
|
412
445
|
}
|
|
413
446
|
async setReasoningEffort(sessionId, effort) {
|
|
414
|
-
this.requireSession(sessionId).reasoningEffort = effort;
|
|
447
|
+
await this.runSessionOperation(sessionId, async () => { this.requireSession(sessionId).reasoningEffort = effort; });
|
|
415
448
|
}
|
|
416
449
|
async setPermissionMode(sessionId, mode) {
|
|
417
|
-
this.requireSession(sessionId).permissionMode = mode;
|
|
450
|
+
await this.runSessionOperation(sessionId, async () => { this.requireSession(sessionId).permissionMode = mode; });
|
|
451
|
+
}
|
|
452
|
+
async setExecutionSettings(sessionId, settings, persist) {
|
|
453
|
+
return this.runSessionOperation(sessionId, () => this.setExecutionSettingsNow(sessionId, settings, persist));
|
|
418
454
|
}
|
|
419
|
-
async
|
|
455
|
+
async setExecutionSettingsNow(sessionId, settings, persist) {
|
|
420
456
|
const session = this.requireSession(sessionId);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
session.
|
|
433
|
-
|
|
434
|
-
|
|
457
|
+
if (session.activeTurnId)
|
|
458
|
+
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
459
|
+
const client = await this.client();
|
|
460
|
+
const metadata = await client.request("thread/read", {
|
|
461
|
+
threadId: session.remoteSessionId, includeTurns: false,
|
|
462
|
+
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
463
|
+
const localActive = await this.localActivityDetector?.activeThreads([session.remoteSessionId]);
|
|
464
|
+
if (metadata.thread.status?.type === "active" || localActive?.has(session.remoteSessionId) || session.activeTurnId) {
|
|
465
|
+
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
466
|
+
}
|
|
467
|
+
let empty = false;
|
|
468
|
+
if (session.canReplaceEmptyThread && !metadata.thread.forkedFromId && !metadata.thread.preview?.trim()) {
|
|
469
|
+
try {
|
|
470
|
+
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "summary");
|
|
471
|
+
empty = turns.length === 0;
|
|
472
|
+
if (!empty)
|
|
473
|
+
session.canReplaceEmptyThread = false;
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list"
|
|
477
|
+
|| !/missing source rollout|no rollout found/iu.test(error.serverMessage))
|
|
478
|
+
throw error;
|
|
479
|
+
empty = true;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const previous = { ...session };
|
|
483
|
+
let replacementId;
|
|
484
|
+
let detached = false;
|
|
485
|
+
try {
|
|
486
|
+
const params = {
|
|
487
|
+
cwd: session.cwd, modelProvider: settings.modelProvider, model: settings.model,
|
|
488
|
+
...threadLifecycleParams(session.cwd), ...permissionParams(settings.permissionMode),
|
|
489
|
+
};
|
|
490
|
+
if (!empty) {
|
|
491
|
+
await this.detachThreadForSettings(client, session);
|
|
492
|
+
detached = true;
|
|
493
|
+
}
|
|
494
|
+
const response = empty
|
|
495
|
+
? await client.request("thread/start", {
|
|
496
|
+
...params, threadSource: "user", allowProviderModelFallback: false,
|
|
497
|
+
}, SESSION_REQUEST_TIMEOUT_MS)
|
|
498
|
+
: await client.request("thread/resume", {
|
|
499
|
+
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
500
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
501
|
+
if (empty)
|
|
502
|
+
replacementId = response.thread.id;
|
|
503
|
+
assertProviderSettingsApplied(response, settings);
|
|
504
|
+
if (!empty && response.thread.id !== session.remoteSessionId)
|
|
505
|
+
throw new Error("App Server 返回了不同的任务 ID,未保存设置。");
|
|
506
|
+
if (empty && session.title)
|
|
507
|
+
await client.request("thread/name/set", {
|
|
508
|
+
threadId: response.thread.id, name: session.title,
|
|
509
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
510
|
+
const candidate = { ...session, ...settings, remoteSessionId: response.thread.id,
|
|
511
|
+
needsResume: false, settingsRecoveryError: undefined };
|
|
512
|
+
await persist?.(candidate);
|
|
513
|
+
Object.assign(session, candidate);
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
if (replacementId)
|
|
517
|
+
await this.discardUnusedThread(client, replacementId);
|
|
518
|
+
let recovery = "原任务设置未更改。";
|
|
519
|
+
if (detached) {
|
|
520
|
+
try {
|
|
521
|
+
await this.detachThreadForSettings(client, previous);
|
|
522
|
+
const restored = await client.request("thread/resume", {
|
|
523
|
+
threadId: previous.remoteSessionId, excludeTurns: true, cwd: previous.cwd,
|
|
524
|
+
modelProvider: previous.modelProvider, model: previous.model,
|
|
525
|
+
...threadLifecycleParams(previous.cwd), ...permissionParams(previous.permissionMode),
|
|
526
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
527
|
+
if (previous.modelProvider && previous.model)
|
|
528
|
+
assertProviderSettingsApplied(restored, {
|
|
529
|
+
modelProvider: previous.modelProvider, model: previous.model,
|
|
530
|
+
});
|
|
531
|
+
if (restored.thread.id !== previous.remoteSessionId)
|
|
532
|
+
throw new Error("恢复时返回了不同的任务 ID。");
|
|
533
|
+
session.needsResume = false;
|
|
534
|
+
session.settingsRecoveryError = undefined;
|
|
535
|
+
}
|
|
536
|
+
catch (restoreError) {
|
|
537
|
+
this.logger.warn({ error: restoreError, sessionId }, "Failed to restore Provider after a rejected settings change.");
|
|
538
|
+
recovery = "原设置仍保留,但远端恢复失败;请重新切换 Provider,确认成功后再发送消息。";
|
|
539
|
+
session.settingsRecoveryError = recovery;
|
|
540
|
+
session.needsResume = true;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
throw new Error(`Provider 切换失败:${providerSwitchFailure(error)} ${recovery}`, { cause: error });
|
|
544
|
+
}
|
|
545
|
+
if (replacementId)
|
|
546
|
+
await this.discardUnusedThread(client, previous.remoteSessionId);
|
|
435
547
|
return session;
|
|
436
548
|
}
|
|
549
|
+
async detachThreadForSettings(client, session) {
|
|
550
|
+
const current = await client.request("thread/read", {
|
|
551
|
+
threadId: session.remoteSessionId, includeTurns: false,
|
|
552
|
+
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
553
|
+
if (session.activeTurnId || current.thread.status?.type === "active")
|
|
554
|
+
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
555
|
+
const result = await client.request("thread/unsubscribe", {
|
|
556
|
+
threadId: session.remoteSessionId,
|
|
557
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
558
|
+
if (result.status !== "unsubscribed" && result.status !== "notLoaded") {
|
|
559
|
+
throw new Error("当前任务仍由其他客户端加载,无法安全切换 Provider。请先在原客户端释放任务。");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async discardUnusedThread(client, threadId) {
|
|
563
|
+
try {
|
|
564
|
+
await client.request("thread/unsubscribe", { threadId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
this.logger.warn({ error, threadId }, "Failed to release an unused empty Thread after changing Provider.");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
437
570
|
async respondToApproval(sessionId, requestId, decision) {
|
|
438
571
|
const pending = this.approvals.get(requestId);
|
|
439
572
|
if (!pending || pending.sessionId !== sessionId)
|
|
@@ -442,7 +575,43 @@ export class CodexRuntime {
|
|
|
442
575
|
pending.resolve({ decision });
|
|
443
576
|
this.emit({ type: "approval_resolved", sessionId, turnId: pending.turnId, requestId, decision });
|
|
444
577
|
}
|
|
445
|
-
async listModels() {
|
|
578
|
+
async listModels(modelProvider, fallbackModel) {
|
|
579
|
+
const requestedProvider = modelProvider?.trim();
|
|
580
|
+
if (!requestedProvider
|
|
581
|
+
|| (requestedProvider === BUILT_IN_CODEX_PROVIDER_ID && this.provider.getAgentFamily?.() !== "traex")) {
|
|
582
|
+
return this.listCatalogModels();
|
|
583
|
+
}
|
|
584
|
+
const config = await this.readConfig();
|
|
585
|
+
const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
|
|
586
|
+
const providerConfig = configuredProviders[requestedProvider];
|
|
587
|
+
let catalog = [];
|
|
588
|
+
try {
|
|
589
|
+
catalog = await this.listCatalogModels();
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
this.logger.warn({ error, modelProvider: requestedProvider }, "Failed to read the App Server model catalog while discovering Provider models.");
|
|
593
|
+
}
|
|
594
|
+
const configuredDefaultModel = stringValue(config.model_provider)?.trim() === requestedProvider
|
|
595
|
+
? stringValue(config.model)?.trim()
|
|
596
|
+
: undefined;
|
|
597
|
+
try {
|
|
598
|
+
return await fetchProviderModels({
|
|
599
|
+
providerId: requestedProvider,
|
|
600
|
+
config: (isRecord(providerConfig) ? providerConfig : {}),
|
|
601
|
+
catalog,
|
|
602
|
+
configuredDefaultModel,
|
|
603
|
+
environmentValue: (name) => this.provider.getEnvironmentVariable?.(name) ?? process.env[name],
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
const fallback = fallbackProviderModel(catalog, fallbackModel, configuredDefaultModel);
|
|
608
|
+
if (!fallback)
|
|
609
|
+
throw error;
|
|
610
|
+
this.logger.warn({ error, modelProvider: requestedProvider, fallbackModel: fallback.id }, "Provider model discovery failed; using the current or configured model as a fallback.");
|
|
611
|
+
return [fallback];
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async listCatalogModels() {
|
|
446
615
|
const response = await (await this.client()).request("model/list", {});
|
|
447
616
|
return response.data.filter((model) => model.id).map((model) => ({
|
|
448
617
|
id: model.id,
|
|
@@ -456,8 +625,7 @@ export class CodexRuntime {
|
|
|
456
625
|
}));
|
|
457
626
|
}
|
|
458
627
|
async listModelProviders() {
|
|
459
|
-
const
|
|
460
|
-
const config = response.config ?? {};
|
|
628
|
+
const config = await this.readConfig();
|
|
461
629
|
const defaultProvider = stringValue(config.model_provider)?.trim();
|
|
462
630
|
const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
|
|
463
631
|
const providers = new Map();
|
|
@@ -471,11 +639,22 @@ export class CodexRuntime {
|
|
|
471
639
|
...(id === defaultProvider ? { isDefault: true } : {}),
|
|
472
640
|
});
|
|
473
641
|
}
|
|
642
|
+
if (this.provider.getAgentFamily?.() === "codex" && !providers.has(BUILT_IN_CODEX_PROVIDER_ID)) {
|
|
643
|
+
providers.set(BUILT_IN_CODEX_PROVIDER_ID, {
|
|
644
|
+
id: BUILT_IN_CODEX_PROVIDER_ID,
|
|
645
|
+
displayName: "OpenAI",
|
|
646
|
+
...(!defaultProvider ? { isDefault: true } : {}),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
474
649
|
if (defaultProvider && !providers.has(defaultProvider)) {
|
|
475
650
|
providers.set(defaultProvider, { id: defaultProvider, isDefault: true });
|
|
476
651
|
}
|
|
477
652
|
return [...providers.values()].sort((left, right) => Number(Boolean(right.isDefault)) - Number(Boolean(left.isDefault)) || left.id.localeCompare(right.id));
|
|
478
653
|
}
|
|
654
|
+
async readConfig() {
|
|
655
|
+
const response = await (await this.client()).request("config/read", {}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
656
|
+
return response.config ?? {};
|
|
657
|
+
}
|
|
479
658
|
onEvent(listener) {
|
|
480
659
|
this.listeners.add(listener);
|
|
481
660
|
return () => this.listeners.delete(listener);
|
|
@@ -484,7 +663,7 @@ export class CodexRuntime {
|
|
|
484
663
|
if (this.releaseInFlight)
|
|
485
664
|
return this.releaseInFlight;
|
|
486
665
|
const activeSessionIds = [...this.sessions.values()]
|
|
487
|
-
.filter((session) => Boolean(session.activeTurnId))
|
|
666
|
+
.filter((session) => Boolean(session.activeTurnId) || this.sessionOperations.has(session.localSessionId))
|
|
488
667
|
.map((session) => session.localSessionId);
|
|
489
668
|
if (activeSessionIds.length > 0 && options.force !== true) {
|
|
490
669
|
return { status: "busy", activeSessionIds };
|
|
@@ -606,6 +785,19 @@ export class CodexRuntime {
|
|
|
606
785
|
turnId: mapped.turnId,
|
|
607
786
|
lastTokens: mapped.lastTokens,
|
|
608
787
|
cumulativeTokens: mapped.cumulativeTokens,
|
|
788
|
+
contextTokens: mapped.contextTokens,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
else if (mapped.kind === "context_compaction") {
|
|
792
|
+
this.emit({
|
|
793
|
+
type: "context_compaction",
|
|
794
|
+
sessionId,
|
|
795
|
+
turnId: mapped.turnId,
|
|
796
|
+
phase: mapped.phase,
|
|
797
|
+
compactionId: mapped.compactionId,
|
|
798
|
+
timestampMs: mapped.timestampMs,
|
|
799
|
+
turnCount: session.turnCount,
|
|
800
|
+
storageBytes: rolloutStorageBytes(session.rolloutPath),
|
|
609
801
|
});
|
|
610
802
|
}
|
|
611
803
|
else if (mapped.kind === "agent_message_phase") {
|
|
@@ -726,11 +918,13 @@ export class CodexRuntime {
|
|
|
726
918
|
if (response.reasoningEffort)
|
|
727
919
|
return response.reasoningEffort;
|
|
728
920
|
const model = input.model ?? response.model;
|
|
729
|
-
const models = await this.listModels();
|
|
921
|
+
const models = await this.listModels(input.modelProvider ?? response.modelProvider, model);
|
|
730
922
|
return models.find((item) => item.id === model)?.defaultReasoningEffort
|
|
731
923
|
?? models.find((item) => item.isDefault)?.defaultReasoningEffort;
|
|
732
924
|
}
|
|
733
925
|
async ensureSessionResumed(session, client) {
|
|
926
|
+
if (session.settingsRecoveryError)
|
|
927
|
+
throw new Error(session.settingsRecoveryError);
|
|
734
928
|
if (!session.needsResume)
|
|
735
929
|
return;
|
|
736
930
|
await this.resumeAppServerSession(session, client);
|
|
@@ -749,9 +943,12 @@ export class CodexRuntime {
|
|
|
749
943
|
}
|
|
750
944
|
async synchronizeSessionNow(sessionId) {
|
|
751
945
|
const session = this.requireSession(sessionId);
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
946
|
+
const client = await this.client();
|
|
947
|
+
const response = await client.request("thread/read", { threadId: session.remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS);
|
|
948
|
+
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "full");
|
|
949
|
+
const thread = { ...response.thread, turns };
|
|
950
|
+
this.reconcileThreadSnapshot(session, thread);
|
|
951
|
+
return { ...session, remoteSummary: await this.decorateRemoteSession(session.remoteSessionId, remoteSessionSummary(thread)) };
|
|
755
952
|
}
|
|
756
953
|
reconcileThreadSnapshot(session, thread) {
|
|
757
954
|
const turns = Array.isArray(thread.turns) ? thread.turns : [];
|
|
@@ -796,6 +993,12 @@ export class CodexRuntime {
|
|
|
796
993
|
this.finishSnapshotTurn(session, latest);
|
|
797
994
|
}
|
|
798
995
|
adoptTurn(session, turnId, startedAt) {
|
|
996
|
+
session.canReplaceEmptyThread = false;
|
|
997
|
+
if (!session.knownTurnIds.has(turnId)) {
|
|
998
|
+
session.knownTurnIds.add(turnId);
|
|
999
|
+
if (session.turnCount !== undefined)
|
|
1000
|
+
session.turnCount += 1;
|
|
1001
|
+
}
|
|
799
1002
|
session.activeTurnId = turnId;
|
|
800
1003
|
session.activeTurnStartedAt = startedAt;
|
|
801
1004
|
session.finalText = "";
|
|
@@ -873,6 +1076,10 @@ export class CodexRuntime {
|
|
|
873
1076
|
generatedImagePaths: [],
|
|
874
1077
|
messagePhases: new Map(),
|
|
875
1078
|
needsResume: false,
|
|
1079
|
+
canReplaceEmptyThread: !("remoteSessionId" in input),
|
|
1080
|
+
rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
|
|
1081
|
+
turnCount: "remoteSessionId" in input ? undefined : 0,
|
|
1082
|
+
knownTurnIds: new Set(),
|
|
876
1083
|
};
|
|
877
1084
|
}
|
|
878
1085
|
requireSession(sessionId) {
|
|
@@ -881,6 +1088,18 @@ export class CodexRuntime {
|
|
|
881
1088
|
throw new Error(`Unknown App Server session: ${sessionId}`);
|
|
882
1089
|
return session;
|
|
883
1090
|
}
|
|
1091
|
+
async runSessionOperation(sessionId, operation) {
|
|
1092
|
+
const previous = this.sessionOperations.get(sessionId);
|
|
1093
|
+
const current = (previous ?? Promise.resolve()).catch(() => undefined).then(operation);
|
|
1094
|
+
this.sessionOperations.set(sessionId, current);
|
|
1095
|
+
try {
|
|
1096
|
+
return await current;
|
|
1097
|
+
}
|
|
1098
|
+
finally {
|
|
1099
|
+
if (this.sessionOperations.get(sessionId) === current)
|
|
1100
|
+
this.sessionOperations.delete(sessionId);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
884
1103
|
requireActiveTurn(sessionId, turnId) {
|
|
885
1104
|
const session = this.requireSession(sessionId);
|
|
886
1105
|
if (session.activeTurnId !== turnId)
|
|
@@ -957,6 +1176,31 @@ function isRecord(value) {
|
|
|
957
1176
|
function stringValue(value) {
|
|
958
1177
|
return typeof value === "string" ? value : undefined;
|
|
959
1178
|
}
|
|
1179
|
+
function rolloutStorageBytes(filePath) {
|
|
1180
|
+
if (!filePath)
|
|
1181
|
+
return undefined;
|
|
1182
|
+
try {
|
|
1183
|
+
const stats = statSync(filePath);
|
|
1184
|
+
return stats.isFile() ? stats.size : undefined;
|
|
1185
|
+
}
|
|
1186
|
+
catch {
|
|
1187
|
+
return undefined;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function fallbackProviderModel(catalog, requestedModel, configuredDefaultModel) {
|
|
1191
|
+
const id = requestedModel?.trim()
|
|
1192
|
+
|| configuredDefaultModel?.trim()
|
|
1193
|
+
|| catalog.find((model) => model.isDefault)?.id
|
|
1194
|
+
|| catalog[0]?.id;
|
|
1195
|
+
if (!id)
|
|
1196
|
+
return undefined;
|
|
1197
|
+
const catalogModel = catalog.find((model) => model.id === id);
|
|
1198
|
+
return {
|
|
1199
|
+
...(catalogModel ?? { id, supportedReasoningEfforts: [] }),
|
|
1200
|
+
id,
|
|
1201
|
+
isDefault: true,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
960
1204
|
function turnStartedAt(turn) {
|
|
961
1205
|
return typeof turn.startedAt === "number" ? turn.startedAt * 1_000 : Date.now();
|
|
962
1206
|
}
|
|
@@ -976,62 +1220,11 @@ function extractGeneratedImagePaths(turn) {
|
|
|
976
1220
|
function uniqueStrings(values) {
|
|
977
1221
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
978
1222
|
}
|
|
979
|
-
function isUnsupportedExcludeTurnsError(error) {
|
|
980
|
-
if (!(error instanceof AppServerRequestError) || error.method !== "thread/fork")
|
|
981
|
-
return false;
|
|
982
|
-
const data = typeof error.data === "string"
|
|
983
|
-
? error.data
|
|
984
|
-
: error.data === undefined
|
|
985
|
-
? ""
|
|
986
|
-
: JSON.stringify(error.data);
|
|
987
|
-
const details = `${error.serverMessage} ${data}`;
|
|
988
|
-
if (!details.toLowerCase().replaceAll(/[^a-z]/g, "").includes("excludeturns"))
|
|
989
|
-
return false;
|
|
990
|
-
return error.code === -32602
|
|
991
|
-
|| /unknown|unrecognized|unexpected|unsupported|not supported|experimental/i.test(details);
|
|
992
|
-
}
|
|
993
1223
|
function isInvalidWorkingDirectoryError(error) {
|
|
994
1224
|
return error instanceof AppServerRequestError
|
|
995
1225
|
&& error.method === "turn/start"
|
|
996
1226
|
&& /invalid cwd/i.test(`${error.serverMessage} ${error.data === undefined ? "" : JSON.stringify(error.data)}`);
|
|
997
1227
|
}
|
|
998
|
-
function isUnsupportedRecencySortError(error) {
|
|
999
|
-
if (!(error instanceof AppServerRequestError) || error.method !== "thread/list")
|
|
1000
|
-
return false;
|
|
1001
|
-
const data = typeof error.data === "string"
|
|
1002
|
-
? error.data
|
|
1003
|
-
: error.data === undefined
|
|
1004
|
-
? ""
|
|
1005
|
-
: JSON.stringify(error.data);
|
|
1006
|
-
const details = `${error.serverMessage} ${data}`;
|
|
1007
|
-
return /recency_at/i.test(details)
|
|
1008
|
-
&& /unknown|unrecognized|unexpected|unsupported|not supported|invalid/i.test(details);
|
|
1009
|
-
}
|
|
1010
|
-
function isPaginatedThreadReadError(error) {
|
|
1011
|
-
const details = appServerErrorDetails(error);
|
|
1012
|
-
return /paginated threads?[^\n]*do(?:es)? not support[^\n]*thread\/read[^\n]*includeTurns\s*=\s*true/i.test(details);
|
|
1013
|
-
}
|
|
1014
|
-
function isUnsupportedThreadTurnsListError(error) {
|
|
1015
|
-
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list")
|
|
1016
|
-
return false;
|
|
1017
|
-
return error.code === -32601
|
|
1018
|
-
|| /method not found|unknown method|unsupported|not supported/i.test(appServerErrorDetails(error));
|
|
1019
|
-
}
|
|
1020
|
-
function isUnmaterializedThreadReadError(error) {
|
|
1021
|
-
const details = appServerErrorDetails(error);
|
|
1022
|
-
return /thread[^\n]*(?:not materialized|not loaded)[^\n]*includeTurns[^\n]*(?:unavailable|before first user message)/i
|
|
1023
|
-
.test(details);
|
|
1024
|
-
}
|
|
1025
|
-
function appServerErrorDetails(error) {
|
|
1026
|
-
if (!(error instanceof AppServerRequestError))
|
|
1027
|
-
return error instanceof Error ? error.message : String(error);
|
|
1028
|
-
const data = typeof error.data === "string"
|
|
1029
|
-
? error.data
|
|
1030
|
-
: error.data === undefined
|
|
1031
|
-
? ""
|
|
1032
|
-
: JSON.stringify(error.data);
|
|
1033
|
-
return `${error.message} ${error.serverMessage} ${data}`;
|
|
1034
|
-
}
|
|
1035
1228
|
function remoteSessionSummary(thread) {
|
|
1036
1229
|
const lastTurn = thread.turns?.at(-1);
|
|
1037
1230
|
const lastCompletedTurn = [...(thread.turns ?? [])]
|