codex-to-im 1.0.62 → 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");
12785
+ try {
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 = [];
11489
12816
  try {
11490
- content = fs5.readFileSync(filePath, "utf-8");
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,
@@ -17341,7 +18702,7 @@ function normalizeProbeRecord(threadId, checkedAt, raw) {
17341
18702
  function escapePowerShellSingleQuoted(value) {
17342
18703
  return value.replace(/'/g, "''");
17343
18704
  }
17344
- async function probeCodexThreadProcess(threadId) {
18705
+ async function probeCodexThreadProcess(threadId, options = {}) {
17345
18706
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
17346
18707
  const trimmedThreadId = threadId.trim();
17347
18708
  if (!trimmedThreadId) {
@@ -17362,11 +18723,14 @@ async function probeCodexThreadProcess(threadId) {
17362
18723
  };
17363
18724
  }
17364
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(",") : "";
17365
18728
  const script = [
17366
18729
  `$threadId = '${psThreadId}'`,
17367
18730
  "$escaped = [regex]::Escape($threadId)",
18731
+ `$excludedPids = @(${excludedLiteral})`,
17368
18732
  "$proc = Get-CimInstance Win32_Process | Where-Object {",
17369
- " $_.Name -ieq 'codex.exe' -and $_.CommandLine -match $escaped",
18733
+ " $_.Name -ieq 'codex.exe' -and $_.CommandLine -match $escaped -and $excludedPids -notcontains [int]$_.ProcessId",
17370
18734
  "} | Select-Object -First 1 ProcessId, CreationDate, CommandLine",
17371
18735
  "if ($null -eq $proc) { '' } else { $proc | ConvertTo-Json -Compress }"
17372
18736
  ].join("; ");
@@ -18084,6 +19448,14 @@ function createTurnCoordinator(deps = {}) {
18084
19448
  function getActiveTurn(sessionId) {
18085
19449
  return activeTurnsBySession.get(sessionId);
18086
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
+ }
18087
19459
  async function claimDesktopTerminal(terminal) {
18088
19460
  const turn = activeTurnsBySession.get(terminal.sessionId);
18089
19461
  if (!turn || turn.kind !== "im_desktop_reuse") {
@@ -18092,6 +19464,9 @@ function createTurnCoordinator(deps = {}) {
18092
19464
  if (turn.desktopThreadId && turn.desktopThreadId !== terminal.desktopThreadId) {
18093
19465
  return { claimed: false };
18094
19466
  }
19467
+ if (!turn.codexTurnId || terminal.turnId !== turn.codexTurnId) {
19468
+ return { claimed: false, turn };
19469
+ }
18095
19470
  const finalized = await deps.finalizeTerminalTurn?.(turn, terminal);
18096
19471
  return finalized ? { claimed: true, turn } : { claimed: false, turn };
18097
19472
  }
@@ -18114,6 +19489,7 @@ function createTurnCoordinator(deps = {}) {
18114
19489
  return {
18115
19490
  registerInteractiveTurn,
18116
19491
  getActiveTurn,
19492
+ associateDesktopTurn,
18117
19493
  claimDesktopTerminal,
18118
19494
  releaseTurn,
18119
19495
  releaseSessionTurn,
@@ -18237,10 +19613,49 @@ var TURN_COORDINATOR = createTurnCoordinator({
18237
19613
  terminal.text
18238
19614
  )
18239
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
+ }
18240
19655
  var SESSION_HEALTH_RUNTIME = createSessionHealthRuntime({
18241
19656
  getStore: () => getBridgeContext().store,
18242
19657
  nowIso: nowIso3,
18243
- probeThreadProcess: (threadId) => probeCodexThreadProcess(threadId)
19658
+ probeThreadProcess: probeManagedThreadProcess
18244
19659
  });
18245
19660
  var MIRROR_SUPPRESSION_CONFIG = {
18246
19661
  suppressionWindowMs: MIRROR_SUPPRESSION_WINDOW_MS,
@@ -18278,7 +19693,22 @@ function filterSuppressedMirrorRecords2(sessionId, records) {
18278
19693
  getMirrorSuppressionStore(),
18279
19694
  sessionId,
18280
19695
  records,
18281
- 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
+ }
18282
19712
  );
18283
19713
  }
18284
19714
  function syncMirrorSessionState(sessionId) {
@@ -18378,13 +19808,16 @@ var MIRROR_RUNTIME = createMirrorRuntime(getState, {
18378
19808
  recordOrphanedMirrorTurn: (sessionId, detail) => {
18379
19809
  SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, "aborted", detail);
18380
19810
  },
18381
- isThreadProcessDefinitelyGone: isCodexThreadProcessDefinitelyGone,
18382
- routeDesktopRecords: (sessionId, threadId, records) => routeDesktopRecords(
18383
- sessionId,
19811
+ isThreadProcessDefinitelyGone: (threadId) => isCodexThreadProcessDefinitelyGone(
18384
19812
  threadId,
18385
- records,
18386
- TURN_COORDINATOR
19813
+ (probeValue) => probeCodexThreadProcess(probeValue, {
19814
+ excludePids: getBridgeOwnedCodexProcessIds()
19815
+ })
18387
19816
  ),
19817
+ routeDesktopRecords: (sessionId, threadId, records) => {
19818
+ associateSuppressedDesktopTurn(sessionId, records);
19819
+ return routeDesktopRecords(sessionId, threadId, records, TURN_COORDINATOR);
19820
+ },
18388
19821
  consumeMirrorRecords: consumeMirrorRecords2,
18389
19822
  flushTimedOutMirrorTurn: (subscription) => flushTimedOutMirrorTurn2(subscription),
18390
19823
  hasPendingMirrorWork: hasPendingMirrorWork2,
@@ -19581,13 +21014,15 @@ function writeRuntimeStatus(filePath, info, options = {}) {
19581
21014
  }
19582
21015
 
19583
21016
  // src/main.ts
19584
- var RUNTIME_DIR = path19.join(CTI_HOME, "runtime");
19585
- var STATUS_FILE = path19.join(RUNTIME_DIR, "status.json");
19586
- var PID_FILE = path19.join(RUNTIME_DIR, "bridge.pid");
19587
- var runId = crypto11.randomUUID();
19588
- 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) {
19589
21022
  const { CodexProvider: CodexProvider2 } = await Promise.resolve().then(() => (init_codex_provider(), codex_provider_exports));
19590
- return new CodexProvider2();
21023
+ return new CodexProvider2(pendingPerms, {
21024
+ transport: process.env.CTI_CODEX_TRANSPORT || "auto"
21025
+ });
19591
21026
  }
19592
21027
  function writeStatus(info, options = {}) {
19593
21028
  return writeRuntimeStatus(STATUS_FILE, info, options);
@@ -19626,7 +21061,7 @@ async function main() {
19626
21061
  const settings = configToSettings(config2);
19627
21062
  const store = new JsonFileStore(settings, { dynamicSettings: true });
19628
21063
  const pendingPerms = new PendingPermissions();
19629
- const llm = await resolveProvider();
21064
+ const llm = await resolveProvider(pendingPerms);
19630
21065
  console.log(`[codex-to-im] Runtime: ${config2.runtime}`);
19631
21066
  const gateway = {
19632
21067
  resolvePendingPermission: (id, resolution) => pendingPerms.resolve(id, resolution)
@@ -19637,8 +21072,8 @@ async function main() {
19637
21072
  permissions: gateway,
19638
21073
  lifecycle: {
19639
21074
  onBridgeStart: () => {
19640
- fs18.mkdirSync(RUNTIME_DIR, { recursive: true });
19641
- 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");
19642
21077
  const channels = getRunningChannels();
19643
21078
  writeStatus({
19644
21079
  running: true,
@@ -19679,6 +21114,11 @@ async function main() {
19679
21114
  } catch (error) {
19680
21115
  console.error("[codex-to-im] Graceful bridge stop failed:", error instanceof Error ? error.stack || error.message : error);
19681
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
+ }
19682
21122
  releaseInstanceLock();
19683
21123
  try {
19684
21124
  writeStatus({ running: false, lastExitReason: reason }, { expectedRunId: runId });