@keyou007/agent-bot 0.1.20-alpha.1 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +13 -0
- package/README.zh.md +9 -0
- package/dist/cli/AgentPrerequisites.js +11 -47
- package/dist/cli/AgentPrerequisites.js.map +1 -1
- package/dist/cli/DefaultAgentSelection.js +1 -1
- package/dist/cli/DefaultAgentSelection.js.map +1 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexLocalActivityDetector.js +32 -10
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +53 -7
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +225 -164
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/CodexVersion.js +18 -0
- package/dist/codex/CodexVersion.js.map +1 -0
- package/dist/codex/ProviderSwitch.js +32 -0
- package/dist/codex/ProviderSwitch.js.map +1 -0
- package/dist/feishu/CardRenderer.js +36 -11
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +17 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/InboundText.js +11 -0
- package/dist/feishu/InboundText.js.map +1 -1
- package/dist/feishu/MergedForwardMessage.js +17 -5
- package/dist/feishu/MergedForwardMessage.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +14 -4
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/MarkdownFileLinks.js +65 -0
- package/dist/local-files/MarkdownFileLinks.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +399 -257
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +58 -65
- package/dist/state/StateStore.js.map +1 -1
- package/dist/utils/semanticVersion.js +39 -0
- package/dist/utils/semanticVersion.js.map +1 -0
- package/docs/technical-reference.md +5 -3
- package/docs/technical-reference.zh.md +5 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +7 -1
- package/src/cli/AgentPrerequisites.ts +14 -40
- package/src/cli/DefaultAgentSelection.ts +1 -1
- package/src/cli.ts +13 -0
- package/src/codex/CodexLocalActivityDetector.ts +41 -9
- package/src/codex/CodexProcessManager.ts +44 -7
- package/src/codex/CodexRuntime.ts +244 -195
- package/src/codex/CodexVersion.ts +18 -0
- package/src/codex/ProviderSwitch.ts +36 -0
- package/src/feishu/CardRenderer.ts +37 -27
- package/src/feishu/FeishuConnector.ts +15 -5
- package/src/feishu/InboundText.ts +11 -0
- package/src/feishu/MergedForwardMessage.ts +15 -5
- package/src/index.ts +1 -1
- package/src/local-files/LocalFileViewerServer.ts +14 -4
- package/src/local-files/MarkdownFileLinks.ts +64 -0
- package/src/proxy/ProxySessionController.ts +369 -238
- package/src/runtime/types.ts +15 -2
- package/src/state/StateStore.ts +77 -90
- package/src/utils/semanticVersion.ts +28 -0
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
RemoteSessionActivity,
|
|
12
12
|
RemoteSessionPage,
|
|
13
13
|
RemoteSessionSummary,
|
|
14
|
+
RemoteTurnPage,
|
|
14
15
|
ResumeRuntimeSessionInput,
|
|
15
16
|
RuntimeGoal,
|
|
16
17
|
RuntimeGoalUpdate,
|
|
@@ -28,6 +29,7 @@ import { mapCodexNotification } from "./CodexEventMapper.js";
|
|
|
28
29
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
29
30
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
30
31
|
import { threadWriterLockPath } from "./ThreadWriterProcess.js";
|
|
32
|
+
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
31
33
|
|
|
32
34
|
const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
33
35
|
"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 +43,13 @@ const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
|
41
43
|
const SESSION_REQUEST_TIMEOUT_MS = 60_000;
|
|
42
44
|
const CONTROL_REQUEST_TIMEOUT_MS = 10_000;
|
|
43
45
|
const SYNC_REQUEST_TIMEOUT_MS = 5_000;
|
|
44
|
-
const THREAD_TURN_PAGE_SIZE = 100;
|
|
45
46
|
const FORK_SOURCE_TURN_PAGE_SIZE = 20;
|
|
46
47
|
const FORK_SOURCE_REQUEST_TIMEOUT_MS = 15_000;
|
|
47
48
|
const USER_RESUMABLE_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer"] as const;
|
|
49
|
+
const BUILT_IN_CODEX_PROVIDER_ID = "openai";
|
|
48
50
|
// A timed-out fork keeps running in App Server and can create an orphan thread.
|
|
49
51
|
// Wait for its response; connection closure still rejects the request.
|
|
50
52
|
const FORK_REQUEST_TIMEOUT_MS = 0;
|
|
51
|
-
type ThreadListSortKey = "recency_at" | "updated_at";
|
|
52
53
|
|
|
53
54
|
export interface AppServerClient {
|
|
54
55
|
request<T = unknown>(method: string, params?: unknown, timeoutMs?: number): Promise<T>;
|
|
@@ -62,6 +63,7 @@ export interface AppServerClient {
|
|
|
62
63
|
|
|
63
64
|
export interface AppServerClientProvider {
|
|
64
65
|
getClient(): Promise<AppServerClient>;
|
|
66
|
+
getAgentFamily?(): "codex" | "traex" | undefined;
|
|
65
67
|
getProcessInfo?(): AgentProcessInfo;
|
|
66
68
|
getCodexHome?(): string;
|
|
67
69
|
onDisconnect?(listener: (error: Error) => void): () => void;
|
|
@@ -76,6 +78,8 @@ interface CodexSession extends RuntimeSession {
|
|
|
76
78
|
generatedImagePaths: string[];
|
|
77
79
|
messagePhases: Map<string, "commentary" | "final_answer">;
|
|
78
80
|
needsResume: boolean;
|
|
81
|
+
canReplaceEmptyThread: boolean;
|
|
82
|
+
settingsRecoveryError?: string;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
interface PendingApproval {
|
|
@@ -93,9 +97,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
93
97
|
private unsubscribe?: () => void;
|
|
94
98
|
private readonly unsubscribeDisconnect?: () => void;
|
|
95
99
|
private readonly sessionSyncs = new Map<string, Promise<RuntimeSession>>();
|
|
100
|
+
private readonly sessionOperations = new Map<string, Promise<unknown>>();
|
|
96
101
|
private readonly localActivityDetector?: CodexLocalActivityDetector;
|
|
97
102
|
private readonly codexHome?: string;
|
|
98
|
-
private threadListSortKey: ThreadListSortKey = "recency_at";
|
|
99
103
|
private releaseInFlight?: Promise<RuntimeReleaseResult>;
|
|
100
104
|
|
|
101
105
|
constructor(
|
|
@@ -120,6 +124,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
async createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
|
|
127
|
+
return this.runSessionOperation(input.localSessionId, () => this.createSessionNow(input));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async createSessionNow(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
|
|
123
131
|
const client = await this.client();
|
|
124
132
|
const response = await client.request<ThreadResponse>("thread/start", {
|
|
125
133
|
cwd: input.cwd,
|
|
@@ -144,6 +152,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
144
152
|
}
|
|
145
153
|
|
|
146
154
|
async resumeSession(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
155
|
+
return this.runSessionOperation(input.localSessionId, () => this.resumeSessionNow(input));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async resumeSessionNow(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
147
159
|
const client = await this.client();
|
|
148
160
|
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
149
161
|
threadId: input.remoteSessionId,
|
|
@@ -161,6 +173,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
async forkSession(input: ForkRuntimeSessionInput): Promise<RuntimeSession> {
|
|
176
|
+
return this.runSessionOperation(input.localSessionId, () => this.forkSessionNow(input));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async forkSessionNow(input: ForkRuntimeSessionInput): Promise<RuntimeSession> {
|
|
164
180
|
const client = await this.client();
|
|
165
181
|
const forkParams = {
|
|
166
182
|
threadId: input.remoteSessionId,
|
|
@@ -172,20 +188,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
172
188
|
...threadLifecycleParams(input.cwd),
|
|
173
189
|
...permissionParams(input.permissionMode),
|
|
174
190
|
};
|
|
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
|
-
}
|
|
191
|
+
const response = await client.request<ThreadResponse>("thread/fork", {
|
|
192
|
+
...forkParams,
|
|
193
|
+
excludeTurns: true,
|
|
194
|
+
}, FORK_REQUEST_TIMEOUT_MS);
|
|
189
195
|
const requestedTitle = normalizeTaskTitle(input.title);
|
|
190
196
|
if (requestedTitle) {
|
|
191
197
|
await client.request("thread/name/set", {
|
|
@@ -201,9 +207,14 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
201
207
|
}
|
|
202
208
|
|
|
203
209
|
async startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string> {
|
|
210
|
+
return this.runSessionOperation(sessionId, () => this.startTurnNow(sessionId, prompt));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async startTurnNow(sessionId: string, prompt: RuntimePrompt): Promise<string> {
|
|
204
214
|
const session = this.requireSession(sessionId);
|
|
205
215
|
const client = await this.client();
|
|
206
216
|
await this.ensureSessionResumed(session, client);
|
|
217
|
+
session.canReplaceEmptyThread = false;
|
|
207
218
|
const start = () => client.request<{ turn: { id: string } }>("turn/start", {
|
|
208
219
|
threadId: session.remoteSessionId,
|
|
209
220
|
input: codexUserInput(prompt),
|
|
@@ -249,28 +260,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
249
260
|
limit?: number;
|
|
250
261
|
} = {}): Promise<RemoteSessionPage> {
|
|
251
262
|
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
|
-
}
|
|
263
|
+
const response = await this.requestThreadList(client, input);
|
|
264
264
|
const sessions = await Promise.all(response.data.map(async (thread) => {
|
|
265
265
|
const listed = remoteSessionSummary(thread);
|
|
266
266
|
if (listed.status === "active" || listed.lastTurnStatus === "inProgress") return listed;
|
|
267
267
|
try {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
{ threadId: thread.id, includeTurns: true },
|
|
271
|
-
SYNC_REQUEST_TIMEOUT_MS,
|
|
272
|
-
);
|
|
273
|
-
return mergeRemoteSessionSummary(listed, remoteSessionSummary(detail.thread));
|
|
268
|
+
const turns = await this.readLatestThreadTurns(client, thread.id, "summary");
|
|
269
|
+
return mergeRemoteSessionSummary(listed, remoteSessionSummary({ ...thread, turns }));
|
|
274
270
|
} catch {
|
|
275
271
|
return listed;
|
|
276
272
|
}
|
|
@@ -287,14 +283,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
287
283
|
private requestThreadList(
|
|
288
284
|
client: AppServerClient,
|
|
289
285
|
input: { searchTerm?: string; cursor?: string; limit?: number },
|
|
290
|
-
sortKey: ThreadListSortKey,
|
|
291
286
|
): Promise<ThreadListResponse> {
|
|
292
287
|
return client.request<ThreadListResponse>(
|
|
293
288
|
"thread/list",
|
|
294
289
|
{
|
|
295
290
|
cursor: input.cursor,
|
|
296
291
|
limit: input.limit ?? 20,
|
|
297
|
-
sortKey,
|
|
292
|
+
sortKey: this.provider.getAgentFamily?.() === "traex" ? "updated_at" : "recency_at",
|
|
298
293
|
sortDirection: "desc",
|
|
299
294
|
sourceKinds: USER_RESUMABLE_THREAD_SOURCE_KINDS,
|
|
300
295
|
archived: false,
|
|
@@ -304,10 +299,32 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
304
299
|
);
|
|
305
300
|
}
|
|
306
301
|
|
|
307
|
-
async readRemoteSession(
|
|
302
|
+
async readRemoteSession(
|
|
303
|
+
remoteSessionId: string,
|
|
304
|
+
view: "metadata" | "latest" | "latest-full" = "latest",
|
|
305
|
+
): Promise<RemoteSessionSummary> {
|
|
308
306
|
const client = await this.client();
|
|
309
|
-
const response = await
|
|
310
|
-
|
|
307
|
+
const response = await client.request<ThreadReadResponse>(
|
|
308
|
+
"thread/read", { threadId: remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS,
|
|
309
|
+
);
|
|
310
|
+
const turns = view === "metadata" ? []
|
|
311
|
+
: await this.readLatestThreadTurns(client, remoteSessionId, view === "latest-full" ? "full" : "summary");
|
|
312
|
+
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary({ ...response.thread, turns }));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async listRemoteTurnSummaries(
|
|
316
|
+
remoteSessionId: string,
|
|
317
|
+
input: { cursor?: string; limit: number },
|
|
318
|
+
): Promise<RemoteTurnPage> {
|
|
319
|
+
const response = await this.readTurnPage(await this.client(), remoteSessionId, {
|
|
320
|
+
cursor: input.cursor,
|
|
321
|
+
limit: Math.max(1, Math.min(100, Math.trunc(input.limit))),
|
|
322
|
+
itemsView: "summary",
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
turns: remoteSessionSummary({ id: remoteSessionId, turns: response.data }).completedTurns ?? [],
|
|
326
|
+
nextCursor: response.nextCursor ?? undefined,
|
|
327
|
+
};
|
|
311
328
|
}
|
|
312
329
|
|
|
313
330
|
async readRemoteForkSource(remoteSessionId: string): Promise<RemoteSessionSummary> {
|
|
@@ -317,17 +334,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
317
334
|
{ threadId: remoteSessionId, includeTurns: false },
|
|
318
335
|
FORK_SOURCE_REQUEST_TIMEOUT_MS,
|
|
319
336
|
);
|
|
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
|
-
}
|
|
337
|
+
const turns = await this.listRecentThreadTurnsThroughCompleted(client, remoteSessionId);
|
|
331
338
|
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary({
|
|
332
339
|
...metadata.thread,
|
|
333
340
|
turns,
|
|
@@ -355,17 +362,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
355
362
|
const seenCursors = new Set<string>();
|
|
356
363
|
let cursor: string | undefined;
|
|
357
364
|
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
|
-
);
|
|
365
|
+
const response = await this.readTurnPage(client, remoteSessionId, {
|
|
366
|
+
cursor, limit: FORK_SOURCE_TURN_PAGE_SIZE, itemsView: "summary",
|
|
367
|
+
}, FORK_SOURCE_REQUEST_TIMEOUT_MS);
|
|
369
368
|
turns.push(...response.data);
|
|
370
369
|
const completedIndex = turns.findIndex((turn) => turn.status === "completed");
|
|
371
370
|
if (completedIndex >= 0) {
|
|
@@ -387,69 +386,34 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
387
386
|
return turns.reverse();
|
|
388
387
|
}
|
|
389
388
|
|
|
390
|
-
private async
|
|
389
|
+
private async readLatestThreadTurns(
|
|
391
390
|
client: AppServerClient,
|
|
392
391
|
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
|
-
}
|
|
392
|
+
itemsView: "summary" | "full",
|
|
393
|
+
): Promise<CodexTurnSnapshot[]> {
|
|
394
|
+
const response = await this.readTurnPage(client, remoteSessionId, { limit: 1, itemsView });
|
|
395
|
+
return response.data;
|
|
418
396
|
}
|
|
419
397
|
|
|
420
|
-
private async
|
|
398
|
+
private async readTurnPage(
|
|
421
399
|
client: AppServerClient,
|
|
422
400
|
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;
|
|
401
|
+
input: { cursor?: string; limit: number; itemsView: "summary" | "full" },
|
|
402
|
+
timeoutMs = SYNC_REQUEST_TIMEOUT_MS,
|
|
403
|
+
): Promise<ThreadTurnsListResponse> {
|
|
404
|
+
try {
|
|
405
|
+
return await client.request<ThreadTurnsListResponse>("thread/turns/list", {
|
|
406
|
+
threadId: remoteSessionId,
|
|
407
|
+
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
408
|
+
limit: input.limit, sortDirection: "desc", itemsView: input.itemsView,
|
|
409
|
+
}, timeoutMs);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (error instanceof AppServerRequestError && error.method === "thread/turns/list"
|
|
412
|
+
&& /not materialized yet; thread\/turns\/list is unavailable before first user message/i.test(error.serverMessage)) {
|
|
413
|
+
return { data: [], nextCursor: null };
|
|
448
414
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
} while (cursor);
|
|
452
|
-
return turns;
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
453
417
|
}
|
|
454
418
|
|
|
455
419
|
async inspectRemoteSessionActivity(remoteSessionId: string): Promise<RemoteSessionActivity> {
|
|
@@ -477,7 +441,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
477
441
|
async synchronizeSession(sessionId: string): Promise<RuntimeSession> {
|
|
478
442
|
const existing = this.sessionSyncs.get(sessionId);
|
|
479
443
|
if (existing) return existing;
|
|
480
|
-
const synchronization = this.synchronizeSessionNow(sessionId);
|
|
444
|
+
const synchronization = this.runSessionOperation(sessionId, () => this.synchronizeSessionNow(sessionId));
|
|
481
445
|
this.sessionSyncs.set(sessionId, synchronization);
|
|
482
446
|
try {
|
|
483
447
|
return await synchronization;
|
|
@@ -513,8 +477,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
513
477
|
}
|
|
514
478
|
|
|
515
479
|
async closeSession(sessionId: string): Promise<void> {
|
|
516
|
-
|
|
517
|
-
|
|
480
|
+
await this.runSessionOperation(sessionId, async () => {
|
|
481
|
+
const session = this.requireSession(sessionId);
|
|
482
|
+
await this.archiveRemoteSession(session.remoteSessionId);
|
|
483
|
+
});
|
|
518
484
|
}
|
|
519
485
|
|
|
520
486
|
async archiveRemoteSession(remoteSessionId: string): Promise<void> {
|
|
@@ -529,6 +495,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
529
495
|
}
|
|
530
496
|
|
|
531
497
|
async setTitle(sessionId: string, title: string): Promise<void> {
|
|
498
|
+
return this.runSessionOperation(sessionId, () => this.setTitleNow(sessionId, title));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private async setTitleNow(sessionId: string, title: string): Promise<void> {
|
|
532
502
|
const session = this.requireSession(sessionId);
|
|
533
503
|
const normalizedTitle = normalizeTaskTitle(title);
|
|
534
504
|
if (!normalizedTitle) throw new Error("任务标题不能为空。");
|
|
@@ -541,6 +511,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
541
511
|
}
|
|
542
512
|
|
|
543
513
|
async getGoal(sessionId: string): Promise<RuntimeGoal | undefined> {
|
|
514
|
+
return this.runSessionOperation(sessionId, () => this.getGoalNow(sessionId));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private async getGoalNow(sessionId: string): Promise<RuntimeGoal | undefined> {
|
|
544
518
|
const session = this.requireSession(sessionId);
|
|
545
519
|
const client = await this.client();
|
|
546
520
|
await this.ensureSessionResumed(session, client);
|
|
@@ -553,9 +527,14 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
553
527
|
}
|
|
554
528
|
|
|
555
529
|
async setGoal(sessionId: string, update: RuntimeGoalUpdate): Promise<RuntimeGoal> {
|
|
530
|
+
return this.runSessionOperation(sessionId, () => this.setGoalNow(sessionId, update));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private async setGoalNow(sessionId: string, update: RuntimeGoalUpdate): Promise<RuntimeGoal> {
|
|
556
534
|
const session = this.requireSession(sessionId);
|
|
557
535
|
const client = await this.client();
|
|
558
536
|
await this.ensureSessionResumed(session, client);
|
|
537
|
+
session.canReplaceEmptyThread = false;
|
|
559
538
|
const response = await client.request<{ goal: RuntimeGoal }>(
|
|
560
539
|
"thread/goal/set",
|
|
561
540
|
{ threadId: session.remoteSessionId, ...update },
|
|
@@ -565,6 +544,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
565
544
|
}
|
|
566
545
|
|
|
567
546
|
async clearGoal(sessionId: string): Promise<boolean> {
|
|
547
|
+
return this.runSessionOperation(sessionId, () => this.clearGoalNow(sessionId));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private async clearGoalNow(sessionId: string): Promise<boolean> {
|
|
568
551
|
const session = this.requireSession(sessionId);
|
|
569
552
|
const client = await this.client();
|
|
570
553
|
await this.ensureSessionResumed(session, client);
|
|
@@ -577,39 +560,132 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
577
560
|
}
|
|
578
561
|
|
|
579
562
|
async setModel(sessionId: string, model: string): Promise<void> {
|
|
580
|
-
this.requireSession(sessionId).model = model;
|
|
563
|
+
await this.runSessionOperation(sessionId, async () => { this.requireSession(sessionId).model = model; });
|
|
581
564
|
}
|
|
582
565
|
|
|
583
566
|
async setReasoningEffort(sessionId: string, effort: string): Promise<void> {
|
|
584
|
-
this.requireSession(sessionId).reasoningEffort = effort;
|
|
567
|
+
await this.runSessionOperation(sessionId, async () => { this.requireSession(sessionId).reasoningEffort = effort; });
|
|
585
568
|
}
|
|
586
569
|
|
|
587
570
|
async setPermissionMode(sessionId: string, mode: PermissionMode): Promise<void> {
|
|
588
|
-
this.requireSession(sessionId).permissionMode = mode;
|
|
571
|
+
await this.runSessionOperation(sessionId, async () => { this.requireSession(sessionId).permissionMode = mode; });
|
|
589
572
|
}
|
|
590
573
|
|
|
591
574
|
async setExecutionSettings(
|
|
592
575
|
sessionId: string,
|
|
593
576
|
settings: RuntimeExecutionSettings,
|
|
577
|
+
persist?: (session: RuntimeSession) => Promise<void>,
|
|
578
|
+
): Promise<RuntimeSession> {
|
|
579
|
+
return this.runSessionOperation(sessionId, () => this.setExecutionSettingsNow(sessionId, settings, persist));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private async setExecutionSettingsNow(
|
|
583
|
+
sessionId: string,
|
|
584
|
+
settings: RuntimeExecutionSettings,
|
|
585
|
+
persist?: (session: RuntimeSession) => Promise<void>,
|
|
594
586
|
): Promise<RuntimeSession> {
|
|
595
587
|
const session = this.requireSession(sessionId);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
session.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
588
|
+
if (session.activeTurnId) throw new Error(PROVIDER_SWITCH_BUSY);
|
|
589
|
+
const client = await this.client();
|
|
590
|
+
const metadata = await client.request<ThreadReadResponse>("thread/read", {
|
|
591
|
+
threadId: session.remoteSessionId, includeTurns: false,
|
|
592
|
+
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
593
|
+
const localActive = await this.localActivityDetector?.activeThreads([session.remoteSessionId]);
|
|
594
|
+
if (metadata.thread.status?.type === "active" || localActive?.has(session.remoteSessionId) || session.activeTurnId) {
|
|
595
|
+
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
596
|
+
}
|
|
597
|
+
let empty = false;
|
|
598
|
+
if (session.canReplaceEmptyThread && !metadata.thread.forkedFromId && !metadata.thread.preview?.trim()) {
|
|
599
|
+
try {
|
|
600
|
+
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "summary");
|
|
601
|
+
empty = turns.length === 0;
|
|
602
|
+
if (!empty) session.canReplaceEmptyThread = false;
|
|
603
|
+
} catch (error) {
|
|
604
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list"
|
|
605
|
+
|| !/missing source rollout|no rollout found/iu.test(error.serverMessage)) throw error;
|
|
606
|
+
empty = true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const previous = { ...session };
|
|
610
|
+
let replacementId: string | undefined;
|
|
611
|
+
let detached = false;
|
|
612
|
+
try {
|
|
613
|
+
const params = {
|
|
614
|
+
cwd: session.cwd, modelProvider: settings.modelProvider, model: settings.model,
|
|
615
|
+
...threadLifecycleParams(session.cwd), ...permissionParams(settings.permissionMode),
|
|
616
|
+
};
|
|
617
|
+
if (!empty) {
|
|
618
|
+
await this.detachThreadForSettings(client, session);
|
|
619
|
+
detached = true;
|
|
620
|
+
}
|
|
621
|
+
const response = empty
|
|
622
|
+
? await client.request<ThreadResponse>("thread/start", {
|
|
623
|
+
...params, threadSource: "user", allowProviderModelFallback: false,
|
|
624
|
+
}, SESSION_REQUEST_TIMEOUT_MS)
|
|
625
|
+
: await client.request<ThreadResponse>("thread/resume", {
|
|
626
|
+
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
627
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
628
|
+
if (empty) replacementId = response.thread.id;
|
|
629
|
+
assertProviderSettingsApplied(response, settings);
|
|
630
|
+
if (!empty && response.thread.id !== session.remoteSessionId) throw new Error("App Server 返回了不同的任务 ID,未保存设置。");
|
|
631
|
+
if (empty && session.title) await client.request("thread/name/set", {
|
|
632
|
+
threadId: response.thread.id, name: session.title,
|
|
633
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
634
|
+
const candidate = { ...session, ...settings, remoteSessionId: response.thread.id,
|
|
635
|
+
needsResume: false, settingsRecoveryError: undefined };
|
|
636
|
+
await persist?.(candidate);
|
|
637
|
+
Object.assign(session, candidate);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (replacementId) await this.discardUnusedThread(client, replacementId);
|
|
640
|
+
let recovery = "原任务设置未更改。";
|
|
641
|
+
if (detached) {
|
|
642
|
+
try {
|
|
643
|
+
await this.detachThreadForSettings(client, previous);
|
|
644
|
+
const restored = await client.request<ThreadResponse>("thread/resume", {
|
|
645
|
+
threadId: previous.remoteSessionId, excludeTurns: true, cwd: previous.cwd,
|
|
646
|
+
modelProvider: previous.modelProvider, model: previous.model,
|
|
647
|
+
...threadLifecycleParams(previous.cwd), ...permissionParams(previous.permissionMode),
|
|
648
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
649
|
+
if (previous.modelProvider && previous.model) assertProviderSettingsApplied(restored, {
|
|
650
|
+
modelProvider: previous.modelProvider, model: previous.model,
|
|
651
|
+
});
|
|
652
|
+
if (restored.thread.id !== previous.remoteSessionId) throw new Error("恢复时返回了不同的任务 ID。");
|
|
653
|
+
session.needsResume = false;
|
|
654
|
+
session.settingsRecoveryError = undefined;
|
|
655
|
+
} catch (restoreError) {
|
|
656
|
+
this.logger.warn({ error: restoreError, sessionId }, "Failed to restore Provider after a rejected settings change.");
|
|
657
|
+
recovery = "原设置仍保留,但远端恢复失败;请重新切换 Provider,确认成功后再发送消息。";
|
|
658
|
+
session.settingsRecoveryError = recovery;
|
|
659
|
+
session.needsResume = true;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
throw new Error(`Provider 切换失败:${providerSwitchFailure(error)} ${recovery}`, { cause: error });
|
|
663
|
+
}
|
|
664
|
+
if (replacementId) await this.discardUnusedThread(client, previous.remoteSessionId);
|
|
610
665
|
return session;
|
|
611
666
|
}
|
|
612
667
|
|
|
668
|
+
private async detachThreadForSettings(client: AppServerClient, session: CodexSession): Promise<void> {
|
|
669
|
+
const current = await client.request<ThreadReadResponse>("thread/read", {
|
|
670
|
+
threadId: session.remoteSessionId, includeTurns: false,
|
|
671
|
+
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
672
|
+
if (session.activeTurnId || current.thread.status?.type === "active") throw new Error(PROVIDER_SWITCH_BUSY);
|
|
673
|
+
const result = await client.request<{ status: string }>("thread/unsubscribe", {
|
|
674
|
+
threadId: session.remoteSessionId,
|
|
675
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
676
|
+
if (result.status !== "unsubscribed" && result.status !== "notLoaded") {
|
|
677
|
+
throw new Error("当前任务仍由其他客户端加载,无法安全切换 Provider。请先在原客户端释放任务。");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private async discardUnusedThread(client: AppServerClient, threadId: string): Promise<void> {
|
|
682
|
+
try {
|
|
683
|
+
await client.request("thread/unsubscribe", { threadId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
this.logger.warn({ error, threadId }, "Failed to release an unused empty Thread after changing Provider.");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
613
689
|
async respondToApproval(
|
|
614
690
|
sessionId: string,
|
|
615
691
|
requestId: string,
|
|
@@ -666,6 +742,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
666
742
|
...(id === defaultProvider ? { isDefault: true } : {}),
|
|
667
743
|
});
|
|
668
744
|
}
|
|
745
|
+
if (this.provider.getAgentFamily?.() === "codex" && !providers.has(BUILT_IN_CODEX_PROVIDER_ID)) {
|
|
746
|
+
providers.set(BUILT_IN_CODEX_PROVIDER_ID, {
|
|
747
|
+
id: BUILT_IN_CODEX_PROVIDER_ID,
|
|
748
|
+
displayName: "OpenAI",
|
|
749
|
+
...(!defaultProvider ? { isDefault: true } : {}),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
669
752
|
if (defaultProvider && !providers.has(defaultProvider)) {
|
|
670
753
|
providers.set(defaultProvider, { id: defaultProvider, isDefault: true });
|
|
671
754
|
}
|
|
@@ -682,7 +765,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
682
765
|
async release(options: { force?: boolean } = {}): Promise<RuntimeReleaseResult> {
|
|
683
766
|
if (this.releaseInFlight) return this.releaseInFlight;
|
|
684
767
|
const activeSessionIds = [...this.sessions.values()]
|
|
685
|
-
.filter((session) => Boolean(session.activeTurnId))
|
|
768
|
+
.filter((session) => Boolean(session.activeTurnId) || this.sessionOperations.has(session.localSessionId))
|
|
686
769
|
.map((session) => session.localSessionId);
|
|
687
770
|
if (activeSessionIds.length > 0 && options.force !== true) {
|
|
688
771
|
return { status: "busy", activeSessionIds };
|
|
@@ -928,6 +1011,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
928
1011
|
}
|
|
929
1012
|
|
|
930
1013
|
private async ensureSessionResumed(session: CodexSession, client: AppServerClient): Promise<void> {
|
|
1014
|
+
if (session.settingsRecoveryError) throw new Error(session.settingsRecoveryError);
|
|
931
1015
|
if (!session.needsResume) return;
|
|
932
1016
|
await this.resumeAppServerSession(session, client);
|
|
933
1017
|
session.needsResume = false;
|
|
@@ -947,13 +1031,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
947
1031
|
|
|
948
1032
|
private async synchronizeSessionNow(sessionId: string): Promise<RuntimeSession> {
|
|
949
1033
|
const session = this.requireSession(sessionId);
|
|
950
|
-
const
|
|
1034
|
+
const client = await this.client();
|
|
1035
|
+
const response = await client.request<ThreadReadResponse>(
|
|
951
1036
|
"thread/read",
|
|
952
|
-
{ threadId: session.remoteSessionId, includeTurns:
|
|
1037
|
+
{ threadId: session.remoteSessionId, includeTurns: false },
|
|
953
1038
|
SYNC_REQUEST_TIMEOUT_MS,
|
|
954
1039
|
);
|
|
955
|
-
this.
|
|
956
|
-
|
|
1040
|
+
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "full");
|
|
1041
|
+
const thread = { ...response.thread, turns };
|
|
1042
|
+
this.reconcileThreadSnapshot(session, thread);
|
|
1043
|
+
return { ...session, remoteSummary: await this.decorateRemoteSession(session.remoteSessionId, remoteSessionSummary(thread)) };
|
|
957
1044
|
}
|
|
958
1045
|
|
|
959
1046
|
private reconcileThreadSnapshot(session: CodexSession, thread: ThreadReadResponse["thread"]): void {
|
|
@@ -1003,6 +1090,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1003
1090
|
}
|
|
1004
1091
|
|
|
1005
1092
|
private adoptTurn(session: CodexSession, turnId: string, startedAt: number): void {
|
|
1093
|
+
session.canReplaceEmptyThread = false;
|
|
1006
1094
|
session.activeTurnId = turnId;
|
|
1007
1095
|
session.activeTurnStartedAt = startedAt;
|
|
1008
1096
|
session.finalText = "";
|
|
@@ -1094,6 +1182,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1094
1182
|
generatedImagePaths: [],
|
|
1095
1183
|
messagePhases: new Map(),
|
|
1096
1184
|
needsResume: false,
|
|
1185
|
+
canReplaceEmptyThread: !("remoteSessionId" in input),
|
|
1097
1186
|
};
|
|
1098
1187
|
}
|
|
1099
1188
|
|
|
@@ -1103,6 +1192,17 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1103
1192
|
return session;
|
|
1104
1193
|
}
|
|
1105
1194
|
|
|
1195
|
+
private async runSessionOperation<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
|
|
1196
|
+
const previous = this.sessionOperations.get(sessionId);
|
|
1197
|
+
const current = (previous ?? Promise.resolve()).catch(() => undefined).then(operation);
|
|
1198
|
+
this.sessionOperations.set(sessionId, current);
|
|
1199
|
+
try {
|
|
1200
|
+
return await current;
|
|
1201
|
+
} finally {
|
|
1202
|
+
if (this.sessionOperations.get(sessionId) === current) this.sessionOperations.delete(sessionId);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1106
1206
|
private requireActiveTurn(sessionId: string, turnId: string): CodexSession {
|
|
1107
1207
|
const session = this.requireSession(sessionId);
|
|
1108
1208
|
if (session.activeTurnId !== turnId) throw new Error(`App Server turn is no longer active: ${turnId}`);
|
|
@@ -1172,6 +1272,7 @@ interface CodexThreadSnapshot {
|
|
|
1172
1272
|
updatedAt?: number;
|
|
1173
1273
|
recencyAt?: number | null;
|
|
1174
1274
|
status?: { type?: string };
|
|
1275
|
+
forkedFromId?: string | null;
|
|
1175
1276
|
turns?: CodexTurnSnapshot[];
|
|
1176
1277
|
}
|
|
1177
1278
|
|
|
@@ -1265,64 +1366,12 @@ function uniqueStrings(values: string[]): string[] {
|
|
|
1265
1366
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
1266
1367
|
}
|
|
1267
1368
|
|
|
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
1369
|
function isInvalidWorkingDirectoryError(error: unknown): boolean {
|
|
1282
1370
|
return error instanceof AppServerRequestError
|
|
1283
1371
|
&& error.method === "turn/start"
|
|
1284
1372
|
&& /invalid cwd/i.test(`${error.serverMessage} ${error.data === undefined ? "" : JSON.stringify(error.data)}`);
|
|
1285
1373
|
}
|
|
1286
1374
|
|
|
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
1375
|
function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary {
|
|
1327
1376
|
const lastTurn = thread.turns?.at(-1);
|
|
1328
1377
|
const lastCompletedTurn = [...(thread.turns ?? [])]
|