baa-conductor


baa-conductor / apps / codexd / src
im_wower  ·  2026-03-25

daemon.ts

   1import { spawn } from "node:child_process";
   2import { access } from "node:fs/promises";
   3
   4import {
   5  CodexAppServerClient,
   6  createCodexAppServerTextInput,
   7  createCodexAppServerWebSocketTransport,
   8  type CodexAppServerAdapter,
   9  type CodexAppServerApprovalPolicy,
  10  type CodexAppServerEvent,
  11  type CodexAppServerInitializeResult,
  12  type CodexAppServerSandboxMode,
  13  type CodexAppServerSandboxPolicy,
  14  type CodexAppServerThreadResumeParams,
  15  type CodexAppServerThreadSession,
  16  type CodexAppServerThreadStartParams,
  17  type CodexAppServerTurnId,
  18  type CodexAppServerTurnStartParams,
  19  type CodexAppServerTurnStartResult,
  20  type CodexAppServerTurnSteerParams,
  21  type CodexAppServerTurnSteerResult,
  22  type CodexAppServerUserInput,
  23  type JsonValue
  24} from "../../../packages/codex-app-server/src/index.js";
  25import {
  26  CODEX_EXEC_PURPOSES,
  27  CODEX_EXEC_SANDBOX_MODES,
  28  runCodexExec,
  29  type CodexExecPurpose,
  30  type CodexExecRunRequest,
  31  type CodexExecRunResponse,
  32  type CodexExecSandboxMode
  33} from "../../../packages/codex-exec/src/index.js";
  34import {
  35  createCodexdAppServerStdioTransport,
  36  type CodexdAppServerTransportCloseDiagnostic,
  37  type CodexdAppServerProcessLike
  38} from "./app-server-transport.js";
  39import type {
  40  CodexdEnvironment,
  41  CodexdManagedChildState,
  42  CodexdRecentEvent,
  43  CodexdResolvedConfig,
  44  CodexdRunRecord,
  45  CodexdSessionPurpose,
  46  CodexdSessionRecord,
  47  CodexdSmokeCheck,
  48  CodexdSmokeResult,
  49  CodexdStatusSnapshot
  50} from "./contracts.js";
  51import { CodexdStateStore, type CodexdStateStoreOptions } from "./state-store.js";
  52
  53export interface CodexdProcessOutput {
  54  on(event: "data", listener: (chunk: string | Uint8Array) => void): this;
  55  on(event: "end", listener: () => void): this;
  56  on(event: "error", listener: (error: Error) => void): this;
  57  setEncoding?(encoding: string): this;
  58}
  59
  60export interface CodexdProcessInput {
  61  end(chunk?: string | Uint8Array): unknown;
  62  write(chunk: string | Uint8Array): boolean;
  63}
  64
  65export interface CodexdChildProcessLike extends CodexdAppServerProcessLike {
  66  stdin?: CodexdProcessInput;
  67  stderr?: CodexdProcessOutput;
  68  stdout?: CodexdProcessOutput;
  69  kill(signal?: string): boolean;
  70  on(event: "error", listener: (error: Error) => void): this;
  71  on(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
  72  on(event: "spawn", listener: () => void): this;
  73  once(event: "error", listener: (error: Error) => void): this;
  74  once(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
  75  once(event: "spawn", listener: () => void): this;
  76}
  77
  78export interface CodexdSpawnOptions {
  79  cwd: string;
  80  env: CodexdEnvironment;
  81}
  82
  83export interface CodexdSpawner {
  84  spawn(command: string, args: readonly string[], options: CodexdSpawnOptions): CodexdChildProcessLike;
  85}
  86
  87export interface CodexdAppServerClientFactoryContext {
  88  child: CodexdChildProcessLike | null;
  89  config: CodexdResolvedConfig;
  90}
  91
  92export interface CodexdAppServerClientFactory {
  93  create(context: CodexdAppServerClientFactoryContext): Promise<CodexAppServerAdapter> | CodexAppServerAdapter;
  94}
  95
  96export interface CodexdCreateSessionInput {
  97  approvalPolicy?: CodexAppServerApprovalPolicy | null;
  98  baseInstructions?: string | null;
  99  config?: Record<string, JsonValue | undefined> | null;
 100  cwd?: string | null;
 101  developerInstructions?: string | null;
 102  ephemeral?: boolean | null;
 103  metadata?: Record<string, string>;
 104  model?: string | null;
 105  modelProvider?: string | null;
 106  personality?: string | null;
 107  purpose: CodexdSessionPurpose;
 108  sandbox?: CodexAppServerSandboxMode | null;
 109  serviceTier?: string | null;
 110  threadId?: string | null;
 111}
 112
 113export interface CodexdSessionInput {
 114  metadata?: Record<string, string>;
 115  purpose: CodexdSessionPurpose;
 116  threadId?: string | null;
 117}
 118
 119export interface CodexdTurnInput {
 120  approvalPolicy?: CodexAppServerApprovalPolicy | null;
 121  collaborationMode?: JsonValue | null;
 122  cwd?: string | null;
 123  effort?: string | null;
 124  expectedTurnId?: string | null;
 125  input: CodexAppServerUserInput[] | string;
 126  model?: string | null;
 127  outputSchema?: JsonValue | null;
 128  personality?: string | null;
 129  sandboxPolicy?: CodexAppServerSandboxPolicy | null;
 130  serviceTier?: string | null;
 131  sessionId: string;
 132  summary?: JsonValue | null;
 133}
 134
 135export interface CodexdTurnResponse {
 136  accepted: true;
 137  session: CodexdSessionRecord;
 138  turnId: string;
 139}
 140
 141export interface CodexdRunInput {
 142  additionalWritableDirectories?: string[];
 143  config?: string[];
 144  cwd?: string;
 145  env?: Record<string, string | undefined>;
 146  images?: string[];
 147  metadata?: Record<string, string>;
 148  model?: string;
 149  profile?: string;
 150  prompt: string;
 151  purpose?: string | null;
 152  sandbox?: string | null;
 153  sessionId?: string | null;
 154  skipGitRepoCheck?: boolean;
 155  timeoutMs?: number;
 156}
 157
 158export interface CodexdRuntimeEventSubscription {
 159  unsubscribe(): void;
 160}
 161
 162export type CodexdRuntimeEventListener = (event: CodexdRecentEvent) => void;
 163export type CodexdRunExecutor = (request: CodexExecRunRequest) => Promise<CodexExecRunResponse>;
 164
 165export interface CodexdDaemonOptions extends CodexdStateStoreOptions {
 166  appServerClientFactory?: CodexdAppServerClientFactory;
 167  env?: CodexdEnvironment;
 168  runExecutor?: CodexdRunExecutor;
 169  spawner?: CodexdSpawner;
 170}
 171
 172const MAX_CHILD_OUTPUT_PREVIEW = 160;
 173const STOP_TIMEOUT_MS = 1_000;
 174const DEFAULT_APP_SERVER_VERSION = "0.0.0";
 175
 176export class CodexdDaemon {
 177  private appServerClient: CodexAppServerAdapter | null = null;
 178  private appServerInitializeResult: CodexAppServerInitializeResult | null = null;
 179  private appServerInitializationPromise: Promise<CodexAppServerAdapter> | null = null;
 180  private child: CodexdChildProcessLike | null = null;
 181  private readonly env: CodexdEnvironment;
 182  private readonly eventListeners = new Set<CodexdRuntimeEventListener>();
 183  private readonly appServerClientFactory: CodexdAppServerClientFactory;
 184  private readonly runExecutor: CodexdRunExecutor;
 185  private readonly stateStore: CodexdStateStore;
 186  private readonly spawner: CodexdSpawner;
 187  private started = false;
 188
 189  constructor(
 190    private readonly config: CodexdResolvedConfig,
 191    options: CodexdDaemonOptions = {}
 192  ) {
 193    const forwardEvent = options.onEvent;
 194
 195    this.env = options.env ?? (typeof process !== "undefined" ? process.env : {});
 196    this.spawner = options.spawner ?? {
 197      spawn(command, args, spawnOptions) {
 198        return spawn(command, [...args], {
 199          cwd: spawnOptions.cwd,
 200          env: spawnOptions.env,
 201          stdio: ["pipe", "pipe", "pipe"]
 202        }) as unknown as CodexdChildProcessLike;
 203      }
 204    };
 205    this.appServerClientFactory = options.appServerClientFactory ?? {
 206      create: async (context) => this.createDefaultAppServerClient(context)
 207    };
 208    this.runExecutor = options.runExecutor ?? runCodexExec;
 209    this.stateStore = new CodexdStateStore(config, {
 210      ...options,
 211      onEvent: (event) => {
 212        forwardEvent?.(event);
 213        this.emitRuntimeEvent(event);
 214      }
 215    });
 216  }
 217
 218  async start(): Promise<CodexdStatusSnapshot> {
 219    await this.stateStore.initialize();
 220
 221    if (this.started) {
 222      return this.stateStore.getSnapshot();
 223    }
 224
 225    await this.stateStore.markDaemonStarted();
 226    await this.stateStore.recordEvent({
 227      level: "info",
 228      type: "daemon.started",
 229      message: `codexd started in ${this.config.server.mode} mode.`,
 230      detail: {
 231        endpoint: this.config.server.endpoint,
 232        localApiBase: this.config.service.localApiBase,
 233        strategy: this.config.server.childStrategy
 234      }
 235    });
 236
 237    if (this.config.server.childStrategy === "external") {
 238      await this.stateStore.updateChildState({
 239        status: "external",
 240        pid: null,
 241        startedAt: new Date().toISOString(),
 242        exitedAt: null,
 243        exitCode: null,
 244        signal: null,
 245        lastError: null
 246      });
 247      await this.stateStore.recordEvent({
 248        level: "info",
 249        type: "child.external",
 250        message: `codexd is pointing at external endpoint ${this.config.server.endpoint}.`
 251      });
 252      this.started = true;
 253      return this.stateStore.getSnapshot();
 254    }
 255
 256    await this.stateStore.updateChildState({
 257      status: "starting",
 258      pid: null,
 259      startedAt: null,
 260      exitedAt: null,
 261      exitCode: null,
 262      signal: null,
 263      lastError: null
 264    });
 265
 266    const child = this.spawner.spawn(
 267      this.config.server.childCommand,
 268      this.config.server.childArgs,
 269      {
 270        cwd: this.config.server.childCwd,
 271        env: {
 272          ...this.env,
 273          BAA_CODEXD_DAEMON_ID: this.stateStore.getSnapshot().identity.daemonId,
 274          BAA_CODEXD_SERVER_ENDPOINT: this.config.server.endpoint
 275        }
 276      }
 277    );
 278    this.child = child;
 279    this.attachChildListeners(child);
 280
 281    try {
 282      await waitForChildSpawn(child);
 283    } catch (error) {
 284      await this.stateStore.updateChildState({
 285        status: "failed",
 286        pid: null,
 287        exitedAt: new Date().toISOString(),
 288        lastError: formatErrorMessage(error)
 289      });
 290      await this.stateStore.recordEvent({
 291        level: "error",
 292        type: "child.spawn.failed",
 293        message: formatErrorMessage(error)
 294      });
 295      this.child = null;
 296      throw error;
 297    }
 298
 299    await this.stateStore.updateChildState({
 300      status: "running",
 301      pid: child.pid ?? null,
 302      startedAt: new Date().toISOString(),
 303      exitedAt: null,
 304      exitCode: null,
 305      signal: null,
 306      lastError: null
 307    });
 308    await this.stateStore.recordEvent({
 309      level: "info",
 310      type: "child.started",
 311      message: `Started Codex child process ${child.pid ?? "unknown"}.`,
 312      detail: {
 313        args: this.config.server.childArgs,
 314        command: this.config.server.childCommand
 315      }
 316    });
 317
 318    this.started = true;
 319    return this.stateStore.getSnapshot();
 320  }
 321
 322  async stop(): Promise<CodexdStatusSnapshot> {
 323    await this.stateStore.initialize();
 324    await this.closeAppServerClient();
 325
 326    if (this.child != null) {
 327      const child = this.child;
 328      this.child = null;
 329
 330      const exited = waitForChildExit(child, STOP_TIMEOUT_MS);
 331
 332      try {
 333        child.kill("SIGTERM");
 334      } catch (error) {
 335        await this.stateStore.recordEvent({
 336          level: "warn",
 337          type: "child.kill.failed",
 338          message: formatErrorMessage(error)
 339        });
 340      }
 341
 342      await exited;
 343    } else {
 344      const currentChildState = this.stateStore.getChildState();
 345
 346      if (currentChildState.status === "external") {
 347        await this.stateStore.updateChildState({
 348          status: "idle",
 349          pid: null,
 350          exitCode: null,
 351          signal: null,
 352          exitedAt: new Date().toISOString()
 353        });
 354      }
 355    }
 356
 357    await this.stateStore.markDaemonStopped();
 358    await this.stateStore.recordEvent({
 359      level: "info",
 360      type: "daemon.stopped",
 361      message: "codexd stopped."
 362    });
 363
 364    this.started = false;
 365    return this.stateStore.getSnapshot();
 366  }
 367
 368  getRun(runId: string): CodexdRunRecord | null {
 369    return this.stateStore.getRun(runId);
 370  }
 371
 372  getSession(sessionId: string): CodexdSessionRecord | null {
 373    return this.stateStore.getSession(sessionId);
 374  }
 375
 376  getStatusSnapshot(): CodexdStatusSnapshot {
 377    return this.stateStore.getSnapshot();
 378  }
 379
 380  listRuns(): CodexdRunRecord[] {
 381    return this.stateStore.listRuns();
 382  }
 383
 384  listSessions(): CodexdSessionRecord[] {
 385    return this.stateStore.listSessions();
 386  }
 387
 388  subscribe(listener: CodexdRuntimeEventListener): CodexdRuntimeEventSubscription {
 389    this.eventListeners.add(listener);
 390
 391    return {
 392      unsubscribe: () => {
 393        this.eventListeners.delete(listener);
 394      }
 395    };
 396  }
 397
 398  async createRun(input: CodexdRunInput): Promise<CodexdRunRecord> {
 399    await this.stateStore.initialize();
 400
 401    if (input.sessionId != null && this.stateStore.getSession(input.sessionId) == null) {
 402      throw new Error(`Unknown codexd session "${input.sessionId}".`);
 403    }
 404
 405    const now = new Date().toISOString();
 406    const run: CodexdRunRecord = {
 407      runId: createRunId(),
 408      sessionId: input.sessionId ?? null,
 409      status: "queued",
 410      adapter: "codex-exec",
 411      prompt: normalizeRequiredString(input.prompt, "prompt"),
 412      purpose: normalizeRunPurpose(input.purpose ?? "fallback-worker"),
 413      cwd: normalizeOptionalString(input.cwd),
 414      createdAt: now,
 415      updatedAt: now,
 416      startedAt: null,
 417      finishedAt: null,
 418      error: null,
 419      summary: null,
 420      metadata: normalizeStringRecord(input.metadata),
 421      result: null
 422    };
 423
 424    await this.stateStore.upsertRun(run);
 425    await this.stateStore.recordEvent({
 426      level: "info",
 427      type: "run.queued",
 428      message: `Queued run ${run.runId}.`,
 429      detail: {
 430        cwd: run.cwd,
 431        purpose: run.purpose,
 432        runId: run.runId,
 433        sessionId: run.sessionId
 434      }
 435    });
 436
 437    void this.executeRun(run, input);
 438    return run;
 439  }
 440
 441  async createSession(input: CodexdCreateSessionInput): Promise<CodexdSessionRecord> {
 442    await this.stateStore.initialize();
 443
 444    if (this.config.server.mode !== "app-server") {
 445      throw new Error("codexd sessions require app-server mode.");
 446    }
 447
 448    const client = await this.ensureAppServerClient();
 449    const sessionResult =
 450      input.threadId != null
 451        ? await client.threadResume(buildThreadResumeParams(input))
 452        : await client.threadStart(buildThreadStartParams(input));
 453    const session = buildSessionRecord(input, sessionResult, this.stateStore.getChildState().pid);
 454
 455    await this.stateStore.upsertSession(session);
 456    await this.stateStore.recordEvent({
 457      level: "info",
 458      type: input.threadId != null ? "session.resumed" : "session.created",
 459      message:
 460        input.threadId != null
 461          ? `Resumed session ${session.sessionId} for thread ${session.threadId}.`
 462          : `Created session ${session.sessionId} for thread ${session.threadId}.`,
 463      detail: {
 464        purpose: session.purpose,
 465        sessionId: session.sessionId,
 466        threadId: session.threadId
 467      }
 468    });
 469
 470    return session;
 471  }
 472
 473  async createTurn(input: CodexdTurnInput): Promise<CodexdTurnResponse> {
 474    await this.stateStore.initialize();
 475
 476    if (this.config.server.mode !== "app-server") {
 477      throw new Error("codexd turns require app-server mode.");
 478    }
 479
 480    const current = this.stateStore.getSession(input.sessionId);
 481
 482    if (current == null) {
 483      throw new Error(`Unknown codexd session "${input.sessionId}".`);
 484    }
 485
 486    if (current.threadId == null) {
 487      throw new Error(`Session "${input.sessionId}" does not have an app-server thread.`);
 488    }
 489
 490    if (current.status !== "active") {
 491      throw new Error(`Session "${input.sessionId}" is not active.`);
 492    }
 493
 494    const client = await this.ensureAppServerClient();
 495    const items = normalizeTurnInputItems(input.input);
 496    let turnId: CodexAppServerTurnId;
 497
 498    if (input.expectedTurnId != null) {
 499      const result = await client.turnSteer(
 500        buildTurnSteerParams(current.threadId, input.expectedTurnId, items)
 501      );
 502      turnId = extractTurnId(result);
 503    } else {
 504      const result = await client.turnStart(buildTurnStartParams(current.threadId, input, items));
 505      turnId = extractTurnId(result);
 506    }
 507
 508    const updatedSession = this.stateStore.getSession(input.sessionId) ?? current;
 509    await this.stateStore.recordEvent({
 510      level: "info",
 511      type: "turn.accepted",
 512      message: `Accepted turn ${turnId} for session ${current.sessionId}.`,
 513      detail: {
 514        sessionId: current.sessionId,
 515        threadId: current.threadId,
 516        turnId
 517      }
 518    });
 519
 520    return {
 521      accepted: true,
 522      session: updatedSession,
 523      turnId
 524    };
 525  }
 526
 527  async registerSession(input: CodexdSessionInput): Promise<CodexdSessionRecord> {
 528    await this.stateStore.initialize();
 529    const now = new Date().toISOString();
 530    const session: CodexdSessionRecord = {
 531      sessionId: createSessionId(),
 532      purpose: input.purpose,
 533      threadId: input.threadId ?? null,
 534      status: "active",
 535      endpoint: this.config.server.endpoint,
 536      childPid: this.stateStore.getChildState().pid,
 537      createdAt: now,
 538      updatedAt: now,
 539      cwd: null,
 540      model: null,
 541      modelProvider: null,
 542      serviceTier: null,
 543      reasoningEffort: null,
 544      currentTurnId: null,
 545      lastTurnId: null,
 546      lastTurnStatus: null,
 547      metadata: normalizeStringRecord(input.metadata)
 548    };
 549
 550    await this.stateStore.upsertSession(session);
 551    await this.stateStore.recordEvent({
 552      level: "info",
 553      type: "session.registered",
 554      message: `Registered ${input.purpose} session ${session.sessionId}.`,
 555      detail: {
 556        sessionId: session.sessionId,
 557        threadId: session.threadId
 558      }
 559    });
 560
 561    return session;
 562  }
 563
 564  async closeSession(sessionId: string): Promise<CodexdSessionRecord | null> {
 565    await this.stateStore.initialize();
 566    const session = await this.stateStore.closeSession(sessionId);
 567
 568    if (session != null) {
 569      await this.stateStore.recordEvent({
 570        level: "info",
 571        type: "session.closed",
 572        message: `Closed session ${sessionId}.`,
 573        detail: {
 574          sessionId
 575        }
 576      });
 577    }
 578
 579    return session;
 580  }
 581
 582  private emitRuntimeEvent(event: CodexdRecentEvent): void {
 583    for (const listener of this.eventListeners) {
 584      listener(event);
 585    }
 586  }
 587
 588  private async ensureAppServerClient(): Promise<CodexAppServerAdapter> {
 589    if (this.appServerClient != null && this.appServerInitializeResult != null) {
 590      return this.appServerClient;
 591    }
 592
 593    if (this.appServerInitializationPromise != null) {
 594      return await this.appServerInitializationPromise;
 595    }
 596
 597    this.appServerInitializationPromise = (async () => {
 598      const client = this.appServerClient
 599        ?? await this.appServerClientFactory.create({
 600          child: this.child,
 601          config: this.config
 602        });
 603
 604      if (this.appServerClient !== client) {
 605        this.appServerClient = client;
 606        client.events.subscribe((event) => {
 607          void this.handleAppServerEvent(event);
 608        });
 609      }
 610
 611      try {
 612        this.appServerInitializeResult = await client.initialize();
 613        await this.stateStore.recordEvent({
 614          level: "info",
 615          type: "app-server.connected",
 616          message: `Connected codexd to app-server ${this.config.server.endpoint}.`,
 617          detail: {
 618            endpoint: this.config.server.endpoint,
 619            platformFamily: this.appServerInitializeResult.platformFamily,
 620            platformOs: this.appServerInitializeResult.platformOs,
 621            userAgent: this.appServerInitializeResult.userAgent
 622          }
 623        });
 624      } catch (error) {
 625        this.appServerClient = null;
 626        this.appServerInitializeResult = null;
 627        await this.stateStore.recordEvent({
 628          level: "error",
 629          type: "app-server.connect.failed",
 630          message: formatErrorMessage(error),
 631          detail: {
 632            endpoint: this.config.server.endpoint
 633          }
 634        });
 635        throw error;
 636      }
 637
 638      return client;
 639    })();
 640
 641    try {
 642      return await this.appServerInitializationPromise;
 643    } finally {
 644      this.appServerInitializationPromise = null;
 645    }
 646  }
 647
 648  private async closeAppServerClient(): Promise<void> {
 649    this.appServerInitializeResult = null;
 650    this.appServerInitializationPromise = null;
 651
 652    const client = this.appServerClient;
 653    this.appServerClient = null;
 654
 655    if (client == null) {
 656      return;
 657    }
 658
 659    try {
 660      await client.close();
 661    } catch (error) {
 662      await this.stateStore.recordEvent({
 663        level: "warn",
 664        type: "app-server.close.failed",
 665        message: formatErrorMessage(error)
 666      });
 667    }
 668  }
 669
 670  private async executeRun(initialRun: CodexdRunRecord, input: CodexdRunInput): Promise<void> {
 671    const startedAt = new Date().toISOString();
 672    let run: CodexdRunRecord = {
 673      ...initialRun,
 674      status: "running",
 675      startedAt,
 676      updatedAt: startedAt
 677    };
 678
 679    await this.stateStore.upsertRun(run);
 680    await this.stateStore.recordEvent({
 681      level: "info",
 682      type: "run.started",
 683      message: `Started run ${run.runId}.`,
 684      detail: {
 685        runId: run.runId
 686      }
 687    });
 688
 689    try {
 690      const response = await this.runExecutor({
 691        additionalWritableDirectories: input.additionalWritableDirectories,
 692        config: input.config,
 693        cwd: input.cwd,
 694        env: input.env,
 695        images: input.images,
 696        model: input.model,
 697        profile: input.profile,
 698        prompt: run.prompt,
 699        purpose: normalizeRunPurpose(run.purpose),
 700        sandbox: normalizeRunSandbox(input.sandbox),
 701        skipGitRepoCheck: input.skipGitRepoCheck,
 702        timeoutMs: input.timeoutMs
 703      });
 704      const finishedAt = new Date().toISOString();
 705
 706      if (response.ok) {
 707        run = {
 708          ...run,
 709          status: "completed",
 710          finishedAt,
 711          updatedAt: finishedAt,
 712          summary: response.result.lastMessage ?? `exit_code:${String(response.result.exitCode)}`,
 713          result: toJsonRecord(response)
 714        };
 715        await this.stateStore.upsertRun(run);
 716        await this.stateStore.recordEvent({
 717          level: "info",
 718          type: "run.completed",
 719          message: `Completed run ${run.runId}.`,
 720          detail: {
 721            runId: run.runId
 722          }
 723        });
 724        return;
 725      }
 726
 727      run = {
 728        ...run,
 729        status: "failed",
 730        finishedAt,
 731        updatedAt: finishedAt,
 732        error: response.error.message,
 733        summary: response.error.message,
 734        result: toJsonRecord(response)
 735      };
 736      await this.stateStore.upsertRun(run);
 737      await this.stateStore.recordEvent({
 738        level: "error",
 739        type: "run.failed",
 740        message: `Run ${run.runId} failed: ${response.error.message}`,
 741        detail: {
 742          runId: run.runId
 743        }
 744      });
 745    } catch (error) {
 746      const finishedAt = new Date().toISOString();
 747      run = {
 748        ...run,
 749        status: "failed",
 750        finishedAt,
 751        updatedAt: finishedAt,
 752        error: formatErrorMessage(error),
 753        summary: formatErrorMessage(error),
 754        result: {
 755          error: formatErrorMessage(error)
 756        }
 757      };
 758      await this.stateStore.upsertRun(run);
 759      await this.stateStore.recordEvent({
 760        level: "error",
 761        type: "run.failed",
 762        message: `Run ${run.runId} failed: ${formatErrorMessage(error)}`,
 763        detail: {
 764          runId: run.runId
 765        }
 766      });
 767    }
 768  }
 769
 770  private async handleAppServerEvent(event: CodexAppServerEvent): Promise<void> {
 771    switch (event.type) {
 772      case "thread.status.changed":
 773        await this.patchSessionsByThreadId(event.threadId, (session) => ({
 774          ...session,
 775          updatedAt: new Date().toISOString()
 776        }));
 777        break;
 778
 779      case "turn.started":
 780        await this.patchSessionsByThreadId(event.threadId, (session) => ({
 781          ...session,
 782          currentTurnId: event.turn.id,
 783          lastTurnId: event.turn.id,
 784          lastTurnStatus: event.turn.status,
 785          updatedAt: new Date().toISOString()
 786        }));
 787        break;
 788
 789      case "turn.completed":
 790        await this.patchSessionsByThreadId(event.threadId, (session) => ({
 791          ...session,
 792          currentTurnId: session.currentTurnId === event.turn.id ? null : session.currentTurnId,
 793          lastTurnId: event.turn.id,
 794          lastTurnStatus: event.turn.status,
 795          updatedAt: new Date().toISOString()
 796        }));
 797        break;
 798
 799      case "turn.error":
 800        await this.patchSessionsByThreadId(event.threadId, (session) => ({
 801          ...session,
 802          currentTurnId: event.willRetry
 803            ? session.currentTurnId ?? event.turnId
 804            : session.currentTurnId === event.turnId
 805              ? null
 806              : session.currentTurnId,
 807          lastTurnId: event.turnId,
 808          lastTurnStatus: event.willRetry ? "inProgress" : "failed",
 809          updatedAt: new Date().toISOString()
 810        }));
 811        break;
 812
 813      default:
 814        break;
 815    }
 816
 817    await this.stateStore.recordEvent(mapAppServerEventToRecentEvent(event));
 818  }
 819
 820  private async handleAppServerTransportClosed(
 821    diagnostic: CodexdAppServerTransportCloseDiagnostic
 822  ): Promise<void> {
 823    this.appServerClient = null;
 824    this.appServerInitializeResult = null;
 825    this.appServerInitializationPromise = null;
 826
 827    await this.stateStore.recordEvent({
 828      level: "warn",
 829      type: "app-server.transport.closed",
 830      message: `App-server transport closed via ${diagnostic.source}: ${diagnostic.message}`,
 831      detail: {
 832        exitCode: diagnostic.exitCode ?? null,
 833        flushedTrailingMessage: diagnostic.flushedTrailingMessage,
 834        signal: diagnostic.signal ?? null,
 835        source: diagnostic.source,
 836        trailingMessageLength: diagnostic.trailingMessageLength
 837      }
 838    });
 839
 840    const childState = this.stateStore.getChildState();
 841    const interruptedSessions = this.stateStore
 842      .listSessions()
 843      .filter(
 844        (session) =>
 845          session.status === "active" && session.threadId != null && session.currentTurnId != null
 846      );
 847
 848    for (const session of interruptedSessions) {
 849      const turnId = session.currentTurnId ?? session.lastTurnId;
 850
 851      if (turnId == null) {
 852        continue;
 853      }
 854
 855      await this.stateStore.upsertSession({
 856        ...session,
 857        currentTurnId: null,
 858        lastTurnId: turnId,
 859        lastTurnStatus: "failed",
 860        updatedAt: new Date().toISOString()
 861      });
 862      await this.stateStore.recordEvent({
 863        level: "error",
 864        type: "app-server.turn.completed.missing",
 865        message: `App-server transport closed before turn ${turnId} completed.`,
 866        detail: {
 867          childExitCode: diagnostic.exitCode ?? childState.exitCode,
 868          childPid: childState.pid,
 869          childSignal: diagnostic.signal ?? childState.signal,
 870          childStatus: childState.status,
 871          failureClass: "transport_closed_before_turn_completed",
 872          flushedTrailingMessage: diagnostic.flushedTrailingMessage,
 873          sessionId: session.sessionId,
 874          threadId: session.threadId,
 875          trailingMessageLength: diagnostic.trailingMessageLength,
 876          transportCloseMessage: diagnostic.message,
 877          transportCloseSource: diagnostic.source,
 878          turnId,
 879          turnStatusAtClose: session.lastTurnStatus
 880        }
 881      });
 882    }
 883  }
 884
 885  private async patchSessionsByThreadId(
 886    threadId: string,
 887    update: (session: CodexdSessionRecord) => CodexdSessionRecord
 888  ): Promise<void> {
 889    const sessions = this.stateStore
 890      .listSessions()
 891      .filter((session) => session.threadId === threadId);
 892
 893    for (const session of sessions) {
 894      await this.stateStore.upsertSession(update(session));
 895    }
 896  }
 897
 898  private attachChildListeners(child: CodexdChildProcessLike): void {
 899    child.stdout?.on("data", (chunk) => {
 900      void this.handleChildOutput("stdout", chunk);
 901    });
 902    child.stderr?.on("data", (chunk) => {
 903      void this.handleChildOutput("stderr", chunk);
 904    });
 905    child.on("error", (error) => {
 906      void this.handleChildError(error);
 907    });
 908    child.on("exit", (code, signal) => {
 909      void this.handleChildExit(code, signal);
 910    });
 911  }
 912
 913  private async handleChildError(error: Error): Promise<void> {
 914    await this.stateStore.updateChildState({
 915      status: "failed",
 916      lastError: error.message
 917    });
 918    await this.stateStore.recordEvent({
 919      level: "error",
 920      type: "child.error",
 921      message: error.message
 922    });
 923  }
 924
 925  private async handleChildExit(code: number | null, signal: string | null): Promise<void> {
 926    const stoppedAt = new Date().toISOString();
 927    const status = code == null || code === 0 ? "stopped" : "failed";
 928
 929    await this.stateStore.updateChildState({
 930      status,
 931      pid: null,
 932      exitedAt: stoppedAt,
 933      exitCode: code,
 934      signal,
 935      lastError: status === "failed" ? `Child exited with code ${String(code)}.` : null
 936    });
 937    await this.stateStore.recordEvent({
 938      level: status === "failed" ? "error" : "info",
 939      type: "child.exited",
 940      message:
 941        status === "failed"
 942          ? `Codex child exited with code ${String(code)}.`
 943          : `Codex child exited${signal ? ` after ${signal}` : ""}.`,
 944      detail: {
 945        code,
 946        signal
 947      }
 948    });
 949  }
 950
 951  private async handleChildOutput(
 952    stream: "stderr" | "stdout",
 953    chunk: string | Uint8Array
 954  ): Promise<void> {
 955    const text = normalizeOutputChunk(chunk);
 956
 957    if (text === "") {
 958      return;
 959    }
 960
 961    await this.stateStore.appendChildOutput(stream, text);
 962
 963    if (this.config.server.mode === "app-server" && stream === "stdout") {
 964      return;
 965    }
 966
 967    await this.stateStore.recordEvent({
 968      level: stream === "stderr" ? "warn" : "info",
 969      type: `child.${stream}`,
 970      message: `${stream}: ${createOutputPreview(text)}`,
 971      detail: {
 972        bytes: text.length,
 973        stream
 974      }
 975    });
 976  }
 977
 978  private async createDefaultAppServerClient(
 979    context: CodexdAppServerClientFactoryContext
 980  ): Promise<CodexAppServerAdapter> {
 981    if (this.config.server.mode !== "app-server") {
 982      throw new Error("codexd app-server client requested while running in exec mode.");
 983    }
 984
 985    if (this.config.server.childStrategy === "spawn") {
 986      if (context.child == null) {
 987        throw new Error("codexd app-server child is not available yet.");
 988      }
 989
 990      return new CodexAppServerClient({
 991        clientInfo: {
 992          name: "baa-conductor-codexd",
 993          title: "codexd local service",
 994          version: this.config.version ?? DEFAULT_APP_SERVER_VERSION
 995        },
 996        transport: createCodexdAppServerStdioTransport({
 997          onCloseDiagnostic: (diagnostic) => {
 998            void this.handleAppServerTransportClosed(diagnostic);
 999          },
1000          process: context.child
1001        })
1002      });
1003    }
1004
1005    const wsUrl = resolveAppServerWebSocketUrl(this.config.server.endpoint);
1006
1007    return new CodexAppServerClient({
1008      clientInfo: {
1009        name: "baa-conductor-codexd",
1010        title: "codexd local service",
1011        version: this.config.version ?? DEFAULT_APP_SERVER_VERSION
1012      },
1013      transport: createCodexAppServerWebSocketTransport({
1014        url: wsUrl
1015      })
1016    });
1017  }
1018}
1019
1020export async function runCodexdSmoke(
1021  baseConfig: CodexdResolvedConfig,
1022  options: CodexdDaemonOptions = {}
1023): Promise<CodexdSmokeResult> {
1024  const smokeConfig: CodexdResolvedConfig = {
1025    ...baseConfig,
1026    server: {
1027      ...baseConfig.server,
1028      childStrategy: "spawn",
1029      childCommand: typeof process !== "undefined" ? process.execPath : "node",
1030      childArgs: ["-e", EMBEDDED_SMOKE_PROGRAM],
1031      endpoint: "stdio://embedded-smoke-child"
1032    }
1033  };
1034  const daemon = new CodexdDaemon(smokeConfig, options);
1035
1036  await daemon.start();
1037  const session = await daemon.registerSession({
1038    purpose: "smoke",
1039    metadata: {
1040      source: "embedded-smoke"
1041    }
1042  });
1043
1044  await sleep(Math.max(smokeConfig.smokeLifetimeMs, 75));
1045  await daemon.closeSession(session.sessionId);
1046  const snapshot = await daemon.stop();
1047  const checks: CodexdSmokeCheck[] = [
1048    await buildFileCheck("structured_event_log", smokeConfig.paths.structuredEventLogPath),
1049    await buildFileCheck("stdout_log", smokeConfig.paths.stdoutLogPath),
1050    await buildFileCheck("stderr_log", smokeConfig.paths.stderrLogPath),
1051    await buildFileCheck("daemon_state", smokeConfig.paths.daemonStatePath),
1052    {
1053      name: "recent_event_cache",
1054      status: snapshot.recentEvents.events.length > 0 ? "ok" : "failed",
1055      detail: `${snapshot.recentEvents.events.length} cached events`
1056    },
1057    {
1058      name: "session_registry",
1059      status: hasClosedSmokeSession(snapshot.recentEvents.events, snapshot.daemon.child, snapshot)
1060        ? "ok"
1061        : "failed",
1062      detail: `${snapshot.sessionRegistry.sessions.length} recorded sessions`
1063    }
1064  ];
1065
1066  return {
1067    checks,
1068    snapshot
1069  };
1070}
1071
1072async function buildFileCheck(name: string, path: string): Promise<CodexdSmokeCheck> {
1073  try {
1074    await access(path);
1075    return {
1076      name,
1077      status: "ok",
1078      detail: path
1079    };
1080  } catch {
1081    return {
1082      name,
1083      status: "failed",
1084      detail: `missing: ${path}`
1085    };
1086  }
1087}
1088
1089function buildSessionRecord(
1090  input: CodexdCreateSessionInput,
1091  session: CodexAppServerThreadSession,
1092  childPid: number | null
1093): CodexdSessionRecord {
1094  const now = new Date().toISOString();
1095  const lastTurn = session.thread.turns?.[session.thread.turns.length - 1] ?? null;
1096
1097  return {
1098    sessionId: createSessionId(),
1099    purpose: input.purpose,
1100    threadId: session.thread.id,
1101    status: "active",
1102    endpoint: session.cwd,
1103    childPid,
1104    createdAt: now,
1105    updatedAt: now,
1106    cwd: session.cwd,
1107    model: session.model,
1108    modelProvider: session.modelProvider,
1109    serviceTier: session.serviceTier,
1110    reasoningEffort: session.reasoningEffort,
1111    currentTurnId: null,
1112    lastTurnId: lastTurn?.id ?? null,
1113    lastTurnStatus: lastTurn?.status ?? null,
1114    metadata: normalizeStringRecord(input.metadata)
1115  };
1116}
1117
1118function buildThreadResumeParams(
1119  input: CodexdCreateSessionInput
1120): CodexAppServerThreadResumeParams {
1121  const threadId = normalizeOptionalString(input.threadId);
1122
1123  if (threadId == null) {
1124    throw new Error("threadId is required when resuming an app-server session.");
1125  }
1126
1127  return {
1128    threadId,
1129    approvalPolicy: input.approvalPolicy ?? null,
1130    baseInstructions: normalizeOptionalString(input.baseInstructions),
1131    config: input.config ?? null,
1132    cwd: normalizeOptionalString(input.cwd),
1133    developerInstructions: normalizeOptionalString(input.developerInstructions),
1134    model: normalizeOptionalString(input.model),
1135    modelProvider: normalizeOptionalString(input.modelProvider),
1136    personality: normalizeOptionalString(input.personality),
1137    sandbox: input.sandbox ?? null,
1138    serviceTier: normalizeOptionalString(input.serviceTier)
1139  };
1140}
1141
1142function buildThreadStartParams(
1143  input: CodexdCreateSessionInput
1144): CodexAppServerThreadStartParams {
1145  return {
1146    approvalPolicy: input.approvalPolicy ?? null,
1147    baseInstructions: normalizeOptionalString(input.baseInstructions),
1148    config: input.config ?? null,
1149    cwd: normalizeOptionalString(input.cwd),
1150    developerInstructions: normalizeOptionalString(input.developerInstructions),
1151    ephemeral: input.ephemeral ?? null,
1152    model: normalizeOptionalString(input.model),
1153    modelProvider: normalizeOptionalString(input.modelProvider),
1154    personality: normalizeOptionalString(input.personality),
1155    sandbox: input.sandbox ?? null,
1156    serviceTier: normalizeOptionalString(input.serviceTier)
1157  };
1158}
1159
1160function buildTurnStartParams(
1161  threadId: string,
1162  input: CodexdTurnInput,
1163  items: CodexAppServerUserInput[]
1164): CodexAppServerTurnStartParams {
1165  return {
1166    threadId,
1167    approvalPolicy: input.approvalPolicy ?? null,
1168    collaborationMode: input.collaborationMode ?? null,
1169    cwd: normalizeOptionalString(input.cwd),
1170    effort: normalizeOptionalString(input.effort),
1171    input: items,
1172    model: normalizeOptionalString(input.model),
1173    outputSchema: input.outputSchema ?? null,
1174    personality: normalizeOptionalString(input.personality),
1175    sandboxPolicy: input.sandboxPolicy ?? null,
1176    serviceTier: normalizeOptionalString(input.serviceTier),
1177    summary: input.summary ?? null
1178  };
1179}
1180
1181function buildTurnSteerParams(
1182  threadId: string,
1183  expectedTurnId: string,
1184  items: CodexAppServerUserInput[]
1185): CodexAppServerTurnSteerParams {
1186  return {
1187    expectedTurnId,
1188    input: items,
1189    threadId
1190  };
1191}
1192
1193function extractTurnId(result: CodexAppServerTurnStartResult | CodexAppServerTurnSteerResult): string {
1194  if ("turn" in result) {
1195    return result.turn.id;
1196  }
1197
1198  return result.turnId;
1199}
1200
1201function hasClosedSmokeSession(
1202  events: readonly CodexdRecentEvent[],
1203  childState: CodexdManagedChildState,
1204  snapshot: CodexdStatusSnapshot
1205): boolean {
1206  return (
1207    snapshot.sessionRegistry.sessions.some(
1208      (session) => session.purpose === "smoke" && session.status === "closed"
1209    ) &&
1210    (events.length > 0 || childState.exitedAt != null)
1211  );
1212}
1213
1214function mapAppServerEventToRecentEvent(event: CodexAppServerEvent): {
1215  detail?: Record<string, unknown> | null;
1216  level: "error" | "info" | "warn";
1217  message: string;
1218  type: string;
1219} {
1220  switch (event.type) {
1221    case "thread.started":
1222      return {
1223        level: "info",
1224        type: "app-server.thread.started",
1225        message: `App-server thread ${event.thread.id} started.`,
1226        detail: {
1227          threadId: event.thread.id
1228        }
1229      };
1230
1231    case "thread.status.changed":
1232      return {
1233        level: "info",
1234        type: "app-server.thread.status.changed",
1235        message: `App-server thread ${event.threadId} changed status.`,
1236        detail: {
1237          status: event.status,
1238          threadId: event.threadId
1239        }
1240      };
1241
1242    case "turn.started":
1243      return {
1244        level: "info",
1245        type: "app-server.turn.started",
1246        message: `App-server turn ${event.turn.id} started.`,
1247        detail: {
1248          threadId: event.threadId,
1249          turnId: event.turn.id
1250        }
1251      };
1252
1253    case "turn.completed":
1254      return {
1255        level: "info",
1256        type: "app-server.turn.completed",
1257        message: `App-server turn ${event.turn.id} completed.`,
1258        detail: {
1259          status: event.turn.status,
1260          threadId: event.threadId,
1261          turnId: event.turn.id
1262        }
1263      };
1264
1265    case "turn.diff.updated":
1266      return {
1267        level: "info",
1268        type: "app-server.turn.diff.updated",
1269        message: `App-server diff updated for turn ${event.turnId}.`,
1270        detail: {
1271          diff: event.diff,
1272          threadId: event.threadId,
1273          turnId: event.turnId
1274        }
1275      };
1276
1277    case "turn.plan.updated":
1278      return {
1279        level: "info",
1280        type: "app-server.turn.plan.updated",
1281        message: `App-server plan updated for turn ${event.turnId}.`,
1282        detail: {
1283          explanation: event.explanation,
1284          plan: event.plan,
1285          threadId: event.threadId,
1286          turnId: event.turnId
1287        }
1288      };
1289
1290    case "turn.message.delta":
1291      return {
1292        level: "info",
1293        type: "app-server.turn.message.delta",
1294        message: `App-server emitted delta for turn ${event.turnId}: ${createOutputPreview(event.delta)}`,
1295        detail: {
1296          delta: event.delta,
1297          itemId: event.itemId,
1298          threadId: event.threadId,
1299          turnId: event.turnId
1300        }
1301      };
1302
1303    case "turn.plan.delta":
1304      return {
1305        level: "info",
1306        type: "app-server.turn.plan.delta",
1307        message: `App-server emitted plan delta for turn ${event.turnId}.`,
1308        detail: {
1309          delta: event.delta,
1310          itemId: event.itemId,
1311          threadId: event.threadId,
1312          turnId: event.turnId
1313        }
1314      };
1315
1316    case "turn.error":
1317      return {
1318        level: "error",
1319        type: "app-server.turn.error",
1320        message: `App-server turn ${event.turnId} failed: ${event.error.message}`,
1321        detail: {
1322          error: event.error,
1323          threadId: event.threadId,
1324          turnId: event.turnId,
1325          willRetry: event.willRetry
1326        }
1327      };
1328
1329    case "command.output.delta":
1330      return {
1331        level: event.stream === "stderr" ? "warn" : "info",
1332        type: "app-server.command.output.delta",
1333        message: `App-server command ${event.processId} emitted ${event.stream} output.`,
1334        detail: {
1335          capReached: event.capReached,
1336          deltaBase64: event.deltaBase64,
1337          processId: event.processId,
1338          stream: event.stream
1339        }
1340      };
1341
1342    case "notification":
1343      return {
1344        level: "info",
1345        type: "app-server.notification",
1346        message: `App-server notification ${event.notificationMethod}.`,
1347        detail: {
1348          notificationMethod: event.notificationMethod,
1349          params: event.params
1350        }
1351      };
1352  }
1353}
1354
1355function createOutputPreview(text: string): string {
1356  const flattened = text.replace(/\s+/gu, " ").trim();
1357  return flattened.length <= MAX_CHILD_OUTPUT_PREVIEW
1358    ? flattened
1359    : `${flattened.slice(0, MAX_CHILD_OUTPUT_PREVIEW - 3)}...`;
1360}
1361
1362function createRunId(): string {
1363  return `run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1364}
1365
1366function createSessionId(): string {
1367  return `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1368}
1369
1370function formatErrorMessage(error: unknown): string {
1371  if (error instanceof Error) {
1372    return error.message;
1373  }
1374
1375  return String(error);
1376}
1377
1378function normalizeOptionalString(value: string | null | undefined): string | null {
1379  if (value == null) {
1380    return null;
1381  }
1382
1383  const normalized = value.trim();
1384  return normalized === "" ? null : normalized;
1385}
1386
1387function normalizeOutputChunk(chunk: string | Uint8Array): string {
1388  return typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
1389}
1390
1391function normalizeRequiredString(value: string, field: string): string {
1392  const normalized = value.trim();
1393
1394  if (normalized === "") {
1395    throw new Error(`${field} must be a non-empty string.`);
1396  }
1397
1398  return normalized;
1399}
1400
1401function normalizeRunPurpose(value: string | null | undefined): CodexExecPurpose {
1402  const normalized = normalizeOptionalString(value);
1403
1404  if (normalized == null) {
1405    return "fallback-worker";
1406  }
1407
1408  if (CODEX_EXEC_PURPOSES.includes(normalized as CodexExecPurpose)) {
1409    return normalized as CodexExecPurpose;
1410  }
1411
1412  throw new Error(`Unsupported codexd run purpose "${value}".`);
1413}
1414
1415function normalizeRunSandbox(value: string | null | undefined): CodexExecSandboxMode | undefined {
1416  const normalized = normalizeOptionalString(value);
1417
1418  if (normalized == null) {
1419    return undefined;
1420  }
1421
1422  if (CODEX_EXEC_SANDBOX_MODES.includes(normalized as CodexExecSandboxMode)) {
1423    return normalized as CodexExecSandboxMode;
1424  }
1425
1426  throw new Error(`Unsupported codexd sandbox mode "${value}".`);
1427}
1428
1429function normalizeStringRecord(input: Record<string, string> | undefined): Record<string, string> {
1430  if (input == null) {
1431    return {};
1432  }
1433
1434  const normalized: Record<string, string> = {};
1435
1436  for (const [key, value] of Object.entries(input)) {
1437    const normalizedKey = normalizeOptionalString(key);
1438    const normalizedValue = normalizeOptionalString(value);
1439
1440    if (normalizedKey != null && normalizedValue != null) {
1441      normalized[normalizedKey] = normalizedValue;
1442    }
1443  }
1444
1445  return normalized;
1446}
1447
1448function normalizeTurnInputItems(input: CodexAppServerUserInput[] | string): CodexAppServerUserInput[] {
1449  if (typeof input === "string") {
1450    const prompt = normalizeRequiredString(input, "input");
1451    return [createCodexAppServerTextInput(prompt)];
1452  }
1453
1454  if (!Array.isArray(input) || input.length === 0) {
1455    throw new Error("turn input must be a non-empty string or non-empty item array.");
1456  }
1457
1458  return input;
1459}
1460
1461function resolveAppServerWebSocketUrl(endpoint: string): string {
1462  if (endpoint.startsWith("ws://") || endpoint.startsWith("wss://")) {
1463    return endpoint;
1464  }
1465
1466  if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
1467    const url = new URL(endpoint);
1468    url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1469    return url.toString();
1470  }
1471
1472  throw new Error(
1473    `Unsupported external app-server endpoint "${endpoint}". Use ws://, wss://, http://, or https://.`
1474  );
1475}
1476
1477function sleep(ms: number): Promise<void> {
1478  return new Promise((resolve) => {
1479    setTimeout(resolve, ms);
1480  });
1481}
1482
1483function toJsonRecord(value: unknown): Record<string, unknown> | null {
1484  if (value == null) {
1485    return null;
1486  }
1487
1488  return JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
1489}
1490
1491function waitForChildExit(child: CodexdChildProcessLike, timeoutMs: number): Promise<void> {
1492  return new Promise((resolve) => {
1493    let settled = false;
1494    const complete = () => {
1495      if (settled) {
1496        return;
1497      }
1498
1499      settled = true;
1500      resolve();
1501    };
1502
1503    child.once("exit", () => {
1504      complete();
1505    });
1506    setTimeout(() => {
1507      complete();
1508    }, timeoutMs);
1509  });
1510}
1511
1512function waitForChildSpawn(child: CodexdChildProcessLike): Promise<void> {
1513  return new Promise((resolve, reject) => {
1514    let settled = false;
1515    const finish = (callback: () => void) => {
1516      if (settled) {
1517        return;
1518      }
1519
1520      settled = true;
1521      callback();
1522    };
1523
1524    child.once("spawn", () => {
1525      finish(resolve);
1526    });
1527    child.once("error", (error) => {
1528      finish(() => reject(error));
1529    });
1530    child.once("exit", (code, signal) => {
1531      finish(() => reject(new Error(`Child exited before spawn completion (code=${String(code)}, signal=${String(signal)}).`)));
1532    });
1533  });
1534}
1535
1536const EMBEDDED_SMOKE_PROGRAM = [
1537  'process.stdout.write("codexd smoke ready\\n");',
1538  'process.stderr.write("codexd smoke stderr\\n");',
1539  'setTimeout(() => {',
1540  '  process.stdout.write("codexd smoke done\\n");',
1541  '  process.exit(0);',
1542  "}, 25);"
1543].join(" ");