pockcode 0.0.1
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/build/client/assets/addon-fit-DthTIhi3.js +1 -0
- package/build/client/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/build/client/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/build/client/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/build/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/build/client/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/build/client/assets/index-D7-eRInW.css +2 -0
- package/build/client/assets/index-DQn6eSCt.js +31 -0
- package/build/client/assets/xterm-DooSxjI5.js +36 -0
- package/build/client/index.html +27 -0
- package/dist/assets/api.server-D6ONM6ZJ.js +1736 -0
- package/dist/assets/auth.server-B_P8Jm6O.js +87 -0
- package/dist/assets/chat-status-monitor.server-D2VG_D8P.js +136 -0
- package/dist/assets/chats.service-H6m23Fna.js +4684 -0
- package/dist/assets/manager.server-Cru4ZxHo.js +1334 -0
- package/dist/assets/message-schedule-monitor.server-C4NZg62k.js +32 -0
- package/dist/assets/message-schedules.service-Be9t4LDg.js +407 -0
- package/dist/assets/runtime-paths.server-D6rRjGVb.js +32 -0
- package/dist/assets/socket.server-7dC-9Fnw.js +436 -0
- package/dist/pockcode.js +305 -0
- package/package.json +71 -0
- package/prisma/schema.prisma +234 -0
|
@@ -0,0 +1,4684 @@
|
|
|
1
|
+
import { i as publishProviderEvent, l as HttpError } from "./socket.server-7dC-9Fnw.js";
|
|
2
|
+
import { a as resolveProviderDataHome, i as resolvePockcodeDatabasePath, n as resolveHomePath, o as sqliteDatabaseUrl, t as ensureParentDirectory } from "./runtime-paths.server-D6rRjGVb.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { copyFile, mkdir, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import "dotenv/config";
|
|
9
|
+
import { PrismaClient } from "@prisma/client";
|
|
10
|
+
import { execFile, spawn } from "node:child_process";
|
|
11
|
+
//#region app/server/prisma.server.ts
|
|
12
|
+
var globalForPrisma = globalThis;
|
|
13
|
+
var databasePath = resolvePockcodeDatabasePath();
|
|
14
|
+
process.env.DATABASE_URL = sqliteDatabaseUrl(databasePath);
|
|
15
|
+
ensureParentDirectory(databasePath);
|
|
16
|
+
var cachedPrisma = globalForPrisma.prisma;
|
|
17
|
+
var prisma = cachedPrisma && hasCurrentPrismaDelegates(cachedPrisma) ? cachedPrisma : createPrismaClient();
|
|
18
|
+
if (cachedPrisma && cachedPrisma !== prisma) cachedPrisma.$disconnect().catch(() => void 0);
|
|
19
|
+
function createPrismaClient() {
|
|
20
|
+
return new PrismaClient({ datasources: { db: { url: process.env.DATABASE_URL } } });
|
|
21
|
+
}
|
|
22
|
+
function hasCurrentPrismaDelegates(client) {
|
|
23
|
+
const maybeClient = client;
|
|
24
|
+
return Boolean(maybeClient.mcpServer && maybeClient.mcpServerInstallation);
|
|
25
|
+
}
|
|
26
|
+
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region app/server/database.server.ts
|
|
29
|
+
var ensurePromise = null;
|
|
30
|
+
function ensureDatabase() {
|
|
31
|
+
ensurePromise ??= setupDatabase();
|
|
32
|
+
return ensurePromise;
|
|
33
|
+
}
|
|
34
|
+
async function setupDatabase() {
|
|
35
|
+
await prisma.$executeRawUnsafe("PRAGMA foreign_keys = ON");
|
|
36
|
+
await prisma.$queryRawUnsafe("PRAGMA journal_mode = WAL");
|
|
37
|
+
await prisma.$queryRawUnsafe("PRAGMA busy_timeout = 30000");
|
|
38
|
+
for (const statement of schemaStatements) await prisma.$executeRawUnsafe(statement);
|
|
39
|
+
await ensureWorkspaceHistoryIds();
|
|
40
|
+
}
|
|
41
|
+
async function ensureWorkspaceHistoryIds() {
|
|
42
|
+
if (!(await prisma.$queryRawUnsafe(`PRAGMA table_info("WorkspaceHistory")`)).some((column) => column.name === "id")) await prisma.$executeRawUnsafe(`ALTER TABLE "WorkspaceHistory" ADD COLUMN "id" TEXT`);
|
|
43
|
+
const rows = await prisma.$queryRawUnsafe(`SELECT "path", "id" FROM "WorkspaceHistory" WHERE "id" IS NULL OR "id" = ''`);
|
|
44
|
+
for (const row of rows) await prisma.$executeRawUnsafe(`UPDATE "WorkspaceHistory" SET "id" = ? WHERE "path" = ?`, randomUUID(), row.path);
|
|
45
|
+
await prisma.$executeRawUnsafe(`CREATE UNIQUE INDEX IF NOT EXISTS "WorkspaceHistory_id_key" ON "WorkspaceHistory"("id")`);
|
|
46
|
+
}
|
|
47
|
+
var schemaStatements = [
|
|
48
|
+
`CREATE TABLE IF NOT EXISTS "ProviderSetting" (
|
|
49
|
+
"providerId" TEXT NOT NULL PRIMARY KEY,
|
|
50
|
+
"settings" JSONB NOT NULL DEFAULT '{}',
|
|
51
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
52
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
53
|
+
)`,
|
|
54
|
+
`CREATE TABLE IF NOT EXISTS "PluginSetting" (
|
|
55
|
+
"pluginId" TEXT NOT NULL PRIMARY KEY,
|
|
56
|
+
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
|
57
|
+
"settings" JSONB NOT NULL DEFAULT '{}',
|
|
58
|
+
"secrets" JSONB NOT NULL DEFAULT '{}',
|
|
59
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
60
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
61
|
+
)`,
|
|
62
|
+
`CREATE TABLE IF NOT EXISTS "PluginState" (
|
|
63
|
+
"pluginId" TEXT NOT NULL PRIMARY KEY,
|
|
64
|
+
"state" JSONB NOT NULL DEFAULT '{}',
|
|
65
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
66
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
67
|
+
)`,
|
|
68
|
+
`CREATE TABLE IF NOT EXISTS "ProviderAccount" (
|
|
69
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
70
|
+
"providerId" TEXT NOT NULL,
|
|
71
|
+
"displayName" TEXT NOT NULL,
|
|
72
|
+
"status" TEXT NOT NULL DEFAULT 'DISCONNECTED',
|
|
73
|
+
"settings" JSONB NOT NULL DEFAULT '{}',
|
|
74
|
+
"runtimeDefaults" JSONB NOT NULL DEFAULT '{}',
|
|
75
|
+
"authState" JSONB,
|
|
76
|
+
"lastAuthUrl" TEXT,
|
|
77
|
+
"lastAuthMode" TEXT,
|
|
78
|
+
"lastAuthLoginId" TEXT,
|
|
79
|
+
"lastAuthUserCode" TEXT,
|
|
80
|
+
"lastError" TEXT,
|
|
81
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
82
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
83
|
+
)`,
|
|
84
|
+
`CREATE INDEX IF NOT EXISTS "ProviderAccount_providerId_idx" ON "ProviderAccount"("providerId")`,
|
|
85
|
+
`CREATE INDEX IF NOT EXISTS "ProviderAccount_status_idx" ON "ProviderAccount"("status")`,
|
|
86
|
+
`CREATE TABLE IF NOT EXISTS "McpServer" (
|
|
87
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
88
|
+
"name" TEXT NOT NULL,
|
|
89
|
+
"displayName" TEXT,
|
|
90
|
+
"transport" TEXT NOT NULL,
|
|
91
|
+
"config" JSONB NOT NULL DEFAULT '{}',
|
|
92
|
+
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
|
93
|
+
"required" BOOLEAN NOT NULL DEFAULT false,
|
|
94
|
+
"startupTimeoutSec" REAL,
|
|
95
|
+
"toolTimeoutSec" REAL,
|
|
96
|
+
"toolPolicy" JSONB NOT NULL DEFAULT '{}',
|
|
97
|
+
"adapterSettings" JSONB NOT NULL DEFAULT '{}',
|
|
98
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
99
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
100
|
+
)`,
|
|
101
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "McpServer_name_key" ON "McpServer"("name")`,
|
|
102
|
+
`CREATE TABLE IF NOT EXISTS "McpServerInstallation" (
|
|
103
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
104
|
+
"serverId" TEXT NOT NULL,
|
|
105
|
+
"providerId" TEXT NOT NULL,
|
|
106
|
+
"accountId" TEXT NOT NULL,
|
|
107
|
+
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
|
108
|
+
"lastSyncAt" DATETIME,
|
|
109
|
+
"lastStatus" TEXT,
|
|
110
|
+
"lastError" TEXT,
|
|
111
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
112
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
113
|
+
CONSTRAINT "McpServerInstallation_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "McpServer" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
114
|
+
)`,
|
|
115
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "McpServerInstallation_serverId_providerId_accountId_key" ON "McpServerInstallation"("serverId", "providerId", "accountId")`,
|
|
116
|
+
`CREATE INDEX IF NOT EXISTS "McpServerInstallation_providerId_accountId_idx" ON "McpServerInstallation"("providerId", "accountId")`,
|
|
117
|
+
`CREATE TABLE IF NOT EXISTS "Chat" (
|
|
118
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
119
|
+
"providerId" TEXT NOT NULL,
|
|
120
|
+
"accountId" TEXT,
|
|
121
|
+
"autoRotateAccount" BOOLEAN NOT NULL DEFAULT false,
|
|
122
|
+
"title" TEXT NOT NULL,
|
|
123
|
+
"workingDirectory" TEXT,
|
|
124
|
+
"model" TEXT,
|
|
125
|
+
"reasoningEffort" TEXT,
|
|
126
|
+
"serviceTier" TEXT,
|
|
127
|
+
"collaborationMode" TEXT NOT NULL DEFAULT 'default',
|
|
128
|
+
"permissionMode" TEXT NOT NULL DEFAULT 'default',
|
|
129
|
+
"status" TEXT NOT NULL DEFAULT 'IDLE',
|
|
130
|
+
"externalThreadId" TEXT,
|
|
131
|
+
"lastActivityAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
132
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
133
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
134
|
+
CONSTRAINT "Chat_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "ProviderAccount" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
|
135
|
+
)`,
|
|
136
|
+
`CREATE INDEX IF NOT EXISTS "Chat_providerId_idx" ON "Chat"("providerId")`,
|
|
137
|
+
`CREATE INDEX IF NOT EXISTS "Chat_accountId_idx" ON "Chat"("accountId")`,
|
|
138
|
+
`CREATE INDEX IF NOT EXISTS "Chat_updatedAt_idx" ON "Chat"("updatedAt")`,
|
|
139
|
+
`CREATE INDEX IF NOT EXISTS "Chat_lastActivityAt_idx" ON "Chat"("lastActivityAt")`,
|
|
140
|
+
`CREATE INDEX IF NOT EXISTS "Chat_externalThreadId_idx" ON "Chat"("externalThreadId")`,
|
|
141
|
+
`CREATE TABLE IF NOT EXISTS "ChatRun" (
|
|
142
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
143
|
+
"chatId" TEXT NOT NULL,
|
|
144
|
+
"providerId" TEXT NOT NULL,
|
|
145
|
+
"accountId" TEXT,
|
|
146
|
+
"status" TEXT NOT NULL DEFAULT 'QUEUED',
|
|
147
|
+
"request" JSONB NOT NULL DEFAULT '{}',
|
|
148
|
+
"error" TEXT,
|
|
149
|
+
"externalTurnId" TEXT,
|
|
150
|
+
"interruptRequestedAt" DATETIME,
|
|
151
|
+
"startedAt" DATETIME,
|
|
152
|
+
"endedAt" DATETIME,
|
|
153
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
154
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
155
|
+
CONSTRAINT "ChatRun_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
|
156
|
+
CONSTRAINT "ChatRun_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "ProviderAccount" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
|
157
|
+
)`,
|
|
158
|
+
`CREATE INDEX IF NOT EXISTS "ChatRun_chatId_createdAt_idx" ON "ChatRun"("chatId", "createdAt")`,
|
|
159
|
+
`CREATE INDEX IF NOT EXISTS "ChatRun_providerId_idx" ON "ChatRun"("providerId")`,
|
|
160
|
+
`CREATE INDEX IF NOT EXISTS "ChatRun_accountId_idx" ON "ChatRun"("accountId")`,
|
|
161
|
+
`CREATE INDEX IF NOT EXISTS "ChatRun_externalTurnId_idx" ON "ChatRun"("externalTurnId")`,
|
|
162
|
+
`CREATE TABLE IF NOT EXISTS "MessageSchedule" (
|
|
163
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
164
|
+
"title" TEXT NOT NULL,
|
|
165
|
+
"message" TEXT NOT NULL,
|
|
166
|
+
"workingDirectory" TEXT NOT NULL,
|
|
167
|
+
"chatId" TEXT,
|
|
168
|
+
"providerId" TEXT NOT NULL,
|
|
169
|
+
"accountId" TEXT,
|
|
170
|
+
"model" TEXT,
|
|
171
|
+
"reasoningEffort" TEXT,
|
|
172
|
+
"serviceTier" TEXT,
|
|
173
|
+
"collaborationMode" TEXT NOT NULL DEFAULT 'default',
|
|
174
|
+
"permissionMode" TEXT NOT NULL DEFAULT 'default',
|
|
175
|
+
"goalObjective" TEXT,
|
|
176
|
+
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
|
|
177
|
+
"recurrence" JSONB NOT NULL DEFAULT '{}',
|
|
178
|
+
"nextRunAt" DATETIME,
|
|
179
|
+
"lastRunAt" DATETIME,
|
|
180
|
+
"lastRunStatus" TEXT,
|
|
181
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
182
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
183
|
+
)`,
|
|
184
|
+
`CREATE INDEX IF NOT EXISTS "MessageSchedule_workingDirectory_idx" ON "MessageSchedule"("workingDirectory")`,
|
|
185
|
+
`CREATE INDEX IF NOT EXISTS "MessageSchedule_status_nextRunAt_idx" ON "MessageSchedule"("status", "nextRunAt")`,
|
|
186
|
+
`CREATE INDEX IF NOT EXISTS "MessageSchedule_chatId_idx" ON "MessageSchedule"("chatId")`,
|
|
187
|
+
`CREATE INDEX IF NOT EXISTS "MessageSchedule_accountId_idx" ON "MessageSchedule"("accountId")`,
|
|
188
|
+
`CREATE TABLE IF NOT EXISTS "MessageScheduleRun" (
|
|
189
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
190
|
+
"scheduleId" TEXT NOT NULL,
|
|
191
|
+
"chatId" TEXT,
|
|
192
|
+
"chatRunId" TEXT,
|
|
193
|
+
"scheduledFor" DATETIME NOT NULL,
|
|
194
|
+
"status" TEXT NOT NULL DEFAULT 'QUEUED',
|
|
195
|
+
"error" TEXT,
|
|
196
|
+
"startedAt" DATETIME,
|
|
197
|
+
"endedAt" DATETIME,
|
|
198
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
199
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
200
|
+
CONSTRAINT "MessageScheduleRun_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "MessageSchedule" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
201
|
+
)`,
|
|
202
|
+
`CREATE INDEX IF NOT EXISTS "MessageScheduleRun_scheduleId_scheduledFor_idx" ON "MessageScheduleRun"("scheduleId", "scheduledFor")`,
|
|
203
|
+
`CREATE INDEX IF NOT EXISTS "MessageScheduleRun_chatId_idx" ON "MessageScheduleRun"("chatId")`,
|
|
204
|
+
`CREATE INDEX IF NOT EXISTS "MessageScheduleRun_chatRunId_idx" ON "MessageScheduleRun"("chatRunId")`,
|
|
205
|
+
`CREATE INDEX IF NOT EXISTS "MessageScheduleRun_status_idx" ON "MessageScheduleRun"("status")`,
|
|
206
|
+
`CREATE TABLE IF NOT EXISTS "WorkspaceHistory" (
|
|
207
|
+
"id" TEXT,
|
|
208
|
+
"path" TEXT NOT NULL PRIMARY KEY,
|
|
209
|
+
"name" TEXT NOT NULL,
|
|
210
|
+
"lastOpenedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
211
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
212
|
+
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
213
|
+
)`,
|
|
214
|
+
`CREATE INDEX IF NOT EXISTS "WorkspaceHistory_lastOpenedAt_idx" ON "WorkspaceHistory"("lastOpenedAt")`
|
|
215
|
+
];
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region app/server/json.server.ts
|
|
218
|
+
function asJsonObject(value) {
|
|
219
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
function readString(value) {
|
|
223
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
224
|
+
}
|
|
225
|
+
function readBoolean(value) {
|
|
226
|
+
return typeof value === "boolean" ? value : void 0;
|
|
227
|
+
}
|
|
228
|
+
function readNumber(value) {
|
|
229
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
230
|
+
}
|
|
231
|
+
function normalizeJsonObject(value) {
|
|
232
|
+
return asJsonObject(value) ?? {};
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region app/server/providers/codex.server.ts
|
|
236
|
+
var require = createRequire(import.meta.url);
|
|
237
|
+
var invalidatedAccountIds = /* @__PURE__ */ new Set();
|
|
238
|
+
var maxProviderChats = 200;
|
|
239
|
+
var codexLiveStatusReadLimit = 8;
|
|
240
|
+
var codexLogReadLimit = 5e3;
|
|
241
|
+
var codexSessionSummaryTailBytes = 1024 * 1024;
|
|
242
|
+
var codexInstructionsFileName = "AGENTS.md";
|
|
243
|
+
var codexTurnCompletionTimeoutMs = 1800 * 1e3;
|
|
244
|
+
var codexMessageReadTimeoutMs = 5e3;
|
|
245
|
+
var codexStoredRunningFreshnessMs = 1800 * 1e3;
|
|
246
|
+
var codexDefaultModel = "gpt-5.5";
|
|
247
|
+
var codexDefaultReasoningEffort = "medium";
|
|
248
|
+
var codexDefaultServiceTier = "standard";
|
|
249
|
+
var codexSupportedReasoningEfforts = [
|
|
250
|
+
{
|
|
251
|
+
description: "None",
|
|
252
|
+
reasoningEffort: "none"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
description: "Minimal",
|
|
256
|
+
reasoningEffort: "minimal"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
description: "Low",
|
|
260
|
+
reasoningEffort: "low"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
description: "Medium",
|
|
264
|
+
reasoningEffort: "medium"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
description: "High",
|
|
268
|
+
reasoningEffort: "high"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
description: "Extra High",
|
|
272
|
+
reasoningEffort: "xhigh"
|
|
273
|
+
}
|
|
274
|
+
];
|
|
275
|
+
var codexDefaultModelOptions = [
|
|
276
|
+
{
|
|
277
|
+
id: "gpt-5.5",
|
|
278
|
+
model: "gpt-5.5",
|
|
279
|
+
displayName: "GPT-5.5",
|
|
280
|
+
defaultReasoningEffort: codexDefaultReasoningEffort,
|
|
281
|
+
supportedReasoningEfforts: codexSupportedReasoningEfforts
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "gpt-5.4",
|
|
285
|
+
model: "gpt-5.4",
|
|
286
|
+
displayName: "GPT-5.4",
|
|
287
|
+
defaultReasoningEffort: codexDefaultReasoningEffort,
|
|
288
|
+
supportedReasoningEfforts: codexSupportedReasoningEfforts
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: "gpt-5.4-mini",
|
|
292
|
+
model: "gpt-5.4-mini",
|
|
293
|
+
displayName: "GPT-5.4-Mini",
|
|
294
|
+
defaultReasoningEffort: codexDefaultReasoningEffort,
|
|
295
|
+
supportedReasoningEfforts: codexSupportedReasoningEfforts
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "gpt-5.3-codex-spark",
|
|
299
|
+
model: "gpt-5.3-codex-spark",
|
|
300
|
+
displayName: "GPT-5.3-Codex-Spark",
|
|
301
|
+
defaultReasoningEffort: codexDefaultReasoningEffort,
|
|
302
|
+
supportedReasoningEfforts: codexSupportedReasoningEfforts
|
|
303
|
+
}
|
|
304
|
+
];
|
|
305
|
+
var codexProviderAdapter = {
|
|
306
|
+
definition: {
|
|
307
|
+
id: "codex",
|
|
308
|
+
label: "OpenAI Codex",
|
|
309
|
+
icon: "codex",
|
|
310
|
+
capabilities: [
|
|
311
|
+
"auth",
|
|
312
|
+
"chat",
|
|
313
|
+
"history",
|
|
314
|
+
"limits",
|
|
315
|
+
"models",
|
|
316
|
+
"accountSwitchHooks",
|
|
317
|
+
"localRuntime",
|
|
318
|
+
"threadLifecycle",
|
|
319
|
+
"fork",
|
|
320
|
+
"archive",
|
|
321
|
+
"review",
|
|
322
|
+
"compact",
|
|
323
|
+
"usage",
|
|
324
|
+
"skills",
|
|
325
|
+
"hooks",
|
|
326
|
+
"plugins",
|
|
327
|
+
"mcp",
|
|
328
|
+
"config",
|
|
329
|
+
"commandExec",
|
|
330
|
+
"shellCommand",
|
|
331
|
+
"providerSwitch"
|
|
332
|
+
],
|
|
333
|
+
composerFeatures: [
|
|
334
|
+
"accessMode",
|
|
335
|
+
"fileAttachment",
|
|
336
|
+
"folderAttachment",
|
|
337
|
+
"goal",
|
|
338
|
+
"imageAttachment",
|
|
339
|
+
"planMode"
|
|
340
|
+
],
|
|
341
|
+
defaultSettings: defaultCodexSettings(),
|
|
342
|
+
settingsFields: [
|
|
343
|
+
{
|
|
344
|
+
key: "accountsHome",
|
|
345
|
+
label: "Accounts home",
|
|
346
|
+
type: "path",
|
|
347
|
+
required: true
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
key: "historyHome",
|
|
351
|
+
label: "History home",
|
|
352
|
+
type: "path",
|
|
353
|
+
required: true
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
key: "sharedChatHome",
|
|
357
|
+
label: "Shared Codex home",
|
|
358
|
+
type: "path",
|
|
359
|
+
required: true
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
key: "defaultCommand",
|
|
363
|
+
label: "Command",
|
|
364
|
+
type: "string",
|
|
365
|
+
required: true
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
key: "defaultArgs",
|
|
369
|
+
label: "Arguments",
|
|
370
|
+
type: "stringArray",
|
|
371
|
+
required: true
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
key: "defaultEnvironment",
|
|
375
|
+
label: "Environment",
|
|
376
|
+
type: "json"
|
|
377
|
+
}
|
|
378
|
+
],
|
|
379
|
+
accountFields: [
|
|
380
|
+
{
|
|
381
|
+
key: "codexHome",
|
|
382
|
+
label: "Codex home override",
|
|
383
|
+
type: "path"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
key: "personality",
|
|
387
|
+
label: "Personality",
|
|
388
|
+
type: "string"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
key: "command",
|
|
392
|
+
label: "Command override",
|
|
393
|
+
type: "string"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
key: "args",
|
|
397
|
+
label: "Arguments override",
|
|
398
|
+
type: "stringArray"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
key: "environment",
|
|
402
|
+
label: "Environment",
|
|
403
|
+
type: "json"
|
|
404
|
+
}
|
|
405
|
+
],
|
|
406
|
+
runtimeFields: [
|
|
407
|
+
{
|
|
408
|
+
key: "model",
|
|
409
|
+
label: "Model",
|
|
410
|
+
type: "string"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
key: "reasoningEffort",
|
|
414
|
+
label: "Reasoning",
|
|
415
|
+
type: "string"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
key: "serviceTier",
|
|
419
|
+
label: "Service tier",
|
|
420
|
+
type: "string"
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
key: "permissionMode",
|
|
424
|
+
label: "Permission mode",
|
|
425
|
+
type: "string"
|
|
426
|
+
}
|
|
427
|
+
]
|
|
428
|
+
},
|
|
429
|
+
defaultSettings: defaultCodexSettings,
|
|
430
|
+
defaultAccountSettings: () => ({ personality: "pragmatic" }),
|
|
431
|
+
defaultRuntimeDefaults: () => ({
|
|
432
|
+
model: codexDefaultModel,
|
|
433
|
+
permissionMode: "askForApproval",
|
|
434
|
+
reasoningEffort: codexDefaultReasoningEffort,
|
|
435
|
+
serviceTier: codexDefaultServiceTier
|
|
436
|
+
}),
|
|
437
|
+
async prepareAccount(account) {
|
|
438
|
+
const codexHome = resolveAccountCodexHome(account);
|
|
439
|
+
ensureCodexHome(codexHome);
|
|
440
|
+
await syncCodexInstructionsToHome(codexHome);
|
|
441
|
+
},
|
|
442
|
+
async authenticate(account, mode = "browser") {
|
|
443
|
+
try {
|
|
444
|
+
if (mode === "local") {
|
|
445
|
+
await connectLocalCodexAccount();
|
|
446
|
+
runtimeService.stopRuntime(account.id);
|
|
447
|
+
return connectedAuthResponse(account.id, "Local Codex account is connected.", localCodexAuthState());
|
|
448
|
+
}
|
|
449
|
+
const runtime = runtimeForAccount(account);
|
|
450
|
+
if (hasAccountAuthFile(account)) {
|
|
451
|
+
runtimeService.stopRuntime(account.id);
|
|
452
|
+
return connectedAuthResponse(account.id, "Codex account is connected.");
|
|
453
|
+
}
|
|
454
|
+
const result = asJsonObject((await runtime.request("account/login/start", mode === "device" ? { type: "chatgptDeviceCode" } : {
|
|
455
|
+
type: "chatgpt",
|
|
456
|
+
codexStreamlinedLogin: true
|
|
457
|
+
}, 3e4)).result);
|
|
458
|
+
const responseMode = readString(result?.type) === "chatgptDeviceCode" ? "device" : mode;
|
|
459
|
+
const authUrl = readLoginAuthUrl(result, responseMode);
|
|
460
|
+
const loginId = readString(result?.loginId) ?? null;
|
|
461
|
+
const userCode = responseMode === "device" ? readString(result?.userCode) ?? readString(result?.user_code) ?? null : null;
|
|
462
|
+
return {
|
|
463
|
+
accountId: account.id,
|
|
464
|
+
status: authUrl ? "AUTHENTICATING" : "CONNECTED",
|
|
465
|
+
authMode: authUrl ? responseMode : null,
|
|
466
|
+
authUrl,
|
|
467
|
+
verificationUrl: responseMode === "device" ? authUrl : null,
|
|
468
|
+
userCode,
|
|
469
|
+
loginId,
|
|
470
|
+
message: authUrl ? responseMode === "device" ? "Open the verification URL and enter the device code to finish Codex authentication." : "Complete Codex authentication in the opened browser." : "Codex account is connected."
|
|
471
|
+
};
|
|
472
|
+
} catch (error) {
|
|
473
|
+
return {
|
|
474
|
+
accountId: account.id,
|
|
475
|
+
status: "ERROR",
|
|
476
|
+
authMode: mode,
|
|
477
|
+
authUrl: null,
|
|
478
|
+
verificationUrl: null,
|
|
479
|
+
userCode: null,
|
|
480
|
+
message: readErrorMessage$1(error)
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
async cancelAuthentication(account) {
|
|
485
|
+
runtimeService.stopRuntime(account.id);
|
|
486
|
+
},
|
|
487
|
+
async completeAuthentication(account, redirectUrl) {
|
|
488
|
+
const callbackUrl = parseLoopbackCallbackUrl(redirectUrl);
|
|
489
|
+
const response = await fetch(callbackUrl);
|
|
490
|
+
if (response.status >= 400) throw new Error(`Codex login callback returned HTTP ${response.status}.`);
|
|
491
|
+
if (hasAccountAuthFile(account)) {
|
|
492
|
+
runtimeService.stopRuntime(account.id);
|
|
493
|
+
return connectedAuthResponse(account.id, "Codex account is connected.");
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
accountId: account.id,
|
|
497
|
+
status: "AUTHENTICATING",
|
|
498
|
+
authMode: readAuthMode(account.lastAuthMode),
|
|
499
|
+
authUrl: account.lastAuthUrl,
|
|
500
|
+
verificationUrl: account.lastAuthMode === "device" ? account.lastAuthUrl : null,
|
|
501
|
+
userCode: account.lastAuthUserCode,
|
|
502
|
+
message: "Callback accepted. Codex is still finishing authentication."
|
|
503
|
+
};
|
|
504
|
+
},
|
|
505
|
+
async listModels(account) {
|
|
506
|
+
try {
|
|
507
|
+
return normalizeModelList((await runtimeForAccount(account).request("model/list", {
|
|
508
|
+
includeHidden: false,
|
|
509
|
+
limit: 100
|
|
510
|
+
}, 3e4)).result);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
await maybeMarkInvalidated(account.id, error);
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
async readLimits(account) {
|
|
517
|
+
try {
|
|
518
|
+
return normalizeLimits((await runtimeForAccount(account).request("account/rateLimits/read", void 0, 3e4)).result);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
await maybeMarkInvalidated(account.id, error);
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
listChats(account) {
|
|
525
|
+
return listCodexChats(account);
|
|
526
|
+
},
|
|
527
|
+
loadChatMessages(account, externalThreadId) {
|
|
528
|
+
return loadCodexChatMessages(account, externalThreadId);
|
|
529
|
+
},
|
|
530
|
+
readChatStatus(account, externalThreadId) {
|
|
531
|
+
return readCodexChatStatus(account, externalThreadId);
|
|
532
|
+
},
|
|
533
|
+
readCachedChatStates(account, externalThreadIds) {
|
|
534
|
+
return readCodexStoredChatStates(account, externalThreadIds);
|
|
535
|
+
},
|
|
536
|
+
async forkThread(account, externalThreadId, request, workingDirectory) {
|
|
537
|
+
const response = await requestCodexRuntime(await prepareCodexThreadForLifecycleAction(account, externalThreadId, workingDirectory), "thread/fork", {
|
|
538
|
+
threadId: externalThreadId,
|
|
539
|
+
lastTurnId: request?.lastTurnId ?? null,
|
|
540
|
+
excludeTurns: true,
|
|
541
|
+
threadSource: "appServer"
|
|
542
|
+
}, 6e4);
|
|
543
|
+
const result = asJsonObject(response.result);
|
|
544
|
+
const thread = asJsonObject(result?.thread);
|
|
545
|
+
const nextThreadId = readString(thread?.id) ?? readString(result?.threadId) ?? readString(result?.thread_id);
|
|
546
|
+
if (!nextThreadId) throw new Error("Codex did not return a forked thread id.");
|
|
547
|
+
await syncAccountThreadToCanonical(nextThreadId, account);
|
|
548
|
+
return {
|
|
549
|
+
externalThreadId: nextThreadId,
|
|
550
|
+
title: readString(thread?.name) ?? readString(thread?.preview) ?? null,
|
|
551
|
+
workingDirectory: readString(result?.cwd) ?? readString(thread?.cwd) ?? null,
|
|
552
|
+
raw: jsonFromUnknown(response)
|
|
553
|
+
};
|
|
554
|
+
},
|
|
555
|
+
async archiveThread(account, externalThreadId, workingDirectory) {
|
|
556
|
+
return requestCodexOptional(await prepareCodexThreadForLifecycleAction(account, externalThreadId, workingDirectory), "thread/archive", { threadId: externalThreadId }, 3e4);
|
|
557
|
+
},
|
|
558
|
+
async deleteThread(account, externalThreadId) {
|
|
559
|
+
return requestCodexOptional(runtimeForAccount(account), "thread/delete", { threadId: externalThreadId }, 3e4);
|
|
560
|
+
},
|
|
561
|
+
async renameThread(account, externalThreadId, title, workingDirectory) {
|
|
562
|
+
return requestCodexOptional(await prepareCodexThreadForLifecycleAction(account, externalThreadId, workingDirectory), "thread/name/set", {
|
|
563
|
+
threadId: externalThreadId,
|
|
564
|
+
name: title
|
|
565
|
+
}, 3e4);
|
|
566
|
+
},
|
|
567
|
+
async compactThread(account, externalThreadId, _request, workingDirectory) {
|
|
568
|
+
return {
|
|
569
|
+
externalThreadId,
|
|
570
|
+
raw: jsonFromUnknown(await requestCodexRuntime(await prepareCodexThreadForLifecycleAction(account, externalThreadId, workingDirectory), "thread/compact/start", { threadId: externalThreadId }, 3e4))
|
|
571
|
+
};
|
|
572
|
+
},
|
|
573
|
+
async reviewThread(account, externalThreadId, request, workingDirectory) {
|
|
574
|
+
const response = await requestCodexRuntime(await prepareCodexThreadForLifecycleAction(account, externalThreadId, workingDirectory), "review/start", {
|
|
575
|
+
threadId: externalThreadId,
|
|
576
|
+
delivery: request?.delivery ?? "inline",
|
|
577
|
+
target: codexReviewTargetPayload(request)
|
|
578
|
+
}, 3e4);
|
|
579
|
+
const result = asJsonObject(response.result);
|
|
580
|
+
const turn = asJsonObject(result?.turn);
|
|
581
|
+
return {
|
|
582
|
+
externalThreadId: readString(result?.reviewThreadId) ?? readString(result?.review_thread_id) ?? externalThreadId,
|
|
583
|
+
turnId: readString(turn?.id) ?? readString(result?.turnId) ?? readString(result?.turn_id) ?? null,
|
|
584
|
+
raw: jsonFromUnknown(response)
|
|
585
|
+
};
|
|
586
|
+
},
|
|
587
|
+
async readUsage(account) {
|
|
588
|
+
return jsonFromUnknown((await requestCodexRuntime(runtimeForAccount(account), "account/usage/read", void 0, 3e4)).result);
|
|
589
|
+
},
|
|
590
|
+
async readConfig(account, workingDirectory) {
|
|
591
|
+
return jsonFromUnknown((await requestCodexRuntime(runtimeForAccount(account, workingDirectory), "config/read", {
|
|
592
|
+
cwd: workingDirectory ?? null,
|
|
593
|
+
includeLayers: true
|
|
594
|
+
}, 3e4)).result);
|
|
595
|
+
},
|
|
596
|
+
async listSkills(account, workingDirectory) {
|
|
597
|
+
return jsonFromUnknown((await requestCodexRuntime(runtimeForAccount(account, workingDirectory), "skills/list", {
|
|
598
|
+
cwds: workingDirectory ? [workingDirectory] : [],
|
|
599
|
+
forceReload: false
|
|
600
|
+
}, 3e4)).result);
|
|
601
|
+
},
|
|
602
|
+
async listHooks(account, workingDirectory) {
|
|
603
|
+
return jsonFromUnknown((await requestCodexRuntime(runtimeForAccount(account, workingDirectory), "hooks/list", { cwds: workingDirectory ? [workingDirectory] : [] }, 3e4)).result);
|
|
604
|
+
},
|
|
605
|
+
async listPlugins(account) {
|
|
606
|
+
return jsonFromUnknown((await requestCodexRuntime(runtimeForAccount(account), "plugin/list", {}, 3e4)).result);
|
|
607
|
+
},
|
|
608
|
+
async sendMessage(account, input) {
|
|
609
|
+
if (input.threadId) await hydrateKnownCodexThreadToAccount(input.threadId, account);
|
|
610
|
+
const runtime = runtimeForAccount(account, input.workingDirectory);
|
|
611
|
+
const threadId = await ensureCodexThread(runtime, input.threadId ?? null, input.workingDirectory, input.collaborationMode);
|
|
612
|
+
await input.onThreadReady?.(threadId);
|
|
613
|
+
const settings = await resolveCollaborationSettings(runtime, input.model ?? null, input.reasoningEffort ?? null, input.serviceTier ?? null);
|
|
614
|
+
if (input.goalObjective?.trim()) await setCodexThreadGoal(runtime, threadId, input.goalObjective);
|
|
615
|
+
const accessMode = codexAccessMode(input.permissionMode);
|
|
616
|
+
let turnId = null;
|
|
617
|
+
const livePlanContentByItemId = /* @__PURE__ */ new Map();
|
|
618
|
+
const completionAbort = new AbortController();
|
|
619
|
+
const unsubscribeLiveMessages = input.onMessage ? runtime.onEvent((message) => {
|
|
620
|
+
const resolvedRequestMessage = readCodexServerRequestResolvedMessage(message, threadId);
|
|
621
|
+
if (resolvedRequestMessage) {
|
|
622
|
+
try {
|
|
623
|
+
input.onMessage?.(resolvedRequestMessage);
|
|
624
|
+
} catch {}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const serverRequestMessage = readCodexServerRequestMessage(message, threadId, turnId);
|
|
628
|
+
if (serverRequestMessage) {
|
|
629
|
+
try {
|
|
630
|
+
input.onMessage?.(serverRequestMessage);
|
|
631
|
+
} catch {}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const planUpdateMessage = readCodexPlanUpdateMessage(message, threadId, turnId);
|
|
635
|
+
if (planUpdateMessage) {
|
|
636
|
+
try {
|
|
637
|
+
input.onMessage?.(planUpdateMessage);
|
|
638
|
+
} catch {}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const liveMessage = readCodexLiveThreadItemMessage(message, threadId, turnId);
|
|
642
|
+
if (liveMessage) try {
|
|
643
|
+
input.onMessage?.(accumulateLivePlanMessage(liveMessage, livePlanContentByItemId));
|
|
644
|
+
} catch {}
|
|
645
|
+
}) : () => void 0;
|
|
646
|
+
const completion = runtime.waitForEvent((message) => isCodexTurnCompletedEvent(message, threadId, turnId), codexTurnCompletionTimeoutMs, completionAbort.signal);
|
|
647
|
+
let response = null;
|
|
648
|
+
try {
|
|
649
|
+
response = await runtime.request("turn/start", {
|
|
650
|
+
threadId,
|
|
651
|
+
cwd: input.workingDirectory,
|
|
652
|
+
input: codexInputItems(input.content, input.attachments ?? []),
|
|
653
|
+
approvalPolicy: accessMode.approvalPolicy,
|
|
654
|
+
collaborationMode: collaborationModePayload(input.collaborationMode, settings),
|
|
655
|
+
sandboxPolicy: accessMode.sandboxPolicy
|
|
656
|
+
}, 3e4);
|
|
657
|
+
const result = asJsonObject(response.result);
|
|
658
|
+
turnId = readString(asJsonObject(result?.turn)?.id) ?? readString(result?.turnId) ?? readString(result?.turn_id) ?? null;
|
|
659
|
+
if (turnId) await input.onTurnStarted?.(turnId);
|
|
660
|
+
await completion;
|
|
661
|
+
} catch (error) {
|
|
662
|
+
completionAbort.abort();
|
|
663
|
+
await completion.catch(() => void 0);
|
|
664
|
+
throw error;
|
|
665
|
+
} finally {
|
|
666
|
+
unsubscribeLiveMessages();
|
|
667
|
+
}
|
|
668
|
+
if (!response) throw new Error("Codex did not return a turn response.");
|
|
669
|
+
return {
|
|
670
|
+
threadId,
|
|
671
|
+
turnId,
|
|
672
|
+
raw: jsonFromUnknown(response)
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
respondToServerRequest(account, requestId, response) {
|
|
676
|
+
return runtimeForAccount(account).respondToServerRequest(requestId, response.result ?? serverRequestResultFromResponse(response));
|
|
677
|
+
},
|
|
678
|
+
async interrupt(account, threadId, turnId) {
|
|
679
|
+
const runtime = runtimeForAccount(account);
|
|
680
|
+
const activeTurnId = turnId ?? await readCodexActiveTurnId(runtime, threadId).catch(() => null);
|
|
681
|
+
try {
|
|
682
|
+
await runtime.request("turn/interrupt", {
|
|
683
|
+
threadId,
|
|
684
|
+
...activeTurnId ? { turnId: activeTurnId } : {}
|
|
685
|
+
}, 3e4);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
runtimeService.stopRuntime(account.id);
|
|
688
|
+
if (activeTurnId) throw error;
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
async steerMessage(account, input) {
|
|
692
|
+
const response = await runtimeForAccount(account, input.workingDirectory).request("turn/steer", {
|
|
693
|
+
threadId: input.threadId,
|
|
694
|
+
input: codexInputItems(input.content, input.attachments ?? []),
|
|
695
|
+
expectedTurnId: input.turnId
|
|
696
|
+
}, 3e4);
|
|
697
|
+
const result = asJsonObject(response.result);
|
|
698
|
+
return {
|
|
699
|
+
turnId: readString(result?.turnId) ?? readString(result?.turn_id) ?? input.turnId,
|
|
700
|
+
raw: jsonFromUnknown(response)
|
|
701
|
+
};
|
|
702
|
+
},
|
|
703
|
+
isAccountConnected(account) {
|
|
704
|
+
return hasAccountAuthFile(account);
|
|
705
|
+
},
|
|
706
|
+
readAccountAlias(account) {
|
|
707
|
+
return readAuthEmailAlias(account);
|
|
708
|
+
},
|
|
709
|
+
async syncThreadFromAccount(threadId, account) {
|
|
710
|
+
return syncAccountThreadToCanonical(threadId, account);
|
|
711
|
+
},
|
|
712
|
+
async hydrateThreadForAccount(threadId, account) {
|
|
713
|
+
return hydrateCanonicalThreadToAccount(threadId, account);
|
|
714
|
+
},
|
|
715
|
+
async moveThreadToAccount(context) {
|
|
716
|
+
if (!context.fromAccount) return hydrateCanonicalThreadToAccount(context.threadId, context.toAccount);
|
|
717
|
+
return moveAccountThreadToAccount(context.threadId, context.fromAccount, context.toAccount);
|
|
718
|
+
},
|
|
719
|
+
async afterAccountSwitch(context) {
|
|
720
|
+
if (context.fromAccount && context.fromAccount.id !== context.toAccount.id && !sameFilesystemPath(resolveAccountCodexHome(context.fromAccount), resolveAccountCodexHome(context.toAccount))) await removeAccountThread(context.threadId, context.fromAccount);
|
|
721
|
+
},
|
|
722
|
+
stopAccountRuntime(accountId) {
|
|
723
|
+
runtimeService.stopRuntime(accountId);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
function defaultCodexSettings() {
|
|
727
|
+
const codexBin = resolveCodexBin();
|
|
728
|
+
return {
|
|
729
|
+
accountsHome: join(resolveProviderDataHome("codex"), "accounts"),
|
|
730
|
+
historyHome: join(resolveProviderDataHome("codex"), "history"),
|
|
731
|
+
sharedChatHome: resolveHomePath("~/.codex"),
|
|
732
|
+
defaultCommand: process.execPath,
|
|
733
|
+
defaultArgs: [
|
|
734
|
+
codexBin,
|
|
735
|
+
"app-server",
|
|
736
|
+
"--enable",
|
|
737
|
+
"goals",
|
|
738
|
+
"--enable",
|
|
739
|
+
"collaboration_modes"
|
|
740
|
+
],
|
|
741
|
+
defaultEnvironment: {}
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function runtimeForAccount(account, workingDirectory) {
|
|
745
|
+
return runtimeService.getRuntime(runtimeConfigForAccount(account, workingDirectory));
|
|
746
|
+
}
|
|
747
|
+
function runtimeConfigForAccount(account, workingDirectory) {
|
|
748
|
+
const providerSettings = mergedProviderSettings();
|
|
749
|
+
const settings = normalizeJsonObject(account.settings);
|
|
750
|
+
const runtimeDefaults = normalizeJsonObject(account.runtimeDefaults);
|
|
751
|
+
const defaultEnvironment = readStringRecord(providerSettings.defaultEnvironment);
|
|
752
|
+
const accountEnvironment = readStringRecord(settings.environment);
|
|
753
|
+
const args = readStringArray(settings.args) ?? readStringArray(providerSettings.defaultArgs) ?? [resolveCodexBin(), "app-server"];
|
|
754
|
+
return {
|
|
755
|
+
accountId: account.id,
|
|
756
|
+
args: codexArgsWithPersonality(args, readCodexPersonality(settings.personality)),
|
|
757
|
+
codexHome: resolveAccountCodexHome(account),
|
|
758
|
+
command: readString(settings.command) ?? readString(providerSettings.defaultCommand) ?? process.execPath,
|
|
759
|
+
environment: {
|
|
760
|
+
...defaultEnvironment,
|
|
761
|
+
...accountEnvironment
|
|
762
|
+
},
|
|
763
|
+
workingDirectory: workingDirectory ?? readString(runtimeDefaults.workingDirectory) ?? null
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function mergedProviderSettings() {
|
|
767
|
+
return defaultCodexSettings();
|
|
768
|
+
}
|
|
769
|
+
function resolveAccountCodexHome(account) {
|
|
770
|
+
if (usesSharedCodexHome(account)) return resolveSharedCodexHome();
|
|
771
|
+
const explicitHome = readString(normalizeJsonObject(account.settings).codexHome);
|
|
772
|
+
if (explicitHome && !sameFilesystemPath(resolveHomePath(explicitHome), resolveSharedCodexHome())) return resolveHomePath(explicitHome);
|
|
773
|
+
return join(resolveHomePath(readString(mergedProviderSettings().accountsHome) ?? "~/.pockcode/providers/codex/accounts"), account.id);
|
|
774
|
+
}
|
|
775
|
+
function resolveCodexAccountHome(account) {
|
|
776
|
+
return resolveAccountCodexHome(account);
|
|
777
|
+
}
|
|
778
|
+
async function reloadCodexMcpServerConfig(account) {
|
|
779
|
+
await runtimeForAccount(account).request("config/mcpServer/reload", void 0, 3e4);
|
|
780
|
+
}
|
|
781
|
+
async function listCodexMcpServerStatuses(account) {
|
|
782
|
+
return readCodexMcpServerStatusList((await runtimeForAccount(account).request("mcpServerStatus/list", {
|
|
783
|
+
detail: "toolsAndAuthOnly",
|
|
784
|
+
limit: 200
|
|
785
|
+
}, 6e4)).result);
|
|
786
|
+
}
|
|
787
|
+
async function startCodexMcpOauthLogin(account, serverName, scopes) {
|
|
788
|
+
const params = { name: serverName };
|
|
789
|
+
if (scopes?.length) params.scopes = scopes;
|
|
790
|
+
const result = asJsonObject((await runtimeForAccount(account).request("mcpServer/oauth/login", params, 12e4)).result) ?? {};
|
|
791
|
+
const authorizationUrl = readString(result.authorizationUrl) ?? readString(result.authorization_url) ?? readString(result.authUrl) ?? readString(result.auth_url);
|
|
792
|
+
if (!authorizationUrl) throw new Error("Codex did not return an MCP authorization URL.");
|
|
793
|
+
return { authorizationUrl };
|
|
794
|
+
}
|
|
795
|
+
function readCodexMcpServerStatusList(result) {
|
|
796
|
+
if (Array.isArray(result)) return result;
|
|
797
|
+
const record = asJsonObject(result);
|
|
798
|
+
const statuses = record?.mcpServers ?? record?.servers ?? record?.data;
|
|
799
|
+
if (Array.isArray(statuses)) return statuses;
|
|
800
|
+
if (statuses && typeof statuses === "object" && !Array.isArray(statuses)) return Object.values(statuses);
|
|
801
|
+
return [];
|
|
802
|
+
}
|
|
803
|
+
function readCodexHistoryWatchPaths(account) {
|
|
804
|
+
const paths = [resolveSharedCodexHome(), resolveCanonicalHistoryHome()];
|
|
805
|
+
if (account) paths.push(resolveAccountCodexHome(account));
|
|
806
|
+
return [...new Set(paths)];
|
|
807
|
+
}
|
|
808
|
+
function resolveCanonicalHistoryHome() {
|
|
809
|
+
return resolveHomePath(readString(mergedProviderSettings().historyHome) ?? "~/.pockcode/providers/codex/history");
|
|
810
|
+
}
|
|
811
|
+
function resolveSharedCodexHome() {
|
|
812
|
+
return resolveHomePath(readString(mergedProviderSettings().sharedChatHome) ?? "~/.codex");
|
|
813
|
+
}
|
|
814
|
+
function ensureCodexHome(codexHome) {
|
|
815
|
+
mkdirSync(codexHome, {
|
|
816
|
+
recursive: true,
|
|
817
|
+
mode: 448
|
|
818
|
+
});
|
|
819
|
+
chmodSync(codexHome, 448);
|
|
820
|
+
}
|
|
821
|
+
async function readCodexInstructions() {
|
|
822
|
+
const homes = await readCodexInstructionHomes();
|
|
823
|
+
return {
|
|
824
|
+
instructions: await readLatestExistingCodexInstructions(homes) ?? "",
|
|
825
|
+
paths: homes.map(codexInstructionsPath)
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
async function updateCodexInstructions(instructions) {
|
|
829
|
+
const homes = await readCodexInstructionHomes();
|
|
830
|
+
const normalized = instructions.trimEnd();
|
|
831
|
+
await Promise.all(homes.map((home) => writeCodexInstructions(home, normalized)));
|
|
832
|
+
return {
|
|
833
|
+
instructions: normalized,
|
|
834
|
+
paths: homes.map(codexInstructionsPath)
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
async function syncCodexInstructionsToHome(codexHome) {
|
|
838
|
+
const instructions = await readLatestExistingCodexInstructions([resolveSharedCodexHome(), resolveCanonicalHistoryHome()]);
|
|
839
|
+
if (instructions === null) return;
|
|
840
|
+
await writeCodexInstructions(codexHome, instructions);
|
|
841
|
+
}
|
|
842
|
+
async function readCodexInstructionHomes() {
|
|
843
|
+
await ensureDatabase();
|
|
844
|
+
const accounts = await prisma.providerAccount.findMany({ where: { providerId: "codex" } });
|
|
845
|
+
return uniquePaths([
|
|
846
|
+
resolveSharedCodexHome(),
|
|
847
|
+
resolveCanonicalHistoryHome(),
|
|
848
|
+
...accounts.map(resolveAccountCodexHome)
|
|
849
|
+
]);
|
|
850
|
+
}
|
|
851
|
+
async function readLatestExistingCodexInstructions(homes) {
|
|
852
|
+
return (await Promise.all(homes.map(async (home, index) => {
|
|
853
|
+
const path = codexInstructionsPath(home);
|
|
854
|
+
const stats = await stat(path).catch(() => null);
|
|
855
|
+
if (!stats?.isFile()) return null;
|
|
856
|
+
const content = await readFile(path, "utf8").catch(() => null);
|
|
857
|
+
if (content === null) return null;
|
|
858
|
+
return {
|
|
859
|
+
content: content.trimEnd(),
|
|
860
|
+
index,
|
|
861
|
+
mtimeMs: stats.mtimeMs
|
|
862
|
+
};
|
|
863
|
+
}))).filter((file) => Boolean(file)).sort((left, right) => right.mtimeMs - left.mtimeMs || left.index - right.index)[0]?.content ?? null;
|
|
864
|
+
}
|
|
865
|
+
async function writeCodexInstructions(codexHome, instructions) {
|
|
866
|
+
await mkdir(codexHome, {
|
|
867
|
+
recursive: true,
|
|
868
|
+
mode: 448
|
|
869
|
+
});
|
|
870
|
+
await writeFile(codexInstructionsPath(codexHome), instructions ? `${instructions}\n` : "", "utf8");
|
|
871
|
+
}
|
|
872
|
+
function codexInstructionsPath(codexHome) {
|
|
873
|
+
return join(codexHome, codexInstructionsFileName);
|
|
874
|
+
}
|
|
875
|
+
function codexArgsWithPersonality(args, personality) {
|
|
876
|
+
if (args.some((arg) => arg.includes("personality"))) return args;
|
|
877
|
+
return [
|
|
878
|
+
...args,
|
|
879
|
+
"-c",
|
|
880
|
+
`personality="${personality}"`
|
|
881
|
+
];
|
|
882
|
+
}
|
|
883
|
+
function readCodexPersonality(value) {
|
|
884
|
+
return value === "friendly" ? "friendly" : "pragmatic";
|
|
885
|
+
}
|
|
886
|
+
function uniquePaths(paths) {
|
|
887
|
+
return [...new Set(paths.map((path) => resolve(path)))];
|
|
888
|
+
}
|
|
889
|
+
async function connectLocalCodexAccount() {
|
|
890
|
+
const authPath = join(resolveSharedCodexHome(), "auth.json");
|
|
891
|
+
if (!(await stat(authPath).catch(() => null))?.isFile()) throw new Error(`No local Codex auth found at ${authPath}.`);
|
|
892
|
+
}
|
|
893
|
+
async function syncAccountThreadToCanonical(threadId, account) {
|
|
894
|
+
return copyCodexThread(resolveAccountCodexHome(account), resolveCanonicalHistoryHome(), threadId);
|
|
895
|
+
}
|
|
896
|
+
async function hydrateCanonicalThreadToAccount(threadId, account) {
|
|
897
|
+
if (usesSharedCodexHome(account)) return true;
|
|
898
|
+
const target = resolveAccountCodexHome(account);
|
|
899
|
+
ensureCodexHome(target);
|
|
900
|
+
return copyCodexThread(resolveCanonicalHistoryHome(), target, threadId, { preserveExistingTarget: true });
|
|
901
|
+
}
|
|
902
|
+
async function hydrateKnownCodexThreadToAccount(threadId, account) {
|
|
903
|
+
if (await hydrateCanonicalThreadToAccount(threadId, account)) return true;
|
|
904
|
+
if (usesSharedCodexHome(account)) return true;
|
|
905
|
+
const target = resolveAccountCodexHome(account);
|
|
906
|
+
ensureCodexHome(target);
|
|
907
|
+
const copied = await copyCodexThread(resolveSharedCodexHome(), target, threadId, { preserveExistingTarget: true });
|
|
908
|
+
if (copied) await copyCodexThread(resolveSharedCodexHome(), resolveCanonicalHistoryHome(), threadId, { preserveExistingTarget: true });
|
|
909
|
+
return copied;
|
|
910
|
+
}
|
|
911
|
+
async function removeAccountThread(threadId, account) {
|
|
912
|
+
if (usesSharedCodexHome(account)) return true;
|
|
913
|
+
return removeCodexThread(resolveAccountCodexHome(account), threadId);
|
|
914
|
+
}
|
|
915
|
+
async function moveAccountThreadToAccount(threadId, fromAccount, toAccount) {
|
|
916
|
+
const source = resolveAccountCodexHome(fromAccount);
|
|
917
|
+
const target = resolveAccountCodexHome(toAccount);
|
|
918
|
+
if (sameFilesystemPath(source, target)) return true;
|
|
919
|
+
const copied = await copyCodexThread(source, target, threadId, { preserveExistingTarget: true }) || await hydrateCanonicalThreadToAccount(threadId, toAccount);
|
|
920
|
+
if (copied) {
|
|
921
|
+
await syncAccountThreadToCanonical(threadId, toAccount);
|
|
922
|
+
await removeAccountThread(threadId, fromAccount);
|
|
923
|
+
}
|
|
924
|
+
return copied;
|
|
925
|
+
}
|
|
926
|
+
async function listCodexChats(account) {
|
|
927
|
+
const appServerChats = await listCodexChatsFromAppServer(account).catch(() => null);
|
|
928
|
+
if (appServerChats) {
|
|
929
|
+
await refreshCodexStoredSummaries(account, appServerChats);
|
|
930
|
+
return appServerChats;
|
|
931
|
+
}
|
|
932
|
+
const codexHome = resolveAccountCodexHome(account);
|
|
933
|
+
const text = await readFile(join(codexHome, "session_index.jsonl"), "utf8").catch(() => "");
|
|
934
|
+
if (!text.trim()) return [];
|
|
935
|
+
const sessionFiles = await listCodexSessionFiles(codexHome);
|
|
936
|
+
const sessionFileById = new Map(sessionFiles.map((file) => [codexSessionIdFromPath(file), file]));
|
|
937
|
+
const rows = text.split(/\r?\n/u).map(parseJsonLine).map((row) => asJsonObject(row)).filter((row) => Boolean(row)).slice(-200).reverse();
|
|
938
|
+
const chats = [];
|
|
939
|
+
for (const row of rows) {
|
|
940
|
+
const externalThreadId = readString(row.threadId) ?? readString(row.thread_id) ?? readString(row.id);
|
|
941
|
+
if (!externalThreadId) continue;
|
|
942
|
+
const sessionFile = sessionFileById.get(externalThreadId);
|
|
943
|
+
const meta = sessionFile ? await readCodexSessionMeta(sessionFile) : {};
|
|
944
|
+
const summary = sessionFile ? await readCodexSessionSummary(sessionFile) : emptyCodexSessionSummary();
|
|
945
|
+
chats.push({
|
|
946
|
+
externalThreadId,
|
|
947
|
+
title: readString(row.thread_name) ?? readString(row.title) ?? "Untitled chat",
|
|
948
|
+
updatedAt: newestCodexTimestamp(summary.updatedAt, readIsoString(row.updatedAt) ?? readIsoString(row.updated_at) ?? readIsoString(row.lastSentAt) ?? readIsoString(row.last_sent_at) ?? null),
|
|
949
|
+
createdAt: readIsoString(meta.timestamp) ?? null,
|
|
950
|
+
status: summary.status,
|
|
951
|
+
workingDirectory: readString(meta.cwd) ?? null
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
return chats;
|
|
955
|
+
}
|
|
956
|
+
function accumulateLivePlanMessage(message, contentByItemId) {
|
|
957
|
+
if (message.kind !== "PLAN" || !message.itemId) return message;
|
|
958
|
+
if (message.status === "STREAMING") {
|
|
959
|
+
const content = `${contentByItemId.get(message.itemId) ?? ""}${message.content}`;
|
|
960
|
+
contentByItemId.set(message.itemId, content);
|
|
961
|
+
return {
|
|
962
|
+
...message,
|
|
963
|
+
content
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
contentByItemId.set(message.itemId, message.content);
|
|
967
|
+
return message;
|
|
968
|
+
}
|
|
969
|
+
async function loadCodexChatMessages(account, externalThreadId) {
|
|
970
|
+
const appServerResult = await loadCodexChatMessagesFromAppServer(account, externalThreadId, codexMessageReadTimeoutMs).catch(() => null);
|
|
971
|
+
if (appServerResult) {
|
|
972
|
+
const storedStatus = appServerResult.status === "RUNNING" ? null : await readCodexStoredChatStatus(account, externalThreadId);
|
|
973
|
+
if (appServerResult.status === "RUNNING" || storedStatus === "RUNNING") return mergeCodexMessages(appServerResult.messages, await loadCodexStoredChatMessages(account, externalThreadId));
|
|
974
|
+
return appServerResult.messages;
|
|
975
|
+
}
|
|
976
|
+
return loadCodexStoredChatMessages(account, externalThreadId);
|
|
977
|
+
}
|
|
978
|
+
async function readCodexChatStatus(account, externalThreadId) {
|
|
979
|
+
const appServerStatus = readCodexAppServerOpenTurnStatus(await readCodexAppServerThread(runtimeForAccount(account), externalThreadId).catch(() => null));
|
|
980
|
+
if (appServerStatus === "RUNNING") return "RUNNING";
|
|
981
|
+
const storedStatus = await readCodexStoredChatStatus(account, externalThreadId);
|
|
982
|
+
return storedStatus === "RUNNING" ? "RUNNING" : appServerStatus ?? storedStatus;
|
|
983
|
+
}
|
|
984
|
+
async function readCodexActiveTurnId(runtime, externalThreadId) {
|
|
985
|
+
return readCodexAppServerOpenTurnId(await readCodexAppServerThread(runtime, externalThreadId));
|
|
986
|
+
}
|
|
987
|
+
async function loadCodexStoredChatMessages(account, externalThreadId) {
|
|
988
|
+
const codexHome = resolveAccountCodexHome(account);
|
|
989
|
+
const sessionFile = await findCodexSessionFile(codexHome, externalThreadId);
|
|
990
|
+
return mergeCodexMessages(sessionFile ? await readCodexSessionMessages(sessionFile) : [], await readCodexLogMessages(codexHome, externalThreadId));
|
|
991
|
+
}
|
|
992
|
+
async function readCodexStoredChatStatus(account, externalThreadId) {
|
|
993
|
+
const sessionFile = await findCodexSessionFile(resolveAccountCodexHome(account), externalThreadId);
|
|
994
|
+
return sessionFile ? (await readCodexSessionSummary(sessionFile)).status : null;
|
|
995
|
+
}
|
|
996
|
+
async function readCodexStoredChatStates(account, externalThreadIds) {
|
|
997
|
+
const ids = new Set(externalThreadIds.map((id) => id.trim()).filter(Boolean));
|
|
998
|
+
const states = /* @__PURE__ */ new Map();
|
|
999
|
+
if (!ids.size) return states;
|
|
1000
|
+
const sessionFileById = await findCodexSessionFiles(resolveAccountCodexHome(account), [...ids], { fallbackToFullScan: false });
|
|
1001
|
+
for (const externalThreadId of ids) {
|
|
1002
|
+
const sessionFile = sessionFileById.get(externalThreadId);
|
|
1003
|
+
if (!sessionFile) continue;
|
|
1004
|
+
const sessionStats = await stat(sessionFile).catch(() => null);
|
|
1005
|
+
if (!sessionStats?.isFile()) continue;
|
|
1006
|
+
if (Date.now() - sessionStats.mtimeMs >= codexStoredRunningFreshnessMs) {
|
|
1007
|
+
states.set(externalThreadId, { updatedAt: sessionStats.mtime.toISOString() });
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
const summary = await readCodexSessionSummary(sessionFile);
|
|
1011
|
+
states.set(externalThreadId, {
|
|
1012
|
+
status: summary.status,
|
|
1013
|
+
updatedAt: summary.updatedAt
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
return states;
|
|
1017
|
+
}
|
|
1018
|
+
async function refreshCodexStoredSummaries(account, chats) {
|
|
1019
|
+
const candidates = chats;
|
|
1020
|
+
if (!candidates.length) return;
|
|
1021
|
+
const sessionFileById = await findCodexSessionFiles(resolveAccountCodexHome(account), candidates.map((chat) => chat.externalThreadId));
|
|
1022
|
+
for (const chat of candidates) {
|
|
1023
|
+
const sessionFile = sessionFileById.get(chat.externalThreadId);
|
|
1024
|
+
if (!sessionFile) continue;
|
|
1025
|
+
const summary = await readCodexSessionSummary(sessionFile);
|
|
1026
|
+
chat.updatedAt = newestCodexTimestamp(summary.updatedAt, chat.updatedAt ?? null);
|
|
1027
|
+
if (summary.status === "RUNNING" || !chat.status) chat.status = summary.status;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
async function listCodexChatsFromAppServer(account) {
|
|
1031
|
+
const runtime = runtimeForAccount(account);
|
|
1032
|
+
const result = asJsonObject((await runtime.request("thread/list", {
|
|
1033
|
+
archived: false,
|
|
1034
|
+
limit: maxProviderChats,
|
|
1035
|
+
modelProviders: [],
|
|
1036
|
+
sortDirection: "desc",
|
|
1037
|
+
sortKey: "updated_at",
|
|
1038
|
+
sourceKinds: [
|
|
1039
|
+
"cli",
|
|
1040
|
+
"vscode",
|
|
1041
|
+
"appServer",
|
|
1042
|
+
"exec",
|
|
1043
|
+
"unknown"
|
|
1044
|
+
]
|
|
1045
|
+
}, 3e4)).result);
|
|
1046
|
+
if (!Array.isArray(result?.data)) return null;
|
|
1047
|
+
const chats = result.data.map(readCodexAppServerChat).filter((chat) => Boolean(chat));
|
|
1048
|
+
await refreshCodexAppServerLiveStatuses(runtime, chats);
|
|
1049
|
+
return chats;
|
|
1050
|
+
}
|
|
1051
|
+
function readCodexAppServerChat(value) {
|
|
1052
|
+
const thread = asJsonObject(value);
|
|
1053
|
+
const externalThreadId = readString(thread?.id);
|
|
1054
|
+
if (!thread || !externalThreadId) return null;
|
|
1055
|
+
const title = readString(thread.name) ?? readString(thread.preview) ?? "Untitled chat";
|
|
1056
|
+
return {
|
|
1057
|
+
externalThreadId,
|
|
1058
|
+
stats: null,
|
|
1059
|
+
status: readCodexAppServerThreadStatus(thread.status),
|
|
1060
|
+
title,
|
|
1061
|
+
updatedAt: readCodexAppServerTimestamp(thread.recencyAt) ?? readCodexAppServerTimestamp(thread.updatedAt),
|
|
1062
|
+
createdAt: readCodexAppServerTimestamp(thread.createdAt),
|
|
1063
|
+
workingDirectory: readString(thread.cwd) ?? null
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
async function loadCodexChatMessagesFromAppServer(account, externalThreadId, timeoutMs = 3e4) {
|
|
1067
|
+
const thread = await readCodexAppServerThread(runtimeForAccount(account), externalThreadId, timeoutMs);
|
|
1068
|
+
if (!thread) return null;
|
|
1069
|
+
return {
|
|
1070
|
+
messages: readCodexAppServerThreadMessages(thread),
|
|
1071
|
+
status: readCodexAppServerOpenTurnStatus(thread)
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
async function refreshCodexAppServerLiveStatuses(runtime, chats) {
|
|
1075
|
+
const candidates = chats.slice(0, codexLiveStatusReadLimit);
|
|
1076
|
+
for (const chat of candidates) {
|
|
1077
|
+
const status = readCodexAppServerOpenTurnStatus(await readCodexAppServerThread(runtime, chat.externalThreadId).catch(() => null));
|
|
1078
|
+
if (status) chat.status = status;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async function readCodexAppServerThread(runtime, externalThreadId, timeoutMs = 3e4) {
|
|
1082
|
+
return asJsonObject(asJsonObject((await runtime.request("thread/read", {
|
|
1083
|
+
threadId: externalThreadId,
|
|
1084
|
+
includeTurns: true
|
|
1085
|
+
}, timeoutMs)).result)?.thread) ?? null;
|
|
1086
|
+
}
|
|
1087
|
+
function readCodexAppServerThreadMessages(thread) {
|
|
1088
|
+
if (!Array.isArray(thread.turns)) return [];
|
|
1089
|
+
const messages = [];
|
|
1090
|
+
for (const turnValue of thread.turns) {
|
|
1091
|
+
const turn = asJsonObject(turnValue);
|
|
1092
|
+
if (!turn || !Array.isArray(turn.items)) continue;
|
|
1093
|
+
const turnId = readString(turn.id) ?? readString(turn.turnId) ?? readString(turn.turn_id) ?? null;
|
|
1094
|
+
const startedAt = readCodexAppServerTimestamp(turn.startedAt);
|
|
1095
|
+
const completedAt = readCodexAppServerTimestamp(turn.completedAt);
|
|
1096
|
+
for (const itemValue of turn.items) {
|
|
1097
|
+
const message = readCodexAppServerThreadItemMessage(itemValue, startedAt, completedAt);
|
|
1098
|
+
if (message) messages.push(turnId && !message.turnId ? {
|
|
1099
|
+
...message,
|
|
1100
|
+
turnId
|
|
1101
|
+
} : message);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return mergeCodexMessages(messages, []);
|
|
1105
|
+
}
|
|
1106
|
+
function readCodexAppServerThreadItemMessage(value, turnStartedAt, turnCompletedAt) {
|
|
1107
|
+
const item = asJsonObject(value);
|
|
1108
|
+
const type = readString(item?.type);
|
|
1109
|
+
if (!item || !type) return null;
|
|
1110
|
+
if (type === "userMessage") {
|
|
1111
|
+
const content = readCodexAppServerUserMessageText(item.content);
|
|
1112
|
+
if (!content || isCodexInjectedContextMessage(content)) return null;
|
|
1113
|
+
return {
|
|
1114
|
+
role: "USER",
|
|
1115
|
+
content,
|
|
1116
|
+
itemId: readString(item.id) ?? readString(item.clientId) ?? null,
|
|
1117
|
+
createdAt: turnStartedAt
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
if (type === "agentMessage") {
|
|
1121
|
+
const content = readString(item.text);
|
|
1122
|
+
if (!content) return null;
|
|
1123
|
+
return {
|
|
1124
|
+
role: "ASSISTANT",
|
|
1125
|
+
content,
|
|
1126
|
+
itemId: readString(item.id) ?? null,
|
|
1127
|
+
createdAt: turnCompletedAt ?? turnStartedAt
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
if (type === "reasoning") {
|
|
1131
|
+
const content = formatCodexReasoning(item);
|
|
1132
|
+
if (!content) return null;
|
|
1133
|
+
return {
|
|
1134
|
+
role: "ASSISTANT",
|
|
1135
|
+
kind: "THINKING",
|
|
1136
|
+
content,
|
|
1137
|
+
itemId: readString(item.id) ?? null,
|
|
1138
|
+
createdAt: turnStartedAt
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
if (isCodexPlanItemType(type)) return codexPlanItemMessage(item, turnStartedAt);
|
|
1142
|
+
if (type === "commandExecution") return codexActionMessage(item, "COMMAND_EXECUTION", formatCodexCommandExecution(item), turnCompletedAt ?? turnStartedAt);
|
|
1143
|
+
if (type === "fileChange") return codexActionMessage(item, "FILE_CHANGE", formatCodexFileChange(item), turnCompletedAt ?? turnStartedAt);
|
|
1144
|
+
if (type === "mcpToolCall") return codexActionMessage(item, "TOOL_ACTIVITY", formatCodexMcpToolCall(item), turnCompletedAt ?? turnStartedAt);
|
|
1145
|
+
if (type === "dynamicToolCall") {
|
|
1146
|
+
const userInputMessage = readCodexUserInputPromptFromToolItem(item, turnStartedAt, turnCompletedAt);
|
|
1147
|
+
if (userInputMessage) return userInputMessage;
|
|
1148
|
+
return codexActionMessage(item, "TOOL_ACTIVITY", formatCodexDynamicToolCall(item), turnCompletedAt ?? turnStartedAt);
|
|
1149
|
+
}
|
|
1150
|
+
if (type === "webSearch") return codexActionMessage(item, "TOOL_ACTIVITY", formatCodexWebSearch(item), turnCompletedAt ?? turnStartedAt);
|
|
1151
|
+
if (type === "collabAgentToolCall") return codexActionMessage(item, "SUBAGENT_ACTIVITY", formatCodexCollabAgentToolCall(item), turnCompletedAt ?? turnStartedAt);
|
|
1152
|
+
if (type === "subAgentActivity") return codexActionMessage(item, "SUBAGENT_ACTIVITY", formatCodexSubAgentActivity(item), turnCompletedAt ?? turnStartedAt);
|
|
1153
|
+
if (type === "enteredReviewMode" || type === "exitedReviewMode") return codexActionMessage(item, "REVIEW", formatCodexReviewMode(item), turnCompletedAt ?? turnStartedAt);
|
|
1154
|
+
if (type === "warning") return codexActionMessage(item, "WARNING", formatCodexWarning(item), turnCompletedAt ?? turnStartedAt);
|
|
1155
|
+
if (type === "contextCompaction") return codexActionMessage(item, "COMPACTION", formatCodexSimpleToolItem(item), turnCompletedAt ?? turnStartedAt);
|
|
1156
|
+
if (type === "imageView" || type === "imageGeneration" || type === "sleep") return codexActionMessage(item, "TOOL_ACTIVITY", formatCodexSimpleToolItem(item), turnCompletedAt ?? turnStartedAt);
|
|
1157
|
+
if (isCodexVisibleFallbackItem(type)) return codexActionMessage(item, "TOOL_ACTIVITY", formatCodexFallbackToolItem(item), turnCompletedAt ?? turnStartedAt);
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
function codexActionMessage(item, kind, content, createdAt) {
|
|
1161
|
+
if (!content) return null;
|
|
1162
|
+
return {
|
|
1163
|
+
role: "TOOL",
|
|
1164
|
+
kind,
|
|
1165
|
+
status: codexItemMessageStatus(item.status),
|
|
1166
|
+
content,
|
|
1167
|
+
itemId: readString(item.id) ?? null,
|
|
1168
|
+
createdAt
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function readCodexUserInputPromptFromToolItem(item, turnStartedAt, turnCompletedAt) {
|
|
1172
|
+
const tool = readString(item.tool) ?? readString(item.name);
|
|
1173
|
+
if (!tool || !isCodexRequestUserInputName(tool)) return null;
|
|
1174
|
+
const requestId = readString(item.requestId) ?? readString(item.request_id);
|
|
1175
|
+
const payload = asJsonObject(item.arguments) ?? item;
|
|
1176
|
+
return codexUserInputPromptMessage({
|
|
1177
|
+
createdAt: turnStartedAt ?? turnCompletedAt,
|
|
1178
|
+
itemId: readString(item.id) ?? requestId ?? null,
|
|
1179
|
+
payload,
|
|
1180
|
+
requestId: requestId ?? null,
|
|
1181
|
+
status: readString(item.status)
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
function codexUserInputPromptMessage({ createdAt, itemId, payload, requestId, status }) {
|
|
1185
|
+
return {
|
|
1186
|
+
role: "TOOL",
|
|
1187
|
+
kind: "USER_INPUT_PROMPT",
|
|
1188
|
+
status: codexUserInputPromptStatus(status),
|
|
1189
|
+
content: formatCodexUserInputPrompt(payload),
|
|
1190
|
+
itemId,
|
|
1191
|
+
createdAt,
|
|
1192
|
+
metadata: { serverRequestMethod: "item/tool/requestUserInput" },
|
|
1193
|
+
rawPayload: jsonFromUnknown(payload),
|
|
1194
|
+
requestId
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
function formatCodexCommandExecution(item) {
|
|
1198
|
+
const parts = [`Command${codexStatusSuffix(item.status)}`];
|
|
1199
|
+
const command = readString(item.command);
|
|
1200
|
+
const cwd = readString(item.cwd);
|
|
1201
|
+
const actions = formatCodexCommandActions(item.commandActions);
|
|
1202
|
+
const output = readString(item.aggregatedOutput);
|
|
1203
|
+
const metadata = [
|
|
1204
|
+
cwd ? `cwd: \`${cwd}\`` : null,
|
|
1205
|
+
readNumber(item.exitCode) !== void 0 ? `exit: \`${readNumber(item.exitCode)}\`` : null,
|
|
1206
|
+
readNumber(item.durationMs) !== void 0 ? `duration: \`${readNumber(item.durationMs)} ms\`` : null
|
|
1207
|
+
].filter(Boolean);
|
|
1208
|
+
const shouldShowCommand = !actions.length || actions.some((action) => action.type === "unknown");
|
|
1209
|
+
if (command && shouldShowCommand) parts.push(fencedBlock("sh", command));
|
|
1210
|
+
if (metadata.length) parts.push(metadata.join(" | "));
|
|
1211
|
+
if (actions.length) parts.push(["Actions", ...actions.map((action) => `- ${action.label}`)].join("\n"));
|
|
1212
|
+
if (output) parts.push(["Output", fencedBlock("text", output)].join("\n"));
|
|
1213
|
+
return parts.join("\n\n");
|
|
1214
|
+
}
|
|
1215
|
+
function formatCodexFileChange(item) {
|
|
1216
|
+
const parts = [`File change${codexStatusSuffix(item.status)}`];
|
|
1217
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
1218
|
+
for (const changeValue of changes) {
|
|
1219
|
+
const change = asJsonObject(changeValue);
|
|
1220
|
+
if (!change) continue;
|
|
1221
|
+
const path = readString(change.path) ?? "unknown file";
|
|
1222
|
+
const kind = readString(change.kind);
|
|
1223
|
+
const diff = readString(change.diff);
|
|
1224
|
+
const stats = diff ? codexDiffStats(diff) : {
|
|
1225
|
+
additions: 0,
|
|
1226
|
+
deletions: 0
|
|
1227
|
+
};
|
|
1228
|
+
parts.push(`\`${path}\`${kind ? ` ${kind}` : ""} +${stats.additions} -${stats.deletions}`);
|
|
1229
|
+
}
|
|
1230
|
+
return parts.join("\n\n");
|
|
1231
|
+
}
|
|
1232
|
+
function codexDiffStats(diff) {
|
|
1233
|
+
let additions = 0;
|
|
1234
|
+
let deletions = 0;
|
|
1235
|
+
for (const line of diff.split(/\r?\n/u)) {
|
|
1236
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
1237
|
+
if (line.startsWith("+")) additions += 1;
|
|
1238
|
+
else if (line.startsWith("-")) deletions += 1;
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
additions,
|
|
1242
|
+
deletions
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function formatCodexMcpToolCall(item) {
|
|
1246
|
+
const parts = [`MCP tool ${[readString(item.server), readString(item.tool)].filter(Boolean).join("/") || "call"}${codexStatusSuffix(item.status)}`];
|
|
1247
|
+
const errorMessage = readString(asJsonObject(item.error)?.message);
|
|
1248
|
+
addJsonBlock(parts, "Arguments", item.arguments);
|
|
1249
|
+
addJsonBlock(parts, "Result", item.result);
|
|
1250
|
+
if (errorMessage) parts.push(`Error\n${fencedBlock("text", errorMessage)}`);
|
|
1251
|
+
return parts.join("\n\n");
|
|
1252
|
+
}
|
|
1253
|
+
function formatCodexDynamicToolCall(item) {
|
|
1254
|
+
const parts = [`Tool ${[readString(item.namespace), readString(item.tool)].filter(Boolean).join("/") || "call"}${codexStatusSuffix(item.status)}`];
|
|
1255
|
+
const output = formatCodexDynamicToolOutput(item.contentItems);
|
|
1256
|
+
addJsonBlock(parts, "Arguments", item.arguments);
|
|
1257
|
+
if (output) parts.push(`Output\n${output}`);
|
|
1258
|
+
return parts.join("\n\n");
|
|
1259
|
+
}
|
|
1260
|
+
function formatCodexUserInputPrompt(payload) {
|
|
1261
|
+
const questionText = (Array.isArray(payload.questions) ? payload.questions.map((entry) => asJsonObject(entry)).filter((entry) => Boolean(entry)) : []).map((question) => readString(question.question) ?? readString(question.prompt) ?? readString(question.header)).filter((text) => Boolean(text));
|
|
1262
|
+
if (questionText.length) return ["User input requested", ...questionText.map((text) => `- ${text}`)].join("\n");
|
|
1263
|
+
return readString(payload.question) ?? readString(payload.prompt) ?? readString(payload.message) ?? "User input requested";
|
|
1264
|
+
}
|
|
1265
|
+
function formatCodexWebSearch(item) {
|
|
1266
|
+
const query = readString(item.query);
|
|
1267
|
+
const parts = [`Web search${codexStatusSuffix(item.status)}`];
|
|
1268
|
+
if (query) parts.push(fencedBlock("text", query));
|
|
1269
|
+
addJsonBlock(parts, "Action", item.action);
|
|
1270
|
+
return parts.join("\n\n");
|
|
1271
|
+
}
|
|
1272
|
+
function formatCodexReasoning(item) {
|
|
1273
|
+
return [readCodexTextLike(item.summary), readCodexTextLike(item.content)].filter(Boolean).join("\n\n").trim() || (readString(item.status) === "inProgress" ? "Thinking" : null);
|
|
1274
|
+
}
|
|
1275
|
+
function formatCodexCollabAgentToolCall(item) {
|
|
1276
|
+
const tool = readString(item.tool);
|
|
1277
|
+
const receivers = Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds.map(readString).filter(Boolean) : [];
|
|
1278
|
+
const parts = [`Subagent ${tool ?? "activity"}${codexStatusSuffix(item.status)}`];
|
|
1279
|
+
const metadata = [
|
|
1280
|
+
readString(item.senderThreadId) ? `from: \`${readString(item.senderThreadId)}\`` : null,
|
|
1281
|
+
receivers.length ? `to: ${receivers.map((id) => `\`${id}\``).join(", ")}` : null,
|
|
1282
|
+
readString(item.model) ? `model: \`${readString(item.model)}\`` : null,
|
|
1283
|
+
readString(item.reasoningEffort) ? `reasoning: \`${readString(item.reasoningEffort)}\`` : null
|
|
1284
|
+
].filter(Boolean);
|
|
1285
|
+
if (metadata.length) parts.push(metadata.join(" | "));
|
|
1286
|
+
const prompt = readString(item.prompt);
|
|
1287
|
+
if (prompt) parts.push(fencedBlock("text", prompt));
|
|
1288
|
+
addJsonBlock(parts, "Agents", item.agentsStates);
|
|
1289
|
+
return parts.join("\n\n");
|
|
1290
|
+
}
|
|
1291
|
+
function formatCodexSubAgentActivity(item) {
|
|
1292
|
+
const kind = readString(item.kind);
|
|
1293
|
+
const path = readString(item.agentPath);
|
|
1294
|
+
const threadId = readString(item.agentThreadId);
|
|
1295
|
+
return [
|
|
1296
|
+
`Subagent ${kind ? humanizeCodexItemType(kind).toLowerCase() : "activity"}`,
|
|
1297
|
+
path ? `path: \`${path}\`` : null,
|
|
1298
|
+
threadId ? `thread: \`${threadId}\`` : null
|
|
1299
|
+
].filter(Boolean).join("\n\n");
|
|
1300
|
+
}
|
|
1301
|
+
function formatCodexReviewMode(item) {
|
|
1302
|
+
const type = readString(item.type);
|
|
1303
|
+
const review = readString(item.review);
|
|
1304
|
+
const label = type === "enteredReviewMode" ? "Review started" : "Review completed";
|
|
1305
|
+
return review ? `${label}\n\n${review}` : label;
|
|
1306
|
+
}
|
|
1307
|
+
function formatCodexWarning(item) {
|
|
1308
|
+
return readString(item.message) ?? readString(item.text) ?? readString(item.warning) ?? formatCodexFallbackToolItem(item);
|
|
1309
|
+
}
|
|
1310
|
+
function formatCodexSimpleToolItem(item) {
|
|
1311
|
+
const type = readString(item.type);
|
|
1312
|
+
if (type === "imageView") return `Image viewed\n\n\`${readString(item.path) ?? "unknown path"}\``;
|
|
1313
|
+
if (type === "imageGeneration") {
|
|
1314
|
+
const parts = [`Image generation${codexStatusSuffix(item.status)}`];
|
|
1315
|
+
const savedPath = readString(item.savedPath) ?? readString(item.saved_path);
|
|
1316
|
+
const revisedPrompt = readString(item.revisedPrompt) ?? readString(item.revised_prompt);
|
|
1317
|
+
if (savedPath) parts.push(`saved: \`${savedPath}\``);
|
|
1318
|
+
if (revisedPrompt) parts.push(fencedBlock("text", revisedPrompt));
|
|
1319
|
+
return parts.join("\n\n");
|
|
1320
|
+
}
|
|
1321
|
+
if (type === "sleep") return `Waited${readNumber(item.durationMs) !== void 0 ? ` \`${readNumber(item.durationMs)} ms\`` : ""}`;
|
|
1322
|
+
if (type === "contextCompaction") return "Context compacted";
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
function formatCodexFallbackToolItem(item) {
|
|
1326
|
+
const type = readString(item.type) ?? "tool";
|
|
1327
|
+
const name = readString(item.tool) ?? readString(item.name);
|
|
1328
|
+
const parts = [`${humanizeCodexItemType(type)}${name ? ` ${name}` : ""}${codexStatusSuffix(item.status)}`];
|
|
1329
|
+
const metadata = [
|
|
1330
|
+
readString(item.id) ? `id: \`${readString(item.id)}\`` : null,
|
|
1331
|
+
readString(item.server) ? `server: \`${readString(item.server)}\`` : null,
|
|
1332
|
+
readString(item.namespace) ? `namespace: \`${readString(item.namespace)}\`` : null
|
|
1333
|
+
].filter(Boolean);
|
|
1334
|
+
if (metadata.length) parts.push(metadata.join(" | "));
|
|
1335
|
+
addJsonBlock(parts, "Details", sanitizeCodexToolDetails(item));
|
|
1336
|
+
return parts.join("\n\n");
|
|
1337
|
+
}
|
|
1338
|
+
function isCodexVisibleFallbackItem(type) {
|
|
1339
|
+
return type !== "hookPrompt";
|
|
1340
|
+
}
|
|
1341
|
+
function humanizeCodexItemType(type) {
|
|
1342
|
+
return type.replace(/[_-]+/gu, " ").replace(/([a-z0-9])([A-Z])/gu, "$1 $2").replace(/\s+/gu, " ").trim().replace(/^./u, (letter) => letter.toUpperCase()) || "Tool";
|
|
1343
|
+
}
|
|
1344
|
+
function sanitizeCodexToolDetails(value, depth = 0) {
|
|
1345
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") return value;
|
|
1346
|
+
if (typeof value === "string") return value.length > 1e3 ? `${value.slice(0, 1e3)}...` : value;
|
|
1347
|
+
if (Array.isArray(value)) {
|
|
1348
|
+
if (depth >= 3) return `[${value.length} items]`;
|
|
1349
|
+
const items = value.slice(0, 20).map((item) => sanitizeCodexToolDetails(item, depth + 1));
|
|
1350
|
+
return value.length > items.length ? [...items, `... ${value.length - items.length} more`] : items;
|
|
1351
|
+
}
|
|
1352
|
+
const object = asJsonObject(value);
|
|
1353
|
+
if (!object) return String(value);
|
|
1354
|
+
if (depth >= 3) return "{...}";
|
|
1355
|
+
const details = {};
|
|
1356
|
+
for (const [key, entry] of Object.entries(object)) details[key] = isCodexHeavyDetailKey(key) ? "[omitted]" : sanitizeCodexToolDetails(entry, depth + 1);
|
|
1357
|
+
return details;
|
|
1358
|
+
}
|
|
1359
|
+
function isCodexHeavyDetailKey(key) {
|
|
1360
|
+
return [
|
|
1361
|
+
"aggregatedOutput",
|
|
1362
|
+
"content",
|
|
1363
|
+
"diff",
|
|
1364
|
+
"encrypted_content",
|
|
1365
|
+
"output",
|
|
1366
|
+
"replacement_history",
|
|
1367
|
+
"result",
|
|
1368
|
+
"stderr",
|
|
1369
|
+
"stdout",
|
|
1370
|
+
"text",
|
|
1371
|
+
"unified_diff"
|
|
1372
|
+
].includes(key);
|
|
1373
|
+
}
|
|
1374
|
+
function formatCodexCommandActions(value) {
|
|
1375
|
+
if (!Array.isArray(value)) return [];
|
|
1376
|
+
return value.map((actionValue) => {
|
|
1377
|
+
const action = asJsonObject(actionValue);
|
|
1378
|
+
const type = readString(action?.type);
|
|
1379
|
+
const command = readString(action?.command);
|
|
1380
|
+
if (!action || !type) return null;
|
|
1381
|
+
if (type === "read") return {
|
|
1382
|
+
type,
|
|
1383
|
+
label: `read ${readString(action.path) ?? readString(action.name) ?? command ?? "file"}`
|
|
1384
|
+
};
|
|
1385
|
+
if (type === "listFiles") return {
|
|
1386
|
+
type,
|
|
1387
|
+
label: `list ${readString(action.path) ?? command ?? "files"}`
|
|
1388
|
+
};
|
|
1389
|
+
if (type === "search") {
|
|
1390
|
+
const query = readString(action.query);
|
|
1391
|
+
const path = readString(action.path);
|
|
1392
|
+
return {
|
|
1393
|
+
type,
|
|
1394
|
+
label: `search${query ? ` "${query}"` : ""}${path ? ` in ${path}` : ""}`
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
type,
|
|
1399
|
+
label: command ?? type
|
|
1400
|
+
};
|
|
1401
|
+
}).filter((action) => Boolean(action));
|
|
1402
|
+
}
|
|
1403
|
+
function formatCodexDynamicToolOutput(value) {
|
|
1404
|
+
if (!Array.isArray(value)) return null;
|
|
1405
|
+
const text = value.map((entryValue) => {
|
|
1406
|
+
const entry = asJsonObject(entryValue);
|
|
1407
|
+
if (readString(entry?.type) === "inputText") return readString(entry?.text) ?? "";
|
|
1408
|
+
if (readString(entry?.type) === "inputImage") return readString(entry?.imageUrl) ?? readString(entry?.image_url) ?? "";
|
|
1409
|
+
return "";
|
|
1410
|
+
}).filter(Boolean).join("\n").trim();
|
|
1411
|
+
return text ? fencedBlock("text", text) : null;
|
|
1412
|
+
}
|
|
1413
|
+
function addJsonBlock(parts, label, value) {
|
|
1414
|
+
if (value === void 0 || value === null) return;
|
|
1415
|
+
const block = formatJsonBlock(value);
|
|
1416
|
+
if (block) parts.push(`${label}\n${block}`);
|
|
1417
|
+
}
|
|
1418
|
+
function formatJsonBlock(value) {
|
|
1419
|
+
try {
|
|
1420
|
+
const json = JSON.stringify(value, null, 2);
|
|
1421
|
+
return json && json !== "null" ? fencedBlock("json", json) : null;
|
|
1422
|
+
} catch {
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
function fencedBlock(language, value) {
|
|
1427
|
+
return `~~~${language}\n${value.trim()}\n~~~`;
|
|
1428
|
+
}
|
|
1429
|
+
function codexStatusSuffix(value) {
|
|
1430
|
+
const status = readString(value);
|
|
1431
|
+
return status ? ` \`${status}\`` : "";
|
|
1432
|
+
}
|
|
1433
|
+
function codexItemMessageStatus(value) {
|
|
1434
|
+
const status = readString(value);
|
|
1435
|
+
if (status === "inProgress") return "STREAMING";
|
|
1436
|
+
if (status === "failed" || status === "declined") return "FAILED";
|
|
1437
|
+
return "COMPLETED";
|
|
1438
|
+
}
|
|
1439
|
+
function readCodexAppServerUserMessageText(value) {
|
|
1440
|
+
if (typeof value === "string") return value.trim() || null;
|
|
1441
|
+
if (!Array.isArray(value)) return null;
|
|
1442
|
+
return value.map((input) => {
|
|
1443
|
+
const inputObject = asJsonObject(input);
|
|
1444
|
+
if (readString(inputObject?.type) !== "text") return "";
|
|
1445
|
+
return readString(inputObject?.text) ?? "";
|
|
1446
|
+
}).filter(Boolean).join("\n").trim() || null;
|
|
1447
|
+
}
|
|
1448
|
+
function readCodexTextLike(value) {
|
|
1449
|
+
if (typeof value === "string") return value.trim() || null;
|
|
1450
|
+
if (!Array.isArray(value)) return null;
|
|
1451
|
+
return value.map((entry) => {
|
|
1452
|
+
if (typeof entry === "string") return entry;
|
|
1453
|
+
const object = asJsonObject(entry);
|
|
1454
|
+
return readString(object?.text) ?? readString(object?.content) ?? "";
|
|
1455
|
+
}).filter(Boolean).join("\n").trim() || null;
|
|
1456
|
+
}
|
|
1457
|
+
function readCodexAppServerTimestamp(value) {
|
|
1458
|
+
if (typeof value === "number" && Number.isFinite(value)) return (/* @__PURE__ */ new Date(value * 1e3)).toISOString();
|
|
1459
|
+
return readIsoString(value);
|
|
1460
|
+
}
|
|
1461
|
+
function readCodexAppServerThreadStatus(value) {
|
|
1462
|
+
const type = readString(asJsonObject(value)?.type) ?? readString(value);
|
|
1463
|
+
if (type === "active" || type === "running" || type === "inProgress" || type === "busy") return "RUNNING";
|
|
1464
|
+
if (type === "idle" || type === "notLoaded" || type === "systemError" || type === "completed") return "IDLE";
|
|
1465
|
+
return null;
|
|
1466
|
+
}
|
|
1467
|
+
function readCodexAppServerOpenTurnStatus(thread) {
|
|
1468
|
+
if (!thread) return null;
|
|
1469
|
+
const status = readCodexAppServerThreadStatus(thread.status);
|
|
1470
|
+
const lastTurn = asJsonObject((Array.isArray(thread.turns) ? thread.turns : []).at(-1));
|
|
1471
|
+
if (!lastTurn) return status;
|
|
1472
|
+
if (readCodexAppServerTimestamp(lastTurn.completedAt)) return status === "RUNNING" && isFreshCodexThreadActivity(thread, lastTurn) ? "RUNNING" : "IDLE";
|
|
1473
|
+
return isFreshCodexThreadActivity(thread, lastTurn) ? "RUNNING" : "IDLE";
|
|
1474
|
+
}
|
|
1475
|
+
function readCodexAppServerOpenTurnId(thread) {
|
|
1476
|
+
if (!thread || readCodexAppServerOpenTurnStatus(thread) !== "RUNNING" || !Array.isArray(thread.turns)) return null;
|
|
1477
|
+
for (const turnValue of [...thread.turns].reverse()) {
|
|
1478
|
+
const turn = asJsonObject(turnValue);
|
|
1479
|
+
if (!turn || readCodexAppServerTimestamp(turn.completedAt)) continue;
|
|
1480
|
+
const turnId = readString(turn.id) ?? readString(turn.turnId) ?? readString(turn.turn_id);
|
|
1481
|
+
if (turnId) return turnId;
|
|
1482
|
+
}
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
function isFreshCodexThreadActivity(thread, turn) {
|
|
1486
|
+
const updatedAt = newestCodexTimestamp(readCodexAppServerTimestamp(turn.updatedAt) ?? readCodexAppServerTimestamp(turn.startedAt) ?? null, readCodexAppServerTimestamp(thread.recencyAt) ?? readCodexAppServerTimestamp(thread.updatedAt) ?? null);
|
|
1487
|
+
const updatedAtMs = updatedAt ? Date.parse(updatedAt) : NaN;
|
|
1488
|
+
return Number.isFinite(updatedAtMs) && Date.now() - updatedAtMs < codexTurnCompletionTimeoutMs;
|
|
1489
|
+
}
|
|
1490
|
+
function isCodexTurnCompletedEvent(message, threadId, turnId) {
|
|
1491
|
+
if (message.method !== "turn/completed") return false;
|
|
1492
|
+
const params = asJsonObject(message.params);
|
|
1493
|
+
if ((readString(params?.threadId) ?? readString(params?.thread_id)) !== threadId) return false;
|
|
1494
|
+
const completedTurnId = readString(asJsonObject(params?.turn)?.id);
|
|
1495
|
+
return !turnId || !completedTurnId || completedTurnId === turnId;
|
|
1496
|
+
}
|
|
1497
|
+
function readCodexLiveThreadItemMessage(message, threadId, turnId) {
|
|
1498
|
+
const planDeltaMessage = readCodexPlanDeltaMessage(message, threadId, turnId);
|
|
1499
|
+
if (planDeltaMessage) return planDeltaMessage;
|
|
1500
|
+
const started = message.method === "item/started";
|
|
1501
|
+
const completed = message.method === "item/completed";
|
|
1502
|
+
if (!started && !completed) return null;
|
|
1503
|
+
const params = asJsonObject(message.params);
|
|
1504
|
+
if (!params || (readString(params.threadId) ?? readString(params.thread_id)) !== threadId) return null;
|
|
1505
|
+
const eventTurnId = readString(params.turnId) ?? readString(params.turn_id);
|
|
1506
|
+
if (turnId && eventTurnId && eventTurnId !== turnId) return null;
|
|
1507
|
+
const item = asJsonObject(params.item);
|
|
1508
|
+
if (!item) return null;
|
|
1509
|
+
const timestamp = readCodexAppServerMillisTimestamp(started ? params.startedAtMs : params.completedAtMs) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1510
|
+
const providerMessage = readCodexAppServerThreadItemMessage(started && item.status === void 0 ? {
|
|
1511
|
+
...item,
|
|
1512
|
+
status: "inProgress"
|
|
1513
|
+
} : item, timestamp, completed ? timestamp : null);
|
|
1514
|
+
if (!providerMessage || !isCodexLiveActionMessage(providerMessage)) return null;
|
|
1515
|
+
return providerMessage;
|
|
1516
|
+
}
|
|
1517
|
+
function readCodexPlanUpdateMessage(message, threadId, turnId) {
|
|
1518
|
+
if (message.method !== "turn/plan/updated") return null;
|
|
1519
|
+
const params = asJsonObject(message.params);
|
|
1520
|
+
if (!params || (readString(params.threadId) ?? readString(params.thread_id)) !== threadId) return null;
|
|
1521
|
+
const eventTurnId = readString(params.turnId) ?? readString(params.turn_id);
|
|
1522
|
+
if (turnId && eventTurnId && eventTurnId !== turnId) return null;
|
|
1523
|
+
return codexPlanUpdateMessage({
|
|
1524
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1525
|
+
explanation: readString(params.explanation) ?? null,
|
|
1526
|
+
itemId: eventTurnId ? `plan-update:${eventTurnId}` : `plan-update:${threadId}`,
|
|
1527
|
+
status: "STREAMING",
|
|
1528
|
+
steps: readCodexPlanSteps(params.plan),
|
|
1529
|
+
turnId: eventTurnId ?? null
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
function readCodexPlanDeltaMessage(message, threadId, turnId) {
|
|
1533
|
+
if (message.method !== "item/plan/delta") return null;
|
|
1534
|
+
const params = asJsonObject(message.params);
|
|
1535
|
+
if (!params || (readString(params.threadId) ?? readString(params.thread_id)) !== threadId) return null;
|
|
1536
|
+
const eventTurnId = readString(params.turnId) ?? readString(params.turn_id);
|
|
1537
|
+
if (turnId && eventTurnId && eventTurnId !== turnId) return null;
|
|
1538
|
+
const itemId = readString(params.itemId) ?? readString(params.item_id);
|
|
1539
|
+
const delta = readString(params.delta);
|
|
1540
|
+
if (!itemId || !delta) return null;
|
|
1541
|
+
return {
|
|
1542
|
+
role: "ASSISTANT",
|
|
1543
|
+
kind: "PLAN",
|
|
1544
|
+
status: "STREAMING",
|
|
1545
|
+
content: delta,
|
|
1546
|
+
itemId,
|
|
1547
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1548
|
+
turnId: eventTurnId ?? null
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
function codexPlanUpdateMessage({ createdAt, explanation, itemId, status, steps, turnId }) {
|
|
1552
|
+
return {
|
|
1553
|
+
role: "ASSISTANT",
|
|
1554
|
+
kind: "PLAN",
|
|
1555
|
+
status,
|
|
1556
|
+
content: explanation?.trim() ?? "",
|
|
1557
|
+
itemId,
|
|
1558
|
+
createdAt,
|
|
1559
|
+
turnId: turnId ?? null,
|
|
1560
|
+
metadata: {
|
|
1561
|
+
planPresentation: "update",
|
|
1562
|
+
planSteps: jsonFromUnknown(steps)
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function readCodexPlanSteps(value) {
|
|
1567
|
+
if (!Array.isArray(value)) return [];
|
|
1568
|
+
return value.map((entry) => {
|
|
1569
|
+
const record = asJsonObject(entry);
|
|
1570
|
+
const step = readString(record?.step);
|
|
1571
|
+
const status = readCodexPlanStepStatus(record?.status);
|
|
1572
|
+
return step ? {
|
|
1573
|
+
step,
|
|
1574
|
+
status
|
|
1575
|
+
} : null;
|
|
1576
|
+
}).filter((step) => Boolean(step));
|
|
1577
|
+
}
|
|
1578
|
+
function readCodexPlanStepStatus(value) {
|
|
1579
|
+
const status = readString(value);
|
|
1580
|
+
if (status === "completed" || status === "inProgress" || status === "pending") return status;
|
|
1581
|
+
if (status === "in_progress") return "inProgress";
|
|
1582
|
+
return "pending";
|
|
1583
|
+
}
|
|
1584
|
+
function readCodexServerRequestMessage(message, threadId, turnId) {
|
|
1585
|
+
if (message.id === void 0 || message.id === null || !message.method || !isCodexServerRequestMethod(message.method)) return null;
|
|
1586
|
+
const params = asJsonObject(message.params);
|
|
1587
|
+
if (!params || (readString(params.threadId) ?? readString(params.thread_id)) !== threadId) return null;
|
|
1588
|
+
const eventTurnId = readString(params.turnId) ?? readString(params.turn_id);
|
|
1589
|
+
if (turnId && eventTurnId && eventTurnId !== turnId) return null;
|
|
1590
|
+
const requestId = String(message.id);
|
|
1591
|
+
const timestamp = readCodexAppServerMillisTimestamp(params.startedAtMs) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1592
|
+
return {
|
|
1593
|
+
content: codexServerRequestContent(message.method, params),
|
|
1594
|
+
createdAt: timestamp,
|
|
1595
|
+
itemId: `request:${requestId}`,
|
|
1596
|
+
kind: isCodexRequestUserInputMethod(message.method) ? "USER_INPUT_PROMPT" : "APPROVAL",
|
|
1597
|
+
metadata: { serverRequestMethod: message.method },
|
|
1598
|
+
rawPayload: jsonFromUnknown(params),
|
|
1599
|
+
requestId,
|
|
1600
|
+
role: "TOOL",
|
|
1601
|
+
status: "PENDING",
|
|
1602
|
+
turnId: eventTurnId ?? null
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
function readCodexServerRequestResolvedMessage(message, threadId) {
|
|
1606
|
+
if (message.method !== "serverRequest/resolved") return null;
|
|
1607
|
+
const params = asJsonObject(message.params);
|
|
1608
|
+
if (!params || (readString(params.threadId) ?? readString(params.thread_id)) !== threadId) return null;
|
|
1609
|
+
const requestId = readString(params.requestId) ?? readString(params.request_id);
|
|
1610
|
+
if (!requestId) return null;
|
|
1611
|
+
return {
|
|
1612
|
+
content: "Request resolved",
|
|
1613
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1614
|
+
itemId: `request:${requestId}`,
|
|
1615
|
+
kind: "APPROVAL",
|
|
1616
|
+
requestId,
|
|
1617
|
+
role: "TOOL",
|
|
1618
|
+
status: "COMPLETED"
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function isCodexServerRequestMethod(method) {
|
|
1622
|
+
return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval" || method === "item/permissions/requestApproval" || isCodexRequestUserInputMethod(method) || normalizedCodexName(method).endsWith("requestapproval");
|
|
1623
|
+
}
|
|
1624
|
+
function codexServerRequestContent(method, params) {
|
|
1625
|
+
const reason = readString(params.reason);
|
|
1626
|
+
const normalized = normalizedCodexName(method);
|
|
1627
|
+
if (normalized.includes("commandexecutionrequestapproval")) {
|
|
1628
|
+
const command = readString(params.command);
|
|
1629
|
+
const cwd = readString(params.cwd);
|
|
1630
|
+
return [
|
|
1631
|
+
"Command approval requested",
|
|
1632
|
+
cwd ? `cwd: \`${cwd}\`` : "",
|
|
1633
|
+
command ? `\n~~~sh\n${command}\n~~~` : "",
|
|
1634
|
+
reason ? `\n${reason}` : ""
|
|
1635
|
+
].filter(Boolean).join("\n");
|
|
1636
|
+
}
|
|
1637
|
+
if (normalized.includes("filechangerequestapproval")) return ["File change approval requested", reason].filter(Boolean).join("\n\n");
|
|
1638
|
+
if (normalized.includes("permissionsrequestapproval")) {
|
|
1639
|
+
const cwd = readString(params.cwd);
|
|
1640
|
+
return [
|
|
1641
|
+
"Permissions requested",
|
|
1642
|
+
cwd ? `cwd: \`${cwd}\`` : "",
|
|
1643
|
+
reason
|
|
1644
|
+
].filter(Boolean).join("\n");
|
|
1645
|
+
}
|
|
1646
|
+
return formatCodexUserInputPrompt(params);
|
|
1647
|
+
}
|
|
1648
|
+
function serverRequestResultFromResponse(response) {
|
|
1649
|
+
if (response.kind === "permissions") return {
|
|
1650
|
+
permissions: {},
|
|
1651
|
+
scope: "turn"
|
|
1652
|
+
};
|
|
1653
|
+
if (response.kind === "userInput") return { answers: {} };
|
|
1654
|
+
return { decision: response.decision ?? "decline" };
|
|
1655
|
+
}
|
|
1656
|
+
function isCodexLiveActionMessage(message) {
|
|
1657
|
+
return message.role === "TOOL" || message.kind === "THINKING" || message.kind === "PLAN" || message.kind === "APPROVAL" || message.kind === "USER_INPUT_PROMPT" || message.kind === "REVIEW" || message.kind === "WARNING" || message.kind === "COMPACTION" || message.kind === "SUBAGENT_ACTIVITY";
|
|
1658
|
+
}
|
|
1659
|
+
function readCodexAppServerMillisTimestamp(value) {
|
|
1660
|
+
const timestamp = readNumber(value);
|
|
1661
|
+
if (timestamp !== void 0) return new Date(timestamp).toISOString();
|
|
1662
|
+
return readIsoString(value);
|
|
1663
|
+
}
|
|
1664
|
+
async function listCodexSessionFiles(codexHome) {
|
|
1665
|
+
const roots = [join(codexHome, "sessions"), join(codexHome, "archived_sessions")];
|
|
1666
|
+
const files = [];
|
|
1667
|
+
for (const root of roots) await collectCodexSessionFiles(root, files);
|
|
1668
|
+
return files;
|
|
1669
|
+
}
|
|
1670
|
+
async function collectCodexSessionFiles(directory, files, depth = 0) {
|
|
1671
|
+
if (depth > 6) return;
|
|
1672
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
1673
|
+
for (const entry of entries) {
|
|
1674
|
+
const entryPath = join(directory, entry.name);
|
|
1675
|
+
if (entry.isDirectory()) {
|
|
1676
|
+
await collectCodexSessionFiles(entryPath, files, depth + 1);
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(entryPath);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
async function findCodexSessionFile(codexHome, externalThreadId) {
|
|
1683
|
+
return (await findCodexSessionFiles(codexHome, [externalThreadId])).get(externalThreadId) ?? null;
|
|
1684
|
+
}
|
|
1685
|
+
function codexSessionIdFromPath(pathname) {
|
|
1686
|
+
return pathname.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/iu)?.[1] ?? pathname;
|
|
1687
|
+
}
|
|
1688
|
+
async function findCodexSessionFiles(codexHome, externalThreadIds, options = {}) {
|
|
1689
|
+
const remaining = new Set(externalThreadIds.map((id) => id.trim()).filter(Boolean));
|
|
1690
|
+
const filesById = /* @__PURE__ */ new Map();
|
|
1691
|
+
if (!remaining.size) return filesById;
|
|
1692
|
+
const directories = /* @__PURE__ */ new Set();
|
|
1693
|
+
for (const externalThreadId of remaining) for (const directory of codexSessionCandidateDirectories(codexHome, externalThreadId)) directories.add(directory);
|
|
1694
|
+
for (const directory of directories) {
|
|
1695
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
1696
|
+
for (const entry of entries) {
|
|
1697
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
1698
|
+
const externalThreadId = codexSessionIdFromPath(entry.name);
|
|
1699
|
+
if (!remaining.has(externalThreadId)) continue;
|
|
1700
|
+
filesById.set(externalThreadId, join(directory, entry.name));
|
|
1701
|
+
remaining.delete(externalThreadId);
|
|
1702
|
+
}
|
|
1703
|
+
if (!remaining.size) return filesById;
|
|
1704
|
+
}
|
|
1705
|
+
if (options.fallbackToFullScan === false) return filesById;
|
|
1706
|
+
for (const file of await listCodexSessionFiles(codexHome)) {
|
|
1707
|
+
const externalThreadId = codexSessionIdFromPath(file);
|
|
1708
|
+
if (!remaining.has(externalThreadId)) continue;
|
|
1709
|
+
filesById.set(externalThreadId, file);
|
|
1710
|
+
remaining.delete(externalThreadId);
|
|
1711
|
+
if (!remaining.size) break;
|
|
1712
|
+
}
|
|
1713
|
+
return filesById;
|
|
1714
|
+
}
|
|
1715
|
+
function codexSessionCandidateDirectories(codexHome, externalThreadId) {
|
|
1716
|
+
const millis = codexUuidTimestampMillis(externalThreadId);
|
|
1717
|
+
if (millis === null) return [];
|
|
1718
|
+
const days = /* @__PURE__ */ new Set();
|
|
1719
|
+
for (const offsetDays of [
|
|
1720
|
+
-1,
|
|
1721
|
+
0,
|
|
1722
|
+
1
|
|
1723
|
+
]) {
|
|
1724
|
+
const date = new Date(millis + offsetDays * 24 * 60 * 60 * 1e3);
|
|
1725
|
+
const year = date.getUTCFullYear().toString().padStart(4, "0");
|
|
1726
|
+
const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
|
|
1727
|
+
const day = date.getUTCDate().toString().padStart(2, "0");
|
|
1728
|
+
days.add(join(year, month, day));
|
|
1729
|
+
}
|
|
1730
|
+
return [...days].flatMap((day) => [join(codexHome, "sessions", day), join(codexHome, "archived_sessions", day)]);
|
|
1731
|
+
}
|
|
1732
|
+
function codexUuidTimestampMillis(externalThreadId) {
|
|
1733
|
+
const timestampHex = externalThreadId.replaceAll("-", "").slice(0, 12);
|
|
1734
|
+
if (!timestampHex.match(/^[0-9a-f]{12}$/iu)) return null;
|
|
1735
|
+
const millis = Number.parseInt(timestampHex, 16);
|
|
1736
|
+
return Number.isFinite(millis) ? millis : null;
|
|
1737
|
+
}
|
|
1738
|
+
async function readCodexSessionMeta(sessionFile) {
|
|
1739
|
+
const text = await readFile(sessionFile, "utf8").catch(() => "");
|
|
1740
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
1741
|
+
const record = asJsonObject(parseJsonLine(line));
|
|
1742
|
+
if (readString(record?.type) === "session_meta") return asJsonObject(record?.payload) ?? {};
|
|
1743
|
+
}
|
|
1744
|
+
return {};
|
|
1745
|
+
}
|
|
1746
|
+
function emptyCodexSessionSummary() {
|
|
1747
|
+
return {
|
|
1748
|
+
pendingCallIds: /* @__PURE__ */ new Set(),
|
|
1749
|
+
status: null,
|
|
1750
|
+
updatedAt: null
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
async function readCodexSessionSummary(sessionFile) {
|
|
1754
|
+
return readCodexSessionTextSummary(await readCodexSessionSummaryText(sessionFile).catch(() => ""));
|
|
1755
|
+
}
|
|
1756
|
+
async function readCodexSessionSummaryText(sessionFile) {
|
|
1757
|
+
const sessionStats = await stat(sessionFile).catch(() => null);
|
|
1758
|
+
if (!sessionStats?.isFile()) return "";
|
|
1759
|
+
if (sessionStats.size <= codexSessionSummaryTailBytes) return readFile(sessionFile, "utf8");
|
|
1760
|
+
const length = Math.min(sessionStats.size, codexSessionSummaryTailBytes);
|
|
1761
|
+
const buffer = Buffer.alloc(length);
|
|
1762
|
+
const file = await open(sessionFile, "r");
|
|
1763
|
+
try {
|
|
1764
|
+
const result = await file.read(buffer, 0, length, sessionStats.size - length);
|
|
1765
|
+
return buffer.subarray(0, result.bytesRead).toString("utf8");
|
|
1766
|
+
} finally {
|
|
1767
|
+
await file.close();
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
function readCodexSessionTextSummary(text) {
|
|
1771
|
+
const pendingCallIds = /* @__PURE__ */ new Set();
|
|
1772
|
+
let activeTurnFinished = false;
|
|
1773
|
+
let activeTurnUpdatedAt = null;
|
|
1774
|
+
let lastTurnId = null;
|
|
1775
|
+
let sawRecord = false;
|
|
1776
|
+
let updatedAt = null;
|
|
1777
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
1778
|
+
const record = asJsonObject(parseJsonLine(line));
|
|
1779
|
+
if (!record) continue;
|
|
1780
|
+
sawRecord = true;
|
|
1781
|
+
updatedAt = newestCodexTimestamp(readIsoString(record.timestamp), updatedAt);
|
|
1782
|
+
const payload = asJsonObject(record.payload);
|
|
1783
|
+
const payloadType = readString(payload?.type);
|
|
1784
|
+
const recordType = readString(record.type);
|
|
1785
|
+
const turnId = readCodexSessionRecordTurnId(record, payload);
|
|
1786
|
+
if (turnId && turnId !== lastTurnId) {
|
|
1787
|
+
lastTurnId = turnId;
|
|
1788
|
+
pendingCallIds.clear();
|
|
1789
|
+
activeTurnFinished = false;
|
|
1790
|
+
activeTurnUpdatedAt = null;
|
|
1791
|
+
}
|
|
1792
|
+
if (turnId && turnId === lastTurnId) activeTurnUpdatedAt = newestCodexTimestamp(readIsoString(record.timestamp), activeTurnUpdatedAt);
|
|
1793
|
+
if (recordType === "event_msg" && payloadType === "turn_aborted") {
|
|
1794
|
+
const abortedTurnId = readString(payload?.turn_id) ?? readString(payload?.turnId);
|
|
1795
|
+
if (!abortedTurnId || !lastTurnId || abortedTurnId === lastTurnId) {
|
|
1796
|
+
pendingCallIds.clear();
|
|
1797
|
+
activeTurnFinished = true;
|
|
1798
|
+
}
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
if (recordType !== "response_item") continue;
|
|
1802
|
+
if (payloadType === "message" && readString(payload?.phase) === "final_answer") {
|
|
1803
|
+
pendingCallIds.clear();
|
|
1804
|
+
activeTurnFinished = true;
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
if (payloadType === "function_call" || payloadType === "custom_tool_call") {
|
|
1808
|
+
const callId = readCodexSessionCallId(payload);
|
|
1809
|
+
if (callId) {
|
|
1810
|
+
pendingCallIds.add(callId);
|
|
1811
|
+
activeTurnFinished = false;
|
|
1812
|
+
}
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
if (payloadType === "function_call_output" || payloadType === "custom_tool_call_output") {
|
|
1816
|
+
const callId = readCodexSessionCallId(payload);
|
|
1817
|
+
if (callId) if (isCodexSessionRunningToolOutput(payload)) {
|
|
1818
|
+
pendingCallIds.add(callId);
|
|
1819
|
+
activeTurnFinished = false;
|
|
1820
|
+
} else pendingCallIds.delete(callId);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const activeTurnUpdatedAtMs = activeTurnUpdatedAt ? Date.parse(activeTurnUpdatedAt) : NaN;
|
|
1824
|
+
const activeTurnFresh = Number.isFinite(activeTurnUpdatedAtMs) && Date.now() - activeTurnUpdatedAtMs < codexStoredRunningFreshnessMs;
|
|
1825
|
+
const freshPendingCallIds = Number.isFinite(activeTurnUpdatedAtMs) && Date.now() - activeTurnUpdatedAtMs < codexTurnCompletionTimeoutMs ? pendingCallIds : /* @__PURE__ */ new Set();
|
|
1826
|
+
return {
|
|
1827
|
+
pendingCallIds: freshPendingCallIds,
|
|
1828
|
+
status: freshPendingCallIds.size > 0 || !activeTurnFinished && activeTurnFresh ? "RUNNING" : sawRecord ? "IDLE" : null,
|
|
1829
|
+
updatedAt
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
async function readCodexSessionMessages(sessionFile) {
|
|
1833
|
+
const text = await readFile(sessionFile, "utf8").catch(() => "");
|
|
1834
|
+
const summary = readCodexSessionTextSummary(text);
|
|
1835
|
+
const outputByCallId = readCodexSessionFunctionOutputs(text);
|
|
1836
|
+
const messages = [];
|
|
1837
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
1838
|
+
const record = asJsonObject(parseJsonLine(line));
|
|
1839
|
+
const message = readCodexSessionRecordMessage(record, asJsonObject(record?.payload), summary.pendingCallIds, outputByCallId);
|
|
1840
|
+
if (message) messages.push(message);
|
|
1841
|
+
}
|
|
1842
|
+
return mergeCodexMessages(messages, []);
|
|
1843
|
+
}
|
|
1844
|
+
function readCodexSessionFunctionOutputs(text) {
|
|
1845
|
+
const outputs = /* @__PURE__ */ new Map();
|
|
1846
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
1847
|
+
const record = asJsonObject(parseJsonLine(line));
|
|
1848
|
+
const payload = asJsonObject(record?.payload);
|
|
1849
|
+
const recordType = readString(record?.type);
|
|
1850
|
+
const payloadType = readString(payload?.type);
|
|
1851
|
+
if (recordType === "event_msg" && payloadType === "exec_command_end") {
|
|
1852
|
+
const callId = readCodexSessionCallId(payload);
|
|
1853
|
+
const output = readCodexSessionExecCommandEndOutput(payload);
|
|
1854
|
+
if (callId && output) outputs.set(callId, mergeCodexSessionFunctionOutput(outputs.get(callId), output));
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
if (recordType === "event_msg" && payloadType === "mcp_tool_call_end") {
|
|
1858
|
+
const callId = readCodexSessionCallId(payload);
|
|
1859
|
+
const output = readCodexSessionMcpToolEndOutput(payload);
|
|
1860
|
+
if (callId && output) outputs.set(callId, mergeCodexSessionFunctionOutput(outputs.get(callId), output));
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
if (recordType !== "response_item" || payloadType !== "function_call_output" && payloadType !== "custom_tool_call_output") continue;
|
|
1864
|
+
const callId = readCodexSessionCallId(payload);
|
|
1865
|
+
const output = readCodexSessionFunctionOutput(payload);
|
|
1866
|
+
if (!callId || !output) continue;
|
|
1867
|
+
outputs.set(callId, mergeCodexSessionFunctionOutput(outputs.get(callId), output));
|
|
1868
|
+
}
|
|
1869
|
+
return outputs;
|
|
1870
|
+
}
|
|
1871
|
+
function mergeCodexSessionFunctionOutput(current, next) {
|
|
1872
|
+
if (!current) return next;
|
|
1873
|
+
const output = [current.output, next.output].filter(Boolean).join("\n").trim() || null;
|
|
1874
|
+
return {
|
|
1875
|
+
durationMs: next.durationMs ?? current.durationMs,
|
|
1876
|
+
exitCode: next.exitCode ?? current.exitCode,
|
|
1877
|
+
output
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
function readCodexSessionFunctionOutput(payload) {
|
|
1881
|
+
const rawOutput = typeof payload?.output === "string" ? payload.output : "";
|
|
1882
|
+
if (!rawOutput.trim()) return null;
|
|
1883
|
+
const exitCodeMatch = rawOutput.match(/\bProcess exited with code (-?\d+)\b/u);
|
|
1884
|
+
const exitCode = exitCodeMatch ? Number.parseInt(exitCodeMatch[1] ?? "", 10) : NaN;
|
|
1885
|
+
const wallTimeMatch = rawOutput.match(/^Wall time:\s*([0-9.]+)\s*seconds$/mu);
|
|
1886
|
+
const wallTimeSeconds = wallTimeMatch ? Number.parseFloat(wallTimeMatch[1] ?? "") : NaN;
|
|
1887
|
+
const output = readCodexSessionOutputBody(rawOutput);
|
|
1888
|
+
return {
|
|
1889
|
+
durationMs: Number.isFinite(wallTimeSeconds) ? Math.round(wallTimeSeconds * 1e3) : null,
|
|
1890
|
+
exitCode: Number.isFinite(exitCode) ? exitCode : null,
|
|
1891
|
+
output: output?.trim() ? output.trim() : null
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
function readCodexSessionExecCommandEndOutput(payload) {
|
|
1895
|
+
const output = readString(payload?.aggregated_output) ?? [readString(payload?.stdout), readString(payload?.stderr)].filter(Boolean).join("\n").trim() ?? null;
|
|
1896
|
+
const exitCode = readNumber(payload?.exit_code) ?? readNumber(payload?.exitCode) ?? null;
|
|
1897
|
+
const durationMs = readCodexDurationMs(payload?.duration) ?? readNumber(payload?.duration_ms) ?? readNumber(payload?.durationMs) ?? null;
|
|
1898
|
+
if (!output && exitCode === null && durationMs === null) return null;
|
|
1899
|
+
return {
|
|
1900
|
+
durationMs,
|
|
1901
|
+
exitCode,
|
|
1902
|
+
output
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
function readCodexSessionMcpToolEndOutput(payload) {
|
|
1906
|
+
const result = payload?.result === void 0 ? null : sanitizeCodexToolDetails(payload.result);
|
|
1907
|
+
const output = result === null ? null : JSON.stringify(result, null, 2);
|
|
1908
|
+
const durationMs = readCodexDurationMs(payload?.duration);
|
|
1909
|
+
if (!output && durationMs === null) return null;
|
|
1910
|
+
return {
|
|
1911
|
+
durationMs,
|
|
1912
|
+
exitCode: null,
|
|
1913
|
+
output
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
function readCodexDurationMs(value) {
|
|
1917
|
+
const duration = asJsonObject(value);
|
|
1918
|
+
const seconds = readNumber(duration?.secs) ?? readNumber(duration?.seconds);
|
|
1919
|
+
const nanos = readNumber(duration?.nanos) ?? readNumber(duration?.nanoseconds);
|
|
1920
|
+
if (seconds === void 0 && nanos === void 0) return null;
|
|
1921
|
+
return Math.round((seconds ?? 0) * 1e3 + (nanos ?? 0) / 1e6);
|
|
1922
|
+
}
|
|
1923
|
+
function readCodexSessionOutputBody(rawOutput) {
|
|
1924
|
+
const markerIndex = rawOutput.indexOf("\nOutput:\n");
|
|
1925
|
+
if (markerIndex >= 0) return rawOutput.slice(markerIndex + 9).trimEnd();
|
|
1926
|
+
if (rawOutput.startsWith("Output:\n")) return rawOutput.slice(8).trimEnd();
|
|
1927
|
+
return rawOutput.trim() || null;
|
|
1928
|
+
}
|
|
1929
|
+
async function readCodexLogMessages(codexHome, externalThreadId) {
|
|
1930
|
+
const databasePath = join(codexHome, "logs_2.sqlite");
|
|
1931
|
+
if (!(await stat(databasePath).catch(() => null))?.isFile()) return [];
|
|
1932
|
+
const submissionRows = await readCodexLogRows(databasePath, `
|
|
1933
|
+
select id, ts, ts_nanos, target, feedback_log_body, process_uuid
|
|
1934
|
+
from logs
|
|
1935
|
+
where thread_id = ${sqlString(externalThreadId)}
|
|
1936
|
+
and target = 'codex_core::session::handlers'
|
|
1937
|
+
and feedback_log_body like '%Submission sub=Submission%'
|
|
1938
|
+
order by ts asc, ts_nanos asc, id asc
|
|
1939
|
+
limit ${codexLogReadLimit}
|
|
1940
|
+
`);
|
|
1941
|
+
if (!submissionRows.length) return [];
|
|
1942
|
+
const messages = [];
|
|
1943
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
1944
|
+
const processIds = /* @__PURE__ */ new Set();
|
|
1945
|
+
let minTs = Number.POSITIVE_INFINITY;
|
|
1946
|
+
for (const row of submissionRows) {
|
|
1947
|
+
const body = row.feedback_log_body ?? "";
|
|
1948
|
+
for (const turnId of extractCodexTurnIds(body)) turnIds.add(turnId);
|
|
1949
|
+
const message = readCodexSubmissionMessage(row);
|
|
1950
|
+
if (message) messages.push(message);
|
|
1951
|
+
if (row.process_uuid) processIds.add(row.process_uuid);
|
|
1952
|
+
if (typeof row.ts === "number" && row.ts < minTs) minTs = row.ts;
|
|
1953
|
+
}
|
|
1954
|
+
if (!turnIds.size || !processIds.size || !Number.isFinite(minTs)) return messages;
|
|
1955
|
+
const turnPredicates = [...turnIds].map((turnId) => `feedback_log_body like ${sqlString(`%"turn_id":"${turnId}"%`)}`);
|
|
1956
|
+
const assistantRows = await readCodexLogRows(databasePath, `
|
|
1957
|
+
select id, ts, ts_nanos, target, feedback_log_body, process_uuid
|
|
1958
|
+
from logs
|
|
1959
|
+
where thread_id is null
|
|
1960
|
+
and process_uuid in (${[...processIds].map(sqlString).join(", ")})
|
|
1961
|
+
and ts >= ${Math.max(0, Math.floor(minTs) - 60)}
|
|
1962
|
+
and feedback_log_body like 'Received message {"type":"response.output_item.done"%'
|
|
1963
|
+
and (${turnPredicates.join(" or ")})
|
|
1964
|
+
order by ts asc, ts_nanos asc, id asc
|
|
1965
|
+
limit ${codexLogReadLimit}
|
|
1966
|
+
`);
|
|
1967
|
+
for (const row of assistantRows) {
|
|
1968
|
+
const message = readCodexReceivedMessage(row);
|
|
1969
|
+
if (message) messages.push(message);
|
|
1970
|
+
}
|
|
1971
|
+
return messages.sort(compareCodexMessages);
|
|
1972
|
+
}
|
|
1973
|
+
async function readCodexLogRows(databasePath, sql) {
|
|
1974
|
+
return new Promise((resolve) => {
|
|
1975
|
+
execFile("sqlite3", [
|
|
1976
|
+
"-readonly",
|
|
1977
|
+
"-json",
|
|
1978
|
+
databasePath,
|
|
1979
|
+
sql
|
|
1980
|
+
], {
|
|
1981
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
1982
|
+
timeout: 2e3
|
|
1983
|
+
}, (error, stdout) => {
|
|
1984
|
+
if (error || !stdout.trim()) {
|
|
1985
|
+
resolve([]);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
const rows = parseJsonLine(stdout);
|
|
1989
|
+
resolve(Array.isArray(rows) ? rows.map((row) => asJsonObject(row) ?? {}).filter(isCodexLogRow) : []);
|
|
1990
|
+
});
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
function isCodexLogRow(row) {
|
|
1994
|
+
return true;
|
|
1995
|
+
}
|
|
1996
|
+
function readCodexSubmissionMessage(row) {
|
|
1997
|
+
const body = row.feedback_log_body ?? "";
|
|
1998
|
+
const turnId = body.match(/Submission sub=Submission \{ id: "([0-9a-f-]{36})"/u)?.[1] ?? extractCodexTurnIds(body)[0];
|
|
1999
|
+
const inputText = [...body.split("final_output_json_schema:")[0].matchAll(/Text \{ text: "((?:\\.|[^"\\])*)"/gu)].map((match) => unescapeCodexLogString(match[1])).filter(Boolean).join("\n").trim();
|
|
2000
|
+
if (!inputText || isCodexInjectedContextMessage(inputText)) return null;
|
|
2001
|
+
return {
|
|
2002
|
+
role: "USER",
|
|
2003
|
+
content: inputText,
|
|
2004
|
+
itemId: turnId ? `${turnId}:user` : null,
|
|
2005
|
+
turnId: turnId ?? null,
|
|
2006
|
+
createdAt: codexLogRowDate(row)
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
function readCodexReceivedMessage(row) {
|
|
2010
|
+
const body = row.feedback_log_body ?? "";
|
|
2011
|
+
if (!body.startsWith("Received message ")) return null;
|
|
2012
|
+
const event = asJsonObject(parseJsonLine(body.slice(17)));
|
|
2013
|
+
if (readString(event?.type) !== "response.output_item.done") return null;
|
|
2014
|
+
const item = asJsonObject(event?.item);
|
|
2015
|
+
if (readString(item?.type) !== "message") return null;
|
|
2016
|
+
const role = codexMessageRole(item?.role);
|
|
2017
|
+
const content = readCodexMessageText(item?.content);
|
|
2018
|
+
if (!role || !content) return null;
|
|
2019
|
+
const turnId = readString(event?.turn_id) ?? readString(event?.turnId) ?? readString(item?.turn_id) ?? readString(item?.turnId) ?? extractCodexTurnIds(body)[0] ?? null;
|
|
2020
|
+
return {
|
|
2021
|
+
role,
|
|
2022
|
+
content,
|
|
2023
|
+
itemId: readString(item?.id) ?? null,
|
|
2024
|
+
turnId,
|
|
2025
|
+
createdAt: codexLogRowDate(row)
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
function extractCodexTurnIds(value) {
|
|
2029
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2030
|
+
for (const match of value.matchAll(/Submission sub=Submission \{ id: "([0-9a-f-]{36})"/gu)) ids.add(match[1]);
|
|
2031
|
+
for (const match of value.matchAll(/(?:submission\.id|turn\.id|turn_id)[=:]"?([0-9a-f-]{36})"?/gu)) ids.add(match[1]);
|
|
2032
|
+
return [...ids];
|
|
2033
|
+
}
|
|
2034
|
+
function unescapeCodexLogString(value) {
|
|
2035
|
+
try {
|
|
2036
|
+
return JSON.parse(`"${value}"`);
|
|
2037
|
+
} catch {
|
|
2038
|
+
return value.replaceAll("\\n", "\n").replaceAll("\\r", "\r").replaceAll("\\t", " ").replaceAll("\\\"", "\"").replaceAll("\\\\", "\\");
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
function codexLogRowDate(row) {
|
|
2042
|
+
if (typeof row.ts !== "number") return null;
|
|
2043
|
+
const milliseconds = row.ts * 1e3 + Math.floor((typeof row.ts_nanos === "number" ? row.ts_nanos : 0) / 1e6);
|
|
2044
|
+
return new Date(milliseconds).toISOString();
|
|
2045
|
+
}
|
|
2046
|
+
function mergeCodexMessages(sessionMessages, logMessages) {
|
|
2047
|
+
const messages = [];
|
|
2048
|
+
for (const message of [...sessionMessages, ...logMessages]) if (!messages.some((current) => sameCodexMessage(current, message))) messages.push(message);
|
|
2049
|
+
return messages.sort(compareCodexMessages);
|
|
2050
|
+
}
|
|
2051
|
+
function sameCodexMessage(left, right) {
|
|
2052
|
+
if (left.itemId && right.itemId && left.itemId === right.itemId) return true;
|
|
2053
|
+
if (left.role !== right.role || normalizeCodexMessageText(left.content) !== normalizeCodexMessageText(right.content)) return false;
|
|
2054
|
+
const leftTime = Date.parse(left.createdAt ?? "");
|
|
2055
|
+
const rightTime = Date.parse(right.createdAt ?? "");
|
|
2056
|
+
if (Number.isFinite(leftTime) && Number.isFinite(rightTime)) return Math.abs(leftTime - rightTime) < 6e4;
|
|
2057
|
+
return !left.itemId || !right.itemId;
|
|
2058
|
+
}
|
|
2059
|
+
function normalizeCodexMessageText(value) {
|
|
2060
|
+
return value.trim().replace(/\s+/gu, " ");
|
|
2061
|
+
}
|
|
2062
|
+
function compareCodexMessages(left, right) {
|
|
2063
|
+
const leftTime = Date.parse(left.createdAt ?? "");
|
|
2064
|
+
const rightTime = Date.parse(right.createdAt ?? "");
|
|
2065
|
+
if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) return leftTime - rightTime;
|
|
2066
|
+
if (Number.isFinite(leftTime) !== Number.isFinite(rightTime)) return Number.isFinite(leftTime) ? -1 : 1;
|
|
2067
|
+
return 0;
|
|
2068
|
+
}
|
|
2069
|
+
function sqlString(value) {
|
|
2070
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2071
|
+
}
|
|
2072
|
+
function readCodexSessionRecordMessage(record, payload, pendingCallIds = /* @__PURE__ */ new Set(), outputByCallId = /* @__PURE__ */ new Map()) {
|
|
2073
|
+
const recordType = readString(record?.type);
|
|
2074
|
+
const payloadType = readString(payload?.type);
|
|
2075
|
+
const createdAt = readIsoString(record?.timestamp) ?? null;
|
|
2076
|
+
if (recordType === "compacted") return readCodexSessionCompactionMessage(payload, createdAt);
|
|
2077
|
+
if (recordType === "response_item" && payloadType === "message") {
|
|
2078
|
+
const role = codexMessageRole(payload?.role);
|
|
2079
|
+
const content = readCodexMessageText(payload?.content);
|
|
2080
|
+
if (!role || !content || role === "USER" && isCodexInjectedContextMessage(content)) return null;
|
|
2081
|
+
return {
|
|
2082
|
+
role,
|
|
2083
|
+
content,
|
|
2084
|
+
itemId: readString(payload?.id) ?? null,
|
|
2085
|
+
createdAt
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
if (recordType === "response_item" && payloadType === "reasoning") return readCodexSessionReasoningMessage(payload, createdAt);
|
|
2089
|
+
if (recordType === "response_item" && payloadType && isCodexPlanItemType(payloadType)) return codexPlanItemMessage(payload ?? {}, createdAt);
|
|
2090
|
+
if (recordType === "event_msg" && payloadType === "user_message") {
|
|
2091
|
+
const content = readString(payload?.message);
|
|
2092
|
+
if (!content || isCodexInjectedContextMessage(content)) return null;
|
|
2093
|
+
return {
|
|
2094
|
+
role: "USER",
|
|
2095
|
+
content,
|
|
2096
|
+
itemId: readString(payload?.client_id) ?? null,
|
|
2097
|
+
createdAt
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
if (recordType === "event_msg" && payloadType === "agent_message") {
|
|
2101
|
+
const content = readString(payload?.message);
|
|
2102
|
+
if (!content) return null;
|
|
2103
|
+
return {
|
|
2104
|
+
role: "ASSISTANT",
|
|
2105
|
+
content,
|
|
2106
|
+
itemId: null,
|
|
2107
|
+
createdAt
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
if (recordType === "response_item" && (payloadType === "function_call" || payloadType === "custom_tool_call")) {
|
|
2111
|
+
if (payloadType === "custom_tool_call" && readString(payload?.name) === "apply_patch") return null;
|
|
2112
|
+
return readCodexSessionFunctionCallMessage(payload, createdAt, pendingCallIds, outputByCallId);
|
|
2113
|
+
}
|
|
2114
|
+
if (recordType === "response_item" && payloadType === "web_search_call") return readCodexSessionWebSearchMessage(payload, createdAt);
|
|
2115
|
+
if (recordType === "response_item" && payloadType === "image_generation_call") return readCodexSessionImageGenerationMessage(payload, createdAt);
|
|
2116
|
+
if (recordType === "event_msg" && payloadType === "patch_apply_end") return readCodexSessionPatchMessage(payload, createdAt);
|
|
2117
|
+
if (recordType === "event_msg" && payloadType === "web_search_end") return readCodexSessionWebSearchMessage(payload, createdAt);
|
|
2118
|
+
if (recordType === "event_msg" && payloadType === "image_generation_end") return readCodexSessionImageGenerationMessage(payload, createdAt);
|
|
2119
|
+
if (recordType === "event_msg" && payloadType === "mcp_tool_call_end") return readCodexSessionMcpToolMessage(payload, createdAt);
|
|
2120
|
+
if (recordType === "event_msg" && payloadType === "context_compacted") return readCodexSessionCompactionMessage(payload, createdAt);
|
|
2121
|
+
if (recordType === "event_msg" && payloadType === "item_completed") return readCodexSessionCompletedItemMessage(payload, createdAt);
|
|
2122
|
+
if (recordType === "event_msg" && payloadType === "error") return readCodexSessionErrorMessage(payload, createdAt);
|
|
2123
|
+
if (recordType === "event_msg" && (payloadType === "turn_aborted" || payloadType === "thread_rolled_back")) return readCodexSessionThreadWarningMessage(payload, createdAt);
|
|
2124
|
+
if (recordType === "response_item") return readCodexSessionFallbackToolMessage(payload, createdAt);
|
|
2125
|
+
return null;
|
|
2126
|
+
}
|
|
2127
|
+
function readCodexSessionReasoningMessage(payload, createdAt) {
|
|
2128
|
+
const content = formatCodexReasoning(payload ?? {});
|
|
2129
|
+
if (!content) return null;
|
|
2130
|
+
return {
|
|
2131
|
+
role: "ASSISTANT",
|
|
2132
|
+
kind: "THINKING",
|
|
2133
|
+
content,
|
|
2134
|
+
itemId: readString(payload?.id) ?? null,
|
|
2135
|
+
createdAt
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
function readCodexSessionFunctionCallMessage(payload, createdAt, pendingCallIds = /* @__PURE__ */ new Set(), outputByCallId = /* @__PURE__ */ new Map()) {
|
|
2139
|
+
const name = readString(payload?.name);
|
|
2140
|
+
const callId = readCodexSessionCallId(payload);
|
|
2141
|
+
const argumentsObject = readCodexSessionToolArguments(payload?.arguments);
|
|
2142
|
+
const toolOutput = callId ? outputByCallId.get(callId) : void 0;
|
|
2143
|
+
const status = callId && pendingCallIds.has(callId) ? "inProgress" : "completed";
|
|
2144
|
+
if (name === "exec_command") {
|
|
2145
|
+
const command = readString(argumentsObject?.cmd) ?? readString(argumentsObject?.command) ?? "command";
|
|
2146
|
+
const item = {
|
|
2147
|
+
type: "commandExecution",
|
|
2148
|
+
id: callId ?? `command:${createdAt ?? command}`,
|
|
2149
|
+
command,
|
|
2150
|
+
cwd: readString(argumentsObject?.workdir) ?? readString(argumentsObject?.cwd) ?? "",
|
|
2151
|
+
processId: null,
|
|
2152
|
+
source: "agent",
|
|
2153
|
+
status,
|
|
2154
|
+
commandActions: inferCodexSessionCommandActions(command),
|
|
2155
|
+
aggregatedOutput: toolOutput?.output ?? null,
|
|
2156
|
+
exitCode: toolOutput?.exitCode ?? null,
|
|
2157
|
+
durationMs: toolOutput?.durationMs ?? null
|
|
2158
|
+
};
|
|
2159
|
+
return codexActionMessage(item, "COMMAND_EXECUTION", formatCodexCommandExecution(item), createdAt);
|
|
2160
|
+
}
|
|
2161
|
+
if (name === "update_plan") return codexPlanUpdateMessage({
|
|
2162
|
+
createdAt,
|
|
2163
|
+
explanation: readString(argumentsObject?.explanation) ?? null,
|
|
2164
|
+
itemId: callId ?? `plan-update:${createdAt ?? name}`,
|
|
2165
|
+
status: status === "inProgress" ? "STREAMING" : "COMPLETED",
|
|
2166
|
+
steps: readCodexPlanSteps(argumentsObject?.plan)
|
|
2167
|
+
});
|
|
2168
|
+
if (name && isCodexRequestUserInputName(name)) {
|
|
2169
|
+
const payload = argumentsObject ?? {};
|
|
2170
|
+
return codexUserInputPromptMessage({
|
|
2171
|
+
createdAt,
|
|
2172
|
+
itemId: callId ?? `tool:${createdAt ?? name}`,
|
|
2173
|
+
payload,
|
|
2174
|
+
requestId: null,
|
|
2175
|
+
status: "completed"
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
if (!name) return null;
|
|
2179
|
+
const item = {
|
|
2180
|
+
type: "dynamicToolCall",
|
|
2181
|
+
id: callId ?? `tool:${createdAt ?? name}`,
|
|
2182
|
+
namespace: null,
|
|
2183
|
+
tool: name,
|
|
2184
|
+
arguments: argumentsObject ?? {},
|
|
2185
|
+
status,
|
|
2186
|
+
contentItems: toolOutput?.output ? [{
|
|
2187
|
+
type: "inputText",
|
|
2188
|
+
text: toolOutput.output
|
|
2189
|
+
}] : null,
|
|
2190
|
+
success: toolOutput?.exitCode === null || toolOutput?.exitCode === void 0 ? null : toolOutput.exitCode === 0,
|
|
2191
|
+
durationMs: toolOutput?.durationMs ?? null
|
|
2192
|
+
};
|
|
2193
|
+
return codexActionMessage(item, "TOOL_ACTIVITY", formatCodexDynamicToolCall(item), createdAt);
|
|
2194
|
+
}
|
|
2195
|
+
function readCodexSessionPatchMessage(payload, createdAt) {
|
|
2196
|
+
const changes = asJsonObject(payload?.changes);
|
|
2197
|
+
if (!changes) return null;
|
|
2198
|
+
const itemChanges = Object.entries(changes).map(([path, changeValue]) => {
|
|
2199
|
+
const change = asJsonObject(changeValue);
|
|
2200
|
+
return {
|
|
2201
|
+
path,
|
|
2202
|
+
kind: readString(change?.type) ?? "update",
|
|
2203
|
+
diff: readString(change?.unified_diff) ?? ""
|
|
2204
|
+
};
|
|
2205
|
+
}).filter((change) => change.path);
|
|
2206
|
+
if (!itemChanges.length) return null;
|
|
2207
|
+
const item = {
|
|
2208
|
+
type: "fileChange",
|
|
2209
|
+
id: readString(payload?.call_id) ?? `patch:${createdAt ?? itemChanges[0]?.path ?? "unknown"}`,
|
|
2210
|
+
changes: itemChanges,
|
|
2211
|
+
status: readString(payload?.status) ?? (payload?.success === false ? "failed" : "completed")
|
|
2212
|
+
};
|
|
2213
|
+
return codexActionMessage(item, "FILE_CHANGE", formatCodexFileChange(item), createdAt);
|
|
2214
|
+
}
|
|
2215
|
+
function readCodexSessionWebSearchMessage(payload, createdAt) {
|
|
2216
|
+
const item = {
|
|
2217
|
+
type: "webSearch",
|
|
2218
|
+
id: readString(payload?.call_id) ?? readString(payload?.id) ?? `web-search:${createdAt ?? "unknown"}`,
|
|
2219
|
+
query: readString(payload?.query) ?? null,
|
|
2220
|
+
action: payload?.action ?? null,
|
|
2221
|
+
status: readString(payload?.status) ?? "completed"
|
|
2222
|
+
};
|
|
2223
|
+
return codexActionMessage(item, "TOOL_ACTIVITY", formatCodexWebSearch(item), createdAt);
|
|
2224
|
+
}
|
|
2225
|
+
function readCodexSessionImageGenerationMessage(payload, createdAt) {
|
|
2226
|
+
const item = {
|
|
2227
|
+
type: "imageGeneration",
|
|
2228
|
+
id: readString(payload?.call_id) ?? readString(payload?.id) ?? `image-generation:${createdAt ?? "unknown"}`,
|
|
2229
|
+
revisedPrompt: readString(payload?.revised_prompt) ?? readString(payload?.revisedPrompt) ?? null,
|
|
2230
|
+
savedPath: readString(payload?.saved_path) ?? readString(payload?.savedPath) ?? null,
|
|
2231
|
+
status: readString(payload?.status) ?? "completed"
|
|
2232
|
+
};
|
|
2233
|
+
return codexActionMessage(item, "TOOL_ACTIVITY", formatCodexSimpleToolItem(item), createdAt);
|
|
2234
|
+
}
|
|
2235
|
+
function readCodexSessionMcpToolMessage(payload, createdAt) {
|
|
2236
|
+
const invocation = asJsonObject(payload?.invocation);
|
|
2237
|
+
const item = {
|
|
2238
|
+
type: "mcpToolCall",
|
|
2239
|
+
id: readString(payload?.call_id) ?? `mcp:${createdAt ?? "unknown"}`,
|
|
2240
|
+
server: readString(invocation?.server) ?? null,
|
|
2241
|
+
tool: readString(invocation?.tool) ?? null,
|
|
2242
|
+
arguments: invocation?.arguments ?? null,
|
|
2243
|
+
result: payload?.result ?? null,
|
|
2244
|
+
status: readString(payload?.status) ?? "completed"
|
|
2245
|
+
};
|
|
2246
|
+
return codexActionMessage(item, "TOOL_ACTIVITY", formatCodexMcpToolCall(item), createdAt);
|
|
2247
|
+
}
|
|
2248
|
+
function readCodexSessionCompactionMessage(payload, createdAt) {
|
|
2249
|
+
const item = {
|
|
2250
|
+
type: "contextCompaction",
|
|
2251
|
+
id: readString(payload?.call_id) ?? readString(payload?.id) ?? `compaction:${createdAt ?? "unknown"}`,
|
|
2252
|
+
status: readString(payload?.status) ?? "completed"
|
|
2253
|
+
};
|
|
2254
|
+
return codexActionMessage(item, "COMPACTION", formatCodexSimpleToolItem(item), createdAt);
|
|
2255
|
+
}
|
|
2256
|
+
function readCodexSessionCompletedItemMessage(payload, createdAt) {
|
|
2257
|
+
const item = asJsonObject(payload?.item);
|
|
2258
|
+
if (!isCodexPlanItemType(readString(item?.type))) return null;
|
|
2259
|
+
return codexPlanItemMessage(item ?? {}, createdAt, readString(payload?.call_id) ?? null);
|
|
2260
|
+
}
|
|
2261
|
+
function isCodexPlanItemType(type) {
|
|
2262
|
+
const normalized = (type ?? "").toLowerCase().replace(/[-_\s]+/gu, "");
|
|
2263
|
+
return normalized === "plan" || normalized === "proposedplan";
|
|
2264
|
+
}
|
|
2265
|
+
function codexPlanItemMessage(item, createdAt, fallbackItemId = null) {
|
|
2266
|
+
const steps = readCodexPlanItemSteps(item);
|
|
2267
|
+
const explanation = readCodexPlanItemText(item);
|
|
2268
|
+
const itemId = readString(item.id) ?? fallbackItemId;
|
|
2269
|
+
if (steps.length) return codexPlanUpdateMessage({
|
|
2270
|
+
createdAt,
|
|
2271
|
+
explanation,
|
|
2272
|
+
itemId,
|
|
2273
|
+
status: "COMPLETED",
|
|
2274
|
+
steps
|
|
2275
|
+
});
|
|
2276
|
+
const content = explanation ?? formatCodexFallbackToolItem(item);
|
|
2277
|
+
if (!content) return null;
|
|
2278
|
+
return {
|
|
2279
|
+
role: "ASSISTANT",
|
|
2280
|
+
kind: "PLAN",
|
|
2281
|
+
content,
|
|
2282
|
+
itemId,
|
|
2283
|
+
createdAt
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
function readCodexPlanItemText(item) {
|
|
2287
|
+
return readString(item.text) ?? readString(item.message) ?? readString(item.summary) ?? readCodexMessageText(item.content) ?? readString(item.plan) ?? null;
|
|
2288
|
+
}
|
|
2289
|
+
function readCodexPlanItemSteps(item) {
|
|
2290
|
+
const directSteps = readCodexPlanSteps(item.plan);
|
|
2291
|
+
if (directSteps.length) return directSteps;
|
|
2292
|
+
const steps = readCodexPlanSteps(item.steps);
|
|
2293
|
+
if (steps.length) return steps;
|
|
2294
|
+
const plan = asJsonObject(item.plan);
|
|
2295
|
+
return plan ? readCodexPlanSteps(plan.steps ?? plan.plan) : [];
|
|
2296
|
+
}
|
|
2297
|
+
function readCodexSessionErrorMessage(payload, createdAt) {
|
|
2298
|
+
const content = readString(payload?.message) ?? "Codex error";
|
|
2299
|
+
return {
|
|
2300
|
+
role: "TOOL",
|
|
2301
|
+
kind: "ERROR",
|
|
2302
|
+
status: "FAILED",
|
|
2303
|
+
content,
|
|
2304
|
+
itemId: readString(payload?.call_id) ?? readString(payload?.id) ?? `error:${createdAt ?? content}`,
|
|
2305
|
+
createdAt,
|
|
2306
|
+
metadata: { codexErrorInfo: readString(payload?.codex_error_info) ?? null }
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
function readCodexSessionThreadWarningMessage(payload, createdAt) {
|
|
2310
|
+
const label = readString(payload?.type) === "turn_aborted" ? "Turn aborted" : "Thread rolled back";
|
|
2311
|
+
const count = readNumber(payload?.num_turns);
|
|
2312
|
+
return {
|
|
2313
|
+
role: "TOOL",
|
|
2314
|
+
kind: "WARNING",
|
|
2315
|
+
content: count === void 0 ? label : `${label}\n\nturns: \`${count}\``,
|
|
2316
|
+
itemId: readString(payload?.turn_id) ?? readString(payload?.turnId) ?? `warning:${createdAt ?? label}`,
|
|
2317
|
+
createdAt
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
function readCodexSessionFallbackToolMessage(payload, createdAt) {
|
|
2321
|
+
const type = readString(payload?.type);
|
|
2322
|
+
if (!payload || !type || !isCodexSessionVisibleFallbackItem(type)) return null;
|
|
2323
|
+
const callId = readString(payload.call_id) ?? readString(payload.id);
|
|
2324
|
+
const item = {
|
|
2325
|
+
...payload,
|
|
2326
|
+
id: callId ?? `tool:${createdAt ?? type}`,
|
|
2327
|
+
status: readString(payload.status) ?? "completed"
|
|
2328
|
+
};
|
|
2329
|
+
return codexActionMessage(item, "TOOL_ACTIVITY", formatCodexFallbackToolItem(item), createdAt);
|
|
2330
|
+
}
|
|
2331
|
+
function isCodexSessionVisibleFallbackItem(type) {
|
|
2332
|
+
return type !== "reasoning" && type !== "function_call_output" && type !== "custom_tool_call" && type !== "custom_tool_call_output";
|
|
2333
|
+
}
|
|
2334
|
+
function readCodexSessionToolArguments(value) {
|
|
2335
|
+
if (typeof value !== "string") return asJsonObject(value) ?? null;
|
|
2336
|
+
return asJsonObject(parseJsonLine(value)) ?? null;
|
|
2337
|
+
}
|
|
2338
|
+
function readCodexSessionCallId(payload) {
|
|
2339
|
+
return readString(payload?.call_id) ?? readString(payload?.callId) ?? readString(payload?.id) ?? null;
|
|
2340
|
+
}
|
|
2341
|
+
function isCodexSessionRunningToolOutput(payload) {
|
|
2342
|
+
const output = readString(payload?.output);
|
|
2343
|
+
return Boolean(output?.match(/\bProcess running with session ID \d+\b/u));
|
|
2344
|
+
}
|
|
2345
|
+
function readCodexSessionRecordTurnId(record, payload) {
|
|
2346
|
+
const metadata = asJsonObject(payload?.internal_chat_message_metadata_passthrough);
|
|
2347
|
+
return readString(metadata?.turn_id) ?? readString(metadata?.turnId) ?? readString(payload?.turn_id) ?? readString(payload?.turnId) ?? readString(record?.turn_id) ?? readString(record?.turnId) ?? null;
|
|
2348
|
+
}
|
|
2349
|
+
function inferCodexSessionCommandActions(command) {
|
|
2350
|
+
const trimmed = command.trim();
|
|
2351
|
+
if (!trimmed) return [];
|
|
2352
|
+
const executable = trimmed.match(/^([A-Za-z0-9_.-]+)/u)?.[1] ?? "";
|
|
2353
|
+
if (executable === "rg" || executable === "grep") return [{
|
|
2354
|
+
type: "search",
|
|
2355
|
+
command: trimmed,
|
|
2356
|
+
query: readShellSearchQuery(trimmed),
|
|
2357
|
+
path: readShellPath(trimmed)
|
|
2358
|
+
}];
|
|
2359
|
+
if (executable === "find") return [{
|
|
2360
|
+
type: "search",
|
|
2361
|
+
command: trimmed,
|
|
2362
|
+
path: readShellPath(trimmed)
|
|
2363
|
+
}];
|
|
2364
|
+
if (executable === "ls") return [{
|
|
2365
|
+
type: "listFiles",
|
|
2366
|
+
command: trimmed,
|
|
2367
|
+
path: readShellPath(trimmed)
|
|
2368
|
+
}];
|
|
2369
|
+
if ([
|
|
2370
|
+
"cat",
|
|
2371
|
+
"head",
|
|
2372
|
+
"tail",
|
|
2373
|
+
"sed",
|
|
2374
|
+
"nl",
|
|
2375
|
+
"wc"
|
|
2376
|
+
].includes(executable)) return [{
|
|
2377
|
+
type: "read",
|
|
2378
|
+
command: trimmed,
|
|
2379
|
+
path: readShellPath(trimmed)
|
|
2380
|
+
}];
|
|
2381
|
+
return [{
|
|
2382
|
+
type: "unknown",
|
|
2383
|
+
command: trimmed
|
|
2384
|
+
}];
|
|
2385
|
+
}
|
|
2386
|
+
function readShellSearchQuery(command) {
|
|
2387
|
+
return readShellArguments(command).find((token) => !token.startsWith("-")) ?? null;
|
|
2388
|
+
}
|
|
2389
|
+
function readShellPath(command) {
|
|
2390
|
+
return readShellArguments(command).filter((token) => !token.startsWith("-")).at(-1) ?? null;
|
|
2391
|
+
}
|
|
2392
|
+
function readShellArguments(command) {
|
|
2393
|
+
return ((command.split(/\s+(?:\||&&|\|\||;)\s+/u)[0] ?? command).match(/"[^"]+"|'[^']+'|[^\s]+/gu) ?? []).slice(1).map((token) => token.replace(/^['"]|['"]$/gu, "")).filter(Boolean);
|
|
2394
|
+
}
|
|
2395
|
+
function readCodexMessageText(value) {
|
|
2396
|
+
if (typeof value === "string") return value.trim() || null;
|
|
2397
|
+
if (!Array.isArray(value)) return null;
|
|
2398
|
+
return value.map((item) => {
|
|
2399
|
+
const object = asJsonObject(item);
|
|
2400
|
+
return readString(object?.text) ?? readString(object?.content) ?? "";
|
|
2401
|
+
}).filter(Boolean).join("\n").trim() || null;
|
|
2402
|
+
}
|
|
2403
|
+
function isCodexInjectedContextMessage(content) {
|
|
2404
|
+
const trimmed = content.trimStart();
|
|
2405
|
+
return content.includes("<environment_context>") || content.includes("# AGENTS.md instructions") || trimmed.startsWith("<user_action>");
|
|
2406
|
+
}
|
|
2407
|
+
function codexMessageRole(value) {
|
|
2408
|
+
if (value === "user") return "USER";
|
|
2409
|
+
if (value === "assistant") return "ASSISTANT";
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
function parseJsonLine(line) {
|
|
2413
|
+
if (!line.trim()) return null;
|
|
2414
|
+
try {
|
|
2415
|
+
return JSON.parse(line);
|
|
2416
|
+
} catch {
|
|
2417
|
+
return null;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
function readIsoString(value) {
|
|
2421
|
+
const raw = readString(value);
|
|
2422
|
+
if (!raw) return null;
|
|
2423
|
+
const date = new Date(raw);
|
|
2424
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
2425
|
+
}
|
|
2426
|
+
function newestCodexTimestamp(candidate, current) {
|
|
2427
|
+
if (!candidate) return current;
|
|
2428
|
+
if (!current) return candidate;
|
|
2429
|
+
const candidateTime = Date.parse(candidate);
|
|
2430
|
+
const currentTime = Date.parse(current);
|
|
2431
|
+
if (!Number.isFinite(candidateTime)) return current;
|
|
2432
|
+
if (!Number.isFinite(currentTime)) return candidate;
|
|
2433
|
+
return candidateTime > currentTime ? candidate : current;
|
|
2434
|
+
}
|
|
2435
|
+
async function copyCodexThread(source, target, threadId, options = {}) {
|
|
2436
|
+
try {
|
|
2437
|
+
if (sameFilesystemPath(source, target)) return true;
|
|
2438
|
+
if (!(await stat(source).catch(() => null))?.isDirectory()) return false;
|
|
2439
|
+
await mkdir(target, {
|
|
2440
|
+
recursive: true,
|
|
2441
|
+
mode: 448
|
|
2442
|
+
});
|
|
2443
|
+
const copiedIndex = await copyCodexThreadIndex(source, target, threadId, options);
|
|
2444
|
+
const copiedSession = await copyCodexThreadSessionFiles(source, target, threadId, options);
|
|
2445
|
+
await copyCodexStateFiles(source, target);
|
|
2446
|
+
return copiedIndex || copiedSession;
|
|
2447
|
+
} catch {
|
|
2448
|
+
return false;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
async function copyCodexThreadIndex(source, target, threadId, options = {}) {
|
|
2452
|
+
const sourceLines = (await readFile(join(source, "session_index.jsonl"), "utf8").catch(() => "")).split(/\r?\n/u).filter((line) => codexIndexLineThreadId(line) === threadId).slice(-1);
|
|
2453
|
+
if (!sourceLines.length) return false;
|
|
2454
|
+
const targetIndex = join(target, "session_index.jsonl");
|
|
2455
|
+
const currentTargetLines = (await readFile(targetIndex, "utf8").catch(() => "")).split(/\r?\n/u).filter((line) => line.trim());
|
|
2456
|
+
if (options.preserveExistingTarget && currentTargetLines.some((line) => codexIndexLineThreadId(line) === threadId)) return true;
|
|
2457
|
+
await writeFile(targetIndex, `${[...currentTargetLines.filter((line) => line.trim() && codexIndexLineThreadId(line) !== threadId), ...sourceLines].join("\n")}\n`);
|
|
2458
|
+
return true;
|
|
2459
|
+
}
|
|
2460
|
+
async function copyCodexThreadSessionFiles(source, target, threadId, options = {}) {
|
|
2461
|
+
const sessionFiles = (await listCodexSessionFiles(source)).filter((file) => codexSessionIdFromPath(file) === threadId);
|
|
2462
|
+
let copied = false;
|
|
2463
|
+
for (const sourceFile of sessionFiles) {
|
|
2464
|
+
const targetFile = join(target, relative(source, sourceFile));
|
|
2465
|
+
if ((options.preserveExistingTarget ? await stat(targetFile).catch(() => null) : null)?.isFile()) {
|
|
2466
|
+
copied = true;
|
|
2467
|
+
continue;
|
|
2468
|
+
}
|
|
2469
|
+
await mkdir(dirname(targetFile), {
|
|
2470
|
+
recursive: true,
|
|
2471
|
+
mode: 448
|
|
2472
|
+
});
|
|
2473
|
+
await copyFile(sourceFile, targetFile);
|
|
2474
|
+
copied = true;
|
|
2475
|
+
}
|
|
2476
|
+
return copied;
|
|
2477
|
+
}
|
|
2478
|
+
async function removeCodexThread(codexHome, threadId) {
|
|
2479
|
+
try {
|
|
2480
|
+
await removeCodexThreadIndex(codexHome, threadId);
|
|
2481
|
+
const sessionFiles = (await listCodexSessionFiles(codexHome)).filter((file) => codexSessionIdFromPath(file) === threadId);
|
|
2482
|
+
for (const sessionFile of sessionFiles) await rm(sessionFile, { force: true });
|
|
2483
|
+
return true;
|
|
2484
|
+
} catch {
|
|
2485
|
+
return false;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
async function removeCodexThreadIndex(codexHome, threadId) {
|
|
2489
|
+
const indexPath = join(codexHome, "session_index.jsonl");
|
|
2490
|
+
const currentLines = (await readFile(indexPath, "utf8").catch(() => "")).split(/\r?\n/u).filter((line) => line.trim());
|
|
2491
|
+
const nextLines = currentLines.filter((line) => codexIndexLineThreadId(line) !== threadId);
|
|
2492
|
+
if (nextLines.length !== currentLines.length) await writeFile(indexPath, nextLines.length ? `${nextLines.join("\n")}\n` : "");
|
|
2493
|
+
}
|
|
2494
|
+
function codexIndexLineThreadId(line) {
|
|
2495
|
+
const row = asJsonObject(parseJsonLine(line));
|
|
2496
|
+
return readString(row?.threadId) ?? readString(row?.thread_id) ?? readString(row?.id) ?? null;
|
|
2497
|
+
}
|
|
2498
|
+
async function copyCodexStateFiles(source, target) {
|
|
2499
|
+
for (const entry of await readdir(source).catch(() => [])) if (/^state_\d+\.sqlite(?:-(?:wal|shm))?$/u.test(entry)) await copyIfExists(join(source, entry), join(target, entry));
|
|
2500
|
+
}
|
|
2501
|
+
async function copyIfExists(source, target) {
|
|
2502
|
+
if (sameFilesystemPath(source, target)) return;
|
|
2503
|
+
if (!(await stat(source).catch(() => null))?.isFile()) return;
|
|
2504
|
+
await mkdir(join(target, ".."), {
|
|
2505
|
+
recursive: true,
|
|
2506
|
+
mode: 448
|
|
2507
|
+
});
|
|
2508
|
+
await copyFile(source, target);
|
|
2509
|
+
}
|
|
2510
|
+
function hasAccountAuthFile(account) {
|
|
2511
|
+
return Boolean(statSyncSafe(join(resolveAccountCodexHome(account), "auth.json"))?.isFile());
|
|
2512
|
+
}
|
|
2513
|
+
function readAuthEmailAlias(account) {
|
|
2514
|
+
const token = readString(asJsonObject(readAuthJson(account)?.tokens)?.id_token);
|
|
2515
|
+
if (!token) return null;
|
|
2516
|
+
return readString(decodeJwtPayload(token)?.email)?.split("@")[0]?.trim() || null;
|
|
2517
|
+
}
|
|
2518
|
+
function readAuthJson(account) {
|
|
2519
|
+
try {
|
|
2520
|
+
return asJsonObject(JSON.parse(require("node:fs").readFileSync(join(resolveAccountCodexHome(account), "auth.json"), "utf8"))) ?? null;
|
|
2521
|
+
} catch {
|
|
2522
|
+
return null;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
function usesSharedCodexHome(account) {
|
|
2526
|
+
return readString(normalizeJsonObject(account.authState).codexHomeMode) === "shared";
|
|
2527
|
+
}
|
|
2528
|
+
function decodeJwtPayload(token) {
|
|
2529
|
+
const [, payload] = token.split(".");
|
|
2530
|
+
if (!payload) return null;
|
|
2531
|
+
try {
|
|
2532
|
+
return asJsonObject(JSON.parse(Buffer.from(payload, "base64url").toString("utf8"))) ?? null;
|
|
2533
|
+
} catch {
|
|
2534
|
+
return null;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
function statSyncSafe(pathname) {
|
|
2538
|
+
try {
|
|
2539
|
+
return require("node:fs").statSync(pathname);
|
|
2540
|
+
} catch {
|
|
2541
|
+
return null;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
function readLoginAuthUrl(result, mode) {
|
|
2545
|
+
if (!result) return null;
|
|
2546
|
+
if (mode === "device") return readString(result.verificationUrl) ?? readString(result.verification_uri) ?? readString(result.verification_uri_complete) ?? readString(result.authUrl) ?? null;
|
|
2547
|
+
return readString(result.authUrl) ?? readString(result.url) ?? null;
|
|
2548
|
+
}
|
|
2549
|
+
function parseLoopbackCallbackUrl(value) {
|
|
2550
|
+
const url = new URL(value);
|
|
2551
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("Callback URL must be HTTP or HTTPS.");
|
|
2552
|
+
if (![
|
|
2553
|
+
"127.0.0.1",
|
|
2554
|
+
"localhost",
|
|
2555
|
+
"[::1]",
|
|
2556
|
+
"::1"
|
|
2557
|
+
].includes(url.hostname)) throw new Error("Callback URL must point at localhost.");
|
|
2558
|
+
return url.toString();
|
|
2559
|
+
}
|
|
2560
|
+
function readAuthMode(value) {
|
|
2561
|
+
return value === "browser" || value === "device" || value === "local" ? value : null;
|
|
2562
|
+
}
|
|
2563
|
+
function sameFilesystemPath(left, right) {
|
|
2564
|
+
return resolve(left) === resolve(right);
|
|
2565
|
+
}
|
|
2566
|
+
function connectedAuthResponse(accountId, message, authState) {
|
|
2567
|
+
return {
|
|
2568
|
+
accountId,
|
|
2569
|
+
status: "CONNECTED",
|
|
2570
|
+
authMode: null,
|
|
2571
|
+
...authState === void 0 ? {} : { authState },
|
|
2572
|
+
authUrl: null,
|
|
2573
|
+
verificationUrl: null,
|
|
2574
|
+
userCode: null,
|
|
2575
|
+
message
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
function localCodexAuthState() {
|
|
2579
|
+
return {
|
|
2580
|
+
codexHome: resolveSharedCodexHome(),
|
|
2581
|
+
codexHomeMode: "shared"
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
function normalizeModelList(value) {
|
|
2585
|
+
const result = asJsonObject(value);
|
|
2586
|
+
return {
|
|
2587
|
+
data: mergeCodexModelOptions((Array.isArray(result?.data) ? result.data : []).map((row) => {
|
|
2588
|
+
const object = asJsonObject(row) ?? {};
|
|
2589
|
+
const id = readString(object.id) ?? readString(object.model) ?? "unknown";
|
|
2590
|
+
const upgrade = readString(object.upgrade);
|
|
2591
|
+
return {
|
|
2592
|
+
id,
|
|
2593
|
+
model: readString(object.model) ?? id,
|
|
2594
|
+
displayName: readString(object.displayName) ?? readString(object.display_name) ?? id,
|
|
2595
|
+
hidden: Boolean(object.hidden),
|
|
2596
|
+
defaultReasoningEffort: readString(object.defaultReasoningEffort) ?? readString(object.default_reasoning_effort) ?? null,
|
|
2597
|
+
defaultServiceTier: readString(object.defaultServiceTier) ?? readString(object.default_service_tier) ?? null,
|
|
2598
|
+
inputModalities: readStringList(object.inputModalities) ?? readStringList(object.input_modalities) ?? [],
|
|
2599
|
+
isDefault: readBooleanValue(object.isDefault) ?? readBooleanValue(object.is_default) ?? false,
|
|
2600
|
+
serviceTiers: readModelServiceTiers(object.serviceTiers) ?? readModelServiceTiers(object.service_tiers) ?? [],
|
|
2601
|
+
supportsPersonality: readBooleanValue(object.supportsPersonality) ?? readBooleanValue(object.supports_personality) ?? false,
|
|
2602
|
+
supportedReasoningEfforts: readSupportedReasoningEfforts(object.supportedReasoningEfforts) ?? readSupportedReasoningEfforts(object.supported_reasoning_efforts) ?? [],
|
|
2603
|
+
upgradeInfo: readModelUpgradeInfo(object.upgradeInfo) ?? readModelUpgradeInfo(object.upgrade_info) ?? (upgrade ? { upgrade } : null)
|
|
2604
|
+
};
|
|
2605
|
+
})),
|
|
2606
|
+
nextCursor: readString(result?.nextCursor) ?? readString(result?.next_cursor) ?? null
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
function readSupportedReasoningEfforts(value) {
|
|
2610
|
+
if (!Array.isArray(value)) return null;
|
|
2611
|
+
const efforts = value.map((entry) => {
|
|
2612
|
+
if (typeof entry === "string") return { reasoningEffort: entry };
|
|
2613
|
+
const object = asJsonObject(entry);
|
|
2614
|
+
const reasoningEffort = readString(object?.reasoningEffort) ?? readString(object?.reasoning_effort);
|
|
2615
|
+
return reasoningEffort ? {
|
|
2616
|
+
reasoningEffort,
|
|
2617
|
+
description: readString(object?.description)
|
|
2618
|
+
} : null;
|
|
2619
|
+
}).filter((entry) => Boolean(entry));
|
|
2620
|
+
return efforts.length ? efforts : null;
|
|
2621
|
+
}
|
|
2622
|
+
function readModelServiceTiers(value) {
|
|
2623
|
+
if (!Array.isArray(value)) return null;
|
|
2624
|
+
const tiers = value.map((entry) => {
|
|
2625
|
+
const object = asJsonObject(entry);
|
|
2626
|
+
const id = readString(object?.id);
|
|
2627
|
+
if (!id) return null;
|
|
2628
|
+
return {
|
|
2629
|
+
id,
|
|
2630
|
+
name: readString(object?.name) ?? id,
|
|
2631
|
+
description: readString(object?.description) ?? ""
|
|
2632
|
+
};
|
|
2633
|
+
}).filter((entry) => Boolean(entry));
|
|
2634
|
+
return tiers.length ? tiers : null;
|
|
2635
|
+
}
|
|
2636
|
+
function readModelUpgradeInfo(value) {
|
|
2637
|
+
if (value === void 0 || value === null) return null;
|
|
2638
|
+
const object = asJsonObject(value);
|
|
2639
|
+
return object ? jsonFromUnknown(object) : null;
|
|
2640
|
+
}
|
|
2641
|
+
function mergeCodexModelOptions(options) {
|
|
2642
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2643
|
+
for (const option of [...codexDefaultModelOptions, ...options]) {
|
|
2644
|
+
const key = (option.model || option.id).trim().toLowerCase();
|
|
2645
|
+
const existing = merged.get(key);
|
|
2646
|
+
merged.set(key, existing ? mergeCodexModelOption(existing, option) : option);
|
|
2647
|
+
}
|
|
2648
|
+
return [...merged.values()];
|
|
2649
|
+
}
|
|
2650
|
+
function mergeCodexModelOption(fallback, option) {
|
|
2651
|
+
return {
|
|
2652
|
+
...fallback,
|
|
2653
|
+
...option,
|
|
2654
|
+
defaultReasoningEffort: option.defaultReasoningEffort ?? fallback.defaultReasoningEffort,
|
|
2655
|
+
supportedReasoningEfforts: option.supportedReasoningEfforts?.length ? option.supportedReasoningEfforts : fallback.supportedReasoningEfforts
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
function normalizeLimits(value) {
|
|
2659
|
+
const result = asJsonObject(value);
|
|
2660
|
+
const snapshot = asJsonObject(result?.rateLimits) ?? asJsonObject(result?.rate_limits) ?? asJsonObject(result);
|
|
2661
|
+
return {
|
|
2662
|
+
rateLimits: snapshot ? {
|
|
2663
|
+
limitId: readString(snapshot.limitId) ?? readString(snapshot.limit_id) ?? null,
|
|
2664
|
+
limitName: readString(snapshot.limitName) ?? readString(snapshot.limit_name) ?? null,
|
|
2665
|
+
planType: readString(snapshot.planType) ?? readString(snapshot.plan_type) ?? null,
|
|
2666
|
+
primary: normalizeLimitWindow(snapshot.primary),
|
|
2667
|
+
secondary: normalizeLimitWindow(snapshot.secondary),
|
|
2668
|
+
rateLimitReachedType: readString(snapshot.rateLimitReachedType) ?? readString(snapshot.rate_limit_reached_type) ?? null
|
|
2669
|
+
} : void 0,
|
|
2670
|
+
raw: jsonFromUnknown(value)
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
function normalizeLimitWindow(value) {
|
|
2674
|
+
const object = asJsonObject(value);
|
|
2675
|
+
if (!object) return;
|
|
2676
|
+
return {
|
|
2677
|
+
usedPercent: readNumberLike(object.usedPercent) ?? readNumberLike(object.used_percent) ?? 0,
|
|
2678
|
+
windowDurationMins: readNumberLike(object.windowDurationMins) ?? readNumberLike(object.window_duration_mins) ?? null,
|
|
2679
|
+
resetsAt: readNumberLike(object.resetsAt) ?? readNumberLike(object.resets_at) ?? null
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
function readNumberLike(value) {
|
|
2683
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2684
|
+
}
|
|
2685
|
+
async function ensureCodexThread(runtime, threadId, workingDirectory, collaborationMode) {
|
|
2686
|
+
const result = asJsonObject((await runtime.request(threadId ? "thread/resume" : "thread/start", {
|
|
2687
|
+
...threadId ? { threadId } : {},
|
|
2688
|
+
cwd: workingDirectory,
|
|
2689
|
+
collaborationMode: collaborationModePayload(collaborationMode, null)
|
|
2690
|
+
}, 3e4)).result);
|
|
2691
|
+
const nextThreadId = readString(asJsonObject(result?.thread)?.id) ?? readString(result?.threadId) ?? readString(result?.thread_id) ?? threadId;
|
|
2692
|
+
if (!nextThreadId) throw new Error("Codex did not return a thread id.");
|
|
2693
|
+
return nextThreadId;
|
|
2694
|
+
}
|
|
2695
|
+
async function prepareCodexThreadForLifecycleAction(account, externalThreadId, workingDirectory) {
|
|
2696
|
+
const runtime = runtimeForAccount(account, workingDirectory);
|
|
2697
|
+
try {
|
|
2698
|
+
if (!await hydrateKnownCodexThreadToAccount(externalThreadId, account)) throw new Error(codexThreadNotFoundMessage(externalThreadId));
|
|
2699
|
+
await ensureCodexThread(runtime, externalThreadId, workingDirectory ?? null, "default");
|
|
2700
|
+
return runtime;
|
|
2701
|
+
} catch (error) {
|
|
2702
|
+
if (isCodexThreadNotFoundError(error)) throw new Error(codexThreadNotFoundMessage(externalThreadId));
|
|
2703
|
+
throw error;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
async function setCodexThreadGoal(runtime, threadId, objective) {
|
|
2707
|
+
await runtime.request("thread/goal/set", {
|
|
2708
|
+
threadId,
|
|
2709
|
+
objective: objective.trim(),
|
|
2710
|
+
status: "active"
|
|
2711
|
+
}, 3e4);
|
|
2712
|
+
}
|
|
2713
|
+
async function requestCodexRuntime(runtime, method, params, timeoutMs = 3e4) {
|
|
2714
|
+
try {
|
|
2715
|
+
return await runtime.request(method, params, timeoutMs);
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
if (isUnsupportedCodexMethodError(error)) throw new Error(`Codex method ${method} is not supported by this Codex binary.`);
|
|
2718
|
+
throw error;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
async function requestCodexOptional(runtime, method, params, timeoutMs = 3e4) {
|
|
2722
|
+
try {
|
|
2723
|
+
await runtime.request(method, params, timeoutMs);
|
|
2724
|
+
return true;
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
if (isUnsupportedCodexMethodError(error)) return false;
|
|
2727
|
+
throw error;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
function isUnsupportedCodexMethodError(error) {
|
|
2731
|
+
const message = readErrorMessage$1(error).toLowerCase();
|
|
2732
|
+
return message.includes("method not found") || message.includes("unknown method") || message.includes("unsupported method") || message.includes("-32601");
|
|
2733
|
+
}
|
|
2734
|
+
function isCodexRequestUserInputMethod(method) {
|
|
2735
|
+
const normalized = normalizedCodexName(method);
|
|
2736
|
+
return normalized === "itemtoolrequestuserinput" || normalized.endsWith("requestuserinput");
|
|
2737
|
+
}
|
|
2738
|
+
function isCodexRequestUserInputName(name) {
|
|
2739
|
+
return normalizedCodexName(name) === "requestuserinput";
|
|
2740
|
+
}
|
|
2741
|
+
function normalizedCodexName(value) {
|
|
2742
|
+
return value.toLowerCase().replace(/[^a-z0-9]/gu, "");
|
|
2743
|
+
}
|
|
2744
|
+
function codexUserInputPromptStatus(value) {
|
|
2745
|
+
if (value === "completed") return "COMPLETED";
|
|
2746
|
+
if (value === "failed" || value === "declined") return "FAILED";
|
|
2747
|
+
return "PENDING";
|
|
2748
|
+
}
|
|
2749
|
+
function isCodexThreadNotFoundError(error) {
|
|
2750
|
+
const message = readErrorMessage$1(error).toLowerCase();
|
|
2751
|
+
return message.includes("thread not found") || message.includes("thread") && message.includes("not found");
|
|
2752
|
+
}
|
|
2753
|
+
function codexThreadNotFoundMessage(threadId) {
|
|
2754
|
+
return `Codex could not find thread ${threadId} in the selected account home. The local chat points at a Codex thread that is missing from that account's history; switch back to the account that owns it or refresh/sync the chat history, then try again.`;
|
|
2755
|
+
}
|
|
2756
|
+
function codexReviewTargetPayload(request) {
|
|
2757
|
+
const target = request?.target;
|
|
2758
|
+
const instructions = request?.instructions?.trim();
|
|
2759
|
+
if (target === "custom" || instructions) return {
|
|
2760
|
+
type: "custom",
|
|
2761
|
+
instructions: instructions || "Review the current changes."
|
|
2762
|
+
};
|
|
2763
|
+
const commitSha = request?.commitSha?.trim();
|
|
2764
|
+
if (target === "commit" || commitSha) return {
|
|
2765
|
+
type: "commit",
|
|
2766
|
+
sha: commitSha || "HEAD",
|
|
2767
|
+
title: request?.commitTitle?.trim() || null
|
|
2768
|
+
};
|
|
2769
|
+
const branch = request?.baseBranch?.trim();
|
|
2770
|
+
if (target === "baseBranch" || branch) return {
|
|
2771
|
+
type: "baseBranch",
|
|
2772
|
+
branch: branch || "main"
|
|
2773
|
+
};
|
|
2774
|
+
return { type: "uncommittedChanges" };
|
|
2775
|
+
}
|
|
2776
|
+
function codexAccessMode(permissionMode) {
|
|
2777
|
+
if (permissionMode === "fullAccess") return {
|
|
2778
|
+
approvalPolicy: "never",
|
|
2779
|
+
sandboxPolicy: { type: "dangerFullAccess" }
|
|
2780
|
+
};
|
|
2781
|
+
if (permissionMode === "askForApproval") return {
|
|
2782
|
+
approvalPolicy: "on-request",
|
|
2783
|
+
sandboxPolicy: null
|
|
2784
|
+
};
|
|
2785
|
+
return {
|
|
2786
|
+
approvalPolicy: null,
|
|
2787
|
+
sandboxPolicy: null
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
function codexInputItems(content, attachments) {
|
|
2791
|
+
const attachmentText = codexAttachmentSummaryText(attachments);
|
|
2792
|
+
const items = [{
|
|
2793
|
+
type: "text",
|
|
2794
|
+
text: [content.trim(), attachmentText].filter(Boolean).join("\n\n") || "Attached context",
|
|
2795
|
+
text_elements: []
|
|
2796
|
+
}];
|
|
2797
|
+
for (const attachment of attachments) {
|
|
2798
|
+
if (attachment.kind !== "image") continue;
|
|
2799
|
+
if (attachment.dataUrl) items.push({
|
|
2800
|
+
type: "image",
|
|
2801
|
+
url: attachment.dataUrl
|
|
2802
|
+
});
|
|
2803
|
+
else if (attachment.path) items.push({
|
|
2804
|
+
type: "localImage",
|
|
2805
|
+
path: attachment.path
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
return items;
|
|
2809
|
+
}
|
|
2810
|
+
function codexAttachmentSummaryText(attachments) {
|
|
2811
|
+
const lines = attachments.filter((attachment) => attachment.kind !== "image" || !attachment.dataUrl).map((attachment) => {
|
|
2812
|
+
const path = attachment.path && attachment.path !== attachment.name ? ` (${attachment.path})` : "";
|
|
2813
|
+
return `- ${attachment.kind}: ${attachment.name}${path}`;
|
|
2814
|
+
});
|
|
2815
|
+
return lines.length ? `Attached context:\n${lines.join("\n")}` : null;
|
|
2816
|
+
}
|
|
2817
|
+
async function resolveCollaborationSettings(runtime, model, reasoningEffort, serviceTier) {
|
|
2818
|
+
const normalizedReasoningEffort = normalizeCodexReasoningEffort(reasoningEffort);
|
|
2819
|
+
if (model) return {
|
|
2820
|
+
model,
|
|
2821
|
+
reasoningEffort: normalizedReasoningEffort,
|
|
2822
|
+
serviceTier
|
|
2823
|
+
};
|
|
2824
|
+
try {
|
|
2825
|
+
const configuredModel = readString(asJsonObject(asJsonObject((await runtime.request("config/read", {}, 3e4)).result)?.config)?.model);
|
|
2826
|
+
if (configuredModel) return {
|
|
2827
|
+
model: configuredModel,
|
|
2828
|
+
reasoningEffort: normalizedReasoningEffort,
|
|
2829
|
+
serviceTier
|
|
2830
|
+
};
|
|
2831
|
+
} catch {
|
|
2832
|
+
return null;
|
|
2833
|
+
}
|
|
2834
|
+
return null;
|
|
2835
|
+
}
|
|
2836
|
+
function collaborationModePayload(collaborationMode, settings) {
|
|
2837
|
+
const reasoningEffort = normalizeCodexReasoningEffort(settings?.reasoningEffort);
|
|
2838
|
+
return {
|
|
2839
|
+
mode: collaborationMode || "default",
|
|
2840
|
+
...settings?.model ? { settings: {
|
|
2841
|
+
model: settings.model,
|
|
2842
|
+
reasoning_effort: reasoningEffort,
|
|
2843
|
+
service_tier: settings.serviceTier ?? null
|
|
2844
|
+
} } : {}
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
function normalizeCodexReasoningEffort(value) {
|
|
2848
|
+
const normalized = value?.trim();
|
|
2849
|
+
if (!normalized) return null;
|
|
2850
|
+
if (normalized === "extraHigh" || normalized === "extra-high" || normalized === "extra_high") return "xhigh";
|
|
2851
|
+
if (normalized === "fast") return "low";
|
|
2852
|
+
if (normalized === "deep") return "high";
|
|
2853
|
+
return normalized;
|
|
2854
|
+
}
|
|
2855
|
+
async function maybeMarkInvalidated(accountId, error) {
|
|
2856
|
+
if (!isAuthInvalidatedError(error)) return;
|
|
2857
|
+
invalidatedAccountIds.add(accountId);
|
|
2858
|
+
runtimeService.stopRuntime(accountId);
|
|
2859
|
+
}
|
|
2860
|
+
function isAuthInvalidatedError(error) {
|
|
2861
|
+
const message = readErrorMessage$1(error).toLowerCase();
|
|
2862
|
+
return message.includes("token_invalidated") || message.includes("authentication token has been invalidated") || message.includes("401") && message.includes("unauthorized") && message.includes("invalidated");
|
|
2863
|
+
}
|
|
2864
|
+
function readStringArray(value) {
|
|
2865
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
|
|
2866
|
+
}
|
|
2867
|
+
function readStringList(value) {
|
|
2868
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : null;
|
|
2869
|
+
}
|
|
2870
|
+
function readBooleanValue(value) {
|
|
2871
|
+
return typeof value === "boolean" ? value : null;
|
|
2872
|
+
}
|
|
2873
|
+
function readStringRecord(value) {
|
|
2874
|
+
const object = asJsonObject(value);
|
|
2875
|
+
if (!object) return {};
|
|
2876
|
+
return Object.fromEntries(Object.entries(object).filter((entry) => typeof entry[1] === "string"));
|
|
2877
|
+
}
|
|
2878
|
+
function jsonFromUnknown(value) {
|
|
2879
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
2880
|
+
}
|
|
2881
|
+
function resolveCodexBin() {
|
|
2882
|
+
try {
|
|
2883
|
+
return require.resolve("@openai/codex/bin/codex.js");
|
|
2884
|
+
} catch {
|
|
2885
|
+
return "codex";
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
function readErrorMessage$1(error) {
|
|
2889
|
+
return error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
2890
|
+
}
|
|
2891
|
+
var CodexRuntime = class {
|
|
2892
|
+
constructor(config) {
|
|
2893
|
+
this.config = config;
|
|
2894
|
+
this.stdoutBuffer = "";
|
|
2895
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
2896
|
+
this.eventHandlers = /* @__PURE__ */ new Set();
|
|
2897
|
+
}
|
|
2898
|
+
request(method, params, timeoutMs = 3e4) {
|
|
2899
|
+
return this.ensureStarted().then(() => this.requestStarted(method, params, timeoutMs));
|
|
2900
|
+
}
|
|
2901
|
+
respondToServerRequest(requestId, result) {
|
|
2902
|
+
return this.ensureStarted().then(() => {
|
|
2903
|
+
this.child?.stdin.write(`${JSON.stringify({
|
|
2904
|
+
id: requestId,
|
|
2905
|
+
result
|
|
2906
|
+
})}\n`);
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
waitForEvent(predicate, timeoutMs = 12e4, signal) {
|
|
2910
|
+
return new Promise((resolve, reject) => {
|
|
2911
|
+
if (signal?.aborted) {
|
|
2912
|
+
reject(/* @__PURE__ */ new Error("Codex event wait cancelled."));
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
let unsubscribe = () => void 0;
|
|
2916
|
+
const cleanup = () => {
|
|
2917
|
+
clearTimeout(timeout);
|
|
2918
|
+
signal?.removeEventListener("abort", abort);
|
|
2919
|
+
unsubscribe();
|
|
2920
|
+
};
|
|
2921
|
+
const abort = () => {
|
|
2922
|
+
cleanup();
|
|
2923
|
+
reject(/* @__PURE__ */ new Error("Codex event wait cancelled."));
|
|
2924
|
+
};
|
|
2925
|
+
const timeout = setTimeout(() => {
|
|
2926
|
+
cleanup();
|
|
2927
|
+
reject(/* @__PURE__ */ new Error("Timed out waiting for Codex event."));
|
|
2928
|
+
}, timeoutMs);
|
|
2929
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2930
|
+
unsubscribe = this.onEvent((message) => {
|
|
2931
|
+
if (!predicate(message)) return;
|
|
2932
|
+
cleanup();
|
|
2933
|
+
resolve(message);
|
|
2934
|
+
});
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
onEvent(handler) {
|
|
2938
|
+
this.eventHandlers.add(handler);
|
|
2939
|
+
return () => this.eventHandlers.delete(handler);
|
|
2940
|
+
}
|
|
2941
|
+
shutdown() {
|
|
2942
|
+
for (const waiter of this.pending.values()) {
|
|
2943
|
+
clearTimeout(waiter.timeout);
|
|
2944
|
+
waiter.reject(/* @__PURE__ */ new Error("Codex runtime stopped."));
|
|
2945
|
+
}
|
|
2946
|
+
this.pending.clear();
|
|
2947
|
+
this.child?.kill("SIGTERM");
|
|
2948
|
+
this.child = void 0;
|
|
2949
|
+
this.initializePromise = void 0;
|
|
2950
|
+
}
|
|
2951
|
+
async ensureStarted() {
|
|
2952
|
+
if (this.initializePromise) return this.initializePromise;
|
|
2953
|
+
ensureCodexHome(this.config.codexHome);
|
|
2954
|
+
this.child = spawn(this.config.command, this.config.args, {
|
|
2955
|
+
cwd: this.config.workingDirectory ?? void 0,
|
|
2956
|
+
env: {
|
|
2957
|
+
...process.env,
|
|
2958
|
+
...this.config.environment,
|
|
2959
|
+
CODEX_HOME: this.config.codexHome
|
|
2960
|
+
},
|
|
2961
|
+
stdio: [
|
|
2962
|
+
"pipe",
|
|
2963
|
+
"pipe",
|
|
2964
|
+
"pipe"
|
|
2965
|
+
]
|
|
2966
|
+
});
|
|
2967
|
+
this.child.stdout.on("data", (chunk) => this.handleStdout(chunk));
|
|
2968
|
+
this.child.stderr.on("data", (chunk) => {
|
|
2969
|
+
const message = chunk.toString("utf8").trim();
|
|
2970
|
+
if (message && isAuthInvalidatedError(message)) invalidatedAccountIds.add(this.config.accountId);
|
|
2971
|
+
});
|
|
2972
|
+
this.child.on("error", (error) => this.failAll(error));
|
|
2973
|
+
this.child.on("close", (code) => {
|
|
2974
|
+
this.failAll(/* @__PURE__ */ new Error(`Codex runtime exited with code ${code ?? "unknown"}.`));
|
|
2975
|
+
this.child = void 0;
|
|
2976
|
+
this.initializePromise = void 0;
|
|
2977
|
+
});
|
|
2978
|
+
this.initializePromise = this.initialize();
|
|
2979
|
+
return this.initializePromise;
|
|
2980
|
+
}
|
|
2981
|
+
async initialize() {
|
|
2982
|
+
await this.requestStarted("initialize", {
|
|
2983
|
+
clientInfo: {
|
|
2984
|
+
name: "pockcode",
|
|
2985
|
+
title: "pockcode",
|
|
2986
|
+
version: "0.1.0"
|
|
2987
|
+
},
|
|
2988
|
+
capabilities: { experimentalApi: true }
|
|
2989
|
+
});
|
|
2990
|
+
this.child?.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
|
|
2991
|
+
}
|
|
2992
|
+
requestStarted(method, params, timeoutMs = 3e4) {
|
|
2993
|
+
const id = `pockcode-${randomUUID()}`;
|
|
2994
|
+
const payload = JSON.stringify(params === void 0 ? {
|
|
2995
|
+
id,
|
|
2996
|
+
method
|
|
2997
|
+
} : {
|
|
2998
|
+
id,
|
|
2999
|
+
method,
|
|
3000
|
+
params
|
|
3001
|
+
});
|
|
3002
|
+
return new Promise((resolve, reject) => {
|
|
3003
|
+
const timeout = setTimeout(() => {
|
|
3004
|
+
this.pending.delete(id);
|
|
3005
|
+
reject(/* @__PURE__ */ new Error(`Codex request timed out: ${method}`));
|
|
3006
|
+
}, timeoutMs);
|
|
3007
|
+
this.pending.set(id, {
|
|
3008
|
+
method,
|
|
3009
|
+
reject,
|
|
3010
|
+
resolve,
|
|
3011
|
+
startedAt: Date.now(),
|
|
3012
|
+
timeout
|
|
3013
|
+
});
|
|
3014
|
+
this.child?.stdin.write(`${payload}\n`);
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
handleStdout(chunk) {
|
|
3018
|
+
this.stdoutBuffer += chunk.toString("utf8");
|
|
3019
|
+
const lines = this.stdoutBuffer.split("\n");
|
|
3020
|
+
this.stdoutBuffer = lines.pop() ?? "";
|
|
3021
|
+
for (const line of lines) {
|
|
3022
|
+
const trimmed = line.trim();
|
|
3023
|
+
if (trimmed) this.handleLine(trimmed);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
handleLine(line) {
|
|
3027
|
+
let message;
|
|
3028
|
+
try {
|
|
3029
|
+
message = JSON.parse(line);
|
|
3030
|
+
} catch {
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
if (message.id === void 0 || message.id === null) {
|
|
3034
|
+
for (const handler of this.eventHandlers) handler(message);
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
const waiter = this.pending.get(String(message.id));
|
|
3038
|
+
if (!waiter) {
|
|
3039
|
+
if (message.method) for (const handler of this.eventHandlers) handler(message);
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
clearTimeout(waiter.timeout);
|
|
3043
|
+
this.pending.delete(String(message.id));
|
|
3044
|
+
if (message.error) {
|
|
3045
|
+
waiter.reject(new Error(message.error.message ?? "Codex request failed."));
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
waiter.resolve(message);
|
|
3049
|
+
}
|
|
3050
|
+
failAll(error) {
|
|
3051
|
+
for (const waiter of this.pending.values()) {
|
|
3052
|
+
clearTimeout(waiter.timeout);
|
|
3053
|
+
waiter.reject(error);
|
|
3054
|
+
}
|
|
3055
|
+
this.pending.clear();
|
|
3056
|
+
}
|
|
3057
|
+
};
|
|
3058
|
+
var CodexRuntimeService = class {
|
|
3059
|
+
constructor() {
|
|
3060
|
+
this.runtimes = /* @__PURE__ */ new Map();
|
|
3061
|
+
}
|
|
3062
|
+
getRuntime(config) {
|
|
3063
|
+
const existing = this.runtimes.get(config.accountId);
|
|
3064
|
+
if (existing) return existing;
|
|
3065
|
+
const runtime = new CodexRuntime(config);
|
|
3066
|
+
this.runtimes.set(config.accountId, runtime);
|
|
3067
|
+
return runtime;
|
|
3068
|
+
}
|
|
3069
|
+
stopRuntime(accountId) {
|
|
3070
|
+
this.runtimes.get(accountId)?.shutdown();
|
|
3071
|
+
this.runtimes.delete(accountId);
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
var runtimeService = new CodexRuntimeService();
|
|
3075
|
+
//#endregion
|
|
3076
|
+
//#region app/server/providers/registry.server.ts
|
|
3077
|
+
var adapters = new Map([[codexProviderAdapter.definition.id, codexProviderAdapter]]);
|
|
3078
|
+
function listProviderAdapters() {
|
|
3079
|
+
return [...adapters.values()];
|
|
3080
|
+
}
|
|
3081
|
+
function getProviderAdapter(providerId) {
|
|
3082
|
+
const adapter = adapters.get(providerId);
|
|
3083
|
+
if (!adapter) throw new Error(`Unknown provider: ${providerId}`);
|
|
3084
|
+
return adapter;
|
|
3085
|
+
}
|
|
3086
|
+
//#endregion
|
|
3087
|
+
//#region app/server/accounts.service.ts
|
|
3088
|
+
async function listAccounts() {
|
|
3089
|
+
await ensureDatabase();
|
|
3090
|
+
const accounts = await prisma.providerAccount.findMany({ orderBy: { createdAt: "asc" } });
|
|
3091
|
+
return (await Promise.all(accounts.map(refreshAccountConnection))).map(serializeAccount);
|
|
3092
|
+
}
|
|
3093
|
+
async function createAccount(dto) {
|
|
3094
|
+
await ensureDatabase();
|
|
3095
|
+
const adapter = getProviderAdapter(dto.providerId);
|
|
3096
|
+
const account = await prisma.providerAccount.create({ data: {
|
|
3097
|
+
providerId: dto.providerId,
|
|
3098
|
+
displayName: normalizedDisplayName(dto.displayName, adapter.definition.label),
|
|
3099
|
+
settings: {
|
|
3100
|
+
...adapter.defaultAccountSettings(),
|
|
3101
|
+
...dto.settings ?? {}
|
|
3102
|
+
},
|
|
3103
|
+
runtimeDefaults: {
|
|
3104
|
+
...adapter.defaultRuntimeDefaults(),
|
|
3105
|
+
...dto.runtimeDefaults ?? {}
|
|
3106
|
+
}
|
|
3107
|
+
} });
|
|
3108
|
+
await adapter.prepareAccount(account);
|
|
3109
|
+
return serializeAccount(account);
|
|
3110
|
+
}
|
|
3111
|
+
async function getAccount(accountId) {
|
|
3112
|
+
await ensureDatabase();
|
|
3113
|
+
const account = await prisma.providerAccount.findUnique({ where: { id: accountId } });
|
|
3114
|
+
if (!account) throw new HttpError(404, "Provider account not found.");
|
|
3115
|
+
return refreshAccountConnection(account);
|
|
3116
|
+
}
|
|
3117
|
+
async function updateAccount(accountId, dto) {
|
|
3118
|
+
const account = await getAccount(accountId);
|
|
3119
|
+
const adapter = getProviderAdapter(account.providerId);
|
|
3120
|
+
adapter.stopAccountRuntime(account.id);
|
|
3121
|
+
const updated = await prisma.providerAccount.update({
|
|
3122
|
+
where: { id: accountId },
|
|
3123
|
+
data: {
|
|
3124
|
+
displayName: dto.displayName,
|
|
3125
|
+
settings: dto.settings === void 0 ? void 0 : dto.settings,
|
|
3126
|
+
runtimeDefaults: dto.runtimeDefaults === void 0 ? void 0 : dto.runtimeDefaults
|
|
3127
|
+
}
|
|
3128
|
+
});
|
|
3129
|
+
await adapter.prepareAccount(updated);
|
|
3130
|
+
return serializeAccount(updated);
|
|
3131
|
+
}
|
|
3132
|
+
async function deleteAccount(accountId) {
|
|
3133
|
+
const account = await getAccount(accountId);
|
|
3134
|
+
getProviderAdapter(account.providerId).stopAccountRuntime(account.id);
|
|
3135
|
+
await prisma.chat.updateMany({
|
|
3136
|
+
where: { accountId },
|
|
3137
|
+
data: { accountId: null }
|
|
3138
|
+
});
|
|
3139
|
+
await prisma.chatRun.updateMany({
|
|
3140
|
+
where: { accountId },
|
|
3141
|
+
data: { accountId: null }
|
|
3142
|
+
});
|
|
3143
|
+
await prisma.mcpServerInstallation.deleteMany({ where: { accountId } });
|
|
3144
|
+
return serializeAccount(await prisma.providerAccount.delete({ where: { id: accountId } }));
|
|
3145
|
+
}
|
|
3146
|
+
async function authenticateAccount(accountId, mode = "browser") {
|
|
3147
|
+
const account = await getAccount(accountId);
|
|
3148
|
+
const adapter = getProviderAdapter(account.providerId);
|
|
3149
|
+
await prisma.providerAccount.update({
|
|
3150
|
+
where: { id: accountId },
|
|
3151
|
+
data: {
|
|
3152
|
+
status: "AUTHENTICATING",
|
|
3153
|
+
lastAuthUrl: null,
|
|
3154
|
+
lastAuthMode: mode,
|
|
3155
|
+
lastAuthLoginId: null,
|
|
3156
|
+
lastAuthUserCode: null,
|
|
3157
|
+
lastError: null
|
|
3158
|
+
}
|
|
3159
|
+
});
|
|
3160
|
+
const response = await adapter.authenticate(account, mode);
|
|
3161
|
+
await prisma.providerAccount.update({
|
|
3162
|
+
where: { id: accountId },
|
|
3163
|
+
data: await accountAuthData(account, adapter, response)
|
|
3164
|
+
});
|
|
3165
|
+
return response;
|
|
3166
|
+
}
|
|
3167
|
+
async function listAccountModels(accountId) {
|
|
3168
|
+
const account = await requireConnectedAccount(accountId);
|
|
3169
|
+
return getProviderAdapter(account.providerId).listModels(account);
|
|
3170
|
+
}
|
|
3171
|
+
async function readConnectedAccountLimits() {
|
|
3172
|
+
await ensureDatabase();
|
|
3173
|
+
const accounts = await prisma.providerAccount.findMany({
|
|
3174
|
+
orderBy: { createdAt: "asc" },
|
|
3175
|
+
where: { status: "CONNECTED" }
|
|
3176
|
+
});
|
|
3177
|
+
const data = {};
|
|
3178
|
+
const errors = {};
|
|
3179
|
+
for (const account of accounts) try {
|
|
3180
|
+
data[account.id] = await getProviderAdapter(account.providerId).readLimits(account);
|
|
3181
|
+
} catch (error) {
|
|
3182
|
+
errors[account.id] = error instanceof Error ? error.message : "Unable to load limits.";
|
|
3183
|
+
}
|
|
3184
|
+
return {
|
|
3185
|
+
data,
|
|
3186
|
+
...Object.keys(errors).length ? { errors } : {}
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
async function requireConnectedAccount(accountId) {
|
|
3190
|
+
const account = await getAccount(accountId);
|
|
3191
|
+
if (account.status !== "CONNECTED") throw new HttpError(400, "Authenticate the provider account before using it.");
|
|
3192
|
+
return account;
|
|
3193
|
+
}
|
|
3194
|
+
function serializeAccount(account) {
|
|
3195
|
+
return {
|
|
3196
|
+
id: account.id,
|
|
3197
|
+
providerId: account.providerId,
|
|
3198
|
+
displayName: account.displayName,
|
|
3199
|
+
status: account.status,
|
|
3200
|
+
settings: account.settings,
|
|
3201
|
+
runtimeDefaults: account.runtimeDefaults,
|
|
3202
|
+
authState: account.authState ?? null,
|
|
3203
|
+
lastAuthUrl: account.lastAuthUrl,
|
|
3204
|
+
lastAuthMode: account.lastAuthMode === "browser" || account.lastAuthMode === "device" || account.lastAuthMode === "local" ? account.lastAuthMode : null,
|
|
3205
|
+
lastAuthLoginId: account.lastAuthLoginId,
|
|
3206
|
+
lastAuthUserCode: account.lastAuthUserCode,
|
|
3207
|
+
lastError: account.lastError,
|
|
3208
|
+
createdAt: account.createdAt.toISOString(),
|
|
3209
|
+
updatedAt: account.updatedAt.toISOString()
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
async function refreshAccountConnection(account) {
|
|
3213
|
+
const adapter = getProviderAdapter(account.providerId);
|
|
3214
|
+
await adapter.prepareAccount(account);
|
|
3215
|
+
if (!adapter.isAccountConnected || !await adapter.isAccountConnected(account)) return account;
|
|
3216
|
+
if (account.status === "CONNECTED" && !usesDefaultDisplayName(account.displayName, adapter.definition.label)) return account;
|
|
3217
|
+
return prisma.providerAccount.update({
|
|
3218
|
+
where: { id: account.id },
|
|
3219
|
+
data: {
|
|
3220
|
+
displayName: await connectedDisplayName(account, adapter),
|
|
3221
|
+
status: "CONNECTED",
|
|
3222
|
+
lastAuthUrl: null,
|
|
3223
|
+
lastAuthMode: null,
|
|
3224
|
+
lastAuthLoginId: null,
|
|
3225
|
+
lastAuthUserCode: null,
|
|
3226
|
+
lastError: null
|
|
3227
|
+
}
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
async function accountAuthData(account, adapter, response) {
|
|
3231
|
+
const accountForResponse = response.authState === void 0 ? account : {
|
|
3232
|
+
...account,
|
|
3233
|
+
authState: response.authState
|
|
3234
|
+
};
|
|
3235
|
+
return {
|
|
3236
|
+
...response.status === "CONNECTED" ? { displayName: await connectedDisplayName(accountForResponse, adapter) } : {},
|
|
3237
|
+
...response.authState === void 0 ? {} : { authState: response.authState },
|
|
3238
|
+
status: response.status,
|
|
3239
|
+
lastAuthUrl: response.authUrl,
|
|
3240
|
+
lastAuthMode: response.authMode,
|
|
3241
|
+
lastAuthLoginId: response.loginId,
|
|
3242
|
+
lastAuthUserCode: response.userCode,
|
|
3243
|
+
lastError: response.status === "ERROR" ? response.message ?? "Authentication failed." : null
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
async function connectedDisplayName(account, adapter) {
|
|
3247
|
+
if (!usesDefaultDisplayName(account.displayName, adapter.definition.label)) return;
|
|
3248
|
+
return await adapter.readAccountAlias?.(account) ?? void 0;
|
|
3249
|
+
}
|
|
3250
|
+
function usesDefaultDisplayName(displayName, providerLabel) {
|
|
3251
|
+
return displayName.trim() === normalizedDisplayName(void 0, providerLabel);
|
|
3252
|
+
}
|
|
3253
|
+
function normalizedDisplayName(displayName, providerLabel) {
|
|
3254
|
+
return displayName?.trim() || `${providerLabel} account`;
|
|
3255
|
+
}
|
|
3256
|
+
//#endregion
|
|
3257
|
+
//#region app/server/chats.service.ts
|
|
3258
|
+
var providerChatsSyncPromise = null;
|
|
3259
|
+
var messageSnapshotsByChatId = /* @__PURE__ */ new Map();
|
|
3260
|
+
var messageSnapshotSeparator = "\0";
|
|
3261
|
+
var localRunGraceMs = 12e4;
|
|
3262
|
+
var recentInterruptSuppressMs = 1800 * 1e3;
|
|
3263
|
+
async function createChat(dto) {
|
|
3264
|
+
await ensureDatabase();
|
|
3265
|
+
const account = await requireConnectedAccount(dto.accountId);
|
|
3266
|
+
const providerId = dto.providerId ?? account.providerId;
|
|
3267
|
+
if (providerId !== account.providerId) throw new HttpError(400, "Chat provider must match the selected account provider.");
|
|
3268
|
+
const chat = await prisma.chat.create({ data: {
|
|
3269
|
+
providerId,
|
|
3270
|
+
accountId: account.id,
|
|
3271
|
+
autoRotateAccount: dto.autoRotateAccount ?? false,
|
|
3272
|
+
title: normalizeTitle(dto.title) ?? "New chat",
|
|
3273
|
+
workingDirectory: dto.workingDirectory,
|
|
3274
|
+
model: nullableString(dto.model),
|
|
3275
|
+
reasoningEffort: nullableString(dto.reasoningEffort),
|
|
3276
|
+
serviceTier: nullableString(dto.serviceTier),
|
|
3277
|
+
collaborationMode: nullableString(dto.collaborationMode) ?? "default",
|
|
3278
|
+
permissionMode: nullableString(dto.permissionMode) ?? "default"
|
|
3279
|
+
} });
|
|
3280
|
+
publishProviderEvent({
|
|
3281
|
+
threadId: chat.id,
|
|
3282
|
+
type: "chat.updated",
|
|
3283
|
+
payload: serializeChat(chat)
|
|
3284
|
+
});
|
|
3285
|
+
return serializeChat(chat);
|
|
3286
|
+
}
|
|
3287
|
+
async function listChats(workingDirectory) {
|
|
3288
|
+
await ensureDatabase();
|
|
3289
|
+
return (await overlayCachedChatStates(dedupeChatList(await prisma.chat.findMany({
|
|
3290
|
+
orderBy: [{ lastActivityAt: "desc" }, { updatedAt: "desc" }],
|
|
3291
|
+
where: {
|
|
3292
|
+
status: { not: "ARCHIVED" },
|
|
3293
|
+
...workingDirectory?.trim() ? { workingDirectory: workingDirectory.trim() } : {}
|
|
3294
|
+
}
|
|
3295
|
+
})))).map((chat) => serializeChat(chat));
|
|
3296
|
+
}
|
|
3297
|
+
async function getChat(chatId) {
|
|
3298
|
+
await ensureDatabase();
|
|
3299
|
+
const chat = await prisma.chat.findUnique({ where: { id: chatId } });
|
|
3300
|
+
if (!chat) throw new HttpError(404, "Chat not found.");
|
|
3301
|
+
return chat;
|
|
3302
|
+
}
|
|
3303
|
+
async function updateChat(chatId, dto) {
|
|
3304
|
+
const chat = await getChat(chatId);
|
|
3305
|
+
if (chat.status === "RUNNING" && hasBlockedRunningChatUpdate(dto)) throw new HttpError(400, "Wait for the current run to finish before changing chat runtime settings.");
|
|
3306
|
+
const targetAccount = dto.accountId === void 0 || dto.accountId === chat.accountId ? null : dto.accountId === null ? null : await requireConnectedAccount(dto.accountId);
|
|
3307
|
+
if (targetAccount && targetAccount.providerId !== chat.providerId) throw new HttpError(400, "Switching provider types for an existing chat is not supported.");
|
|
3308
|
+
if (targetAccount && targetAccount.id !== chat.accountId && chat.externalThreadId) {
|
|
3309
|
+
publishAccountSwitchProgress(chat.id, chat.accountId, targetAccount.id, "preparing");
|
|
3310
|
+
try {
|
|
3311
|
+
const fromAdapter = getProviderAdapter(chat.providerId);
|
|
3312
|
+
const toAdapter = getProviderAdapter(targetAccount.providerId);
|
|
3313
|
+
const fromAccount = chat.accountId ? await requireConnectedAccount(chat.accountId).catch(() => null) : null;
|
|
3314
|
+
const threadId = chat.externalThreadId;
|
|
3315
|
+
const context = {
|
|
3316
|
+
threadId,
|
|
3317
|
+
fromAccount,
|
|
3318
|
+
toAccount: targetAccount
|
|
3319
|
+
};
|
|
3320
|
+
publishAccountSwitchProgress(chat.id, chat.accountId, targetAccount.id, "syncingSource");
|
|
3321
|
+
if (!(fromAccount ? await fromAdapter.moveThreadToAccount?.(context) ?? false : false)) {
|
|
3322
|
+
await fromAdapter.beforeAccountSwitch?.(context);
|
|
3323
|
+
const synced = fromAccount ? await fromAdapter.syncThreadFromAccount(threadId, fromAccount) : true;
|
|
3324
|
+
publishAccountSwitchProgress(chat.id, chat.accountId, targetAccount.id, "hydratingTarget");
|
|
3325
|
+
const hydrated = await toAdapter.hydrateThreadForAccount(threadId, targetAccount);
|
|
3326
|
+
if (!synced || !hydrated) throw new HttpError(500, "Unable to move chat history to the selected provider account.");
|
|
3327
|
+
await fromAdapter.afterAccountSwitch?.(context);
|
|
3328
|
+
if (fromAdapter !== toAdapter) await toAdapter.afterAccountSwitch?.(context);
|
|
3329
|
+
}
|
|
3330
|
+
} catch (error) {
|
|
3331
|
+
publishAccountSwitchProgress(chat.id, chat.accountId, targetAccount.id, "failed", readErrorMessage(error));
|
|
3332
|
+
throw error;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
if (dto.title !== void 0 && chat.externalThreadId) {
|
|
3336
|
+
const renameAccount = targetAccount ?? (chat.accountId ? await requireConnectedAccount(chat.accountId).catch(() => null) : null);
|
|
3337
|
+
const nextTitle = normalizeTitle(dto.title) ?? "Untitled chat";
|
|
3338
|
+
if (renameAccount) await getProviderAdapter(renameAccount.providerId).renameThread?.(renameAccount, chat.externalThreadId, nextTitle, chat.workingDirectory).catch(() => false);
|
|
3339
|
+
}
|
|
3340
|
+
const updated = await prisma.chat.update({
|
|
3341
|
+
where: { id: chatId },
|
|
3342
|
+
data: {
|
|
3343
|
+
accountId: dto.accountId === void 0 ? void 0 : dto.accountId,
|
|
3344
|
+
autoRotateAccount: dto.autoRotateAccount,
|
|
3345
|
+
providerId: targetAccount ? targetAccount.providerId : void 0,
|
|
3346
|
+
title: dto.title === void 0 ? void 0 : normalizeTitle(dto.title) ?? "Untitled chat",
|
|
3347
|
+
workingDirectory: dto.workingDirectory,
|
|
3348
|
+
model: dto.model === void 0 ? void 0 : nullableString(dto.model),
|
|
3349
|
+
reasoningEffort: dto.reasoningEffort === void 0 ? void 0 : nullableString(dto.reasoningEffort),
|
|
3350
|
+
serviceTier: dto.serviceTier === void 0 ? void 0 : nullableString(dto.serviceTier),
|
|
3351
|
+
collaborationMode: dto.collaborationMode === void 0 ? void 0 : nullableString(dto.collaborationMode) ?? "default",
|
|
3352
|
+
permissionMode: dto.permissionMode === void 0 ? void 0 : nullableString(dto.permissionMode) ?? "default"
|
|
3353
|
+
}
|
|
3354
|
+
});
|
|
3355
|
+
if (targetAccount && updated.externalThreadId) await prisma.chat.updateMany({
|
|
3356
|
+
where: {
|
|
3357
|
+
externalThreadId: updated.externalThreadId,
|
|
3358
|
+
id: { not: updated.id },
|
|
3359
|
+
status: { not: "ARCHIVED" }
|
|
3360
|
+
},
|
|
3361
|
+
data: { status: "ARCHIVED" }
|
|
3362
|
+
});
|
|
3363
|
+
const response = serializeChat(updated);
|
|
3364
|
+
publishProviderEvent({
|
|
3365
|
+
threadId: updated.id,
|
|
3366
|
+
type: "chat.updated",
|
|
3367
|
+
payload: response
|
|
3368
|
+
});
|
|
3369
|
+
if (targetAccount && updated.externalThreadId) {
|
|
3370
|
+
publishAccountSwitchProgress(updated.id, chat.accountId, targetAccount.id, "refreshingMessages");
|
|
3371
|
+
await publishMessageDeltas(updated.id);
|
|
3372
|
+
publishAccountSwitchProgress(updated.id, chat.accountId, targetAccount.id, "completed");
|
|
3373
|
+
}
|
|
3374
|
+
return response;
|
|
3375
|
+
}
|
|
3376
|
+
function hasBlockedRunningChatUpdate(dto) {
|
|
3377
|
+
return dto.accountId !== void 0 || dto.autoRotateAccount !== void 0 || dto.collaborationMode !== void 0 || dto.model !== void 0 || dto.reasoningEffort !== void 0 || dto.serviceTier !== void 0 || dto.title !== void 0 || dto.workingDirectory !== void 0;
|
|
3378
|
+
}
|
|
3379
|
+
async function archiveChat(chatId) {
|
|
3380
|
+
const chat = await getChat(chatId);
|
|
3381
|
+
if (chat.accountId && chat.externalThreadId) {
|
|
3382
|
+
const account = await requireConnectedAccount(chat.accountId).catch(() => null);
|
|
3383
|
+
if (account) await getProviderAdapter(chat.providerId).archiveThread?.(account, chat.externalThreadId, chat.workingDirectory).catch(() => false);
|
|
3384
|
+
}
|
|
3385
|
+
const archived = await prisma.chat.update({
|
|
3386
|
+
where: { id: chatId },
|
|
3387
|
+
data: { status: "ARCHIVED" }
|
|
3388
|
+
});
|
|
3389
|
+
publishProviderEvent({
|
|
3390
|
+
threadId: archived.id,
|
|
3391
|
+
type: "chat.updated",
|
|
3392
|
+
payload: serializeChat(archived)
|
|
3393
|
+
});
|
|
3394
|
+
return serializeChat(archived);
|
|
3395
|
+
}
|
|
3396
|
+
async function forkChat(chatId, request = {}) {
|
|
3397
|
+
const { account, adapter, chat, externalThreadId } = await requireProviderBackedChat(chatId, "fork");
|
|
3398
|
+
if (!adapter.forkThread) throw new HttpError(400, "This provider does not support forking chats.");
|
|
3399
|
+
const forked = await adapter.forkThread(account, externalThreadId, request, chat.workingDirectory);
|
|
3400
|
+
const created = await prisma.chat.create({ data: {
|
|
3401
|
+
accountId: account.id,
|
|
3402
|
+
autoRotateAccount: chat.autoRotateAccount,
|
|
3403
|
+
collaborationMode: chat.collaborationMode,
|
|
3404
|
+
externalThreadId: forked.externalThreadId,
|
|
3405
|
+
lastActivityAt: /* @__PURE__ */ new Date(),
|
|
3406
|
+
model: chat.model,
|
|
3407
|
+
permissionMode: chat.permissionMode,
|
|
3408
|
+
providerId: chat.providerId,
|
|
3409
|
+
reasoningEffort: chat.reasoningEffort,
|
|
3410
|
+
serviceTier: chat.serviceTier,
|
|
3411
|
+
status: "IDLE",
|
|
3412
|
+
title: normalizeTitle(forked.title) ?? `${chat.title} fork`,
|
|
3413
|
+
workingDirectory: forked.workingDirectory ?? chat.workingDirectory
|
|
3414
|
+
} });
|
|
3415
|
+
const response = serializeChat(created);
|
|
3416
|
+
publishProviderEvent({
|
|
3417
|
+
threadId: created.id,
|
|
3418
|
+
type: "chat.updated",
|
|
3419
|
+
payload: response
|
|
3420
|
+
});
|
|
3421
|
+
await publishMessageDeltas(created.id);
|
|
3422
|
+
return response;
|
|
3423
|
+
}
|
|
3424
|
+
async function compactChat(chatId, request = {}) {
|
|
3425
|
+
const { account, adapter, chat, externalThreadId } = await requireProviderBackedChat(chatId, "compact");
|
|
3426
|
+
if (!adapter.compactThread) throw new HttpError(400, "This provider does not support compacting chats.");
|
|
3427
|
+
await adapter.compactThread(account, externalThreadId, request, chat.workingDirectory);
|
|
3428
|
+
const updated = await prisma.chat.update({
|
|
3429
|
+
where: { id: chat.id },
|
|
3430
|
+
data: { lastActivityAt: /* @__PURE__ */ new Date() }
|
|
3431
|
+
});
|
|
3432
|
+
const response = serializeChat(updated);
|
|
3433
|
+
publishProviderEvent({
|
|
3434
|
+
threadId: updated.id,
|
|
3435
|
+
type: "chat.updated",
|
|
3436
|
+
payload: response
|
|
3437
|
+
});
|
|
3438
|
+
await publishMessageDeltas(updated.id);
|
|
3439
|
+
return response;
|
|
3440
|
+
}
|
|
3441
|
+
async function reviewChat(chatId, request = {}) {
|
|
3442
|
+
const { account, adapter, chat, externalThreadId } = await requireProviderBackedChat(chatId, "review");
|
|
3443
|
+
if (!adapter.reviewThread) throw new HttpError(400, "This provider does not support review.");
|
|
3444
|
+
await adapter.reviewThread(account, externalThreadId, request, chat.workingDirectory);
|
|
3445
|
+
const updated = await prisma.chat.update({
|
|
3446
|
+
where: { id: chat.id },
|
|
3447
|
+
data: {
|
|
3448
|
+
lastActivityAt: /* @__PURE__ */ new Date(),
|
|
3449
|
+
status: "RUNNING"
|
|
3450
|
+
}
|
|
3451
|
+
});
|
|
3452
|
+
const response = serializeChat(updated);
|
|
3453
|
+
publishProviderEvent({
|
|
3454
|
+
threadId: updated.id,
|
|
3455
|
+
type: "chat.updated",
|
|
3456
|
+
payload: response
|
|
3457
|
+
});
|
|
3458
|
+
await publishMessageDeltas(updated.id);
|
|
3459
|
+
return response;
|
|
3460
|
+
}
|
|
3461
|
+
async function refreshChat(chatId) {
|
|
3462
|
+
const chat = serializeChat(await getChat(chatId));
|
|
3463
|
+
const messages = await readMessagePage(chatId);
|
|
3464
|
+
rememberMessageSnapshot(chatId, messages.data);
|
|
3465
|
+
return {
|
|
3466
|
+
chat,
|
|
3467
|
+
messages
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
async function listMessages(chatId, limit = 1e3) {
|
|
3471
|
+
const page = await readMessagePage(chatId, limit);
|
|
3472
|
+
rememberMessageSnapshot(chatId, page.data);
|
|
3473
|
+
return page;
|
|
3474
|
+
}
|
|
3475
|
+
async function readMessagePage(chatId, limit = 1e3) {
|
|
3476
|
+
const chat = await getChat(chatId);
|
|
3477
|
+
const safeLimit = Math.min(Math.max(limit, 1), 1e3);
|
|
3478
|
+
const providerMessages = await loadProviderMessages(chat);
|
|
3479
|
+
const messages = [...providerMessages, ...await readRunPreviewMessages(chat, providerMessages)].slice(0, safeLimit);
|
|
3480
|
+
return {
|
|
3481
|
+
data: messages.map((message, index) => serializeProviderMessage(chat.id, message, index + 1)),
|
|
3482
|
+
hasMoreBefore: false,
|
|
3483
|
+
nextCursor: messages.length ? messages.length : null,
|
|
3484
|
+
previousCursor: messages.length ? 1 : null
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
async function publishMessageDeltas(chatId) {
|
|
3488
|
+
const page = await readMessagePage(chatId);
|
|
3489
|
+
const previous = messageSnapshotsByChatId.get(chatId) ?? /* @__PURE__ */ new Map();
|
|
3490
|
+
const next = messageSnapshotMap(page.data);
|
|
3491
|
+
const removedIds = [...previous.keys()].filter((messageId) => !next.has(messageId));
|
|
3492
|
+
const changedMessages = page.data.filter((message) => previous.get(message.id) !== next.get(message.id));
|
|
3493
|
+
rememberMessageSnapshot(chatId, page.data);
|
|
3494
|
+
for (const messageId of removedIds) publishProviderEvent({
|
|
3495
|
+
threadId: chatId,
|
|
3496
|
+
type: "message.deleted",
|
|
3497
|
+
payload: {
|
|
3498
|
+
chatId,
|
|
3499
|
+
messageId
|
|
3500
|
+
}
|
|
3501
|
+
});
|
|
3502
|
+
for (const message of changedMessages) publishMessageCreated(chatId, message);
|
|
3503
|
+
}
|
|
3504
|
+
function publishMessageCreated(chatId, message) {
|
|
3505
|
+
rememberPublishedMessage(chatId, message);
|
|
3506
|
+
publishProviderEvent({
|
|
3507
|
+
threadId: chatId,
|
|
3508
|
+
type: "message.created",
|
|
3509
|
+
payload: message
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
function publishAccountSwitchProgress(chatId, fromAccountId, toAccountId, phase, error) {
|
|
3513
|
+
publishProviderEvent({
|
|
3514
|
+
threadId: chatId,
|
|
3515
|
+
type: "chat.accountSwitch",
|
|
3516
|
+
payload: {
|
|
3517
|
+
chatId,
|
|
3518
|
+
fromAccountId,
|
|
3519
|
+
toAccountId,
|
|
3520
|
+
phase,
|
|
3521
|
+
error: error ?? null
|
|
3522
|
+
}
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
function rememberMessageSnapshot(chatId, messages) {
|
|
3526
|
+
messageSnapshotsByChatId.set(chatId, messageSnapshotMap(messages));
|
|
3527
|
+
}
|
|
3528
|
+
function rememberPublishedMessage(chatId, message) {
|
|
3529
|
+
const snapshot = messageSnapshotsByChatId.get(chatId) ?? /* @__PURE__ */ new Map();
|
|
3530
|
+
snapshot.set(message.id, messageSnapshot(message));
|
|
3531
|
+
messageSnapshotsByChatId.set(chatId, snapshot);
|
|
3532
|
+
}
|
|
3533
|
+
function messageSnapshotMap(messages) {
|
|
3534
|
+
return new Map(messages.map((message) => [message.id, messageSnapshot(message)]));
|
|
3535
|
+
}
|
|
3536
|
+
function messageSnapshot(message) {
|
|
3537
|
+
return [
|
|
3538
|
+
message.sequence,
|
|
3539
|
+
message.itemId,
|
|
3540
|
+
message.role,
|
|
3541
|
+
message.kind,
|
|
3542
|
+
message.status,
|
|
3543
|
+
message.createdAt,
|
|
3544
|
+
message.completedAt,
|
|
3545
|
+
message.content
|
|
3546
|
+
].join(messageSnapshotSeparator);
|
|
3547
|
+
}
|
|
3548
|
+
function knownMessageSequence(chatId, messageId) {
|
|
3549
|
+
const snapshot = messageSnapshotsByChatId.get(chatId)?.get(messageId);
|
|
3550
|
+
if (!snapshot) return null;
|
|
3551
|
+
const sequence = Number.parseInt(snapshot.split(messageSnapshotSeparator, 1)[0] ?? "", 10);
|
|
3552
|
+
return Number.isFinite(sequence) && sequence > 0 ? sequence : null;
|
|
3553
|
+
}
|
|
3554
|
+
function nextKnownMessageSequence(chatId) {
|
|
3555
|
+
const snapshot = messageSnapshotsByChatId.get(chatId);
|
|
3556
|
+
if (!snapshot?.size) return 1;
|
|
3557
|
+
let maxSequence = 0;
|
|
3558
|
+
for (const value of snapshot.values()) {
|
|
3559
|
+
const sequence = Number.parseInt(value.split(messageSnapshotSeparator, 1)[0] ?? "", 10);
|
|
3560
|
+
if (Number.isFinite(sequence)) maxSequence = Math.max(maxSequence, sequence);
|
|
3561
|
+
}
|
|
3562
|
+
return maxSequence + 1;
|
|
3563
|
+
}
|
|
3564
|
+
function nextMessageSequenceForChat(chat) {
|
|
3565
|
+
if (messageSnapshotsByChatId.get(chat.id)?.size) return nextKnownMessageSequence(chat.id);
|
|
3566
|
+
return 1;
|
|
3567
|
+
}
|
|
3568
|
+
async function overlayCachedChatStates(chats) {
|
|
3569
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3570
|
+
for (const chat of chats) {
|
|
3571
|
+
if (!chat.accountId || !chat.externalThreadId) continue;
|
|
3572
|
+
const key = `${chat.providerId}:${chat.accountId}`;
|
|
3573
|
+
const group = groups.get(key) ?? {
|
|
3574
|
+
accountId: chat.accountId,
|
|
3575
|
+
chats: [],
|
|
3576
|
+
providerId: chat.providerId
|
|
3577
|
+
};
|
|
3578
|
+
group.chats.push(chat);
|
|
3579
|
+
groups.set(key, group);
|
|
3580
|
+
}
|
|
3581
|
+
if (!groups.size) return chats;
|
|
3582
|
+
const overlays = /* @__PURE__ */ new Map();
|
|
3583
|
+
for (const group of groups.values()) {
|
|
3584
|
+
const account = await requireConnectedAccount(group.accountId).catch(() => null);
|
|
3585
|
+
if (!account) continue;
|
|
3586
|
+
const adapter = getProviderAdapter(group.providerId);
|
|
3587
|
+
if (!adapter.readCachedChatStates) continue;
|
|
3588
|
+
const states = await adapter.readCachedChatStates(account, group.chats.map((chat) => chat.externalThreadId).filter((id) => Boolean(id))).catch(() => null);
|
|
3589
|
+
if (!states?.size) continue;
|
|
3590
|
+
for (const chat of group.chats) {
|
|
3591
|
+
const state = chat.externalThreadId ? states.get(chat.externalThreadId) : null;
|
|
3592
|
+
if (!state) continue;
|
|
3593
|
+
let next = overlays.get(chat.id) ?? chat;
|
|
3594
|
+
const updatedAt = parseProviderDate(state.updatedAt ?? null);
|
|
3595
|
+
if (updatedAt && updatedAt.getTime() > next.lastActivityAt.getTime()) next = {
|
|
3596
|
+
...next,
|
|
3597
|
+
lastActivityAt: updatedAt
|
|
3598
|
+
};
|
|
3599
|
+
if (state.status === "RUNNING") {
|
|
3600
|
+
if (await hasRecentInterruptRequest(chat.id) && !await hasActiveChatRun(chat.id, "RUNNING")) next = {
|
|
3601
|
+
...next,
|
|
3602
|
+
status: "IDLE"
|
|
3603
|
+
};
|
|
3604
|
+
else if (next.status !== "RUNNING") next = {
|
|
3605
|
+
...next,
|
|
3606
|
+
status: "RUNNING"
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
if (next !== chat) overlays.set(chat.id, next);
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
return chats.map((chat) => overlays.get(chat.id) ?? chat);
|
|
3613
|
+
}
|
|
3614
|
+
async function refreshChatStatusesForWorkspaces(workspacePaths) {
|
|
3615
|
+
await ensureDatabase();
|
|
3616
|
+
const paths = workspacePaths.map((path) => path.trim()).filter(Boolean);
|
|
3617
|
+
if (!paths.length) return [];
|
|
3618
|
+
const chats = await prisma.chat.findMany({
|
|
3619
|
+
orderBy: [{ lastActivityAt: "desc" }, { updatedAt: "desc" }],
|
|
3620
|
+
where: {
|
|
3621
|
+
status: { not: "ARCHIVED" },
|
|
3622
|
+
workingDirectory: { in: paths },
|
|
3623
|
+
externalThreadId: { not: null },
|
|
3624
|
+
accountId: { not: null }
|
|
3625
|
+
}
|
|
3626
|
+
});
|
|
3627
|
+
const updatedChats = [];
|
|
3628
|
+
for (const chat of chats) {
|
|
3629
|
+
if (!chat.accountId || !chat.externalThreadId) continue;
|
|
3630
|
+
const account = await requireConnectedAccount(chat.accountId).catch(() => null);
|
|
3631
|
+
if (!account) continue;
|
|
3632
|
+
const adapter = getProviderAdapter(chat.providerId);
|
|
3633
|
+
let status = adapter.readChatStatus ? await adapter.readChatStatus(account, chat.externalThreadId).catch(() => null) : null;
|
|
3634
|
+
const hasActiveRun = await hasActiveChatRun(chat.id, status);
|
|
3635
|
+
if (status === "RUNNING" && !hasActiveRun && await hasRecentInterruptRequest(chat.id)) status = "IDLE";
|
|
3636
|
+
if (status !== "RUNNING" && hasActiveRun) status = "RUNNING";
|
|
3637
|
+
if (!status || status === chat.status) continue;
|
|
3638
|
+
const updated = await prisma.chat.update({
|
|
3639
|
+
where: { id: chat.id },
|
|
3640
|
+
data: { status }
|
|
3641
|
+
});
|
|
3642
|
+
updatedChats.push(serializeChat(updated));
|
|
3643
|
+
}
|
|
3644
|
+
return updatedChats;
|
|
3645
|
+
}
|
|
3646
|
+
async function syncProviderChats() {
|
|
3647
|
+
const stats = /* @__PURE__ */ new Map();
|
|
3648
|
+
const accounts = await prisma.providerAccount.findMany({
|
|
3649
|
+
orderBy: { createdAt: "asc" },
|
|
3650
|
+
where: { status: "CONNECTED" }
|
|
3651
|
+
});
|
|
3652
|
+
for (const account of accounts) for (const [key, value] of await syncProviderAccountChats(account)) stats.set(key, value);
|
|
3653
|
+
return stats;
|
|
3654
|
+
}
|
|
3655
|
+
async function syncProviderChatsOnce() {
|
|
3656
|
+
if (!providerChatsSyncPromise) providerChatsSyncPromise = syncProviderChats().finally(() => {
|
|
3657
|
+
providerChatsSyncPromise = null;
|
|
3658
|
+
});
|
|
3659
|
+
return providerChatsSyncPromise;
|
|
3660
|
+
}
|
|
3661
|
+
async function syncProviderAccountChats(account) {
|
|
3662
|
+
const stats = /* @__PURE__ */ new Map();
|
|
3663
|
+
const adapter = getProviderAdapter(account.providerId);
|
|
3664
|
+
let providerChats;
|
|
3665
|
+
try {
|
|
3666
|
+
providerChats = await adapter.listChats(account);
|
|
3667
|
+
} catch {
|
|
3668
|
+
return stats;
|
|
3669
|
+
}
|
|
3670
|
+
for (const providerChat of dedupeProviderChatList(providerChats)) {
|
|
3671
|
+
const externalThreadId = providerChat.externalThreadId.trim();
|
|
3672
|
+
if (!externalThreadId) continue;
|
|
3673
|
+
if (providerChat.stats && (providerChat.stats.additions > 0 || providerChat.stats.deletions > 0)) stats.set(providerChatStatsKey(account.id, externalThreadId), providerChat.stats);
|
|
3674
|
+
const existing = await prisma.chat.findFirst({ where: {
|
|
3675
|
+
accountId: account.id,
|
|
3676
|
+
externalThreadId,
|
|
3677
|
+
providerId: account.providerId
|
|
3678
|
+
} });
|
|
3679
|
+
const timestamp = parseProviderDate(providerChat.updatedAt) ?? /* @__PURE__ */ new Date();
|
|
3680
|
+
const title = normalizeTitle(providerChat.title) ?? "Untitled chat";
|
|
3681
|
+
const workingDirectory = nullableString(providerChat.workingDirectory);
|
|
3682
|
+
const data = {
|
|
3683
|
+
title,
|
|
3684
|
+
workingDirectory,
|
|
3685
|
+
lastActivityAt: timestamp
|
|
3686
|
+
};
|
|
3687
|
+
if (existing) {
|
|
3688
|
+
if (providerChat.status && existing.status !== "ARCHIVED") {
|
|
3689
|
+
const hasActiveRun = await hasActiveProviderThreadRun(account, externalThreadId);
|
|
3690
|
+
data.status = providerChat.status === "RUNNING" && !hasActiveRun && await hasRecentInterruptRequest(existing.id) ? "IDLE" : providerChat.status === "RUNNING" || hasActiveRun ? "RUNNING" : providerChat.status;
|
|
3691
|
+
}
|
|
3692
|
+
await prisma.chat.updateMany({
|
|
3693
|
+
where: {
|
|
3694
|
+
accountId: account.id,
|
|
3695
|
+
externalThreadId,
|
|
3696
|
+
providerId: account.providerId,
|
|
3697
|
+
status: { not: "ARCHIVED" }
|
|
3698
|
+
},
|
|
3699
|
+
data
|
|
3700
|
+
});
|
|
3701
|
+
continue;
|
|
3702
|
+
}
|
|
3703
|
+
await prisma.chat.create({ data: {
|
|
3704
|
+
title,
|
|
3705
|
+
workingDirectory,
|
|
3706
|
+
lastActivityAt: timestamp,
|
|
3707
|
+
accountId: account.id,
|
|
3708
|
+
providerId: account.providerId,
|
|
3709
|
+
externalThreadId,
|
|
3710
|
+
status: providerChat.status ?? "IDLE"
|
|
3711
|
+
} });
|
|
3712
|
+
}
|
|
3713
|
+
return stats;
|
|
3714
|
+
}
|
|
3715
|
+
function dedupeChatList(chats) {
|
|
3716
|
+
const visible = [];
|
|
3717
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
3718
|
+
for (const chat of chats) {
|
|
3719
|
+
const key = chatListDedupeKey(chat);
|
|
3720
|
+
if (!key) {
|
|
3721
|
+
visible.push(chat);
|
|
3722
|
+
continue;
|
|
3723
|
+
}
|
|
3724
|
+
const existingIndex = indexByKey.get(key);
|
|
3725
|
+
if (existingIndex === void 0) {
|
|
3726
|
+
indexByKey.set(key, visible.length);
|
|
3727
|
+
visible.push(chat);
|
|
3728
|
+
continue;
|
|
3729
|
+
}
|
|
3730
|
+
const existing = visible[existingIndex];
|
|
3731
|
+
if (preferChatListItem(chat, existing)) visible[existingIndex] = chat;
|
|
3732
|
+
}
|
|
3733
|
+
return visible;
|
|
3734
|
+
}
|
|
3735
|
+
function chatListDedupeKey(chat) {
|
|
3736
|
+
return chat.externalThreadId && chat.accountId ? `${chat.providerId}:${chat.accountId}:${chat.externalThreadId}` : null;
|
|
3737
|
+
}
|
|
3738
|
+
function preferChatListItem(candidate, current) {
|
|
3739
|
+
if (candidate.status !== current.status) return candidate.status === "RUNNING";
|
|
3740
|
+
if (candidate.lastActivityAt.getTime() !== current.lastActivityAt.getTime()) return candidate.lastActivityAt.getTime() > current.lastActivityAt.getTime();
|
|
3741
|
+
return candidate.updatedAt.getTime() > current.updatedAt.getTime();
|
|
3742
|
+
}
|
|
3743
|
+
function dedupeProviderChatList(chats) {
|
|
3744
|
+
const visible = [];
|
|
3745
|
+
const indexByThreadId = /* @__PURE__ */ new Map();
|
|
3746
|
+
for (const chat of chats) {
|
|
3747
|
+
const externalThreadId = chat.externalThreadId.trim();
|
|
3748
|
+
if (!externalThreadId) continue;
|
|
3749
|
+
const existingIndex = indexByThreadId.get(externalThreadId);
|
|
3750
|
+
if (existingIndex === void 0) {
|
|
3751
|
+
indexByThreadId.set(externalThreadId, visible.length);
|
|
3752
|
+
visible.push(chat);
|
|
3753
|
+
continue;
|
|
3754
|
+
}
|
|
3755
|
+
const existing = visible[existingIndex];
|
|
3756
|
+
if (preferProviderChatListItem(chat, existing)) visible[existingIndex] = chat;
|
|
3757
|
+
}
|
|
3758
|
+
return visible;
|
|
3759
|
+
}
|
|
3760
|
+
function preferProviderChatListItem(candidate, current) {
|
|
3761
|
+
if (candidate.status !== current.status) return candidate.status === "RUNNING";
|
|
3762
|
+
return (parseProviderDate(candidate.updatedAt)?.getTime() ?? 0) > (parseProviderDate(current.updatedAt)?.getTime() ?? 0);
|
|
3763
|
+
}
|
|
3764
|
+
async function loadProviderMessages(chat) {
|
|
3765
|
+
if (!chat.accountId || !chat.externalThreadId) return [];
|
|
3766
|
+
const account = await requireConnectedAccount(chat.accountId).catch(() => null);
|
|
3767
|
+
if (!account) return [];
|
|
3768
|
+
return getProviderAdapter(chat.providerId).loadChatMessages(account, chat.externalThreadId).catch(() => []);
|
|
3769
|
+
}
|
|
3770
|
+
async function hasActiveProviderThreadRun(account, externalThreadId) {
|
|
3771
|
+
const run = await prisma.chatRun.findFirst({
|
|
3772
|
+
select: { id: true },
|
|
3773
|
+
where: {
|
|
3774
|
+
status: { in: ["QUEUED", "RUNNING"] },
|
|
3775
|
+
chat: {
|
|
3776
|
+
accountId: account.id,
|
|
3777
|
+
externalThreadId,
|
|
3778
|
+
providerId: account.providerId
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
});
|
|
3782
|
+
return Boolean(run);
|
|
3783
|
+
}
|
|
3784
|
+
async function hasRecentInterruptRequest(chatId) {
|
|
3785
|
+
const run = await prisma.chatRun.findFirst({
|
|
3786
|
+
select: { id: true },
|
|
3787
|
+
where: {
|
|
3788
|
+
chatId,
|
|
3789
|
+
interruptRequestedAt: { gte: /* @__PURE__ */ new Date(Date.now() - recentInterruptSuppressMs) },
|
|
3790
|
+
status: "CANCELLED"
|
|
3791
|
+
}
|
|
3792
|
+
});
|
|
3793
|
+
return Boolean(run);
|
|
3794
|
+
}
|
|
3795
|
+
async function hasActiveChatRun(chatId, providerStatus) {
|
|
3796
|
+
const runs = await prisma.chatRun.findMany({
|
|
3797
|
+
select: {
|
|
3798
|
+
createdAt: true,
|
|
3799
|
+
externalTurnId: true,
|
|
3800
|
+
id: true,
|
|
3801
|
+
startedAt: true
|
|
3802
|
+
},
|
|
3803
|
+
where: {
|
|
3804
|
+
chatId,
|
|
3805
|
+
status: { in: ["QUEUED", "RUNNING"] }
|
|
3806
|
+
}
|
|
3807
|
+
});
|
|
3808
|
+
if (!runs.length) return false;
|
|
3809
|
+
if (providerStatus && providerStatus !== "RUNNING") {
|
|
3810
|
+
const now = Date.now();
|
|
3811
|
+
const freshLocalRuns = runs.filter((run) => !run.externalTurnId && now - (run.startedAt ?? run.createdAt).getTime() < localRunGraceMs);
|
|
3812
|
+
const staleRunIds = runs.filter((run) => !freshLocalRuns.some((freshRun) => freshRun.id === run.id)).map((run) => run.id);
|
|
3813
|
+
if (staleRunIds.length) await prisma.chatRun.updateMany({
|
|
3814
|
+
where: { id: { in: staleRunIds } },
|
|
3815
|
+
data: {
|
|
3816
|
+
endedAt: /* @__PURE__ */ new Date(),
|
|
3817
|
+
status: "CANCELLED"
|
|
3818
|
+
}
|
|
3819
|
+
});
|
|
3820
|
+
return freshLocalRuns.length > 0;
|
|
3821
|
+
}
|
|
3822
|
+
return true;
|
|
3823
|
+
}
|
|
3824
|
+
async function hasRunningChatRunExcept(chatId, excludedRunId) {
|
|
3825
|
+
const run = await prisma.chatRun.findFirst({
|
|
3826
|
+
select: { id: true },
|
|
3827
|
+
where: {
|
|
3828
|
+
chatId,
|
|
3829
|
+
id: { not: excludedRunId },
|
|
3830
|
+
status: "RUNNING"
|
|
3831
|
+
}
|
|
3832
|
+
});
|
|
3833
|
+
return Boolean(run);
|
|
3834
|
+
}
|
|
3835
|
+
async function executeMessage(chatId, dto) {
|
|
3836
|
+
const chat = await getChat(chatId);
|
|
3837
|
+
if (!dto.content.trim()) throw new HttpError(400, "Message content is required.");
|
|
3838
|
+
const accountId = dto.accountId ?? chat.accountId;
|
|
3839
|
+
if (!accountId) throw new HttpError(400, "Choose a provider account before sending messages.");
|
|
3840
|
+
const account = await requireConnectedAccount(accountId);
|
|
3841
|
+
if (account.providerId !== chat.providerId) throw new HttpError(400, "Message account provider must match the chat provider.");
|
|
3842
|
+
if (!chat.workingDirectory) throw new HttpError(400, "Select a working directory before sending messages.");
|
|
3843
|
+
if (chat.status === "RUNNING") return dto.delivery === "steer" ? steerActiveMessage(chat, account, dto) : queueMessage(chat, account, dto);
|
|
3844
|
+
const now = /* @__PURE__ */ new Date();
|
|
3845
|
+
const run = await prisma.chatRun.create({ data: {
|
|
3846
|
+
chatId: chat.id,
|
|
3847
|
+
providerId: chat.providerId,
|
|
3848
|
+
accountId: account.id,
|
|
3849
|
+
status: "QUEUED",
|
|
3850
|
+
request: runRequestFromDto(dto)
|
|
3851
|
+
} });
|
|
3852
|
+
const previewSequence = await nextMessageSequenceForChat(chat);
|
|
3853
|
+
const userMessage = serializeRunMessage(chat.id, run, "USER", dto.content, "COMPLETED", previewSequence);
|
|
3854
|
+
const assistantMessage = serializeRunMessage(chat.id, run, "ASSISTANT", "Running", "STREAMING", previewSequence + 1);
|
|
3855
|
+
const runningChat = await prisma.chat.update({
|
|
3856
|
+
where: { id: chat.id },
|
|
3857
|
+
data: {
|
|
3858
|
+
status: "RUNNING",
|
|
3859
|
+
accountId: account.id,
|
|
3860
|
+
collaborationMode: nullableString(dto.collaborationMode) ?? chat.collaborationMode,
|
|
3861
|
+
lastActivityAt: now,
|
|
3862
|
+
permissionMode: nullableString(dto.permissionMode) ?? chat.permissionMode
|
|
3863
|
+
}
|
|
3864
|
+
});
|
|
3865
|
+
publishMessageCreated(chat.id, userMessage);
|
|
3866
|
+
publishMessageCreated(chat.id, assistantMessage);
|
|
3867
|
+
publishProviderEvent({
|
|
3868
|
+
threadId: chat.id,
|
|
3869
|
+
type: "chat.updated",
|
|
3870
|
+
payload: serializeChat(runningChat)
|
|
3871
|
+
});
|
|
3872
|
+
publishProviderEvent({
|
|
3873
|
+
threadId: chat.id,
|
|
3874
|
+
type: "run.status",
|
|
3875
|
+
payload: {
|
|
3876
|
+
runId: run.id,
|
|
3877
|
+
status: "QUEUED"
|
|
3878
|
+
}
|
|
3879
|
+
});
|
|
3880
|
+
runProviderTurn({
|
|
3881
|
+
accountId: account.id,
|
|
3882
|
+
attachments: dto.attachments ?? [],
|
|
3883
|
+
chatId: chat.id,
|
|
3884
|
+
content: dto.content,
|
|
3885
|
+
goalObjective: dto.goalObjective ?? null,
|
|
3886
|
+
model: readRunMetadataString(dto.metadata, "model"),
|
|
3887
|
+
permissionMode: dto.permissionMode ?? null,
|
|
3888
|
+
reasoningEffort: readRunMetadataString(dto.metadata, "reasoningEffort"),
|
|
3889
|
+
runId: run.id,
|
|
3890
|
+
requestedCollaborationMode: dto.collaborationMode ?? null,
|
|
3891
|
+
serviceTier: readRunMetadataString(dto.metadata, "serviceTier")
|
|
3892
|
+
}).catch((error) => {
|
|
3893
|
+
console.error("Provider run failed.", error);
|
|
3894
|
+
});
|
|
3895
|
+
return {
|
|
3896
|
+
message: userMessage,
|
|
3897
|
+
assistantMessage,
|
|
3898
|
+
runId: run.id,
|
|
3899
|
+
status: "QUEUED"
|
|
3900
|
+
};
|
|
3901
|
+
}
|
|
3902
|
+
async function respondToServerRequest(chatId, requestId, dto) {
|
|
3903
|
+
const chat = await getChat(chatId);
|
|
3904
|
+
const accountId = chat.accountId;
|
|
3905
|
+
if (!accountId) throw new HttpError(400, "Choose a provider account before responding.");
|
|
3906
|
+
const account = await requireConnectedAccount(accountId);
|
|
3907
|
+
if (account.providerId !== chat.providerId) throw new HttpError(400, "Response account provider must match the chat provider.");
|
|
3908
|
+
const responder = getProviderAdapter(chat.providerId).respondToServerRequest;
|
|
3909
|
+
if (!responder) throw new HttpError(400, "This provider does not support server request responses.");
|
|
3910
|
+
if (dto.kind === "userInput" && isCodexFunctionCallRequestId(requestId)) throw new HttpError(409, "This input prompt came from saved Codex history, not a live request. Refresh the chat or send a new message to continue.");
|
|
3911
|
+
if (dto.kind === "userInput" && !await hasActiveChatRun(chat.id)) throw new HttpError(409, "This input request is no longer active. Refresh the chat or send a new message to continue.");
|
|
3912
|
+
await responder(account, requestId, dto);
|
|
3913
|
+
return null;
|
|
3914
|
+
}
|
|
3915
|
+
async function queueMessage(chat, account, dto) {
|
|
3916
|
+
const run = await prisma.chatRun.create({ data: {
|
|
3917
|
+
chatId: chat.id,
|
|
3918
|
+
providerId: chat.providerId,
|
|
3919
|
+
accountId: account.id,
|
|
3920
|
+
status: "QUEUED",
|
|
3921
|
+
request: runRequestFromDto(dto)
|
|
3922
|
+
} });
|
|
3923
|
+
const message = serializeRunMessage(chat.id, run, "USER", dto.content, "PENDING", await nextMessageSequenceForChat(chat));
|
|
3924
|
+
const updatedChat = await prisma.chat.update({
|
|
3925
|
+
where: { id: chat.id },
|
|
3926
|
+
data: {
|
|
3927
|
+
accountId: account.id,
|
|
3928
|
+
collaborationMode: nullableString(dto.collaborationMode) ?? chat.collaborationMode,
|
|
3929
|
+
lastActivityAt: /* @__PURE__ */ new Date(),
|
|
3930
|
+
permissionMode: nullableString(dto.permissionMode) ?? chat.permissionMode,
|
|
3931
|
+
status: "RUNNING"
|
|
3932
|
+
}
|
|
3933
|
+
});
|
|
3934
|
+
publishMessageCreated(chat.id, message);
|
|
3935
|
+
publishProviderEvent({
|
|
3936
|
+
threadId: chat.id,
|
|
3937
|
+
type: "chat.updated",
|
|
3938
|
+
payload: serializeChat(updatedChat)
|
|
3939
|
+
});
|
|
3940
|
+
publishProviderEvent({
|
|
3941
|
+
threadId: chat.id,
|
|
3942
|
+
type: "run.status",
|
|
3943
|
+
payload: {
|
|
3944
|
+
runId: run.id,
|
|
3945
|
+
status: "QUEUED"
|
|
3946
|
+
}
|
|
3947
|
+
});
|
|
3948
|
+
return {
|
|
3949
|
+
message,
|
|
3950
|
+
assistantMessage: null,
|
|
3951
|
+
runId: run.id,
|
|
3952
|
+
status: "QUEUED"
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
async function steerActiveMessage(chat, account, dto) {
|
|
3956
|
+
const run = await prisma.chatRun.create({ data: {
|
|
3957
|
+
chatId: chat.id,
|
|
3958
|
+
providerId: chat.providerId,
|
|
3959
|
+
accountId: account.id,
|
|
3960
|
+
status: "QUEUED",
|
|
3961
|
+
request: runRequestFromDto(dto)
|
|
3962
|
+
} });
|
|
3963
|
+
let result;
|
|
3964
|
+
try {
|
|
3965
|
+
result = await steerQueuedRun(chat, run);
|
|
3966
|
+
} catch (error) {
|
|
3967
|
+
await prisma.chatRun.update({
|
|
3968
|
+
where: { id: run.id },
|
|
3969
|
+
data: {
|
|
3970
|
+
endedAt: /* @__PURE__ */ new Date(),
|
|
3971
|
+
status: "CANCELLED"
|
|
3972
|
+
}
|
|
3973
|
+
}).catch(() => void 0);
|
|
3974
|
+
throw error;
|
|
3975
|
+
}
|
|
3976
|
+
if (!result.message) throw new HttpError(409, "Unable to steer the active turn.");
|
|
3977
|
+
return {
|
|
3978
|
+
message: result.message,
|
|
3979
|
+
assistantMessage: null,
|
|
3980
|
+
runId: run.id,
|
|
3981
|
+
status: "RUNNING"
|
|
3982
|
+
};
|
|
3983
|
+
}
|
|
3984
|
+
async function interruptChatRun(chatId) {
|
|
3985
|
+
const chat = await getChat(chatId);
|
|
3986
|
+
const run = await prisma.chatRun.findFirst({
|
|
3987
|
+
orderBy: { createdAt: "desc" },
|
|
3988
|
+
where: {
|
|
3989
|
+
chatId,
|
|
3990
|
+
status: "RUNNING"
|
|
3991
|
+
}
|
|
3992
|
+
});
|
|
3993
|
+
const accountId = run?.accountId ?? chat.accountId;
|
|
3994
|
+
const account = accountId ? await requireConnectedAccount(accountId).catch(() => null) : null;
|
|
3995
|
+
const adapter = getProviderAdapter(chat.providerId);
|
|
3996
|
+
const providerRunning = (account && chat.externalThreadId && adapter.readChatStatus ? await adapter.readChatStatus(account, chat.externalThreadId).catch(() => null) : null) === "RUNNING";
|
|
3997
|
+
if (chat.status !== "RUNNING" && !run && !providerRunning) throw new HttpError(409, "There is no running task to stop.");
|
|
3998
|
+
if (!run) {
|
|
3999
|
+
if (account && chat.externalThreadId && providerRunning) {
|
|
4000
|
+
await adapter.interrupt(account, chat.externalThreadId, null).catch(() => void 0);
|
|
4001
|
+
const interruptedAt = /* @__PURE__ */ new Date();
|
|
4002
|
+
await prisma.chatRun.create({ data: {
|
|
4003
|
+
accountId: account.id,
|
|
4004
|
+
chatId,
|
|
4005
|
+
endedAt: interruptedAt,
|
|
4006
|
+
interruptRequestedAt: interruptedAt,
|
|
4007
|
+
providerId: chat.providerId,
|
|
4008
|
+
request: {},
|
|
4009
|
+
startedAt: interruptedAt,
|
|
4010
|
+
status: "CANCELLED"
|
|
4011
|
+
} });
|
|
4012
|
+
}
|
|
4013
|
+
publishProviderEvent({
|
|
4014
|
+
threadId: chatId,
|
|
4015
|
+
type: "chat.updated",
|
|
4016
|
+
payload: serializeChat(await prisma.chat.update({
|
|
4017
|
+
where: { id: chatId },
|
|
4018
|
+
data: { status: "IDLE" }
|
|
4019
|
+
}))
|
|
4020
|
+
});
|
|
4021
|
+
await publishMessageDeltas(chatId);
|
|
4022
|
+
return {
|
|
4023
|
+
chatId,
|
|
4024
|
+
runId: null,
|
|
4025
|
+
status: "CANCELLED",
|
|
4026
|
+
message: providerRunning ? "Provider task cancelled." : "Marked stale run as cancelled."
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
if (account && chat.externalThreadId) await getProviderAdapter(chat.providerId).interrupt(account, chat.externalThreadId, run.externalTurnId).catch(() => void 0);
|
|
4030
|
+
await prisma.chatRun.update({
|
|
4031
|
+
where: { id: run.id },
|
|
4032
|
+
data: {
|
|
4033
|
+
status: "CANCELLED",
|
|
4034
|
+
endedAt: /* @__PURE__ */ new Date(),
|
|
4035
|
+
interruptRequestedAt: /* @__PURE__ */ new Date()
|
|
4036
|
+
}
|
|
4037
|
+
});
|
|
4038
|
+
publishProviderEvent({
|
|
4039
|
+
threadId: chatId,
|
|
4040
|
+
type: "chat.updated",
|
|
4041
|
+
payload: serializeChat(await prisma.chat.update({
|
|
4042
|
+
where: { id: chatId },
|
|
4043
|
+
data: { status: "IDLE" }
|
|
4044
|
+
}))
|
|
4045
|
+
});
|
|
4046
|
+
await publishMessageDeltas(chatId);
|
|
4047
|
+
publishProviderEvent({
|
|
4048
|
+
threadId: chatId,
|
|
4049
|
+
type: "run.status",
|
|
4050
|
+
payload: {
|
|
4051
|
+
runId: run.id,
|
|
4052
|
+
status: "CANCELLED"
|
|
4053
|
+
}
|
|
4054
|
+
});
|
|
4055
|
+
return {
|
|
4056
|
+
chatId,
|
|
4057
|
+
runId: run.id,
|
|
4058
|
+
status: "CANCELLED",
|
|
4059
|
+
message: "Task cancelled."
|
|
4060
|
+
};
|
|
4061
|
+
}
|
|
4062
|
+
async function updateQueuedChatRun(chatId, runId, dto) {
|
|
4063
|
+
const content = dto.content.trim();
|
|
4064
|
+
if (!content) throw new HttpError(400, "Message content is required.");
|
|
4065
|
+
const run = await requireQueuedRun(chatId, runId);
|
|
4066
|
+
const request = readRunRequestRecord(run);
|
|
4067
|
+
const updatedRun = await prisma.chatRun.update({
|
|
4068
|
+
where: { id: run.id },
|
|
4069
|
+
data: { request: {
|
|
4070
|
+
...request,
|
|
4071
|
+
content
|
|
4072
|
+
} }
|
|
4073
|
+
});
|
|
4074
|
+
const message = serializeRunMessage(chatId, updatedRun, "USER", content, "PENDING", knownMessageSequence(chatId, runMessageId(updatedRun, "USER")) ?? nextKnownMessageSequence(chatId));
|
|
4075
|
+
publishMessageCreated(chatId, message);
|
|
4076
|
+
await publishMessageDeltas(chatId);
|
|
4077
|
+
return {
|
|
4078
|
+
chatId,
|
|
4079
|
+
runId,
|
|
4080
|
+
status: "QUEUED",
|
|
4081
|
+
message
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
async function reorderQueuedChatRuns(chatId, dto) {
|
|
4085
|
+
await getChat(chatId);
|
|
4086
|
+
const queuedRuns = sortQueuedRuns(await prisma.chatRun.findMany({
|
|
4087
|
+
orderBy: { createdAt: "asc" },
|
|
4088
|
+
where: {
|
|
4089
|
+
chatId,
|
|
4090
|
+
status: "QUEUED"
|
|
4091
|
+
}
|
|
4092
|
+
}));
|
|
4093
|
+
const queuedRunIds = new Set(queuedRuns.map((run) => run.id));
|
|
4094
|
+
const requestedRunIds = dto.runIds.filter((runId, index, runIds) => runIds.indexOf(runId) === index);
|
|
4095
|
+
if (requestedRunIds.find((runId) => !queuedRunIds.has(runId))) throw new HttpError(404, "Queued message not found.");
|
|
4096
|
+
const orderedIds = [...requestedRunIds, ...queuedRuns.map((run) => run.id).filter((runId) => !requestedRunIds.includes(runId))];
|
|
4097
|
+
const runById = new Map(queuedRuns.map((run) => [run.id, run]));
|
|
4098
|
+
await prisma.$transaction(orderedIds.map((orderedRunId, index) => {
|
|
4099
|
+
const run = runById.get(orderedRunId);
|
|
4100
|
+
if (!run) throw new HttpError(404, "Queued message not found.");
|
|
4101
|
+
const request = readRunRequestRecord(run);
|
|
4102
|
+
const metadata = readRunRequestMetadata(request.metadata);
|
|
4103
|
+
return prisma.chatRun.update({
|
|
4104
|
+
where: { id: orderedRunId },
|
|
4105
|
+
data: { request: {
|
|
4106
|
+
...request,
|
|
4107
|
+
metadata: {
|
|
4108
|
+
...metadata,
|
|
4109
|
+
queueOrder: index + 1
|
|
4110
|
+
}
|
|
4111
|
+
} }
|
|
4112
|
+
});
|
|
4113
|
+
}));
|
|
4114
|
+
await publishMessageDeltas(chatId);
|
|
4115
|
+
return {
|
|
4116
|
+
chatId,
|
|
4117
|
+
runIds: orderedIds
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
async function deleteQueuedChatRun(chatId, runId) {
|
|
4121
|
+
const run = await requireQueuedRun(chatId, runId);
|
|
4122
|
+
await prisma.chatRun.update({
|
|
4123
|
+
where: { id: run.id },
|
|
4124
|
+
data: {
|
|
4125
|
+
status: "CANCELLED",
|
|
4126
|
+
endedAt: /* @__PURE__ */ new Date()
|
|
4127
|
+
}
|
|
4128
|
+
});
|
|
4129
|
+
await publishMessageDeltas(chatId);
|
|
4130
|
+
publishProviderEvent({
|
|
4131
|
+
threadId: chatId,
|
|
4132
|
+
type: "run.status",
|
|
4133
|
+
payload: {
|
|
4134
|
+
runId,
|
|
4135
|
+
status: "CANCELLED"
|
|
4136
|
+
}
|
|
4137
|
+
});
|
|
4138
|
+
return {
|
|
4139
|
+
chatId,
|
|
4140
|
+
runId,
|
|
4141
|
+
status: "CANCELLED",
|
|
4142
|
+
message: null
|
|
4143
|
+
};
|
|
4144
|
+
}
|
|
4145
|
+
async function steerQueuedChatRun(chatId, runId) {
|
|
4146
|
+
return steerQueuedRun(await getChat(chatId), await requireQueuedRun(chatId, runId));
|
|
4147
|
+
}
|
|
4148
|
+
async function requireProviderBackedChat(chatId, action) {
|
|
4149
|
+
const chat = await getChat(chatId);
|
|
4150
|
+
if (!chat.externalThreadId) throw new HttpError(400, `Start this chat before using ${action}.`);
|
|
4151
|
+
if (!chat.accountId) throw new HttpError(400, `Choose a provider account before using ${action}.`);
|
|
4152
|
+
const account = await requireConnectedAccount(chat.accountId);
|
|
4153
|
+
if (account.providerId !== chat.providerId) throw new HttpError(400, "Chat provider must match the selected account provider.");
|
|
4154
|
+
return {
|
|
4155
|
+
account,
|
|
4156
|
+
adapter: getProviderAdapter(chat.providerId),
|
|
4157
|
+
chat,
|
|
4158
|
+
externalThreadId: chat.externalThreadId
|
|
4159
|
+
};
|
|
4160
|
+
}
|
|
4161
|
+
async function requireQueuedRun(chatId, runId) {
|
|
4162
|
+
const run = await prisma.chatRun.findFirst({ where: {
|
|
4163
|
+
chatId,
|
|
4164
|
+
id: runId,
|
|
4165
|
+
status: "QUEUED"
|
|
4166
|
+
} });
|
|
4167
|
+
if (!run) throw new HttpError(404, "Queued message not found.");
|
|
4168
|
+
return run;
|
|
4169
|
+
}
|
|
4170
|
+
async function steerQueuedRun(chat, queuedRun) {
|
|
4171
|
+
if (!chat.workingDirectory) throw new HttpError(400, "Select a working directory before steering messages.");
|
|
4172
|
+
const activeRun = await prisma.chatRun.findFirst({
|
|
4173
|
+
orderBy: { startedAt: "desc" },
|
|
4174
|
+
where: {
|
|
4175
|
+
chatId: chat.id,
|
|
4176
|
+
status: "RUNNING",
|
|
4177
|
+
externalTurnId: { not: null }
|
|
4178
|
+
}
|
|
4179
|
+
});
|
|
4180
|
+
if (!activeRun?.externalTurnId || !chat.externalThreadId) throw new HttpError(409, "There is no steerable active turn yet.");
|
|
4181
|
+
const accountId = activeRun.accountId ?? queuedRun.accountId ?? chat.accountId;
|
|
4182
|
+
if (!accountId) throw new HttpError(400, "Choose a provider account before steering messages.");
|
|
4183
|
+
const account = await requireConnectedAccount(accountId);
|
|
4184
|
+
const adapter = getProviderAdapter(chat.providerId);
|
|
4185
|
+
if (!adapter.steerMessage) throw new HttpError(400, "This provider does not support steering.");
|
|
4186
|
+
const request = readRunRequest(queuedRun);
|
|
4187
|
+
const content = request.content;
|
|
4188
|
+
await adapter.steerMessage(account, {
|
|
4189
|
+
attachments: request.attachments,
|
|
4190
|
+
content,
|
|
4191
|
+
threadId: chat.externalThreadId,
|
|
4192
|
+
turnId: activeRun.externalTurnId,
|
|
4193
|
+
workingDirectory: chat.workingDirectory
|
|
4194
|
+
});
|
|
4195
|
+
const completedAt = /* @__PURE__ */ new Date();
|
|
4196
|
+
const updatedRun = await prisma.chatRun.update({
|
|
4197
|
+
where: { id: queuedRun.id },
|
|
4198
|
+
data: {
|
|
4199
|
+
endedAt: completedAt,
|
|
4200
|
+
externalTurnId: activeRun.externalTurnId,
|
|
4201
|
+
startedAt: completedAt,
|
|
4202
|
+
status: "COMPLETED"
|
|
4203
|
+
}
|
|
4204
|
+
});
|
|
4205
|
+
const messageId = runMessageId(updatedRun, "USER");
|
|
4206
|
+
const sequence = knownMessageSequence(chat.id, messageId) ?? await nextMessageSequenceForChat(chat);
|
|
4207
|
+
const message = serializeRunMessage(chat.id, updatedRun, "USER", content, "COMPLETED", sequence);
|
|
4208
|
+
publishMessageCreated(chat.id, message);
|
|
4209
|
+
await publishMessageDeltas(chat.id);
|
|
4210
|
+
publishProviderEvent({
|
|
4211
|
+
threadId: chat.id,
|
|
4212
|
+
type: "run.status",
|
|
4213
|
+
payload: {
|
|
4214
|
+
runId: queuedRun.id,
|
|
4215
|
+
status: "COMPLETED"
|
|
4216
|
+
}
|
|
4217
|
+
});
|
|
4218
|
+
return {
|
|
4219
|
+
chatId: chat.id,
|
|
4220
|
+
runId: queuedRun.id,
|
|
4221
|
+
status: "COMPLETED",
|
|
4222
|
+
message
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
async function runProviderTurn({ accountId, attachments, chatId, content, goalObjective, model, permissionMode, reasoningEffort, requestedCollaborationMode, runId, serviceTier }) {
|
|
4226
|
+
const [chat, account] = await Promise.all([getChat(chatId), requireConnectedAccount(accountId)]);
|
|
4227
|
+
const adapter = getProviderAdapter(chat.providerId);
|
|
4228
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
4229
|
+
await prisma.chatRun.update({
|
|
4230
|
+
where: { id: runId },
|
|
4231
|
+
data: {
|
|
4232
|
+
status: "RUNNING",
|
|
4233
|
+
startedAt
|
|
4234
|
+
}
|
|
4235
|
+
});
|
|
4236
|
+
publishProviderEvent({
|
|
4237
|
+
threadId: chatId,
|
|
4238
|
+
type: "run.status",
|
|
4239
|
+
payload: {
|
|
4240
|
+
runId,
|
|
4241
|
+
status: "RUNNING"
|
|
4242
|
+
}
|
|
4243
|
+
});
|
|
4244
|
+
try {
|
|
4245
|
+
const liveMessageSequences = /* @__PURE__ */ new Map();
|
|
4246
|
+
let nextLiveMessageSequence = nextKnownMessageSequence(chatId);
|
|
4247
|
+
const result = await adapter.sendMessage(account, {
|
|
4248
|
+
attachments,
|
|
4249
|
+
collaborationMode: requestedCollaborationMode ?? chat.collaborationMode,
|
|
4250
|
+
content,
|
|
4251
|
+
goalObjective,
|
|
4252
|
+
model: model ?? chat.model,
|
|
4253
|
+
onMessage: (message) => {
|
|
4254
|
+
const key = message.itemId ?? `${message.role}:${message.kind ?? "CHAT"}:${message.content}`;
|
|
4255
|
+
let sequence = liveMessageSequences.get(key);
|
|
4256
|
+
if (!sequence) {
|
|
4257
|
+
sequence = nextLiveMessageSequence;
|
|
4258
|
+
nextLiveMessageSequence += 1;
|
|
4259
|
+
liveMessageSequences.set(key, sequence);
|
|
4260
|
+
}
|
|
4261
|
+
publishMessageCreated(chatId, serializeProviderMessage(chatId, {
|
|
4262
|
+
...message,
|
|
4263
|
+
runId
|
|
4264
|
+
}, sequence));
|
|
4265
|
+
},
|
|
4266
|
+
onThreadReady: async (threadId) => {
|
|
4267
|
+
publishProviderEvent({
|
|
4268
|
+
threadId: chatId,
|
|
4269
|
+
type: "chat.updated",
|
|
4270
|
+
payload: serializeChat(await prisma.chat.update({
|
|
4271
|
+
where: { id: chatId },
|
|
4272
|
+
data: {
|
|
4273
|
+
externalThreadId: threadId,
|
|
4274
|
+
status: "RUNNING"
|
|
4275
|
+
}
|
|
4276
|
+
}))
|
|
4277
|
+
});
|
|
4278
|
+
},
|
|
4279
|
+
onTurnStarted: async (turnId) => {
|
|
4280
|
+
await prisma.chatRun.update({
|
|
4281
|
+
where: { id: runId },
|
|
4282
|
+
data: { externalTurnId: turnId }
|
|
4283
|
+
});
|
|
4284
|
+
publishProviderEvent({
|
|
4285
|
+
threadId: chatId,
|
|
4286
|
+
type: "run.status",
|
|
4287
|
+
payload: {
|
|
4288
|
+
runId,
|
|
4289
|
+
status: "RUNNING",
|
|
4290
|
+
turnId
|
|
4291
|
+
}
|
|
4292
|
+
});
|
|
4293
|
+
},
|
|
4294
|
+
permissionMode: permissionMode ?? chat.permissionMode,
|
|
4295
|
+
reasoningEffort: reasoningEffort ?? chat.reasoningEffort,
|
|
4296
|
+
serviceTier: serviceTier ?? chat.serviceTier,
|
|
4297
|
+
threadId: chat.externalThreadId,
|
|
4298
|
+
workingDirectory: chat.workingDirectory
|
|
4299
|
+
});
|
|
4300
|
+
const completedAt = /* @__PURE__ */ new Date();
|
|
4301
|
+
const currentRun = await prisma.chatRun.findUnique({
|
|
4302
|
+
where: { id: runId },
|
|
4303
|
+
select: { status: true }
|
|
4304
|
+
});
|
|
4305
|
+
if (!currentRun || currentRun.status !== "RUNNING") {
|
|
4306
|
+
const hasOtherRunningRun = await hasRunningChatRunExcept(chatId, runId);
|
|
4307
|
+
const updatedChat = await prisma.chat.update({
|
|
4308
|
+
where: { id: chatId },
|
|
4309
|
+
data: {
|
|
4310
|
+
externalThreadId: result.threadId,
|
|
4311
|
+
lastActivityAt: completedAt,
|
|
4312
|
+
status: hasOtherRunningRun ? "RUNNING" : "IDLE"
|
|
4313
|
+
}
|
|
4314
|
+
});
|
|
4315
|
+
await adapter.syncThreadFromAccount(result.threadId, account);
|
|
4316
|
+
publishProviderEvent({
|
|
4317
|
+
threadId: chatId,
|
|
4318
|
+
type: "chat.updated",
|
|
4319
|
+
payload: serializeChat(updatedChat)
|
|
4320
|
+
});
|
|
4321
|
+
await publishMessageDeltas(chatId);
|
|
4322
|
+
if (!hasOtherRunningRun) await startNextQueuedRun(chatId);
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
const updatedChat = await prisma.chat.update({
|
|
4326
|
+
where: { id: chatId },
|
|
4327
|
+
data: {
|
|
4328
|
+
externalThreadId: result.threadId,
|
|
4329
|
+
lastActivityAt: completedAt,
|
|
4330
|
+
status: "IDLE"
|
|
4331
|
+
}
|
|
4332
|
+
});
|
|
4333
|
+
await prisma.chatRun.update({
|
|
4334
|
+
where: { id: runId },
|
|
4335
|
+
data: {
|
|
4336
|
+
endedAt: completedAt,
|
|
4337
|
+
externalTurnId: result.turnId,
|
|
4338
|
+
status: "COMPLETED"
|
|
4339
|
+
}
|
|
4340
|
+
});
|
|
4341
|
+
await adapter.syncThreadFromAccount(result.threadId, account);
|
|
4342
|
+
publishProviderEvent({
|
|
4343
|
+
threadId: chatId,
|
|
4344
|
+
type: "chat.updated",
|
|
4345
|
+
payload: serializeChat(updatedChat)
|
|
4346
|
+
});
|
|
4347
|
+
await publishMessageDeltas(chatId);
|
|
4348
|
+
publishProviderEvent({
|
|
4349
|
+
threadId: chatId,
|
|
4350
|
+
type: "run.status",
|
|
4351
|
+
payload: {
|
|
4352
|
+
runId,
|
|
4353
|
+
status: "COMPLETED"
|
|
4354
|
+
}
|
|
4355
|
+
});
|
|
4356
|
+
await startNextQueuedRun(chatId);
|
|
4357
|
+
} catch (error) {
|
|
4358
|
+
const currentRun = await prisma.chatRun.findUnique({
|
|
4359
|
+
where: { id: runId },
|
|
4360
|
+
select: { status: true }
|
|
4361
|
+
});
|
|
4362
|
+
if (!currentRun || currentRun.status !== "RUNNING") {
|
|
4363
|
+
const hasOtherRunningRun = await hasRunningChatRunExcept(chatId, runId);
|
|
4364
|
+
if (!hasOtherRunningRun) {
|
|
4365
|
+
const settledAt = /* @__PURE__ */ new Date();
|
|
4366
|
+
publishProviderEvent({
|
|
4367
|
+
threadId: chatId,
|
|
4368
|
+
type: "chat.updated",
|
|
4369
|
+
payload: serializeChat(await prisma.chat.update({
|
|
4370
|
+
where: { id: chatId },
|
|
4371
|
+
data: {
|
|
4372
|
+
status: "IDLE",
|
|
4373
|
+
lastActivityAt: settledAt
|
|
4374
|
+
}
|
|
4375
|
+
}))
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
await publishMessageDeltas(chatId);
|
|
4379
|
+
if (!hasOtherRunningRun) await startNextQueuedRun(chatId);
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
const message = error instanceof Error ? error.message : "Provider run failed.";
|
|
4383
|
+
const failedAt = /* @__PURE__ */ new Date();
|
|
4384
|
+
const updatedChat = await prisma.chat.update({
|
|
4385
|
+
where: { id: chatId },
|
|
4386
|
+
data: {
|
|
4387
|
+
status: "IDLE",
|
|
4388
|
+
lastActivityAt: failedAt
|
|
4389
|
+
}
|
|
4390
|
+
});
|
|
4391
|
+
await prisma.chatRun.update({
|
|
4392
|
+
where: { id: runId },
|
|
4393
|
+
data: {
|
|
4394
|
+
endedAt: failedAt,
|
|
4395
|
+
error: message,
|
|
4396
|
+
status: "FAILED"
|
|
4397
|
+
}
|
|
4398
|
+
});
|
|
4399
|
+
publishProviderEvent({
|
|
4400
|
+
threadId: chatId,
|
|
4401
|
+
type: "chat.updated",
|
|
4402
|
+
payload: serializeChat(updatedChat)
|
|
4403
|
+
});
|
|
4404
|
+
await publishMessageDeltas(chatId);
|
|
4405
|
+
publishProviderEvent({
|
|
4406
|
+
threadId: chatId,
|
|
4407
|
+
type: "run.status",
|
|
4408
|
+
payload: {
|
|
4409
|
+
runId,
|
|
4410
|
+
status: "FAILED",
|
|
4411
|
+
error: message
|
|
4412
|
+
}
|
|
4413
|
+
});
|
|
4414
|
+
await startNextQueuedRun(chatId);
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
async function startNextQueuedRun(chatId) {
|
|
4418
|
+
if (await hasRunningChatRunExcept(chatId, "")) return false;
|
|
4419
|
+
const queuedRun = sortQueuedRuns(await prisma.chatRun.findMany({
|
|
4420
|
+
orderBy: { createdAt: "asc" },
|
|
4421
|
+
where: {
|
|
4422
|
+
chatId,
|
|
4423
|
+
status: "QUEUED"
|
|
4424
|
+
}
|
|
4425
|
+
}))[0];
|
|
4426
|
+
if (!queuedRun) return false;
|
|
4427
|
+
const chat = await getChat(chatId);
|
|
4428
|
+
const accountId = queuedRun.accountId ?? chat.accountId;
|
|
4429
|
+
if (!accountId) {
|
|
4430
|
+
await prisma.chatRun.update({
|
|
4431
|
+
where: { id: queuedRun.id },
|
|
4432
|
+
data: {
|
|
4433
|
+
endedAt: /* @__PURE__ */ new Date(),
|
|
4434
|
+
error: "Queued message has no provider account.",
|
|
4435
|
+
status: "FAILED"
|
|
4436
|
+
}
|
|
4437
|
+
});
|
|
4438
|
+
await publishMessageDeltas(chatId);
|
|
4439
|
+
return startNextQueuedRun(chatId);
|
|
4440
|
+
}
|
|
4441
|
+
const request = readRunRequest(queuedRun);
|
|
4442
|
+
publishProviderEvent({
|
|
4443
|
+
threadId: chatId,
|
|
4444
|
+
type: "chat.updated",
|
|
4445
|
+
payload: serializeChat(await prisma.chat.update({
|
|
4446
|
+
where: { id: chatId },
|
|
4447
|
+
data: {
|
|
4448
|
+
accountId,
|
|
4449
|
+
collaborationMode: nullableString(request.collaborationMode) ?? chat.collaborationMode,
|
|
4450
|
+
lastActivityAt: /* @__PURE__ */ new Date(),
|
|
4451
|
+
permissionMode: nullableString(request.permissionMode) ?? chat.permissionMode,
|
|
4452
|
+
status: "RUNNING"
|
|
4453
|
+
}
|
|
4454
|
+
}))
|
|
4455
|
+
});
|
|
4456
|
+
runProviderTurn({
|
|
4457
|
+
accountId,
|
|
4458
|
+
attachments: request.attachments,
|
|
4459
|
+
chatId,
|
|
4460
|
+
content: request.content,
|
|
4461
|
+
goalObjective: request.goalObjective,
|
|
4462
|
+
model: readRunMetadataString(request.metadata, "model"),
|
|
4463
|
+
permissionMode: request.permissionMode,
|
|
4464
|
+
reasoningEffort: readRunMetadataString(request.metadata, "reasoningEffort"),
|
|
4465
|
+
requestedCollaborationMode: request.collaborationMode,
|
|
4466
|
+
runId: queuedRun.id,
|
|
4467
|
+
serviceTier: readRunMetadataString(request.metadata, "serviceTier")
|
|
4468
|
+
}).catch((error) => {
|
|
4469
|
+
console.error("Queued provider run failed.", error);
|
|
4470
|
+
});
|
|
4471
|
+
return true;
|
|
4472
|
+
}
|
|
4473
|
+
async function readRunPreviewMessages(chat, providerMessages) {
|
|
4474
|
+
const runs = await prisma.chatRun.findMany({
|
|
4475
|
+
orderBy: { createdAt: "asc" },
|
|
4476
|
+
where: {
|
|
4477
|
+
chatId: chat.id,
|
|
4478
|
+
status: { in: [
|
|
4479
|
+
"QUEUED",
|
|
4480
|
+
"RUNNING",
|
|
4481
|
+
"FAILED"
|
|
4482
|
+
] }
|
|
4483
|
+
}
|
|
4484
|
+
});
|
|
4485
|
+
const messages = [];
|
|
4486
|
+
for (const run of sortRunPreviewRuns(runs)) {
|
|
4487
|
+
const content = readRunRequestContent(run);
|
|
4488
|
+
if (!content) continue;
|
|
4489
|
+
const runUserIndex = findLastIndex(providerMessages, (message) => message.role === "USER" && message.content.trim() === content.trim());
|
|
4490
|
+
const assistantAfterRunUser = runUserIndex >= 0 ? providerMessages.slice(runUserIndex + 1).some((message) => message.role === "ASSISTANT") : false;
|
|
4491
|
+
if (runUserIndex < 0) messages.push({
|
|
4492
|
+
id: `run:${run.id}:user`,
|
|
4493
|
+
runId: run.id,
|
|
4494
|
+
role: "USER",
|
|
4495
|
+
content,
|
|
4496
|
+
createdAt: run.createdAt.toISOString(),
|
|
4497
|
+
status: run.status === "QUEUED" ? "PENDING" : "COMPLETED"
|
|
4498
|
+
});
|
|
4499
|
+
if (run.status !== "QUEUED" && !assistantAfterRunUser) {
|
|
4500
|
+
const failed = run.status === "FAILED";
|
|
4501
|
+
const timestamp = (run.endedAt ?? run.startedAt ?? run.createdAt).toISOString();
|
|
4502
|
+
messages.push({
|
|
4503
|
+
id: `run:${run.id}:assistant`,
|
|
4504
|
+
runId: run.id,
|
|
4505
|
+
role: "ASSISTANT",
|
|
4506
|
+
content: failed ? run.error ?? "Provider run failed." : "Running",
|
|
4507
|
+
createdAt: timestamp,
|
|
4508
|
+
completedAt: failed ? timestamp : null,
|
|
4509
|
+
kind: failed ? "ERROR" : "CHAT",
|
|
4510
|
+
status: failed ? "FAILED" : "STREAMING"
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
return messages;
|
|
4515
|
+
}
|
|
4516
|
+
function readRunRequestContent(run) {
|
|
4517
|
+
return readRunRequest(run).content || null;
|
|
4518
|
+
}
|
|
4519
|
+
function sortQueuedRuns(runs) {
|
|
4520
|
+
return [...runs].sort(compareQueuedRuns);
|
|
4521
|
+
}
|
|
4522
|
+
function sortRunPreviewRuns(runs) {
|
|
4523
|
+
return [...runs].sort((left, right) => {
|
|
4524
|
+
if (left.status === "QUEUED" && right.status === "QUEUED") return compareQueuedRuns(left, right);
|
|
4525
|
+
if (left.status === "QUEUED" || right.status === "QUEUED") return left.status === "QUEUED" ? 1 : -1;
|
|
4526
|
+
return left.createdAt.getTime() - right.createdAt.getTime();
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
function compareQueuedRuns(left, right) {
|
|
4530
|
+
const leftOrder = readQueuedRunOrder(left);
|
|
4531
|
+
const rightOrder = readQueuedRunOrder(right);
|
|
4532
|
+
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
|
4533
|
+
return left.createdAt.getTime() - right.createdAt.getTime();
|
|
4534
|
+
}
|
|
4535
|
+
function readQueuedRunOrder(run) {
|
|
4536
|
+
const order = readRunRequestMetadata(readRunRequestRecord(run).metadata).queueOrder;
|
|
4537
|
+
return typeof order === "number" && Number.isFinite(order) ? order : run.createdAt.getTime();
|
|
4538
|
+
}
|
|
4539
|
+
function runRequestFromDto(dto) {
|
|
4540
|
+
return {
|
|
4541
|
+
attachments: runRequestAttachments(dto.attachments),
|
|
4542
|
+
collaborationMode: dto.collaborationMode ?? null,
|
|
4543
|
+
content: dto.content,
|
|
4544
|
+
goalObjective: dto.goalObjective ?? null,
|
|
4545
|
+
metadata: dto.metadata ?? {},
|
|
4546
|
+
permissionMode: dto.permissionMode ?? null
|
|
4547
|
+
};
|
|
4548
|
+
}
|
|
4549
|
+
function runRequestAttachments(attachments) {
|
|
4550
|
+
return (attachments ?? []).map((attachment) => ({
|
|
4551
|
+
...attachment.dataUrl ? { dataUrl: attachment.dataUrl } : {},
|
|
4552
|
+
kind: attachment.kind,
|
|
4553
|
+
...attachment.mimeType ? { mimeType: attachment.mimeType } : {},
|
|
4554
|
+
name: attachment.name,
|
|
4555
|
+
...attachment.path ? { path: attachment.path } : {},
|
|
4556
|
+
...attachment.size !== void 0 ? { size: attachment.size } : {}
|
|
4557
|
+
}));
|
|
4558
|
+
}
|
|
4559
|
+
function readRunRequestRecord(run) {
|
|
4560
|
+
return run.request && typeof run.request === "object" && !Array.isArray(run.request) ? run.request : {};
|
|
4561
|
+
}
|
|
4562
|
+
function readRunRequest(run) {
|
|
4563
|
+
const request = readRunRequestRecord(run);
|
|
4564
|
+
return {
|
|
4565
|
+
attachments: readRunRequestAttachments(request.attachments),
|
|
4566
|
+
collaborationMode: readRequestString(request.collaborationMode),
|
|
4567
|
+
content: readRequestString(request.content) ?? "",
|
|
4568
|
+
goalObjective: readRequestString(request.goalObjective),
|
|
4569
|
+
metadata: readRunRequestMetadata(request.metadata),
|
|
4570
|
+
permissionMode: readRequestString(request.permissionMode)
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
function readRunRequestMetadata(value) {
|
|
4574
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4575
|
+
}
|
|
4576
|
+
function readRunMetadataString(value, key) {
|
|
4577
|
+
const field = readRunRequestMetadata(value)[key];
|
|
4578
|
+
return typeof field === "string" && field.trim() ? field.trim() : null;
|
|
4579
|
+
}
|
|
4580
|
+
function readRunRequestAttachments(value) {
|
|
4581
|
+
if (!Array.isArray(value)) return [];
|
|
4582
|
+
const attachments = [];
|
|
4583
|
+
for (const entry of value) {
|
|
4584
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
4585
|
+
const record = entry;
|
|
4586
|
+
const kind = record.kind;
|
|
4587
|
+
if (kind !== "file" && kind !== "folder" && kind !== "image") continue;
|
|
4588
|
+
attachments.push({
|
|
4589
|
+
dataUrl: readRequestString(record.dataUrl) ?? void 0,
|
|
4590
|
+
kind,
|
|
4591
|
+
mimeType: readRequestString(record.mimeType) ?? void 0,
|
|
4592
|
+
name: readRequestString(record.name) ?? "attachment",
|
|
4593
|
+
path: readRequestString(record.path) ?? void 0,
|
|
4594
|
+
size: typeof record.size === "number" && Number.isFinite(record.size) ? record.size : void 0
|
|
4595
|
+
});
|
|
4596
|
+
}
|
|
4597
|
+
return attachments;
|
|
4598
|
+
}
|
|
4599
|
+
function readRequestString(value) {
|
|
4600
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
4601
|
+
}
|
|
4602
|
+
function findLastIndex(items, predicate) {
|
|
4603
|
+
for (let index = items.length - 1; index >= 0; index -= 1) if (predicate(items[index])) return index;
|
|
4604
|
+
return -1;
|
|
4605
|
+
}
|
|
4606
|
+
function serializeRunMessage(chatId, run, role, content, status, sequence) {
|
|
4607
|
+
return serializeProviderMessage(chatId, {
|
|
4608
|
+
id: runMessageId(run, role),
|
|
4609
|
+
runId: run.id,
|
|
4610
|
+
role,
|
|
4611
|
+
content,
|
|
4612
|
+
createdAt: run.createdAt.toISOString(),
|
|
4613
|
+
completedAt: status === "COMPLETED" ? run.createdAt.toISOString() : null,
|
|
4614
|
+
status
|
|
4615
|
+
}, sequence);
|
|
4616
|
+
}
|
|
4617
|
+
function runMessageId(run, role) {
|
|
4618
|
+
return `run:${run.id}:${role.toLowerCase()}`;
|
|
4619
|
+
}
|
|
4620
|
+
function isCodexFunctionCallRequestId(requestId) {
|
|
4621
|
+
return /^call_[A-Za-z0-9_-]+$/u.test(requestId);
|
|
4622
|
+
}
|
|
4623
|
+
function serializeChat(chat, stats) {
|
|
4624
|
+
return {
|
|
4625
|
+
id: chat.id,
|
|
4626
|
+
providerId: chat.providerId,
|
|
4627
|
+
accountId: chat.accountId,
|
|
4628
|
+
autoRotateAccount: chat.autoRotateAccount,
|
|
4629
|
+
title: chat.title,
|
|
4630
|
+
workingDirectory: chat.workingDirectory,
|
|
4631
|
+
model: chat.model,
|
|
4632
|
+
reasoningEffort: chat.reasoningEffort,
|
|
4633
|
+
serviceTier: chat.serviceTier,
|
|
4634
|
+
stats: stats ?? null,
|
|
4635
|
+
collaborationMode: chat.collaborationMode,
|
|
4636
|
+
permissionMode: chat.permissionMode,
|
|
4637
|
+
status: chat.status,
|
|
4638
|
+
externalThreadId: chat.externalThreadId,
|
|
4639
|
+
lastActivityAt: chat.lastActivityAt.toISOString(),
|
|
4640
|
+
createdAt: chat.createdAt.toISOString(),
|
|
4641
|
+
updatedAt: chat.updatedAt.toISOString()
|
|
4642
|
+
};
|
|
4643
|
+
}
|
|
4644
|
+
function providerChatStatsKey(accountId, externalThreadId) {
|
|
4645
|
+
return `${accountId}:${externalThreadId}`;
|
|
4646
|
+
}
|
|
4647
|
+
function serializeProviderMessage(chatId, message, sequence) {
|
|
4648
|
+
const createdAt = parseProviderDate(message.createdAt) ?? /* @__PURE__ */ new Date();
|
|
4649
|
+
const status = message.status ?? "COMPLETED";
|
|
4650
|
+
return {
|
|
4651
|
+
id: message.id ?? `provider:${chatId}:${message.itemId ?? sequence}`,
|
|
4652
|
+
chatId,
|
|
4653
|
+
runId: message.runId,
|
|
4654
|
+
sequence,
|
|
4655
|
+
role: message.role,
|
|
4656
|
+
kind: message.kind ?? "CHAT",
|
|
4657
|
+
status,
|
|
4658
|
+
turnId: message.turnId ?? null,
|
|
4659
|
+
itemId: message.itemId,
|
|
4660
|
+
requestId: message.requestId ?? null,
|
|
4661
|
+
content: message.content,
|
|
4662
|
+
metadata: message.metadata ?? null,
|
|
4663
|
+
rawPayload: message.rawPayload ?? null,
|
|
4664
|
+
createdAt: createdAt.toISOString(),
|
|
4665
|
+
completedAt: message.completedAt ?? (status === "COMPLETED" || status === "FAILED" ? createdAt.toISOString() : null)
|
|
4666
|
+
};
|
|
4667
|
+
}
|
|
4668
|
+
function nullableString(value) {
|
|
4669
|
+
if (value === void 0) return;
|
|
4670
|
+
return value?.trim() || null;
|
|
4671
|
+
}
|
|
4672
|
+
function normalizeTitle(value) {
|
|
4673
|
+
return value?.trim() || null;
|
|
4674
|
+
}
|
|
4675
|
+
function parseProviderDate(value) {
|
|
4676
|
+
if (!value) return null;
|
|
4677
|
+
const date = new Date(value);
|
|
4678
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
4679
|
+
}
|
|
4680
|
+
function readErrorMessage(error) {
|
|
4681
|
+
return error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
4682
|
+
}
|
|
4683
|
+
//#endregion
|
|
4684
|
+
export { listProviderAdapters as A, readNumber as B, createAccount as C, readConnectedAccountLimits as D, listAccounts as E, resolveCodexAccountHome as F, ensureDatabase as H, startCodexMcpOauthLogin as I, updateCodexInstructions as L, readCodexHistoryWatchPaths as M, readCodexInstructions as N, requireConnectedAccount as O, reloadCodexMcpServerConfig as P, asJsonObject as R, authenticateAccount as S, listAccountModels as T, prisma as U, readString as V, serializeChat as _, executeMessage as a, updateChat as b, interruptChatRun as c, publishMessageDeltas as d, refreshChat as f, reviewChat as g, respondToServerRequest as h, deleteQueuedChatRun as i, listCodexMcpServerStatuses as j, updateAccount as k, listChats as l, reorderQueuedChatRuns as m, compactChat as n, forkChat as o, refreshChatStatusesForWorkspaces as p, createChat as r, getChat as s, archiveChat as t, listMessages as u, steerQueuedChatRun as v, deleteAccount as w, updateQueuedChatRun as x, syncProviderChatsOnce as y, readBoolean as z };
|