codex-to-im 1.0.62 → 1.0.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/README_EN.md +6 -0
- package/dist/cli.mjs +1 -1
- package/dist/daemon.mjs +1512 -72
- package/dist/ui-server.mjs +98 -17
- package/docs/install-windows.md +2 -0
- package/package.json +1 -1
- package/scripts/build.js +13 -1
package/dist/daemon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createRequire } from 'module'; const require =
|
|
1
|
+
import { createRequire as __ctiCreateRequire } from 'module'; const require = __ctiCreateRequire(import.meta.url);
|
|
2
2
|
var __create = Object.create;
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -318,15 +318,900 @@ var init_sse_utils = __esm({
|
|
|
318
318
|
}
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
+
// src/codex-app-server-client.ts
|
|
322
|
+
import crypto11 from "node:crypto";
|
|
323
|
+
import fs17 from "node:fs";
|
|
324
|
+
import path18 from "node:path";
|
|
325
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
326
|
+
import { spawn } from "node:child_process";
|
|
327
|
+
function asRecord(value) {
|
|
328
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
329
|
+
}
|
|
330
|
+
function asString(value) {
|
|
331
|
+
return typeof value === "string" ? value : "";
|
|
332
|
+
}
|
|
333
|
+
function abortError() {
|
|
334
|
+
const error = new Error("Task stopped by user");
|
|
335
|
+
error.name = "AbortError";
|
|
336
|
+
return error;
|
|
337
|
+
}
|
|
338
|
+
function mapApprovalPolicy(permissionMode) {
|
|
339
|
+
return permissionMode === "never" ? "never" : "on-request";
|
|
340
|
+
}
|
|
341
|
+
function mapSandboxMode(value) {
|
|
342
|
+
if (value === "read-only" || value === "danger-full-access") return value;
|
|
343
|
+
return "workspace-write";
|
|
344
|
+
}
|
|
345
|
+
function getThreadId(params) {
|
|
346
|
+
return asString(params.threadId ?? params.thread_id);
|
|
347
|
+
}
|
|
348
|
+
function getTurnId(params) {
|
|
349
|
+
return asString(params.turnId ?? params.turn_id ?? asRecord(params.turn).id);
|
|
350
|
+
}
|
|
351
|
+
function getItemId(params) {
|
|
352
|
+
return asString(params.itemId ?? params.item_id);
|
|
353
|
+
}
|
|
354
|
+
function getThreadStatus(value) {
|
|
355
|
+
const type = asString(asRecord(value).type);
|
|
356
|
+
if (type === "idle" || type === "active" || type === "notLoaded") {
|
|
357
|
+
return type === "notLoaded" ? "not_loaded" : type;
|
|
358
|
+
}
|
|
359
|
+
if (type === "systemError") return "system_error";
|
|
360
|
+
return "unknown";
|
|
361
|
+
}
|
|
362
|
+
function resolveBundledCodexExecutable() {
|
|
363
|
+
const target = PLATFORM_TARGETS[`${process.platform}:${process.arch}`];
|
|
364
|
+
if (!target) {
|
|
365
|
+
throw new Error(`Unsupported Codex platform: ${process.platform} (${process.arch})`);
|
|
366
|
+
}
|
|
367
|
+
let codexPackageJson = "";
|
|
368
|
+
try {
|
|
369
|
+
codexPackageJson = localRequire2.resolve("@openai/codex/package.json");
|
|
370
|
+
} catch {
|
|
371
|
+
const sdkEntry = localRequire2.resolve("@openai/codex-sdk");
|
|
372
|
+
codexPackageJson = createRequire2(sdkEntry).resolve("@openai/codex/package.json");
|
|
373
|
+
}
|
|
374
|
+
const codexRequire = createRequire2(codexPackageJson);
|
|
375
|
+
let vendorRoot = path18.join(path18.dirname(codexPackageJson), "vendor");
|
|
376
|
+
try {
|
|
377
|
+
const platformPackageJson = codexRequire.resolve(`${target.packageName}/package.json`);
|
|
378
|
+
vendorRoot = path18.join(path18.dirname(platformPackageJson), "vendor");
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
const executablePath = path18.join(
|
|
382
|
+
vendorRoot,
|
|
383
|
+
target.triple,
|
|
384
|
+
"bin",
|
|
385
|
+
process.platform === "win32" ? "codex.exe" : "codex"
|
|
386
|
+
);
|
|
387
|
+
if (!fs17.existsSync(executablePath)) {
|
|
388
|
+
throw new Error(`Bundled Codex executable is missing: ${executablePath}`);
|
|
389
|
+
}
|
|
390
|
+
return executablePath;
|
|
391
|
+
}
|
|
392
|
+
var DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_INTERRUPT_GRACE_MS, DEFAULT_THREAD_IDLE_RELEASE_MS, STDERR_TAIL_LIMIT, localRequire2, PLATFORM_TARGETS, AsyncEventQueue, AppServerPreTurnError, AppServerTurnStartUncertainError, CodexAppServerClient;
|
|
393
|
+
var init_codex_app_server_client = __esm({
|
|
394
|
+
"src/codex-app-server-client.ts"() {
|
|
395
|
+
"use strict";
|
|
396
|
+
DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
397
|
+
DEFAULT_INTERRUPT_GRACE_MS = 15e3;
|
|
398
|
+
DEFAULT_THREAD_IDLE_RELEASE_MS = 10 * 6e4;
|
|
399
|
+
STDERR_TAIL_LIMIT = 16384;
|
|
400
|
+
localRequire2 = createRequire2(import.meta.url);
|
|
401
|
+
PLATFORM_TARGETS = {
|
|
402
|
+
"linux:x64": { packageName: "@openai/codex-linux-x64", triple: "x86_64-unknown-linux-musl" },
|
|
403
|
+
"linux:arm64": { packageName: "@openai/codex-linux-arm64", triple: "aarch64-unknown-linux-musl" },
|
|
404
|
+
"darwin:x64": { packageName: "@openai/codex-darwin-x64", triple: "x86_64-apple-darwin" },
|
|
405
|
+
"darwin:arm64": { packageName: "@openai/codex-darwin-arm64", triple: "aarch64-apple-darwin" },
|
|
406
|
+
"win32:x64": { packageName: "@openai/codex-win32-x64", triple: "x86_64-pc-windows-msvc" },
|
|
407
|
+
"win32:arm64": { packageName: "@openai/codex-win32-arm64", triple: "aarch64-pc-windows-msvc" }
|
|
408
|
+
};
|
|
409
|
+
AsyncEventQueue = class {
|
|
410
|
+
values = [];
|
|
411
|
+
waiters = [];
|
|
412
|
+
closed = false;
|
|
413
|
+
failure = null;
|
|
414
|
+
push(value) {
|
|
415
|
+
if (this.closed) return;
|
|
416
|
+
const waiter = this.waiters.shift();
|
|
417
|
+
if (waiter) waiter.resolve({ value, done: false });
|
|
418
|
+
else this.values.push(value);
|
|
419
|
+
}
|
|
420
|
+
close() {
|
|
421
|
+
if (this.closed) return;
|
|
422
|
+
this.closed = true;
|
|
423
|
+
for (const waiter of this.waiters.splice(0)) waiter.resolve({ value: void 0, done: true });
|
|
424
|
+
}
|
|
425
|
+
fail(error) {
|
|
426
|
+
if (this.closed) return;
|
|
427
|
+
this.failure = error;
|
|
428
|
+
this.closed = true;
|
|
429
|
+
for (const waiter of this.waiters.splice(0)) waiter.reject(error);
|
|
430
|
+
}
|
|
431
|
+
[Symbol.asyncIterator]() {
|
|
432
|
+
return {
|
|
433
|
+
next: () => {
|
|
434
|
+
const value = this.values.shift();
|
|
435
|
+
if (value !== void 0) return Promise.resolve({ value, done: false });
|
|
436
|
+
if (this.failure) return Promise.reject(this.failure);
|
|
437
|
+
if (this.closed) return Promise.resolve({ value: void 0, done: true });
|
|
438
|
+
return new Promise((resolve2, reject) => {
|
|
439
|
+
this.waiters.push({ resolve: resolve2, reject });
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
AppServerPreTurnError = class extends Error {
|
|
446
|
+
constructor(message, options) {
|
|
447
|
+
super(message, options);
|
|
448
|
+
this.name = "AppServerPreTurnError";
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
AppServerTurnStartUncertainError = class extends Error {
|
|
452
|
+
constructor(message, options) {
|
|
453
|
+
super(message, options);
|
|
454
|
+
this.name = "AppServerTurnStartUncertainError";
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
CodexAppServerClient = class {
|
|
458
|
+
constructor(pendingPermissions, options = {}) {
|
|
459
|
+
this.pendingPermissions = pendingPermissions;
|
|
460
|
+
this.options = options;
|
|
461
|
+
}
|
|
462
|
+
child = null;
|
|
463
|
+
startupPromise = null;
|
|
464
|
+
nextRequestId = 1;
|
|
465
|
+
generation = 0;
|
|
466
|
+
stdoutBuffer = "";
|
|
467
|
+
stderrTail = "";
|
|
468
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
469
|
+
activeTurns = /* @__PURE__ */ new Map();
|
|
470
|
+
startingTurns = /* @__PURE__ */ new Map();
|
|
471
|
+
approvalPermissionIds = /* @__PURE__ */ new Map();
|
|
472
|
+
unknownMethods = /* @__PURE__ */ new Set();
|
|
473
|
+
subscribedThreadIds = /* @__PURE__ */ new Set();
|
|
474
|
+
threadStatuses = /* @__PURE__ */ new Map();
|
|
475
|
+
threadReleaseTimers = /* @__PURE__ */ new Map();
|
|
476
|
+
recyclePromise = null;
|
|
477
|
+
getOwnedProcessIds() {
|
|
478
|
+
const pid = this.child?.pid;
|
|
479
|
+
return typeof pid === "number" && pid > 0 ? [pid] : [];
|
|
480
|
+
}
|
|
481
|
+
getThreadRuntimeStatus(threadId) {
|
|
482
|
+
if (!this.subscribedThreadIds.has(threadId) && !this.threadStatuses.has(threadId)) return null;
|
|
483
|
+
return {
|
|
484
|
+
status: this.threadStatuses.get(threadId) || "unknown",
|
|
485
|
+
...this.child?.pid ? { processId: this.child.pid } : {}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
async startTurn(params) {
|
|
489
|
+
if (params.abortSignal?.aborted) throw abortError();
|
|
490
|
+
let turnStartDispatched = false;
|
|
491
|
+
let threadResolutionStarted = false;
|
|
492
|
+
try {
|
|
493
|
+
await this.ensureStarted();
|
|
494
|
+
threadResolutionStarted = true;
|
|
495
|
+
const threadId = await this.resolveThread(params);
|
|
496
|
+
if (params.abortSignal?.aborted) {
|
|
497
|
+
this.scheduleThreadRelease(threadId);
|
|
498
|
+
throw abortError();
|
|
499
|
+
}
|
|
500
|
+
this.clearThreadReleaseTimer(threadId);
|
|
501
|
+
const starting = {
|
|
502
|
+
threadId,
|
|
503
|
+
messages: [],
|
|
504
|
+
queue: new AsyncEventQueue(),
|
|
505
|
+
permissionRequestIds: /* @__PURE__ */ new Set()
|
|
506
|
+
};
|
|
507
|
+
this.startingTurns.set(threadId, starting);
|
|
508
|
+
let response;
|
|
509
|
+
try {
|
|
510
|
+
response = asRecord(await this.sendRequest("turn/start", {
|
|
511
|
+
threadId,
|
|
512
|
+
clientUserMessageId: crypto11.randomUUID(),
|
|
513
|
+
input: [
|
|
514
|
+
{ type: "text", text: params.prompt, text_elements: [] },
|
|
515
|
+
...(params.imagePaths || []).map((imagePath) => ({ type: "localImage", path: imagePath }))
|
|
516
|
+
],
|
|
517
|
+
...params.workingDirectory ? { cwd: params.workingDirectory } : {},
|
|
518
|
+
...params.forceModel && params.model ? { model: params.model } : {},
|
|
519
|
+
...params.modelReasoningEffort ? { effort: params.modelReasoningEffort } : {},
|
|
520
|
+
approvalPolicy: mapApprovalPolicy(params.permissionMode)
|
|
521
|
+
}, () => {
|
|
522
|
+
turnStartDispatched = true;
|
|
523
|
+
}));
|
|
524
|
+
} catch (error) {
|
|
525
|
+
this.startingTurns.delete(threadId);
|
|
526
|
+
this.cancelPermissions(starting, "Turn failed before startup completed");
|
|
527
|
+
starting.queue.fail(error instanceof Error ? error : new Error(String(error)));
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
const turn = asRecord(response.turn);
|
|
531
|
+
const turnId = asString(turn.id);
|
|
532
|
+
if (!turnId) {
|
|
533
|
+
this.startingTurns.delete(threadId);
|
|
534
|
+
const error = new Error("turn/start returned no turn id");
|
|
535
|
+
this.cancelPermissions(starting, error.message);
|
|
536
|
+
starting.queue.fail(error);
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
const active = {
|
|
540
|
+
threadId,
|
|
541
|
+
turnId,
|
|
542
|
+
queue: starting.queue,
|
|
543
|
+
abortSignal: params.abortSignal,
|
|
544
|
+
permissionRequestIds: starting.permissionRequestIds
|
|
545
|
+
};
|
|
546
|
+
this.activeTurns.set(turnId, active);
|
|
547
|
+
this.threadStatuses.set(threadId, "active");
|
|
548
|
+
this.startingTurns.delete(threadId);
|
|
549
|
+
for (const message of starting.messages) this.routeTurnMessage(active, message);
|
|
550
|
+
if (params.abortSignal && this.activeTurns.has(turnId)) {
|
|
551
|
+
active.abortListener = () => {
|
|
552
|
+
void this.interruptTurn(threadId, turnId);
|
|
553
|
+
};
|
|
554
|
+
if (params.abortSignal.aborted) {
|
|
555
|
+
void this.interruptTurn(threadId, turnId);
|
|
556
|
+
} else {
|
|
557
|
+
params.abortSignal.addEventListener("abort", active.abortListener, { once: true });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
threadId,
|
|
562
|
+
turnId,
|
|
563
|
+
events: active.queue,
|
|
564
|
+
interrupt: () => this.interruptTurn(threadId, turnId)
|
|
565
|
+
};
|
|
566
|
+
} catch (error) {
|
|
567
|
+
if (error instanceof AppServerPreTurnError || error instanceof AppServerTurnStartUncertainError || error instanceof Error && error.name === "AbortError") {
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
571
|
+
if (turnStartDispatched) {
|
|
572
|
+
await this.recycleChild(`turn/start result is unknown for thread ${params.savedThreadId || "new"}`);
|
|
573
|
+
throw new AppServerTurnStartUncertainError(
|
|
574
|
+
`Codex app-server turn/start was dispatched but its result is unknown: ${detail}`,
|
|
575
|
+
{ cause: error }
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
if (threadResolutionStarted) {
|
|
579
|
+
await this.recycleChild(`thread preparation failed before turn/start: ${detail}`);
|
|
580
|
+
}
|
|
581
|
+
throw new AppServerPreTurnError(`Codex app-server could not start the turn: ${detail}`, { cause: error });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async close() {
|
|
585
|
+
const child = this.child;
|
|
586
|
+
if (!child) return;
|
|
587
|
+
const error = new Error("Codex app-server client closed");
|
|
588
|
+
for (const pending of this.pendingRequests.values()) {
|
|
589
|
+
clearTimeout(pending.timer);
|
|
590
|
+
pending.reject(error);
|
|
591
|
+
}
|
|
592
|
+
this.pendingRequests.clear();
|
|
593
|
+
for (const active of this.activeTurns.values()) {
|
|
594
|
+
if (active.interruptTimer) clearTimeout(active.interruptTimer);
|
|
595
|
+
this.cancelPermissions(active, error.message);
|
|
596
|
+
active.queue.fail(error);
|
|
597
|
+
}
|
|
598
|
+
this.activeTurns.clear();
|
|
599
|
+
for (const starting of this.startingTurns.values()) {
|
|
600
|
+
this.cancelPermissions(starting, error.message);
|
|
601
|
+
starting.queue.fail(error);
|
|
602
|
+
}
|
|
603
|
+
this.startingTurns.clear();
|
|
604
|
+
this.approvalPermissionIds.clear();
|
|
605
|
+
this.clearAllThreadReleaseTimers();
|
|
606
|
+
this.subscribedThreadIds.clear();
|
|
607
|
+
this.threadStatuses.clear();
|
|
608
|
+
this.startupPromise = null;
|
|
609
|
+
if (child.exitCode !== null) {
|
|
610
|
+
if (this.child === child) this.child = null;
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
await new Promise((resolve2) => {
|
|
614
|
+
let settled = false;
|
|
615
|
+
const finish = () => {
|
|
616
|
+
if (settled) return;
|
|
617
|
+
settled = true;
|
|
618
|
+
clearTimeout(timer);
|
|
619
|
+
child.off("exit", finish);
|
|
620
|
+
resolve2();
|
|
621
|
+
};
|
|
622
|
+
const timer = setTimeout(finish, 2e3);
|
|
623
|
+
child.once("exit", finish);
|
|
624
|
+
try {
|
|
625
|
+
child.stdin.end();
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
child.kill();
|
|
630
|
+
} catch {
|
|
631
|
+
finish();
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
if (this.child === child) this.child = null;
|
|
635
|
+
}
|
|
636
|
+
async resolveThread(params) {
|
|
637
|
+
const common = {
|
|
638
|
+
...params.forceModel && params.model ? { model: params.model } : {},
|
|
639
|
+
...params.workingDirectory ? { cwd: params.workingDirectory } : {},
|
|
640
|
+
approvalPolicy: mapApprovalPolicy(params.permissionMode),
|
|
641
|
+
sandbox: mapSandboxMode(params.sandboxMode),
|
|
642
|
+
...params.systemPrompt ? { developerInstructions: params.systemPrompt } : {},
|
|
643
|
+
...params.skipGitRepoCheck ? { config: { skip_git_repo_check: true } } : {}
|
|
644
|
+
};
|
|
645
|
+
if (params.savedThreadId) {
|
|
646
|
+
if (this.subscribedThreadIds.has(params.savedThreadId)) {
|
|
647
|
+
if (this.threadStatuses.get(params.savedThreadId) !== "system_error") {
|
|
648
|
+
return params.savedThreadId;
|
|
649
|
+
}
|
|
650
|
+
this.forgetThread(params.savedThreadId);
|
|
651
|
+
}
|
|
652
|
+
const resumed = asRecord(await this.sendRequest("thread/resume", {
|
|
653
|
+
threadId: params.savedThreadId,
|
|
654
|
+
excludeTurns: true,
|
|
655
|
+
...common
|
|
656
|
+
}));
|
|
657
|
+
const resumedThread = asRecord(resumed.thread);
|
|
658
|
+
const resumedThreadId = asString(resumedThread.id);
|
|
659
|
+
if (!resumedThreadId) throw new Error("thread/resume returned no thread id");
|
|
660
|
+
this.markThreadSubscribed(resumedThreadId, getThreadStatus(resumedThread.status));
|
|
661
|
+
return resumedThreadId;
|
|
662
|
+
}
|
|
663
|
+
const started = asRecord(await this.sendRequest("thread/start", {
|
|
664
|
+
...common,
|
|
665
|
+
threadSource: "codex-to-im"
|
|
666
|
+
}));
|
|
667
|
+
const startedThread = asRecord(started.thread);
|
|
668
|
+
const threadId = asString(startedThread.id);
|
|
669
|
+
if (!threadId) throw new Error("thread/start returned no thread id");
|
|
670
|
+
this.markThreadSubscribed(threadId, getThreadStatus(startedThread.status));
|
|
671
|
+
return threadId;
|
|
672
|
+
}
|
|
673
|
+
async ensureStarted() {
|
|
674
|
+
if (this.recyclePromise) await this.recyclePromise;
|
|
675
|
+
if (this.child && this.child.exitCode === null && this.startupPromise) {
|
|
676
|
+
return this.startupPromise;
|
|
677
|
+
}
|
|
678
|
+
if (this.startupPromise) return this.startupPromise;
|
|
679
|
+
this.startupPromise = (async () => {
|
|
680
|
+
let executablePath;
|
|
681
|
+
try {
|
|
682
|
+
executablePath = this.options.executablePath || resolveBundledCodexExecutable();
|
|
683
|
+
} catch (error) {
|
|
684
|
+
throw new AppServerPreTurnError(
|
|
685
|
+
error instanceof Error ? error.message : String(error),
|
|
686
|
+
{ cause: error }
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
const env = { ...process.env };
|
|
690
|
+
const apiKey = process.env.CTI_CODEX_API_KEY || process.env.CODEX_API_KEY;
|
|
691
|
+
if (apiKey) env.OPENAI_API_KEY = apiKey;
|
|
692
|
+
if (process.env.CTI_CODEX_BASE_URL) env.OPENAI_BASE_URL = process.env.CTI_CODEX_BASE_URL;
|
|
693
|
+
const spawnProcess = this.options.spawnProcess || spawn;
|
|
694
|
+
const child = spawnProcess(executablePath, this.options.executableArgs || ["app-server"], {
|
|
695
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
696
|
+
windowsHide: true,
|
|
697
|
+
env
|
|
698
|
+
});
|
|
699
|
+
this.child = child;
|
|
700
|
+
this.generation += 1;
|
|
701
|
+
this.stdoutBuffer = "";
|
|
702
|
+
this.stderrTail = "";
|
|
703
|
+
child.stdout.setEncoding("utf8");
|
|
704
|
+
child.stdout.on("data", (chunk) => this.handleStdout(chunk));
|
|
705
|
+
child.stderr.setEncoding("utf8");
|
|
706
|
+
child.stderr.on("data", (chunk) => {
|
|
707
|
+
this.stderrTail = `${this.stderrTail}${chunk}`.slice(-STDERR_TAIL_LIMIT);
|
|
708
|
+
});
|
|
709
|
+
child.on("exit", (code2, signal) => this.handleExit(child, code2, signal));
|
|
710
|
+
await new Promise((resolve2, reject) => {
|
|
711
|
+
const onSpawn = () => {
|
|
712
|
+
cleanup();
|
|
713
|
+
resolve2();
|
|
714
|
+
};
|
|
715
|
+
const onError = (error) => {
|
|
716
|
+
cleanup();
|
|
717
|
+
reject(error);
|
|
718
|
+
};
|
|
719
|
+
const cleanup = () => {
|
|
720
|
+
child.off("spawn", onSpawn);
|
|
721
|
+
child.off("error", onError);
|
|
722
|
+
};
|
|
723
|
+
child.once("spawn", onSpawn);
|
|
724
|
+
child.once("error", onError);
|
|
725
|
+
});
|
|
726
|
+
child.on("error", (error) => {
|
|
727
|
+
if (this.child !== child) return;
|
|
728
|
+
console.warn("[codex-app-server] Child process error:", error.message);
|
|
729
|
+
try {
|
|
730
|
+
child.kill();
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
child.stdin.on("error", (error) => {
|
|
735
|
+
if (this.child !== child) return;
|
|
736
|
+
console.warn("[codex-app-server] stdin error:", error.message);
|
|
737
|
+
try {
|
|
738
|
+
child.kill();
|
|
739
|
+
} catch {
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
await this.sendRequest("initialize", {
|
|
743
|
+
clientInfo: { name: "codex-to-im", title: "Codex to IM", version: "1" },
|
|
744
|
+
capabilities: {
|
|
745
|
+
experimentalApi: true,
|
|
746
|
+
requestAttestation: false
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
this.sendNotification("initialized", {});
|
|
750
|
+
})().catch((error) => {
|
|
751
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
752
|
+
this.startupPromise = null;
|
|
753
|
+
try {
|
|
754
|
+
this.child?.kill();
|
|
755
|
+
} catch {
|
|
756
|
+
}
|
|
757
|
+
this.child = null;
|
|
758
|
+
throw error instanceof AppServerPreTurnError ? error : new AppServerPreTurnError(`Codex app-server initialization failed: ${detail}`, { cause: error });
|
|
759
|
+
});
|
|
760
|
+
return this.startupPromise;
|
|
761
|
+
}
|
|
762
|
+
sendRequest(method, params, onDispatched) {
|
|
763
|
+
const child = this.child;
|
|
764
|
+
if (!child || child.exitCode !== null || !child.stdin.writable) {
|
|
765
|
+
return Promise.reject(new Error("Codex app-server is not running"));
|
|
766
|
+
}
|
|
767
|
+
const id = this.nextRequestId++;
|
|
768
|
+
const timeoutMs = Math.max(10, this.options.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS);
|
|
769
|
+
return new Promise((resolve2, reject) => {
|
|
770
|
+
const timer = setTimeout(() => {
|
|
771
|
+
this.pendingRequests.delete(id);
|
|
772
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
773
|
+
}, timeoutMs);
|
|
774
|
+
this.pendingRequests.set(id, { method, resolve: resolve2, reject, timer });
|
|
775
|
+
try {
|
|
776
|
+
this.writeMessage({ id, method, params });
|
|
777
|
+
onDispatched?.();
|
|
778
|
+
} catch (error) {
|
|
779
|
+
clearTimeout(timer);
|
|
780
|
+
this.pendingRequests.delete(id);
|
|
781
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
sendNotification(method, params) {
|
|
786
|
+
this.writeMessage({ method, params });
|
|
787
|
+
}
|
|
788
|
+
writeMessage(message) {
|
|
789
|
+
const child = this.child;
|
|
790
|
+
if (!child || child.exitCode !== null || !child.stdin.writable) {
|
|
791
|
+
throw new Error("Codex app-server stdin is unavailable");
|
|
792
|
+
}
|
|
793
|
+
child.stdin.write(`${JSON.stringify(message)}
|
|
794
|
+
`);
|
|
795
|
+
}
|
|
796
|
+
handleStdout(chunk) {
|
|
797
|
+
this.stdoutBuffer += chunk;
|
|
798
|
+
while (true) {
|
|
799
|
+
const newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
800
|
+
if (newlineIndex < 0) break;
|
|
801
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
802
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
803
|
+
if (!line) continue;
|
|
804
|
+
try {
|
|
805
|
+
this.handleMessage(JSON.parse(line));
|
|
806
|
+
} catch (error) {
|
|
807
|
+
console.warn("[codex-app-server] Ignored invalid protocol line:", error instanceof Error ? error.message : error);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
handleMessage(message) {
|
|
812
|
+
if (message.id !== void 0 && !message.method) {
|
|
813
|
+
const pending = this.pendingRequests.get(message.id);
|
|
814
|
+
if (!pending) return;
|
|
815
|
+
this.pendingRequests.delete(message.id);
|
|
816
|
+
clearTimeout(pending.timer);
|
|
817
|
+
if (message.error) {
|
|
818
|
+
const detail = message.error.message || `JSON-RPC error ${message.error.code ?? ""}`.trim();
|
|
819
|
+
pending.reject(new Error(`${pending.method} failed: ${detail}`));
|
|
820
|
+
} else {
|
|
821
|
+
pending.resolve(message.result);
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (!message.method) return;
|
|
826
|
+
if (message.id !== void 0) {
|
|
827
|
+
void this.handleServerRequest(message);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const params = asRecord(message.params);
|
|
831
|
+
if (message.method === "thread/started") {
|
|
832
|
+
const thread = asRecord(params.thread);
|
|
833
|
+
const threadId2 = asString(thread.id);
|
|
834
|
+
if (threadId2) this.markThreadSubscribed(threadId2, getThreadStatus(thread.status));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (message.method === "thread/status/changed") {
|
|
838
|
+
const threadId2 = getThreadId(params);
|
|
839
|
+
const status = getThreadStatus(params.status);
|
|
840
|
+
if (threadId2 && (status === "system_error" || status === "not_loaded")) {
|
|
841
|
+
const active2 = this.findActiveTurnByThreadId(threadId2);
|
|
842
|
+
if (active2) {
|
|
843
|
+
active2.queue.push({
|
|
844
|
+
type: "turn.completed",
|
|
845
|
+
status: "failed",
|
|
846
|
+
errorMessage: status === "system_error" ? "Codex app-server reported a system error for this thread." : "Codex app-server unloaded this thread before the turn completed."
|
|
847
|
+
});
|
|
848
|
+
this.finishTurn(active2, {
|
|
849
|
+
threadStatus: status,
|
|
850
|
+
releaseThread: status === "system_error"
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
if (status === "not_loaded") this.forgetThread(threadId2);
|
|
854
|
+
else this.threadStatuses.set(threadId2, status);
|
|
855
|
+
} else if (threadId2) {
|
|
856
|
+
this.threadStatuses.set(threadId2, status);
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (message.method === "thread/closed") {
|
|
861
|
+
const threadId2 = getThreadId(params);
|
|
862
|
+
if (threadId2) {
|
|
863
|
+
const active2 = this.findActiveTurnByThreadId(threadId2);
|
|
864
|
+
if (active2) {
|
|
865
|
+
active2.queue.push({
|
|
866
|
+
type: "turn.completed",
|
|
867
|
+
status: "failed",
|
|
868
|
+
errorMessage: "Codex app-server closed the thread before the turn completed."
|
|
869
|
+
});
|
|
870
|
+
this.finishTurn(active2, { threadStatus: "not_loaded", releaseThread: false });
|
|
871
|
+
}
|
|
872
|
+
this.forgetThread(threadId2);
|
|
873
|
+
}
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (message.method === "serverRequest/resolved") {
|
|
877
|
+
const protocolRequestId = String(params.requestId ?? "");
|
|
878
|
+
const permissionRequestId = this.approvalPermissionIds.get(protocolRequestId);
|
|
879
|
+
if (permissionRequestId) {
|
|
880
|
+
this.approvalPermissionIds.delete(protocolRequestId);
|
|
881
|
+
this.pendingPermissions?.resolve(permissionRequestId, {
|
|
882
|
+
behavior: "deny",
|
|
883
|
+
message: "Permission request was resolved by Codex"
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const turnId = getTurnId(params);
|
|
889
|
+
const threadId = getThreadId(params);
|
|
890
|
+
const active = turnId ? this.activeTurns.get(turnId) : threadId ? this.findActiveTurnByThreadId(threadId) : void 0;
|
|
891
|
+
if (active) {
|
|
892
|
+
this.routeTurnMessage(active, message);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const starting = threadId ? this.startingTurns.get(threadId) : void 0;
|
|
896
|
+
if (starting) starting.messages.push(message);
|
|
897
|
+
}
|
|
898
|
+
routeTurnMessage(active, message) {
|
|
899
|
+
const method = message.method || "";
|
|
900
|
+
const params = asRecord(message.params);
|
|
901
|
+
const messageTurnId = getTurnId(params);
|
|
902
|
+
if (messageTurnId && messageTurnId !== active.turnId) return;
|
|
903
|
+
switch (method) {
|
|
904
|
+
case "item/started":
|
|
905
|
+
case "item/completed":
|
|
906
|
+
active.queue.push({
|
|
907
|
+
type: method === "item/started" ? "item.started" : "item.completed",
|
|
908
|
+
item: asRecord(params.item)
|
|
909
|
+
});
|
|
910
|
+
return;
|
|
911
|
+
case "item/agentMessage/delta":
|
|
912
|
+
active.queue.push({ type: "agent_message.delta", itemId: getItemId(params), delta: asString(params.delta) });
|
|
913
|
+
return;
|
|
914
|
+
case "item/reasoning/summaryTextDelta":
|
|
915
|
+
case "item/reasoning/textDelta":
|
|
916
|
+
active.queue.push({ type: "reasoning.delta", itemId: getItemId(params), delta: asString(params.delta) });
|
|
917
|
+
return;
|
|
918
|
+
case "turn/plan/updated": {
|
|
919
|
+
const plan = Array.isArray(params.plan) ? params.plan.map((entry) => {
|
|
920
|
+
const record = asRecord(entry);
|
|
921
|
+
return { step: asString(record.step), status: asString(record.status) };
|
|
922
|
+
}).filter((entry) => entry.step) : [];
|
|
923
|
+
active.queue.push({ type: "plan.updated", plan });
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
case "thread/tokenUsage/updated": {
|
|
927
|
+
const tokenUsage = asRecord(params.tokenUsage);
|
|
928
|
+
active.queue.push({ type: "usage.updated", usage: asRecord(tokenUsage.last) });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
case "model/rerouted": {
|
|
932
|
+
const model = asString(params.toModel ?? params.model);
|
|
933
|
+
if (model) active.queue.push({ type: "model.updated", model });
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
case "error": {
|
|
937
|
+
const error = asRecord(params.error);
|
|
938
|
+
const messageText = asString(error.message);
|
|
939
|
+
if (messageText && params.willRetry === true) {
|
|
940
|
+
active.queue.push({ type: "warning", message: messageText });
|
|
941
|
+
}
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
case "turn/completed": {
|
|
945
|
+
const turn = asRecord(params.turn);
|
|
946
|
+
const error = asRecord(turn.error);
|
|
947
|
+
active.queue.push({
|
|
948
|
+
type: "turn.completed",
|
|
949
|
+
status: asString(turn.status) || "completed",
|
|
950
|
+
...asString(error.message) ? { errorMessage: asString(error.message) } : {}
|
|
951
|
+
});
|
|
952
|
+
this.finishTurn(active);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
case "warning": {
|
|
956
|
+
const messageText = asString(params.message);
|
|
957
|
+
if (messageText) active.queue.push({ type: "warning", message: messageText });
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
default:
|
|
961
|
+
if (method && getTurnId(params) && !this.unknownMethods.has(method)) {
|
|
962
|
+
this.unknownMethods.add(method);
|
|
963
|
+
console.warn(`[codex-app-server] Unhandled turn notification: ${method}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async handleServerRequest(message) {
|
|
968
|
+
const id = message.id;
|
|
969
|
+
const method = message.method || "";
|
|
970
|
+
const params = asRecord(message.params);
|
|
971
|
+
try {
|
|
972
|
+
if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval" || method === "item/permissions/requestApproval" || method === "execCommandApproval" || method === "applyPatchApproval") {
|
|
973
|
+
const result = await this.requestApproval(id, method, params);
|
|
974
|
+
this.writeMessage({ id, result });
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (method === "item/tool/requestUserInput") {
|
|
978
|
+
const owner = this.getRequestOwner(params);
|
|
979
|
+
owner?.queue.push({
|
|
980
|
+
type: "warning",
|
|
981
|
+
message: "Codex requested interactive input, but this IM bridge cannot answer structured questions yet."
|
|
982
|
+
});
|
|
983
|
+
this.writeMessage({ id, result: { answers: {} } });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (method === "mcpServer/elicitation/request") {
|
|
987
|
+
const owner = this.getRequestOwner(params);
|
|
988
|
+
owner?.queue.push({
|
|
989
|
+
type: "warning",
|
|
990
|
+
message: "An MCP server requested interactive input; the request was declined by the IM bridge."
|
|
991
|
+
});
|
|
992
|
+
this.writeMessage({ id, result: { action: "decline", content: null, _meta: null } });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
this.writeMessage({ id, error: { code: -32601, message: `Unsupported server request: ${method}` } });
|
|
996
|
+
} catch (error) {
|
|
997
|
+
try {
|
|
998
|
+
this.writeMessage({
|
|
999
|
+
id,
|
|
1000
|
+
error: { code: -32603, message: error instanceof Error ? error.message : String(error) }
|
|
1001
|
+
});
|
|
1002
|
+
} catch {
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async requestApproval(id, method, params) {
|
|
1007
|
+
const requestId = `app-server:${this.generation}:${String(id)}`;
|
|
1008
|
+
const turnId = getTurnId(params);
|
|
1009
|
+
const active = turnId ? this.activeTurns.get(turnId) : void 0;
|
|
1010
|
+
const starting = this.startingTurns.get(getThreadId(params));
|
|
1011
|
+
const owner = active || starting;
|
|
1012
|
+
const toolName = method.includes("fileChange") || method === "applyPatchApproval" ? "apply_patch" : method.includes("permissions") ? "request_permissions" : "Bash";
|
|
1013
|
+
const suggestions = [params.reason, params.proposedExecpolicyAmendment, params.proposedNetworkPolicyAmendments].filter((value) => value != null);
|
|
1014
|
+
const resolutionPromise = this.pendingPermissions ? this.pendingPermissions.waitFor(requestId) : Promise.resolve({ behavior: "deny", message: "Permission gateway unavailable" });
|
|
1015
|
+
this.approvalPermissionIds.set(String(id), requestId);
|
|
1016
|
+
owner?.permissionRequestIds.add(requestId);
|
|
1017
|
+
owner?.queue.push({
|
|
1018
|
+
type: "permission.request",
|
|
1019
|
+
requestId,
|
|
1020
|
+
toolName,
|
|
1021
|
+
toolInput: params,
|
|
1022
|
+
...suggestions.length > 0 ? { suggestions } : {}
|
|
1023
|
+
});
|
|
1024
|
+
const resolution = await resolutionPromise;
|
|
1025
|
+
this.approvalPermissionIds.delete(String(id));
|
|
1026
|
+
owner?.permissionRequestIds.delete(requestId);
|
|
1027
|
+
const allowed = resolution.behavior === "allow";
|
|
1028
|
+
if (method === "item/permissions/requestApproval") {
|
|
1029
|
+
const requested = asRecord(params.permissions);
|
|
1030
|
+
const granted = {};
|
|
1031
|
+
if (allowed && requested.network) granted.network = requested.network;
|
|
1032
|
+
if (allowed && requested.fileSystem) granted.fileSystem = requested.fileSystem;
|
|
1033
|
+
return { permissions: granted, scope: "turn" };
|
|
1034
|
+
}
|
|
1035
|
+
if (method === "execCommandApproval" || method === "applyPatchApproval") {
|
|
1036
|
+
return { decision: allowed ? "approved" : "denied" };
|
|
1037
|
+
}
|
|
1038
|
+
return { decision: allowed ? "accept" : "decline" };
|
|
1039
|
+
}
|
|
1040
|
+
async interruptTurn(threadId, turnId) {
|
|
1041
|
+
const active = this.activeTurns.get(turnId);
|
|
1042
|
+
if (active && !active.interruptTimer) {
|
|
1043
|
+
const graceMs = Math.max(10, this.options.interruptGraceMs || DEFAULT_INTERRUPT_GRACE_MS);
|
|
1044
|
+
active.interruptTimer = setTimeout(() => {
|
|
1045
|
+
active.interruptTimer = void 0;
|
|
1046
|
+
if (this.activeTurns.get(turnId) !== active) return;
|
|
1047
|
+
active.queue.push({
|
|
1048
|
+
type: "turn.completed",
|
|
1049
|
+
status: "interrupted",
|
|
1050
|
+
errorMessage: "Codex app-server did not confirm interruption before the timeout."
|
|
1051
|
+
});
|
|
1052
|
+
this.finishTurn(active);
|
|
1053
|
+
void this.recycleChild(`interrupt timeout for turn ${turnId}`);
|
|
1054
|
+
}, graceMs);
|
|
1055
|
+
active.interruptTimer.unref?.();
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
await this.sendRequest("turn/interrupt", { threadId, turnId });
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
console.warn("[codex-app-server] Failed to interrupt turn:", error instanceof Error ? error.message : error);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
finishTurn(active, options = {}) {
|
|
1064
|
+
this.activeTurns.delete(active.turnId);
|
|
1065
|
+
this.threadStatuses.set(active.threadId, options.threadStatus || "idle");
|
|
1066
|
+
if (active.interruptTimer) {
|
|
1067
|
+
clearTimeout(active.interruptTimer);
|
|
1068
|
+
active.interruptTimer = void 0;
|
|
1069
|
+
}
|
|
1070
|
+
this.cancelPermissions(active, "Turn completed before the permission request was resolved");
|
|
1071
|
+
if (active.abortSignal && active.abortListener) {
|
|
1072
|
+
active.abortSignal.removeEventListener("abort", active.abortListener);
|
|
1073
|
+
}
|
|
1074
|
+
active.queue.close();
|
|
1075
|
+
if (options.releaseThread === false) this.clearThreadReleaseTimer(active.threadId);
|
|
1076
|
+
else this.scheduleThreadRelease(active.threadId);
|
|
1077
|
+
}
|
|
1078
|
+
findActiveTurnByThreadId(threadId) {
|
|
1079
|
+
for (const active of this.activeTurns.values()) {
|
|
1080
|
+
if (active.threadId === threadId) return active;
|
|
1081
|
+
}
|
|
1082
|
+
return void 0;
|
|
1083
|
+
}
|
|
1084
|
+
getRequestOwner(params) {
|
|
1085
|
+
const turnId = getTurnId(params);
|
|
1086
|
+
if (turnId) return this.activeTurns.get(turnId);
|
|
1087
|
+
const threadId = getThreadId(params);
|
|
1088
|
+
if (!threadId) return void 0;
|
|
1089
|
+
return this.findActiveTurnByThreadId(threadId) || this.startingTurns.get(threadId);
|
|
1090
|
+
}
|
|
1091
|
+
markThreadSubscribed(threadId, status) {
|
|
1092
|
+
this.subscribedThreadIds.add(threadId);
|
|
1093
|
+
this.threadStatuses.set(threadId, status === "unknown" ? "idle" : status);
|
|
1094
|
+
this.clearThreadReleaseTimer(threadId);
|
|
1095
|
+
}
|
|
1096
|
+
forgetThread(threadId) {
|
|
1097
|
+
this.clearThreadReleaseTimer(threadId);
|
|
1098
|
+
this.subscribedThreadIds.delete(threadId);
|
|
1099
|
+
this.threadStatuses.delete(threadId);
|
|
1100
|
+
}
|
|
1101
|
+
scheduleThreadRelease(threadId) {
|
|
1102
|
+
this.clearThreadReleaseTimer(threadId);
|
|
1103
|
+
const idleMs = this.options.threadIdleReleaseMs ?? DEFAULT_THREAD_IDLE_RELEASE_MS;
|
|
1104
|
+
if (!Number.isFinite(idleMs) || idleMs < 0) return;
|
|
1105
|
+
const timer = setTimeout(() => {
|
|
1106
|
+
this.threadReleaseTimers.delete(threadId);
|
|
1107
|
+
void this.unsubscribeThread(threadId);
|
|
1108
|
+
}, Math.max(1, idleMs));
|
|
1109
|
+
timer.unref?.();
|
|
1110
|
+
this.threadReleaseTimers.set(threadId, timer);
|
|
1111
|
+
}
|
|
1112
|
+
clearThreadReleaseTimer(threadId) {
|
|
1113
|
+
const timer = this.threadReleaseTimers.get(threadId);
|
|
1114
|
+
if (!timer) return;
|
|
1115
|
+
clearTimeout(timer);
|
|
1116
|
+
this.threadReleaseTimers.delete(threadId);
|
|
1117
|
+
}
|
|
1118
|
+
clearAllThreadReleaseTimers() {
|
|
1119
|
+
for (const timer of this.threadReleaseTimers.values()) clearTimeout(timer);
|
|
1120
|
+
this.threadReleaseTimers.clear();
|
|
1121
|
+
}
|
|
1122
|
+
async unsubscribeThread(threadId) {
|
|
1123
|
+
if (!this.subscribedThreadIds.has(threadId) || this.findActiveTurnByThreadId(threadId)) return;
|
|
1124
|
+
try {
|
|
1125
|
+
await this.sendRequest("thread/unsubscribe", { threadId });
|
|
1126
|
+
this.forgetThread(threadId);
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
console.warn(
|
|
1129
|
+
`[codex-app-server] Failed to release idle thread ${threadId}:`,
|
|
1130
|
+
error instanceof Error ? error.message : error
|
|
1131
|
+
);
|
|
1132
|
+
if (this.child) this.scheduleThreadRelease(threadId);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
cancelPermissions(owner, message) {
|
|
1136
|
+
for (const requestId of owner.permissionRequestIds) {
|
|
1137
|
+
this.pendingPermissions?.resolve(requestId, { behavior: "deny", message });
|
|
1138
|
+
}
|
|
1139
|
+
owner.permissionRequestIds.clear();
|
|
1140
|
+
}
|
|
1141
|
+
recycleChild(reason) {
|
|
1142
|
+
const child = this.child;
|
|
1143
|
+
if (this.recyclePromise) return this.recyclePromise;
|
|
1144
|
+
if (!child || child.exitCode !== null) return Promise.resolve();
|
|
1145
|
+
console.warn(`[codex-app-server] Recycling app-server: ${reason}`);
|
|
1146
|
+
this.recyclePromise = new Promise((resolve2) => {
|
|
1147
|
+
let settled = false;
|
|
1148
|
+
const finish = () => {
|
|
1149
|
+
if (settled) return;
|
|
1150
|
+
settled = true;
|
|
1151
|
+
clearTimeout(timer);
|
|
1152
|
+
child.off("exit", finish);
|
|
1153
|
+
this.recyclePromise = null;
|
|
1154
|
+
resolve2();
|
|
1155
|
+
};
|
|
1156
|
+
const timer = setTimeout(() => {
|
|
1157
|
+
if (this.child === child) this.handleExit(child, null, "SIGKILL");
|
|
1158
|
+
try {
|
|
1159
|
+
child.kill("SIGKILL");
|
|
1160
|
+
} catch {
|
|
1161
|
+
}
|
|
1162
|
+
finish();
|
|
1163
|
+
}, 2e3);
|
|
1164
|
+
child.once("exit", finish);
|
|
1165
|
+
try {
|
|
1166
|
+
child.kill();
|
|
1167
|
+
} catch {
|
|
1168
|
+
finish();
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
return this.recyclePromise;
|
|
1172
|
+
}
|
|
1173
|
+
handleExit(child, code2, signal) {
|
|
1174
|
+
if (this.child !== child) return;
|
|
1175
|
+
this.child = null;
|
|
1176
|
+
this.startupPromise = null;
|
|
1177
|
+
this.clearAllThreadReleaseTimers();
|
|
1178
|
+
this.subscribedThreadIds.clear();
|
|
1179
|
+
this.threadStatuses.clear();
|
|
1180
|
+
const detail = this.stderrTail.trim();
|
|
1181
|
+
const error = new Error(
|
|
1182
|
+
`Codex app-server exited (${signal || (code2 ?? "unknown")})${detail ? `: ${detail}` : ""}`
|
|
1183
|
+
);
|
|
1184
|
+
for (const pending of this.pendingRequests.values()) {
|
|
1185
|
+
clearTimeout(pending.timer);
|
|
1186
|
+
pending.reject(error);
|
|
1187
|
+
}
|
|
1188
|
+
this.pendingRequests.clear();
|
|
1189
|
+
for (const active of this.activeTurns.values()) {
|
|
1190
|
+
if (active.interruptTimer) clearTimeout(active.interruptTimer);
|
|
1191
|
+
this.cancelPermissions(active, "Codex app-server exited");
|
|
1192
|
+
active.queue.fail(error);
|
|
1193
|
+
}
|
|
1194
|
+
this.activeTurns.clear();
|
|
1195
|
+
for (const starting of this.startingTurns.values()) {
|
|
1196
|
+
this.cancelPermissions(starting, "Codex app-server exited");
|
|
1197
|
+
starting.queue.fail(error);
|
|
1198
|
+
}
|
|
1199
|
+
this.startingTurns.clear();
|
|
1200
|
+
this.approvalPermissionIds.clear();
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
|
|
321
1206
|
// src/codex-provider.ts
|
|
322
1207
|
var codex_provider_exports = {};
|
|
323
1208
|
__export(codex_provider_exports, {
|
|
324
1209
|
CodexProvider: () => CodexProvider,
|
|
325
1210
|
mapCodexUsage: () => mapCodexUsage
|
|
326
1211
|
});
|
|
327
|
-
import
|
|
1212
|
+
import fs18 from "node:fs";
|
|
328
1213
|
import os4 from "node:os";
|
|
329
|
-
import
|
|
1214
|
+
import path19 from "node:path";
|
|
330
1215
|
function isRecord(value) {
|
|
331
1216
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
332
1217
|
}
|
|
@@ -335,13 +1220,13 @@ function normalizeTokenCount(value) {
|
|
|
335
1220
|
}
|
|
336
1221
|
function mapCodexUsage(usage) {
|
|
337
1222
|
if (!isRecord(usage)) return void 0;
|
|
338
|
-
const cacheRead = usage.cached_input_tokens ?? usage.cache_read_input_tokens;
|
|
339
|
-
const cacheCreation = usage.cache_write_input_tokens ?? usage.cache_creation_input_tokens;
|
|
1223
|
+
const cacheRead = usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? usage.cachedInputTokens;
|
|
1224
|
+
const cacheCreation = usage.cache_write_input_tokens ?? usage.cache_creation_input_tokens ?? usage.cacheWriteInputTokens;
|
|
340
1225
|
return {
|
|
341
|
-
input_tokens: normalizeTokenCount(usage.input_tokens),
|
|
342
|
-
output_tokens: normalizeTokenCount(usage.output_tokens),
|
|
1226
|
+
input_tokens: normalizeTokenCount(usage.input_tokens ?? usage.inputTokens),
|
|
1227
|
+
output_tokens: normalizeTokenCount(usage.output_tokens ?? usage.outputTokens),
|
|
343
1228
|
cache_read_input_tokens: normalizeTokenCount(cacheRead),
|
|
344
|
-
reasoning_output_tokens: normalizeTokenCount(usage.reasoning_output_tokens),
|
|
1229
|
+
reasoning_output_tokens: normalizeTokenCount(usage.reasoning_output_tokens ?? usage.reasoningOutputTokens),
|
|
345
1230
|
...cacheCreation === void 0 ? {} : { cache_creation_input_tokens: normalizeTokenCount(cacheCreation) }
|
|
346
1231
|
};
|
|
347
1232
|
}
|
|
@@ -449,11 +1334,88 @@ function stringifyUnknown(value) {
|
|
|
449
1334
|
return String(value);
|
|
450
1335
|
}
|
|
451
1336
|
}
|
|
1337
|
+
function normalizeTransport(value) {
|
|
1338
|
+
return value === "app-server" || value === "auto" ? value : "sdk";
|
|
1339
|
+
}
|
|
1340
|
+
function normalizeAppServerItem(item) {
|
|
1341
|
+
const type = typeof item.type === "string" ? item.type : "";
|
|
1342
|
+
const id = typeof item.id === "string" ? item.id : `item-${Date.now()}`;
|
|
1343
|
+
switch (type) {
|
|
1344
|
+
case "agentMessage":
|
|
1345
|
+
return { type: "agent_message", id, text: typeof item.text === "string" ? item.text : "" };
|
|
1346
|
+
case "reasoning": {
|
|
1347
|
+
const summary = Array.isArray(item.summary) ? item.summary.filter((part) => typeof part === "string") : [];
|
|
1348
|
+
const content = Array.isArray(item.content) ? item.content.filter((part) => typeof part === "string") : [];
|
|
1349
|
+
return { type: "reasoning", id, text: [...summary, ...content].join("\n\n") };
|
|
1350
|
+
}
|
|
1351
|
+
case "commandExecution":
|
|
1352
|
+
return {
|
|
1353
|
+
type: "command_execution",
|
|
1354
|
+
id,
|
|
1355
|
+
command: typeof item.command === "string" ? item.command : "",
|
|
1356
|
+
aggregated_output: typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : "",
|
|
1357
|
+
exit_code: typeof item.exitCode === "number" ? item.exitCode : void 0,
|
|
1358
|
+
status: item.status
|
|
1359
|
+
};
|
|
1360
|
+
case "fileChange":
|
|
1361
|
+
return {
|
|
1362
|
+
type: "file_change",
|
|
1363
|
+
id,
|
|
1364
|
+
changes: Array.isArray(item.changes) ? item.changes : [],
|
|
1365
|
+
status: item.status
|
|
1366
|
+
};
|
|
1367
|
+
case "mcpToolCall":
|
|
1368
|
+
return {
|
|
1369
|
+
type: "mcp_tool_call",
|
|
1370
|
+
id,
|
|
1371
|
+
server: typeof item.server === "string" ? item.server : "",
|
|
1372
|
+
tool: typeof item.tool === "string" ? item.tool : "",
|
|
1373
|
+
arguments: item.arguments,
|
|
1374
|
+
status: item.status,
|
|
1375
|
+
result: item.result,
|
|
1376
|
+
error: item.error
|
|
1377
|
+
};
|
|
1378
|
+
case "dynamicToolCall":
|
|
1379
|
+
return {
|
|
1380
|
+
type: "mcp_tool_call",
|
|
1381
|
+
id,
|
|
1382
|
+
server: typeof item.namespace === "string" ? item.namespace : "dynamic",
|
|
1383
|
+
tool: typeof item.tool === "string" ? item.tool : "tool",
|
|
1384
|
+
arguments: item.arguments,
|
|
1385
|
+
status: item.status,
|
|
1386
|
+
result: { content: item.contentItems },
|
|
1387
|
+
error: item.success === false ? { message: "Dynamic tool call failed" } : void 0
|
|
1388
|
+
};
|
|
1389
|
+
case "webSearch":
|
|
1390
|
+
return {
|
|
1391
|
+
type: "web_search",
|
|
1392
|
+
id,
|
|
1393
|
+
query: typeof item.query === "string" ? item.query : ""
|
|
1394
|
+
};
|
|
1395
|
+
case "imageGeneration": {
|
|
1396
|
+
const savedPath = typeof item.savedPath === "string" ? item.savedPath : "";
|
|
1397
|
+
const resultText = savedPath || (typeof item.result === "string" ? item.result : "Image generation completed");
|
|
1398
|
+
return {
|
|
1399
|
+
type: "mcp_tool_call",
|
|
1400
|
+
id,
|
|
1401
|
+
server: "codex",
|
|
1402
|
+
tool: "image_generation",
|
|
1403
|
+
arguments: { prompt: item.revisedPrompt },
|
|
1404
|
+
status: item.status,
|
|
1405
|
+
result: { content: [{ type: "text", text: resultText }] },
|
|
1406
|
+
error: item.status === "failed" ? { message: resultText } : void 0
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
default:
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
452
1413
|
var MIME_EXT, DEFAULT_TERMINAL_DRAIN_TIMEOUT_MS, CodexProvider;
|
|
453
1414
|
var init_codex_provider = __esm({
|
|
454
1415
|
"src/codex-provider.ts"() {
|
|
455
1416
|
"use strict";
|
|
456
1417
|
init_sse_utils();
|
|
1418
|
+
init_codex_app_server_client();
|
|
457
1419
|
init_runtime_options();
|
|
458
1420
|
MIME_EXT = {
|
|
459
1421
|
"image/png": ".png",
|
|
@@ -466,9 +1428,26 @@ var init_codex_provider = __esm({
|
|
|
466
1428
|
CodexProvider = class {
|
|
467
1429
|
sdk = null;
|
|
468
1430
|
codex = null;
|
|
1431
|
+
pendingPermissions;
|
|
1432
|
+
transport;
|
|
1433
|
+
appServerClient;
|
|
1434
|
+
warnedDesktopAppServerOverride = false;
|
|
469
1435
|
/** Maps session IDs to Codex thread IDs for resume. */
|
|
470
1436
|
threadIds = /* @__PURE__ */ new Map();
|
|
471
|
-
constructor(
|
|
1437
|
+
constructor(pendingPerms, options = {}) {
|
|
1438
|
+
this.pendingPermissions = pendingPerms;
|
|
1439
|
+
this.transport = normalizeTransport(options.transport ?? process.env.CTI_CODEX_TRANSPORT);
|
|
1440
|
+
this.appServerClient = options.appServerClient || null;
|
|
1441
|
+
}
|
|
1442
|
+
async dispose() {
|
|
1443
|
+
await this.appServerClient?.close();
|
|
1444
|
+
}
|
|
1445
|
+
getOwnedProcessIds() {
|
|
1446
|
+
return this.appServerClient?.getOwnedProcessIds() || [];
|
|
1447
|
+
}
|
|
1448
|
+
getThreadRuntimeStatus(threadId) {
|
|
1449
|
+
const runtime = this.appServerClient?.getThreadRuntimeStatus(threadId);
|
|
1450
|
+
return runtime ? { transport: "app-server", ...runtime } : null;
|
|
472
1451
|
}
|
|
473
1452
|
clearCachedThreadId(sessionId) {
|
|
474
1453
|
this.threadIds.delete(sessionId);
|
|
@@ -514,6 +1493,20 @@ var init_codex_provider = __esm({
|
|
|
514
1493
|
return { sdk: this.sdk, codex: this.codex };
|
|
515
1494
|
}
|
|
516
1495
|
streamChat(params) {
|
|
1496
|
+
const desktopBacked = params.sessionOrigin === "desktop" || Boolean(params.desktopThreadId);
|
|
1497
|
+
if (desktopBacked && this.transport === "app-server" && !this.warnedDesktopAppServerOverride) {
|
|
1498
|
+
this.warnedDesktopAppServerOverride = true;
|
|
1499
|
+
console.warn(
|
|
1500
|
+
"[codex-provider] Ignoring CTI_CODEX_TRANSPORT=app-server for Desktop-backed sessions; a separate app-server cannot safely attach to the Desktop host process."
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
const useAppServer = !desktopBacked && (this.transport === "app-server" || this.transport === "auto");
|
|
1504
|
+
if (useAppServer) {
|
|
1505
|
+
return this.streamChatViaAppServer(params, this.transport === "auto");
|
|
1506
|
+
}
|
|
1507
|
+
return this.streamChatViaSdk(params);
|
|
1508
|
+
}
|
|
1509
|
+
streamChatViaSdk(params) {
|
|
517
1510
|
const self = this;
|
|
518
1511
|
return new ReadableStream({
|
|
519
1512
|
start(controller) {
|
|
@@ -543,13 +1536,13 @@ var init_codex_provider = __esm({
|
|
|
543
1536
|
{ type: "text", text: params.prompt }
|
|
544
1537
|
];
|
|
545
1538
|
for (const file of imageFiles) {
|
|
546
|
-
if (file.filePath &&
|
|
1539
|
+
if (file.filePath && fs18.existsSync(file.filePath)) {
|
|
547
1540
|
parts.push({ type: "local_image", path: file.filePath });
|
|
548
1541
|
continue;
|
|
549
1542
|
}
|
|
550
1543
|
const ext = MIME_EXT[file.type] || ".png";
|
|
551
|
-
const tmpPath =
|
|
552
|
-
|
|
1544
|
+
const tmpPath = path19.join(os4.tmpdir(), `cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
|
|
1545
|
+
fs18.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
|
|
553
1546
|
tempFiles.push(tmpPath);
|
|
554
1547
|
parts.push({ type: "local_image", path: tmpPath });
|
|
555
1548
|
}
|
|
@@ -715,7 +1708,212 @@ var init_codex_provider = __esm({
|
|
|
715
1708
|
} finally {
|
|
716
1709
|
for (const tmp of tempFiles) {
|
|
717
1710
|
try {
|
|
718
|
-
|
|
1711
|
+
fs18.unlinkSync(tmp);
|
|
1712
|
+
} catch {
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
})();
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
streamChatViaAppServer(params, allowSdkFallback) {
|
|
1721
|
+
const self = this;
|
|
1722
|
+
return new ReadableStream({
|
|
1723
|
+
start(controller) {
|
|
1724
|
+
(async () => {
|
|
1725
|
+
const tempFiles = [];
|
|
1726
|
+
try {
|
|
1727
|
+
const imageFiles = params.files?.filter((file) => file.type.startsWith("image/")) || [];
|
|
1728
|
+
const imagePaths = [];
|
|
1729
|
+
for (const file of imageFiles) {
|
|
1730
|
+
if (file.filePath && fs18.existsSync(file.filePath)) {
|
|
1731
|
+
imagePaths.push(file.filePath);
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
const ext = MIME_EXT[file.type] || ".png";
|
|
1735
|
+
const tmpPath = path19.join(
|
|
1736
|
+
os4.tmpdir(),
|
|
1737
|
+
`cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`
|
|
1738
|
+
);
|
|
1739
|
+
fs18.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
|
|
1740
|
+
tempFiles.push(tmpPath);
|
|
1741
|
+
imagePaths.push(tmpPath);
|
|
1742
|
+
}
|
|
1743
|
+
self.appServerClient ||= new CodexAppServerClient(self.pendingPermissions);
|
|
1744
|
+
let turn;
|
|
1745
|
+
try {
|
|
1746
|
+
turn = await self.appServerClient.startTurn({
|
|
1747
|
+
savedThreadId: self.threadIds.get(params.sessionId) || params.sdkSessionId || void 0,
|
|
1748
|
+
prompt: params.prompt,
|
|
1749
|
+
imagePaths,
|
|
1750
|
+
model: params.model,
|
|
1751
|
+
forceModel: params.forceModel,
|
|
1752
|
+
modelReasoningEffort: parseReasoningEffort(params.modelReasoningEffort),
|
|
1753
|
+
workingDirectory: params.workingDirectory,
|
|
1754
|
+
sandboxMode: normalizeSandboxMode(params.sandboxMode),
|
|
1755
|
+
permissionMode: params.permissionMode,
|
|
1756
|
+
systemPrompt: params.systemPrompt,
|
|
1757
|
+
skipGitRepoCheck: shouldSkipGitRepoCheck(),
|
|
1758
|
+
abortSignal: params.abortController?.signal
|
|
1759
|
+
});
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
if (allowSdkFallback && error instanceof AppServerPreTurnError) {
|
|
1762
|
+
console.warn("[codex-provider] app-server unavailable before turn/start; falling back to SDK:", error.message);
|
|
1763
|
+
const fallback = self.streamChatViaSdk(params).getReader();
|
|
1764
|
+
try {
|
|
1765
|
+
while (true) {
|
|
1766
|
+
const { done, value } = await fallback.read();
|
|
1767
|
+
if (done) break;
|
|
1768
|
+
controller.enqueue(value);
|
|
1769
|
+
}
|
|
1770
|
+
controller.close();
|
|
1771
|
+
} finally {
|
|
1772
|
+
fallback.releaseLock();
|
|
1773
|
+
}
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
throw error;
|
|
1777
|
+
}
|
|
1778
|
+
self.threadIds.set(params.sessionId, turn.threadId);
|
|
1779
|
+
controller.enqueue(sseEvent("status", {
|
|
1780
|
+
session_id: turn.threadId,
|
|
1781
|
+
turn_id: turn.turnId,
|
|
1782
|
+
transport: "app-server"
|
|
1783
|
+
}));
|
|
1784
|
+
const emittedToolStarts = /* @__PURE__ */ new Set();
|
|
1785
|
+
const streamedAgentItems = /* @__PURE__ */ new Set();
|
|
1786
|
+
const agentItemPhases = /* @__PURE__ */ new Map();
|
|
1787
|
+
const reasoningText = /* @__PURE__ */ new Map();
|
|
1788
|
+
let usage;
|
|
1789
|
+
let terminalSeen = false;
|
|
1790
|
+
for await (const event of turn.events) {
|
|
1791
|
+
if (params.abortController?.signal.aborted && event.type !== "turn.completed") {
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
switch (event.type) {
|
|
1795
|
+
case "agent_message.delta":
|
|
1796
|
+
if (event.delta) {
|
|
1797
|
+
if (agentItemPhases.get(event.itemId) === "commentary") {
|
|
1798
|
+
const accumulated = `${reasoningText.get(event.itemId) || ""}${event.delta}`;
|
|
1799
|
+
reasoningText.set(event.itemId, accumulated);
|
|
1800
|
+
controller.enqueue(sseEvent("status", { reasoning: accumulated }));
|
|
1801
|
+
break;
|
|
1802
|
+
}
|
|
1803
|
+
streamedAgentItems.add(event.itemId);
|
|
1804
|
+
controller.enqueue(sseEvent("text", event.delta));
|
|
1805
|
+
}
|
|
1806
|
+
break;
|
|
1807
|
+
case "reasoning.delta": {
|
|
1808
|
+
const accumulated = `${reasoningText.get(event.itemId) || ""}${event.delta}`;
|
|
1809
|
+
reasoningText.set(event.itemId, accumulated);
|
|
1810
|
+
if (accumulated.trim()) {
|
|
1811
|
+
controller.enqueue(sseEvent("status", { reasoning: accumulated }));
|
|
1812
|
+
}
|
|
1813
|
+
break;
|
|
1814
|
+
}
|
|
1815
|
+
case "item.started":
|
|
1816
|
+
case "item.completed": {
|
|
1817
|
+
const itemId = typeof event.item.id === "string" ? event.item.id : "";
|
|
1818
|
+
if (event.item.type === "agentMessage" && itemId) {
|
|
1819
|
+
const phase = typeof event.item.phase === "string" ? event.item.phase : "";
|
|
1820
|
+
if (phase) agentItemPhases.set(itemId, phase);
|
|
1821
|
+
if (phase === "commentary") {
|
|
1822
|
+
if (event.type === "item.completed" && !reasoningText.has(itemId)) {
|
|
1823
|
+
const text2 = typeof event.item.text === "string" ? event.item.text : "";
|
|
1824
|
+
if (text2.trim()) controller.enqueue(sseEvent("status", { reasoning: text2 }));
|
|
1825
|
+
}
|
|
1826
|
+
break;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const normalized = normalizeAppServerItem(event.item);
|
|
1830
|
+
if (!normalized) break;
|
|
1831
|
+
if (event.type === "item.completed" && normalized.type === "agent_message" && streamedAgentItems.has(normalized.id)) {
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
self.handleItemEvent(
|
|
1835
|
+
controller,
|
|
1836
|
+
normalized,
|
|
1837
|
+
event.type === "item.started" ? "started" : "completed",
|
|
1838
|
+
params.sessionId,
|
|
1839
|
+
emittedToolStarts
|
|
1840
|
+
);
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
case "plan.updated": {
|
|
1844
|
+
const tasks = event.plan.map((step) => ({
|
|
1845
|
+
text: step.step,
|
|
1846
|
+
status: step.status === "completed" ? "completed" : step.status === "inProgress" || step.status === "in_progress" ? "in_progress" : "pending"
|
|
1847
|
+
}));
|
|
1848
|
+
controller.enqueue(sseEvent("task_update", {
|
|
1849
|
+
session_id: turn.threadId,
|
|
1850
|
+
sdk_session_id: turn.threadId,
|
|
1851
|
+
tasks,
|
|
1852
|
+
todos: tasks
|
|
1853
|
+
}));
|
|
1854
|
+
break;
|
|
1855
|
+
}
|
|
1856
|
+
case "usage.updated":
|
|
1857
|
+
usage = mapCodexUsage(event.usage);
|
|
1858
|
+
break;
|
|
1859
|
+
case "model.updated":
|
|
1860
|
+
controller.enqueue(sseEvent("status", { model: event.model }));
|
|
1861
|
+
break;
|
|
1862
|
+
case "permission.request":
|
|
1863
|
+
controller.enqueue(sseEvent("permission_request", {
|
|
1864
|
+
permissionRequestId: event.requestId,
|
|
1865
|
+
toolName: event.toolName,
|
|
1866
|
+
toolInput: event.toolInput,
|
|
1867
|
+
suggestions: event.suggestions
|
|
1868
|
+
}));
|
|
1869
|
+
break;
|
|
1870
|
+
case "warning":
|
|
1871
|
+
controller.enqueue(sseEvent("status", { reasoning: event.message }));
|
|
1872
|
+
break;
|
|
1873
|
+
case "turn.completed":
|
|
1874
|
+
terminalSeen = true;
|
|
1875
|
+
if (event.status === "completed") {
|
|
1876
|
+
controller.enqueue(sseEvent("result", {
|
|
1877
|
+
usage,
|
|
1878
|
+
session_id: turn.threadId,
|
|
1879
|
+
turn_id: turn.turnId
|
|
1880
|
+
}));
|
|
1881
|
+
} else if (event.status === "failed") {
|
|
1882
|
+
self.clearCachedThreadId(params.sessionId);
|
|
1883
|
+
controller.enqueue(sseEvent(
|
|
1884
|
+
"error",
|
|
1885
|
+
normalizeCodexErrorMessage(event.errorMessage || "Turn failed")
|
|
1886
|
+
));
|
|
1887
|
+
} else if (!params.abortController?.signal.aborted) {
|
|
1888
|
+
controller.enqueue(sseEvent("error", event.errorMessage || "Task interrupted"));
|
|
1889
|
+
}
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
if (!terminalSeen && !params.abortController?.signal.aborted) {
|
|
1894
|
+
throw new Error("Codex app-server stream ended without turn/completed");
|
|
1895
|
+
}
|
|
1896
|
+
controller.close();
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1899
|
+
console.error("[codex-provider] app-server error:", error instanceof Error ? error.stack || error.message : error);
|
|
1900
|
+
self.clearCachedThreadId(params.sessionId);
|
|
1901
|
+
if (params.abortController?.signal.aborted) {
|
|
1902
|
+
try {
|
|
1903
|
+
controller.close();
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
try {
|
|
1909
|
+
controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(message)));
|
|
1910
|
+
controller.close();
|
|
1911
|
+
} catch {
|
|
1912
|
+
}
|
|
1913
|
+
} finally {
|
|
1914
|
+
for (const tempFile of tempFiles) {
|
|
1915
|
+
try {
|
|
1916
|
+
fs18.unlinkSync(tempFile);
|
|
719
1917
|
} catch {
|
|
720
1918
|
}
|
|
721
1919
|
}
|
|
@@ -845,9 +2043,9 @@ var init_codex_provider = __esm({
|
|
|
845
2043
|
});
|
|
846
2044
|
|
|
847
2045
|
// src/main.ts
|
|
848
|
-
import
|
|
849
|
-
import
|
|
850
|
-
import
|
|
2046
|
+
import fs19 from "node:fs";
|
|
2047
|
+
import path20 from "node:path";
|
|
2048
|
+
import crypto12 from "node:crypto";
|
|
851
2049
|
|
|
852
2050
|
// src/lib/bridge/context.ts
|
|
853
2051
|
var CONTEXT_KEY = "__bridge_context__";
|
|
@@ -3286,20 +4484,20 @@ ${trimmedResponse}`;
|
|
|
3286
4484
|
buffer = Buffer.concat(chunks);
|
|
3287
4485
|
} catch (streamErr) {
|
|
3288
4486
|
console.warn("[feishu-adapter] Stream read failed, falling back to writeFile:", streamErr instanceof Error ? streamErr.message : streamErr);
|
|
3289
|
-
const
|
|
4487
|
+
const fs20 = await import("fs");
|
|
3290
4488
|
const os5 = await import("os");
|
|
3291
|
-
const
|
|
3292
|
-
const tmpPath =
|
|
4489
|
+
const path21 = await import("path");
|
|
4490
|
+
const tmpPath = path21.join(os5.tmpdir(), `feishu-dl-${crypto3.randomUUID()}`);
|
|
3293
4491
|
try {
|
|
3294
4492
|
await res.writeFile(tmpPath);
|
|
3295
|
-
buffer =
|
|
4493
|
+
buffer = fs20.readFileSync(tmpPath);
|
|
3296
4494
|
if (buffer.length > MAX_FILE_SIZE) {
|
|
3297
4495
|
console.warn(`[feishu-adapter] Resource too large (>${MAX_FILE_SIZE} bytes), key: ${fileKey}`);
|
|
3298
4496
|
return null;
|
|
3299
4497
|
}
|
|
3300
4498
|
} finally {
|
|
3301
4499
|
try {
|
|
3302
|
-
|
|
4500
|
+
fs20.unlinkSync(tmpPath);
|
|
3303
4501
|
} catch {
|
|
3304
4502
|
}
|
|
3305
4503
|
}
|
|
@@ -10331,11 +11529,25 @@ import fs5 from "node:fs";
|
|
|
10331
11529
|
import os2 from "node:os";
|
|
10332
11530
|
import path4 from "node:path";
|
|
10333
11531
|
import crypto7 from "node:crypto";
|
|
10334
|
-
import {
|
|
11532
|
+
import { createRequire } from "node:module";
|
|
10335
11533
|
var ACTIVE_WINDOW_MS = 15 * 60 * 1e3;
|
|
10336
11534
|
var MAX_SESSION_META_BYTES = 4 * 1024 * 1024;
|
|
10337
11535
|
var MAX_SESSION_TITLE_SCAN_BYTES = 512 * 1024;
|
|
11536
|
+
var INITIAL_HISTORY_TAIL_BYTES = 256 * 1024;
|
|
11537
|
+
var MAX_HISTORY_TAIL_BYTES = 64 * 1024 * 1024;
|
|
10338
11538
|
var TITLE_MAX_CHARS = 72;
|
|
11539
|
+
var localRequire = createRequire(import.meta.url);
|
|
11540
|
+
var databaseSyncConstructor;
|
|
11541
|
+
function getDatabaseSyncConstructor() {
|
|
11542
|
+
if (databaseSyncConstructor !== void 0) return databaseSyncConstructor;
|
|
11543
|
+
try {
|
|
11544
|
+
const sqlite = localRequire("node:sqlite");
|
|
11545
|
+
databaseSyncConstructor = typeof sqlite.DatabaseSync === "function" ? sqlite.DatabaseSync : null;
|
|
11546
|
+
} catch {
|
|
11547
|
+
databaseSyncConstructor = null;
|
|
11548
|
+
}
|
|
11549
|
+
return databaseSyncConstructor;
|
|
11550
|
+
}
|
|
10339
11551
|
function getCodexHome() {
|
|
10340
11552
|
return process.env.CODEX_HOME || path4.join(os2.homedir(), ".codex");
|
|
10341
11553
|
}
|
|
@@ -10478,10 +11690,37 @@ function walkSessionFiles(dirPath, target) {
|
|
|
10478
11690
|
}
|
|
10479
11691
|
}
|
|
10480
11692
|
}
|
|
11693
|
+
function normalizeSourceKind(value) {
|
|
11694
|
+
if (typeof value === "string") {
|
|
11695
|
+
const normalized = value.trim();
|
|
11696
|
+
if (!normalized) return "";
|
|
11697
|
+
if (normalized.startsWith("{")) {
|
|
11698
|
+
try {
|
|
11699
|
+
return normalizeSourceKind(JSON.parse(normalized));
|
|
11700
|
+
} catch {
|
|
11701
|
+
return normalized.toLowerCase();
|
|
11702
|
+
}
|
|
11703
|
+
}
|
|
11704
|
+
return normalized.toLowerCase();
|
|
11705
|
+
}
|
|
11706
|
+
if (!value || typeof value !== "object") return "";
|
|
11707
|
+
const record = value;
|
|
11708
|
+
if ("subagent" in record || "sub_agent" in record) return "subagent";
|
|
11709
|
+
for (const key of ["type", "kind", "source", "name"]) {
|
|
11710
|
+
const nested = normalizeSourceKind(record[key]);
|
|
11711
|
+
if (nested) return nested;
|
|
11712
|
+
}
|
|
11713
|
+
return "";
|
|
11714
|
+
}
|
|
11715
|
+
function isSubagentThread(source, threadSource, parentThreadId) {
|
|
11716
|
+
if (typeof parentThreadId === "string" && parentThreadId.trim()) return true;
|
|
11717
|
+
return normalizeSourceKind(threadSource) === "subagent" || normalizeSourceKind(source) === "subagent";
|
|
11718
|
+
}
|
|
10481
11719
|
function isDesktopLike(meta) {
|
|
10482
11720
|
const originator = typeof meta?.originator === "string" ? meta.originator.toLowerCase() : "";
|
|
10483
|
-
const source =
|
|
11721
|
+
const source = normalizeSourceKind(meta?.source);
|
|
10484
11722
|
if (source === "exec") return false;
|
|
11723
|
+
if (isSubagentThread(meta?.source, meta?.thread_source, meta?.parent_thread_id)) return false;
|
|
10485
11724
|
return originator.includes("desktop") || source === "vscode" || source === "desktop";
|
|
10486
11725
|
}
|
|
10487
11726
|
function loadThreadIndexEntries(archivedThreadIds) {
|
|
@@ -10530,25 +11769,46 @@ function parseUpdatedAtValue(value) {
|
|
|
10530
11769
|
function loadVisibleDesktopThreads(limit) {
|
|
10531
11770
|
const dbPath = getDesktopStateDbPath();
|
|
10532
11771
|
if (!dbPath || !fs5.existsSync(dbPath)) return null;
|
|
11772
|
+
const DatabaseSync = getDatabaseSyncConstructor();
|
|
11773
|
+
if (!DatabaseSync) return null;
|
|
10533
11774
|
let db = null;
|
|
10534
11775
|
try {
|
|
10535
11776
|
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
11777
|
+
const columns = new Set(
|
|
11778
|
+
db.prepare("PRAGMA table_info(threads)").all().map((row) => row && typeof row === "object" ? row.name : null).filter((name) => typeof name === "string")
|
|
11779
|
+
);
|
|
11780
|
+
if (!columns.has("id") || !columns.has("updated_at")) return null;
|
|
10536
11781
|
const hasLimit = typeof limit === "number" && Number.isFinite(limit) && limit > 0;
|
|
11782
|
+
const selectedColumns = [
|
|
11783
|
+
"id",
|
|
11784
|
+
"updated_at",
|
|
11785
|
+
...["updated_at_ms", "rollout_path", "title", "cwd", "source", "thread_source"].filter((column) => columns.has(column))
|
|
11786
|
+
];
|
|
11787
|
+
const whereClauses = columns.has("archived") ? ["archived = 0"] : ["1 = 1"];
|
|
11788
|
+
if (columns.has("source")) whereClauses.push("COALESCE(source, '') != 'exec'");
|
|
11789
|
+
if (columns.has("thread_source")) whereClauses.push("COALESCE(thread_source, '') != 'subagent'");
|
|
10537
11790
|
const sql = `
|
|
10538
|
-
SELECT
|
|
11791
|
+
SELECT ${selectedColumns.join(", ")}
|
|
10539
11792
|
FROM threads
|
|
10540
|
-
WHERE
|
|
10541
|
-
|
|
10542
|
-
ORDER BY updated_at DESC
|
|
11793
|
+
WHERE ${whereClauses.join("\n AND ")}
|
|
11794
|
+
ORDER BY ${columns.has("updated_at_ms") ? "updated_at_ms" : "updated_at"} DESC
|
|
10543
11795
|
${hasLimit ? "LIMIT ?" : ""}
|
|
10544
11796
|
`;
|
|
10545
11797
|
const rows = hasLimit ? db.prepare(sql).all(Math.max(1, Math.floor(limit))) : db.prepare(sql).all();
|
|
10546
11798
|
const ids = rows.map((row) => {
|
|
10547
|
-
|
|
11799
|
+
if (!row || typeof row !== "object") return null;
|
|
11800
|
+
const record = row;
|
|
11801
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
10548
11802
|
if (!id) return null;
|
|
11803
|
+
if (isSubagentThread(record.source, record.thread_source)) return null;
|
|
10549
11804
|
return {
|
|
10550
11805
|
id,
|
|
10551
|
-
updatedAtMs: parseUpdatedAtValue(
|
|
11806
|
+
updatedAtMs: parseUpdatedAtValue(record.updated_at_ms ?? record.updated_at),
|
|
11807
|
+
...typeof record.rollout_path === "string" && record.rollout_path.trim() ? { rolloutPath: record.rollout_path.trim() } : {},
|
|
11808
|
+
...typeof record.title === "string" && trimTitle(record.title) ? { title: trimTitle(record.title) } : {},
|
|
11809
|
+
...typeof record.cwd === "string" ? { cwd: record.cwd } : {},
|
|
11810
|
+
...typeof record.source === "string" ? { source: record.source } : {},
|
|
11811
|
+
...typeof record.thread_source === "string" ? { threadSource: record.thread_source } : {}
|
|
10552
11812
|
};
|
|
10553
11813
|
}).filter((row) => Boolean(row));
|
|
10554
11814
|
return ids.length > 0 ? ids : null;
|
|
@@ -10579,7 +11839,7 @@ function buildFallbackTitle(threadId, filePath, cwd) {
|
|
|
10579
11839
|
if (dirName) return dirName;
|
|
10580
11840
|
return `Session ${threadId.slice(0, 8)}`;
|
|
10581
11841
|
}
|
|
10582
|
-
function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
|
|
11842
|
+
function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds, visibleThread) {
|
|
10583
11843
|
const firstLine = readFirstLine(filePath);
|
|
10584
11844
|
if (!firstLine) return null;
|
|
10585
11845
|
let parsed;
|
|
@@ -10591,7 +11851,8 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
|
|
|
10591
11851
|
if (parsed.type !== "session_meta" || !parsed.payload?.id || !isDesktopLike(parsed.payload)) {
|
|
10592
11852
|
return null;
|
|
10593
11853
|
}
|
|
10594
|
-
|
|
11854
|
+
const threadId = visibleThread?.id || parsed.payload.id;
|
|
11855
|
+
if (archivedThreadIds.has(threadId)) {
|
|
10595
11856
|
return null;
|
|
10596
11857
|
}
|
|
10597
11858
|
let stat;
|
|
@@ -10600,20 +11861,19 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
|
|
|
10600
11861
|
} catch {
|
|
10601
11862
|
return null;
|
|
10602
11863
|
}
|
|
10603
|
-
const cwd = parsed.payload.cwd || "";
|
|
11864
|
+
const cwd = visibleThread?.cwd || parsed.payload.cwd || "";
|
|
10604
11865
|
if (isInternalSkillWorkspace(cwd)) {
|
|
10605
11866
|
return null;
|
|
10606
11867
|
}
|
|
10607
|
-
const lastEventAt = stat.mtime.toISOString();
|
|
11868
|
+
const lastEventAt = visibleThread?.updatedAtMs ? new Date(visibleThread.updatedAtMs).toISOString() : stat.mtime.toISOString();
|
|
10608
11869
|
const firstSeenAt = parsed.payload.timestamp || parsed.timestamp || stat.birthtime.toISOString();
|
|
10609
|
-
const
|
|
10610
|
-
const title = threadIndexEntries.get(threadId)?.title || buildFallbackTitle(threadId, filePath, cwd);
|
|
11870
|
+
const title = visibleThread?.title || threadIndexEntries.get(threadId)?.title || buildFallbackTitle(threadId, filePath, cwd);
|
|
10611
11871
|
return {
|
|
10612
11872
|
threadId,
|
|
10613
11873
|
filePath,
|
|
10614
11874
|
cwd,
|
|
10615
11875
|
originator: typeof parsed.payload.originator === "string" ? parsed.payload.originator : "Codex Desktop",
|
|
10616
|
-
source: typeof parsed.payload.source === "string" ? parsed.payload.source : void 0,
|
|
11876
|
+
source: visibleThread?.source || (typeof parsed.payload.source === "string" ? parsed.payload.source : void 0),
|
|
10617
11877
|
cliVersion: typeof parsed.payload.cli_version === "string" ? parsed.payload.cli_version : void 0,
|
|
10618
11878
|
firstSeenAt,
|
|
10619
11879
|
lastEventAt,
|
|
@@ -10826,6 +12086,7 @@ function isTurnContextLine(line) {
|
|
|
10826
12086
|
}
|
|
10827
12087
|
var IGNORED_EVENT_MSG_TYPES = /* @__PURE__ */ new Set([
|
|
10828
12088
|
"context_compacted",
|
|
12089
|
+
"sub_agent_activity",
|
|
10829
12090
|
"thread_settings_applied",
|
|
10830
12091
|
"thread_goal_updated",
|
|
10831
12092
|
"thread_name_updated",
|
|
@@ -10833,11 +12094,13 @@ var IGNORED_EVENT_MSG_TYPES = /* @__PURE__ */ new Set([
|
|
|
10833
12094
|
"token_count"
|
|
10834
12095
|
]);
|
|
10835
12096
|
var IGNORED_RESPONSE_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
12097
|
+
"agent_message",
|
|
10836
12098
|
"image_generation_call",
|
|
10837
12099
|
"web_search_call"
|
|
10838
12100
|
]);
|
|
10839
12101
|
var IGNORED_TOP_LEVEL_TYPES = /* @__PURE__ */ new Set([
|
|
10840
12102
|
"compacted",
|
|
12103
|
+
"inter_agent_communication_metadata",
|
|
10841
12104
|
"session_meta",
|
|
10842
12105
|
"turn_context",
|
|
10843
12106
|
"world_state"
|
|
@@ -10853,6 +12116,13 @@ function isTerminalCompletionEventType(value) {
|
|
|
10853
12116
|
function getEventTurnId(payload) {
|
|
10854
12117
|
return payload?.turn_id || payload?.turnId || "";
|
|
10855
12118
|
}
|
|
12119
|
+
function getResponseItemTurnId(payload) {
|
|
12120
|
+
const metadata = payload?.internal_chat_message_metadata_passthrough;
|
|
12121
|
+
return extractNormalizedFreeText(metadata?.turn_id ?? metadata?.turnId);
|
|
12122
|
+
}
|
|
12123
|
+
function getResponseItemClientId(payload) {
|
|
12124
|
+
return extractNormalizedFreeText(payload?.client_id);
|
|
12125
|
+
}
|
|
10856
12126
|
function extractTerminalCompletionText(payload) {
|
|
10857
12127
|
if (!payload) return "";
|
|
10858
12128
|
for (const value of [
|
|
@@ -10906,12 +12176,31 @@ function listDesktopSessions(limit) {
|
|
|
10906
12176
|
const visibleThreadIds = visibleThreads?.map((thread) => thread.id) || null;
|
|
10907
12177
|
const visibleThreadSet = visibleThreadIds ? new Set(visibleThreadIds) : null;
|
|
10908
12178
|
const visibleThreadUpdatedAt = new Map(visibleThreads?.map((thread) => [thread.id, thread.updatedAtMs]) || []);
|
|
12179
|
+
const visibleThreadRows = new Map(visibleThreads?.map((thread) => [thread.id, thread]) || []);
|
|
10909
12180
|
const oldestVisibleUpdatedAtMs = visibleThreads && visibleThreads.length > 0 ? Math.min(...visibleThreads.map((thread) => thread.updatedAtMs || Number.MAX_SAFE_INTEGER)) : 0;
|
|
10910
12181
|
const files = [];
|
|
10911
12182
|
walkSessionFiles(root, files);
|
|
10912
12183
|
const allSessions = /* @__PURE__ */ new Map();
|
|
12184
|
+
for (const thread of visibleThreads || []) {
|
|
12185
|
+
if (!thread.rolloutPath || !fs5.existsSync(thread.rolloutPath)) continue;
|
|
12186
|
+
const session = parseDesktopSession(
|
|
12187
|
+
thread.rolloutPath,
|
|
12188
|
+
threadIndexEntries,
|
|
12189
|
+
archivedThreadIds,
|
|
12190
|
+
thread
|
|
12191
|
+
);
|
|
12192
|
+
if (!session || !isWithinSavedWorkspaceRoots(session.cwd, savedWorkspaceRoots)) continue;
|
|
12193
|
+
allSessions.set(session.threadId, session);
|
|
12194
|
+
}
|
|
10913
12195
|
for (const filePath of files) {
|
|
10914
|
-
const
|
|
12196
|
+
const threadId = extractThreadIdFromRolloutName(path4.basename(filePath));
|
|
12197
|
+
if (threadId && allSessions.has(threadId)) continue;
|
|
12198
|
+
const session = parseDesktopSession(
|
|
12199
|
+
filePath,
|
|
12200
|
+
threadIndexEntries,
|
|
12201
|
+
archivedThreadIds,
|
|
12202
|
+
threadId ? visibleThreadRows.get(threadId) : void 0
|
|
12203
|
+
);
|
|
10915
12204
|
if (!session) continue;
|
|
10916
12205
|
if (!isWithinSavedWorkspaceRoots(session.cwd, savedWorkspaceRoots)) continue;
|
|
10917
12206
|
allSessions.set(session.threadId, session);
|
|
@@ -11210,6 +12499,8 @@ function pushDesktopMirrorEventRecord(records, parsed, rawLine, activeTurnId) {
|
|
|
11210
12499
|
function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId, activeSpecialCallIds) {
|
|
11211
12500
|
const signature = createDesktopEventSignature(rawLine);
|
|
11212
12501
|
const timestamp = parsed.timestamp || "";
|
|
12502
|
+
activeTurnId = getResponseItemTurnId(parsed.payload) || activeTurnId;
|
|
12503
|
+
const clientId = getResponseItemClientId(parsed.payload);
|
|
11213
12504
|
if (isIgnoredMirrorLineKind(parsed)) {
|
|
11214
12505
|
return true;
|
|
11215
12506
|
}
|
|
@@ -11221,7 +12512,8 @@ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId,
|
|
|
11221
12512
|
type: "reasoning",
|
|
11222
12513
|
content: text2,
|
|
11223
12514
|
timestamp,
|
|
11224
|
-
...activeTurnId ? { turnId: activeTurnId } : {}
|
|
12515
|
+
...activeTurnId ? { turnId: activeTurnId } : {},
|
|
12516
|
+
...clientId ? { clientId } : {}
|
|
11225
12517
|
});
|
|
11226
12518
|
return true;
|
|
11227
12519
|
}
|
|
@@ -11234,7 +12526,8 @@ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId,
|
|
|
11234
12526
|
role: parsed.payload.phase === "commentary" ? "commentary" : "assistant",
|
|
11235
12527
|
content: parsed.payload.phase === "commentary" ? text2.replace(/^\[commentary\]\n/, "") : text2,
|
|
11236
12528
|
timestamp,
|
|
11237
|
-
...activeTurnId ? { turnId: activeTurnId } : {}
|
|
12529
|
+
...activeTurnId ? { turnId: activeTurnId } : {},
|
|
12530
|
+
...clientId ? { clientId } : {}
|
|
11238
12531
|
});
|
|
11239
12532
|
return true;
|
|
11240
12533
|
}
|
|
@@ -11251,7 +12544,8 @@ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId,
|
|
|
11251
12544
|
role: "user",
|
|
11252
12545
|
content: text2,
|
|
11253
12546
|
timestamp,
|
|
11254
|
-
...activeTurnId ? { turnId: activeTurnId } : {}
|
|
12547
|
+
...activeTurnId ? { turnId: activeTurnId } : {},
|
|
12548
|
+
...clientId ? { clientId } : {}
|
|
11255
12549
|
});
|
|
11256
12550
|
return true;
|
|
11257
12551
|
}
|
|
@@ -11475,23 +12769,67 @@ function parseDesktopMirrorRecordText(content, leadingText = "", flushTrailingTe
|
|
|
11475
12769
|
unknownKinds: Array.from(unknownKinds)
|
|
11476
12770
|
};
|
|
11477
12771
|
}
|
|
11478
|
-
function
|
|
11479
|
-
|
|
12772
|
+
function mapDesktopEventsToMessages(events) {
|
|
12773
|
+
return events.map((event) => ({
|
|
11480
12774
|
role: event.role === "commentary" ? "assistant" : event.role,
|
|
11481
12775
|
content: event.role === "commentary" ? `[commentary]
|
|
11482
12776
|
${event.content}` : event.content
|
|
11483
12777
|
}));
|
|
11484
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8;
|
|
11485
|
-
return messages.slice(-safeLimit);
|
|
11486
12778
|
}
|
|
11487
|
-
function
|
|
11488
|
-
|
|
12779
|
+
function readUtf8FileTail(filePath, maxBytes) {
|
|
12780
|
+
const stat = fs5.statSync(filePath);
|
|
12781
|
+
const bytesToRead = Math.min(stat.size, Math.max(1, maxBytes));
|
|
12782
|
+
const startOffset = Math.max(0, stat.size - bytesToRead);
|
|
12783
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
12784
|
+
const fd = fs5.openSync(filePath, "r");
|
|
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
|
-
|
|
12817
|
+
while (bytesToRead <= MAX_HISTORY_TAIL_BYTES) {
|
|
12818
|
+
const tail = readUtf8FileTail(filePath, bytesToRead);
|
|
12819
|
+
events = parseDesktopSessionEventText(tail.content, "", true).events;
|
|
12820
|
+
if (tail.reachedStart || events.length >= targetEventCount) break;
|
|
12821
|
+
if (bytesToRead === MAX_HISTORY_TAIL_BYTES) break;
|
|
12822
|
+
bytesToRead = Math.min(MAX_HISTORY_TAIL_BYTES, bytesToRead * 2);
|
|
12823
|
+
}
|
|
11491
12824
|
} catch {
|
|
11492
12825
|
return [];
|
|
11493
12826
|
}
|
|
11494
|
-
return
|
|
12827
|
+
return mapDesktopEventsToMessages(events).slice(-safeLimit);
|
|
12828
|
+
}
|
|
12829
|
+
function readDesktopSessionMessages(threadId, limit = 8) {
|
|
12830
|
+
const session = getDesktopSessionByThreadId(threadId);
|
|
12831
|
+
if (!session) return [];
|
|
12832
|
+
return readDesktopSessionMessagesByFilePath(session.filePath, limit);
|
|
11495
12833
|
}
|
|
11496
12834
|
function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, endOffset, trailingText = "", currentTurnId = null, currentSpecialCallIds = []) {
|
|
11497
12835
|
let range;
|
|
@@ -11523,11 +12861,6 @@ function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, en
|
|
|
11523
12861
|
unknownKinds: parsed.unknownKinds
|
|
11524
12862
|
};
|
|
11525
12863
|
}
|
|
11526
|
-
function readDesktopSessionEventStream(threadId) {
|
|
11527
|
-
const session = getDesktopSessionByThreadId(threadId);
|
|
11528
|
-
if (!session) return [];
|
|
11529
|
-
return readDesktopSessionEventStreamByFilePath(session.filePath);
|
|
11530
|
-
}
|
|
11531
12864
|
|
|
11532
12865
|
// src/lib/bridge/turns/turn-classifier.ts
|
|
11533
12866
|
function normalizeThreadId(value) {
|
|
@@ -13145,7 +14478,7 @@ function abortMirrorSuppression(store, sessionId, config2, suppressionId, nowMs
|
|
|
13145
14478
|
}
|
|
13146
14479
|
target.until = nowMs + config2.suppressionWindowMs;
|
|
13147
14480
|
}
|
|
13148
|
-
function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs = Date.now()) {
|
|
14481
|
+
function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs = Date.now(), onTurnAssociated) {
|
|
13149
14482
|
const suppressions = getMirrorSuppressionStates(store, sessionId, nowMs);
|
|
13150
14483
|
const ignoredTurnIds = cleanupIgnoredMirrorTurns(store, sessionId, nowMs);
|
|
13151
14484
|
if (suppressions.length === 0 && ignoredTurnIds.size === 0 || records.length === 0) return records;
|
|
@@ -13182,6 +14515,9 @@ function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs
|
|
|
13182
14515
|
suppression.awaitingPromptMatch = false;
|
|
13183
14516
|
suppression.droppingTurn = true;
|
|
13184
14517
|
suppression.activeTurnId = record.turnId || suppression.candidateTurnId || null;
|
|
14518
|
+
if (suppression.activeTurnId) {
|
|
14519
|
+
onTurnAssociated?.(sessionId, suppression.activeTurnId);
|
|
14520
|
+
}
|
|
13185
14521
|
handled = true;
|
|
13186
14522
|
break;
|
|
13187
14523
|
}
|
|
@@ -13241,6 +14577,29 @@ function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs
|
|
|
13241
14577
|
}
|
|
13242
14578
|
return filtered;
|
|
13243
14579
|
}
|
|
14580
|
+
function associateMirrorSuppressionTurns(store, sessionId, records, nowMs = Date.now(), onTurnAssociated) {
|
|
14581
|
+
const suppressions = getMirrorSuppressionStates(store, sessionId, nowMs);
|
|
14582
|
+
if (suppressions.length === 0 || records.length === 0) return;
|
|
14583
|
+
const suppression = suppressions[0];
|
|
14584
|
+
if (!suppression.awaitingPromptMatch) return;
|
|
14585
|
+
for (const record of records) {
|
|
14586
|
+
if (record.type === "task_started" && record.turnId) {
|
|
14587
|
+
suppression.candidateTurnId = record.turnId;
|
|
14588
|
+
continue;
|
|
14589
|
+
}
|
|
14590
|
+
if (record.type !== "message" || record.role !== "user") continue;
|
|
14591
|
+
if (isSyntheticDesktopUserContext(record.content || "")) continue;
|
|
14592
|
+
const normalizedContent = normalizeMirrorPromptText(record.content || "");
|
|
14593
|
+
if (!suppression.promptText || normalizedContent !== suppression.promptText) break;
|
|
14594
|
+
const turnId = record.turnId || suppression.candidateTurnId;
|
|
14595
|
+
if (!turnId) break;
|
|
14596
|
+
suppression.awaitingPromptMatch = false;
|
|
14597
|
+
suppression.droppingTurn = true;
|
|
14598
|
+
suppression.activeTurnId = turnId;
|
|
14599
|
+
onTurnAssociated?.(sessionId, turnId);
|
|
14600
|
+
break;
|
|
14601
|
+
}
|
|
14602
|
+
}
|
|
13244
14603
|
|
|
13245
14604
|
// src/lib/bridge/adapter-sync-plan.ts
|
|
13246
14605
|
function stableFingerprintValue(value) {
|
|
@@ -15030,6 +16389,8 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
|
|
|
15030
16389
|
prompt: promptText,
|
|
15031
16390
|
sessionId,
|
|
15032
16391
|
sdkSessionId: binding.sdkSessionId || void 0,
|
|
16392
|
+
sessionOrigin: session?.thread_origin === "desktop" ? "desktop" : "bridge",
|
|
16393
|
+
desktopThreadId: session?.desktop_thread_id || void 0,
|
|
15033
16394
|
model: effectiveModel,
|
|
15034
16395
|
forceModel: !binding.sdkSessionId && Boolean(effectiveModel),
|
|
15035
16396
|
sandboxMode,
|
|
@@ -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:
|
|
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
|
-
|
|
18386
|
-
|
|
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 =
|
|
19585
|
-
var STATUS_FILE =
|
|
19586
|
-
var PID_FILE =
|
|
19587
|
-
var runId =
|
|
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
|
-
|
|
19641
|
-
|
|
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 });
|