mesurer-solid 0.2.0-beta.9 → 0.2.0
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/AGENT_INTEGRATION.md +8 -13
- package/README.md +12 -20
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2278 -1989
- package/dist/inject-script.js +20 -14
- package/dist/inject.js +2273 -1984
- package/dist/plugin-catalog.d.ts +5 -1
- package/dist/{codex-plugin.d.ts → plugins/codex/index.d.ts} +17 -19
- package/dist/plugins.d.ts +3 -3
- package/dist/plugins.js +1386 -1100
- package/package.json +8 -9
- package/plugins/codex/bridge.d.ts +63 -0
- package/plugins/codex/bridge.mjs +1304 -0
- package/plugins/codex/desktop.mjs +155 -0
- package/scripts/install-skill.mjs +1 -10
- package/skills/mesurer-ui/SKILL.md +12 -29
- package/codex/codex-bridge.mjs +0 -2037
- package/codex/codex-connect.mjs +0 -324
- package/codex/codex-lifecycle.mjs +0 -51
- package/scripts/codex-bridge.mjs +0 -2
- package/scripts/codex-connect.mjs +0 -2
- package/scripts/codex-lifecycle.mjs +0 -2
|
@@ -0,0 +1,1304 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { constants as fsConstants } from "node:fs";
|
|
4
|
+
import { access, mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createConnection } from "node:net";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { delimiter, join } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
desktopSessionFromEnvironment,
|
|
10
|
+
openDesktopThread,
|
|
11
|
+
queueDesktopThread,
|
|
12
|
+
} from "./desktop.mjs";
|
|
13
|
+
|
|
14
|
+
const APP_SERVER_TIMEOUT_MS = 10_000;
|
|
15
|
+
|
|
16
|
+
const DAEMON_START_TIMEOUT_MS = 10_000;
|
|
17
|
+
|
|
18
|
+
const MAX_DISCOVERED_THREADS = 10;
|
|
19
|
+
|
|
20
|
+
const TURN_HISTORY_LIMIT = 100;
|
|
21
|
+
|
|
22
|
+
const DELIVERY_TTL_MS = 24 * 60 * 60_000;
|
|
23
|
+
|
|
24
|
+
const TERMINAL_DELIVERY_TTL_MS = 10 * 60_000;
|
|
25
|
+
|
|
26
|
+
const DELIVERY_TURN_START_SKEW_MS = 5_000;
|
|
27
|
+
|
|
28
|
+
const MAX_DELIVERIES = 100;
|
|
29
|
+
|
|
30
|
+
const deliveries = new Map();
|
|
31
|
+
|
|
32
|
+
let deliveryStateLoadPromise = null;
|
|
33
|
+
|
|
34
|
+
let loadedDeliveryStatePath = null;
|
|
35
|
+
|
|
36
|
+
const normalizeString = (value) => {
|
|
37
|
+
if (value?.constructor !== String) return null;
|
|
38
|
+
|
|
39
|
+
const text = value.trim();
|
|
40
|
+
|
|
41
|
+
return text || null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const normalizeTitle = (value) => {
|
|
45
|
+
const title = normalizeString(value)?.replace(/\s+/g, " ");
|
|
46
|
+
|
|
47
|
+
if (!title) return null;
|
|
48
|
+
|
|
49
|
+
return title.length > 80 ? `${title.slice(0, 79)}…` : title;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const normalizeTimestamp = (value) => Number.isFinite(value) ? Number(value) : null;
|
|
53
|
+
|
|
54
|
+
const shortThread = (thread) => thread.length > 16
|
|
55
|
+
? `${thread.slice(0, 8)}…${thread.slice(-4)}`
|
|
56
|
+
: thread;
|
|
57
|
+
|
|
58
|
+
const codexHome = (options) =>
|
|
59
|
+
options.codexHome?.trim()
|
|
60
|
+
|| process.env.CODEX_HOME?.trim()
|
|
61
|
+
|| join(homedir(), ".codex");
|
|
62
|
+
|
|
63
|
+
const explicitCodexBin = (options) => options.codex?.trim() || null;
|
|
64
|
+
|
|
65
|
+
const environmentCodexBin = () => process.env.CODEX_BIN?.trim() || null;
|
|
66
|
+
|
|
67
|
+
const codexExecutableName = () => process.platform === "win32" ? "codex.exe" : "codex";
|
|
68
|
+
|
|
69
|
+
const packagedCodexBins = (options) => {
|
|
70
|
+
const home = codexHome(options);
|
|
71
|
+
const executable = codexExecutableName();
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
join(home, "packages", "app-server-daemon", "current", "bin", executable),
|
|
75
|
+
join(home, "packages", "standalone", "current", executable),
|
|
76
|
+
join(home, "packages", "standalone", "current", "bin", executable),
|
|
77
|
+
];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const canExecute = async (path) => {
|
|
81
|
+
try {
|
|
82
|
+
await access(
|
|
83
|
+
path,
|
|
84
|
+
process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const resolvePathCommand = async (command) => {
|
|
94
|
+
const pathValue = process.env.PATH?.trim();
|
|
95
|
+
|
|
96
|
+
if (!pathValue) return null;
|
|
97
|
+
|
|
98
|
+
const executableNames = process.platform === "win32"
|
|
99
|
+
? command.endsWith(".exe") || command.endsWith(".com")
|
|
100
|
+
? [command]
|
|
101
|
+
: [`${command}.exe`, `${command}.com`]
|
|
102
|
+
: [command];
|
|
103
|
+
|
|
104
|
+
for (const directory of pathValue.split(delimiter)) {
|
|
105
|
+
const root = directory.trim().replace(/^"(.*)"$/, "$1");
|
|
106
|
+
|
|
107
|
+
if (!root) continue;
|
|
108
|
+
|
|
109
|
+
for (const executable of executableNames) {
|
|
110
|
+
const candidate = join(root, executable);
|
|
111
|
+
|
|
112
|
+
if (!(await canExecute(candidate))) continue;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
return await realpath(candidate);
|
|
116
|
+
} catch {
|
|
117
|
+
return candidate;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const desktopBundledCodex = (path) => {
|
|
126
|
+
const value = path.replaceAll("\\", "/").toLowerCase();
|
|
127
|
+
|
|
128
|
+
if (value.includes(".app/contents/resources/")) return true;
|
|
129
|
+
|
|
130
|
+
if (value.includes("/microsoft/windowsapps/codex.exe")) return true;
|
|
131
|
+
|
|
132
|
+
return value.includes("/windowsapps/")
|
|
133
|
+
&& (value.includes("openai.codex") || value.includes("chatgpt"));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const installedDesktopCodexBins = () => {
|
|
137
|
+
if (process.platform === "darwin") {
|
|
138
|
+
return [
|
|
139
|
+
"/Applications/Codex.app/Contents/Resources/codex",
|
|
140
|
+
"/Applications/ChatGPT.app/Contents/Resources/codex",
|
|
141
|
+
join(homedir(), "Applications", "Codex.app", "Contents", "Resources", "codex"),
|
|
142
|
+
join(homedir(), "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (process.platform === "win32") {
|
|
147
|
+
const localAppData = process.env.LOCALAPPDATA?.trim();
|
|
148
|
+
|
|
149
|
+
return localAppData
|
|
150
|
+
? [join(localAppData, "Microsoft", "WindowsApps", "codex.exe")]
|
|
151
|
+
: [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return [];
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const publicRuntime = (source, transport, available, reason = null) => ({
|
|
158
|
+
source,
|
|
159
|
+
transport,
|
|
160
|
+
available,
|
|
161
|
+
reason,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const resolveCommand = async (command) => {
|
|
165
|
+
if (!command) return null;
|
|
166
|
+
|
|
167
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
168
|
+
return realpath(command).catch(() => command);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return resolvePathCommand(command);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const bootstrapRuntime = async (options) => {
|
|
175
|
+
const explicit = await resolveCommand(explicitCodexBin(options));
|
|
176
|
+
|
|
177
|
+
if (explicit && await canExecute(explicit)) {
|
|
178
|
+
if (desktopBundledCodex(explicit)) {
|
|
179
|
+
return {
|
|
180
|
+
...publicRuntime("desktop", "private-stdio", false, "desktop-private-transport"),
|
|
181
|
+
command: explicit,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...publicRuntime("standalone", "shared-app-server", true),
|
|
187
|
+
command: explicit,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const candidate of packagedCodexBins(options)) {
|
|
192
|
+
if (await canExecute(candidate)) {
|
|
193
|
+
return {
|
|
194
|
+
...publicRuntime("standalone", "shared-app-server", true),
|
|
195
|
+
command: candidate,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const environment = await resolveCommand(environmentCodexBin());
|
|
201
|
+
|
|
202
|
+
if (environment) {
|
|
203
|
+
if (desktopBundledCodex(environment)) {
|
|
204
|
+
return {
|
|
205
|
+
...publicRuntime("desktop", "private-stdio", false, "desktop-private-transport"),
|
|
206
|
+
command: environment,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
...publicRuntime("standalone", "shared-app-server", true),
|
|
212
|
+
command: environment,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const fromPath = await resolvePathCommand("codex");
|
|
217
|
+
|
|
218
|
+
if (fromPath) {
|
|
219
|
+
if (desktopBundledCodex(fromPath)) {
|
|
220
|
+
return {
|
|
221
|
+
...publicRuntime("desktop", "private-stdio", false, "desktop-private-transport"),
|
|
222
|
+
command: fromPath,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
...publicRuntime("standalone", "shared-app-server", true),
|
|
228
|
+
command: fromPath,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const candidate of installedDesktopCodexBins()) {
|
|
233
|
+
if (await canExecute(candidate)) {
|
|
234
|
+
return {
|
|
235
|
+
...publicRuntime("desktop", "private-stdio", false, "desktop-private-transport"),
|
|
236
|
+
command: candidate,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
...publicRuntime("none", "none", false, "runtime-not-found"),
|
|
243
|
+
command: null,
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const sharedSocketAvailable = (options) => new Promise((resolve) => {
|
|
248
|
+
const socket = createConnection(controlSocketPath(options));
|
|
249
|
+
let settled = false;
|
|
250
|
+
|
|
251
|
+
const finish = (available) => {
|
|
252
|
+
if (settled) return;
|
|
253
|
+
settled = true;
|
|
254
|
+
socket.destroy();
|
|
255
|
+
resolve(available);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const timeout = setTimeout(() => finish(false), 250);
|
|
259
|
+
|
|
260
|
+
socket.once("connect", () => {
|
|
261
|
+
clearTimeout(timeout);
|
|
262
|
+
finish(true);
|
|
263
|
+
});
|
|
264
|
+
socket.once("error", () => {
|
|
265
|
+
clearTimeout(timeout);
|
|
266
|
+
finish(false);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const desktopTransport = async (options) => {
|
|
271
|
+
const session = desktopSessionFromEnvironment();
|
|
272
|
+
|
|
273
|
+
if (!session) return null;
|
|
274
|
+
|
|
275
|
+
const runtime = await bootstrapRuntime(options);
|
|
276
|
+
|
|
277
|
+
if (!runtime.command) return null;
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
session,
|
|
281
|
+
command: runtime.command,
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const inspectRuntime = async (options) => {
|
|
286
|
+
if (await desktopTransport(options)) {
|
|
287
|
+
return publicRuntime("desktop", "desktop-queue", true);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (await sharedSocketAvailable(options)) {
|
|
291
|
+
return publicRuntime("shared", "shared-app-server", true);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const runtime = await bootstrapRuntime(options);
|
|
295
|
+
|
|
296
|
+
return publicRuntime(
|
|
297
|
+
runtime.source,
|
|
298
|
+
runtime.transport,
|
|
299
|
+
runtime.available,
|
|
300
|
+
runtime.reason,
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const codexEnv = (options) => {
|
|
305
|
+
const env = { ...process.env };
|
|
306
|
+
const home = options.codexHome?.trim();
|
|
307
|
+
|
|
308
|
+
if (home) env.CODEX_HOME = home;
|
|
309
|
+
|
|
310
|
+
return env;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const controlSocketPath = (options) =>
|
|
314
|
+
join(codexHome(options), "app-server-control", "app-server-control.sock");
|
|
315
|
+
|
|
316
|
+
const deliveryStatePath = (options) =>
|
|
317
|
+
join(codexHome(options), "mesurer", "codex-deliveries.json");
|
|
318
|
+
|
|
319
|
+
const missingDaemonSocket = (cause) => {
|
|
320
|
+
if (!(cause instanceof Error)) return false;
|
|
321
|
+
|
|
322
|
+
return cause.code === "ENOENT"
|
|
323
|
+
|| cause.code === "ECONNREFUSED"
|
|
324
|
+
|| cause.message.includes("No such file or directory")
|
|
325
|
+
|| cause.message.includes("ENOENT")
|
|
326
|
+
|| cause.message.includes("ECONNREFUSED")
|
|
327
|
+
|| cause.message.includes("os error 2");
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const spawnCommand = (
|
|
331
|
+
command,
|
|
332
|
+
args,
|
|
333
|
+
options,
|
|
334
|
+
timeoutMs,
|
|
335
|
+
label,
|
|
336
|
+
) => new Promise((resolve, reject) => {
|
|
337
|
+
const child = spawn(command, args, {
|
|
338
|
+
env: codexEnv(options),
|
|
339
|
+
shell: false,
|
|
340
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
341
|
+
windowsHide: true,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
let stdout = "";
|
|
345
|
+
let stderr = "";
|
|
346
|
+
let settled = false;
|
|
347
|
+
|
|
348
|
+
const timeout = setTimeout(() => {
|
|
349
|
+
if (child.exitCode === null) child.kill("SIGTERM");
|
|
350
|
+
|
|
351
|
+
if (settled) return;
|
|
352
|
+
settled = true;
|
|
353
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms.`));
|
|
354
|
+
}, timeoutMs);
|
|
355
|
+
|
|
356
|
+
const finish = (error, output) => {
|
|
357
|
+
if (settled) return;
|
|
358
|
+
settled = true;
|
|
359
|
+
clearTimeout(timeout);
|
|
360
|
+
|
|
361
|
+
if (error) reject(error);
|
|
362
|
+
else resolve(output);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
366
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
367
|
+
child.on("error", (error) => finish(error));
|
|
368
|
+
child.on("close", (code, signal) => {
|
|
369
|
+
if (code === 0) {
|
|
370
|
+
finish(null, stdout.trim());
|
|
371
|
+
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const detail = stderr.trim()
|
|
376
|
+
|| stdout.trim()
|
|
377
|
+
|| `exit code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`;
|
|
378
|
+
|
|
379
|
+
finish(new Error(`${label} failed: ${detail}`));
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const startDaemon = async (options) => {
|
|
384
|
+
const runtime = await bootstrapRuntime(options);
|
|
385
|
+
|
|
386
|
+
const command = runtime.available && runtime.source === "standalone"
|
|
387
|
+
? runtime.command
|
|
388
|
+
: null;
|
|
389
|
+
|
|
390
|
+
if (!command) {
|
|
391
|
+
if (runtime.source === "desktop") {
|
|
392
|
+
throw new Error(
|
|
393
|
+
"Codex shared app-server is not running. Codex Desktop is installed, but its private app-server is not exposed through the shared local socket.",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
throw new Error(
|
|
398
|
+
"Codex shared app-server is not running. Mesurer found no standalone Codex installation that can start it.",
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
return await spawnCommand(
|
|
404
|
+
command,
|
|
405
|
+
["app-server", "daemon", "start"],
|
|
406
|
+
options,
|
|
407
|
+
DAEMON_START_TIMEOUT_MS,
|
|
408
|
+
"Codex app-server daemon start",
|
|
409
|
+
);
|
|
410
|
+
} catch (cause) {
|
|
411
|
+
if (
|
|
412
|
+
cause instanceof Error
|
|
413
|
+
&& (
|
|
414
|
+
cause.message.includes("no complete local package")
|
|
415
|
+
|| cause.message.includes("install a packaged Codex CLI")
|
|
416
|
+
|| cause.message.includes("standalone installer")
|
|
417
|
+
)
|
|
418
|
+
) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
"Codex shared app-server is not running. The discovered Codex executable cannot provide a standalone daemon, so Mesurer did not start another Codex server. Codex Desktop's private app-server is not exposed through the shared local socket.",
|
|
421
|
+
{ cause },
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
throw cause;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
430
|
+
|
|
431
|
+
const daemonRequestOnce = (
|
|
432
|
+
method,
|
|
433
|
+
params,
|
|
434
|
+
options,
|
|
435
|
+
experimentalApi = true,
|
|
436
|
+
) => new Promise((resolve, reject) => {
|
|
437
|
+
const socket = createConnection(controlSocketPath(options));
|
|
438
|
+
const requestId = `mesurer-codex-${randomUUID()}`;
|
|
439
|
+
let buffer = "";
|
|
440
|
+
let settled = false;
|
|
441
|
+
|
|
442
|
+
const timeout = setTimeout(() => {
|
|
443
|
+
if (settled) return;
|
|
444
|
+
settled = true;
|
|
445
|
+
socket.destroy();
|
|
446
|
+
reject(new Error(`Codex app-server ${method} timed out after ${APP_SERVER_TIMEOUT_MS}ms.`));
|
|
447
|
+
}, APP_SERVER_TIMEOUT_MS);
|
|
448
|
+
|
|
449
|
+
const finish = (error, result) => {
|
|
450
|
+
if (settled) return;
|
|
451
|
+
settled = true;
|
|
452
|
+
clearTimeout(timeout);
|
|
453
|
+
socket.destroy();
|
|
454
|
+
|
|
455
|
+
if (error) reject(error);
|
|
456
|
+
else resolve(result);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const send = (message) => {
|
|
460
|
+
socket.write(`${JSON.stringify(message)}\n`);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const handleMessage = (message) => {
|
|
464
|
+
if (message?.id === `${requestId}-initialize`) {
|
|
465
|
+
if (message.error) {
|
|
466
|
+
finish(new Error(message.error.message || "Codex app-server initialize failed."));
|
|
467
|
+
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
send({ method: "initialized" });
|
|
472
|
+
send({ id: requestId, method, params });
|
|
473
|
+
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (message?.id !== requestId) return;
|
|
478
|
+
|
|
479
|
+
if (message.error) {
|
|
480
|
+
finish(new Error(message.error.message || `Codex app-server ${method} failed.`));
|
|
481
|
+
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
finish(null, message.result ?? {});
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
socket.setEncoding("utf8");
|
|
489
|
+
socket.on("connect", () => {
|
|
490
|
+
send({
|
|
491
|
+
id: `${requestId}-initialize`,
|
|
492
|
+
method: "initialize",
|
|
493
|
+
params: {
|
|
494
|
+
clientInfo: {
|
|
495
|
+
name: "mesurer-solid",
|
|
496
|
+
title: "Mesurer Solid",
|
|
497
|
+
version: "1",
|
|
498
|
+
},
|
|
499
|
+
capabilities: {
|
|
500
|
+
experimentalApi,
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
socket.on("data", (chunk) => {
|
|
507
|
+
buffer += chunk;
|
|
508
|
+
|
|
509
|
+
while (true) {
|
|
510
|
+
const newline = buffer.indexOf("\n");
|
|
511
|
+
|
|
512
|
+
if (newline < 0) break;
|
|
513
|
+
const line = buffer.slice(0, newline).trim();
|
|
514
|
+
buffer = buffer.slice(newline + 1);
|
|
515
|
+
|
|
516
|
+
if (!line) continue;
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
handleMessage(JSON.parse(line));
|
|
520
|
+
} catch {
|
|
521
|
+
// Codex app-server uses JSONL. Ignore unrelated malformed frames defensively.
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
socket.on("error", (error) => finish(error));
|
|
527
|
+
socket.on("close", () => {
|
|
528
|
+
if (!settled) finish(new Error(`Codex app-server socket closed during ${method}.`));
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const daemonRequest = async (
|
|
533
|
+
method,
|
|
534
|
+
params = {},
|
|
535
|
+
options = {},
|
|
536
|
+
experimentalApi = true,
|
|
537
|
+
) => {
|
|
538
|
+
try {
|
|
539
|
+
return await daemonRequestOnce(method, params, options, experimentalApi);
|
|
540
|
+
} catch (cause) {
|
|
541
|
+
if (!missingDaemonSocket(cause)) throw cause;
|
|
542
|
+
await startDaemon(options);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const deadline = Date.now() + DAEMON_START_TIMEOUT_MS;
|
|
546
|
+
let lastError = null;
|
|
547
|
+
|
|
548
|
+
while (Date.now() < deadline) {
|
|
549
|
+
try {
|
|
550
|
+
return await daemonRequestOnce(method, params, options, experimentalApi);
|
|
551
|
+
} catch (cause) {
|
|
552
|
+
if (!missingDaemonSocket(cause)) throw cause;
|
|
553
|
+
lastError = cause;
|
|
554
|
+
await sleep(50);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
throw lastError ?? new Error("Codex app-server daemon did not become available.");
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const loadedThreadIds = async (options) => {
|
|
562
|
+
const result = await daemonRequest(
|
|
563
|
+
"thread/loaded/list",
|
|
564
|
+
{ limit: MAX_DISCOVERED_THREADS },
|
|
565
|
+
options,
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
return Array.isArray(result?.data)
|
|
569
|
+
? result.data.map(normalizeString).filter(Boolean)
|
|
570
|
+
: [];
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const threadSummary = (thread, loadedIds) => {
|
|
574
|
+
const id = normalizeString(thread?.id);
|
|
575
|
+
|
|
576
|
+
if (!id || !loadedIds.has(id)) return null;
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
id,
|
|
580
|
+
title: normalizeTitle(thread?.name)
|
|
581
|
+
?? normalizeTitle(thread?.preview)
|
|
582
|
+
?? `Codex ${shortThread(id)}`,
|
|
583
|
+
updatedAt: normalizeTimestamp(thread?.recencyAt ?? thread?.updatedAt),
|
|
584
|
+
connected: true,
|
|
585
|
+
};
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const listLoadedThreads = async (options) => {
|
|
589
|
+
const ids = await loadedThreadIds(options);
|
|
590
|
+
const loadedIds = new Set(ids);
|
|
591
|
+
const byId = new Map();
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const result = await daemonRequest(
|
|
595
|
+
"thread/list",
|
|
596
|
+
{
|
|
597
|
+
limit: Math.max(MAX_DISCOVERED_THREADS * 4, 40),
|
|
598
|
+
sortKey: "recency_at",
|
|
599
|
+
sortDirection: "desc",
|
|
600
|
+
},
|
|
601
|
+
options,
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
for (const thread of Array.isArray(result?.data) ? result.data : []) {
|
|
605
|
+
const summary = threadSummary(thread, loadedIds);
|
|
606
|
+
|
|
607
|
+
if (summary) byId.set(summary.id, summary);
|
|
608
|
+
}
|
|
609
|
+
} catch {
|
|
610
|
+
// Loaded-thread discovery remains authoritative if metadata listing fails.
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
for (const id of ids) {
|
|
614
|
+
if (byId.has(id)) continue;
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const result = await daemonRequest(
|
|
618
|
+
"thread/read",
|
|
619
|
+
{ threadId: id, includeTurns: false },
|
|
620
|
+
options,
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
const summary = threadSummary(result?.thread, loadedIds);
|
|
624
|
+
|
|
625
|
+
if (summary) byId.set(id, summary);
|
|
626
|
+
} catch {
|
|
627
|
+
byId.set(id, {
|
|
628
|
+
id,
|
|
629
|
+
title: `Codex ${shortThread(id)}`,
|
|
630
|
+
updatedAt: null,
|
|
631
|
+
connected: true,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return ids
|
|
637
|
+
.map((id) => byId.get(id))
|
|
638
|
+
.filter(Boolean)
|
|
639
|
+
.sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const publicDelivery = (delivery) => ({
|
|
643
|
+
deliveryId: delivery.id,
|
|
644
|
+
thread: delivery.thread,
|
|
645
|
+
status: delivery.status,
|
|
646
|
+
turnId: delivery.turnId,
|
|
647
|
+
queuedSubmissionId: delivery.queuedSubmissionId,
|
|
648
|
+
dispatch: delivery.dispatch,
|
|
649
|
+
dispatchError: delivery.dispatchError,
|
|
650
|
+
createdAt: delivery.createdAt,
|
|
651
|
+
updatedAt: delivery.updatedAt,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const persistedDelivery = (delivery) => ({
|
|
655
|
+
id: delivery.id,
|
|
656
|
+
thread: delivery.thread,
|
|
657
|
+
message: delivery.message,
|
|
658
|
+
messageHash: delivery.messageHash,
|
|
659
|
+
status: delivery.status,
|
|
660
|
+
turnId: delivery.turnId,
|
|
661
|
+
queuedSubmissionId: delivery.queuedSubmissionId,
|
|
662
|
+
transport: delivery.transport,
|
|
663
|
+
dispatch: delivery.dispatch,
|
|
664
|
+
dispatchError: delivery.dispatchError,
|
|
665
|
+
createdAt: delivery.createdAt,
|
|
666
|
+
updatedAt: delivery.updatedAt,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const pruneDeliveries = () => {
|
|
670
|
+
const now = Date.now();
|
|
671
|
+
|
|
672
|
+
for (const [id, delivery] of deliveries) {
|
|
673
|
+
const terminal = delivery.status === "completed" || delivery.status === "interrupted";
|
|
674
|
+
const ttl = terminal ? TERMINAL_DELIVERY_TTL_MS : DELIVERY_TTL_MS;
|
|
675
|
+
|
|
676
|
+
if (now - delivery.updatedAt > ttl) deliveries.delete(id);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (deliveries.size <= MAX_DELIVERIES) return;
|
|
680
|
+
|
|
681
|
+
const oldest = [...deliveries.values()].sort((left, right) => left.updatedAt - right.updatedAt);
|
|
682
|
+
|
|
683
|
+
for (const delivery of oldest.slice(0, deliveries.size - MAX_DELIVERIES)) {
|
|
684
|
+
deliveries.delete(delivery.id);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const persistDeliveries = async (options) => {
|
|
689
|
+
pruneDeliveries();
|
|
690
|
+
const path = deliveryStatePath(options);
|
|
691
|
+
const directory = join(path, "..");
|
|
692
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
693
|
+
|
|
694
|
+
const payload = {
|
|
695
|
+
version: 1,
|
|
696
|
+
deliveries: [...deliveries.values()].map(persistedDelivery),
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
await mkdir(directory, { recursive: true });
|
|
700
|
+
await writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
701
|
+
await rename(tmp, path);
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const loadDeliveries = async (options) => {
|
|
705
|
+
try {
|
|
706
|
+
const raw = await readFile(deliveryStatePath(options), "utf8");
|
|
707
|
+
const parsed = JSON.parse(raw);
|
|
708
|
+
|
|
709
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.deliveries)) return;
|
|
710
|
+
|
|
711
|
+
for (const value of parsed.deliveries) {
|
|
712
|
+
const id = normalizeString(value?.id);
|
|
713
|
+
const thread = normalizeString(value?.thread);
|
|
714
|
+
const message = normalizeString(value?.message);
|
|
715
|
+
const messageHash = normalizeString(value?.messageHash);
|
|
716
|
+
const status = normalizeString(value?.status);
|
|
717
|
+
|
|
718
|
+
if (!id || !thread || !message || !messageHash) continue;
|
|
719
|
+
|
|
720
|
+
if (!["queued", "working", "completed", "interrupted"].includes(status)) continue;
|
|
721
|
+
|
|
722
|
+
deliveries.set(id, {
|
|
723
|
+
id,
|
|
724
|
+
thread,
|
|
725
|
+
message,
|
|
726
|
+
messageHash,
|
|
727
|
+
status,
|
|
728
|
+
turnId: normalizeString(value?.turnId),
|
|
729
|
+
queuedSubmissionId: normalizeString(value?.queuedSubmissionId),
|
|
730
|
+
transport: value?.transport === "desktop-queue" ? "desktop-queue" : "shared-app-server",
|
|
731
|
+
dispatch: value?.transport === "desktop-queue"
|
|
732
|
+
? normalizeString(value?.dispatch) ?? "persisted"
|
|
733
|
+
: "persisted",
|
|
734
|
+
dispatchError: normalizeString(value?.dispatchError),
|
|
735
|
+
createdAt: Number.isFinite(value?.createdAt) ? Number(value.createdAt) : Date.now(),
|
|
736
|
+
updatedAt: Number.isFinite(value?.updatedAt) ? Number(value.updatedAt) : Date.now(),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
pruneDeliveries();
|
|
741
|
+
} catch (cause) {
|
|
742
|
+
if (cause?.code !== "ENOENT") throw cause;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const ensureDeliveriesLoaded = (options) => {
|
|
747
|
+
const path = deliveryStatePath(options);
|
|
748
|
+
|
|
749
|
+
if (loadedDeliveryStatePath !== path) {
|
|
750
|
+
deliveries.clear();
|
|
751
|
+
deliveryStateLoadPromise = null;
|
|
752
|
+
loadedDeliveryStatePath = path;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (!deliveryStateLoadPromise) {
|
|
756
|
+
deliveryStateLoadPromise = loadDeliveries(options).catch((cause) => {
|
|
757
|
+
deliveryStateLoadPromise = null;
|
|
758
|
+
throw cause;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return deliveryStateLoadPromise;
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
const hashMessage = (message) => createHash("sha256").update(message).digest("hex");
|
|
766
|
+
|
|
767
|
+
const turnUserMessages = (turn) => {
|
|
768
|
+
if (!Array.isArray(turn?.items)) return [];
|
|
769
|
+
|
|
770
|
+
return turn.items.flatMap((item) => {
|
|
771
|
+
if (item?.type !== "userMessage" || !Array.isArray(item.content)) return [];
|
|
772
|
+
|
|
773
|
+
const text = item.content
|
|
774
|
+
.filter((input) => input?.type === "text")
|
|
775
|
+
.map((input) => String(input.text ?? ""))
|
|
776
|
+
.join("\n")
|
|
777
|
+
.trim();
|
|
778
|
+
|
|
779
|
+
return text ? [text] : [];
|
|
780
|
+
});
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
const readTurnHistory = async (thread, options) => {
|
|
784
|
+
try {
|
|
785
|
+
const result = await daemonRequest(
|
|
786
|
+
"thread/turns/list",
|
|
787
|
+
{
|
|
788
|
+
threadId: thread,
|
|
789
|
+
limit: TURN_HISTORY_LIMIT,
|
|
790
|
+
sortDirection: "desc",
|
|
791
|
+
itemsView: "summary",
|
|
792
|
+
},
|
|
793
|
+
options,
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
return Array.isArray(result?.data) ? result.data : [];
|
|
797
|
+
} catch {
|
|
798
|
+
const result = await daemonRequest(
|
|
799
|
+
"thread/read",
|
|
800
|
+
{ threadId: thread, includeTurns: true },
|
|
801
|
+
options,
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
return Array.isArray(result?.thread?.turns) ? result.thread.turns : [];
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const matchingTurn = (delivery, turns) => {
|
|
809
|
+
if (delivery.turnId) {
|
|
810
|
+
return turns.find((turn) => normalizeString(turn?.id) === delivery.turnId) ?? null;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const earliestStartedAt = delivery.createdAt - DELIVERY_TURN_START_SKEW_MS;
|
|
814
|
+
|
|
815
|
+
const matches = turns.filter((turn) => {
|
|
816
|
+
const id = normalizeString(turn?.id);
|
|
817
|
+
const startedAt = Number(turn?.startedAt);
|
|
818
|
+
|
|
819
|
+
if (!id || !Number.isFinite(startedAt) || startedAt * 1_000 < earliestStartedAt) {
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return turnUserMessages(turn).some((prompt) =>
|
|
824
|
+
hashMessage(prompt) === delivery.messageHash
|
|
825
|
+
|| prompt.includes(delivery.message));
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
return matches.length === 1 ? matches[0] : null;
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const reconcileDelivery = async (delivery, options) => {
|
|
832
|
+
if (delivery.status === "completed" || delivery.transport === "desktop-queue") return;
|
|
833
|
+
|
|
834
|
+
const turns = await readTurnHistory(delivery.thread, options);
|
|
835
|
+
const turn = matchingTurn(delivery, turns);
|
|
836
|
+
|
|
837
|
+
if (!turn) return;
|
|
838
|
+
|
|
839
|
+
const turnId = normalizeString(turn.id);
|
|
840
|
+
const ended = turn.completedAt != null && Number.isFinite(Number(turn.completedAt));
|
|
841
|
+
let nextStatus = null;
|
|
842
|
+
|
|
843
|
+
if (turn.status === "inProgress") nextStatus = "working";
|
|
844
|
+
|
|
845
|
+
if (turn.status === "interrupted" && !ended) nextStatus = "working";
|
|
846
|
+
|
|
847
|
+
if (turn.status === "completed") nextStatus = "completed";
|
|
848
|
+
|
|
849
|
+
if ((turn.status === "interrupted" && ended) || turn.status === "failed") {
|
|
850
|
+
nextStatus = "interrupted";
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (!turnId || !nextStatus) return;
|
|
854
|
+
|
|
855
|
+
const failed = turn.status === "failed";
|
|
856
|
+
|
|
857
|
+
const failureMessage = normalizeString(turn?.error?.message) ?? "Codex turn failed.";
|
|
858
|
+
|
|
859
|
+
const changed = delivery.turnId !== turnId
|
|
860
|
+
|| delivery.status !== nextStatus
|
|
861
|
+
|| (failed && delivery.dispatchError !== failureMessage);
|
|
862
|
+
|
|
863
|
+
if (!changed) return;
|
|
864
|
+
|
|
865
|
+
delivery.turnId = turnId;
|
|
866
|
+
delivery.status = nextStatus;
|
|
867
|
+
|
|
868
|
+
if (failed) delivery.dispatchError = failureMessage;
|
|
869
|
+
|
|
870
|
+
delivery.updatedAt = Date.now();
|
|
871
|
+
await persistDeliveries(options);
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const queueLookup = async (thread, queuedSubmissionId, options) => {
|
|
875
|
+
let cursor = null;
|
|
876
|
+
|
|
877
|
+
while (true) {
|
|
878
|
+
const params = {
|
|
879
|
+
threadId: thread,
|
|
880
|
+
limit: queuedSubmissionId ? 100 : 2,
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
if (cursor) params.cursor = cursor;
|
|
884
|
+
|
|
885
|
+
const result = await daemonRequest("thread/queue/list", params, options);
|
|
886
|
+
const data = Array.isArray(result?.data) ? result.data : [];
|
|
887
|
+
const nextCursor = normalizeString(result?.nextCursor);
|
|
888
|
+
|
|
889
|
+
if (queuedSubmissionId) {
|
|
890
|
+
const submission = data.find((candidate) => candidate?.id === queuedSubmissionId);
|
|
891
|
+
|
|
892
|
+
if (submission) return { submission, ambiguous: false };
|
|
893
|
+
|
|
894
|
+
if (nextCursor) {
|
|
895
|
+
cursor = nextCursor;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return { submission: null, ambiguous: false };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (data.length === 1 && !nextCursor) {
|
|
903
|
+
return { submission: data[0], ambiguous: false };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return {
|
|
907
|
+
submission: null,
|
|
908
|
+
ambiguous: data.length > 1 || Boolean(nextCursor),
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
const queuedMessage = (submission) => {
|
|
914
|
+
if (!Array.isArray(submission?.input) || submission.input.length !== 1) return null;
|
|
915
|
+
|
|
916
|
+
const input = submission.input[0];
|
|
917
|
+
const text = normalizeString(input?.text);
|
|
918
|
+
|
|
919
|
+
return input?.type === "text" ? text : null;
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const validateLoadedThread = async (thread, options) => {
|
|
923
|
+
const ids = await loadedThreadIds(options);
|
|
924
|
+
|
|
925
|
+
if (!ids.includes(thread)) {
|
|
926
|
+
throw new Error(`Codex thread is not loaded: ${thread}`);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return ids;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const desktopHealth = (desktop) => ({
|
|
933
|
+
ok: true,
|
|
934
|
+
thread: desktop.session.thread,
|
|
935
|
+
threads: [desktop.session.thread],
|
|
936
|
+
runtime: publicRuntime("desktop", "desktop-queue", true),
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
const desktopThreadList = (desktop) => ({
|
|
940
|
+
ok: true,
|
|
941
|
+
thread: desktop.session.thread,
|
|
942
|
+
threadDetails: [{
|
|
943
|
+
id: desktop.session.thread,
|
|
944
|
+
title: "Current Codex Desktop thread",
|
|
945
|
+
updatedAt: null,
|
|
946
|
+
connected: true,
|
|
947
|
+
}],
|
|
948
|
+
hasMore: false,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
const validateDesktopThread = (thread, desktop) => {
|
|
952
|
+
if (thread !== desktop.session.thread) {
|
|
953
|
+
throw new Error(`Codex Desktop thread is not available to this Mesurer host: ${thread}`);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return [desktop.session.thread];
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const health = async (preferredThread, options) => {
|
|
960
|
+
const threads = await listLoadedThreads(options);
|
|
961
|
+
|
|
962
|
+
const ids = threads.map((thread) => thread.id);
|
|
963
|
+
|
|
964
|
+
const thread = preferredThread && ids.includes(preferredThread)
|
|
965
|
+
? preferredThread
|
|
966
|
+
: ids[0] ?? null;
|
|
967
|
+
|
|
968
|
+
return {
|
|
969
|
+
ok: true,
|
|
970
|
+
thread,
|
|
971
|
+
threads: ids,
|
|
972
|
+
runtime: publicRuntime("shared", "shared-app-server", true),
|
|
973
|
+
};
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const listThreads = async (request, options) => {
|
|
977
|
+
const threads = await listLoadedThreads(options);
|
|
978
|
+
|
|
979
|
+
const limit = Number.isInteger(request.limit)
|
|
980
|
+
? Math.min(MAX_DISCOVERED_THREADS, Math.max(1, request.limit))
|
|
981
|
+
: 5;
|
|
982
|
+
|
|
983
|
+
const ids = threads.map((thread) => thread.id);
|
|
984
|
+
|
|
985
|
+
const preferred = normalizeString(request.thread);
|
|
986
|
+
|
|
987
|
+
const selected = preferred && ids.includes(preferred)
|
|
988
|
+
? preferred
|
|
989
|
+
: ids[0] ?? null;
|
|
990
|
+
|
|
991
|
+
const ordered = selected
|
|
992
|
+
? [
|
|
993
|
+
...threads.filter((thread) => thread.id === selected),
|
|
994
|
+
...threads.filter((thread) => thread.id !== selected),
|
|
995
|
+
]
|
|
996
|
+
: threads;
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
ok: true,
|
|
1000
|
+
thread: selected,
|
|
1001
|
+
threadDetails: ordered.slice(0, limit),
|
|
1002
|
+
hasMore: ordered.length > limit,
|
|
1003
|
+
};
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
const queueMessage = async (request, options) => {
|
|
1007
|
+
const message = normalizeString(request.message);
|
|
1008
|
+
let thread = normalizeString(request.thread);
|
|
1009
|
+
|
|
1010
|
+
if (!message) throw new Error("message must be a non-empty string.");
|
|
1011
|
+
|
|
1012
|
+
if (!thread) {
|
|
1013
|
+
const ids = await loadedThreadIds(options);
|
|
1014
|
+
|
|
1015
|
+
if (ids.length !== 1) {
|
|
1016
|
+
throw new Error("Choose a Codex thread before queueing feedback.");
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
[thread] = ids;
|
|
1020
|
+
} else {
|
|
1021
|
+
await validateLoadedThread(thread, options);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const result = await daemonRequest(
|
|
1025
|
+
"thread/queue/add",
|
|
1026
|
+
{
|
|
1027
|
+
threadId: thread,
|
|
1028
|
+
input: [{
|
|
1029
|
+
type: "text",
|
|
1030
|
+
text: message,
|
|
1031
|
+
textElements: [],
|
|
1032
|
+
}],
|
|
1033
|
+
clientUserMessageId: randomUUID(),
|
|
1034
|
+
},
|
|
1035
|
+
options,
|
|
1036
|
+
true,
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
const queuedSubmissionId = normalizeString(result?.queuedSubmission?.id);
|
|
1040
|
+
|
|
1041
|
+
if (!queuedSubmissionId) {
|
|
1042
|
+
throw new Error("Codex app-server accepted the request without a queued submission id.");
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const now = Date.now();
|
|
1046
|
+
|
|
1047
|
+
const delivery = {
|
|
1048
|
+
id: randomUUID(),
|
|
1049
|
+
thread,
|
|
1050
|
+
message,
|
|
1051
|
+
messageHash: hashMessage(message),
|
|
1052
|
+
status: "queued",
|
|
1053
|
+
turnId: null,
|
|
1054
|
+
queuedSubmissionId,
|
|
1055
|
+
transport: "shared-app-server",
|
|
1056
|
+
dispatch: "persisted",
|
|
1057
|
+
dispatchError: null,
|
|
1058
|
+
createdAt: now,
|
|
1059
|
+
updatedAt: now,
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
pruneDeliveries();
|
|
1063
|
+
deliveries.set(delivery.id, delivery);
|
|
1064
|
+
await persistDeliveries(options);
|
|
1065
|
+
|
|
1066
|
+
return {
|
|
1067
|
+
ok: true,
|
|
1068
|
+
thread,
|
|
1069
|
+
output: `Queued message ${queuedSubmissionId} for thread ${thread}.`,
|
|
1070
|
+
delivery: "queued",
|
|
1071
|
+
...publicDelivery(delivery),
|
|
1072
|
+
};
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
const queueDesktopMessage = async (request, desktop, options) => {
|
|
1076
|
+
const message = normalizeString(request.message);
|
|
1077
|
+
const requestedThread = normalizeString(request.thread);
|
|
1078
|
+
const thread = requestedThread ?? desktop.session.thread;
|
|
1079
|
+
|
|
1080
|
+
if (!message) throw new Error("message must be a non-empty string.");
|
|
1081
|
+
|
|
1082
|
+
validateDesktopThread(thread, desktop);
|
|
1083
|
+
|
|
1084
|
+
const queued = await queueDesktopThread({
|
|
1085
|
+
command: desktop.command,
|
|
1086
|
+
thread,
|
|
1087
|
+
message,
|
|
1088
|
+
env: codexEnv(options),
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
let dispatch = "desktop-wake-failed";
|
|
1092
|
+
let dispatchError = null;
|
|
1093
|
+
|
|
1094
|
+
try {
|
|
1095
|
+
await openDesktopThread({
|
|
1096
|
+
thread,
|
|
1097
|
+
env: codexEnv(options),
|
|
1098
|
+
});
|
|
1099
|
+
dispatch = "desktop-opened";
|
|
1100
|
+
} catch (cause) {
|
|
1101
|
+
dispatchError = cause instanceof Error ? cause.message : String(cause);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const now = Date.now();
|
|
1105
|
+
|
|
1106
|
+
const delivery = {
|
|
1107
|
+
id: randomUUID(),
|
|
1108
|
+
thread,
|
|
1109
|
+
message,
|
|
1110
|
+
messageHash: hashMessage(message),
|
|
1111
|
+
status: "queued",
|
|
1112
|
+
turnId: null,
|
|
1113
|
+
queuedSubmissionId: queued.queuedSubmissionId,
|
|
1114
|
+
transport: "desktop-queue",
|
|
1115
|
+
dispatch,
|
|
1116
|
+
dispatchError,
|
|
1117
|
+
createdAt: now,
|
|
1118
|
+
updatedAt: now,
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
pruneDeliveries();
|
|
1122
|
+
deliveries.set(delivery.id, delivery);
|
|
1123
|
+
await persistDeliveries(options);
|
|
1124
|
+
|
|
1125
|
+
return {
|
|
1126
|
+
ok: true,
|
|
1127
|
+
thread,
|
|
1128
|
+
output: queued.output,
|
|
1129
|
+
delivery: "queued",
|
|
1130
|
+
...publicDelivery(delivery),
|
|
1131
|
+
};
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const readDelivery = async (request, options) => {
|
|
1135
|
+
const deliveryId = normalizeString(request.deliveryId);
|
|
1136
|
+
|
|
1137
|
+
if (!deliveryId) throw new Error("deliveryId must be a non-empty string.");
|
|
1138
|
+
|
|
1139
|
+
const delivery = deliveries.get(deliveryId);
|
|
1140
|
+
|
|
1141
|
+
if (!delivery) throw new Error(`Codex delivery is not available: ${deliveryId}`);
|
|
1142
|
+
|
|
1143
|
+
await reconcileDelivery(delivery, options).catch(() => undefined);
|
|
1144
|
+
|
|
1145
|
+
return {
|
|
1146
|
+
ok: true,
|
|
1147
|
+
...publicDelivery(delivery),
|
|
1148
|
+
};
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
const restoreDelivery = async (request, options, desktop = null) => {
|
|
1152
|
+
const deliveryId = normalizeString(request.deliveryId);
|
|
1153
|
+
const thread = normalizeString(request.thread);
|
|
1154
|
+
const queuedSubmissionId = normalizeString(request.queuedSubmissionId);
|
|
1155
|
+
|
|
1156
|
+
if (!deliveryId || !thread) {
|
|
1157
|
+
throw new Error("deliveryId and thread are required.");
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const existing = deliveries.get(deliveryId);
|
|
1161
|
+
|
|
1162
|
+
if (existing) {
|
|
1163
|
+
if (existing.thread !== thread) {
|
|
1164
|
+
throw new Error(`Codex delivery ${deliveryId} belongs to a different thread.`);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
await reconcileDelivery(existing, options).catch(() => undefined);
|
|
1168
|
+
|
|
1169
|
+
return {
|
|
1170
|
+
ok: true,
|
|
1171
|
+
restored: false,
|
|
1172
|
+
...publicDelivery(existing),
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (desktop) {
|
|
1177
|
+
validateDesktopThread(thread, desktop);
|
|
1178
|
+
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
"Codex Desktop delivery state is unavailable. Mesurer will not requeue the message without its original delivery record.",
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
await validateLoadedThread(thread, options);
|
|
1185
|
+
|
|
1186
|
+
const lookup = await queueLookup(thread, queuedSubmissionId, options);
|
|
1187
|
+
|
|
1188
|
+
if (!lookup.submission) {
|
|
1189
|
+
if (lookup.ambiguous) {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
"Multiple queued Codex submissions exist for this thread; an exact queuedSubmissionId is required to restore the delivery safely.",
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
if (queuedSubmissionId) {
|
|
1196
|
+
throw new Error(`Codex queued submission is not available: ${queuedSubmissionId}`);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
throw new Error("No queued Codex submission is available to restore for this thread.");
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const message = queuedMessage(lookup.submission);
|
|
1203
|
+
|
|
1204
|
+
if (!message) {
|
|
1205
|
+
throw new Error("The queued Codex submission is not a single text message and cannot be restored safely.");
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const now = Date.now();
|
|
1209
|
+
|
|
1210
|
+
const delivery = {
|
|
1211
|
+
id: deliveryId,
|
|
1212
|
+
thread,
|
|
1213
|
+
message,
|
|
1214
|
+
messageHash: hashMessage(message),
|
|
1215
|
+
status: "queued",
|
|
1216
|
+
turnId: null,
|
|
1217
|
+
queuedSubmissionId: normalizeString(lookup.submission.id),
|
|
1218
|
+
transport: "shared-app-server",
|
|
1219
|
+
dispatch: "persisted",
|
|
1220
|
+
dispatchError: null,
|
|
1221
|
+
createdAt: now,
|
|
1222
|
+
updatedAt: now,
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
pruneDeliveries();
|
|
1226
|
+
deliveries.set(delivery.id, delivery);
|
|
1227
|
+
await persistDeliveries(options);
|
|
1228
|
+
|
|
1229
|
+
return {
|
|
1230
|
+
ok: true,
|
|
1231
|
+
restored: true,
|
|
1232
|
+
...publicDelivery(delivery),
|
|
1233
|
+
};
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Native Codex transport for Mesurer's Codex plugin.
|
|
1238
|
+
*
|
|
1239
|
+
* This helper runs inside the host process. It does not open a server or start a
|
|
1240
|
+
* second Electron process. Requests are relayed to Codex's shared local
|
|
1241
|
+
* app-server. When the host inherits ownership from a Codex Desktop thread,
|
|
1242
|
+
* delivery uses Codex's durable queue command and wakes that exact thread with
|
|
1243
|
+
* the native codex:// deep link. The Desktop private app-tools pipe is never opened.
|
|
1244
|
+
*/
|
|
1245
|
+
export async function codexBridge(request, options = {}) {
|
|
1246
|
+
if (request?.constructor !== Object) {
|
|
1247
|
+
throw new Error("Codex Bridge request must be an object.");
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const action = normalizeString(request.action);
|
|
1251
|
+
|
|
1252
|
+
if (action === "runtime") {
|
|
1253
|
+
return {
|
|
1254
|
+
ok: true,
|
|
1255
|
+
runtime: await inspectRuntime(options),
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const desktop = await desktopTransport(options);
|
|
1260
|
+
|
|
1261
|
+
await ensureDeliveriesLoaded(options);
|
|
1262
|
+
|
|
1263
|
+
if (action === "health") {
|
|
1264
|
+
return desktop
|
|
1265
|
+
? desktopHealth(desktop)
|
|
1266
|
+
: health(normalizeString(request.thread), options);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (action === "threads") {
|
|
1270
|
+
return desktop ? desktopThreadList(desktop) : listThreads(request, options);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (action === "target") {
|
|
1274
|
+
const thread = normalizeString(request.thread);
|
|
1275
|
+
|
|
1276
|
+
if (!thread) throw new Error("thread must be a non-empty string.");
|
|
1277
|
+
|
|
1278
|
+
const threads = desktop
|
|
1279
|
+
? validateDesktopThread(thread, desktop)
|
|
1280
|
+
: await validateLoadedThread(thread, options);
|
|
1281
|
+
|
|
1282
|
+
return {
|
|
1283
|
+
ok: true,
|
|
1284
|
+
thread,
|
|
1285
|
+
threads,
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (action === "queue") {
|
|
1290
|
+
return desktop
|
|
1291
|
+
? queueDesktopMessage(request, desktop, options)
|
|
1292
|
+
: queueMessage(request, options);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (action === "delivery") {
|
|
1296
|
+
return readDelivery(request, options);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (action === "restore") {
|
|
1300
|
+
return restoreDelivery(request, options, desktop);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
throw new Error(`Unsupported Codex Bridge action: ${action ?? "<missing>"}`);
|
|
1304
|
+
}
|