@livedesk/hub 0.1.27 → 0.1.29
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/package.json +2 -2
- package/src/agents/agent-manager.js +27 -11
- package/src/agents/codex-agent-runtime.js +325 -41
- package/src/frame-packet-contract.mjs +427 -0
- package/src/live-capture-transition-retry.mjs +297 -0
- package/src/mode4-atlas-pool.js +17 -3
- package/src/mode4-atlas-worker.js +134 -21
- package/src/mode4-atlas.js +180 -16
- package/src/remote-audio-subscription-contract.mjs +109 -0
- package/src/remote-audio-subscription-contract.test.mjs +72 -0
- package/src/remote-hub.js +6405 -4236
- package/src/server.js +3234 -2206
- package/src/transport/agent-binary-ingress.js +810 -0
- package/src/transport/relay-hub-control.js +388 -57
- package/src/transport/udp-hub-transport.js +222 -46
- package/src/transport/udp-protocol.js +302 -70
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/hub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "LiveDesk local Hub API and browser frame bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
18
|
-
"@livedesk/runtime-core": "0.1.
|
|
18
|
+
"@livedesk/runtime-core": "0.1.1",
|
|
19
19
|
"@openai/codex-sdk": "0.145.0",
|
|
20
20
|
"cors": "^2.8.5",
|
|
21
21
|
"express": "^4.21.2",
|
|
@@ -30,23 +30,37 @@ export function createAgentManager({
|
|
|
30
30
|
const settings = settingsStore || new AgentSettingsStore({ dataDir });
|
|
31
31
|
const runtime = codexRuntime;
|
|
32
32
|
let statusCache = { expiresAt: 0, value: null };
|
|
33
|
+
let statusRefreshPromise = null;
|
|
34
|
+
let statusRefreshGeneration = 0;
|
|
33
35
|
|
|
34
36
|
async function getCodexStatus() {
|
|
35
37
|
if (!runtime) return { installed: false, authenticated: 'unknown', status: 'unavailable', codexPath: '' };
|
|
36
38
|
if (statusCache.value && statusCache.expiresAt > Date.now()) return statusCache.value;
|
|
37
|
-
|
|
39
|
+
if (statusRefreshPromise) return statusRefreshPromise;
|
|
40
|
+
const generation = statusRefreshGeneration;
|
|
41
|
+
const refresh = (async () => {
|
|
42
|
+
let value;
|
|
43
|
+
try {
|
|
44
|
+
value = publicCodexStatus(await runtime.getStatus());
|
|
45
|
+
} catch (error) {
|
|
46
|
+
value = publicCodexStatus({
|
|
47
|
+
installed: true,
|
|
48
|
+
authenticated: 'unknown',
|
|
49
|
+
status: error?.code || 'security-unavailable',
|
|
50
|
+
detail: error?.message || 'Codex security isolation is unavailable.'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (generation === statusRefreshGeneration) {
|
|
54
|
+
statusCache = { value, expiresAt: Date.now() + 10000 };
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
})();
|
|
58
|
+
statusRefreshPromise = refresh;
|
|
38
59
|
try {
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
installed: true,
|
|
43
|
-
authenticated: 'unknown',
|
|
44
|
-
status: error?.code || 'security-unavailable',
|
|
45
|
-
detail: error?.message || 'Codex security isolation is unavailable.'
|
|
46
|
-
});
|
|
60
|
+
return await refresh;
|
|
61
|
+
} finally {
|
|
62
|
+
if (statusRefreshPromise === refresh) statusRefreshPromise = null;
|
|
47
63
|
}
|
|
48
|
-
statusCache = { value, expiresAt: Date.now() + 10000 };
|
|
49
|
-
return value;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
async function publicSettings() {
|
|
@@ -65,6 +79,8 @@ export function createAgentManager({
|
|
|
65
79
|
async testConnection() {
|
|
66
80
|
if (!runtime) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
67
81
|
statusCache = { expiresAt: 0, value: null };
|
|
82
|
+
statusRefreshGeneration += 1;
|
|
83
|
+
statusRefreshPromise = null;
|
|
68
84
|
return publicCodexConnection(await runtime.testConnection());
|
|
69
85
|
},
|
|
70
86
|
async createSummary(input) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { access, link, mkdir, stat, unlink } from 'node:fs/promises';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
@@ -9,10 +10,25 @@ import { AGENT_TOOL_NAMES } from './agent-tool-registry.js';
|
|
|
9
10
|
import { createAgentPermissionPolicy, hashAgentPermissionPolicy } from './agent-permissions.js';
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
|
-
const MAX_EVENTS = 240;
|
|
13
|
-
const MAX_RUNS = 500;
|
|
14
|
-
const RUN_TTL_MS = 60 * 60 * 1000;
|
|
15
|
-
const
|
|
13
|
+
const MAX_EVENTS = 240;
|
|
14
|
+
const MAX_RUNS = 500;
|
|
15
|
+
const RUN_TTL_MS = 60 * 60 * 1000;
|
|
16
|
+
const MAX_CODEX_CLI_OUTPUT_BYTES = 64 * 1024;
|
|
17
|
+
const MAX_CODEX_CLI_OUTPUT_CHUNKS = 256;
|
|
18
|
+
const CODEX_CLI_TERMINATE_GRACE_MS = 200;
|
|
19
|
+
const CODEX_CLI_FORCE_CLOSE_MS = 1000;
|
|
20
|
+
const activeCliProcessOwners = new Map();
|
|
21
|
+
let nextCliProcessOwnerId = 0;
|
|
22
|
+
let bundledCodexCliPathCache;
|
|
23
|
+
const CODEX_NATIVE_TARGETS = Object.freeze({
|
|
24
|
+
'linux:x64': ['@openai/codex-linux-x64', 'x86_64-unknown-linux-musl', 'codex'],
|
|
25
|
+
'linux:arm64': ['@openai/codex-linux-arm64', 'aarch64-unknown-linux-musl', 'codex'],
|
|
26
|
+
'darwin:x64': ['@openai/codex-darwin-x64', 'x86_64-apple-darwin', 'codex'],
|
|
27
|
+
'darwin:arm64': ['@openai/codex-darwin-arm64', 'aarch64-apple-darwin', 'codex'],
|
|
28
|
+
'win32:x64': ['@openai/codex-win32-x64', 'x86_64-pc-windows-msvc', 'codex.exe'],
|
|
29
|
+
'win32:arm64': ['@openai/codex-win32-arm64', 'aarch64-pc-windows-msvc', 'codex.exe']
|
|
30
|
+
});
|
|
31
|
+
const SAFE_ENV_KEYS = [
|
|
16
32
|
'Path',
|
|
17
33
|
'PATH',
|
|
18
34
|
'SystemRoot',
|
|
@@ -206,33 +222,281 @@ async function ensureCodexHome(codexHome, globalCodexHome, { preferGlobalAuth =
|
|
|
206
222
|
}
|
|
207
223
|
}
|
|
208
224
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
225
|
+
function boundedCliOption(value, fallback, minimum = 1) {
|
|
226
|
+
const number = Number(value);
|
|
227
|
+
if (!Number.isFinite(number)) return fallback;
|
|
228
|
+
return Math.min(fallback, Math.max(minimum, Math.floor(number)));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function resolveBundledCodexCliPath() {
|
|
232
|
+
if (bundledCodexCliPathCache !== undefined) return bundledCodexCliPathCache;
|
|
233
|
+
const target = CODEX_NATIVE_TARGETS[`${process.platform}:${process.arch}`];
|
|
234
|
+
if (!target) {
|
|
235
|
+
bundledCodexCliPathCache = '';
|
|
236
|
+
return bundledCodexCliPathCache;
|
|
237
|
+
}
|
|
238
|
+
const [platformPackage, targetTriple, executableName] = target;
|
|
239
|
+
try {
|
|
240
|
+
const packageJsonPath = require.resolve(`${platformPackage}/package.json`);
|
|
241
|
+
const executablePath = path.join(
|
|
242
|
+
path.dirname(packageJsonPath),
|
|
243
|
+
'vendor',
|
|
244
|
+
targetTriple,
|
|
245
|
+
'bin',
|
|
246
|
+
executableName
|
|
247
|
+
);
|
|
248
|
+
if (existsSync(executablePath)) {
|
|
249
|
+
bundledCodexCliPathCache = executablePath;
|
|
250
|
+
return bundledCodexCliPathCache;
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
// Older Codex packages may keep the platform payload under the portable
|
|
254
|
+
// package. That fallback remains a native executable, never the JS wrapper.
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const portableCliPath = require.resolve('@openai/codex/bin/codex.js');
|
|
258
|
+
const executablePath = path.join(
|
|
259
|
+
path.dirname(portableCliPath),
|
|
260
|
+
'..',
|
|
261
|
+
'vendor',
|
|
262
|
+
targetTriple,
|
|
263
|
+
'bin',
|
|
264
|
+
executableName
|
|
265
|
+
);
|
|
266
|
+
bundledCodexCliPathCache = existsSync(executablePath) ? executablePath : '';
|
|
267
|
+
return bundledCodexCliPathCache;
|
|
268
|
+
} catch {
|
|
269
|
+
bundledCodexCliPathCache = '';
|
|
270
|
+
return bundledCodexCliPathCache;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function getCodexCliProcessOwnerSnapshot() {
|
|
275
|
+
const owners = [...activeCliProcessOwners.values()];
|
|
276
|
+
return {
|
|
277
|
+
activeOwners: owners.length,
|
|
278
|
+
activeProcesses: owners.filter(owner => owner.child !== null).length,
|
|
279
|
+
activeStdoutStreams: owners.filter(owner => owner.stdoutStream !== null).length,
|
|
280
|
+
activeStderrStreams: owners.filter(owner => owner.stderrStream !== null).length,
|
|
281
|
+
activeListeners: owners.reduce((total, owner) => total + owner.listenerCount, 0),
|
|
282
|
+
activeTimers: owners.reduce((total, owner) => total
|
|
283
|
+
+ Number(owner.timeoutTimer !== null)
|
|
284
|
+
+ Number(owner.forceKillTimer !== null)
|
|
285
|
+
+ Number(owner.forceCloseTimer !== null), 0),
|
|
286
|
+
retainedOutputBytes: owners.reduce((total, owner) => total + owner.retainedOutputBytes, 0)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function runCli(
|
|
291
|
+
cliPath,
|
|
292
|
+
args,
|
|
293
|
+
timeoutMs = 5000,
|
|
294
|
+
env = process.env,
|
|
295
|
+
lifecycleOptions = {}
|
|
296
|
+
) {
|
|
297
|
+
const maxOutputBytes = boundedCliOption(
|
|
298
|
+
lifecycleOptions.maxOutputBytes,
|
|
299
|
+
MAX_CODEX_CLI_OUTPUT_BYTES
|
|
300
|
+
);
|
|
301
|
+
const terminateGraceMs = boundedCliOption(
|
|
302
|
+
lifecycleOptions.terminateGraceMs,
|
|
303
|
+
CODEX_CLI_TERMINATE_GRACE_MS
|
|
304
|
+
);
|
|
305
|
+
const forceCloseMs = boundedCliOption(
|
|
306
|
+
lifecycleOptions.forceCloseMs,
|
|
307
|
+
CODEX_CLI_FORCE_CLOSE_MS
|
|
308
|
+
);
|
|
309
|
+
const boundedTimeoutMs = Math.max(1, Math.floor(Number(timeoutMs) || 5000));
|
|
310
|
+
return new Promise((resolve, reject) => {
|
|
311
|
+
let child;
|
|
312
|
+
try {
|
|
313
|
+
const usesNodeLauncher = /\.(?:c|m)?js$/i.test(cliPath);
|
|
314
|
+
child = spawn(usesNodeLauncher ? process.execPath : cliPath, [
|
|
315
|
+
...(usesNodeLauncher ? [cliPath] : []),
|
|
316
|
+
...args
|
|
317
|
+
], {
|
|
318
|
+
env,
|
|
319
|
+
windowsHide: true,
|
|
320
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
321
|
+
});
|
|
322
|
+
} catch (error) {
|
|
323
|
+
reject(error);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const owner = {
|
|
328
|
+
id: ++nextCliProcessOwnerId,
|
|
329
|
+
child,
|
|
330
|
+
stdoutStream: child.stdout,
|
|
331
|
+
stderrStream: child.stderr,
|
|
332
|
+
stdoutChunks: [],
|
|
333
|
+
stderrChunks: [],
|
|
334
|
+
stdoutBytes: 0,
|
|
335
|
+
stderrBytes: 0,
|
|
336
|
+
retainedOutputBytes: 0,
|
|
337
|
+
listenerCount: 6,
|
|
338
|
+
acceptingOutput: true,
|
|
339
|
+
failure: null,
|
|
340
|
+
settled: false,
|
|
341
|
+
timeoutTimer: null,
|
|
342
|
+
forceKillTimer: null,
|
|
343
|
+
forceCloseTimer: null
|
|
344
|
+
};
|
|
345
|
+
activeCliProcessOwners.set(owner.id, owner);
|
|
346
|
+
|
|
347
|
+
const clearOwnerTimer = key => {
|
|
348
|
+
if (owner[key] === null) return;
|
|
349
|
+
clearTimeout(owner[key]);
|
|
350
|
+
owner[key] = null;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const removeOutputDataListeners = () => {
|
|
354
|
+
if (!owner.acceptingOutput) return;
|
|
355
|
+
owner.acceptingOutput = false;
|
|
356
|
+
owner.stdoutStream?.removeListener('data', onStdoutData);
|
|
357
|
+
owner.stderrStream?.removeListener('data', onStderrData);
|
|
358
|
+
owner.listenerCount = Math.max(0, owner.listenerCount - 2);
|
|
359
|
+
owner.stdoutStream?.resume();
|
|
360
|
+
owner.stderrStream?.resume();
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const cleanupOwner = () => {
|
|
364
|
+
clearOwnerTimer('timeoutTimer');
|
|
365
|
+
clearOwnerTimer('forceKillTimer');
|
|
366
|
+
clearOwnerTimer('forceCloseTimer');
|
|
367
|
+
owner.child?.removeListener('error', onChildError);
|
|
368
|
+
owner.child?.removeListener('close', onChildClose);
|
|
369
|
+
owner.stdoutStream?.removeListener('data', onStdoutData);
|
|
370
|
+
owner.stdoutStream?.removeListener('error', onStdoutError);
|
|
371
|
+
owner.stderrStream?.removeListener('data', onStderrData);
|
|
372
|
+
owner.stderrStream?.removeListener('error', onStderrError);
|
|
373
|
+
owner.stdoutStream?.destroy();
|
|
374
|
+
owner.stderrStream?.destroy();
|
|
375
|
+
owner.child?.unref?.();
|
|
376
|
+
owner.child = null;
|
|
377
|
+
owner.stdoutStream = null;
|
|
378
|
+
owner.stderrStream = null;
|
|
379
|
+
owner.stdoutChunks.length = 0;
|
|
380
|
+
owner.stderrChunks.length = 0;
|
|
381
|
+
owner.stdoutBytes = 0;
|
|
382
|
+
owner.stderrBytes = 0;
|
|
383
|
+
owner.retainedOutputBytes = 0;
|
|
384
|
+
owner.listenerCount = 0;
|
|
385
|
+
activeCliProcessOwners.delete(owner.id);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const finish = code => {
|
|
389
|
+
if (owner.settled) return;
|
|
390
|
+
owner.settled = true;
|
|
391
|
+
const failure = owner.failure;
|
|
392
|
+
const result = failure ? null : {
|
|
393
|
+
code,
|
|
394
|
+
stdout: Buffer.concat(owner.stdoutChunks, owner.stdoutBytes).toString('utf8').trim(),
|
|
395
|
+
stderr: Buffer.concat(owner.stderrChunks, owner.stderrBytes).toString('utf8').trim()
|
|
396
|
+
};
|
|
397
|
+
cleanupOwner();
|
|
398
|
+
if (failure) reject(failure);
|
|
399
|
+
else resolve(result);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const killExactChild = signal => {
|
|
403
|
+
if (owner.child === null || owner.child.exitCode !== null || owner.child.signalCode !== null) return;
|
|
404
|
+
try {
|
|
405
|
+
owner.child.kill(signal);
|
|
406
|
+
} catch {
|
|
407
|
+
// The bounded forced-close owner below still releases every local
|
|
408
|
+
// handle if the OS reports that this exact child already disappeared.
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const failAfterExactChildStops = error => {
|
|
413
|
+
if (owner.failure || owner.settled) return;
|
|
414
|
+
owner.failure = error;
|
|
415
|
+
clearOwnerTimer('timeoutTimer');
|
|
416
|
+
removeOutputDataListeners();
|
|
417
|
+
killExactChild('SIGTERM');
|
|
418
|
+
owner.forceKillTimer = setTimeout(() => {
|
|
419
|
+
owner.forceKillTimer = null;
|
|
420
|
+
killExactChild('SIGKILL');
|
|
421
|
+
owner.forceCloseTimer = setTimeout(() => {
|
|
422
|
+
owner.forceCloseTimer = null;
|
|
423
|
+
error.cleanupTimedOut = true;
|
|
424
|
+
killExactChild('SIGKILL');
|
|
425
|
+
finish(null);
|
|
426
|
+
}, forceCloseMs);
|
|
427
|
+
owner.forceCloseTimer.unref?.();
|
|
428
|
+
}, terminateGraceMs);
|
|
429
|
+
owner.forceKillTimer.unref?.();
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const retainOutput = (channel, chunk) => {
|
|
433
|
+
if (!owner.acceptingOutput || owner.settled) return;
|
|
434
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
435
|
+
const nextBytes = owner.retainedOutputBytes + buffer.length;
|
|
436
|
+
const nextChunks = owner.stdoutChunks.length + owner.stderrChunks.length + 1;
|
|
437
|
+
if (nextBytes > maxOutputBytes || nextChunks > MAX_CODEX_CLI_OUTPUT_CHUNKS) {
|
|
438
|
+
failAfterExactChildStops(Object.assign(
|
|
439
|
+
new Error(`Codex CLI status output exceeded its bounded retention budget.`),
|
|
440
|
+
{
|
|
441
|
+
code: 'codex-status-output-limit',
|
|
442
|
+
maxOutputBytes,
|
|
443
|
+
observedOutputBytes: nextBytes,
|
|
444
|
+
maxOutputChunks: MAX_CODEX_CLI_OUTPUT_CHUNKS,
|
|
445
|
+
observedOutputChunks: nextChunks
|
|
446
|
+
}
|
|
447
|
+
));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
owner.retainedOutputBytes = nextBytes;
|
|
451
|
+
if (channel === 'stdout') {
|
|
452
|
+
owner.stdoutChunks.push(buffer);
|
|
453
|
+
owner.stdoutBytes += buffer.length;
|
|
454
|
+
} else {
|
|
455
|
+
owner.stderrChunks.push(buffer);
|
|
456
|
+
owner.stderrBytes += buffer.length;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
function onStdoutData(chunk) {
|
|
461
|
+
retainOutput('stdout', chunk);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function onStderrData(chunk) {
|
|
465
|
+
retainOutput('stderr', chunk);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function onStdoutError(error) {
|
|
469
|
+
failAfterExactChildStops(error);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function onStderrError(error) {
|
|
473
|
+
failAfterExactChildStops(error);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function onChildError(error) {
|
|
477
|
+
failAfterExactChildStops(error);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function onChildClose(code) {
|
|
481
|
+
finish(code);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
child.stdout.on('data', onStdoutData);
|
|
485
|
+
child.stdout.once('error', onStdoutError);
|
|
486
|
+
child.stderr.on('data', onStderrData);
|
|
487
|
+
child.stderr.once('error', onStderrError);
|
|
488
|
+
child.once('error', onChildError);
|
|
489
|
+
child.once('close', onChildClose);
|
|
490
|
+
owner.timeoutTimer = setTimeout(() => {
|
|
491
|
+
owner.timeoutTimer = null;
|
|
492
|
+
failAfterExactChildStops(Object.assign(
|
|
493
|
+
new Error('Codex CLI status check timed out.'),
|
|
494
|
+
{ code: 'codex-status-timeout' }
|
|
495
|
+
));
|
|
496
|
+
}, boundedTimeoutMs);
|
|
497
|
+
owner.timeoutTimer.unref?.();
|
|
498
|
+
});
|
|
499
|
+
}
|
|
236
500
|
|
|
237
501
|
function isTerminalStatus(status) {
|
|
238
502
|
return ['completed', 'failed', 'cancelled'].includes(status);
|
|
@@ -371,13 +635,9 @@ export function createCodexAgentRuntime({
|
|
|
371
635
|
return codexModulePromise;
|
|
372
636
|
}
|
|
373
637
|
|
|
374
|
-
function cliPath() {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
} catch {
|
|
378
|
-
return '';
|
|
379
|
-
}
|
|
380
|
-
}
|
|
638
|
+
function cliPath() {
|
|
639
|
+
return resolveBundledCodexCliPath();
|
|
640
|
+
}
|
|
381
641
|
|
|
382
642
|
async function getStatus() {
|
|
383
643
|
if (typeof injectedCodexStatus === 'function') return injectedCodexStatus();
|
|
@@ -531,6 +791,7 @@ export function createCodexAgentRuntime({
|
|
|
531
791
|
run.status = 'running';
|
|
532
792
|
run.updatedAt = new Date().toISOString();
|
|
533
793
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
794
|
+
let turnCompleted = false;
|
|
534
795
|
try {
|
|
535
796
|
await prepareCodexHome({ preferGlobalAuth: attempt > 0 && run.authRecoveryRequested === true });
|
|
536
797
|
reportProgress(run, 'thinking', attempt === 0 ? 'Codex is analyzing the request.' : 'Codex is retrying the request with a refreshed runtime.');
|
|
@@ -565,10 +826,27 @@ export function createCodexAgentRuntime({
|
|
|
565
826
|
throw new Error(safeText(event.item?.error?.message, 800) || `LiveDesk tool ${safeText(event.item?.tool, 120) || 'call'} failed.`);
|
|
566
827
|
}
|
|
567
828
|
if (event.type === 'item.completed' && event.item?.type === 'agent_message') run.finalResponse = safeText(event.item.text, 4000);
|
|
568
|
-
if (event.type === 'turn.completed')
|
|
829
|
+
if (event.type === 'turn.completed') turnCompleted = true;
|
|
569
830
|
if (event.type === 'turn.failed') throw new Error(safeText(event.error?.message, 800) || 'Codex run failed.');
|
|
570
831
|
if (event.type === 'error') throw new Error(safeText(event.message, 800) || 'Codex run failed.');
|
|
571
832
|
}
|
|
833
|
+
if (!turnCompleted) {
|
|
834
|
+
throw new AgentRuntimeError(
|
|
835
|
+
'codex-incomplete-turn',
|
|
836
|
+
'Codex ended without a completed turn.',
|
|
837
|
+
{ status: 502 }
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
if (!safeText(run.finalResponse, 4000)) {
|
|
841
|
+
throw new AgentRuntimeError(
|
|
842
|
+
'codex-empty-response',
|
|
843
|
+
'Codex completed without a final response.',
|
|
844
|
+
{ status: 502 }
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
// Terminal state is externally observable. Publish it only after the
|
|
848
|
+
// complete event stream has drained and its final response is owned.
|
|
849
|
+
run.status = 'completed';
|
|
572
850
|
break;
|
|
573
851
|
} catch (error) {
|
|
574
852
|
const normalized = normalizeCodexError(error);
|
|
@@ -589,7 +867,13 @@ export function createCodexAgentRuntime({
|
|
|
589
867
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
590
868
|
}
|
|
591
869
|
}
|
|
592
|
-
if (run.status
|
|
870
|
+
if (run.status !== 'completed') {
|
|
871
|
+
throw new AgentRuntimeError(
|
|
872
|
+
'codex-incomplete-turn',
|
|
873
|
+
'Codex did not reach a completed terminal state.',
|
|
874
|
+
{ status: 502 }
|
|
875
|
+
);
|
|
876
|
+
}
|
|
593
877
|
} catch (error) {
|
|
594
878
|
if (run.abortReason === 'timeout') {
|
|
595
879
|
run.status = 'failed';
|