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/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 fs17 from "node:fs";
1212
+ import fs18 from "node:fs";
328
1213
  import os4 from "node:os";
329
- import path18 from "node:path";
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(_pendingPerms) {
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 && fs17.existsSync(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 = path18.join(os4.tmpdir(), `cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
552
- fs17.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
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
- fs17.unlinkSync(tmp);
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 fs18 from "node:fs";
849
- import path19 from "node:path";
850
- import crypto11 from "node:crypto";
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 fs19 = await import("fs");
4487
+ const fs20 = await import("fs");
3290
4488
  const os5 = await import("os");
3291
- const path20 = await import("path");
3292
- const tmpPath = path20.join(os5.tmpdir(), `feishu-dl-${crypto3.randomUUID()}`);
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 = fs19.readFileSync(tmpPath);
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
- fs19.unlinkSync(tmpPath);
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 { DatabaseSync } from "node:sqlite";
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 = typeof meta?.source === "string" ? meta.source.toLowerCase() : "";
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 id, updated_at
11791
+ SELECT ${selectedColumns.join(", ")}
10539
11792
  FROM threads
10540
- WHERE archived = 0
10541
- AND source != 'exec'
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
- const id = typeof row.id === "string" ? row.id.trim() : "";
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(row.updated_at)
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
- if (archivedThreadIds.has(parsed.payload.id)) {
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 threadId = parsed.payload.id;
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 session = parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds);
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 readDesktopSessionMessages(threadId, limit = 8) {
11479
- const messages = readDesktopSessionEventStream(threadId).map((event) => ({
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 readDesktopSessionEventStreamByFilePath(filePath) {
11488
- let content = "";
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
- content = fs5.readFileSync(filePath, "utf-8");
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 parseDesktopSessionEventText(content, "", true).events;
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
- if (!isRunningRuntimeStatus(runtimeStatus) && isRunningHealthStatus(previousStatus)) {
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: (threadId) => probeCodexThreadProcess(threadId)
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
- routeDesktopRecords: (sessionId, threadId, records) => routeDesktopRecords(
18319
- sessionId,
19808
+ recordOrphanedMirrorTurn: (sessionId, detail) => {
19809
+ SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, "aborted", detail);
19810
+ },
19811
+ isThreadProcessDefinitelyGone: (threadId) => isCodexThreadProcessDefinitelyGone(
18320
19812
  threadId,
18321
- records,
18322
- TURN_COORDINATOR
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 = path19.join(CTI_HOME, "runtime");
19521
- var STATUS_FILE = path19.join(RUNTIME_DIR, "status.json");
19522
- var PID_FILE = path19.join(RUNTIME_DIR, "bridge.pid");
19523
- var runId = crypto11.randomUUID();
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
- fs18.mkdirSync(RUNTIME_DIR, { recursive: true });
19577
- fs18.writeFileSync(PID_FILE, String(process.pid), "utf-8");
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 });