codex-to-im 1.0.61 → 1.0.63
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/README.md +12 -0
- package/README_EN.md +6 -0
- package/dist/daemon.mjs +1576 -72
- package/dist/ui-server.mjs +97 -16
- package/docs/install-windows.md +2 -0
- package/package.json +1 -1
package/dist/daemon.mjs
CHANGED
|
@@ -318,15 +318,900 @@ var init_sse_utils = __esm({
|
|
|
318
318
|
}
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
+
// src/codex-app-server-client.ts
|
|
322
|
+
import crypto11 from "node:crypto";
|
|
323
|
+
import fs17 from "node:fs";
|
|
324
|
+
import path18 from "node:path";
|
|
325
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
326
|
+
import { spawn } from "node:child_process";
|
|
327
|
+
function asRecord(value) {
|
|
328
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
329
|
+
}
|
|
330
|
+
function asString(value) {
|
|
331
|
+
return typeof value === "string" ? value : "";
|
|
332
|
+
}
|
|
333
|
+
function abortError() {
|
|
334
|
+
const error = new Error("Task stopped by user");
|
|
335
|
+
error.name = "AbortError";
|
|
336
|
+
return error;
|
|
337
|
+
}
|
|
338
|
+
function mapApprovalPolicy(permissionMode) {
|
|
339
|
+
return permissionMode === "never" ? "never" : "on-request";
|
|
340
|
+
}
|
|
341
|
+
function mapSandboxMode(value) {
|
|
342
|
+
if (value === "read-only" || value === "danger-full-access") return value;
|
|
343
|
+
return "workspace-write";
|
|
344
|
+
}
|
|
345
|
+
function getThreadId(params) {
|
|
346
|
+
return asString(params.threadId ?? params.thread_id);
|
|
347
|
+
}
|
|
348
|
+
function getTurnId(params) {
|
|
349
|
+
return asString(params.turnId ?? params.turn_id ?? asRecord(params.turn).id);
|
|
350
|
+
}
|
|
351
|
+
function getItemId(params) {
|
|
352
|
+
return asString(params.itemId ?? params.item_id);
|
|
353
|
+
}
|
|
354
|
+
function getThreadStatus(value) {
|
|
355
|
+
const type = asString(asRecord(value).type);
|
|
356
|
+
if (type === "idle" || type === "active" || type === "notLoaded") {
|
|
357
|
+
return type === "notLoaded" ? "not_loaded" : type;
|
|
358
|
+
}
|
|
359
|
+
if (type === "systemError") return "system_error";
|
|
360
|
+
return "unknown";
|
|
361
|
+
}
|
|
362
|
+
function resolveBundledCodexExecutable() {
|
|
363
|
+
const target = PLATFORM_TARGETS[`${process.platform}:${process.arch}`];
|
|
364
|
+
if (!target) {
|
|
365
|
+
throw new Error(`Unsupported Codex platform: ${process.platform} (${process.arch})`);
|
|
366
|
+
}
|
|
367
|
+
let codexPackageJson = "";
|
|
368
|
+
try {
|
|
369
|
+
codexPackageJson = localRequire2.resolve("@openai/codex/package.json");
|
|
370
|
+
} catch {
|
|
371
|
+
const sdkEntry = localRequire2.resolve("@openai/codex-sdk");
|
|
372
|
+
codexPackageJson = createRequire2(sdkEntry).resolve("@openai/codex/package.json");
|
|
373
|
+
}
|
|
374
|
+
const codexRequire = createRequire2(codexPackageJson);
|
|
375
|
+
let vendorRoot = path18.join(path18.dirname(codexPackageJson), "vendor");
|
|
376
|
+
try {
|
|
377
|
+
const platformPackageJson = codexRequire.resolve(`${target.packageName}/package.json`);
|
|
378
|
+
vendorRoot = path18.join(path18.dirname(platformPackageJson), "vendor");
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
const executablePath = path18.join(
|
|
382
|
+
vendorRoot,
|
|
383
|
+
target.triple,
|
|
384
|
+
"bin",
|
|
385
|
+
process.platform === "win32" ? "codex.exe" : "codex"
|
|
386
|
+
);
|
|
387
|
+
if (!fs17.existsSync(executablePath)) {
|
|
388
|
+
throw new Error(`Bundled Codex executable is missing: ${executablePath}`);
|
|
389
|
+
}
|
|
390
|
+
return executablePath;
|
|
391
|
+
}
|
|
392
|
+
var DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_INTERRUPT_GRACE_MS, DEFAULT_THREAD_IDLE_RELEASE_MS, STDERR_TAIL_LIMIT, localRequire2, PLATFORM_TARGETS, AsyncEventQueue, AppServerPreTurnError, AppServerTurnStartUncertainError, CodexAppServerClient;
|
|
393
|
+
var init_codex_app_server_client = __esm({
|
|
394
|
+
"src/codex-app-server-client.ts"() {
|
|
395
|
+
"use strict";
|
|
396
|
+
DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
397
|
+
DEFAULT_INTERRUPT_GRACE_MS = 15e3;
|
|
398
|
+
DEFAULT_THREAD_IDLE_RELEASE_MS = 10 * 6e4;
|
|
399
|
+
STDERR_TAIL_LIMIT = 16384;
|
|
400
|
+
localRequire2 = createRequire2(import.meta.url);
|
|
401
|
+
PLATFORM_TARGETS = {
|
|
402
|
+
"linux:x64": { packageName: "@openai/codex-linux-x64", triple: "x86_64-unknown-linux-musl" },
|
|
403
|
+
"linux:arm64": { packageName: "@openai/codex-linux-arm64", triple: "aarch64-unknown-linux-musl" },
|
|
404
|
+
"darwin:x64": { packageName: "@openai/codex-darwin-x64", triple: "x86_64-apple-darwin" },
|
|
405
|
+
"darwin:arm64": { packageName: "@openai/codex-darwin-arm64", triple: "aarch64-apple-darwin" },
|
|
406
|
+
"win32:x64": { packageName: "@openai/codex-win32-x64", triple: "x86_64-pc-windows-msvc" },
|
|
407
|
+
"win32:arm64": { packageName: "@openai/codex-win32-arm64", triple: "aarch64-pc-windows-msvc" }
|
|
408
|
+
};
|
|
409
|
+
AsyncEventQueue = class {
|
|
410
|
+
values = [];
|
|
411
|
+
waiters = [];
|
|
412
|
+
closed = false;
|
|
413
|
+
failure = null;
|
|
414
|
+
push(value) {
|
|
415
|
+
if (this.closed) return;
|
|
416
|
+
const waiter = this.waiters.shift();
|
|
417
|
+
if (waiter) waiter.resolve({ value, done: false });
|
|
418
|
+
else this.values.push(value);
|
|
419
|
+
}
|
|
420
|
+
close() {
|
|
421
|
+
if (this.closed) return;
|
|
422
|
+
this.closed = true;
|
|
423
|
+
for (const waiter of this.waiters.splice(0)) waiter.resolve({ value: void 0, done: true });
|
|
424
|
+
}
|
|
425
|
+
fail(error) {
|
|
426
|
+
if (this.closed) return;
|
|
427
|
+
this.failure = error;
|
|
428
|
+
this.closed = true;
|
|
429
|
+
for (const waiter of this.waiters.splice(0)) waiter.reject(error);
|
|
430
|
+
}
|
|
431
|
+
[Symbol.asyncIterator]() {
|
|
432
|
+
return {
|
|
433
|
+
next: () => {
|
|
434
|
+
const value = this.values.shift();
|
|
435
|
+
if (value !== void 0) return Promise.resolve({ value, done: false });
|
|
436
|
+
if (this.failure) return Promise.reject(this.failure);
|
|
437
|
+
if (this.closed) return Promise.resolve({ value: void 0, done: true });
|
|
438
|
+
return new Promise((resolve2, reject) => {
|
|
439
|
+
this.waiters.push({ resolve: resolve2, reject });
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
AppServerPreTurnError = class extends Error {
|
|
446
|
+
constructor(message, options) {
|
|
447
|
+
super(message, options);
|
|
448
|
+
this.name = "AppServerPreTurnError";
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
AppServerTurnStartUncertainError = class extends Error {
|
|
452
|
+
constructor(message, options) {
|
|
453
|
+
super(message, options);
|
|
454
|
+
this.name = "AppServerTurnStartUncertainError";
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
CodexAppServerClient = class {
|
|
458
|
+
constructor(pendingPermissions, options = {}) {
|
|
459
|
+
this.pendingPermissions = pendingPermissions;
|
|
460
|
+
this.options = options;
|
|
461
|
+
}
|
|
462
|
+
child = null;
|
|
463
|
+
startupPromise = null;
|
|
464
|
+
nextRequestId = 1;
|
|
465
|
+
generation = 0;
|
|
466
|
+
stdoutBuffer = "";
|
|
467
|
+
stderrTail = "";
|
|
468
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
469
|
+
activeTurns = /* @__PURE__ */ new Map();
|
|
470
|
+
startingTurns = /* @__PURE__ */ new Map();
|
|
471
|
+
approvalPermissionIds = /* @__PURE__ */ new Map();
|
|
472
|
+
unknownMethods = /* @__PURE__ */ new Set();
|
|
473
|
+
subscribedThreadIds = /* @__PURE__ */ new Set();
|
|
474
|
+
threadStatuses = /* @__PURE__ */ new Map();
|
|
475
|
+
threadReleaseTimers = /* @__PURE__ */ new Map();
|
|
476
|
+
recyclePromise = null;
|
|
477
|
+
getOwnedProcessIds() {
|
|
478
|
+
const pid = this.child?.pid;
|
|
479
|
+
return typeof pid === "number" && pid > 0 ? [pid] : [];
|
|
480
|
+
}
|
|
481
|
+
getThreadRuntimeStatus(threadId) {
|
|
482
|
+
if (!this.subscribedThreadIds.has(threadId) && !this.threadStatuses.has(threadId)) return null;
|
|
483
|
+
return {
|
|
484
|
+
status: this.threadStatuses.get(threadId) || "unknown",
|
|
485
|
+
...this.child?.pid ? { processId: this.child.pid } : {}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
async startTurn(params) {
|
|
489
|
+
if (params.abortSignal?.aborted) throw abortError();
|
|
490
|
+
let turnStartDispatched = false;
|
|
491
|
+
let threadResolutionStarted = false;
|
|
492
|
+
try {
|
|
493
|
+
await this.ensureStarted();
|
|
494
|
+
threadResolutionStarted = true;
|
|
495
|
+
const threadId = await this.resolveThread(params);
|
|
496
|
+
if (params.abortSignal?.aborted) {
|
|
497
|
+
this.scheduleThreadRelease(threadId);
|
|
498
|
+
throw abortError();
|
|
499
|
+
}
|
|
500
|
+
this.clearThreadReleaseTimer(threadId);
|
|
501
|
+
const starting = {
|
|
502
|
+
threadId,
|
|
503
|
+
messages: [],
|
|
504
|
+
queue: new AsyncEventQueue(),
|
|
505
|
+
permissionRequestIds: /* @__PURE__ */ new Set()
|
|
506
|
+
};
|
|
507
|
+
this.startingTurns.set(threadId, starting);
|
|
508
|
+
let response;
|
|
509
|
+
try {
|
|
510
|
+
response = asRecord(await this.sendRequest("turn/start", {
|
|
511
|
+
threadId,
|
|
512
|
+
clientUserMessageId: crypto11.randomUUID(),
|
|
513
|
+
input: [
|
|
514
|
+
{ type: "text", text: params.prompt, text_elements: [] },
|
|
515
|
+
...(params.imagePaths || []).map((imagePath) => ({ type: "localImage", path: imagePath }))
|
|
516
|
+
],
|
|
517
|
+
...params.workingDirectory ? { cwd: params.workingDirectory } : {},
|
|
518
|
+
...params.forceModel && params.model ? { model: params.model } : {},
|
|
519
|
+
...params.modelReasoningEffort ? { effort: params.modelReasoningEffort } : {},
|
|
520
|
+
approvalPolicy: mapApprovalPolicy(params.permissionMode)
|
|
521
|
+
}, () => {
|
|
522
|
+
turnStartDispatched = true;
|
|
523
|
+
}));
|
|
524
|
+
} catch (error) {
|
|
525
|
+
this.startingTurns.delete(threadId);
|
|
526
|
+
this.cancelPermissions(starting, "Turn failed before startup completed");
|
|
527
|
+
starting.queue.fail(error instanceof Error ? error : new Error(String(error)));
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
const turn = asRecord(response.turn);
|
|
531
|
+
const turnId = asString(turn.id);
|
|
532
|
+
if (!turnId) {
|
|
533
|
+
this.startingTurns.delete(threadId);
|
|
534
|
+
const error = new Error("turn/start returned no turn id");
|
|
535
|
+
this.cancelPermissions(starting, error.message);
|
|
536
|
+
starting.queue.fail(error);
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
const active = {
|
|
540
|
+
threadId,
|
|
541
|
+
turnId,
|
|
542
|
+
queue: starting.queue,
|
|
543
|
+
abortSignal: params.abortSignal,
|
|
544
|
+
permissionRequestIds: starting.permissionRequestIds
|
|
545
|
+
};
|
|
546
|
+
this.activeTurns.set(turnId, active);
|
|
547
|
+
this.threadStatuses.set(threadId, "active");
|
|
548
|
+
this.startingTurns.delete(threadId);
|
|
549
|
+
for (const message of starting.messages) this.routeTurnMessage(active, message);
|
|
550
|
+
if (params.abortSignal && this.activeTurns.has(turnId)) {
|
|
551
|
+
active.abortListener = () => {
|
|
552
|
+
void this.interruptTurn(threadId, turnId);
|
|
553
|
+
};
|
|
554
|
+
if (params.abortSignal.aborted) {
|
|
555
|
+
void this.interruptTurn(threadId, turnId);
|
|
556
|
+
} else {
|
|
557
|
+
params.abortSignal.addEventListener("abort", active.abortListener, { once: true });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
threadId,
|
|
562
|
+
turnId,
|
|
563
|
+
events: active.queue,
|
|
564
|
+
interrupt: () => this.interruptTurn(threadId, turnId)
|
|
565
|
+
};
|
|
566
|
+
} catch (error) {
|
|
567
|
+
if (error instanceof AppServerPreTurnError || error instanceof AppServerTurnStartUncertainError || error instanceof Error && error.name === "AbortError") {
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
571
|
+
if (turnStartDispatched) {
|
|
572
|
+
await this.recycleChild(`turn/start result is unknown for thread ${params.savedThreadId || "new"}`);
|
|
573
|
+
throw new AppServerTurnStartUncertainError(
|
|
574
|
+
`Codex app-server turn/start was dispatched but its result is unknown: ${detail}`,
|
|
575
|
+
{ cause: error }
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
if (threadResolutionStarted) {
|
|
579
|
+
await this.recycleChild(`thread preparation failed before turn/start: ${detail}`);
|
|
580
|
+
}
|
|
581
|
+
throw new AppServerPreTurnError(`Codex app-server could not start the turn: ${detail}`, { cause: error });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async close() {
|
|
585
|
+
const child = this.child;
|
|
586
|
+
if (!child) return;
|
|
587
|
+
const error = new Error("Codex app-server client closed");
|
|
588
|
+
for (const pending of this.pendingRequests.values()) {
|
|
589
|
+
clearTimeout(pending.timer);
|
|
590
|
+
pending.reject(error);
|
|
591
|
+
}
|
|
592
|
+
this.pendingRequests.clear();
|
|
593
|
+
for (const active of this.activeTurns.values()) {
|
|
594
|
+
if (active.interruptTimer) clearTimeout(active.interruptTimer);
|
|
595
|
+
this.cancelPermissions(active, error.message);
|
|
596
|
+
active.queue.fail(error);
|
|
597
|
+
}
|
|
598
|
+
this.activeTurns.clear();
|
|
599
|
+
for (const starting of this.startingTurns.values()) {
|
|
600
|
+
this.cancelPermissions(starting, error.message);
|
|
601
|
+
starting.queue.fail(error);
|
|
602
|
+
}
|
|
603
|
+
this.startingTurns.clear();
|
|
604
|
+
this.approvalPermissionIds.clear();
|
|
605
|
+
this.clearAllThreadReleaseTimers();
|
|
606
|
+
this.subscribedThreadIds.clear();
|
|
607
|
+
this.threadStatuses.clear();
|
|
608
|
+
this.startupPromise = null;
|
|
609
|
+
if (child.exitCode !== null) {
|
|
610
|
+
if (this.child === child) this.child = null;
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
await new Promise((resolve2) => {
|
|
614
|
+
let settled = false;
|
|
615
|
+
const finish = () => {
|
|
616
|
+
if (settled) return;
|
|
617
|
+
settled = true;
|
|
618
|
+
clearTimeout(timer);
|
|
619
|
+
child.off("exit", finish);
|
|
620
|
+
resolve2();
|
|
621
|
+
};
|
|
622
|
+
const timer = setTimeout(finish, 2e3);
|
|
623
|
+
child.once("exit", finish);
|
|
624
|
+
try {
|
|
625
|
+
child.stdin.end();
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
child.kill();
|
|
630
|
+
} catch {
|
|
631
|
+
finish();
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
if (this.child === child) this.child = null;
|
|
635
|
+
}
|
|
636
|
+
async resolveThread(params) {
|
|
637
|
+
const common = {
|
|
638
|
+
...params.forceModel && params.model ? { model: params.model } : {},
|
|
639
|
+
...params.workingDirectory ? { cwd: params.workingDirectory } : {},
|
|
640
|
+
approvalPolicy: mapApprovalPolicy(params.permissionMode),
|
|
641
|
+
sandbox: mapSandboxMode(params.sandboxMode),
|
|
642
|
+
...params.systemPrompt ? { developerInstructions: params.systemPrompt } : {},
|
|
643
|
+
...params.skipGitRepoCheck ? { config: { skip_git_repo_check: true } } : {}
|
|
644
|
+
};
|
|
645
|
+
if (params.savedThreadId) {
|
|
646
|
+
if (this.subscribedThreadIds.has(params.savedThreadId)) {
|
|
647
|
+
if (this.threadStatuses.get(params.savedThreadId) !== "system_error") {
|
|
648
|
+
return params.savedThreadId;
|
|
649
|
+
}
|
|
650
|
+
this.forgetThread(params.savedThreadId);
|
|
651
|
+
}
|
|
652
|
+
const resumed = asRecord(await this.sendRequest("thread/resume", {
|
|
653
|
+
threadId: params.savedThreadId,
|
|
654
|
+
excludeTurns: true,
|
|
655
|
+
...common
|
|
656
|
+
}));
|
|
657
|
+
const resumedThread = asRecord(resumed.thread);
|
|
658
|
+
const resumedThreadId = asString(resumedThread.id);
|
|
659
|
+
if (!resumedThreadId) throw new Error("thread/resume returned no thread id");
|
|
660
|
+
this.markThreadSubscribed(resumedThreadId, getThreadStatus(resumedThread.status));
|
|
661
|
+
return resumedThreadId;
|
|
662
|
+
}
|
|
663
|
+
const started = asRecord(await this.sendRequest("thread/start", {
|
|
664
|
+
...common,
|
|
665
|
+
threadSource: "codex-to-im"
|
|
666
|
+
}));
|
|
667
|
+
const startedThread = asRecord(started.thread);
|
|
668
|
+
const threadId = asString(startedThread.id);
|
|
669
|
+
if (!threadId) throw new Error("thread/start returned no thread id");
|
|
670
|
+
this.markThreadSubscribed(threadId, getThreadStatus(startedThread.status));
|
|
671
|
+
return threadId;
|
|
672
|
+
}
|
|
673
|
+
async ensureStarted() {
|
|
674
|
+
if (this.recyclePromise) await this.recyclePromise;
|
|
675
|
+
if (this.child && this.child.exitCode === null && this.startupPromise) {
|
|
676
|
+
return this.startupPromise;
|
|
677
|
+
}
|
|
678
|
+
if (this.startupPromise) return this.startupPromise;
|
|
679
|
+
this.startupPromise = (async () => {
|
|
680
|
+
let executablePath;
|
|
681
|
+
try {
|
|
682
|
+
executablePath = this.options.executablePath || resolveBundledCodexExecutable();
|
|
683
|
+
} catch (error) {
|
|
684
|
+
throw new AppServerPreTurnError(
|
|
685
|
+
error instanceof Error ? error.message : String(error),
|
|
686
|
+
{ cause: error }
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
const env = { ...process.env };
|
|
690
|
+
const apiKey = process.env.CTI_CODEX_API_KEY || process.env.CODEX_API_KEY;
|
|
691
|
+
if (apiKey) env.OPENAI_API_KEY = apiKey;
|
|
692
|
+
if (process.env.CTI_CODEX_BASE_URL) env.OPENAI_BASE_URL = process.env.CTI_CODEX_BASE_URL;
|
|
693
|
+
const spawnProcess = this.options.spawnProcess || spawn;
|
|
694
|
+
const child = spawnProcess(executablePath, this.options.executableArgs || ["app-server"], {
|
|
695
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
696
|
+
windowsHide: true,
|
|
697
|
+
env
|
|
698
|
+
});
|
|
699
|
+
this.child = child;
|
|
700
|
+
this.generation += 1;
|
|
701
|
+
this.stdoutBuffer = "";
|
|
702
|
+
this.stderrTail = "";
|
|
703
|
+
child.stdout.setEncoding("utf8");
|
|
704
|
+
child.stdout.on("data", (chunk) => this.handleStdout(chunk));
|
|
705
|
+
child.stderr.setEncoding("utf8");
|
|
706
|
+
child.stderr.on("data", (chunk) => {
|
|
707
|
+
this.stderrTail = `${this.stderrTail}${chunk}`.slice(-STDERR_TAIL_LIMIT);
|
|
708
|
+
});
|
|
709
|
+
child.on("exit", (code2, signal) => this.handleExit(child, code2, signal));
|
|
710
|
+
await new Promise((resolve2, reject) => {
|
|
711
|
+
const onSpawn = () => {
|
|
712
|
+
cleanup();
|
|
713
|
+
resolve2();
|
|
714
|
+
};
|
|
715
|
+
const onError = (error) => {
|
|
716
|
+
cleanup();
|
|
717
|
+
reject(error);
|
|
718
|
+
};
|
|
719
|
+
const cleanup = () => {
|
|
720
|
+
child.off("spawn", onSpawn);
|
|
721
|
+
child.off("error", onError);
|
|
722
|
+
};
|
|
723
|
+
child.once("spawn", onSpawn);
|
|
724
|
+
child.once("error", onError);
|
|
725
|
+
});
|
|
726
|
+
child.on("error", (error) => {
|
|
727
|
+
if (this.child !== child) return;
|
|
728
|
+
console.warn("[codex-app-server] Child process error:", error.message);
|
|
729
|
+
try {
|
|
730
|
+
child.kill();
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
child.stdin.on("error", (error) => {
|
|
735
|
+
if (this.child !== child) return;
|
|
736
|
+
console.warn("[codex-app-server] stdin error:", error.message);
|
|
737
|
+
try {
|
|
738
|
+
child.kill();
|
|
739
|
+
} catch {
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
await this.sendRequest("initialize", {
|
|
743
|
+
clientInfo: { name: "codex-to-im", title: "Codex to IM", version: "1" },
|
|
744
|
+
capabilities: {
|
|
745
|
+
experimentalApi: true,
|
|
746
|
+
requestAttestation: false
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
this.sendNotification("initialized", {});
|
|
750
|
+
})().catch((error) => {
|
|
751
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
752
|
+
this.startupPromise = null;
|
|
753
|
+
try {
|
|
754
|
+
this.child?.kill();
|
|
755
|
+
} catch {
|
|
756
|
+
}
|
|
757
|
+
this.child = null;
|
|
758
|
+
throw error instanceof AppServerPreTurnError ? error : new AppServerPreTurnError(`Codex app-server initialization failed: ${detail}`, { cause: error });
|
|
759
|
+
});
|
|
760
|
+
return this.startupPromise;
|
|
761
|
+
}
|
|
762
|
+
sendRequest(method, params, onDispatched) {
|
|
763
|
+
const child = this.child;
|
|
764
|
+
if (!child || child.exitCode !== null || !child.stdin.writable) {
|
|
765
|
+
return Promise.reject(new Error("Codex app-server is not running"));
|
|
766
|
+
}
|
|
767
|
+
const id = this.nextRequestId++;
|
|
768
|
+
const timeoutMs = Math.max(10, this.options.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS);
|
|
769
|
+
return new Promise((resolve2, reject) => {
|
|
770
|
+
const timer = setTimeout(() => {
|
|
771
|
+
this.pendingRequests.delete(id);
|
|
772
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
773
|
+
}, timeoutMs);
|
|
774
|
+
this.pendingRequests.set(id, { method, resolve: resolve2, reject, timer });
|
|
775
|
+
try {
|
|
776
|
+
this.writeMessage({ id, method, params });
|
|
777
|
+
onDispatched?.();
|
|
778
|
+
} catch (error) {
|
|
779
|
+
clearTimeout(timer);
|
|
780
|
+
this.pendingRequests.delete(id);
|
|
781
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
sendNotification(method, params) {
|
|
786
|
+
this.writeMessage({ method, params });
|
|
787
|
+
}
|
|
788
|
+
writeMessage(message) {
|
|
789
|
+
const child = this.child;
|
|
790
|
+
if (!child || child.exitCode !== null || !child.stdin.writable) {
|
|
791
|
+
throw new Error("Codex app-server stdin is unavailable");
|
|
792
|
+
}
|
|
793
|
+
child.stdin.write(`${JSON.stringify(message)}
|
|
794
|
+
`);
|
|
795
|
+
}
|
|
796
|
+
handleStdout(chunk) {
|
|
797
|
+
this.stdoutBuffer += chunk;
|
|
798
|
+
while (true) {
|
|
799
|
+
const newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
800
|
+
if (newlineIndex < 0) break;
|
|
801
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
802
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
803
|
+
if (!line) continue;
|
|
804
|
+
try {
|
|
805
|
+
this.handleMessage(JSON.parse(line));
|
|
806
|
+
} catch (error) {
|
|
807
|
+
console.warn("[codex-app-server] Ignored invalid protocol line:", error instanceof Error ? error.message : error);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
handleMessage(message) {
|
|
812
|
+
if (message.id !== void 0 && !message.method) {
|
|
813
|
+
const pending = this.pendingRequests.get(message.id);
|
|
814
|
+
if (!pending) return;
|
|
815
|
+
this.pendingRequests.delete(message.id);
|
|
816
|
+
clearTimeout(pending.timer);
|
|
817
|
+
if (message.error) {
|
|
818
|
+
const detail = message.error.message || `JSON-RPC error ${message.error.code ?? ""}`.trim();
|
|
819
|
+
pending.reject(new Error(`${pending.method} failed: ${detail}`));
|
|
820
|
+
} else {
|
|
821
|
+
pending.resolve(message.result);
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (!message.method) return;
|
|
826
|
+
if (message.id !== void 0) {
|
|
827
|
+
void this.handleServerRequest(message);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const params = asRecord(message.params);
|
|
831
|
+
if (message.method === "thread/started") {
|
|
832
|
+
const thread = asRecord(params.thread);
|
|
833
|
+
const threadId2 = asString(thread.id);
|
|
834
|
+
if (threadId2) this.markThreadSubscribed(threadId2, getThreadStatus(thread.status));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (message.method === "thread/status/changed") {
|
|
838
|
+
const threadId2 = getThreadId(params);
|
|
839
|
+
const status = getThreadStatus(params.status);
|
|
840
|
+
if (threadId2 && (status === "system_error" || status === "not_loaded")) {
|
|
841
|
+
const active2 = this.findActiveTurnByThreadId(threadId2);
|
|
842
|
+
if (active2) {
|
|
843
|
+
active2.queue.push({
|
|
844
|
+
type: "turn.completed",
|
|
845
|
+
status: "failed",
|
|
846
|
+
errorMessage: status === "system_error" ? "Codex app-server reported a system error for this thread." : "Codex app-server unloaded this thread before the turn completed."
|
|
847
|
+
});
|
|
848
|
+
this.finishTurn(active2, {
|
|
849
|
+
threadStatus: status,
|
|
850
|
+
releaseThread: status === "system_error"
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
if (status === "not_loaded") this.forgetThread(threadId2);
|
|
854
|
+
else this.threadStatuses.set(threadId2, status);
|
|
855
|
+
} else if (threadId2) {
|
|
856
|
+
this.threadStatuses.set(threadId2, status);
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (message.method === "thread/closed") {
|
|
861
|
+
const threadId2 = getThreadId(params);
|
|
862
|
+
if (threadId2) {
|
|
863
|
+
const active2 = this.findActiveTurnByThreadId(threadId2);
|
|
864
|
+
if (active2) {
|
|
865
|
+
active2.queue.push({
|
|
866
|
+
type: "turn.completed",
|
|
867
|
+
status: "failed",
|
|
868
|
+
errorMessage: "Codex app-server closed the thread before the turn completed."
|
|
869
|
+
});
|
|
870
|
+
this.finishTurn(active2, { threadStatus: "not_loaded", releaseThread: false });
|
|
871
|
+
}
|
|
872
|
+
this.forgetThread(threadId2);
|
|
873
|
+
}
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (message.method === "serverRequest/resolved") {
|
|
877
|
+
const protocolRequestId = String(params.requestId ?? "");
|
|
878
|
+
const permissionRequestId = this.approvalPermissionIds.get(protocolRequestId);
|
|
879
|
+
if (permissionRequestId) {
|
|
880
|
+
this.approvalPermissionIds.delete(protocolRequestId);
|
|
881
|
+
this.pendingPermissions?.resolve(permissionRequestId, {
|
|
882
|
+
behavior: "deny",
|
|
883
|
+
message: "Permission request was resolved by Codex"
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const turnId = getTurnId(params);
|
|
889
|
+
const threadId = getThreadId(params);
|
|
890
|
+
const active = turnId ? this.activeTurns.get(turnId) : threadId ? this.findActiveTurnByThreadId(threadId) : void 0;
|
|
891
|
+
if (active) {
|
|
892
|
+
this.routeTurnMessage(active, message);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const starting = threadId ? this.startingTurns.get(threadId) : void 0;
|
|
896
|
+
if (starting) starting.messages.push(message);
|
|
897
|
+
}
|
|
898
|
+
routeTurnMessage(active, message) {
|
|
899
|
+
const method = message.method || "";
|
|
900
|
+
const params = asRecord(message.params);
|
|
901
|
+
const messageTurnId = getTurnId(params);
|
|
902
|
+
if (messageTurnId && messageTurnId !== active.turnId) return;
|
|
903
|
+
switch (method) {
|
|
904
|
+
case "item/started":
|
|
905
|
+
case "item/completed":
|
|
906
|
+
active.queue.push({
|
|
907
|
+
type: method === "item/started" ? "item.started" : "item.completed",
|
|
908
|
+
item: asRecord(params.item)
|
|
909
|
+
});
|
|
910
|
+
return;
|
|
911
|
+
case "item/agentMessage/delta":
|
|
912
|
+
active.queue.push({ type: "agent_message.delta", itemId: getItemId(params), delta: asString(params.delta) });
|
|
913
|
+
return;
|
|
914
|
+
case "item/reasoning/summaryTextDelta":
|
|
915
|
+
case "item/reasoning/textDelta":
|
|
916
|
+
active.queue.push({ type: "reasoning.delta", itemId: getItemId(params), delta: asString(params.delta) });
|
|
917
|
+
return;
|
|
918
|
+
case "turn/plan/updated": {
|
|
919
|
+
const plan = Array.isArray(params.plan) ? params.plan.map((entry) => {
|
|
920
|
+
const record = asRecord(entry);
|
|
921
|
+
return { step: asString(record.step), status: asString(record.status) };
|
|
922
|
+
}).filter((entry) => entry.step) : [];
|
|
923
|
+
active.queue.push({ type: "plan.updated", plan });
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
case "thread/tokenUsage/updated": {
|
|
927
|
+
const tokenUsage = asRecord(params.tokenUsage);
|
|
928
|
+
active.queue.push({ type: "usage.updated", usage: asRecord(tokenUsage.last) });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
case "model/rerouted": {
|
|
932
|
+
const model = asString(params.toModel ?? params.model);
|
|
933
|
+
if (model) active.queue.push({ type: "model.updated", model });
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
case "error": {
|
|
937
|
+
const error = asRecord(params.error);
|
|
938
|
+
const messageText = asString(error.message);
|
|
939
|
+
if (messageText && params.willRetry === true) {
|
|
940
|
+
active.queue.push({ type: "warning", message: messageText });
|
|
941
|
+
}
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
case "turn/completed": {
|
|
945
|
+
const turn = asRecord(params.turn);
|
|
946
|
+
const error = asRecord(turn.error);
|
|
947
|
+
active.queue.push({
|
|
948
|
+
type: "turn.completed",
|
|
949
|
+
status: asString(turn.status) || "completed",
|
|
950
|
+
...asString(error.message) ? { errorMessage: asString(error.message) } : {}
|
|
951
|
+
});
|
|
952
|
+
this.finishTurn(active);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
case "warning": {
|
|
956
|
+
const messageText = asString(params.message);
|
|
957
|
+
if (messageText) active.queue.push({ type: "warning", message: messageText });
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
default:
|
|
961
|
+
if (method && getTurnId(params) && !this.unknownMethods.has(method)) {
|
|
962
|
+
this.unknownMethods.add(method);
|
|
963
|
+
console.warn(`[codex-app-server] Unhandled turn notification: ${method}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async handleServerRequest(message) {
|
|
968
|
+
const id = message.id;
|
|
969
|
+
const method = message.method || "";
|
|
970
|
+
const params = asRecord(message.params);
|
|
971
|
+
try {
|
|
972
|
+
if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval" || method === "item/permissions/requestApproval" || method === "execCommandApproval" || method === "applyPatchApproval") {
|
|
973
|
+
const result = await this.requestApproval(id, method, params);
|
|
974
|
+
this.writeMessage({ id, result });
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (method === "item/tool/requestUserInput") {
|
|
978
|
+
const owner = this.getRequestOwner(params);
|
|
979
|
+
owner?.queue.push({
|
|
980
|
+
type: "warning",
|
|
981
|
+
message: "Codex requested interactive input, but this IM bridge cannot answer structured questions yet."
|
|
982
|
+
});
|
|
983
|
+
this.writeMessage({ id, result: { answers: {} } });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (method === "mcpServer/elicitation/request") {
|
|
987
|
+
const owner = this.getRequestOwner(params);
|
|
988
|
+
owner?.queue.push({
|
|
989
|
+
type: "warning",
|
|
990
|
+
message: "An MCP server requested interactive input; the request was declined by the IM bridge."
|
|
991
|
+
});
|
|
992
|
+
this.writeMessage({ id, result: { action: "decline", content: null, _meta: null } });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
this.writeMessage({ id, error: { code: -32601, message: `Unsupported server request: ${method}` } });
|
|
996
|
+
} catch (error) {
|
|
997
|
+
try {
|
|
998
|
+
this.writeMessage({
|
|
999
|
+
id,
|
|
1000
|
+
error: { code: -32603, message: error instanceof Error ? error.message : String(error) }
|
|
1001
|
+
});
|
|
1002
|
+
} catch {
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async requestApproval(id, method, params) {
|
|
1007
|
+
const requestId = `app-server:${this.generation}:${String(id)}`;
|
|
1008
|
+
const turnId = getTurnId(params);
|
|
1009
|
+
const active = turnId ? this.activeTurns.get(turnId) : void 0;
|
|
1010
|
+
const starting = this.startingTurns.get(getThreadId(params));
|
|
1011
|
+
const owner = active || starting;
|
|
1012
|
+
const toolName = method.includes("fileChange") || method === "applyPatchApproval" ? "apply_patch" : method.includes("permissions") ? "request_permissions" : "Bash";
|
|
1013
|
+
const suggestions = [params.reason, params.proposedExecpolicyAmendment, params.proposedNetworkPolicyAmendments].filter((value) => value != null);
|
|
1014
|
+
const resolutionPromise = this.pendingPermissions ? this.pendingPermissions.waitFor(requestId) : Promise.resolve({ behavior: "deny", message: "Permission gateway unavailable" });
|
|
1015
|
+
this.approvalPermissionIds.set(String(id), requestId);
|
|
1016
|
+
owner?.permissionRequestIds.add(requestId);
|
|
1017
|
+
owner?.queue.push({
|
|
1018
|
+
type: "permission.request",
|
|
1019
|
+
requestId,
|
|
1020
|
+
toolName,
|
|
1021
|
+
toolInput: params,
|
|
1022
|
+
...suggestions.length > 0 ? { suggestions } : {}
|
|
1023
|
+
});
|
|
1024
|
+
const resolution = await resolutionPromise;
|
|
1025
|
+
this.approvalPermissionIds.delete(String(id));
|
|
1026
|
+
owner?.permissionRequestIds.delete(requestId);
|
|
1027
|
+
const allowed = resolution.behavior === "allow";
|
|
1028
|
+
if (method === "item/permissions/requestApproval") {
|
|
1029
|
+
const requested = asRecord(params.permissions);
|
|
1030
|
+
const granted = {};
|
|
1031
|
+
if (allowed && requested.network) granted.network = requested.network;
|
|
1032
|
+
if (allowed && requested.fileSystem) granted.fileSystem = requested.fileSystem;
|
|
1033
|
+
return { permissions: granted, scope: "turn" };
|
|
1034
|
+
}
|
|
1035
|
+
if (method === "execCommandApproval" || method === "applyPatchApproval") {
|
|
1036
|
+
return { decision: allowed ? "approved" : "denied" };
|
|
1037
|
+
}
|
|
1038
|
+
return { decision: allowed ? "accept" : "decline" };
|
|
1039
|
+
}
|
|
1040
|
+
async interruptTurn(threadId, turnId) {
|
|
1041
|
+
const active = this.activeTurns.get(turnId);
|
|
1042
|
+
if (active && !active.interruptTimer) {
|
|
1043
|
+
const graceMs = Math.max(10, this.options.interruptGraceMs || DEFAULT_INTERRUPT_GRACE_MS);
|
|
1044
|
+
active.interruptTimer = setTimeout(() => {
|
|
1045
|
+
active.interruptTimer = void 0;
|
|
1046
|
+
if (this.activeTurns.get(turnId) !== active) return;
|
|
1047
|
+
active.queue.push({
|
|
1048
|
+
type: "turn.completed",
|
|
1049
|
+
status: "interrupted",
|
|
1050
|
+
errorMessage: "Codex app-server did not confirm interruption before the timeout."
|
|
1051
|
+
});
|
|
1052
|
+
this.finishTurn(active);
|
|
1053
|
+
void this.recycleChild(`interrupt timeout for turn ${turnId}`);
|
|
1054
|
+
}, graceMs);
|
|
1055
|
+
active.interruptTimer.unref?.();
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
await this.sendRequest("turn/interrupt", { threadId, turnId });
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
console.warn("[codex-app-server] Failed to interrupt turn:", error instanceof Error ? error.message : error);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
finishTurn(active, options = {}) {
|
|
1064
|
+
this.activeTurns.delete(active.turnId);
|
|
1065
|
+
this.threadStatuses.set(active.threadId, options.threadStatus || "idle");
|
|
1066
|
+
if (active.interruptTimer) {
|
|
1067
|
+
clearTimeout(active.interruptTimer);
|
|
1068
|
+
active.interruptTimer = void 0;
|
|
1069
|
+
}
|
|
1070
|
+
this.cancelPermissions(active, "Turn completed before the permission request was resolved");
|
|
1071
|
+
if (active.abortSignal && active.abortListener) {
|
|
1072
|
+
active.abortSignal.removeEventListener("abort", active.abortListener);
|
|
1073
|
+
}
|
|
1074
|
+
active.queue.close();
|
|
1075
|
+
if (options.releaseThread === false) this.clearThreadReleaseTimer(active.threadId);
|
|
1076
|
+
else this.scheduleThreadRelease(active.threadId);
|
|
1077
|
+
}
|
|
1078
|
+
findActiveTurnByThreadId(threadId) {
|
|
1079
|
+
for (const active of this.activeTurns.values()) {
|
|
1080
|
+
if (active.threadId === threadId) return active;
|
|
1081
|
+
}
|
|
1082
|
+
return void 0;
|
|
1083
|
+
}
|
|
1084
|
+
getRequestOwner(params) {
|
|
1085
|
+
const turnId = getTurnId(params);
|
|
1086
|
+
if (turnId) return this.activeTurns.get(turnId);
|
|
1087
|
+
const threadId = getThreadId(params);
|
|
1088
|
+
if (!threadId) return void 0;
|
|
1089
|
+
return this.findActiveTurnByThreadId(threadId) || this.startingTurns.get(threadId);
|
|
1090
|
+
}
|
|
1091
|
+
markThreadSubscribed(threadId, status) {
|
|
1092
|
+
this.subscribedThreadIds.add(threadId);
|
|
1093
|
+
this.threadStatuses.set(threadId, status === "unknown" ? "idle" : status);
|
|
1094
|
+
this.clearThreadReleaseTimer(threadId);
|
|
1095
|
+
}
|
|
1096
|
+
forgetThread(threadId) {
|
|
1097
|
+
this.clearThreadReleaseTimer(threadId);
|
|
1098
|
+
this.subscribedThreadIds.delete(threadId);
|
|
1099
|
+
this.threadStatuses.delete(threadId);
|
|
1100
|
+
}
|
|
1101
|
+
scheduleThreadRelease(threadId) {
|
|
1102
|
+
this.clearThreadReleaseTimer(threadId);
|
|
1103
|
+
const idleMs = this.options.threadIdleReleaseMs ?? DEFAULT_THREAD_IDLE_RELEASE_MS;
|
|
1104
|
+
if (!Number.isFinite(idleMs) || idleMs < 0) return;
|
|
1105
|
+
const timer = setTimeout(() => {
|
|
1106
|
+
this.threadReleaseTimers.delete(threadId);
|
|
1107
|
+
void this.unsubscribeThread(threadId);
|
|
1108
|
+
}, Math.max(1, idleMs));
|
|
1109
|
+
timer.unref?.();
|
|
1110
|
+
this.threadReleaseTimers.set(threadId, timer);
|
|
1111
|
+
}
|
|
1112
|
+
clearThreadReleaseTimer(threadId) {
|
|
1113
|
+
const timer = this.threadReleaseTimers.get(threadId);
|
|
1114
|
+
if (!timer) return;
|
|
1115
|
+
clearTimeout(timer);
|
|
1116
|
+
this.threadReleaseTimers.delete(threadId);
|
|
1117
|
+
}
|
|
1118
|
+
clearAllThreadReleaseTimers() {
|
|
1119
|
+
for (const timer of this.threadReleaseTimers.values()) clearTimeout(timer);
|
|
1120
|
+
this.threadReleaseTimers.clear();
|
|
1121
|
+
}
|
|
1122
|
+
async unsubscribeThread(threadId) {
|
|
1123
|
+
if (!this.subscribedThreadIds.has(threadId) || this.findActiveTurnByThreadId(threadId)) return;
|
|
1124
|
+
try {
|
|
1125
|
+
await this.sendRequest("thread/unsubscribe", { threadId });
|
|
1126
|
+
this.forgetThread(threadId);
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
console.warn(
|
|
1129
|
+
`[codex-app-server] Failed to release idle thread ${threadId}:`,
|
|
1130
|
+
error instanceof Error ? error.message : error
|
|
1131
|
+
);
|
|
1132
|
+
if (this.child) this.scheduleThreadRelease(threadId);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
cancelPermissions(owner, message) {
|
|
1136
|
+
for (const requestId of owner.permissionRequestIds) {
|
|
1137
|
+
this.pendingPermissions?.resolve(requestId, { behavior: "deny", message });
|
|
1138
|
+
}
|
|
1139
|
+
owner.permissionRequestIds.clear();
|
|
1140
|
+
}
|
|
1141
|
+
recycleChild(reason) {
|
|
1142
|
+
const child = this.child;
|
|
1143
|
+
if (this.recyclePromise) return this.recyclePromise;
|
|
1144
|
+
if (!child || child.exitCode !== null) return Promise.resolve();
|
|
1145
|
+
console.warn(`[codex-app-server] Recycling app-server: ${reason}`);
|
|
1146
|
+
this.recyclePromise = new Promise((resolve2) => {
|
|
1147
|
+
let settled = false;
|
|
1148
|
+
const finish = () => {
|
|
1149
|
+
if (settled) return;
|
|
1150
|
+
settled = true;
|
|
1151
|
+
clearTimeout(timer);
|
|
1152
|
+
child.off("exit", finish);
|
|
1153
|
+
this.recyclePromise = null;
|
|
1154
|
+
resolve2();
|
|
1155
|
+
};
|
|
1156
|
+
const timer = setTimeout(() => {
|
|
1157
|
+
if (this.child === child) this.handleExit(child, null, "SIGKILL");
|
|
1158
|
+
try {
|
|
1159
|
+
child.kill("SIGKILL");
|
|
1160
|
+
} catch {
|
|
1161
|
+
}
|
|
1162
|
+
finish();
|
|
1163
|
+
}, 2e3);
|
|
1164
|
+
child.once("exit", finish);
|
|
1165
|
+
try {
|
|
1166
|
+
child.kill();
|
|
1167
|
+
} catch {
|
|
1168
|
+
finish();
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
return this.recyclePromise;
|
|
1172
|
+
}
|
|
1173
|
+
handleExit(child, code2, signal) {
|
|
1174
|
+
if (this.child !== child) return;
|
|
1175
|
+
this.child = null;
|
|
1176
|
+
this.startupPromise = null;
|
|
1177
|
+
this.clearAllThreadReleaseTimers();
|
|
1178
|
+
this.subscribedThreadIds.clear();
|
|
1179
|
+
this.threadStatuses.clear();
|
|
1180
|
+
const detail = this.stderrTail.trim();
|
|
1181
|
+
const error = new Error(
|
|
1182
|
+
`Codex app-server exited (${signal || (code2 ?? "unknown")})${detail ? `: ${detail}` : ""}`
|
|
1183
|
+
);
|
|
1184
|
+
for (const pending of this.pendingRequests.values()) {
|
|
1185
|
+
clearTimeout(pending.timer);
|
|
1186
|
+
pending.reject(error);
|
|
1187
|
+
}
|
|
1188
|
+
this.pendingRequests.clear();
|
|
1189
|
+
for (const active of this.activeTurns.values()) {
|
|
1190
|
+
if (active.interruptTimer) clearTimeout(active.interruptTimer);
|
|
1191
|
+
this.cancelPermissions(active, "Codex app-server exited");
|
|
1192
|
+
active.queue.fail(error);
|
|
1193
|
+
}
|
|
1194
|
+
this.activeTurns.clear();
|
|
1195
|
+
for (const starting of this.startingTurns.values()) {
|
|
1196
|
+
this.cancelPermissions(starting, "Codex app-server exited");
|
|
1197
|
+
starting.queue.fail(error);
|
|
1198
|
+
}
|
|
1199
|
+
this.startingTurns.clear();
|
|
1200
|
+
this.approvalPermissionIds.clear();
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
|
|
321
1206
|
// src/codex-provider.ts
|
|
322
1207
|
var codex_provider_exports = {};
|
|
323
1208
|
__export(codex_provider_exports, {
|
|
324
1209
|
CodexProvider: () => CodexProvider,
|
|
325
1210
|
mapCodexUsage: () => mapCodexUsage
|
|
326
1211
|
});
|
|
327
|
-
import
|
|
1212
|
+
import fs18 from "node:fs";
|
|
328
1213
|
import os4 from "node:os";
|
|
329
|
-
import
|
|
1214
|
+
import path19 from "node:path";
|
|
330
1215
|
function isRecord(value) {
|
|
331
1216
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
332
1217
|
}
|
|
@@ -335,13 +1220,13 @@ function normalizeTokenCount(value) {
|
|
|
335
1220
|
}
|
|
336
1221
|
function mapCodexUsage(usage) {
|
|
337
1222
|
if (!isRecord(usage)) return void 0;
|
|
338
|
-
const cacheRead = usage.cached_input_tokens ?? usage.cache_read_input_tokens;
|
|
339
|
-
const cacheCreation = usage.cache_write_input_tokens ?? usage.cache_creation_input_tokens;
|
|
1223
|
+
const cacheRead = usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? usage.cachedInputTokens;
|
|
1224
|
+
const cacheCreation = usage.cache_write_input_tokens ?? usage.cache_creation_input_tokens ?? usage.cacheWriteInputTokens;
|
|
340
1225
|
return {
|
|
341
|
-
input_tokens: normalizeTokenCount(usage.input_tokens),
|
|
342
|
-
output_tokens: normalizeTokenCount(usage.output_tokens),
|
|
1226
|
+
input_tokens: normalizeTokenCount(usage.input_tokens ?? usage.inputTokens),
|
|
1227
|
+
output_tokens: normalizeTokenCount(usage.output_tokens ?? usage.outputTokens),
|
|
343
1228
|
cache_read_input_tokens: normalizeTokenCount(cacheRead),
|
|
344
|
-
reasoning_output_tokens: normalizeTokenCount(usage.reasoning_output_tokens),
|
|
1229
|
+
reasoning_output_tokens: normalizeTokenCount(usage.reasoning_output_tokens ?? usage.reasoningOutputTokens),
|
|
345
1230
|
...cacheCreation === void 0 ? {} : { cache_creation_input_tokens: normalizeTokenCount(cacheCreation) }
|
|
346
1231
|
};
|
|
347
1232
|
}
|
|
@@ -449,11 +1334,88 @@ function stringifyUnknown(value) {
|
|
|
449
1334
|
return String(value);
|
|
450
1335
|
}
|
|
451
1336
|
}
|
|
1337
|
+
function normalizeTransport(value) {
|
|
1338
|
+
return value === "app-server" || value === "auto" ? value : "sdk";
|
|
1339
|
+
}
|
|
1340
|
+
function normalizeAppServerItem(item) {
|
|
1341
|
+
const type = typeof item.type === "string" ? item.type : "";
|
|
1342
|
+
const id = typeof item.id === "string" ? item.id : `item-${Date.now()}`;
|
|
1343
|
+
switch (type) {
|
|
1344
|
+
case "agentMessage":
|
|
1345
|
+
return { type: "agent_message", id, text: typeof item.text === "string" ? item.text : "" };
|
|
1346
|
+
case "reasoning": {
|
|
1347
|
+
const summary = Array.isArray(item.summary) ? item.summary.filter((part) => typeof part === "string") : [];
|
|
1348
|
+
const content = Array.isArray(item.content) ? item.content.filter((part) => typeof part === "string") : [];
|
|
1349
|
+
return { type: "reasoning", id, text: [...summary, ...content].join("\n\n") };
|
|
1350
|
+
}
|
|
1351
|
+
case "commandExecution":
|
|
1352
|
+
return {
|
|
1353
|
+
type: "command_execution",
|
|
1354
|
+
id,
|
|
1355
|
+
command: typeof item.command === "string" ? item.command : "",
|
|
1356
|
+
aggregated_output: typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : "",
|
|
1357
|
+
exit_code: typeof item.exitCode === "number" ? item.exitCode : void 0,
|
|
1358
|
+
status: item.status
|
|
1359
|
+
};
|
|
1360
|
+
case "fileChange":
|
|
1361
|
+
return {
|
|
1362
|
+
type: "file_change",
|
|
1363
|
+
id,
|
|
1364
|
+
changes: Array.isArray(item.changes) ? item.changes : [],
|
|
1365
|
+
status: item.status
|
|
1366
|
+
};
|
|
1367
|
+
case "mcpToolCall":
|
|
1368
|
+
return {
|
|
1369
|
+
type: "mcp_tool_call",
|
|
1370
|
+
id,
|
|
1371
|
+
server: typeof item.server === "string" ? item.server : "",
|
|
1372
|
+
tool: typeof item.tool === "string" ? item.tool : "",
|
|
1373
|
+
arguments: item.arguments,
|
|
1374
|
+
status: item.status,
|
|
1375
|
+
result: item.result,
|
|
1376
|
+
error: item.error
|
|
1377
|
+
};
|
|
1378
|
+
case "dynamicToolCall":
|
|
1379
|
+
return {
|
|
1380
|
+
type: "mcp_tool_call",
|
|
1381
|
+
id,
|
|
1382
|
+
server: typeof item.namespace === "string" ? item.namespace : "dynamic",
|
|
1383
|
+
tool: typeof item.tool === "string" ? item.tool : "tool",
|
|
1384
|
+
arguments: item.arguments,
|
|
1385
|
+
status: item.status,
|
|
1386
|
+
result: { content: item.contentItems },
|
|
1387
|
+
error: item.success === false ? { message: "Dynamic tool call failed" } : void 0
|
|
1388
|
+
};
|
|
1389
|
+
case "webSearch":
|
|
1390
|
+
return {
|
|
1391
|
+
type: "web_search",
|
|
1392
|
+
id,
|
|
1393
|
+
query: typeof item.query === "string" ? item.query : ""
|
|
1394
|
+
};
|
|
1395
|
+
case "imageGeneration": {
|
|
1396
|
+
const savedPath = typeof item.savedPath === "string" ? item.savedPath : "";
|
|
1397
|
+
const resultText = savedPath || (typeof item.result === "string" ? item.result : "Image generation completed");
|
|
1398
|
+
return {
|
|
1399
|
+
type: "mcp_tool_call",
|
|
1400
|
+
id,
|
|
1401
|
+
server: "codex",
|
|
1402
|
+
tool: "image_generation",
|
|
1403
|
+
arguments: { prompt: item.revisedPrompt },
|
|
1404
|
+
status: item.status,
|
|
1405
|
+
result: { content: [{ type: "text", text: resultText }] },
|
|
1406
|
+
error: item.status === "failed" ? { message: resultText } : void 0
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
default:
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
452
1413
|
var MIME_EXT, DEFAULT_TERMINAL_DRAIN_TIMEOUT_MS, CodexProvider;
|
|
453
1414
|
var init_codex_provider = __esm({
|
|
454
1415
|
"src/codex-provider.ts"() {
|
|
455
1416
|
"use strict";
|
|
456
1417
|
init_sse_utils();
|
|
1418
|
+
init_codex_app_server_client();
|
|
457
1419
|
init_runtime_options();
|
|
458
1420
|
MIME_EXT = {
|
|
459
1421
|
"image/png": ".png",
|
|
@@ -466,9 +1428,26 @@ var init_codex_provider = __esm({
|
|
|
466
1428
|
CodexProvider = class {
|
|
467
1429
|
sdk = null;
|
|
468
1430
|
codex = null;
|
|
1431
|
+
pendingPermissions;
|
|
1432
|
+
transport;
|
|
1433
|
+
appServerClient;
|
|
1434
|
+
warnedDesktopAppServerOverride = false;
|
|
469
1435
|
/** Maps session IDs to Codex thread IDs for resume. */
|
|
470
1436
|
threadIds = /* @__PURE__ */ new Map();
|
|
471
|
-
constructor(
|
|
1437
|
+
constructor(pendingPerms, options = {}) {
|
|
1438
|
+
this.pendingPermissions = pendingPerms;
|
|
1439
|
+
this.transport = normalizeTransport(options.transport ?? process.env.CTI_CODEX_TRANSPORT);
|
|
1440
|
+
this.appServerClient = options.appServerClient || null;
|
|
1441
|
+
}
|
|
1442
|
+
async dispose() {
|
|
1443
|
+
await this.appServerClient?.close();
|
|
1444
|
+
}
|
|
1445
|
+
getOwnedProcessIds() {
|
|
1446
|
+
return this.appServerClient?.getOwnedProcessIds() || [];
|
|
1447
|
+
}
|
|
1448
|
+
getThreadRuntimeStatus(threadId) {
|
|
1449
|
+
const runtime = this.appServerClient?.getThreadRuntimeStatus(threadId);
|
|
1450
|
+
return runtime ? { transport: "app-server", ...runtime } : null;
|
|
472
1451
|
}
|
|
473
1452
|
clearCachedThreadId(sessionId) {
|
|
474
1453
|
this.threadIds.delete(sessionId);
|
|
@@ -514,6 +1493,20 @@ var init_codex_provider = __esm({
|
|
|
514
1493
|
return { sdk: this.sdk, codex: this.codex };
|
|
515
1494
|
}
|
|
516
1495
|
streamChat(params) {
|
|
1496
|
+
const desktopBacked = params.sessionOrigin === "desktop" || Boolean(params.desktopThreadId);
|
|
1497
|
+
if (desktopBacked && this.transport === "app-server" && !this.warnedDesktopAppServerOverride) {
|
|
1498
|
+
this.warnedDesktopAppServerOverride = true;
|
|
1499
|
+
console.warn(
|
|
1500
|
+
"[codex-provider] Ignoring CTI_CODEX_TRANSPORT=app-server for Desktop-backed sessions; a separate app-server cannot safely attach to the Desktop host process."
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
const useAppServer = !desktopBacked && (this.transport === "app-server" || this.transport === "auto");
|
|
1504
|
+
if (useAppServer) {
|
|
1505
|
+
return this.streamChatViaAppServer(params, this.transport === "auto");
|
|
1506
|
+
}
|
|
1507
|
+
return this.streamChatViaSdk(params);
|
|
1508
|
+
}
|
|
1509
|
+
streamChatViaSdk(params) {
|
|
517
1510
|
const self = this;
|
|
518
1511
|
return new ReadableStream({
|
|
519
1512
|
start(controller) {
|
|
@@ -543,13 +1536,13 @@ var init_codex_provider = __esm({
|
|
|
543
1536
|
{ type: "text", text: params.prompt }
|
|
544
1537
|
];
|
|
545
1538
|
for (const file of imageFiles) {
|
|
546
|
-
if (file.filePath &&
|
|
1539
|
+
if (file.filePath && fs18.existsSync(file.filePath)) {
|
|
547
1540
|
parts.push({ type: "local_image", path: file.filePath });
|
|
548
1541
|
continue;
|
|
549
1542
|
}
|
|
550
1543
|
const ext = MIME_EXT[file.type] || ".png";
|
|
551
|
-
const tmpPath =
|
|
552
|
-
|
|
1544
|
+
const tmpPath = path19.join(os4.tmpdir(), `cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
|
|
1545
|
+
fs18.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
|
|
553
1546
|
tempFiles.push(tmpPath);
|
|
554
1547
|
parts.push({ type: "local_image", path: tmpPath });
|
|
555
1548
|
}
|
|
@@ -715,7 +1708,212 @@ var init_codex_provider = __esm({
|
|
|
715
1708
|
} finally {
|
|
716
1709
|
for (const tmp of tempFiles) {
|
|
717
1710
|
try {
|
|
718
|
-
|
|
1711
|
+
fs18.unlinkSync(tmp);
|
|
1712
|
+
} catch {
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
})();
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
streamChatViaAppServer(params, allowSdkFallback) {
|
|
1721
|
+
const self = this;
|
|
1722
|
+
return new ReadableStream({
|
|
1723
|
+
start(controller) {
|
|
1724
|
+
(async () => {
|
|
1725
|
+
const tempFiles = [];
|
|
1726
|
+
try {
|
|
1727
|
+
const imageFiles = params.files?.filter((file) => file.type.startsWith("image/")) || [];
|
|
1728
|
+
const imagePaths = [];
|
|
1729
|
+
for (const file of imageFiles) {
|
|
1730
|
+
if (file.filePath && fs18.existsSync(file.filePath)) {
|
|
1731
|
+
imagePaths.push(file.filePath);
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
const ext = MIME_EXT[file.type] || ".png";
|
|
1735
|
+
const tmpPath = path19.join(
|
|
1736
|
+
os4.tmpdir(),
|
|
1737
|
+
`cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`
|
|
1738
|
+
);
|
|
1739
|
+
fs18.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
|
|
1740
|
+
tempFiles.push(tmpPath);
|
|
1741
|
+
imagePaths.push(tmpPath);
|
|
1742
|
+
}
|
|
1743
|
+
self.appServerClient ||= new CodexAppServerClient(self.pendingPermissions);
|
|
1744
|
+
let turn;
|
|
1745
|
+
try {
|
|
1746
|
+
turn = await self.appServerClient.startTurn({
|
|
1747
|
+
savedThreadId: self.threadIds.get(params.sessionId) || params.sdkSessionId || void 0,
|
|
1748
|
+
prompt: params.prompt,
|
|
1749
|
+
imagePaths,
|
|
1750
|
+
model: params.model,
|
|
1751
|
+
forceModel: params.forceModel,
|
|
1752
|
+
modelReasoningEffort: parseReasoningEffort(params.modelReasoningEffort),
|
|
1753
|
+
workingDirectory: params.workingDirectory,
|
|
1754
|
+
sandboxMode: normalizeSandboxMode(params.sandboxMode),
|
|
1755
|
+
permissionMode: params.permissionMode,
|
|
1756
|
+
systemPrompt: params.systemPrompt,
|
|
1757
|
+
skipGitRepoCheck: shouldSkipGitRepoCheck(),
|
|
1758
|
+
abortSignal: params.abortController?.signal
|
|
1759
|
+
});
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
if (allowSdkFallback && error instanceof AppServerPreTurnError) {
|
|
1762
|
+
console.warn("[codex-provider] app-server unavailable before turn/start; falling back to SDK:", error.message);
|
|
1763
|
+
const fallback = self.streamChatViaSdk(params).getReader();
|
|
1764
|
+
try {
|
|
1765
|
+
while (true) {
|
|
1766
|
+
const { done, value } = await fallback.read();
|
|
1767
|
+
if (done) break;
|
|
1768
|
+
controller.enqueue(value);
|
|
1769
|
+
}
|
|
1770
|
+
controller.close();
|
|
1771
|
+
} finally {
|
|
1772
|
+
fallback.releaseLock();
|
|
1773
|
+
}
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
throw error;
|
|
1777
|
+
}
|
|
1778
|
+
self.threadIds.set(params.sessionId, turn.threadId);
|
|
1779
|
+
controller.enqueue(sseEvent("status", {
|
|
1780
|
+
session_id: turn.threadId,
|
|
1781
|
+
turn_id: turn.turnId,
|
|
1782
|
+
transport: "app-server"
|
|
1783
|
+
}));
|
|
1784
|
+
const emittedToolStarts = /* @__PURE__ */ new Set();
|
|
1785
|
+
const streamedAgentItems = /* @__PURE__ */ new Set();
|
|
1786
|
+
const agentItemPhases = /* @__PURE__ */ new Map();
|
|
1787
|
+
const reasoningText = /* @__PURE__ */ new Map();
|
|
1788
|
+
let usage;
|
|
1789
|
+
let terminalSeen = false;
|
|
1790
|
+
for await (const event of turn.events) {
|
|
1791
|
+
if (params.abortController?.signal.aborted && event.type !== "turn.completed") {
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
switch (event.type) {
|
|
1795
|
+
case "agent_message.delta":
|
|
1796
|
+
if (event.delta) {
|
|
1797
|
+
if (agentItemPhases.get(event.itemId) === "commentary") {
|
|
1798
|
+
const accumulated = `${reasoningText.get(event.itemId) || ""}${event.delta}`;
|
|
1799
|
+
reasoningText.set(event.itemId, accumulated);
|
|
1800
|
+
controller.enqueue(sseEvent("status", { reasoning: accumulated }));
|
|
1801
|
+
break;
|
|
1802
|
+
}
|
|
1803
|
+
streamedAgentItems.add(event.itemId);
|
|
1804
|
+
controller.enqueue(sseEvent("text", event.delta));
|
|
1805
|
+
}
|
|
1806
|
+
break;
|
|
1807
|
+
case "reasoning.delta": {
|
|
1808
|
+
const accumulated = `${reasoningText.get(event.itemId) || ""}${event.delta}`;
|
|
1809
|
+
reasoningText.set(event.itemId, accumulated);
|
|
1810
|
+
if (accumulated.trim()) {
|
|
1811
|
+
controller.enqueue(sseEvent("status", { reasoning: accumulated }));
|
|
1812
|
+
}
|
|
1813
|
+
break;
|
|
1814
|
+
}
|
|
1815
|
+
case "item.started":
|
|
1816
|
+
case "item.completed": {
|
|
1817
|
+
const itemId = typeof event.item.id === "string" ? event.item.id : "";
|
|
1818
|
+
if (event.item.type === "agentMessage" && itemId) {
|
|
1819
|
+
const phase = typeof event.item.phase === "string" ? event.item.phase : "";
|
|
1820
|
+
if (phase) agentItemPhases.set(itemId, phase);
|
|
1821
|
+
if (phase === "commentary") {
|
|
1822
|
+
if (event.type === "item.completed" && !reasoningText.has(itemId)) {
|
|
1823
|
+
const text2 = typeof event.item.text === "string" ? event.item.text : "";
|
|
1824
|
+
if (text2.trim()) controller.enqueue(sseEvent("status", { reasoning: text2 }));
|
|
1825
|
+
}
|
|
1826
|
+
break;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const normalized = normalizeAppServerItem(event.item);
|
|
1830
|
+
if (!normalized) break;
|
|
1831
|
+
if (event.type === "item.completed" && normalized.type === "agent_message" && streamedAgentItems.has(normalized.id)) {
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
self.handleItemEvent(
|
|
1835
|
+
controller,
|
|
1836
|
+
normalized,
|
|
1837
|
+
event.type === "item.started" ? "started" : "completed",
|
|
1838
|
+
params.sessionId,
|
|
1839
|
+
emittedToolStarts
|
|
1840
|
+
);
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
case "plan.updated": {
|
|
1844
|
+
const tasks = event.plan.map((step) => ({
|
|
1845
|
+
text: step.step,
|
|
1846
|
+
status: step.status === "completed" ? "completed" : step.status === "inProgress" || step.status === "in_progress" ? "in_progress" : "pending"
|
|
1847
|
+
}));
|
|
1848
|
+
controller.enqueue(sseEvent("task_update", {
|
|
1849
|
+
session_id: turn.threadId,
|
|
1850
|
+
sdk_session_id: turn.threadId,
|
|
1851
|
+
tasks,
|
|
1852
|
+
todos: tasks
|
|
1853
|
+
}));
|
|
1854
|
+
break;
|
|
1855
|
+
}
|
|
1856
|
+
case "usage.updated":
|
|
1857
|
+
usage = mapCodexUsage(event.usage);
|
|
1858
|
+
break;
|
|
1859
|
+
case "model.updated":
|
|
1860
|
+
controller.enqueue(sseEvent("status", { model: event.model }));
|
|
1861
|
+
break;
|
|
1862
|
+
case "permission.request":
|
|
1863
|
+
controller.enqueue(sseEvent("permission_request", {
|
|
1864
|
+
permissionRequestId: event.requestId,
|
|
1865
|
+
toolName: event.toolName,
|
|
1866
|
+
toolInput: event.toolInput,
|
|
1867
|
+
suggestions: event.suggestions
|
|
1868
|
+
}));
|
|
1869
|
+
break;
|
|
1870
|
+
case "warning":
|
|
1871
|
+
controller.enqueue(sseEvent("status", { reasoning: event.message }));
|
|
1872
|
+
break;
|
|
1873
|
+
case "turn.completed":
|
|
1874
|
+
terminalSeen = true;
|
|
1875
|
+
if (event.status === "completed") {
|
|
1876
|
+
controller.enqueue(sseEvent("result", {
|
|
1877
|
+
usage,
|
|
1878
|
+
session_id: turn.threadId,
|
|
1879
|
+
turn_id: turn.turnId
|
|
1880
|
+
}));
|
|
1881
|
+
} else if (event.status === "failed") {
|
|
1882
|
+
self.clearCachedThreadId(params.sessionId);
|
|
1883
|
+
controller.enqueue(sseEvent(
|
|
1884
|
+
"error",
|
|
1885
|
+
normalizeCodexErrorMessage(event.errorMessage || "Turn failed")
|
|
1886
|
+
));
|
|
1887
|
+
} else if (!params.abortController?.signal.aborted) {
|
|
1888
|
+
controller.enqueue(sseEvent("error", event.errorMessage || "Task interrupted"));
|
|
1889
|
+
}
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
if (!terminalSeen && !params.abortController?.signal.aborted) {
|
|
1894
|
+
throw new Error("Codex app-server stream ended without turn/completed");
|
|
1895
|
+
}
|
|
1896
|
+
controller.close();
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1899
|
+
console.error("[codex-provider] app-server error:", error instanceof Error ? error.stack || error.message : error);
|
|
1900
|
+
self.clearCachedThreadId(params.sessionId);
|
|
1901
|
+
if (params.abortController?.signal.aborted) {
|
|
1902
|
+
try {
|
|
1903
|
+
controller.close();
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
try {
|
|
1909
|
+
controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(message)));
|
|
1910
|
+
controller.close();
|
|
1911
|
+
} catch {
|
|
1912
|
+
}
|
|
1913
|
+
} finally {
|
|
1914
|
+
for (const tempFile of tempFiles) {
|
|
1915
|
+
try {
|
|
1916
|
+
fs18.unlinkSync(tempFile);
|
|
719
1917
|
} catch {
|
|
720
1918
|
}
|
|
721
1919
|
}
|
|
@@ -845,9 +2043,9 @@ var init_codex_provider = __esm({
|
|
|
845
2043
|
});
|
|
846
2044
|
|
|
847
2045
|
// src/main.ts
|
|
848
|
-
import
|
|
849
|
-
import
|
|
850
|
-
import
|
|
2046
|
+
import fs19 from "node:fs";
|
|
2047
|
+
import path20 from "node:path";
|
|
2048
|
+
import crypto12 from "node:crypto";
|
|
851
2049
|
|
|
852
2050
|
// src/lib/bridge/context.ts
|
|
853
2051
|
var CONTEXT_KEY = "__bridge_context__";
|
|
@@ -3286,20 +4484,20 @@ ${trimmedResponse}`;
|
|
|
3286
4484
|
buffer = Buffer.concat(chunks);
|
|
3287
4485
|
} catch (streamErr) {
|
|
3288
4486
|
console.warn("[feishu-adapter] Stream read failed, falling back to writeFile:", streamErr instanceof Error ? streamErr.message : streamErr);
|
|
3289
|
-
const
|
|
4487
|
+
const fs20 = await import("fs");
|
|
3290
4488
|
const os5 = await import("os");
|
|
3291
|
-
const
|
|
3292
|
-
const tmpPath =
|
|
4489
|
+
const path21 = await import("path");
|
|
4490
|
+
const tmpPath = path21.join(os5.tmpdir(), `feishu-dl-${crypto3.randomUUID()}`);
|
|
3293
4491
|
try {
|
|
3294
4492
|
await res.writeFile(tmpPath);
|
|
3295
|
-
buffer =
|
|
4493
|
+
buffer = fs20.readFileSync(tmpPath);
|
|
3296
4494
|
if (buffer.length > MAX_FILE_SIZE) {
|
|
3297
4495
|
console.warn(`[feishu-adapter] Resource too large (>${MAX_FILE_SIZE} bytes), key: ${fileKey}`);
|
|
3298
4496
|
return null;
|
|
3299
4497
|
}
|
|
3300
4498
|
} finally {
|
|
3301
4499
|
try {
|
|
3302
|
-
|
|
4500
|
+
fs20.unlinkSync(tmpPath);
|
|
3303
4501
|
} catch {
|
|
3304
4502
|
}
|
|
3305
4503
|
}
|
|
@@ -10331,11 +11529,25 @@ import fs5 from "node:fs";
|
|
|
10331
11529
|
import os2 from "node:os";
|
|
10332
11530
|
import path4 from "node:path";
|
|
10333
11531
|
import crypto7 from "node:crypto";
|
|
10334
|
-
import {
|
|
11532
|
+
import { createRequire } from "node:module";
|
|
10335
11533
|
var ACTIVE_WINDOW_MS = 15 * 60 * 1e3;
|
|
10336
11534
|
var MAX_SESSION_META_BYTES = 4 * 1024 * 1024;
|
|
10337
11535
|
var MAX_SESSION_TITLE_SCAN_BYTES = 512 * 1024;
|
|
11536
|
+
var INITIAL_HISTORY_TAIL_BYTES = 256 * 1024;
|
|
11537
|
+
var MAX_HISTORY_TAIL_BYTES = 64 * 1024 * 1024;
|
|
10338
11538
|
var TITLE_MAX_CHARS = 72;
|
|
11539
|
+
var localRequire = createRequire(import.meta.url);
|
|
11540
|
+
var databaseSyncConstructor;
|
|
11541
|
+
function getDatabaseSyncConstructor() {
|
|
11542
|
+
if (databaseSyncConstructor !== void 0) return databaseSyncConstructor;
|
|
11543
|
+
try {
|
|
11544
|
+
const sqlite = localRequire("node:sqlite");
|
|
11545
|
+
databaseSyncConstructor = typeof sqlite.DatabaseSync === "function" ? sqlite.DatabaseSync : null;
|
|
11546
|
+
} catch {
|
|
11547
|
+
databaseSyncConstructor = null;
|
|
11548
|
+
}
|
|
11549
|
+
return databaseSyncConstructor;
|
|
11550
|
+
}
|
|
10339
11551
|
function getCodexHome() {
|
|
10340
11552
|
return process.env.CODEX_HOME || path4.join(os2.homedir(), ".codex");
|
|
10341
11553
|
}
|
|
@@ -10478,10 +11690,37 @@ function walkSessionFiles(dirPath, target) {
|
|
|
10478
11690
|
}
|
|
10479
11691
|
}
|
|
10480
11692
|
}
|
|
11693
|
+
function normalizeSourceKind(value) {
|
|
11694
|
+
if (typeof value === "string") {
|
|
11695
|
+
const normalized = value.trim();
|
|
11696
|
+
if (!normalized) return "";
|
|
11697
|
+
if (normalized.startsWith("{")) {
|
|
11698
|
+
try {
|
|
11699
|
+
return normalizeSourceKind(JSON.parse(normalized));
|
|
11700
|
+
} catch {
|
|
11701
|
+
return normalized.toLowerCase();
|
|
11702
|
+
}
|
|
11703
|
+
}
|
|
11704
|
+
return normalized.toLowerCase();
|
|
11705
|
+
}
|
|
11706
|
+
if (!value || typeof value !== "object") return "";
|
|
11707
|
+
const record = value;
|
|
11708
|
+
if ("subagent" in record || "sub_agent" in record) return "subagent";
|
|
11709
|
+
for (const key of ["type", "kind", "source", "name"]) {
|
|
11710
|
+
const nested = normalizeSourceKind(record[key]);
|
|
11711
|
+
if (nested) return nested;
|
|
11712
|
+
}
|
|
11713
|
+
return "";
|
|
11714
|
+
}
|
|
11715
|
+
function isSubagentThread(source, threadSource, parentThreadId) {
|
|
11716
|
+
if (typeof parentThreadId === "string" && parentThreadId.trim()) return true;
|
|
11717
|
+
return normalizeSourceKind(threadSource) === "subagent" || normalizeSourceKind(source) === "subagent";
|
|
11718
|
+
}
|
|
10481
11719
|
function isDesktopLike(meta) {
|
|
10482
11720
|
const originator = typeof meta?.originator === "string" ? meta.originator.toLowerCase() : "";
|
|
10483
|
-
const source =
|
|
11721
|
+
const source = normalizeSourceKind(meta?.source);
|
|
10484
11722
|
if (source === "exec") return false;
|
|
11723
|
+
if (isSubagentThread(meta?.source, meta?.thread_source, meta?.parent_thread_id)) return false;
|
|
10485
11724
|
return originator.includes("desktop") || source === "vscode" || source === "desktop";
|
|
10486
11725
|
}
|
|
10487
11726
|
function loadThreadIndexEntries(archivedThreadIds) {
|
|
@@ -10530,25 +11769,46 @@ function parseUpdatedAtValue(value) {
|
|
|
10530
11769
|
function loadVisibleDesktopThreads(limit) {
|
|
10531
11770
|
const dbPath = getDesktopStateDbPath();
|
|
10532
11771
|
if (!dbPath || !fs5.existsSync(dbPath)) return null;
|
|
11772
|
+
const DatabaseSync = getDatabaseSyncConstructor();
|
|
11773
|
+
if (!DatabaseSync) return null;
|
|
10533
11774
|
let db = null;
|
|
10534
11775
|
try {
|
|
10535
11776
|
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
11777
|
+
const columns = new Set(
|
|
11778
|
+
db.prepare("PRAGMA table_info(threads)").all().map((row) => row && typeof row === "object" ? row.name : null).filter((name) => typeof name === "string")
|
|
11779
|
+
);
|
|
11780
|
+
if (!columns.has("id") || !columns.has("updated_at")) return null;
|
|
10536
11781
|
const hasLimit = typeof limit === "number" && Number.isFinite(limit) && limit > 0;
|
|
11782
|
+
const selectedColumns = [
|
|
11783
|
+
"id",
|
|
11784
|
+
"updated_at",
|
|
11785
|
+
...["updated_at_ms", "rollout_path", "title", "cwd", "source", "thread_source"].filter((column) => columns.has(column))
|
|
11786
|
+
];
|
|
11787
|
+
const whereClauses = columns.has("archived") ? ["archived = 0"] : ["1 = 1"];
|
|
11788
|
+
if (columns.has("source")) whereClauses.push("COALESCE(source, '') != 'exec'");
|
|
11789
|
+
if (columns.has("thread_source")) whereClauses.push("COALESCE(thread_source, '') != 'subagent'");
|
|
10537
11790
|
const sql = `
|
|
10538
|
-
SELECT
|
|
11791
|
+
SELECT ${selectedColumns.join(", ")}
|
|
10539
11792
|
FROM threads
|
|
10540
|
-
WHERE
|
|
10541
|
-
|
|
10542
|
-
ORDER BY updated_at DESC
|
|
11793
|
+
WHERE ${whereClauses.join("\n AND ")}
|
|
11794
|
+
ORDER BY ${columns.has("updated_at_ms") ? "updated_at_ms" : "updated_at"} DESC
|
|
10543
11795
|
${hasLimit ? "LIMIT ?" : ""}
|
|
10544
11796
|
`;
|
|
10545
11797
|
const rows = hasLimit ? db.prepare(sql).all(Math.max(1, Math.floor(limit))) : db.prepare(sql).all();
|
|
10546
11798
|
const ids = rows.map((row) => {
|
|
10547
|
-
|
|
11799
|
+
if (!row || typeof row !== "object") return null;
|
|
11800
|
+
const record = row;
|
|
11801
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
10548
11802
|
if (!id) return null;
|
|
11803
|
+
if (isSubagentThread(record.source, record.thread_source)) return null;
|
|
10549
11804
|
return {
|
|
10550
11805
|
id,
|
|
10551
|
-
updatedAtMs: parseUpdatedAtValue(
|
|
11806
|
+
updatedAtMs: parseUpdatedAtValue(record.updated_at_ms ?? record.updated_at),
|
|
11807
|
+
...typeof record.rollout_path === "string" && record.rollout_path.trim() ? { rolloutPath: record.rollout_path.trim() } : {},
|
|
11808
|
+
...typeof record.title === "string" && trimTitle(record.title) ? { title: trimTitle(record.title) } : {},
|
|
11809
|
+
...typeof record.cwd === "string" ? { cwd: record.cwd } : {},
|
|
11810
|
+
...typeof record.source === "string" ? { source: record.source } : {},
|
|
11811
|
+
...typeof record.thread_source === "string" ? { threadSource: record.thread_source } : {}
|
|
10552
11812
|
};
|
|
10553
11813
|
}).filter((row) => Boolean(row));
|
|
10554
11814
|
return ids.length > 0 ? ids : null;
|
|
@@ -10579,7 +11839,7 @@ function buildFallbackTitle(threadId, filePath, cwd) {
|
|
|
10579
11839
|
if (dirName) return dirName;
|
|
10580
11840
|
return `Session ${threadId.slice(0, 8)}`;
|
|
10581
11841
|
}
|
|
10582
|
-
function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
|
|
11842
|
+
function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds, visibleThread) {
|
|
10583
11843
|
const firstLine = readFirstLine(filePath);
|
|
10584
11844
|
if (!firstLine) return null;
|
|
10585
11845
|
let parsed;
|
|
@@ -10591,7 +11851,8 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
|
|
|
10591
11851
|
if (parsed.type !== "session_meta" || !parsed.payload?.id || !isDesktopLike(parsed.payload)) {
|
|
10592
11852
|
return null;
|
|
10593
11853
|
}
|
|
10594
|
-
|
|
11854
|
+
const threadId = visibleThread?.id || parsed.payload.id;
|
|
11855
|
+
if (archivedThreadIds.has(threadId)) {
|
|
10595
11856
|
return null;
|
|
10596
11857
|
}
|
|
10597
11858
|
let stat;
|
|
@@ -10600,20 +11861,19 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
|
|
|
10600
11861
|
} catch {
|
|
10601
11862
|
return null;
|
|
10602
11863
|
}
|
|
10603
|
-
const cwd = parsed.payload.cwd || "";
|
|
11864
|
+
const cwd = visibleThread?.cwd || parsed.payload.cwd || "";
|
|
10604
11865
|
if (isInternalSkillWorkspace(cwd)) {
|
|
10605
11866
|
return null;
|
|
10606
11867
|
}
|
|
10607
|
-
const lastEventAt = stat.mtime.toISOString();
|
|
11868
|
+
const lastEventAt = visibleThread?.updatedAtMs ? new Date(visibleThread.updatedAtMs).toISOString() : stat.mtime.toISOString();
|
|
10608
11869
|
const firstSeenAt = parsed.payload.timestamp || parsed.timestamp || stat.birthtime.toISOString();
|
|
10609
|
-
const
|
|
10610
|
-
const title = threadIndexEntries.get(threadId)?.title || buildFallbackTitle(threadId, filePath, cwd);
|
|
11870
|
+
const title = visibleThread?.title || threadIndexEntries.get(threadId)?.title || buildFallbackTitle(threadId, filePath, cwd);
|
|
10611
11871
|
return {
|
|
10612
11872
|
threadId,
|
|
10613
11873
|
filePath,
|
|
10614
11874
|
cwd,
|
|
10615
11875
|
originator: typeof parsed.payload.originator === "string" ? parsed.payload.originator : "Codex Desktop",
|
|
10616
|
-
source: typeof parsed.payload.source === "string" ? parsed.payload.source : void 0,
|
|
11876
|
+
source: visibleThread?.source || (typeof parsed.payload.source === "string" ? parsed.payload.source : void 0),
|
|
10617
11877
|
cliVersion: typeof parsed.payload.cli_version === "string" ? parsed.payload.cli_version : void 0,
|
|
10618
11878
|
firstSeenAt,
|
|
10619
11879
|
lastEventAt,
|
|
@@ -10826,6 +12086,7 @@ function isTurnContextLine(line) {
|
|
|
10826
12086
|
}
|
|
10827
12087
|
var IGNORED_EVENT_MSG_TYPES = /* @__PURE__ */ new Set([
|
|
10828
12088
|
"context_compacted",
|
|
12089
|
+
"sub_agent_activity",
|
|
10829
12090
|
"thread_settings_applied",
|
|
10830
12091
|
"thread_goal_updated",
|
|
10831
12092
|
"thread_name_updated",
|
|
@@ -10833,11 +12094,13 @@ var IGNORED_EVENT_MSG_TYPES = /* @__PURE__ */ new Set([
|
|
|
10833
12094
|
"token_count"
|
|
10834
12095
|
]);
|
|
10835
12096
|
var IGNORED_RESPONSE_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
12097
|
+
"agent_message",
|
|
10836
12098
|
"image_generation_call",
|
|
10837
12099
|
"web_search_call"
|
|
10838
12100
|
]);
|
|
10839
12101
|
var IGNORED_TOP_LEVEL_TYPES = /* @__PURE__ */ new Set([
|
|
10840
12102
|
"compacted",
|
|
12103
|
+
"inter_agent_communication_metadata",
|
|
10841
12104
|
"session_meta",
|
|
10842
12105
|
"turn_context",
|
|
10843
12106
|
"world_state"
|
|
@@ -10853,6 +12116,13 @@ function isTerminalCompletionEventType(value) {
|
|
|
10853
12116
|
function getEventTurnId(payload) {
|
|
10854
12117
|
return payload?.turn_id || payload?.turnId || "";
|
|
10855
12118
|
}
|
|
12119
|
+
function getResponseItemTurnId(payload) {
|
|
12120
|
+
const metadata = payload?.internal_chat_message_metadata_passthrough;
|
|
12121
|
+
return extractNormalizedFreeText(metadata?.turn_id ?? metadata?.turnId);
|
|
12122
|
+
}
|
|
12123
|
+
function getResponseItemClientId(payload) {
|
|
12124
|
+
return extractNormalizedFreeText(payload?.client_id);
|
|
12125
|
+
}
|
|
10856
12126
|
function extractTerminalCompletionText(payload) {
|
|
10857
12127
|
if (!payload) return "";
|
|
10858
12128
|
for (const value of [
|
|
@@ -10906,12 +12176,31 @@ function listDesktopSessions(limit) {
|
|
|
10906
12176
|
const visibleThreadIds = visibleThreads?.map((thread) => thread.id) || null;
|
|
10907
12177
|
const visibleThreadSet = visibleThreadIds ? new Set(visibleThreadIds) : null;
|
|
10908
12178
|
const visibleThreadUpdatedAt = new Map(visibleThreads?.map((thread) => [thread.id, thread.updatedAtMs]) || []);
|
|
12179
|
+
const visibleThreadRows = new Map(visibleThreads?.map((thread) => [thread.id, thread]) || []);
|
|
10909
12180
|
const oldestVisibleUpdatedAtMs = visibleThreads && visibleThreads.length > 0 ? Math.min(...visibleThreads.map((thread) => thread.updatedAtMs || Number.MAX_SAFE_INTEGER)) : 0;
|
|
10910
12181
|
const files = [];
|
|
10911
12182
|
walkSessionFiles(root, files);
|
|
10912
12183
|
const allSessions = /* @__PURE__ */ new Map();
|
|
12184
|
+
for (const thread of visibleThreads || []) {
|
|
12185
|
+
if (!thread.rolloutPath || !fs5.existsSync(thread.rolloutPath)) continue;
|
|
12186
|
+
const session = parseDesktopSession(
|
|
12187
|
+
thread.rolloutPath,
|
|
12188
|
+
threadIndexEntries,
|
|
12189
|
+
archivedThreadIds,
|
|
12190
|
+
thread
|
|
12191
|
+
);
|
|
12192
|
+
if (!session || !isWithinSavedWorkspaceRoots(session.cwd, savedWorkspaceRoots)) continue;
|
|
12193
|
+
allSessions.set(session.threadId, session);
|
|
12194
|
+
}
|
|
10913
12195
|
for (const filePath of files) {
|
|
10914
|
-
const
|
|
12196
|
+
const threadId = extractThreadIdFromRolloutName(path4.basename(filePath));
|
|
12197
|
+
if (threadId && allSessions.has(threadId)) continue;
|
|
12198
|
+
const session = parseDesktopSession(
|
|
12199
|
+
filePath,
|
|
12200
|
+
threadIndexEntries,
|
|
12201
|
+
archivedThreadIds,
|
|
12202
|
+
threadId ? visibleThreadRows.get(threadId) : void 0
|
|
12203
|
+
);
|
|
10915
12204
|
if (!session) continue;
|
|
10916
12205
|
if (!isWithinSavedWorkspaceRoots(session.cwd, savedWorkspaceRoots)) continue;
|
|
10917
12206
|
allSessions.set(session.threadId, session);
|
|
@@ -11210,6 +12499,8 @@ function pushDesktopMirrorEventRecord(records, parsed, rawLine, activeTurnId) {
|
|
|
11210
12499
|
function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId, activeSpecialCallIds) {
|
|
11211
12500
|
const signature = createDesktopEventSignature(rawLine);
|
|
11212
12501
|
const timestamp = parsed.timestamp || "";
|
|
12502
|
+
activeTurnId = getResponseItemTurnId(parsed.payload) || activeTurnId;
|
|
12503
|
+
const clientId = getResponseItemClientId(parsed.payload);
|
|
11213
12504
|
if (isIgnoredMirrorLineKind(parsed)) {
|
|
11214
12505
|
return true;
|
|
11215
12506
|
}
|
|
@@ -11221,7 +12512,8 @@ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId,
|
|
|
11221
12512
|
type: "reasoning",
|
|
11222
12513
|
content: text2,
|
|
11223
12514
|
timestamp,
|
|
11224
|
-
...activeTurnId ? { turnId: activeTurnId } : {}
|
|
12515
|
+
...activeTurnId ? { turnId: activeTurnId } : {},
|
|
12516
|
+
...clientId ? { clientId } : {}
|
|
11225
12517
|
});
|
|
11226
12518
|
return true;
|
|
11227
12519
|
}
|
|
@@ -11234,7 +12526,8 @@ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId,
|
|
|
11234
12526
|
role: parsed.payload.phase === "commentary" ? "commentary" : "assistant",
|
|
11235
12527
|
content: parsed.payload.phase === "commentary" ? text2.replace(/^\[commentary\]\n/, "") : text2,
|
|
11236
12528
|
timestamp,
|
|
11237
|
-
...activeTurnId ? { turnId: activeTurnId } : {}
|
|
12529
|
+
...activeTurnId ? { turnId: activeTurnId } : {},
|
|
12530
|
+
...clientId ? { clientId } : {}
|
|
11238
12531
|
});
|
|
11239
12532
|
return true;
|
|
11240
12533
|
}
|
|
@@ -11251,7 +12544,8 @@ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId,
|
|
|
11251
12544
|
role: "user",
|
|
11252
12545
|
content: text2,
|
|
11253
12546
|
timestamp,
|
|
11254
|
-
...activeTurnId ? { turnId: activeTurnId } : {}
|
|
12547
|
+
...activeTurnId ? { turnId: activeTurnId } : {},
|
|
12548
|
+
...clientId ? { clientId } : {}
|
|
11255
12549
|
});
|
|
11256
12550
|
return true;
|
|
11257
12551
|
}
|
|
@@ -11475,23 +12769,67 @@ function parseDesktopMirrorRecordText(content, leadingText = "", flushTrailingTe
|
|
|
11475
12769
|
unknownKinds: Array.from(unknownKinds)
|
|
11476
12770
|
};
|
|
11477
12771
|
}
|
|
11478
|
-
function
|
|
11479
|
-
|
|
12772
|
+
function mapDesktopEventsToMessages(events) {
|
|
12773
|
+
return events.map((event) => ({
|
|
11480
12774
|
role: event.role === "commentary" ? "assistant" : event.role,
|
|
11481
12775
|
content: event.role === "commentary" ? `[commentary]
|
|
11482
12776
|
${event.content}` : event.content
|
|
11483
12777
|
}));
|
|
11484
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8;
|
|
11485
|
-
return messages.slice(-safeLimit);
|
|
11486
12778
|
}
|
|
11487
|
-
function
|
|
11488
|
-
|
|
12779
|
+
function readUtf8FileTail(filePath, maxBytes) {
|
|
12780
|
+
const stat = fs5.statSync(filePath);
|
|
12781
|
+
const bytesToRead = Math.min(stat.size, Math.max(1, maxBytes));
|
|
12782
|
+
const startOffset = Math.max(0, stat.size - bytesToRead);
|
|
12783
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
12784
|
+
const fd = fs5.openSync(filePath, "r");
|
|
11489
12785
|
try {
|
|
11490
|
-
|
|
12786
|
+
let totalRead = 0;
|
|
12787
|
+
while (totalRead < bytesToRead) {
|
|
12788
|
+
const bytesRead = fs5.readSync(
|
|
12789
|
+
fd,
|
|
12790
|
+
buffer,
|
|
12791
|
+
totalRead,
|
|
12792
|
+
bytesToRead - totalRead,
|
|
12793
|
+
startOffset + totalRead
|
|
12794
|
+
);
|
|
12795
|
+
if (bytesRead <= 0) break;
|
|
12796
|
+
totalRead += bytesRead;
|
|
12797
|
+
}
|
|
12798
|
+
let contentBuffer = buffer.subarray(0, totalRead);
|
|
12799
|
+
if (startOffset > 0) {
|
|
12800
|
+
const firstNewline = contentBuffer.indexOf(10);
|
|
12801
|
+
contentBuffer = firstNewline >= 0 ? contentBuffer.subarray(firstNewline + 1) : Buffer.alloc(0);
|
|
12802
|
+
}
|
|
12803
|
+
return {
|
|
12804
|
+
content: contentBuffer.toString("utf-8"),
|
|
12805
|
+
reachedStart: startOffset === 0
|
|
12806
|
+
};
|
|
12807
|
+
} finally {
|
|
12808
|
+
fs5.closeSync(fd);
|
|
12809
|
+
}
|
|
12810
|
+
}
|
|
12811
|
+
function readDesktopSessionMessagesByFilePath(filePath, limit = 8) {
|
|
12812
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8;
|
|
12813
|
+
const targetEventCount = safeLimit + 4;
|
|
12814
|
+
let bytesToRead = INITIAL_HISTORY_TAIL_BYTES;
|
|
12815
|
+
let events = [];
|
|
12816
|
+
try {
|
|
12817
|
+
while (bytesToRead <= MAX_HISTORY_TAIL_BYTES) {
|
|
12818
|
+
const tail = readUtf8FileTail(filePath, bytesToRead);
|
|
12819
|
+
events = parseDesktopSessionEventText(tail.content, "", true).events;
|
|
12820
|
+
if (tail.reachedStart || events.length >= targetEventCount) break;
|
|
12821
|
+
if (bytesToRead === MAX_HISTORY_TAIL_BYTES) break;
|
|
12822
|
+
bytesToRead = Math.min(MAX_HISTORY_TAIL_BYTES, bytesToRead * 2);
|
|
12823
|
+
}
|
|
11491
12824
|
} catch {
|
|
11492
12825
|
return [];
|
|
11493
12826
|
}
|
|
11494
|
-
return
|
|
12827
|
+
return mapDesktopEventsToMessages(events).slice(-safeLimit);
|
|
12828
|
+
}
|
|
12829
|
+
function readDesktopSessionMessages(threadId, limit = 8) {
|
|
12830
|
+
const session = getDesktopSessionByThreadId(threadId);
|
|
12831
|
+
if (!session) return [];
|
|
12832
|
+
return readDesktopSessionMessagesByFilePath(session.filePath, limit);
|
|
11495
12833
|
}
|
|
11496
12834
|
function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, endOffset, trailingText = "", currentTurnId = null, currentSpecialCallIds = []) {
|
|
11497
12835
|
let range;
|
|
@@ -11523,11 +12861,6 @@ function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, en
|
|
|
11523
12861
|
unknownKinds: parsed.unknownKinds
|
|
11524
12862
|
};
|
|
11525
12863
|
}
|
|
11526
|
-
function readDesktopSessionEventStream(threadId) {
|
|
11527
|
-
const session = getDesktopSessionByThreadId(threadId);
|
|
11528
|
-
if (!session) return [];
|
|
11529
|
-
return readDesktopSessionEventStreamByFilePath(session.filePath);
|
|
11530
|
-
}
|
|
11531
12864
|
|
|
11532
12865
|
// src/lib/bridge/turns/turn-classifier.ts
|
|
11533
12866
|
function normalizeThreadId(value) {
|
|
@@ -13145,7 +14478,7 @@ function abortMirrorSuppression(store, sessionId, config2, suppressionId, nowMs
|
|
|
13145
14478
|
}
|
|
13146
14479
|
target.until = nowMs + config2.suppressionWindowMs;
|
|
13147
14480
|
}
|
|
13148
|
-
function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs = Date.now()) {
|
|
14481
|
+
function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs = Date.now(), onTurnAssociated) {
|
|
13149
14482
|
const suppressions = getMirrorSuppressionStates(store, sessionId, nowMs);
|
|
13150
14483
|
const ignoredTurnIds = cleanupIgnoredMirrorTurns(store, sessionId, nowMs);
|
|
13151
14484
|
if (suppressions.length === 0 && ignoredTurnIds.size === 0 || records.length === 0) return records;
|
|
@@ -13182,6 +14515,9 @@ function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs
|
|
|
13182
14515
|
suppression.awaitingPromptMatch = false;
|
|
13183
14516
|
suppression.droppingTurn = true;
|
|
13184
14517
|
suppression.activeTurnId = record.turnId || suppression.candidateTurnId || null;
|
|
14518
|
+
if (suppression.activeTurnId) {
|
|
14519
|
+
onTurnAssociated?.(sessionId, suppression.activeTurnId);
|
|
14520
|
+
}
|
|
13185
14521
|
handled = true;
|
|
13186
14522
|
break;
|
|
13187
14523
|
}
|
|
@@ -13241,6 +14577,29 @@ function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs
|
|
|
13241
14577
|
}
|
|
13242
14578
|
return filtered;
|
|
13243
14579
|
}
|
|
14580
|
+
function associateMirrorSuppressionTurns(store, sessionId, records, nowMs = Date.now(), onTurnAssociated) {
|
|
14581
|
+
const suppressions = getMirrorSuppressionStates(store, sessionId, nowMs);
|
|
14582
|
+
if (suppressions.length === 0 || records.length === 0) return;
|
|
14583
|
+
const suppression = suppressions[0];
|
|
14584
|
+
if (!suppression.awaitingPromptMatch) return;
|
|
14585
|
+
for (const record of records) {
|
|
14586
|
+
if (record.type === "task_started" && record.turnId) {
|
|
14587
|
+
suppression.candidateTurnId = record.turnId;
|
|
14588
|
+
continue;
|
|
14589
|
+
}
|
|
14590
|
+
if (record.type !== "message" || record.role !== "user") continue;
|
|
14591
|
+
if (isSyntheticDesktopUserContext(record.content || "")) continue;
|
|
14592
|
+
const normalizedContent = normalizeMirrorPromptText(record.content || "");
|
|
14593
|
+
if (!suppression.promptText || normalizedContent !== suppression.promptText) break;
|
|
14594
|
+
const turnId = record.turnId || suppression.candidateTurnId;
|
|
14595
|
+
if (!turnId) break;
|
|
14596
|
+
suppression.awaitingPromptMatch = false;
|
|
14597
|
+
suppression.droppingTurn = true;
|
|
14598
|
+
suppression.activeTurnId = turnId;
|
|
14599
|
+
onTurnAssociated?.(sessionId, turnId);
|
|
14600
|
+
break;
|
|
14601
|
+
}
|
|
14602
|
+
}
|
|
13244
14603
|
|
|
13245
14604
|
// src/lib/bridge/adapter-sync-plan.ts
|
|
13246
14605
|
function stableFingerprintValue(value) {
|
|
@@ -15030,6 +16389,8 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
|
|
|
15030
16389
|
prompt: promptText,
|
|
15031
16390
|
sessionId,
|
|
15032
16391
|
sdkSessionId: binding.sdkSessionId || void 0,
|
|
16392
|
+
sessionOrigin: session?.thread_origin === "desktop" ? "desktop" : "bridge",
|
|
16393
|
+
desktopThreadId: session?.desktop_thread_id || void 0,
|
|
15033
16394
|
model: effectiveModel,
|
|
15034
16395
|
forceModel: !binding.sdkSessionId && Boolean(effectiveModel),
|
|
15035
16396
|
sandboxMode,
|
|
@@ -16689,7 +18050,9 @@ async function runMirrorReconcileBatch(deps) {
|
|
|
16689
18050
|
}
|
|
16690
18051
|
|
|
16691
18052
|
// src/lib/bridge/mirror-runtime.ts
|
|
18053
|
+
var MIRROR_ORPHAN_PROCESS_PROBE_INTERVAL_MS = 6e4;
|
|
16692
18054
|
function createMirrorRuntime(getState2, options, deps) {
|
|
18055
|
+
const orphanProbeAt = /* @__PURE__ */ new Map();
|
|
16693
18056
|
function isAtOrBefore(timestamp, boundary) {
|
|
16694
18057
|
if (!timestamp) return true;
|
|
16695
18058
|
const timestampMs = Date.parse(timestamp);
|
|
@@ -16776,8 +18139,35 @@ function createMirrorRuntime(getState2, options, deps) {
|
|
|
16776
18139
|
deps.stopMirrorStreaming(existing);
|
|
16777
18140
|
closeMirrorWatcher(existing);
|
|
16778
18141
|
state.mirrorSubscriptions.delete(bindingId);
|
|
18142
|
+
orphanProbeAt.delete(bindingId);
|
|
16779
18143
|
deps.syncMirrorSessionStateSafe(existing.sessionId, "mirror subscription removal");
|
|
16780
18144
|
}
|
|
18145
|
+
async function finalizeOrphanedMirrorStream(subscription, snapshot, blocked, runtimeBusy, nowMs) {
|
|
18146
|
+
const pendingTurn = subscription.pendingTurn;
|
|
18147
|
+
if (!pendingTurn?.streamStarted || blocked || runtimeBusy) return null;
|
|
18148
|
+
const timeoutMs = options.streamOrphanTimeoutMs;
|
|
18149
|
+
const lastActivityMs = Date.parse(pendingTurn.lastActivityAt);
|
|
18150
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isFinite(lastActivityMs)) {
|
|
18151
|
+
return null;
|
|
18152
|
+
}
|
|
18153
|
+
const sourceActivityMs = Math.max(lastActivityMs, snapshot.mtimeMs);
|
|
18154
|
+
if (nowMs - sourceActivityMs < timeoutMs) return null;
|
|
18155
|
+
const lastProbeAt = orphanProbeAt.get(subscription.bindingId);
|
|
18156
|
+
if (typeof lastProbeAt === "number" && nowMs - lastProbeAt < MIRROR_ORPHAN_PROCESS_PROBE_INTERVAL_MS) {
|
|
18157
|
+
return null;
|
|
18158
|
+
}
|
|
18159
|
+
orphanProbeAt.set(subscription.bindingId, nowMs);
|
|
18160
|
+
if (!await deps.isThreadProcessDefinitelyGone(subscription.threadId)) return null;
|
|
18161
|
+
const finalized = flushTimedOutMirrorTurn(subscription, timeoutMs, nowMs);
|
|
18162
|
+
if (!finalized) return null;
|
|
18163
|
+
orphanProbeAt.delete(subscription.bindingId);
|
|
18164
|
+
const detail = "\u684C\u9762\u7EBF\u7A0B\u957F\u65F6\u95F4\u6CA1\u6709\u65B0\u8BB0\u5F55\uFF0C\u4E14\u672C\u673A\u672A\u627E\u5230\u5BF9\u5E94\u8FDB\u7A0B\uFF1B\u5DF2\u7ED3\u675F\u5B64\u7ACB\u7684\u6D41\u5F0F\u540C\u6B65\u3002";
|
|
18165
|
+
deps.recordOrphanedMirrorTurn(subscription.sessionId, detail);
|
|
18166
|
+
console.warn(
|
|
18167
|
+
`[bridge-manager] Finalized orphaned mirror turn ${pendingTurn.turnId || pendingTurn.streamKey} for thread ${subscription.threadId}`
|
|
18168
|
+
);
|
|
18169
|
+
return finalized;
|
|
18170
|
+
}
|
|
16781
18171
|
function clearDanglingMirrorThread(subscription, reason) {
|
|
16782
18172
|
const { store } = getBridgeContext();
|
|
16783
18173
|
const session = store.getSession(subscription.sessionId);
|
|
@@ -16835,6 +18225,7 @@ function createMirrorRuntime(getState2, options, deps) {
|
|
|
16835
18225
|
});
|
|
16836
18226
|
if (threadChanged || filePathChanged) {
|
|
16837
18227
|
deps.stopMirrorStreaming(existing);
|
|
18228
|
+
orphanProbeAt.delete(existing.bindingId);
|
|
16838
18229
|
}
|
|
16839
18230
|
watchMirrorFile(existing, filePath);
|
|
16840
18231
|
if (previousSessionId !== binding.codepilotSessionId) {
|
|
@@ -16934,6 +18325,20 @@ function createMirrorRuntime(getState2, options, deps) {
|
|
|
16934
18325
|
flushTimedOutTurn: (currentSubscription) => deps.flushTimedOutMirrorTurn(currentSubscription),
|
|
16935
18326
|
consumeBufferedTurns: (currentSubscription) => deps.consumeBufferedMirrorTurns(currentSubscription)
|
|
16936
18327
|
});
|
|
18328
|
+
const runtimeBusy = session.runtime_status === "running" || session.runtime_status === "queued";
|
|
18329
|
+
const orphanedTurn = await finalizeOrphanedMirrorStream(
|
|
18330
|
+
subscription,
|
|
18331
|
+
snapshot,
|
|
18332
|
+
blocked,
|
|
18333
|
+
runtimeBusy,
|
|
18334
|
+
Date.now()
|
|
18335
|
+
);
|
|
18336
|
+
if (orphanedTurn) {
|
|
18337
|
+
deliveryPlan.finalizedTurns.push(orphanedTurn);
|
|
18338
|
+
deliveryPlan.syncReason = "mirror reconcile delivered turns";
|
|
18339
|
+
} else if (!subscription.pendingTurn?.streamStarted) {
|
|
18340
|
+
orphanProbeAt.delete(subscription.bindingId);
|
|
18341
|
+
}
|
|
16937
18342
|
if (deliveryPlan.finalizedTurns.length > 0) {
|
|
16938
18343
|
enqueuePendingMirrorDeliveries(subscription, deliveryPlan.finalizedTurns);
|
|
16939
18344
|
}
|
|
@@ -17297,7 +18702,7 @@ function normalizeProbeRecord(threadId, checkedAt, raw) {
|
|
|
17297
18702
|
function escapePowerShellSingleQuoted(value) {
|
|
17298
18703
|
return value.replace(/'/g, "''");
|
|
17299
18704
|
}
|
|
17300
|
-
async function probeCodexThreadProcess(threadId) {
|
|
18705
|
+
async function probeCodexThreadProcess(threadId, options = {}) {
|
|
17301
18706
|
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17302
18707
|
const trimmedThreadId = threadId.trim();
|
|
17303
18708
|
if (!trimmedThreadId) {
|
|
@@ -17318,11 +18723,14 @@ async function probeCodexThreadProcess(threadId) {
|
|
|
17318
18723
|
};
|
|
17319
18724
|
}
|
|
17320
18725
|
const psThreadId = escapePowerShellSingleQuoted(trimmedThreadId);
|
|
18726
|
+
const excludePids = Array.from(options.excludePids || []).filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
18727
|
+
const excludedLiteral = excludePids.length > 0 ? excludePids.join(",") : "";
|
|
17321
18728
|
const script = [
|
|
17322
18729
|
`$threadId = '${psThreadId}'`,
|
|
17323
18730
|
"$escaped = [regex]::Escape($threadId)",
|
|
18731
|
+
`$excludedPids = @(${excludedLiteral})`,
|
|
17324
18732
|
"$proc = Get-CimInstance Win32_Process | Where-Object {",
|
|
17325
|
-
" $_.Name -ieq 'codex.exe' -and $_.CommandLine -match $escaped",
|
|
18733
|
+
" $_.Name -ieq 'codex.exe' -and $_.CommandLine -match $escaped -and $excludedPids -notcontains [int]$_.ProcessId",
|
|
17326
18734
|
"} | Select-Object -First 1 ProcessId, CreationDate, CommandLine",
|
|
17327
18735
|
"if ($null -eq $proc) { '' } else { $proc | ConvertTo-Json -Compress }"
|
|
17328
18736
|
].join("; ");
|
|
@@ -17356,6 +18764,12 @@ async function probeCodexThreadProcess(threadId) {
|
|
|
17356
18764
|
};
|
|
17357
18765
|
}
|
|
17358
18766
|
}
|
|
18767
|
+
async function isCodexThreadProcessDefinitelyGone(threadId, probe = probeCodexThreadProcess) {
|
|
18768
|
+
const threadProbe = await probe(threadId);
|
|
18769
|
+
if (threadProbe.status !== "not_found") return false;
|
|
18770
|
+
const hostProbe = await probe("app-server");
|
|
18771
|
+
return hostProbe.status === "not_found";
|
|
18772
|
+
}
|
|
17359
18773
|
|
|
17360
18774
|
// src/lib/bridge/session-health-reducer.ts
|
|
17361
18775
|
var HEALTH_RECENT_PROGRESS_MS = 10 * 60 * 1e3;
|
|
@@ -17495,7 +18909,9 @@ function computeBaseDiagnosis(session, nowMs) {
|
|
|
17495
18909
|
const sdkSessionId = trimOrNull(session.sdk_session_id);
|
|
17496
18910
|
const lastProgressMs = parseIsoMs(lastProgressAt || void 0);
|
|
17497
18911
|
const previousStatus = session.health_status || "idle";
|
|
17498
|
-
|
|
18912
|
+
const terminalHealth = previousStatus === "completed" || previousStatus === "failed" || previousStatus === "aborted";
|
|
18913
|
+
const activeDesktopMirror = session.thread_origin === "desktop" && session.mirror_status === "watching" && isRunningHealthStatus(previousStatus);
|
|
18914
|
+
if (!isRunningRuntimeStatus(runtimeStatus) && !terminalHealth && !activeDesktopMirror) {
|
|
17499
18915
|
return {
|
|
17500
18916
|
sessionId: session.id,
|
|
17501
18917
|
checkedAt: null,
|
|
@@ -17583,6 +18999,12 @@ function applyProcessProbeDiagnosis(diagnosis, processProbe) {
|
|
|
17583
18999
|
processProbe: null
|
|
17584
19000
|
};
|
|
17585
19001
|
}
|
|
19002
|
+
if (!isRunningHealthStatus(diagnosis.healthStatus)) {
|
|
19003
|
+
return {
|
|
19004
|
+
...diagnosis,
|
|
19005
|
+
processProbe
|
|
19006
|
+
};
|
|
19007
|
+
}
|
|
17586
19008
|
if (processProbe.status === "alive") {
|
|
17587
19009
|
const healthStatus = diagnosis.activeToolName ? "waiting_tool" : "slow_observed";
|
|
17588
19010
|
const healthReason = diagnosis.activeToolName ? "\u68C0\u6D4B\u5230\u672C\u673A\u7EBF\u7A0B\u8FDB\u7A0B\u4ECD\u5728\u8FD0\u884C\uFF0C\u5F53\u524D\u66F4\u50CF\u662F\u957F\u65F6\u5DE5\u5177\u6267\u884C\u3002" : "\u68C0\u6D4B\u5230\u672C\u673A\u7EBF\u7A0B\u8FDB\u7A0B\u4ECD\u5728\u8FD0\u884C\uFF0C\u5F53\u524D\u66F4\u50CF\u662F\u957F\u65F6\u4EFB\u52A1\u3002";
|
|
@@ -18026,6 +19448,14 @@ function createTurnCoordinator(deps = {}) {
|
|
|
18026
19448
|
function getActiveTurn(sessionId) {
|
|
18027
19449
|
return activeTurnsBySession.get(sessionId);
|
|
18028
19450
|
}
|
|
19451
|
+
function associateDesktopTurn(sessionId, turnId) {
|
|
19452
|
+
const turn = activeTurnsBySession.get(sessionId);
|
|
19453
|
+
const normalizedTurnId = turnId.trim();
|
|
19454
|
+
if (!turn || turn.kind !== "im_desktop_reuse" || !normalizedTurnId) return false;
|
|
19455
|
+
if (turn.codexTurnId && turn.codexTurnId !== normalizedTurnId) return false;
|
|
19456
|
+
turn.codexTurnId = normalizedTurnId;
|
|
19457
|
+
return true;
|
|
19458
|
+
}
|
|
18029
19459
|
async function claimDesktopTerminal(terminal) {
|
|
18030
19460
|
const turn = activeTurnsBySession.get(terminal.sessionId);
|
|
18031
19461
|
if (!turn || turn.kind !== "im_desktop_reuse") {
|
|
@@ -18034,6 +19464,9 @@ function createTurnCoordinator(deps = {}) {
|
|
|
18034
19464
|
if (turn.desktopThreadId && turn.desktopThreadId !== terminal.desktopThreadId) {
|
|
18035
19465
|
return { claimed: false };
|
|
18036
19466
|
}
|
|
19467
|
+
if (!turn.codexTurnId || terminal.turnId !== turn.codexTurnId) {
|
|
19468
|
+
return { claimed: false, turn };
|
|
19469
|
+
}
|
|
18037
19470
|
const finalized = await deps.finalizeTerminalTurn?.(turn, terminal);
|
|
18038
19471
|
return finalized ? { claimed: true, turn } : { claimed: false, turn };
|
|
18039
19472
|
}
|
|
@@ -18056,6 +19489,7 @@ function createTurnCoordinator(deps = {}) {
|
|
|
18056
19489
|
return {
|
|
18057
19490
|
registerInteractiveTurn,
|
|
18058
19491
|
getActiveTurn,
|
|
19492
|
+
associateDesktopTurn,
|
|
18059
19493
|
claimDesktopTerminal,
|
|
18060
19494
|
releaseTurn,
|
|
18061
19495
|
releaseSessionTurn,
|
|
@@ -18076,6 +19510,7 @@ var MIRROR_PROMPT_MATCH_GRACE_MS = 12e4;
|
|
|
18076
19510
|
var DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS = 3e4;
|
|
18077
19511
|
var MIRROR_STREAM_STATUS_IDLE_START_MS = 18e4;
|
|
18078
19512
|
var MIRROR_STREAM_STATUS_HEARTBEAT_MS = 1e4;
|
|
19513
|
+
var MIRROR_STREAM_ORPHAN_TIMEOUT_MS = 30 * 6e4;
|
|
18079
19514
|
var MIRROR_TURN_BUFFER_TIMEOUT_MS = 10 * 6e4;
|
|
18080
19515
|
function describeUnknownError(error) {
|
|
18081
19516
|
if (error instanceof Error) {
|
|
@@ -18178,10 +19613,49 @@ var TURN_COORDINATOR = createTurnCoordinator({
|
|
|
18178
19613
|
terminal.text
|
|
18179
19614
|
)
|
|
18180
19615
|
});
|
|
19616
|
+
function getBridgeOwnedCodexProcessIds() {
|
|
19617
|
+
try {
|
|
19618
|
+
return getBridgeContext().llm.getOwnedProcessIds?.() || [];
|
|
19619
|
+
} catch {
|
|
19620
|
+
return [];
|
|
19621
|
+
}
|
|
19622
|
+
}
|
|
19623
|
+
async function probeManagedThreadProcess(threadId) {
|
|
19624
|
+
const checkedAt = nowIso3();
|
|
19625
|
+
try {
|
|
19626
|
+
const runtime = getBridgeContext().llm.getThreadRuntimeStatus?.(threadId);
|
|
19627
|
+
if (runtime) {
|
|
19628
|
+
if (runtime.status === "system_error") {
|
|
19629
|
+
return {
|
|
19630
|
+
threadId,
|
|
19631
|
+
status: "error",
|
|
19632
|
+
supported: true,
|
|
19633
|
+
checkedAt,
|
|
19634
|
+
...runtime.processId ? { pid: runtime.processId } : {},
|
|
19635
|
+
error: "Codex app-server reported a system error for this thread"
|
|
19636
|
+
};
|
|
19637
|
+
}
|
|
19638
|
+
if (runtime.status !== "not_loaded") {
|
|
19639
|
+
return {
|
|
19640
|
+
threadId,
|
|
19641
|
+
status: "alive",
|
|
19642
|
+
supported: true,
|
|
19643
|
+
checkedAt,
|
|
19644
|
+
...runtime.processId ? { pid: runtime.processId } : {},
|
|
19645
|
+
commandLine: `codex app-server (${runtime.status})`
|
|
19646
|
+
};
|
|
19647
|
+
}
|
|
19648
|
+
}
|
|
19649
|
+
} catch {
|
|
19650
|
+
}
|
|
19651
|
+
return probeCodexThreadProcess(threadId, {
|
|
19652
|
+
excludePids: getBridgeOwnedCodexProcessIds()
|
|
19653
|
+
});
|
|
19654
|
+
}
|
|
18181
19655
|
var SESSION_HEALTH_RUNTIME = createSessionHealthRuntime({
|
|
18182
19656
|
getStore: () => getBridgeContext().store,
|
|
18183
19657
|
nowIso: nowIso3,
|
|
18184
|
-
probeThreadProcess:
|
|
19658
|
+
probeThreadProcess: probeManagedThreadProcess
|
|
18185
19659
|
});
|
|
18186
19660
|
var MIRROR_SUPPRESSION_CONFIG = {
|
|
18187
19661
|
suppressionWindowMs: MIRROR_SUPPRESSION_WINDOW_MS,
|
|
@@ -18219,7 +19693,22 @@ function filterSuppressedMirrorRecords2(sessionId, records) {
|
|
|
18219
19693
|
getMirrorSuppressionStore(),
|
|
18220
19694
|
sessionId,
|
|
18221
19695
|
records,
|
|
18222
|
-
MIRROR_SUPPRESSION_CONFIG
|
|
19696
|
+
MIRROR_SUPPRESSION_CONFIG,
|
|
19697
|
+
Date.now(),
|
|
19698
|
+
(associatedSessionId, turnId) => {
|
|
19699
|
+
TURN_COORDINATOR.associateDesktopTurn(associatedSessionId, turnId);
|
|
19700
|
+
}
|
|
19701
|
+
);
|
|
19702
|
+
}
|
|
19703
|
+
function associateSuppressedDesktopTurn(sessionId, records) {
|
|
19704
|
+
associateMirrorSuppressionTurns(
|
|
19705
|
+
getMirrorSuppressionStore(),
|
|
19706
|
+
sessionId,
|
|
19707
|
+
records,
|
|
19708
|
+
Date.now(),
|
|
19709
|
+
(associatedSessionId, turnId) => {
|
|
19710
|
+
TURN_COORDINATOR.associateDesktopTurn(associatedSessionId, turnId);
|
|
19711
|
+
}
|
|
18223
19712
|
);
|
|
18224
19713
|
}
|
|
18225
19714
|
function syncMirrorSessionState(sessionId) {
|
|
@@ -18305,7 +19794,8 @@ var MIRROR_RUNTIME = createMirrorRuntime(getState, {
|
|
|
18305
19794
|
watchDebounceMs: MIRROR_WATCH_DEBOUNCE_MS,
|
|
18306
19795
|
danglingThreadRetryLimit: DANGLING_MIRROR_THREAD_RETRY_LIMIT,
|
|
18307
19796
|
failureSuspendThreshold: MIRROR_FAILURE_SUSPEND_THRESHOLD,
|
|
18308
|
-
failureSuspendMs: MIRROR_FAILURE_SUSPEND_MS
|
|
19797
|
+
failureSuspendMs: MIRROR_FAILURE_SUSPEND_MS,
|
|
19798
|
+
streamOrphanTimeoutMs: MIRROR_STREAM_ORPHAN_TIMEOUT_MS
|
|
18309
19799
|
}, {
|
|
18310
19800
|
nowIso: nowIso3,
|
|
18311
19801
|
describeUnknownError,
|
|
@@ -18315,12 +19805,19 @@ var MIRROR_RUNTIME = createMirrorRuntime(getState, {
|
|
|
18315
19805
|
observeSessionHealthRecords: (sessionId, threadId, records) => {
|
|
18316
19806
|
SESSION_HEALTH_RUNTIME.observeDesktopMirrorRecords(sessionId, threadId, records);
|
|
18317
19807
|
},
|
|
18318
|
-
|
|
18319
|
-
sessionId,
|
|
19808
|
+
recordOrphanedMirrorTurn: (sessionId, detail) => {
|
|
19809
|
+
SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, "aborted", detail);
|
|
19810
|
+
},
|
|
19811
|
+
isThreadProcessDefinitelyGone: (threadId) => isCodexThreadProcessDefinitelyGone(
|
|
18320
19812
|
threadId,
|
|
18321
|
-
|
|
18322
|
-
|
|
19813
|
+
(probeValue) => probeCodexThreadProcess(probeValue, {
|
|
19814
|
+
excludePids: getBridgeOwnedCodexProcessIds()
|
|
19815
|
+
})
|
|
18323
19816
|
),
|
|
19817
|
+
routeDesktopRecords: (sessionId, threadId, records) => {
|
|
19818
|
+
associateSuppressedDesktopTurn(sessionId, records);
|
|
19819
|
+
return routeDesktopRecords(sessionId, threadId, records, TURN_COORDINATOR);
|
|
19820
|
+
},
|
|
18324
19821
|
consumeMirrorRecords: consumeMirrorRecords2,
|
|
18325
19822
|
flushTimedOutMirrorTurn: (subscription) => flushTimedOutMirrorTurn2(subscription),
|
|
18326
19823
|
hasPendingMirrorWork: hasPendingMirrorWork2,
|
|
@@ -19517,13 +21014,15 @@ function writeRuntimeStatus(filePath, info, options = {}) {
|
|
|
19517
21014
|
}
|
|
19518
21015
|
|
|
19519
21016
|
// src/main.ts
|
|
19520
|
-
var RUNTIME_DIR =
|
|
19521
|
-
var STATUS_FILE =
|
|
19522
|
-
var PID_FILE =
|
|
19523
|
-
var runId =
|
|
19524
|
-
async function resolveProvider() {
|
|
21017
|
+
var RUNTIME_DIR = path20.join(CTI_HOME, "runtime");
|
|
21018
|
+
var STATUS_FILE = path20.join(RUNTIME_DIR, "status.json");
|
|
21019
|
+
var PID_FILE = path20.join(RUNTIME_DIR, "bridge.pid");
|
|
21020
|
+
var runId = crypto12.randomUUID();
|
|
21021
|
+
async function resolveProvider(pendingPerms) {
|
|
19525
21022
|
const { CodexProvider: CodexProvider2 } = await Promise.resolve().then(() => (init_codex_provider(), codex_provider_exports));
|
|
19526
|
-
return new CodexProvider2(
|
|
21023
|
+
return new CodexProvider2(pendingPerms, {
|
|
21024
|
+
transport: process.env.CTI_CODEX_TRANSPORT || "auto"
|
|
21025
|
+
});
|
|
19527
21026
|
}
|
|
19528
21027
|
function writeStatus(info, options = {}) {
|
|
19529
21028
|
return writeRuntimeStatus(STATUS_FILE, info, options);
|
|
@@ -19562,7 +21061,7 @@ async function main() {
|
|
|
19562
21061
|
const settings = configToSettings(config2);
|
|
19563
21062
|
const store = new JsonFileStore(settings, { dynamicSettings: true });
|
|
19564
21063
|
const pendingPerms = new PendingPermissions();
|
|
19565
|
-
const llm = await resolveProvider();
|
|
21064
|
+
const llm = await resolveProvider(pendingPerms);
|
|
19566
21065
|
console.log(`[codex-to-im] Runtime: ${config2.runtime}`);
|
|
19567
21066
|
const gateway = {
|
|
19568
21067
|
resolvePendingPermission: (id, resolution) => pendingPerms.resolve(id, resolution)
|
|
@@ -19573,8 +21072,8 @@ async function main() {
|
|
|
19573
21072
|
permissions: gateway,
|
|
19574
21073
|
lifecycle: {
|
|
19575
21074
|
onBridgeStart: () => {
|
|
19576
|
-
|
|
19577
|
-
|
|
21075
|
+
fs19.mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
21076
|
+
fs19.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
19578
21077
|
const channels = getRunningChannels();
|
|
19579
21078
|
writeStatus({
|
|
19580
21079
|
running: true,
|
|
@@ -19615,6 +21114,11 @@ async function main() {
|
|
|
19615
21114
|
} catch (error) {
|
|
19616
21115
|
console.error("[codex-to-im] Graceful bridge stop failed:", error instanceof Error ? error.stack || error.message : error);
|
|
19617
21116
|
} finally {
|
|
21117
|
+
try {
|
|
21118
|
+
await llm.dispose?.();
|
|
21119
|
+
} catch (error) {
|
|
21120
|
+
console.error("[codex-to-im] Failed to stop Codex provider:", error instanceof Error ? error.stack || error.message : error);
|
|
21121
|
+
}
|
|
19618
21122
|
releaseInstanceLock();
|
|
19619
21123
|
try {
|
|
19620
21124
|
writeStatus({ running: false, lastExitReason: reason }, { expectedRunId: runId });
|