codex@macbookpro
·
2026-04-03
index.ts
1import { mkdirSync } from "node:fs";
2import {
3 createServer,
4 type IncomingMessage,
5 type Server,
6 type ServerResponse
7} from "node:http";
8import type { AddressInfo } from "node:net";
9import { join, resolve } from "node:path";
10import {
11 ARTIFACTS_DIRNAME,
12 ARTIFACT_DB_FILENAME,
13 ArtifactStore,
14 DEFAULT_SUMMARY_LENGTH
15} from "../../../packages/artifact-db/dist/index.js";
16import {
17 DEFAULT_AUTOMATION_MODE,
18 DEFAULT_BAA_EXECUTION_JOURNAL_LIMIT,
19 type ControlPlaneRepository
20} from "../../../packages/db/dist/index.js";
21import {
22 createD1SyncWorker,
23 type D1SyncWorker
24} from "../../../packages/d1-client/dist/index.js";
25
26import {
27 type ConductorHttpRequest,
28 type ConductorHttpResponse
29} from "./http-types.js";
30import {
31 buildBrowserWebSocketUrl,
32 ConductorFirefoxWebSocketServer,
33 buildFirefoxWebSocketUrl
34} from "./firefox-ws.js";
35import { DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD } from "./artifacts/upload-session.js";
36import {
37 BrowserRequestPolicyController,
38 createArtifactStoreBrowserRequestPolicyPersistence,
39 type BrowserRequestPolicyControllerOptions
40} from "./browser-request-policy.js";
41import type { BrowserBridgeController } from "./browser-types.js";
42import type { FirefoxBridgeService } from "./firefox-bridge.js";
43import { BaaLiveInstructionIngest } from "./instructions/ingest.js";
44import { BaaInstructionCenter } from "./instructions/loop.js";
45import {
46 PersistentBaaInstructionDeduper,
47 PersistentBaaLiveInstructionMessageDeduper,
48 PersistentBaaLiveInstructionSnapshotStore
49} from "./instructions/store.js";
50import { handleConductorHttpRequest as handleConductorLocalHttpRequest } from "./local-api.js";
51import {
52 ConductorLocalControlPlane,
53 resolveDefaultConductorStateDir
54} from "./local-control-plane.js";
55import {
56 DEFAULT_UI_SESSION_TTL_SEC,
57 UiSessionManager
58} from "./ui-session.js";
59import { createRenewalDispatcherRunner } from "./renewal/dispatcher.js";
60import { createRenewalProjectorRunner } from "./renewal/projector.js";
61import { ConductorTimedJobs } from "./timed-jobs/index.js";
62
63export type { ConductorHttpRequest, ConductorHttpResponse } from "./http-types.js";
64export {
65 FirefoxBridgeError,
66 FirefoxBridgeService as ConductorFirefoxBridgeService,
67 type FirefoxApiRequestCommandInput,
68 type FirefoxBridgeApiResponse,
69 type FirefoxBridgeCommandTarget,
70 type FirefoxBridgeDispatchReceipt,
71 type FirefoxInjectMessageCommandInput,
72 type FirefoxOpenTabCommandInput,
73 type FirefoxReloadCommandInput,
74 type FirefoxSendMessageCommandInput,
75 type FirefoxRequestCredentialsCommandInput
76} from "./firefox-bridge.js";
77export {
78 BrowserRequestPolicyController,
79 createArtifactStoreBrowserRequestPolicyPersistence,
80 type BrowserRequestPolicyControllerOptions
81} from "./browser-request-policy.js";
82export { handleConductorHttpRequest } from "./local-api.js";
83export * from "./artifacts/index.js";
84export * from "./instructions/index.js";
85export * from "./renewal/automation.js";
86export * from "./renewal/dispatcher.js";
87export * from "./renewal/projector.js";
88export * from "./timed-jobs/index.js";
89
90export type ConductorRole = "primary" | "standby";
91export type ConductorLeadershipRole = "leader" | "standby";
92export type LeaseState = "leader" | "standby" | "degraded";
93export type SchedulerDecision = "scheduled" | "skipped_not_leader";
94export type TimerHandle = ReturnType<typeof globalThis.setInterval>;
95export type LeaderLeaseOperation = "acquire" | "renew";
96export type ConductorCliAction = "checklist" | "config" | "help" | "start";
97
98const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
99const DEFAULT_LEASE_RENEW_INTERVAL_MS = 5_000;
100const DEFAULT_LEASE_TTL_SEC = 30;
101const DEFAULT_RENEW_FAILURE_THRESHOLD = 2;
102const DEFAULT_CODE_ROOT_DIR = "/Users/george/code/";
103const DEFAULT_TIMED_JOBS_INTERVAL_MS = 10_000;
104const DEFAULT_TIMED_JOBS_MAX_MESSAGES_PER_TICK = 10;
105const DEFAULT_TIMED_JOBS_MAX_TASKS_PER_TICK = 10;
106const DEFAULT_TIMED_JOBS_SETTLE_DELAY_MS = 10_000;
107const DEFAULT_RESTART_RENEWAL_RECOVERY_DELAY_MS = 60_000;
108
109const STARTUP_CHECKLIST: StartupChecklistItem[] = [
110 { key: "register-controller", description: "注册 controller 并写入初始 heartbeat" },
111 { key: "acquire-lease", description: "尝试获取或续租 leader lease" },
112 { key: "load-runs", description: "扫描本地未完成 runs" },
113 { key: "reconcile-runs", description: "对账本地过期 runs 与控制平面状态" },
114 { key: "start-heartbeat-loop", description: "启动 controller heartbeat loop" },
115 { key: "start-scheduler", description: "仅 leader 进入 scheduler loop,standby 拒绝调度" }
116];
117
118export interface ControllerHeartbeatInput {
119 controllerId: string;
120 host: string;
121 role: string;
122 priority: number;
123 status: string;
124 version?: string | null;
125 heartbeatAt?: number;
126 startedAt?: number | null;
127}
128
129export interface LeaderLeaseRecord {
130 leaseName: string;
131 holderId: string;
132 holderHost: string;
133 term: number;
134 leaseExpiresAt: number;
135 renewedAt: number;
136 preferredHolderId: string | null;
137 metadataJson: string | null;
138}
139
140export interface LeaderLeaseAcquireInput {
141 controllerId: string;
142 host: string;
143 ttlSec: number;
144 preferred?: boolean;
145 metadataJson?: string | null;
146 now?: number;
147}
148
149export interface LeaderLeaseAcquireResult {
150 holderId: string;
151 holderHost: string;
152 term: number;
153 leaseExpiresAt: number;
154 renewedAt: number;
155 isLeader: boolean;
156 operation: LeaderLeaseOperation;
157 lease: LeaderLeaseRecord;
158}
159
160export interface ConductorConfig {
161 nodeId: string;
162 host: string;
163 role: ConductorRole;
164 publicApiBase?: string;
165 controlApiBase?: string;
166 priority?: number;
167 version?: string | null;
168 preferred?: boolean;
169 heartbeatIntervalMs?: number;
170 leaseRenewIntervalMs?: number;
171 leaseTtlSec?: number;
172 renewFailureThreshold?: number;
173 startedAt?: number;
174}
175
176export interface ConductorRuntimePaths {
177 logsDir: string | null;
178 runsDir: string | null;
179 stateDir: string | null;
180 tmpDir: string | null;
181 worktreesDir: string | null;
182}
183
184export interface ConductorRuntimeConfig extends ConductorConfig {
185 artifactInlineThreshold?: number | null;
186 artifactSummaryLength?: number | null;
187 claudeCodedLocalApiBase?: string | null;
188 codeRootDir?: string | null;
189 codexdLocalApiBase?: string | null;
190 localApiAllowedHosts?: readonly string[] | string | null;
191 localApiBase?: string | null;
192 paths?: Partial<ConductorRuntimePaths>;
193 sharedToken?: string | null;
194 timedJobsIntervalMs?: number;
195 timedJobsMaxMessagesPerTick?: number;
196 timedJobsMaxTasksPerTick?: number;
197 timedJobsSettleDelayMs?: number;
198 uiBrowserAdminPassword?: string | null;
199 uiReadonlyPassword?: string | null;
200 uiSessionTtlSec?: number | null;
201}
202
203export interface ResolvedConductorRuntimeConfig
204 extends Omit<ConductorConfig, "controlApiBase" | "publicApiBase"> {
205 artifactInlineThreshold: number;
206 artifactSummaryLength: number;
207 claudeCodedLocalApiBase: string | null;
208 codeRootDir: string;
209 controlApiBase: string;
210 heartbeatIntervalMs: number;
211 leaseRenewIntervalMs: number;
212 localApiAllowedHosts: string[];
213 leaseTtlSec: number;
214 codexdLocalApiBase: string | null;
215 localApiBase: string | null;
216 paths: ConductorRuntimePaths;
217 preferred: boolean;
218 priority: number;
219 publicApiBase: string;
220 renewFailureThreshold: number;
221 sharedToken: string | null;
222 timedJobsIntervalMs: number;
223 timedJobsMaxMessagesPerTick: number;
224 timedJobsMaxTasksPerTick: number;
225 timedJobsSettleDelayMs: number;
226 uiBrowserAdminPassword: string | null;
227 uiReadonlyPassword: string | null;
228 uiSessionTtlSec: number;
229 version: string | null;
230}
231
232export interface StartupChecklistItem {
233 key: string;
234 description: string;
235}
236
237export interface ConductorStatusSnapshot {
238 nodeId: string;
239 host: string;
240 role: ConductorRole;
241 leaseState: LeaseState;
242 schedulerEnabled: boolean;
243 currentLeaderId: string | null;
244 currentTerm: number | null;
245 leaseExpiresAt: number | null;
246 lastHeartbeatAt: number | null;
247 lastLeaseOperation: LeaderLeaseOperation | null;
248 nextLeaseOperation: LeaderLeaseOperation;
249 consecutiveRenewFailures: number;
250 lastError: string | null;
251}
252
253export interface ConductorRuntimeSnapshot {
254 claudeCoded: {
255 localApiBase: string | null;
256 };
257 daemon: ConductorStatusSnapshot;
258 identity: string;
259 loops: {
260 heartbeat: boolean;
261 lease: boolean;
262 };
263 paths: ConductorRuntimePaths;
264 controlApi: {
265 baseUrl: string;
266 browserWsUrl: string | null;
267 firefoxWsUrl: string | null;
268 localApiBase: string | null;
269 hasSharedToken: boolean;
270 usesPlaceholderToken: boolean;
271 };
272 codexd: {
273 localApiBase: string | null;
274 };
275 runtime: {
276 pid: number | null;
277 started: boolean;
278 startedAt: number;
279 };
280 startupChecklist: StartupChecklistItem[];
281 warnings: string[];
282}
283
284export interface SchedulerContext {
285 controllerId: string;
286 host: string;
287 term: number;
288}
289
290export interface ConductorControlApiClient {
291 acquireLeaderLease(input: LeaderLeaseAcquireInput): Promise<LeaderLeaseAcquireResult>;
292 sendControllerHeartbeat(input: ControllerHeartbeatInput): Promise<void>;
293}
294
295export interface ConductorControlApiClientOptions {
296 bearerToken?: string | null;
297 defaultHeaders?: Record<string, string>;
298 fetchImpl?: typeof fetch;
299}
300
301export interface ConductorDaemonHooks {
302 loadLocalRuns?: () => Promise<void>;
303 onLeaseStateChange?: (snapshot: ConductorStatusSnapshot) => Promise<void> | void;
304 reconcileLocalRuns?: () => Promise<void>;
305}
306
307export interface ConductorDaemonOptions {
308 autoStartLoops?: boolean;
309 clearIntervalImpl?: (handle: TimerHandle) => void;
310 client?: ConductorControlApiClient;
311 controlApiBearerToken?: string | null;
312 fetchImpl?: typeof fetch;
313 hooks?: ConductorDaemonHooks;
314 now?: () => number;
315 setIntervalImpl?: (handler: () => void, intervalMs: number) => TimerHandle;
316}
317
318export interface ConductorRuntimeOptions extends ConductorDaemonOptions {
319 browserRequestPolicyOptions?: BrowserRequestPolicyControllerOptions;
320 env?: ConductorEnvironment;
321}
322
323export type ConductorEnvironment = Record<string, string | undefined>;
324
325export interface ConductorTextWriter {
326 write(chunk: string): unknown;
327}
328
329type ConductorOutputWriter = ConductorTextWriter | typeof console;
330
331export interface ConductorProcessLike {
332 argv: string[];
333 env: ConductorEnvironment;
334 exitCode?: number;
335 off?(event: string, listener: () => void): unknown;
336 on?(event: string, listener: () => void): unknown;
337 pid?: number;
338}
339
340export type ConductorCliRequest =
341 | {
342 action: "help";
343 }
344 | {
345 action: "checklist";
346 printJson: boolean;
347 }
348 | {
349 action: "config";
350 config: ResolvedConductorRuntimeConfig;
351 printJson: boolean;
352 }
353 | {
354 action: "start";
355 config: ResolvedConductorRuntimeConfig;
356 printJson: boolean;
357 runOnce: boolean;
358 };
359
360export interface RunConductorCliOptions {
361 argv?: readonly string[];
362 env?: ConductorEnvironment;
363 fetchImpl?: typeof fetch;
364 processLike?: ConductorProcessLike;
365 stderr?: ConductorTextWriter;
366 stdout?: ConductorTextWriter;
367}
368
369interface ConductorRuntimeState {
370 consecutiveRenewFailures: number;
371 currentLeaderId: string | null;
372 currentTerm: number | null;
373 lastError: string | null;
374 lastHeartbeatAt: number | null;
375 lastLeaseOperation: LeaderLeaseOperation | null;
376 leaseExpiresAt: number | null;
377 leaseState: LeaseState;
378}
379
380interface ControlApiSuccessEnvelope<T> {
381 ok: true;
382 request_id: string;
383 data: T;
384}
385
386interface ControlApiErrorEnvelope {
387 ok: false;
388 request_id: string;
389 error: string;
390 message: string;
391 details?: unknown;
392}
393
394interface JsonRequestOptions {
395 headers?: Record<string, string>;
396}
397
398interface LocalApiListenConfig {
399 host: string;
400 port: number;
401}
402
403interface CliValueOverrides {
404 artifactInlineThreshold?: string;
405 artifactSummaryLength?: string;
406 claudeCodedLocalApiBase?: string;
407 codexdLocalApiBase?: string;
408 controlApiBase?: string;
409 heartbeatIntervalMs?: string;
410 host?: string;
411 leaseRenewIntervalMs?: string;
412 leaseTtlSec?: string;
413 localApiAllowedHosts?: string;
414 localApiBase?: string;
415 logsDir?: string;
416 nodeId?: string;
417 preferred?: boolean;
418 priority?: string;
419 publicApiBase?: string;
420 renewFailureThreshold?: string;
421 role?: string;
422 runOnce: boolean;
423 runsDir?: string;
424 sharedToken?: string;
425 stateDir?: string;
426 tmpDir?: string;
427 timedJobsIntervalMs?: string;
428 timedJobsMaxMessagesPerTick?: string;
429 timedJobsMaxTasksPerTick?: string;
430 timedJobsSettleDelayMs?: string;
431 version?: string;
432 worktreesDir?: string;
433}
434
435function getDefaultStartupChecklist(): StartupChecklistItem[] {
436 return STARTUP_CHECKLIST.map((item) => ({ ...item }));
437}
438
439function defaultNowUnixSeconds(): number {
440 return Math.floor(Date.now() / 1000);
441}
442
443function normalizeBaseUrl(baseUrl: string): string {
444 return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
445}
446
447function normalizeOptionalString(value: string | null | undefined): string | null {
448 if (value == null) {
449 return null;
450 }
451
452 const normalized = value.trim();
453 return normalized === "" ? null : normalized;
454}
455
456function normalizePath(value: string): string {
457 const baseUrl = "http://conductor.local";
458 const url = new URL(value || "/", baseUrl);
459 const normalized = url.pathname.replace(/\/+$/u, "");
460
461 return normalized === "" ? "/" : normalized;
462}
463
464function normalizeLoopbackHost(hostname: string): string {
465 const normalized = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
466 return normalized.toLowerCase();
467}
468
469function isLoopbackHost(hostname: string): boolean {
470 const normalized = normalizeLoopbackHost(hostname);
471 return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
472}
473
474function isTailscaleIpv4Host(hostname: string): boolean {
475 const normalized = normalizeLoopbackHost(hostname);
476
477 const octets = normalized.split(".");
478
479 if (octets.length !== 4) {
480 return false;
481 }
482
483 const [firstOctetText, secondOctetText, thirdOctetText, fourthOctetText] = octets;
484 const firstOctet = Number(firstOctetText);
485 const secondOctet = Number(secondOctetText);
486 const thirdOctet = Number(thirdOctetText);
487 const fourthOctet = Number(fourthOctetText);
488
489 if (
490 !Number.isInteger(firstOctet) ||
491 !Number.isInteger(secondOctet) ||
492 !Number.isInteger(thirdOctet) ||
493 !Number.isInteger(fourthOctet)
494 ) {
495 return false;
496 }
497
498 if (
499 firstOctet < 0 ||
500 firstOctet > 255 ||
501 secondOctet < 0 ||
502 secondOctet > 255 ||
503 thirdOctet < 0 ||
504 thirdOctet > 255 ||
505 fourthOctet < 0 ||
506 fourthOctet > 255
507 ) {
508 return false;
509 }
510
511 return firstOctet === 100 && secondOctet >= 64 && secondOctet <= 127;
512}
513
514function parseLocalApiAllowedHosts(hosts: readonly string[] | string | null | undefined): string[] {
515 if (hosts == null) {
516 return [];
517 }
518
519 const tokens = typeof hosts === "string" ? hosts.split(/[,\s]+/u) : hosts;
520 const normalizedHosts = new Set<string>();
521
522 for (const token of tokens) {
523 const normalized = normalizeOptionalString(token);
524
525 if (normalized == null) {
526 continue;
527 }
528
529 const host = normalizeLoopbackHost(normalized);
530
531 if (!isLoopbackHost(host) && !isTailscaleIpv4Host(host)) {
532 throw new Error(
533 "Conductor localApiAllowedHosts must contain only loopback hosts or Tailscale 100.x IPv4 addresses."
534 );
535 }
536
537 normalizedHosts.add(host);
538 }
539
540 return [...normalizedHosts];
541}
542
543function isAllowedLocalApiHost(hostname: string, allowedHosts: readonly string[]): boolean {
544 const normalized = normalizeLoopbackHost(hostname);
545
546 if (isLoopbackHost(normalized)) {
547 return true;
548 }
549
550 return allowedHosts.includes(normalized);
551}
552
553function readHttpPort(url: URL): number {
554 if (url.port === "") {
555 return 80;
556 }
557
558 const port = Number(url.port);
559
560 if (!Number.isInteger(port) || port < 0 || port > 65_535) {
561 throw new Error("Conductor localApiBase must use a valid TCP port.");
562 }
563
564 return port;
565}
566
567function formatLocalApiBaseUrl(hostname: string, port: number): string {
568 const normalizedHost = normalizeLoopbackHost(hostname);
569 const formattedHost = normalizedHost.includes(":") ? `[${normalizedHost}]` : normalizedHost;
570
571 return `http://${formattedHost}${port === 80 ? "" : `:${port}`}`;
572}
573
574function resolveLocalApiBase(
575 value: string | null | undefined,
576 allowedHosts: readonly string[] = []
577): string | null {
578 const normalized = normalizeOptionalString(value);
579
580 if (normalized == null) {
581 return null;
582 }
583
584 let url: URL;
585
586 try {
587 url = new URL(normalized);
588 } catch {
589 throw new Error("Conductor localApiBase must be a valid absolute http:// URL.");
590 }
591
592 if (url.protocol !== "http:") {
593 throw new Error("Conductor localApiBase must use the http:// scheme.");
594 }
595
596 if (!isAllowedLocalApiHost(url.hostname, allowedHosts)) {
597 throw new Error(
598 "Conductor localApiBase must use a loopback host or an explicitly allowed Tailscale 100.x host."
599 );
600 }
601
602 if (url.pathname !== "/" || url.search !== "" || url.hash !== "") {
603 throw new Error("Conductor localApiBase must not include a path, query, or hash.");
604 }
605
606 if (url.username !== "" || url.password !== "") {
607 throw new Error("Conductor localApiBase must not include credentials.");
608 }
609
610 return formatLocalApiBaseUrl(url.hostname, readHttpPort(url));
611}
612
613function resolveLocalApiListenConfig(localApiBase: string): LocalApiListenConfig {
614 const url = new URL(localApiBase);
615
616 return {
617 host: normalizeLoopbackHost(url.hostname),
618 port: readHttpPort(url)
619 };
620}
621
622async function awaitWritableDrainOrClose(
623 writableResponse: {
624 destroyed?: boolean;
625 off?(event: string, listener: () => void): unknown;
626 on?(event: string, listener: () => void): unknown;
627 }
628): Promise<boolean> {
629 if (writableResponse.destroyed === true) {
630 return false;
631 }
632
633 if (typeof writableResponse.on !== "function") {
634 return true;
635 }
636
637 return new Promise<boolean>((resolve) => {
638 const cleanup = () => {
639 writableResponse.off?.("drain", onDrain);
640 writableResponse.off?.("close", onClose);
641 writableResponse.off?.("error", onError);
642 };
643 const onDrain = () => {
644 cleanup();
645 resolve(true);
646 };
647 const onClose = () => {
648 cleanup();
649 resolve(false);
650 };
651 const onError = () => {
652 cleanup();
653 resolve(false);
654 };
655
656 writableResponse.on?.("drain", onDrain);
657 writableResponse.on?.("close", onClose);
658 writableResponse.on?.("error", onError);
659
660 if (writableResponse.destroyed === true) {
661 cleanup();
662 resolve(false);
663 }
664 });
665}
666
667export async function writeHttpResponse(
668 response: ServerResponse<IncomingMessage>,
669 payload: ConductorHttpResponse
670): Promise<void> {
671 const writableResponse = response as ServerResponse<IncomingMessage> & {
672 destroyed?: boolean;
673 off?(event: string, listener: () => void): unknown;
674 on?(event: string, listener: () => void): unknown;
675 writableEnded?: boolean;
676 write?(chunk: string | Uint8Array): boolean;
677 };
678 response.statusCode = payload.status;
679
680 for (const [name, value] of Object.entries(payload.headers)) {
681 response.setHeader(name, value);
682 }
683
684 if (payload.streamBody == null) {
685 response.end(payload.body);
686 return;
687 }
688
689 const hasInitialBody = typeof payload.body === "string" ? payload.body !== "" : payload.body.byteLength > 0;
690
691 if (hasInitialBody && typeof writableResponse.write === "function") {
692 if (!writableResponse.write(payload.body)) {
693 const canContinue = await awaitWritableDrainOrClose(writableResponse);
694
695 if (!canContinue) {
696 return;
697 }
698 }
699 }
700
701 for await (const chunk of payload.streamBody) {
702 if (writableResponse.destroyed === true) {
703 break;
704 }
705
706 if (typeof writableResponse.write !== "function") {
707 continue;
708 }
709
710 if (!writableResponse.write(chunk)) {
711 const canContinue = await awaitWritableDrainOrClose(writableResponse);
712
713 if (!canContinue) {
714 break;
715 }
716 }
717 }
718
719 if (writableResponse.destroyed !== true && writableResponse.writableEnded !== true) {
720 response.end();
721 }
722}
723
724async function readIncomingRequestBody(request: IncomingMessage): Promise<string | null> {
725 if (request.method == null || request.method.toUpperCase() === "GET") {
726 return null;
727 }
728
729 return new Promise((resolve, reject) => {
730 let body = "";
731 request.setEncoding?.("utf8");
732 request.on?.("data", (chunk) => {
733 body += typeof chunk === "string" ? chunk : String(chunk);
734 });
735 request.on?.("end", () => {
736 resolve(body === "" ? null : body);
737 });
738 request.on?.("error", (error) => {
739 reject(error);
740 });
741 });
742}
743
744function normalizeIncomingRequestHeaders(
745 headers: IncomingMessage["headers"]
746): Record<string, string | undefined> {
747 const normalized: Record<string, string | undefined> = {};
748
749 for (const [name, value] of Object.entries(headers)) {
750 if (typeof value === "string") {
751 normalized[name] = value;
752 continue;
753 }
754
755 if (Array.isArray(value)) {
756 normalized[name] = value.join(", ");
757 }
758 }
759
760 return normalized;
761}
762
763class ConductorLocalHttpServer {
764 private readonly artifactStore: ArtifactStore;
765 private readonly browserRequestPolicy: BrowserRequestPolicyController;
766 private readonly claudeCodedLocalApiBase: string | null;
767 private readonly codeRootDir: string;
768 private readonly codexdLocalApiBase: string | null;
769 private readonly fetchImpl: typeof fetch;
770 private readonly firefoxWebSocketServer: ConductorFirefoxWebSocketServer;
771 private readonly instructionIngest: BaaLiveInstructionIngest;
772 private readonly localApiBase: string;
773 private readonly now: () => number;
774 private readonly repository: ControlPlaneRepository;
775 private readonly sharedToken: string | null;
776 private readonly snapshotLoader: () => ConductorRuntimeSnapshot;
777 private readonly uiSessionManager: UiSessionManager;
778 private readonly version: string | null;
779 private resolvedBaseUrl: string;
780 private server: Server | null = null;
781
782 constructor(
783 localApiBase: string,
784 repository: ControlPlaneRepository,
785 artifactStore: ArtifactStore,
786 snapshotLoader: () => ConductorRuntimeSnapshot,
787 codeRootDir: string,
788 codexdLocalApiBase: string | null,
789 claudeCodedLocalApiBase: string | null,
790 fetchImpl: typeof fetch,
791 sharedToken: string | null,
792 version: string | null,
793 now: () => number,
794 uiBrowserAdminPassword: string | null,
795 uiReadonlyPassword: string | null,
796 uiSessionTtlSec: number,
797 artifactInlineThreshold: number,
798 artifactSummaryLength: number,
799 browserRequestPolicyOptions: BrowserRequestPolicyControllerOptions = {},
800 ingestLogDir: string | null = null,
801 pluginDiagnosticLogDir: string | null = null
802 ) {
803 this.artifactStore = artifactStore;
804 this.browserRequestPolicy = new BrowserRequestPolicyController({
805 ...browserRequestPolicyOptions,
806 persistence: browserRequestPolicyOptions.persistence
807 ?? createArtifactStoreBrowserRequestPolicyPersistence(this.artifactStore)
808 });
809 this.claudeCodedLocalApiBase = claudeCodedLocalApiBase;
810 this.codeRootDir = codeRootDir;
811 this.codexdLocalApiBase = codexdLocalApiBase;
812 this.fetchImpl = fetchImpl;
813 this.localApiBase = localApiBase;
814 this.now = now;
815 this.repository = repository;
816 this.sharedToken = sharedToken;
817 this.snapshotLoader = snapshotLoader;
818 this.uiSessionManager = new UiSessionManager({
819 browserAdminPassword: uiBrowserAdminPassword,
820 now: () => this.now() * 1000,
821 readonlyPassword: uiReadonlyPassword,
822 ttlSec: uiSessionTtlSec
823 });
824 this.version = version;
825 this.resolvedBaseUrl = localApiBase;
826 const nowMs = () => this.now() * 1000;
827 const localApiContext = {
828 artifactStore: this.artifactStore,
829 codeRootDir: this.codeRootDir,
830 fetchImpl: this.fetchImpl,
831 now: this.now,
832 repository: this.repository,
833 sharedToken: this.sharedToken,
834 snapshotLoader: this.snapshotLoader,
835 uiSessionManager: this.uiSessionManager,
836 version: this.version
837 };
838 const instructionIngest = new BaaLiveInstructionIngest({
839 center: new BaaInstructionCenter({
840 deduper: new PersistentBaaInstructionDeduper(this.repository, nowMs),
841 localApiContext
842 }),
843 historyLimit: DEFAULT_BAA_EXECUTION_JOURNAL_LIMIT,
844 localApiContext: {
845 ...localApiContext
846 },
847 messageDeduper: new PersistentBaaLiveInstructionMessageDeduper(this.repository, nowMs),
848 now: nowMs,
849 snapshotStore: new PersistentBaaLiveInstructionSnapshotStore(
850 this.repository,
851 DEFAULT_BAA_EXECUTION_JOURNAL_LIMIT
852 )
853 });
854 this.instructionIngest = instructionIngest;
855 this.firefoxWebSocketServer = new ConductorFirefoxWebSocketServer({
856 artifactStore: this.artifactStore,
857 artifactInlineThreshold,
858 artifactSummaryLength,
859 baseUrlLoader: () => this.resolvedBaseUrl,
860 ingestLogDir,
861 instructionIngest,
862 now: this.now,
863 pluginDiagnosticLogDir,
864 repository: this.repository,
865 snapshotLoader: this.snapshotLoader
866 });
867 }
868
869 getBaseUrl(): string {
870 return this.resolvedBaseUrl;
871 }
872
873 getFirefoxWebSocketUrl(): string | null {
874 return this.firefoxWebSocketServer.getFirefoxCompatUrl();
875 }
876
877 getBrowserWebSocketUrl(): string | null {
878 return this.firefoxWebSocketServer.getUrl();
879 }
880
881 getFirefoxBridgeService(): FirefoxBridgeService {
882 return this.firefoxWebSocketServer.getBridgeService();
883 }
884
885 async start(): Promise<string> {
886 if (this.server != null) {
887 return this.resolvedBaseUrl;
888 }
889
890 await this.browserRequestPolicy.initialize();
891 await this.instructionIngest.initialize();
892
893 const listenConfig = resolveLocalApiListenConfig(this.localApiBase);
894 const server = createServer((request, response) => {
895 void (async () => {
896 const requestAbortController = new AbortController();
897 const abortRequest = () => {
898 if (!requestAbortController.signal.aborted) {
899 requestAbortController.abort();
900 }
901 };
902 const requestWithExtendedEvents = request as IncomingMessage & {
903 on?(event: string, listener: () => void): unknown;
904 };
905 const responseWithExtendedEvents = response as ServerResponse<IncomingMessage> & {
906 on?(event: string, listener: () => void): unknown;
907 };
908 requestWithExtendedEvents.on?.("aborted", abortRequest);
909 responseWithExtendedEvents.on?.("close", abortRequest);
910
911 const payload = await handleConductorLocalHttpRequest(
912 {
913 body: await readIncomingRequestBody(request),
914 headers: normalizeIncomingRequestHeaders(request.headers),
915 method: request.method ?? "GET",
916 path: request.url ?? "/",
917 signal: requestAbortController.signal
918 },
919 {
920 artifactStore: this.artifactStore,
921 deliveryBridge: this.firefoxWebSocketServer.getDeliveryBridge(),
922 browserBridge:
923 this.firefoxWebSocketServer.getBridgeService() as unknown as BrowserBridgeController,
924 browserRequestPolicy: this.browserRequestPolicy,
925 browserStateLoader: () => this.firefoxWebSocketServer.getStateSnapshot(),
926 claudeCodedLocalApiBase: this.claudeCodedLocalApiBase,
927 codexdLocalApiBase: this.codexdLocalApiBase,
928 fetchImpl: this.fetchImpl,
929 repository: this.repository,
930 sharedToken: this.sharedToken,
931 snapshotLoader: this.snapshotLoader,
932 uiSessionManager: this.uiSessionManager,
933 version: this.version
934 }
935 );
936
937 await writeHttpResponse(response, payload);
938 })().catch((error: unknown) => {
939 response.statusCode = 500;
940 response.setHeader("cache-control", "no-store");
941 response.setHeader("content-type", "application/json; charset=utf-8");
942 response.end(
943 `${JSON.stringify(
944 {
945 ok: false,
946 error: "internal_error",
947 message: toErrorMessage(error)
948 },
949 null,
950 2
951 )}\n`
952 );
953 });
954 });
955 server.on("upgrade", (request, socket, head) => {
956 this.firefoxWebSocketServer.handleUpgrade(request, socket, head);
957 });
958
959 await new Promise<void>((resolve, reject) => {
960 const onError = (error: Error) => {
961 server.off("listening", onListening);
962 reject(error);
963 };
964 const onListening = () => {
965 server.off("error", onError);
966 resolve();
967 };
968
969 server.once("error", onError);
970 server.once("listening", onListening);
971 server.listen({
972 host: listenConfig.host,
973 port: listenConfig.port
974 });
975 });
976
977 const address = server.address();
978
979 if (address == null || typeof address === "string") {
980 server.close();
981 throw new Error("Conductor local API started without a TCP listen address.");
982 }
983
984 this.resolvedBaseUrl = formatLocalApiBaseUrl(address.address, (address as AddressInfo).port);
985 this.server = server;
986 this.firefoxWebSocketServer.start();
987 return this.resolvedBaseUrl;
988 }
989
990 async stop(): Promise<void> {
991 if (this.server == null) {
992 return;
993 }
994
995 const server = this.server;
996 this.server = null;
997 await this.firefoxWebSocketServer.stop();
998 await this.browserRequestPolicy.flush();
999
1000 await new Promise<void>((resolve, reject) => {
1001 server.close((error) => {
1002 if (error) {
1003 reject(error);
1004 return;
1005 }
1006
1007 resolve();
1008 });
1009 server.closeAllConnections?.();
1010 });
1011 }
1012}
1013
1014function toErrorMessage(error: unknown): string {
1015 if (error instanceof Error) {
1016 return error.message;
1017 }
1018
1019 return String(error);
1020}
1021
1022function isControlApiSuccessEnvelope<T>(value: unknown): value is ControlApiSuccessEnvelope<T> {
1023 if (value === null || typeof value !== "object") {
1024 return false;
1025 }
1026
1027 const record = value as Record<string, unknown>;
1028 return record.ok === true && "data" in record;
1029}
1030
1031function isControlApiErrorEnvelope(value: unknown): value is ControlApiErrorEnvelope {
1032 if (value === null || typeof value !== "object") {
1033 return false;
1034 }
1035
1036 const record = value as Record<string, unknown>;
1037 return record.ok === false && typeof record.error === "string" && typeof record.message === "string";
1038}
1039
1040function asRecord(value: unknown): Record<string, unknown> | null {
1041 if (value === null || typeof value !== "object" || Array.isArray(value)) {
1042 return null;
1043 }
1044
1045 return value as Record<string, unknown>;
1046}
1047
1048function compactRecord(input: Record<string, unknown>): Record<string, unknown> {
1049 const result: Record<string, unknown> = {};
1050
1051 for (const [key, value] of Object.entries(input)) {
1052 if (value !== undefined) {
1053 result[key] = value;
1054 }
1055 }
1056
1057 return result;
1058}
1059
1060function readFirstString(record: Record<string, unknown>, keys: readonly string[]): string | undefined {
1061 for (const key of keys) {
1062 const value = record[key];
1063
1064 if (typeof value === "string" && value.trim() !== "") {
1065 return value;
1066 }
1067 }
1068
1069 return undefined;
1070}
1071
1072function readFirstNullableString(record: Record<string, unknown>, keys: readonly string[]): string | null | undefined {
1073 for (const key of keys) {
1074 const value = record[key];
1075
1076 if (value === null) {
1077 return null;
1078 }
1079
1080 if (typeof value === "string") {
1081 return value;
1082 }
1083 }
1084
1085 return undefined;
1086}
1087
1088function readFirstNumber(record: Record<string, unknown>, keys: readonly string[]): number | undefined {
1089 for (const key of keys) {
1090 const value = record[key];
1091
1092 if (typeof value === "number" && Number.isFinite(value)) {
1093 return value;
1094 }
1095 }
1096
1097 return undefined;
1098}
1099
1100function readFirstBoolean(record: Record<string, unknown>, keys: readonly string[]): boolean | undefined {
1101 for (const key of keys) {
1102 const value = record[key];
1103
1104 if (typeof value === "boolean") {
1105 return value;
1106 }
1107 }
1108
1109 return undefined;
1110}
1111
1112function parseResponseJson(text: string, path: string): unknown {
1113 try {
1114 return JSON.parse(text) as unknown;
1115 } catch {
1116 throw new Error(`Control API returned invalid JSON for ${path}.`);
1117 }
1118}
1119
1120function buildControlApiError(
1121 path: string,
1122 status: number,
1123 statusText: string,
1124 parsedBody: unknown,
1125 rawBody: string
1126): Error {
1127 if (isControlApiErrorEnvelope(parsedBody)) {
1128 return new Error(`Control API ${status} ${path}: ${parsedBody.error}: ${parsedBody.message}`);
1129 }
1130
1131 if (rawBody !== "") {
1132 return new Error(`Control API ${status} ${path}: ${rawBody}`);
1133 }
1134
1135 return new Error(`Control API ${status} ${path}: ${statusText}`);
1136}
1137
1138async function postJson<T>(
1139 fetchImpl: typeof fetch,
1140 baseUrl: string,
1141 path: string,
1142 body: unknown,
1143 options: JsonRequestOptions = {}
1144): Promise<T> {
1145 const headers = new Headers(options.headers);
1146 headers.set("accept", "application/json");
1147 headers.set("content-type", "application/json");
1148
1149 const response = await fetchImpl(`${normalizeBaseUrl(baseUrl)}${path}`, {
1150 method: "POST",
1151 headers,
1152 body: JSON.stringify(body)
1153 });
1154 const text = await response.text();
1155 const parsedBody = text === "" ? undefined : parseResponseJson(text, path);
1156
1157 if (!response.ok) {
1158 throw buildControlApiError(path, response.status, response.statusText, parsedBody, text);
1159 }
1160
1161 if (parsedBody === undefined) {
1162 return undefined as T;
1163 }
1164
1165 if (isControlApiSuccessEnvelope<T>(parsedBody)) {
1166 return parsedBody.data;
1167 }
1168
1169 if (isControlApiErrorEnvelope(parsedBody)) {
1170 throw new Error(`Control API ${path}: ${parsedBody.error}: ${parsedBody.message}`);
1171 }
1172
1173 return parsedBody as T;
1174}
1175
1176function normalizeLeaseRecord(
1177 value: unknown,
1178 fallback: Pick<LeaderLeaseRecord, "holderHost" | "holderId" | "leaseExpiresAt" | "renewedAt" | "term">
1179): LeaderLeaseRecord {
1180 const record = asRecord(value) ?? {};
1181
1182 return {
1183 leaseName: readFirstString(record, ["leaseName", "lease_name"]) ?? "global",
1184 holderId: readFirstString(record, ["holderId", "holder_id"]) ?? fallback.holderId,
1185 holderHost: readFirstString(record, ["holderHost", "holder_host"]) ?? fallback.holderHost,
1186 term: readFirstNumber(record, ["term"]) ?? fallback.term,
1187 leaseExpiresAt:
1188 readFirstNumber(record, ["leaseExpiresAt", "lease_expires_at"]) ?? fallback.leaseExpiresAt,
1189 renewedAt: readFirstNumber(record, ["renewedAt", "renewed_at"]) ?? fallback.renewedAt,
1190 preferredHolderId:
1191 readFirstNullableString(record, ["preferredHolderId", "preferred_holder_id"]) ?? null,
1192 metadataJson: readFirstNullableString(record, ["metadataJson", "metadata_json"]) ?? null
1193 };
1194}
1195
1196function normalizeAcquireLeaderLeaseResult(
1197 value: unknown,
1198 input: LeaderLeaseAcquireInput
1199): LeaderLeaseAcquireResult {
1200 const record = asRecord(value);
1201
1202 if (!record) {
1203 throw new Error("Control API /v1/leader/acquire returned a non-object payload.");
1204 }
1205
1206 const holderId = readFirstString(record, ["holderId", "holder_id"]);
1207 const term = readFirstNumber(record, ["term"]);
1208 const leaseExpiresAt = readFirstNumber(record, ["leaseExpiresAt", "lease_expires_at"]);
1209
1210 if (!holderId || term == null || leaseExpiresAt == null) {
1211 throw new Error("Control API /v1/leader/acquire response is missing lease identity fields.");
1212 }
1213
1214 const holderHost =
1215 readFirstString(record, ["holderHost", "holder_host"]) ??
1216 (holderId === input.controllerId ? input.host : "unknown");
1217 const renewedAt =
1218 readFirstNumber(record, ["renewedAt", "renewed_at"]) ?? input.now ?? defaultNowUnixSeconds();
1219 const isLeader =
1220 readFirstBoolean(record, ["isLeader", "is_leader"]) ?? holderId === input.controllerId;
1221 const rawOperation = readFirstString(record, ["operation"]);
1222 const operation: LeaderLeaseOperation = rawOperation === "renew" ? "renew" : "acquire";
1223
1224 return {
1225 holderId,
1226 holderHost,
1227 term,
1228 leaseExpiresAt,
1229 renewedAt,
1230 isLeader,
1231 operation,
1232 lease: normalizeLeaseRecord(record.lease, {
1233 holderHost,
1234 holderId,
1235 leaseExpiresAt,
1236 renewedAt,
1237 term
1238 })
1239 };
1240}
1241
1242function resolveFetchClientOptions(
1243 fetchOrOptions: typeof fetch | ConductorControlApiClientOptions | undefined,
1244 maybeOptions: ConductorControlApiClientOptions
1245): { fetchImpl: typeof fetch; options: ConductorControlApiClientOptions } {
1246 if (typeof fetchOrOptions === "function") {
1247 return {
1248 fetchImpl: fetchOrOptions,
1249 options: maybeOptions
1250 };
1251 }
1252
1253 return {
1254 fetchImpl: fetchOrOptions?.fetchImpl ?? globalThis.fetch,
1255 options: fetchOrOptions ?? {}
1256 };
1257}
1258
1259export function createFetchControlApiClient(
1260 baseUrl: string,
1261 fetchOrOptions: typeof fetch | ConductorControlApiClientOptions = globalThis.fetch,
1262 maybeOptions: ConductorControlApiClientOptions = {}
1263): ConductorControlApiClient {
1264 const { fetchImpl, options } = resolveFetchClientOptions(fetchOrOptions, maybeOptions);
1265 const requestHeaders = { ...(options.defaultHeaders ?? {}) };
1266
1267 if (options.bearerToken) {
1268 requestHeaders.authorization = `Bearer ${options.bearerToken}`;
1269 }
1270
1271 return {
1272 async acquireLeaderLease(input: LeaderLeaseAcquireInput): Promise<LeaderLeaseAcquireResult> {
1273 const payload = compactRecord({
1274 controller_id: input.controllerId,
1275 host: input.host,
1276 ttl_sec: input.ttlSec,
1277 preferred: input.preferred ?? false,
1278 metadata_json: input.metadataJson ?? undefined,
1279 now: input.now
1280 });
1281 const response = await postJson<unknown>(
1282 fetchImpl,
1283 baseUrl,
1284 "/v1/leader/acquire",
1285 payload,
1286 {
1287 headers: requestHeaders
1288 }
1289 );
1290
1291 return normalizeAcquireLeaderLeaseResult(response, input);
1292 },
1293 async sendControllerHeartbeat(input: ControllerHeartbeatInput): Promise<void> {
1294 const payload = compactRecord({
1295 controller_id: input.controllerId,
1296 host: input.host,
1297 role: input.role,
1298 priority: input.priority,
1299 status: input.status,
1300 version: input.version ?? undefined,
1301 heartbeat_at: input.heartbeatAt,
1302 started_at: input.startedAt
1303 });
1304
1305 await postJson(fetchImpl, baseUrl, "/v1/controllers/heartbeat", payload, {
1306 headers: requestHeaders
1307 });
1308 }
1309 };
1310}
1311
1312export function createRepositoryControlApiClient(
1313 repository: Pick<ControlPlaneRepository, "acquireLeaderLease" | "heartbeatController">,
1314 now: () => number = defaultNowUnixSeconds
1315): ConductorControlApiClient {
1316 return {
1317 async acquireLeaderLease(input: LeaderLeaseAcquireInput): Promise<LeaderLeaseAcquireResult> {
1318 return repository.acquireLeaderLease(input);
1319 },
1320 async sendControllerHeartbeat(input: ControllerHeartbeatInput): Promise<void> {
1321 await repository.heartbeatController({
1322 ...input,
1323 heartbeatAt: input.heartbeatAt ?? now(),
1324 startedAt: input.startedAt ?? null,
1325 version: input.version ?? null
1326 });
1327 }
1328 };
1329}
1330
1331export class ConductorDaemon {
1332 private readonly autoStartLoops: boolean;
1333 private readonly clearIntervalImpl: (handle: TimerHandle) => void;
1334 private readonly client: ConductorControlApiClient;
1335 private readonly config: CanonicalConductorConfig;
1336 private heartbeatTimer: TimerHandle | null = null;
1337 private readonly hooks?: ConductorDaemonHooks;
1338 private leaseTimer: TimerHandle | null = null;
1339 private readonly now: () => number;
1340 private readonly setIntervalImpl: (handler: () => void, intervalMs: number) => TimerHandle;
1341 private readonly startedAt: number;
1342 private readonly state: ConductorRuntimeState = {
1343 consecutiveRenewFailures: 0,
1344 currentLeaderId: null,
1345 currentTerm: null,
1346 lastError: null,
1347 lastHeartbeatAt: null,
1348 lastLeaseOperation: null,
1349 leaseExpiresAt: null,
1350 leaseState: "standby"
1351 };
1352
1353 constructor(config: ConductorConfig, options: ConductorDaemonOptions = {}) {
1354 const publicApiBase = resolveConfiguredPublicApiBase(config);
1355
1356 if (!publicApiBase) {
1357 throw new Error("Conductor config requires a non-empty publicApiBase.");
1358 }
1359
1360 this.config = {
1361 ...config,
1362 controlApiBase: normalizeBaseUrl(publicApiBase),
1363 publicApiBase: normalizeBaseUrl(publicApiBase)
1364 };
1365 this.autoStartLoops = options.autoStartLoops ?? true;
1366 this.clearIntervalImpl =
1367 options.clearIntervalImpl ?? ((handle) => globalThis.clearInterval(handle));
1368 this.client =
1369 options.client ??
1370 createFetchControlApiClient(this.config.publicApiBase, {
1371 bearerToken: options.controlApiBearerToken,
1372 fetchImpl: options.fetchImpl
1373 });
1374 this.hooks = options.hooks;
1375 this.now = options.now ?? defaultNowUnixSeconds;
1376 this.setIntervalImpl =
1377 options.setIntervalImpl ?? ((handler, intervalMs) => globalThis.setInterval(handler, intervalMs));
1378 this.startedAt = this.config.startedAt ?? this.now();
1379 }
1380
1381 getStartupChecklist(): StartupChecklistItem[] {
1382 return getDefaultStartupChecklist();
1383 }
1384
1385 describeIdentity(): string {
1386 return `${this.config.nodeId}@${this.config.host}(${this.config.role})`;
1387 }
1388
1389 getLoopStatus(): { heartbeat: boolean; lease: boolean } {
1390 return {
1391 heartbeat: this.heartbeatTimer != null,
1392 lease: this.leaseTimer != null
1393 };
1394 }
1395
1396 getStatusSnapshot(now: number = this.now()): ConductorStatusSnapshot {
1397 return {
1398 nodeId: this.config.nodeId,
1399 host: this.config.host,
1400 role: this.config.role,
1401 leaseState: this.state.leaseState,
1402 schedulerEnabled: this.canSchedule(now),
1403 currentLeaderId: this.state.currentLeaderId,
1404 currentTerm: this.state.currentTerm,
1405 leaseExpiresAt: this.state.leaseExpiresAt,
1406 lastHeartbeatAt: this.state.lastHeartbeatAt,
1407 lastLeaseOperation: this.state.lastLeaseOperation,
1408 nextLeaseOperation: this.getNextLeaseOperation(now),
1409 consecutiveRenewFailures: this.state.consecutiveRenewFailures,
1410 lastError: this.state.lastError
1411 };
1412 }
1413
1414 getNextLeaseOperation(now: number = this.now()): LeaderLeaseOperation {
1415 return this.state.currentLeaderId === this.config.nodeId &&
1416 this.state.leaseExpiresAt != null &&
1417 this.state.leaseExpiresAt > now
1418 ? "renew"
1419 : "acquire";
1420 }
1421
1422 canSchedule(now: number = this.now()): boolean {
1423 return (
1424 this.state.leaseState === "leader" &&
1425 this.state.currentLeaderId === this.config.nodeId &&
1426 this.state.leaseExpiresAt != null &&
1427 this.state.leaseExpiresAt > now
1428 );
1429 }
1430
1431 async start(): Promise<LeaseState> {
1432 try {
1433 await this.sendHeartbeat();
1434 } catch (error) {
1435 this.state.lastError = toErrorMessage(error);
1436 await this.transitionTo("degraded");
1437 }
1438
1439 try {
1440 await this.runLeaseCycle();
1441 } catch {
1442 // Keep startup non-throwing so the process can surface degraded state.
1443 }
1444
1445 await this.hooks?.loadLocalRuns?.();
1446 await this.hooks?.reconcileLocalRuns?.();
1447
1448 if (this.autoStartLoops) {
1449 this.startLoops();
1450 }
1451
1452 return this.state.leaseState;
1453 }
1454
1455 stop(): void {
1456 if (this.heartbeatTimer != null) {
1457 this.clearIntervalImpl(this.heartbeatTimer);
1458 this.heartbeatTimer = null;
1459 }
1460
1461 if (this.leaseTimer != null) {
1462 this.clearIntervalImpl(this.leaseTimer);
1463 this.leaseTimer = null;
1464 }
1465 }
1466
1467 async sendHeartbeat(): Promise<void> {
1468 const request = this.buildHeartbeatRequest();
1469 await this.client.sendControllerHeartbeat(request);
1470 this.state.lastHeartbeatAt = request.heartbeatAt ?? this.now();
1471 this.state.lastError = null;
1472 }
1473
1474 async runLeaseCycle(): Promise<LeaseState> {
1475 const requestedOperation = this.getNextLeaseOperation();
1476 this.state.lastLeaseOperation = requestedOperation;
1477
1478 try {
1479 const result = await this.client.acquireLeaderLease(this.buildLeaseRequest());
1480 this.applyLeaseResult(result);
1481 this.state.consecutiveRenewFailures = 0;
1482 this.state.lastError = null;
1483 await this.transitionTo(result.isLeader ? "leader" : "standby");
1484 return this.state.leaseState;
1485 } catch (error) {
1486 this.state.lastError = toErrorMessage(error);
1487
1488 if (requestedOperation === "renew" && this.state.leaseState === "leader") {
1489 this.state.consecutiveRenewFailures += 1;
1490
1491 if (this.state.consecutiveRenewFailures >= this.getRenewFailureThreshold()) {
1492 await this.transitionTo("degraded");
1493 }
1494 } else {
1495 await this.transitionTo("degraded");
1496 }
1497
1498 throw error;
1499 }
1500 }
1501
1502 async runSchedulerPass(
1503 work: (context: SchedulerContext) => Promise<void>
1504 ): Promise<SchedulerDecision> {
1505 if (!this.canSchedule()) {
1506 return "skipped_not_leader";
1507 }
1508
1509 if (this.state.currentTerm == null) {
1510 return "skipped_not_leader";
1511 }
1512
1513 await work({
1514 controllerId: this.config.nodeId,
1515 host: this.config.host,
1516 term: this.state.currentTerm
1517 });
1518
1519 return "scheduled";
1520 }
1521
1522 private applyLeaseResult(result: LeaderLeaseAcquireResult): void {
1523 this.state.currentLeaderId = result.holderId;
1524 this.state.currentTerm = result.term;
1525 this.state.lastLeaseOperation = result.operation;
1526 this.state.leaseExpiresAt = result.leaseExpiresAt;
1527 }
1528
1529 private buildHeartbeatRequest(): ControllerHeartbeatInput {
1530 return {
1531 controllerId: this.config.nodeId,
1532 host: this.config.host,
1533 role: this.config.role,
1534 priority: this.config.priority ?? (this.config.role === "primary" ? 100 : 50),
1535 status: this.state.leaseState === "degraded" ? "degraded" : "alive",
1536 version: this.config.version ?? null,
1537 heartbeatAt: this.now(),
1538 startedAt: this.startedAt
1539 };
1540 }
1541
1542 private buildLeaseRequest(): LeaderLeaseAcquireInput {
1543 return {
1544 controllerId: this.config.nodeId,
1545 host: this.config.host,
1546 ttlSec: this.config.leaseTtlSec ?? DEFAULT_LEASE_TTL_SEC,
1547 preferred: this.config.preferred ?? this.config.role === "primary",
1548 now: this.now()
1549 };
1550 }
1551
1552 private getHeartbeatIntervalMs(): number {
1553 return this.config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
1554 }
1555
1556 private getLeaseRenewIntervalMs(): number {
1557 return this.config.leaseRenewIntervalMs ?? DEFAULT_LEASE_RENEW_INTERVAL_MS;
1558 }
1559
1560 private getRenewFailureThreshold(): number {
1561 return this.config.renewFailureThreshold ?? DEFAULT_RENEW_FAILURE_THRESHOLD;
1562 }
1563
1564 private startLoops(): void {
1565 if (this.heartbeatTimer == null) {
1566 this.heartbeatTimer = this.setIntervalImpl(() => {
1567 void this.safeHeartbeatTick();
1568 }, this.getHeartbeatIntervalMs());
1569 }
1570
1571 if (this.leaseTimer == null) {
1572 this.leaseTimer = this.setIntervalImpl(() => {
1573 void this.safeLeaseTick();
1574 }, this.getLeaseRenewIntervalMs());
1575 }
1576 }
1577
1578 private async safeHeartbeatTick(): Promise<void> {
1579 try {
1580 await this.sendHeartbeat();
1581 } catch (error) {
1582 this.state.lastError = toErrorMessage(error);
1583 }
1584 }
1585
1586 private async safeLeaseTick(): Promise<void> {
1587 try {
1588 await this.runLeaseCycle();
1589 } catch {
1590 // Background loop keeps the last known state and waits for the next cycle.
1591 }
1592 }
1593
1594 private async transitionTo(nextState: LeaseState): Promise<void> {
1595 if (this.state.leaseState === nextState) {
1596 return;
1597 }
1598
1599 this.state.leaseState = nextState;
1600 await this.hooks?.onLeaseStateChange?.(this.getStatusSnapshot());
1601 }
1602}
1603
1604function createDefaultNodeId(host: string, role: ConductorRole): string {
1605 return `${host}-${role === "primary" ? "main" : "standby"}`;
1606}
1607
1608function parseConductorRole(name: string, value: string | null): ConductorRole {
1609 if (value === "primary" || value === "standby") {
1610 return value;
1611 }
1612
1613 throw new Error(`${name} must be either "primary" or "standby".`);
1614}
1615
1616function parseBooleanValue(name: string, value: string | boolean | null | undefined): boolean | undefined {
1617 if (typeof value === "boolean") {
1618 return value;
1619 }
1620
1621 if (value == null) {
1622 return undefined;
1623 }
1624
1625 switch (value.trim().toLowerCase()) {
1626 case "1":
1627 case "on":
1628 case "true":
1629 case "yes":
1630 return true;
1631 case "0":
1632 case "false":
1633 case "no":
1634 case "off":
1635 return false;
1636 default:
1637 throw new Error(`${name} must be a boolean flag.`);
1638 }
1639}
1640
1641function parseIntegerValue(
1642 name: string,
1643 value: string | null | undefined,
1644 options: { minimum?: number } = {}
1645): number | undefined {
1646 if (value == null) {
1647 return undefined;
1648 }
1649
1650 const normalized = value.trim();
1651
1652 if (!/^-?\d+$/u.test(normalized)) {
1653 throw new Error(`${name} must be an integer.`);
1654 }
1655
1656 const parsed = Number(normalized);
1657
1658 if (!Number.isSafeInteger(parsed)) {
1659 throw new Error(`${name} is outside the supported integer range.`);
1660 }
1661
1662 if (options.minimum != null && parsed < options.minimum) {
1663 throw new Error(`${name} must be >= ${options.minimum}.`);
1664 }
1665
1666 return parsed;
1667}
1668
1669function resolvePathConfig(paths?: Partial<ConductorRuntimePaths>): ConductorRuntimePaths {
1670 return {
1671 logsDir: normalizeOptionalString(paths?.logsDir),
1672 runsDir: normalizeOptionalString(paths?.runsDir),
1673 stateDir: normalizeOptionalString(paths?.stateDir),
1674 tmpDir: normalizeOptionalString(paths?.tmpDir),
1675 worktreesDir: normalizeOptionalString(paths?.worktreesDir)
1676 };
1677}
1678
1679function resolveIngestLogDir(logsDir: string | null): string | null {
1680 return resolveLogSubdir(logsDir, "baa-ingest", "baa-ingest-log");
1681}
1682
1683function resolvePluginDiagnosticLogDir(logsDir: string | null): string | null {
1684 return resolveLogSubdir(logsDir, "baa-plugin", "baa-plugin-log");
1685}
1686
1687function resolveTimedJobsLogDir(logsDir: string | null): string | null {
1688 return resolveLogSubdir(logsDir, "timed-jobs", "timed-jobs-log");
1689}
1690
1691function resolveLogSubdir(
1692 logsDir: string | null,
1693 subdir: string,
1694 label: string
1695): string | null {
1696 const base = logsDir ?? "logs";
1697 const dir = join(base, subdir);
1698
1699 try {
1700 mkdirSync(dir, { recursive: true });
1701 } catch (error) {
1702 console.error(`[${label}] failed to create log directory ${dir}: ${String(error)}`);
1703 return null;
1704 }
1705
1706 return dir;
1707}
1708
1709function resolveConfiguredPublicApiBase(
1710 values: Pick<ConductorConfig, "controlApiBase" | "publicApiBase">
1711): string | null {
1712 return normalizeOptionalString(values.publicApiBase ?? values.controlApiBase);
1713}
1714
1715function resolveSourcePublicApiBase(
1716 env: ConductorEnvironment,
1717 overrides: CliValueOverrides
1718): string | null {
1719 return normalizeOptionalString(
1720 overrides.publicApiBase ??
1721 overrides.controlApiBase ??
1722 env.BAA_CONDUCTOR_PUBLIC_API_BASE ??
1723 env.BAA_CONTROL_API_BASE
1724 );
1725}
1726
1727function resolveCodeRootDir(value: string | null | undefined): string {
1728 return resolve(normalizeOptionalString(value) ?? DEFAULT_CODE_ROOT_DIR);
1729}
1730
1731type CanonicalConductorConfig = Omit<ConductorConfig, "controlApiBase" | "publicApiBase"> & {
1732 controlApiBase: string;
1733 publicApiBase: string;
1734};
1735
1736export function resolveConductorRuntimeConfig(
1737 config: ConductorRuntimeConfig
1738): ResolvedConductorRuntimeConfig {
1739 const nodeId = normalizeOptionalString(config.nodeId);
1740 const host = normalizeOptionalString(config.host);
1741 const publicApiBase = resolveConfiguredPublicApiBase(config);
1742
1743 if (!nodeId) {
1744 throw new Error("Conductor config requires a non-empty nodeId.");
1745 }
1746
1747 if (!host) {
1748 throw new Error("Conductor config requires a non-empty host.");
1749 }
1750
1751 if (!publicApiBase) {
1752 throw new Error("Conductor config requires a non-empty publicApiBase.");
1753 }
1754
1755 const heartbeatIntervalMs = config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
1756 const leaseRenewIntervalMs = config.leaseRenewIntervalMs ?? DEFAULT_LEASE_RENEW_INTERVAL_MS;
1757 const localApiAllowedHosts = parseLocalApiAllowedHosts(config.localApiAllowedHosts);
1758 const leaseTtlSec = config.leaseTtlSec ?? DEFAULT_LEASE_TTL_SEC;
1759 const renewFailureThreshold = config.renewFailureThreshold ?? DEFAULT_RENEW_FAILURE_THRESHOLD;
1760 const artifactInlineThreshold = config.artifactInlineThreshold ?? DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD;
1761 const artifactSummaryLength = config.artifactSummaryLength ?? DEFAULT_SUMMARY_LENGTH;
1762 const timedJobsIntervalMs = config.timedJobsIntervalMs ?? DEFAULT_TIMED_JOBS_INTERVAL_MS;
1763 const timedJobsMaxMessagesPerTick =
1764 config.timedJobsMaxMessagesPerTick ?? DEFAULT_TIMED_JOBS_MAX_MESSAGES_PER_TICK;
1765 const timedJobsMaxTasksPerTick =
1766 config.timedJobsMaxTasksPerTick ?? DEFAULT_TIMED_JOBS_MAX_TASKS_PER_TICK;
1767 const timedJobsSettleDelayMs =
1768 config.timedJobsSettleDelayMs ?? DEFAULT_TIMED_JOBS_SETTLE_DELAY_MS;
1769 const uiSessionTtlSec = config.uiSessionTtlSec ?? DEFAULT_UI_SESSION_TTL_SEC;
1770 const priority = config.priority ?? (config.role === "primary" ? 100 : 50);
1771
1772 if (heartbeatIntervalMs <= 0) {
1773 throw new Error("Conductor heartbeatIntervalMs must be > 0.");
1774 }
1775
1776 if (leaseRenewIntervalMs <= 0) {
1777 throw new Error("Conductor leaseRenewIntervalMs must be > 0.");
1778 }
1779
1780 if (leaseTtlSec <= 0) {
1781 throw new Error("Conductor leaseTtlSec must be > 0.");
1782 }
1783
1784 if (renewFailureThreshold <= 0) {
1785 throw new Error("Conductor renewFailureThreshold must be > 0.");
1786 }
1787
1788 if (!Number.isInteger(artifactInlineThreshold) || artifactInlineThreshold <= 0) {
1789 throw new Error("Conductor artifactInlineThreshold must be a positive integer.");
1790 }
1791
1792 if (!Number.isInteger(artifactSummaryLength) || artifactSummaryLength <= 0) {
1793 throw new Error("Conductor artifactSummaryLength must be a positive integer.");
1794 }
1795
1796 if (!Number.isInteger(timedJobsIntervalMs) || timedJobsIntervalMs <= 0) {
1797 throw new Error("Conductor timedJobsIntervalMs must be a positive integer.");
1798 }
1799
1800 if (!Number.isInteger(timedJobsMaxMessagesPerTick) || timedJobsMaxMessagesPerTick <= 0) {
1801 throw new Error("Conductor timedJobsMaxMessagesPerTick must be a positive integer.");
1802 }
1803
1804 if (!Number.isInteger(timedJobsMaxTasksPerTick) || timedJobsMaxTasksPerTick <= 0) {
1805 throw new Error("Conductor timedJobsMaxTasksPerTick must be a positive integer.");
1806 }
1807
1808 if (!Number.isInteger(timedJobsSettleDelayMs) || timedJobsSettleDelayMs < 0) {
1809 throw new Error("Conductor timedJobsSettleDelayMs must be a non-negative integer.");
1810 }
1811
1812 if (!Number.isInteger(uiSessionTtlSec) || uiSessionTtlSec <= 0) {
1813 throw new Error("Conductor uiSessionTtlSec must be a positive integer.");
1814 }
1815
1816 return {
1817 ...config,
1818 artifactInlineThreshold,
1819 artifactSummaryLength,
1820 nodeId,
1821 host,
1822 role: parseConductorRole("Conductor role", config.role),
1823 controlApiBase: normalizeBaseUrl(publicApiBase),
1824 claudeCodedLocalApiBase: resolveLocalApiBase(config.claudeCodedLocalApiBase),
1825 codeRootDir: resolveCodeRootDir(config.codeRootDir),
1826 codexdLocalApiBase: resolveLocalApiBase(config.codexdLocalApiBase),
1827 heartbeatIntervalMs,
1828 leaseRenewIntervalMs,
1829 localApiAllowedHosts,
1830 leaseTtlSec,
1831 localApiBase: resolveLocalApiBase(config.localApiBase, localApiAllowedHosts),
1832 paths: resolvePathConfig(config.paths),
1833 preferred: config.preferred ?? config.role === "primary",
1834 priority,
1835 publicApiBase: normalizeBaseUrl(publicApiBase),
1836 renewFailureThreshold,
1837 sharedToken: normalizeOptionalString(config.sharedToken),
1838 timedJobsIntervalMs,
1839 timedJobsMaxMessagesPerTick,
1840 timedJobsMaxTasksPerTick,
1841 timedJobsSettleDelayMs,
1842 uiBrowserAdminPassword: normalizeOptionalString(config.uiBrowserAdminPassword),
1843 uiReadonlyPassword: normalizeOptionalString(config.uiReadonlyPassword),
1844 uiSessionTtlSec,
1845 version: normalizeOptionalString(config.version)
1846 };
1847}
1848
1849function readOptionValue(argv: string[], option: string, index: number): string {
1850 const value = argv[index + 1];
1851
1852 if (!value || value.startsWith("--")) {
1853 throw new Error(`${option} requires a value.`);
1854 }
1855
1856 return value;
1857}
1858
1859function isCliAction(value: string): value is ConductorCliAction {
1860 return value === "checklist" || value === "config" || value === "help" || value === "start";
1861}
1862
1863function resolveRuntimeConfigFromSources(
1864 env: ConductorEnvironment,
1865 overrides: CliValueOverrides
1866): ResolvedConductorRuntimeConfig {
1867 const role = parseConductorRole(
1868 "Conductor role",
1869 normalizeOptionalString(overrides.role ?? env.BAA_CONDUCTOR_ROLE) ?? "primary"
1870 );
1871 const host = normalizeOptionalString(overrides.host ?? env.BAA_CONDUCTOR_HOST) ?? "localhost";
1872 const nodeId =
1873 normalizeOptionalString(overrides.nodeId ?? env.BAA_NODE_ID) ?? createDefaultNodeId(host, role);
1874 const publicApiBase = resolveSourcePublicApiBase(env, overrides);
1875
1876 if (!publicApiBase) {
1877 throw new Error(
1878 "Missing conductor public API base URL. Use --public-api-base or BAA_CONDUCTOR_PUBLIC_API_BASE. Legacy aliases: --control-api-base / BAA_CONTROL_API_BASE."
1879 );
1880 }
1881
1882 return resolveConductorRuntimeConfig({
1883 nodeId,
1884 host,
1885 role,
1886 publicApiBase,
1887 controlApiBase: publicApiBase,
1888 artifactInlineThreshold: parseIntegerValue(
1889 "Conductor artifact inline threshold",
1890 overrides.artifactInlineThreshold ?? env.BAA_ARTIFACT_INLINE_THRESHOLD,
1891 { minimum: 1 }
1892 ),
1893 artifactSummaryLength: parseIntegerValue(
1894 "Conductor artifact summary length",
1895 overrides.artifactSummaryLength ?? env.BAA_ARTIFACT_SUMMARY_LENGTH,
1896 { minimum: 1 }
1897 ),
1898 priority: parseIntegerValue("Conductor priority", overrides.priority ?? env.BAA_CONDUCTOR_PRIORITY, {
1899 minimum: 0
1900 }),
1901 version: normalizeOptionalString(overrides.version ?? env.BAA_CONDUCTOR_VERSION),
1902 preferred:
1903 parseBooleanValue(
1904 "Conductor preferred flag",
1905 overrides.preferred ?? normalizeOptionalString(env.BAA_CONDUCTOR_PREFERRED)
1906 ) ?? role === "primary",
1907 heartbeatIntervalMs: parseIntegerValue(
1908 "Conductor heartbeat interval",
1909 overrides.heartbeatIntervalMs ?? env.BAA_CONDUCTOR_HEARTBEAT_INTERVAL_MS,
1910 { minimum: 1 }
1911 ),
1912 leaseRenewIntervalMs: parseIntegerValue(
1913 "Conductor lease renew interval",
1914 overrides.leaseRenewIntervalMs ?? env.BAA_CONDUCTOR_LEASE_RENEW_INTERVAL_MS,
1915 { minimum: 1 }
1916 ),
1917 leaseTtlSec: parseIntegerValue(
1918 "Conductor lease ttl",
1919 overrides.leaseTtlSec ?? env.BAA_CONDUCTOR_LEASE_TTL_SEC,
1920 { minimum: 1 }
1921 ),
1922 renewFailureThreshold: parseIntegerValue(
1923 "Conductor renew failure threshold",
1924 overrides.renewFailureThreshold ?? env.BAA_CONDUCTOR_RENEW_FAILURE_THRESHOLD,
1925 { minimum: 1 }
1926 ),
1927 timedJobsIntervalMs: parseIntegerValue(
1928 "Conductor timed jobs interval",
1929 overrides.timedJobsIntervalMs ?? env.BAA_TIMED_JOBS_INTERVAL_MS,
1930 { minimum: 1 }
1931 ),
1932 timedJobsMaxMessagesPerTick: parseIntegerValue(
1933 "Conductor timed jobs max messages per tick",
1934 overrides.timedJobsMaxMessagesPerTick ?? env.BAA_TIMED_JOBS_MAX_MESSAGES_PER_TICK,
1935 { minimum: 1 }
1936 ),
1937 timedJobsMaxTasksPerTick: parseIntegerValue(
1938 "Conductor timed jobs max tasks per tick",
1939 overrides.timedJobsMaxTasksPerTick ?? env.BAA_TIMED_JOBS_MAX_TASKS_PER_TICK,
1940 { minimum: 1 }
1941 ),
1942 timedJobsSettleDelayMs: parseIntegerValue(
1943 "Conductor timed jobs settle delay",
1944 overrides.timedJobsSettleDelayMs ?? env.BAA_TIMED_JOBS_SETTLE_DELAY_MS,
1945 { minimum: 0 }
1946 ),
1947 claudeCodedLocalApiBase: normalizeOptionalString(
1948 overrides.claudeCodedLocalApiBase ?? env.BAA_CLAUDE_CODED_LOCAL_API_BASE
1949 ),
1950 codeRootDir: normalizeOptionalString(env.BAA_CODE_ROOT_DIR),
1951 codexdLocalApiBase: normalizeOptionalString(
1952 overrides.codexdLocalApiBase ?? env.BAA_CODEXD_LOCAL_API_BASE
1953 ),
1954 localApiAllowedHosts: overrides.localApiAllowedHosts ?? env.BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS,
1955 localApiBase: normalizeOptionalString(overrides.localApiBase ?? env.BAA_CONDUCTOR_LOCAL_API),
1956 sharedToken: normalizeOptionalString(overrides.sharedToken ?? env.BAA_SHARED_TOKEN),
1957 uiBrowserAdminPassword: normalizeOptionalString(env.BAA_UI_BROWSER_ADMIN_PASSWORD),
1958 uiReadonlyPassword: normalizeOptionalString(env.BAA_UI_READONLY_PASSWORD),
1959 uiSessionTtlSec: parseIntegerValue("Conductor UI session TTL", env.BAA_UI_SESSION_TTL_SEC, {
1960 minimum: 1
1961 }),
1962 paths: {
1963 logsDir: normalizeOptionalString(overrides.logsDir ?? env.BAA_LOGS_DIR),
1964 runsDir: normalizeOptionalString(overrides.runsDir ?? env.BAA_RUNS_DIR),
1965 stateDir: normalizeOptionalString(overrides.stateDir ?? env.BAA_STATE_DIR),
1966 tmpDir: normalizeOptionalString(overrides.tmpDir ?? env.BAA_TMP_DIR),
1967 worktreesDir: normalizeOptionalString(overrides.worktreesDir ?? env.BAA_WORKTREES_DIR)
1968 }
1969 });
1970}
1971
1972export function parseConductorCliRequest(
1973 argv: readonly string[],
1974 env: ConductorEnvironment = {}
1975): ConductorCliRequest {
1976 const tokens = [...argv];
1977 let action: ConductorCliAction = "start";
1978
1979 if (tokens[0] && !tokens[0].startsWith("--")) {
1980 const maybeAction = tokens.shift();
1981
1982 if (!maybeAction || !isCliAction(maybeAction)) {
1983 throw new Error(`Unknown conductor command: ${maybeAction ?? "<empty>"}.`);
1984 }
1985
1986 action = maybeAction;
1987 }
1988
1989 let printJson = false;
1990 let showHelp = action === "help";
1991 const overrides: CliValueOverrides = {
1992 runOnce: false
1993 };
1994
1995 for (let index = 0; index < tokens.length; index += 1) {
1996 const token = tokens[index];
1997
1998 switch (token) {
1999 case "--help":
2000 showHelp = true;
2001 break;
2002 case "--json":
2003 printJson = true;
2004 break;
2005 case "--run-once":
2006 overrides.runOnce = true;
2007 break;
2008 case "--preferred":
2009 overrides.preferred = true;
2010 break;
2011 case "--no-preferred":
2012 overrides.preferred = false;
2013 break;
2014 case "--node-id":
2015 overrides.nodeId = readOptionValue(tokens, token, index);
2016 index += 1;
2017 break;
2018 case "--host":
2019 overrides.host = readOptionValue(tokens, token, index);
2020 index += 1;
2021 break;
2022 case "--role":
2023 overrides.role = readOptionValue(tokens, token, index);
2024 index += 1;
2025 break;
2026 case "--public-api-base":
2027 overrides.publicApiBase = readOptionValue(tokens, token, index);
2028 index += 1;
2029 break;
2030 case "--control-api-base":
2031 overrides.controlApiBase = readOptionValue(tokens, token, index);
2032 index += 1;
2033 break;
2034 case "--codexd-local-api":
2035 overrides.codexdLocalApiBase = readOptionValue(tokens, token, index);
2036 index += 1;
2037 break;
2038 case "--claude-coded-local-api":
2039 overrides.claudeCodedLocalApiBase = readOptionValue(tokens, token, index);
2040 index += 1;
2041 break;
2042 case "--local-api":
2043 overrides.localApiBase = readOptionValue(tokens, token, index);
2044 index += 1;
2045 break;
2046 case "--shared-token":
2047 overrides.sharedToken = readOptionValue(tokens, token, index);
2048 index += 1;
2049 break;
2050 case "--artifact-inline-threshold":
2051 overrides.artifactInlineThreshold = readOptionValue(tokens, token, index);
2052 index += 1;
2053 break;
2054 case "--artifact-summary-length":
2055 overrides.artifactSummaryLength = readOptionValue(tokens, token, index);
2056 index += 1;
2057 break;
2058 case "--priority":
2059 overrides.priority = readOptionValue(tokens, token, index);
2060 index += 1;
2061 break;
2062 case "--version":
2063 overrides.version = readOptionValue(tokens, token, index);
2064 index += 1;
2065 break;
2066 case "--heartbeat-interval-ms":
2067 overrides.heartbeatIntervalMs = readOptionValue(tokens, token, index);
2068 index += 1;
2069 break;
2070 case "--lease-renew-interval-ms":
2071 overrides.leaseRenewIntervalMs = readOptionValue(tokens, token, index);
2072 index += 1;
2073 break;
2074 case "--lease-ttl-sec":
2075 overrides.leaseTtlSec = readOptionValue(tokens, token, index);
2076 index += 1;
2077 break;
2078 case "--renew-failure-threshold":
2079 overrides.renewFailureThreshold = readOptionValue(tokens, token, index);
2080 index += 1;
2081 break;
2082 case "--timed-jobs-interval-ms":
2083 overrides.timedJobsIntervalMs = readOptionValue(tokens, token, index);
2084 index += 1;
2085 break;
2086 case "--timed-jobs-max-messages-per-tick":
2087 overrides.timedJobsMaxMessagesPerTick = readOptionValue(tokens, token, index);
2088 index += 1;
2089 break;
2090 case "--timed-jobs-max-tasks-per-tick":
2091 overrides.timedJobsMaxTasksPerTick = readOptionValue(tokens, token, index);
2092 index += 1;
2093 break;
2094 case "--timed-jobs-settle-delay-ms":
2095 overrides.timedJobsSettleDelayMs = readOptionValue(tokens, token, index);
2096 index += 1;
2097 break;
2098 case "--runs-dir":
2099 overrides.runsDir = readOptionValue(tokens, token, index);
2100 index += 1;
2101 break;
2102 case "--worktrees-dir":
2103 overrides.worktreesDir = readOptionValue(tokens, token, index);
2104 index += 1;
2105 break;
2106 case "--logs-dir":
2107 overrides.logsDir = readOptionValue(tokens, token, index);
2108 index += 1;
2109 break;
2110 case "--tmp-dir":
2111 overrides.tmpDir = readOptionValue(tokens, token, index);
2112 index += 1;
2113 break;
2114 case "--state-dir":
2115 overrides.stateDir = readOptionValue(tokens, token, index);
2116 index += 1;
2117 break;
2118 default:
2119 throw new Error(`Unknown conductor option: ${token}.`);
2120 }
2121 }
2122
2123 if (showHelp) {
2124 return { action: "help" };
2125 }
2126
2127 if (action === "checklist") {
2128 return {
2129 action,
2130 printJson
2131 };
2132 }
2133
2134 const config = resolveRuntimeConfigFromSources(env, overrides);
2135
2136 if (action === "config") {
2137 return {
2138 action,
2139 config,
2140 printJson
2141 };
2142 }
2143
2144 return {
2145 action: "start",
2146 config,
2147 printJson,
2148 runOnce: overrides.runOnce
2149 };
2150}
2151
2152function usesPlaceholderToken(token: string | null): boolean {
2153 return token === "replace-me";
2154}
2155
2156function buildRuntimeWarnings(config: ResolvedConductorRuntimeConfig): string[] {
2157 const warnings: string[] = [];
2158
2159 if (config.sharedToken == null) {
2160 warnings.push("BAA_SHARED_TOKEN is not configured; authenticated Control API requests may fail.");
2161 } else if (usesPlaceholderToken(config.sharedToken)) {
2162 warnings.push("BAA_SHARED_TOKEN is still set to replace-me; replace it before production rollout.");
2163 }
2164
2165 if (config.uiBrowserAdminPassword == null && config.uiReadonlyPassword == null) {
2166 warnings.push("No UI session password is configured; /app/login cannot authenticate any browser session.");
2167 }
2168
2169 if (config.localApiBase == null) {
2170 warnings.push("BAA_CONDUCTOR_LOCAL_API is not configured; only the in-process snapshot interface is available.");
2171 }
2172
2173 if (config.codexdLocalApiBase == null) {
2174 warnings.push("BAA_CODEXD_LOCAL_API_BASE is not configured; /v1/codex routes will stay unavailable.");
2175 }
2176
2177 if (config.claudeCodedLocalApiBase == null) {
2178 warnings.push("BAA_CLAUDE_CODED_LOCAL_API_BASE is not configured; /v1/claude-coded routes will stay unavailable.");
2179 }
2180
2181 if (config.leaseRenewIntervalMs >= config.leaseTtlSec * 1_000) {
2182 warnings.push("lease renew interval is >= lease TTL; leader renewals may race with lease expiry.");
2183 }
2184
2185 return warnings;
2186}
2187
2188function getProcessLike(): ConductorProcessLike | undefined {
2189 return (globalThis as { process?: ConductorProcessLike }).process;
2190}
2191
2192function writeLine(writer: ConductorOutputWriter, line: string): void {
2193 if ("write" in writer) {
2194 writer.write(`${line}\n`);
2195 return;
2196 }
2197
2198 writer.log(line);
2199}
2200
2201function formatChecklistText(checklist: StartupChecklistItem[]): string {
2202 return checklist.map((item, index) => `${index + 1}. ${item.key}: ${item.description}`).join("\n");
2203}
2204
2205function formatConfigText(config: ResolvedConductorRuntimeConfig): string {
2206 return [
2207 `identity: ${config.nodeId}@${config.host}(${config.role})`,
2208 `public_api_base: ${config.publicApiBase}`,
2209 `claude_coded_local_api_base: ${config.claudeCodedLocalApiBase ?? "not-configured"}`,
2210 `codexd_local_api_base: ${config.codexdLocalApiBase ?? "not-configured"}`,
2211 `code_root_dir: ${config.codeRootDir}`,
2212 `local_api_base: ${config.localApiBase ?? "not-configured"}`,
2213 `browser_ws_url: ${buildBrowserWebSocketUrl(config.localApiBase) ?? "not-configured"}`,
2214 `firefox_ws_url: ${buildFirefoxWebSocketUrl(config.localApiBase) ?? "not-configured"}`,
2215 `local_api_allowed_hosts: ${config.localApiAllowedHosts.join(",") || "loopback-only"}`,
2216 `priority: ${config.priority}`,
2217 `preferred: ${String(config.preferred)}`,
2218 `artifact_inline_threshold: ${config.artifactInlineThreshold}`,
2219 `artifact_summary_length: ${config.artifactSummaryLength}`,
2220 `heartbeat_interval_ms: ${config.heartbeatIntervalMs}`,
2221 `lease_renew_interval_ms: ${config.leaseRenewIntervalMs}`,
2222 `lease_ttl_sec: ${config.leaseTtlSec}`,
2223 `renew_failure_threshold: ${config.renewFailureThreshold}`,
2224 `timed_jobs_interval_ms: ${config.timedJobsIntervalMs}`,
2225 `timed_jobs_max_messages_per_tick: ${config.timedJobsMaxMessagesPerTick}`,
2226 `timed_jobs_max_tasks_per_tick: ${config.timedJobsMaxTasksPerTick}`,
2227 `timed_jobs_settle_delay_ms: ${config.timedJobsSettleDelayMs}`,
2228 `shared_token_configured: ${String(config.sharedToken != null)}`,
2229 `runs_dir: ${config.paths.runsDir ?? "not-configured"}`,
2230 `worktrees_dir: ${config.paths.worktreesDir ?? "not-configured"}`,
2231 `logs_dir: ${config.paths.logsDir ?? "not-configured"}`,
2232 `tmp_dir: ${config.paths.tmpDir ?? "not-configured"}`,
2233 `state_dir: ${config.paths.stateDir ?? "not-configured"}`
2234 ].join("\n");
2235}
2236
2237function formatRuntimeSummary(snapshot: ConductorRuntimeSnapshot): string {
2238 return [
2239 `identity=${snapshot.identity}`,
2240 `lease=${snapshot.daemon.leaseState}`,
2241 `leader=${snapshot.daemon.currentLeaderId ?? "none"}`,
2242 `term=${snapshot.daemon.currentTerm ?? "none"}`,
2243 `scheduler=${snapshot.daemon.schedulerEnabled ? "enabled" : "disabled"}`,
2244 `heartbeat_loop=${snapshot.loops.heartbeat ? "running" : "stopped"}`,
2245 `lease_loop=${snapshot.loops.lease ? "running" : "stopped"}`
2246 ].join(" ");
2247}
2248
2249function getUsageText(): string {
2250 return [
2251 "Usage:",
2252 " node apps/conductor-daemon/dist/index.js [start] [options]",
2253 " node --experimental-strip-types apps/conductor-daemon/src/index.ts [start] [options]",
2254 " node .../index.js config [options]",
2255 " node .../index.js checklist [--json]",
2256 " node .../index.js help",
2257 "",
2258 "Options:",
2259 " --node-id <id>",
2260 " --host <host>",
2261 " --role <primary|standby>",
2262 " --public-api-base <url> conductor upstream/public API base",
2263 " --control-api-base <url> legacy alias for --public-api-base",
2264 " --codexd-local-api <url>",
2265 " --local-api <url>",
2266 " --shared-token <token>",
2267 " --artifact-inline-threshold <integer>",
2268 " --artifact-summary-length <integer>",
2269 " --priority <integer>",
2270 " --version <string>",
2271 " --preferred | --no-preferred",
2272 " --heartbeat-interval-ms <integer>",
2273 " --lease-renew-interval-ms <integer>",
2274 " --lease-ttl-sec <integer>",
2275 " --renew-failure-threshold <integer>",
2276 " --timed-jobs-interval-ms <integer>",
2277 " --timed-jobs-max-messages-per-tick <integer>",
2278 " --timed-jobs-max-tasks-per-tick <integer>",
2279 " --timed-jobs-settle-delay-ms <integer>",
2280 " --runs-dir <path>",
2281 " --worktrees-dir <path>",
2282 " --logs-dir <path>",
2283 " --tmp-dir <path>",
2284 " --state-dir <path>",
2285 " --run-once",
2286 " --json",
2287 " --help",
2288 "",
2289 "Environment:",
2290 " BAA_NODE_ID",
2291 " BAA_CONDUCTOR_HOST",
2292 " BAA_CONDUCTOR_ROLE",
2293 " BAA_CONDUCTOR_PUBLIC_API_BASE",
2294 " BAA_CONTROL_API_BASE (legacy env alias for conductor upstream/public API base)",
2295 " BAA_CODE_ROOT_DIR",
2296 " BAA_CODEXD_LOCAL_API_BASE",
2297 " BAA_CONDUCTOR_LOCAL_API",
2298 " BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS",
2299 " BAA_SHARED_TOKEN",
2300 " BAA_UI_BROWSER_ADMIN_PASSWORD",
2301 " BAA_UI_READONLY_PASSWORD",
2302 " BAA_UI_SESSION_TTL_SEC",
2303 " BAA_ARTIFACT_INLINE_THRESHOLD",
2304 " BAA_ARTIFACT_SUMMARY_LENGTH",
2305 " BAA_CONDUCTOR_PRIORITY",
2306 " BAA_CONDUCTOR_VERSION",
2307 " BAA_CONDUCTOR_PREFERRED",
2308 " BAA_CONDUCTOR_HEARTBEAT_INTERVAL_MS",
2309 " BAA_CONDUCTOR_LEASE_RENEW_INTERVAL_MS",
2310 " BAA_CONDUCTOR_LEASE_TTL_SEC",
2311 " BAA_CONDUCTOR_RENEW_FAILURE_THRESHOLD",
2312 " BAA_TIMED_JOBS_INTERVAL_MS",
2313 " BAA_TIMED_JOBS_MAX_MESSAGES_PER_TICK",
2314 " BAA_TIMED_JOBS_MAX_TASKS_PER_TICK",
2315 " BAA_TIMED_JOBS_SETTLE_DELAY_MS",
2316 " BAA_RUNS_DIR",
2317 " BAA_WORKTREES_DIR",
2318 " BAA_LOGS_DIR",
2319 " BAA_TMP_DIR",
2320 " BAA_STATE_DIR",
2321 "",
2322 "Precedence:",
2323 " CLI values override environment values.",
2324 " --public-api-base / BAA_CONDUCTOR_PUBLIC_API_BASE override legacy control-api aliases."
2325 ].join("\n");
2326}
2327
2328export class ConductorRuntime {
2329 private readonly artifactStore: ArtifactStore;
2330 private readonly config: ResolvedConductorRuntimeConfig;
2331 private readonly daemon: ConductorDaemon;
2332 private readonly d1SyncWorker: D1SyncWorker | null;
2333 private readonly localControlPlane: ConductorLocalControlPlane;
2334 private readonly localApiServer: ConductorLocalHttpServer | null;
2335 private localControlPlaneInitialized = false;
2336 private readonly now: () => number;
2337 private readonly timedJobs: ConductorTimedJobs;
2338 private started = false;
2339
2340 constructor(config: ConductorRuntimeConfig, options: ConductorRuntimeOptions = {}) {
2341 this.now = options.now ?? defaultNowUnixSeconds;
2342 const startedAt = config.startedAt ?? this.now();
2343
2344 this.config = resolveConductorRuntimeConfig({
2345 ...config,
2346 startedAt
2347 });
2348 const resolvedStateDir = this.config.paths.stateDir ?? resolveDefaultConductorStateDir();
2349 this.localControlPlane = new ConductorLocalControlPlane({
2350 stateDir: resolvedStateDir
2351 });
2352 this.artifactStore = new ArtifactStore({
2353 artifactDir: join(resolvedStateDir, ARTIFACTS_DIRNAME),
2354 databasePath: join(resolvedStateDir, ARTIFACT_DB_FILENAME),
2355 publicBaseUrl: this.config.publicApiBase,
2356 summaryLength: this.config.artifactSummaryLength
2357 });
2358 this.daemon = new ConductorDaemon(this.config, {
2359 ...options,
2360 client:
2361 options.client ?? createRepositoryControlApiClient(this.localControlPlane.repository, this.now),
2362 controlApiBearerToken: options.controlApiBearerToken ?? this.config.sharedToken,
2363 now: this.now
2364 });
2365 const ingestLogDir = resolveIngestLogDir(this.config.paths.logsDir);
2366 const pluginDiagnosticLogDir = resolvePluginDiagnosticLogDir(this.config.paths.logsDir);
2367 const timedJobsLogDir = resolveTimedJobsLogDir(this.config.paths.logsDir);
2368 this.timedJobs = new ConductorTimedJobs(
2369 {
2370 intervalMs: this.config.timedJobsIntervalMs,
2371 maxMessagesPerTick: this.config.timedJobsMaxMessagesPerTick,
2372 maxTasksPerTick: this.config.timedJobsMaxTasksPerTick,
2373 settleDelayMs: this.config.timedJobsSettleDelayMs
2374 },
2375 {
2376 artifactStore: this.artifactStore,
2377 autoStart: options.autoStartLoops ?? true,
2378 clearIntervalImpl: options.clearIntervalImpl,
2379 logDir: timedJobsLogDir,
2380 schedule: async (work) => {
2381 const automationState = await this.localControlPlane.repository.getAutomationState();
2382 const automationMode = automationState?.mode ?? DEFAULT_AUTOMATION_MODE;
2383
2384 if (automationMode === "paused") {
2385 return "skipped_system_paused";
2386 }
2387
2388 return this.daemon.runSchedulerPass(work);
2389 },
2390 setIntervalImpl: options.setIntervalImpl
2391 }
2392 );
2393 this.timedJobs.registerRunner(
2394 createRenewalProjectorRunner({
2395 now: () => this.now() * 1000,
2396 repository: this.localControlPlane.repository
2397 })
2398 );
2399 this.localApiServer =
2400 this.config.localApiBase == null
2401 ? null
2402 : new ConductorLocalHttpServer(
2403 this.config.localApiBase,
2404 this.localControlPlane.repository,
2405 this.artifactStore,
2406 () => this.getRuntimeSnapshot(),
2407 this.config.codeRootDir,
2408 this.config.codexdLocalApiBase,
2409 this.config.claudeCodedLocalApiBase,
2410 options.fetchImpl ?? globalThis.fetch,
2411 this.config.sharedToken,
2412 this.config.version,
2413 this.now,
2414 this.config.uiBrowserAdminPassword,
2415 this.config.uiReadonlyPassword,
2416 this.config.uiSessionTtlSec,
2417 this.config.artifactInlineThreshold,
2418 this.config.artifactSummaryLength,
2419 options.browserRequestPolicyOptions,
2420 ingestLogDir,
2421 pluginDiagnosticLogDir
2422 );
2423 this.timedJobs.registerRunner(
2424 createRenewalDispatcherRunner({
2425 browserBridge:
2426 this.localApiServer?.getFirefoxBridgeService() as unknown as BrowserBridgeController ?? null,
2427 now: () => this.now() * 1000
2428 })
2429 );
2430
2431 // D1 sync worker — silently skipped when env vars are not set.
2432 this.d1SyncWorker = createD1SyncWorker({
2433 env: options.env ?? getProcessLike()?.env ?? {},
2434 databasePath: join(resolvedStateDir, ARTIFACT_DB_FILENAME),
2435 fetchImpl: options.fetchImpl
2436 });
2437
2438 // Inject sync queue into artifact store so local writes enqueue D1 sync records.
2439 if (this.d1SyncWorker != null) {
2440 this.artifactStore.setSyncQueue(this.d1SyncWorker.getQueue());
2441 }
2442 }
2443
2444 async start(): Promise<ConductorRuntimeSnapshot> {
2445 if (!this.started) {
2446 if (!this.localControlPlaneInitialized) {
2447 await this.localControlPlane.initialize();
2448 this.localControlPlaneInitialized = true;
2449 }
2450
2451 await this.artifactStore.recoverAutomationRuntimeState({
2452 now: this.now() * 1000,
2453 renewalRecoveryDelayMs: Math.max(
2454 DEFAULT_RESTART_RENEWAL_RECOVERY_DELAY_MS,
2455 this.config.timedJobsIntervalMs
2456 )
2457 });
2458
2459 await this.daemon.start();
2460
2461 try {
2462 await this.localApiServer?.start();
2463 await this.timedJobs.start();
2464 this.started = true;
2465 } catch (error) {
2466 this.started = false;
2467 await this.timedJobs.stop();
2468 await this.localApiServer?.stop();
2469 await this.daemon.stop();
2470 throw error;
2471 }
2472
2473 this.d1SyncWorker?.start();
2474 }
2475
2476 return this.getRuntimeSnapshot();
2477 }
2478
2479 async stop(): Promise<ConductorRuntimeSnapshot> {
2480 this.started = false;
2481 this.d1SyncWorker?.stop();
2482 await this.timedJobs.stop();
2483 await this.daemon.stop();
2484 await this.localApiServer?.stop();
2485
2486 return this.getRuntimeSnapshot();
2487 }
2488
2489 getRuntimeSnapshot(now: number = this.now()): ConductorRuntimeSnapshot {
2490 const localApiBase = this.localApiServer?.getBaseUrl() ?? this.config.localApiBase;
2491 const browserWsUrl =
2492 this.localApiServer?.getBrowserWebSocketUrl() ?? buildBrowserWebSocketUrl(localApiBase);
2493 const firefoxWsUrl =
2494 this.localApiServer?.getFirefoxWebSocketUrl() ?? buildFirefoxWebSocketUrl(localApiBase);
2495
2496 return {
2497 daemon: this.daemon.getStatusSnapshot(now),
2498 identity: this.daemon.describeIdentity(),
2499 loops: this.daemon.getLoopStatus(),
2500 paths: { ...this.config.paths },
2501 controlApi: {
2502 baseUrl: this.config.publicApiBase,
2503 browserWsUrl,
2504 firefoxWsUrl,
2505 localApiBase,
2506 hasSharedToken: this.config.sharedToken != null,
2507 usesPlaceholderToken: usesPlaceholderToken(this.config.sharedToken)
2508 },
2509 claudeCoded: {
2510 localApiBase: this.config.claudeCodedLocalApiBase
2511 },
2512 codexd: {
2513 localApiBase: this.config.codexdLocalApiBase
2514 },
2515 runtime: {
2516 pid: getProcessLike()?.pid ?? null,
2517 started: this.started,
2518 startedAt: this.config.startedAt ?? now
2519 },
2520 startupChecklist: this.daemon.getStartupChecklist(),
2521 warnings: buildRuntimeWarnings(this.config)
2522 };
2523 }
2524
2525 getFirefoxBridgeService(): FirefoxBridgeService | null {
2526 return this.localApiServer?.getFirefoxBridgeService() ?? null;
2527 }
2528}
2529
2530async function waitForShutdownSignal(processLike: ConductorProcessLike | undefined): Promise<string | null> {
2531 const subscribe = processLike?.on;
2532
2533 if (!subscribe || !processLike) {
2534 return null;
2535 }
2536
2537 return new Promise((resolve) => {
2538 const signals = ["SIGINT", "SIGTERM"] as const;
2539 const listeners: Partial<Record<(typeof signals)[number], () => void>> = {};
2540 const cleanup = () => {
2541 if (!processLike.off) {
2542 return;
2543 }
2544
2545 for (const signal of signals) {
2546 const listener = listeners[signal];
2547
2548 if (listener) {
2549 processLike.off(signal, listener);
2550 }
2551 }
2552 };
2553
2554 for (const signal of signals) {
2555 const listener = () => {
2556 cleanup();
2557 resolve(signal);
2558 };
2559 listeners[signal] = listener;
2560 subscribe.call(processLike, signal, listener);
2561 }
2562 });
2563}
2564
2565export async function runConductorCli(options: RunConductorCliOptions = {}): Promise<number> {
2566 const processLike = options.processLike ?? getProcessLike();
2567 const stdout = options.stdout ?? console;
2568 const stderr = options.stderr ?? console;
2569 const argv = options.argv ?? processLike?.argv?.slice(2) ?? [];
2570 const env = options.env ?? processLike?.env ?? {};
2571 const request = parseConductorCliRequest(argv, env);
2572
2573 if (request.action === "help") {
2574 writeLine(stdout, getUsageText());
2575 return 0;
2576 }
2577
2578 if (request.action === "checklist") {
2579 const checklist = getDefaultStartupChecklist();
2580
2581 if (request.printJson) {
2582 writeLine(stdout, JSON.stringify(checklist, null, 2));
2583 } else {
2584 writeLine(stdout, formatChecklistText(checklist));
2585 }
2586
2587 return 0;
2588 }
2589
2590 if (request.action === "config") {
2591 if (request.printJson) {
2592 writeLine(stdout, JSON.stringify(request.config, null, 2));
2593 } else {
2594 writeLine(stdout, formatConfigText(request.config));
2595 }
2596
2597 for (const warning of buildRuntimeWarnings(request.config)) {
2598 writeLine(stderr, `warning: ${warning}`);
2599 }
2600
2601 return 0;
2602 }
2603
2604 const runtime = new ConductorRuntime(request.config, {
2605 autoStartLoops: !request.runOnce,
2606 env,
2607 fetchImpl: options.fetchImpl
2608 });
2609 const snapshot = await runtime.start();
2610
2611 if (request.printJson || request.runOnce) {
2612 writeLine(stdout, JSON.stringify(snapshot, null, 2));
2613 } else {
2614 writeLine(stdout, formatRuntimeSummary(snapshot));
2615 }
2616
2617 for (const warning of snapshot.warnings) {
2618 writeLine(stderr, `warning: ${warning}`);
2619 }
2620
2621 if (request.runOnce) {
2622 await runtime.stop();
2623 return 0;
2624 }
2625
2626 const signal = await waitForShutdownSignal(processLike);
2627 await runtime.stop();
2628
2629 if (!request.printJson) {
2630 writeLine(stdout, `conductor stopped${signal ? ` after ${signal}` : ""}`);
2631 }
2632
2633 return 0;
2634}
2635
2636function normalizeFilePath(path: string): string {
2637 return path.replace(/\\/gu, "/");
2638}
2639
2640function fileUrlToPath(fileUrl: string): string {
2641 if (!fileUrl.startsWith("file://")) {
2642 return fileUrl;
2643 }
2644
2645 const decoded = decodeURIComponent(fileUrl.slice("file://".length));
2646 return decoded.startsWith("/") && /^[A-Za-z]:/u.test(decoded.slice(1)) ? decoded.slice(1) : decoded;
2647}
2648
2649function isMainModule(metaUrl: string, argv: readonly string[]): boolean {
2650 const entryPoint = argv[1];
2651
2652 if (!entryPoint) {
2653 return false;
2654 }
2655
2656 return normalizeFilePath(entryPoint) === normalizeFilePath(fileUrlToPath(metaUrl));
2657}
2658
2659if (isMainModule(import.meta.url, getProcessLike()?.argv ?? [])) {
2660 void runConductorCli().then(
2661 (exitCode) => {
2662 const processLike = getProcessLike();
2663
2664 if (processLike) {
2665 processLike.exitCode = exitCode;
2666 }
2667 },
2668 (error: unknown) => {
2669 console.error(error instanceof Error ? error.stack ?? error.message : String(error));
2670 const processLike = getProcessLike();
2671
2672 if (processLike) {
2673 processLike.exitCode = 1;
2674 }
2675 }
2676 );
2677}