@love-moon/conductor-cli 0.2.31 → 0.2.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/daemon.js CHANGED
@@ -8,13 +8,21 @@ import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import dotenv from "dotenv";
9
9
  import yaml from "js-yaml";
10
10
 
11
- import { ConductorWebSocketClient, ConductorConfig, loadConfig, ConfigFileNotFound } from "@love-moon/conductor-sdk";
11
+ import {
12
+ ConductorWebSocketClient,
13
+ ConductorConfig,
14
+ loadConfig,
15
+ ConfigFileNotFound,
16
+ ProjectContext,
17
+ } from "@love-moon/conductor-sdk";
12
18
  import { DaemonLogCollector } from "./log-collector.js";
13
19
  import { resolveResumeContext } from "./fire/resume.js";
14
20
  import {
15
- RUNTIME_SUPPORTED_BACKENDS,
21
+ filterRuntimeSupportedAllowCliList,
22
+ listAdvertisedBackends,
23
+ resolveConfiguredRuntimeBackend,
24
+ isBuiltInRuntimeBackend,
16
25
  isRuntimeSupportedBackend,
17
- listRuntimeSupportedBackends,
18
26
  normalizeRuntimeBackendAlias,
19
27
  normalizeRuntimeBackendName,
20
28
  } from "./runtime-backends.js";
@@ -59,6 +67,7 @@ const DEFAULT_TERMINAL_ROWS = 40;
59
67
  const DEFAULT_TERMINAL_RING_BUFFER_MAX_BYTES = 2 * 1024 * 1024;
60
68
  const DEFAULT_TERMINAL_RESUME_SNAPSHOT_MAX_BYTES = 128 * 1024;
61
69
  const DEFAULT_RTC_MODULE_CANDIDATES = ["@roamhq/wrtc", "wrtc"];
70
+ const BLOCKING_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
62
71
  let nodePtySpawnPromise = null;
63
72
 
64
73
  function resolveNodePtySpawnExport(mod) {
@@ -117,6 +126,20 @@ function logError(message) {
117
126
  appendDaemonLog(line);
118
127
  }
119
128
 
129
+ function sleepSync(ms) {
130
+ if (!Number.isFinite(ms) || ms <= 0) {
131
+ return;
132
+ }
133
+ try {
134
+ Atomics.wait(BLOCKING_SLEEP_BUFFER, 0, 0, ms);
135
+ } catch {
136
+ const deadline = Date.now() + ms;
137
+ while (Date.now() < deadline) {
138
+ // best-effort fallback for runtimes that disallow Atomics.wait
139
+ }
140
+ }
141
+ }
142
+
120
143
  function getUserConfig(configFilePath) {
121
144
  try {
122
145
  const home = os.homedir();
@@ -192,14 +215,12 @@ function filterConfiguredAllowCliList(allowCliList) {
192
215
  if (!allowCliList || typeof allowCliList !== "object") {
193
216
  return {};
194
217
  }
195
- const builtInBackends = new Set(RUNTIME_SUPPORTED_BACKENDS);
196
218
  const filtered = {};
197
219
  for (const [backend, command] of Object.entries(allowCliList)) {
198
220
  const normalizedBackend = normalizeRuntimeBackendName(backend);
199
221
  if (
200
222
  !normalizedBackend ||
201
- LEGACY_RUNTIME_BACKEND_ALIASES.has(normalizedBackend) ||
202
- !builtInBackends.has(normalizedBackend)
223
+ LEGACY_RUNTIME_BACKEND_ALIASES.has(normalizedBackend)
203
224
  ) {
204
225
  continue;
205
226
  }
@@ -222,10 +243,28 @@ function getAllowCliList(userConfig) {
222
243
  return DEFAULT_CLI_LIST;
223
244
  }
224
245
 
246
+ function getRawAllowCliList(userConfig) {
247
+ if (userConfig.allow_cli_list && typeof userConfig.allow_cli_list === "object") {
248
+ return userConfig.allow_cli_list;
249
+ }
250
+ return DEFAULT_CLI_LIST;
251
+ }
252
+
225
253
  function formatBackendLaunchCommand(cliCommand) {
226
254
  return typeof cliCommand === "string" && cliCommand.trim() ? cliCommand.trim() : "ai-sdk-managed";
227
255
  }
228
256
 
257
+ function serializeRuntimeBackendMap(runtimeBackendMap) {
258
+ if (!runtimeBackendMap || typeof runtimeBackendMap !== "object") {
259
+ return "";
260
+ }
261
+ return Object.entries(runtimeBackendMap)
262
+ .filter(([backend, runtimeBackend]) => backend && runtimeBackend)
263
+ .sort(([left], [right]) => left.localeCompare(right))
264
+ .map(([backend, runtimeBackend]) => `${backend}=${runtimeBackend}`)
265
+ .join(",");
266
+ }
267
+
229
268
  async function defaultCreatePty(command, args, options) {
230
269
  if (!nodePtySpawnPromise) {
231
270
  const spawnHelperInfo = ensureNodePtySpawnHelperExecutable();
@@ -393,6 +432,64 @@ function normalizeLaunchConfig(value) {
393
432
  return value;
394
433
  }
395
434
 
435
+ function normalizeBooleanFlag(value) {
436
+ if (typeof value === "boolean") {
437
+ return value;
438
+ }
439
+ if (typeof value === "number") {
440
+ return value === 1;
441
+ }
442
+ if (typeof value !== "string") {
443
+ return false;
444
+ }
445
+ const normalized = value.trim().toLowerCase();
446
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
447
+ }
448
+
449
+ function parseTaskWorktreeLaunchConfig(launchConfig) {
450
+ const normalizedLaunchConfig = normalizeLaunchConfig(launchConfig);
451
+ const worktreeEnabled = normalizeBooleanFlag(
452
+ normalizedLaunchConfig.worktree ??
453
+ normalizedLaunchConfig.createWorktree ??
454
+ normalizedLaunchConfig.create_worktree,
455
+ );
456
+ if (!worktreeEnabled) {
457
+ return null;
458
+ }
459
+
460
+ const worktreeId =
461
+ normalizeOptionalString(normalizedLaunchConfig.worktreeId) ||
462
+ normalizeOptionalString(normalizedLaunchConfig.worktree_id);
463
+ const worktreeBranch =
464
+ normalizeOptionalString(normalizedLaunchConfig.worktreeBranch) ||
465
+ normalizeOptionalString(normalizedLaunchConfig.worktree_branch);
466
+ const projectRepoRoot =
467
+ normalizeOptionalString(normalizedLaunchConfig.projectRepoRoot) ||
468
+ normalizeOptionalString(normalizedLaunchConfig.project_repo_root);
469
+ const projectWorkspacePath =
470
+ normalizeOptionalString(normalizedLaunchConfig.projectWorkspacePath) ||
471
+ normalizeOptionalString(normalizedLaunchConfig.project_workspace_path);
472
+ const projectRelativePath =
473
+ normalizeOptionalString(normalizedLaunchConfig.projectRelativePath) ||
474
+ normalizeOptionalString(normalizedLaunchConfig.project_relative_path) ||
475
+ ".";
476
+ if (!worktreeId || !worktreeBranch || !projectRepoRoot || !projectWorkspacePath) {
477
+ return null;
478
+ }
479
+
480
+ return {
481
+ worktreeId,
482
+ worktreeBranch,
483
+ worktreeBaseRef:
484
+ normalizeOptionalString(normalizedLaunchConfig.worktreeBaseRef) ||
485
+ normalizeOptionalString(normalizedLaunchConfig.worktree_base_ref) ||
486
+ "HEAD",
487
+ projectRepoRoot,
488
+ projectWorkspacePath,
489
+ projectRelativePath,
490
+ };
491
+ }
492
+
396
493
  function normalizeTerminalEnv(value) {
397
494
  if (!value || typeof value !== "object" || Array.isArray(value)) {
398
495
  return {};
@@ -410,6 +507,36 @@ function normalizeTerminalEnv(value) {
410
507
  return env;
411
508
  }
412
509
 
510
+ const PTY_TASK_SCOPED_ENV_KEYS = [
511
+ "CONDUCTOR_PROJECT_ID",
512
+ "CONDUCTOR_TASK_ID",
513
+ "CONDUCTOR_PTY_SESSION_ID",
514
+ "CONDUCTOR_LAUNCHED_BY_DAEMON",
515
+ "CONDUCTOR_RESUME_CWD",
516
+ ];
517
+
518
+ function stripPtyTaskScopedEnv(source) {
519
+ const env = {
520
+ ...(source && typeof source === "object" ? source : {}),
521
+ };
522
+ for (const key of PTY_TASK_SCOPED_ENV_KEYS) {
523
+ delete env[key];
524
+ }
525
+ return env;
526
+ }
527
+
528
+ function buildPtyTaskEnv(baseEnv = process.env, launchEnv = {}) {
529
+ const parentEnv = stripPtyTaskScopedEnv(baseEnv);
530
+ const taskLaunchEnv = stripPtyTaskScopedEnv(launchEnv);
531
+ const env = {
532
+ ...parentEnv,
533
+ };
534
+ return {
535
+ ...env,
536
+ ...taskLaunchEnv,
537
+ };
538
+ }
539
+
413
540
  export function startDaemon(config = {}, deps = {}) {
414
541
  const exitFn = deps.exit || process.exit;
415
542
  const killFn = deps.kill || process.kill;
@@ -495,8 +622,10 @@ export function startDaemon(config = {}, deps = {}) {
495
622
  const CLI_PATH_VAL = config.CLI_PATH || CLI_PATH;
496
623
 
497
624
  // Get allow_cli_list from config
498
- const ALLOW_CLI_LIST = getAllowCliList(userConfig);
499
- let SUPPORTED_BACKENDS = Object.keys(ALLOW_CLI_LIST);
625
+ const RAW_ALLOW_CLI_LIST = getRawAllowCliList(userConfig);
626
+ let ALLOW_CLI_LIST = {};
627
+ let SUPPORTED_BACKENDS = [];
628
+ let SUPPORTED_BACKEND_RUNTIME_MAP = {};
500
629
  const fetchLatestVersionFn = deps.fetchLatestVersion || fetchLatestVersion;
501
630
  const isNewerVersionFn = deps.isNewerVersion || isNewerVersion;
502
631
  const detectPackageManagerFn = deps.detectPackageManager || detectPackageManager;
@@ -539,7 +668,11 @@ export function startDaemon(config = {}, deps = {}) {
539
668
  const mkdirSyncFn = deps.mkdirSync || fs.mkdirSync;
540
669
  const writeFileSyncFn = deps.writeFileSync || fs.writeFileSync;
541
670
  const existsSyncFn = deps.existsSync || fs.existsSync;
671
+ const statSyncFn = deps.statSync || fs.statSync;
672
+ const lstatSyncFn = deps.lstatSync || fs.lstatSync;
542
673
  const readFileSyncFn = deps.readFileSync || fs.readFileSync;
674
+ const readlinkSyncFn = deps.readlinkSync || fs.readlinkSync;
675
+ const symlinkSyncFn = deps.symlinkSync || fs.symlinkSync;
543
676
  const unlinkSyncFn = deps.unlinkSync || fs.unlinkSync;
544
677
  const renameSyncFn = deps.renameSync || fs.renameSync;
545
678
  const createWriteStreamFn = deps.createWriteStream || fs.createWriteStream;
@@ -550,6 +683,263 @@ export function startDaemon(config = {}, deps = {}) {
550
683
  deps.createWebSocketClient ||
551
684
  ((clientConfig, options) => new ConductorWebSocketClient(clientConfig, options));
552
685
  const createLogCollector = deps.createLogCollector || ((backendUrl) => new DaemonLogCollector(backendUrl));
686
+ const resolveProjectSnapshotFn =
687
+ deps.resolveProjectSnapshot || ((projectPath) => new ProjectContext(projectPath).snapshot());
688
+
689
+ function buildTaskWorktreeRoot(projectWorkspacePath, worktreeId) {
690
+ const sanitized = String(worktreeId).replace(/[/\\]/g, "_").replace(/\.\./g, "_");
691
+ return path.join(projectWorkspacePath, ".conductor", "worktrees", sanitized);
692
+ }
693
+
694
+ function resolveTaskWorktreeCwd(worktreeRoot, projectRelativePath) {
695
+ return projectRelativePath && projectRelativePath !== "."
696
+ ? path.join(worktreeRoot, projectRelativePath)
697
+ : worktreeRoot;
698
+ }
699
+
700
+ function normalizeConfiguredPathList(value, projectWorkspacePath = "") {
701
+ const rawList = typeof value === "string"
702
+ ? [value]
703
+ : Array.isArray(value)
704
+ ? value
705
+ : [];
706
+ const deduped = [];
707
+ for (const entry of rawList) {
708
+ const normalizedEntry = normalizeOptionalString(entry);
709
+ if (!normalizedEntry) continue;
710
+ const exactConfiguredPathExists =
711
+ projectWorkspacePath &&
712
+ existsSyncFn(path.resolve(projectWorkspacePath, normalizedEntry));
713
+ const normalizedEntries =
714
+ !exactConfiguredPathExists && /\s/.test(normalizedEntry)
715
+ ? normalizedEntry.split(/\s+/).map((part) => normalizeOptionalString(part)).filter(Boolean)
716
+ : [normalizedEntry];
717
+ for (const candidate of normalizedEntries) {
718
+ if (deduped.includes(candidate)) {
719
+ continue;
720
+ }
721
+ deduped.push(candidate);
722
+ }
723
+ }
724
+ return deduped;
725
+ }
726
+
727
+ function resolveProjectScopedPath(basePath, configuredPath, label) {
728
+ const resolvedPath = path.resolve(basePath, configuredPath);
729
+ const relativePath = path.relative(basePath, resolvedPath);
730
+ if (
731
+ relativePath === "" ||
732
+ relativePath === "." ||
733
+ relativePath.startsWith("..") ||
734
+ path.isAbsolute(relativePath)
735
+ ) {
736
+ throw new Error(`${label} must stay within ${basePath}`);
737
+ }
738
+ return resolvedPath;
739
+ }
740
+
741
+ function readProjectWorktreeSettings(projectWorkspacePath) {
742
+ const settingsCandidates = [
743
+ path.join(projectWorkspacePath, ".conductor", "settings.yaml"),
744
+ path.join(projectWorkspacePath, ".conductor", "settings.yml"),
745
+ path.join(projectWorkspacePath, ".conductor", "setttings.yaml"),
746
+ path.join(projectWorkspacePath, ".conductor", "setttings.yml"),
747
+ ];
748
+
749
+ for (const settingsPath of settingsCandidates) {
750
+ if (!existsSyncFn(settingsPath)) {
751
+ continue;
752
+ }
753
+ try {
754
+ const parsed = yaml.load(readFileSyncFn(settingsPath, "utf8"));
755
+ const worktreeSettings =
756
+ parsed && typeof parsed === "object" && !Array.isArray(parsed) &&
757
+ parsed.worktree && typeof parsed.worktree === "object" && !Array.isArray(parsed.worktree)
758
+ ? parsed.worktree
759
+ : {};
760
+ return {
761
+ symlinkPaths: normalizeConfiguredPathList(worktreeSettings.symlink, projectWorkspacePath),
762
+ settingsPath,
763
+ };
764
+ } catch (error) {
765
+ throw new Error(`Failed to read ${settingsPath}: ${error?.message || error}`);
766
+ }
767
+ }
768
+
769
+ return {
770
+ symlinkPaths: [],
771
+ settingsPath: null,
772
+ };
773
+ }
774
+
775
+ function ensureTaskWorktreeSymlinks({ projectWorkspacePath, finalCwd }) {
776
+ const { symlinkPaths } = readProjectWorktreeSettings(projectWorkspacePath);
777
+ for (const configuredPath of symlinkPaths) {
778
+ const sourcePath = resolveProjectScopedPath(
779
+ projectWorkspacePath,
780
+ configuredPath,
781
+ `worktree.symlink entry ${configuredPath}`,
782
+ );
783
+ const linkPath = resolveProjectScopedPath(
784
+ finalCwd,
785
+ configuredPath,
786
+ `worktree.symlink destination ${configuredPath}`,
787
+ );
788
+ mkdirSyncFn(path.dirname(linkPath), { recursive: true });
789
+
790
+ if (existsSyncFn(linkPath)) {
791
+ try {
792
+ const stat = lstatSyncFn(linkPath);
793
+ if (!stat.isSymbolicLink()) {
794
+ throw new Error(`worktree symlink destination already exists: ${linkPath}`);
795
+ }
796
+ const currentTarget = readlinkSyncFn(linkPath);
797
+ const currentResolvedTarget = path.resolve(path.dirname(linkPath), currentTarget);
798
+ if (currentResolvedTarget === sourcePath) {
799
+ continue;
800
+ }
801
+ throw new Error(`worktree symlink destination already points elsewhere: ${linkPath}`);
802
+ } catch (error) {
803
+ throw error instanceof Error ? error : new Error(String(error));
804
+ }
805
+ }
806
+
807
+ const relativeTarget = path.relative(path.dirname(linkPath), sourcePath) || ".";
808
+ symlinkSyncFn(relativeTarget, linkPath);
809
+ }
810
+ }
811
+
812
+ async function runSpawnProcess(command, args, options = {}) {
813
+ let child;
814
+ try {
815
+ child = spawnFn(command, args, {
816
+ ...options,
817
+ stdio: ["ignore", "pipe", "pipe"],
818
+ });
819
+ } catch (error) {
820
+ throw error instanceof Error ? error : new Error(String(error));
821
+ }
822
+
823
+ return await new Promise((resolve, reject) => {
824
+ let stdout = "";
825
+ let stderr = "";
826
+ let settled = false;
827
+
828
+ const finishResolve = () => {
829
+ if (settled) {
830
+ return;
831
+ }
832
+ settled = true;
833
+ resolve({ stdout, stderr });
834
+ };
835
+
836
+ const finishReject = (error) => {
837
+ if (settled) {
838
+ return;
839
+ }
840
+ settled = true;
841
+ reject(error instanceof Error ? error : new Error(String(error)));
842
+ };
843
+
844
+ if (child.stdout && typeof child.stdout.on === "function") {
845
+ child.stdout.on("data", (chunk) => {
846
+ stdout += String(chunk ?? "");
847
+ });
848
+ }
849
+ if (child.stderr && typeof child.stderr.on === "function") {
850
+ child.stderr.on("data", (chunk) => {
851
+ stderr += String(chunk ?? "");
852
+ });
853
+ }
854
+ if (typeof child.on === "function") {
855
+ child.on("error", (error) => {
856
+ finishReject(error);
857
+ });
858
+ child.on("close", (code, signal) => {
859
+ if (code === 0) {
860
+ finishResolve();
861
+ return;
862
+ }
863
+ const detail = (stderr || stdout).trim();
864
+ finishReject(
865
+ new Error(
866
+ `${command} ${args.join(" ")} failed` +
867
+ (signal ? ` (signal ${signal})` : ` (exit ${code ?? "unknown"})`) +
868
+ (detail ? `: ${detail}` : ""),
869
+ ),
870
+ );
871
+ });
872
+ return;
873
+ }
874
+ finishResolve();
875
+ });
876
+ }
877
+
878
+ async function ensureTaskWorktree({ taskId, projectId, launchConfig }) {
879
+ const worktreeConfig = parseTaskWorktreeLaunchConfig(launchConfig);
880
+ if (!worktreeConfig) {
881
+ return null;
882
+ }
883
+
884
+ const worktreeRoot = buildTaskWorktreeRoot(
885
+ worktreeConfig.projectWorkspacePath,
886
+ worktreeConfig.worktreeId,
887
+ );
888
+ const finalCwd = resolveTaskWorktreeCwd(worktreeRoot, worktreeConfig.projectRelativePath);
889
+ const gitMarkerPath = path.join(worktreeRoot, ".git");
890
+ if (!existsSyncFn(gitMarkerPath)) {
891
+ mkdirSyncFn(path.dirname(worktreeRoot), { recursive: true });
892
+ try {
893
+ await runSpawnProcess(
894
+ "git",
895
+ [
896
+ "-C",
897
+ worktreeConfig.projectRepoRoot,
898
+ "worktree",
899
+ "add",
900
+ "-b",
901
+ worktreeConfig.worktreeBranch,
902
+ worktreeRoot,
903
+ worktreeConfig.worktreeBaseRef,
904
+ ],
905
+ {
906
+ cwd: worktreeConfig.projectRepoRoot,
907
+ },
908
+ );
909
+ } catch (primaryError) {
910
+ if (!existsSyncFn(gitMarkerPath)) {
911
+ try {
912
+ await runSpawnProcess(
913
+ "git",
914
+ [
915
+ "-C",
916
+ worktreeConfig.projectRepoRoot,
917
+ "worktree",
918
+ "add",
919
+ worktreeRoot,
920
+ worktreeConfig.worktreeBranch,
921
+ ],
922
+ {
923
+ cwd: worktreeConfig.projectRepoRoot,
924
+ },
925
+ );
926
+ } catch (reuseError) {
927
+ throw new Error(
928
+ `Failed to prepare git worktree for ${taskId}: ${reuseError?.message || primaryError?.message || primaryError}`,
929
+ );
930
+ }
931
+ }
932
+ }
933
+ }
934
+
935
+ mkdirSyncFn(finalCwd, { recursive: true });
936
+ ensureTaskWorktreeSymlinks({
937
+ projectWorkspacePath: worktreeConfig.projectWorkspacePath,
938
+ finalCwd,
939
+ });
940
+ return finalCwd;
941
+ }
942
+
553
943
  const RTC_MODULE_CANDIDATES = resolveRtcModuleCandidates(process.env.CONDUCTOR_PTY_RTC_MODULES);
554
944
  const RTC_DIRECT_DISABLED = parseBooleanEnv(process.env.CONDUCTOR_DISABLE_PTY_DIRECT_RTC);
555
945
  const PROJECT_PATH_LOOKUP_TIMEOUT_MS = parsePositiveInt(
@@ -560,6 +950,18 @@ export function startDaemon(config = {}, deps = {}) {
560
950
  process.env.CONDUCTOR_STOP_FORCE_KILL_TIMEOUT_MS,
561
951
  5000,
562
952
  );
953
+ const DAEMON_FORCE_STOP_GRACE_MS = parsePositiveInt(
954
+ process.env.CONDUCTOR_DAEMON_FORCE_STOP_GRACE_MS,
955
+ 15_000,
956
+ );
957
+ const DAEMON_FORCE_STOP_POLL_INTERVAL_MS = parsePositiveInt(
958
+ process.env.CONDUCTOR_DAEMON_FORCE_STOP_POLL_INTERVAL_MS,
959
+ 100,
960
+ );
961
+ const DAEMON_FORCE_KILL_WAIT_MS = parsePositiveInt(
962
+ process.env.CONDUCTOR_DAEMON_FORCE_KILL_WAIT_MS,
963
+ 2_000,
964
+ );
563
965
  const SHUTDOWN_STATUS_REPORT_TIMEOUT_MS = parsePositiveInt(
564
966
  process.env.CONDUCTOR_SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
565
967
  1000,
@@ -648,6 +1050,20 @@ export function startDaemon(config = {}, deps = {}) {
648
1050
  return true;
649
1051
  };
650
1052
 
1053
+ const waitForProcessExitSync = (pid, timeoutMs) => {
1054
+ const deadline = Date.now() + timeoutMs;
1055
+ while (true) {
1056
+ if (!isProcessAlive(pid)) {
1057
+ return true;
1058
+ }
1059
+ const remainingMs = deadline - Date.now();
1060
+ if (remainingMs <= 0) {
1061
+ return false;
1062
+ }
1063
+ sleepSync(Math.min(DAEMON_FORCE_STOP_POLL_INTERVAL_MS, remainingMs));
1064
+ }
1065
+ };
1066
+
651
1067
  try {
652
1068
  mkdirSyncFn(WORKSPACE_ROOT, { recursive: true });
653
1069
  } catch (err) {
@@ -670,17 +1086,35 @@ export function startDaemon(config = {}, deps = {}) {
670
1086
  if (alive) {
671
1087
  if (config.FORCE) {
672
1088
  log(`Force enabled: stopping existing daemon PID ${pid}`);
1089
+ let alreadyExited = false;
673
1090
  try {
674
1091
  killFn(pid, "SIGTERM");
675
1092
  } catch (killErr) {
676
- if (!killErr || killErr.code !== "ESRCH") {
1093
+ if (killErr?.code === "ESRCH") {
1094
+ alreadyExited = true;
1095
+ } else {
677
1096
  logError(`Failed to stop existing daemon PID ${pid}: ${killErr.message}`);
678
1097
  return exitAndReturn(1);
679
1098
  }
680
1099
  }
681
1100
  try {
682
- if (isProcessAlive(pid)) {
683
- logError(`Existing daemon PID ${pid} is still running; please stop it manually.`);
1101
+ let exited = alreadyExited || waitForProcessExitSync(pid, DAEMON_FORCE_STOP_GRACE_MS);
1102
+ if (!exited) {
1103
+ log(
1104
+ `Existing daemon PID ${pid} did not exit within ${DAEMON_FORCE_STOP_GRACE_MS}ms; sending SIGKILL`,
1105
+ );
1106
+ try {
1107
+ killFn(pid, "SIGKILL");
1108
+ } catch (killErr) {
1109
+ if (killErr?.code !== "ESRCH") {
1110
+ logError(`Failed to force kill existing daemon PID ${pid}: ${killErr.message}`);
1111
+ return exitAndReturn(1);
1112
+ }
1113
+ }
1114
+ exited = waitForProcessExitSync(pid, DAEMON_FORCE_KILL_WAIT_MS);
1115
+ }
1116
+ if (!exited) {
1117
+ logError(`Existing daemon PID ${pid} is still running after force restart; please stop it manually.`);
684
1118
  return exitAndReturn(1);
685
1119
  }
686
1120
  } catch (checkErr) {
@@ -688,7 +1122,9 @@ export function startDaemon(config = {}, deps = {}) {
688
1122
  return exitAndReturn(1);
689
1123
  }
690
1124
  log("Removing lock file after force stop");
691
- unlinkSyncFn(LOCK_FILE);
1125
+ if (existsSyncFn(LOCK_FILE)) {
1126
+ unlinkSyncFn(LOCK_FILE);
1127
+ }
692
1128
  } else {
693
1129
  logError(`Daemon already running with PID ${pid}`);
694
1130
  return exitAndReturn(1);
@@ -878,8 +1314,12 @@ export function startDaemon(config = {}, deps = {}) {
878
1314
  "x-conductor-backends": SUPPORTED_BACKENDS.join(","),
879
1315
  "x-conductor-version": cliVersion,
880
1316
  };
1317
+ const advertisedCapabilities = ["project_path_validation"];
881
1318
  if (ptyTaskCapabilityEnabled) {
882
- extraHeaders["x-conductor-capabilities"] = "pty_task,terminal_snapshot";
1319
+ advertisedCapabilities.push("pty_task", "terminal_snapshot");
1320
+ }
1321
+ if (advertisedCapabilities.length > 0) {
1322
+ extraHeaders["x-conductor-capabilities"] = advertisedCapabilities.join(",");
883
1323
  }
884
1324
  const client = createWebSocketClient(sdkConfig, {
885
1325
  extraHeaders,
@@ -937,14 +1377,31 @@ export function startDaemon(config = {}, deps = {}) {
937
1377
 
938
1378
  void (async () => {
939
1379
  try {
940
- const runtimeBackends = await listRuntimeSupportedBackends({ configFilePath: config.CONFIG_FILE });
941
- const externalBackends = runtimeBackends.filter((backend) => !RUNTIME_SUPPORTED_BACKENDS.includes(backend));
942
- SUPPORTED_BACKENDS = [...new Set([...Object.keys(ALLOW_CLI_LIST), ...externalBackends])];
1380
+ ALLOW_CLI_LIST = await filterRuntimeSupportedAllowCliList(RAW_ALLOW_CLI_LIST, {
1381
+ configFilePath: config.CONFIG_FILE,
1382
+ });
943
1383
  } catch (error) {
944
- logError(`Failed to discover external backends: ${error?.message || error}`);
1384
+ ALLOW_CLI_LIST = {};
1385
+ logError(`Failed to filter configured backends: ${error?.message || error}`);
1386
+ }
1387
+
1388
+ try {
1389
+ const advertisedBackends = await listAdvertisedBackends(ALLOW_CLI_LIST, {
1390
+ configFilePath: config.CONFIG_FILE,
1391
+ });
1392
+ SUPPORTED_BACKENDS = advertisedBackends.supportedBackends;
1393
+ SUPPORTED_BACKEND_RUNTIME_MAP = advertisedBackends.runtimeBackendMap;
1394
+ if (advertisedBackends.discoveryError) {
1395
+ logError(`Failed to discover external backends: ${advertisedBackends.discoveryError?.message || advertisedBackends.discoveryError}`);
1396
+ }
1397
+ } catch (error) {
1398
+ SUPPORTED_BACKENDS = [];
1399
+ SUPPORTED_BACKEND_RUNTIME_MAP = {};
1400
+ logError(`Failed to resolve advertised backends: ${error?.message || error}`);
945
1401
  }
946
1402
 
947
1403
  extraHeaders["x-conductor-backends"] = SUPPORTED_BACKENDS.join(",");
1404
+ extraHeaders["x-conductor-backend-runtime-map"] = serializeRuntimeBackendMap(SUPPORTED_BACKEND_RUNTIME_MAP);
948
1405
  if (typeof client?.setExtraHeaders === "function") {
949
1406
  client.setExtraHeaders(extraHeaders);
950
1407
  }
@@ -1581,6 +2038,30 @@ export function startDaemon(config = {}, deps = {}) {
1581
2038
  });
1582
2039
  }
1583
2040
 
2041
+ function reportTaskWorktreeCleanupResult({
2042
+ requestId,
2043
+ taskId,
2044
+ worktreeBranch = null,
2045
+ removedPath = null,
2046
+ cleaned = true,
2047
+ error = null,
2048
+ }) {
2049
+ if (!requestId || !taskId) return Promise.resolve();
2050
+ return client.sendJson({
2051
+ type: "task_worktree_cleanup_result",
2052
+ payload: {
2053
+ request_id: String(requestId),
2054
+ task_id: String(taskId),
2055
+ daemon_host: AGENT_NAME || os.hostname(),
2056
+ worktree_branch: worktreeBranch || undefined,
2057
+ removed_path: removedPath || undefined,
2058
+ cleaned: Boolean(cleaned),
2059
+ error: error ? String(error) : null,
2060
+ cleaned_at: new Date().toISOString(),
2061
+ },
2062
+ });
2063
+ }
2064
+
1584
2065
  function sendPtyTransportStatus(payload) {
1585
2066
  return client.sendJson({
1586
2067
  type: "pty_transport_status",
@@ -2202,7 +2683,9 @@ export function startDaemon(config = {}, deps = {}) {
2202
2683
  rejectCreatePtyTaskDuringShutdown(payload);
2203
2684
  return;
2204
2685
  }
2205
- let taskDir = normalizeOptionalString(launchConfig.cwd) || boundPath;
2686
+ let taskDir =
2687
+ normalizeOptionalString(launchConfig.cwd) ||
2688
+ boundPath;
2206
2689
  if (!taskDir) {
2207
2690
  const now = new Date();
2208
2691
  const dayDir = path.join(WORKSPACE_ROOT, formatWorkspaceDate(now));
@@ -2255,22 +2738,7 @@ export function startDaemon(config = {}, deps = {}) {
2255
2738
  logError(`Failed to report agent_command_ack(create_pty_task) for ${taskId}: ${err?.message || err}`);
2256
2739
  });
2257
2740
 
2258
- const env = {
2259
- ...process.env,
2260
- ...launchSpec.env,
2261
- CONDUCTOR_PROJECT_ID: projectId,
2262
- CONDUCTOR_TASK_ID: taskId,
2263
- CONDUCTOR_PTY_SESSION_ID: ptySessionId,
2264
- };
2265
- if (config.CONFIG_FILE) {
2266
- env.CONDUCTOR_CONFIG = config.CONFIG_FILE;
2267
- }
2268
- if (AGENT_TOKEN) {
2269
- env.CONDUCTOR_AGENT_TOKEN = AGENT_TOKEN;
2270
- }
2271
- if (BACKEND_HTTP) {
2272
- env.CONDUCTOR_BACKEND_URL = BACKEND_HTTP;
2273
- }
2741
+ const env = buildPtyTaskEnv(process.env, launchSpec.env);
2274
2742
 
2275
2743
  const logPath = path.join(launchSpec.cwd, "conductor-terminal.log");
2276
2744
  let logStream;
@@ -2360,6 +2828,119 @@ export function startDaemon(config = {}, deps = {}) {
2360
2828
  }
2361
2829
  }
2362
2830
 
2831
+ async function handleCleanupTaskWorktree(payload) {
2832
+ const taskId = payload?.task_id ? String(payload.task_id) : "";
2833
+ const projectId = payload?.project_id ? String(payload.project_id) : "";
2834
+ const requestId = payload?.request_id ? String(payload.request_id) : "";
2835
+ const forceCleanup = payload?.force === true;
2836
+ const launchConfig = normalizeLaunchConfig(payload?.launch_config);
2837
+
2838
+ if (!taskId || !projectId || !requestId) {
2839
+ logError(`Invalid cleanup_task_worktree payload: ${JSON.stringify(payload)}`);
2840
+ sendAgentCommandAck({
2841
+ requestId,
2842
+ taskId,
2843
+ eventType: "cleanup_task_worktree",
2844
+ accepted: false,
2845
+ }).catch(() => {});
2846
+ return;
2847
+ }
2848
+
2849
+ const worktreeConfig = parseTaskWorktreeLaunchConfig(launchConfig);
2850
+ if (!worktreeConfig) {
2851
+ sendAgentCommandAck({
2852
+ requestId,
2853
+ taskId,
2854
+ eventType: "cleanup_task_worktree",
2855
+ accepted: false,
2856
+ }).catch(() => {});
2857
+ await reportTaskWorktreeCleanupResult({
2858
+ requestId,
2859
+ taskId,
2860
+ cleaned: false,
2861
+ error: "Task does not use an isolated worktree",
2862
+ }).catch((error) => {
2863
+ logError(`Failed to report task_worktree_cleanup_result for ${taskId}: ${error?.message || error}`);
2864
+ });
2865
+ return;
2866
+ }
2867
+
2868
+ sendAgentCommandAck({
2869
+ requestId,
2870
+ taskId,
2871
+ eventType: "cleanup_task_worktree",
2872
+ accepted: true,
2873
+ }).catch((error) => {
2874
+ logError(`Failed to report agent_command_ack(cleanup_task_worktree) for ${taskId}: ${error?.message || error}`);
2875
+ });
2876
+
2877
+ if (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId)) {
2878
+ await reportTaskWorktreeCleanupResult({
2879
+ requestId,
2880
+ taskId,
2881
+ worktreeBranch: worktreeConfig.worktreeBranch,
2882
+ cleaned: false,
2883
+ error: "Task is still active",
2884
+ }).catch((error) => {
2885
+ logError(`Failed to report task_worktree_cleanup_result for ${taskId}: ${error?.message || error}`);
2886
+ });
2887
+ return;
2888
+ }
2889
+
2890
+ const worktreeRoot = buildTaskWorktreeRoot(
2891
+ worktreeConfig.projectWorkspacePath,
2892
+ worktreeConfig.worktreeId,
2893
+ );
2894
+ const worktreeCwd = resolveTaskWorktreeCwd(worktreeRoot, worktreeConfig.projectRelativePath);
2895
+ const statusCwd = existsSyncFn(worktreeCwd) ? worktreeCwd : worktreeRoot;
2896
+
2897
+ try {
2898
+ if (existsSyncFn(worktreeRoot)) {
2899
+ if (!forceCleanup) {
2900
+ const { stdout } = await runSpawnProcess(
2901
+ "git",
2902
+ ["-C", statusCwd, "status", "--porcelain"],
2903
+ { cwd: statusCwd },
2904
+ );
2905
+ if (stdout.trim()) {
2906
+ throw new Error("Worktree has uncommitted changes");
2907
+ }
2908
+ }
2909
+ await runSpawnProcess(
2910
+ "git",
2911
+ [
2912
+ "-C",
2913
+ worktreeConfig.projectRepoRoot,
2914
+ "worktree",
2915
+ "remove",
2916
+ ...(forceCleanup ? ["--force"] : []),
2917
+ worktreeRoot,
2918
+ ],
2919
+ { cwd: worktreeConfig.projectRepoRoot },
2920
+ );
2921
+ }
2922
+
2923
+ await reportTaskWorktreeCleanupResult({
2924
+ requestId,
2925
+ taskId,
2926
+ worktreeBranch: worktreeConfig.worktreeBranch,
2927
+ removedPath: worktreeRoot,
2928
+ cleaned: true,
2929
+ });
2930
+ } catch (error) {
2931
+ await reportTaskWorktreeCleanupResult({
2932
+ requestId,
2933
+ taskId,
2934
+ worktreeBranch: worktreeConfig.worktreeBranch,
2935
+ removedPath: worktreeRoot,
2936
+ cleaned: false,
2937
+ error: error instanceof Error ? error.message : String(error),
2938
+ }).catch((reportError) => {
2939
+ logError(`Failed to report task_worktree_cleanup_result for ${taskId}: ${reportError?.message || reportError}`);
2940
+ });
2941
+ }
2942
+ }
2943
+
2363
2944
  async function handleTerminalAttach(payload) {
2364
2945
  const taskId = payload?.task_id ? String(payload.task_id) : "";
2365
2946
  if (!taskId) return;
@@ -2610,6 +3191,23 @@ export function startDaemon(config = {}, deps = {}) {
2610
3191
  rejectCreatePtyTaskDuringShutdown(event.payload);
2611
3192
  return;
2612
3193
  }
3194
+ if (event.type === "cleanup_task_worktree") {
3195
+ const requestId = event?.payload?.request_id ? String(event.payload.request_id) : "";
3196
+ const taskId = event?.payload?.task_id ? String(event.payload.task_id) : "";
3197
+ sendAgentCommandAck({
3198
+ requestId,
3199
+ taskId,
3200
+ eventType: "cleanup_task_worktree",
3201
+ accepted: false,
3202
+ }).catch(() => {});
3203
+ void reportTaskWorktreeCleanupResult({
3204
+ requestId,
3205
+ taskId,
3206
+ cleaned: false,
3207
+ error: "daemon shutting down",
3208
+ });
3209
+ return;
3210
+ }
2613
3211
  }
2614
3212
 
2615
3213
  if (event.type === "create_task") {
@@ -2626,6 +3224,10 @@ export function startDaemon(config = {}, deps = {}) {
2626
3224
  void handleCreatePtyTask(event.payload);
2627
3225
  return;
2628
3226
  }
3227
+ if (event.type === "cleanup_task_worktree") {
3228
+ void handleCleanupTaskWorktree(event.payload);
3229
+ return;
3230
+ }
2629
3231
  if (event.type === "stop_task") {
2630
3232
  handleStopTask(event.payload);
2631
3233
  return;
@@ -2652,6 +3254,10 @@ export function startDaemon(config = {}, deps = {}) {
2652
3254
  }
2653
3255
  if (event.type === "collect_logs") {
2654
3256
  void handleCollectLogs(event.payload);
3257
+ return;
3258
+ }
3259
+ if (event.type === "validate_project_path") {
3260
+ void handleValidateProjectPath(event.payload);
2655
3261
  }
2656
3262
  }
2657
3263
 
@@ -2724,6 +3330,96 @@ export function startDaemon(config = {}, deps = {}) {
2724
3330
  }
2725
3331
  }
2726
3332
 
3333
+ async function handleValidateProjectPath(payload) {
3334
+ const requestId = payload?.request_id ? String(payload.request_id).trim() : "";
3335
+ const rawWorkspacePath = payload?.workspace_path ? String(payload.workspace_path).trim() : "";
3336
+ const validatedAt = new Date().toISOString();
3337
+
3338
+ if (!requestId || !rawWorkspacePath) {
3339
+ logError(`Invalid validate_project_path payload: ${JSON.stringify(payload)}`);
3340
+ return;
3341
+ }
3342
+
3343
+ let result = {
3344
+ workspacePath: null,
3345
+ repoRoot: null,
3346
+ worktreeBranch: null,
3347
+ lastCommit: null,
3348
+ fileCount: null,
3349
+ error: null,
3350
+ errorCode: null,
3351
+ validatedAt,
3352
+ };
3353
+
3354
+ try {
3355
+ const resolvedPath = path.resolve(rawWorkspacePath);
3356
+ if (!existsSyncFn(resolvedPath)) {
3357
+ result = {
3358
+ ...result,
3359
+ error: `Workspace path does not exist on daemon ${AGENT_NAME}: ${rawWorkspacePath}`,
3360
+ errorCode: "workspace_not_found",
3361
+ };
3362
+ } else if (!statSyncFn(resolvedPath).isDirectory()) {
3363
+ result = {
3364
+ ...result,
3365
+ error: `Workspace path is not a directory on daemon ${AGENT_NAME}: ${rawWorkspacePath}`,
3366
+ errorCode: "workspace_not_directory",
3367
+ };
3368
+ } else {
3369
+ const snapshot = await Promise.resolve(resolveProjectSnapshotFn(resolvedPath));
3370
+ result = {
3371
+ ...result,
3372
+ workspacePath:
3373
+ typeof snapshot?.projectRoot === "string" && snapshot.projectRoot.trim()
3374
+ ? snapshot.projectRoot.trim()
3375
+ : resolvedPath,
3376
+ repoRoot:
3377
+ typeof snapshot?.repoRoot === "string" && snapshot.repoRoot.trim()
3378
+ ? snapshot.repoRoot.trim()
3379
+ : null,
3380
+ worktreeBranch:
3381
+ typeof snapshot?.worktreeBranch === "string" && snapshot.worktreeBranch.trim()
3382
+ ? snapshot.worktreeBranch.trim()
3383
+ : null,
3384
+ lastCommit:
3385
+ typeof snapshot?.lastCommit === "string" && snapshot.lastCommit.trim()
3386
+ ? snapshot.lastCommit.trim()
3387
+ : null,
3388
+ fileCount:
3389
+ typeof snapshot?.fileCount === "number" && Number.isInteger(snapshot.fileCount)
3390
+ ? snapshot.fileCount
3391
+ : null,
3392
+ };
3393
+ }
3394
+ } catch (error) {
3395
+ result = {
3396
+ ...result,
3397
+ error: `Failed to validate workspace path on daemon ${AGENT_NAME}: ${error?.message || error}`,
3398
+ errorCode: "workspace_validation_failed",
3399
+ };
3400
+ }
3401
+
3402
+ try {
3403
+ await client.sendJson({
3404
+ type: "project_path_validated",
3405
+ payload: {
3406
+ request_id: requestId,
3407
+ daemon_host: AGENT_NAME,
3408
+ workspace_path: result.workspacePath,
3409
+ repo_root: result.repoRoot,
3410
+ worktree_branch: result.worktreeBranch,
3411
+ last_commit: result.lastCommit,
3412
+ file_count: result.fileCount,
3413
+ error: result.error,
3414
+ error_code: result.errorCode,
3415
+ validated_at: result.validatedAt,
3416
+ },
3417
+ });
3418
+ } catch (error) {
3419
+ logError(`Failed to report project_path_validated for ${rawWorkspacePath}: ${error?.message || error}`);
3420
+ }
3421
+ }
3422
+
2727
3423
  function handleStopTask(payload) {
2728
3424
  const taskId = payload?.task_id;
2729
3425
  if (!taskId) return;
@@ -2852,6 +3548,20 @@ export function startDaemon(config = {}, deps = {}) {
2852
3548
  return null;
2853
3549
  }
2854
3550
  const project = await response.json();
3551
+ const daemonHost =
3552
+ (typeof project.daemon_host === "string" && project.daemon_host.trim()) ||
3553
+ (typeof project.daemonHost === "string" && project.daemonHost.trim()) ||
3554
+ "";
3555
+ const workspacePath =
3556
+ (typeof project.workspace_path === "string" && project.workspace_path.trim()) ||
3557
+ (typeof project.workspacePath === "string" && project.workspacePath.trim()) ||
3558
+ "";
3559
+ if (workspacePath) {
3560
+ if (daemonHost && daemonHost !== AGENT_NAME) {
3561
+ return null;
3562
+ }
3563
+ return workspacePath;
3564
+ }
2855
3565
  if (!project.metadata) {
2856
3566
  return null;
2857
3567
  }
@@ -2974,8 +3684,10 @@ export function startDaemon(config = {}, deps = {}) {
2974
3684
  }
2975
3685
 
2976
3686
  async function resolveRestartCwd({
3687
+ taskId,
2977
3688
  projectId,
2978
3689
  preferredCwd = "",
3690
+ launchConfig = null,
2979
3691
  backendType,
2980
3692
  sessionId,
2981
3693
  sourceSessionFilePath = "",
@@ -2985,6 +3697,15 @@ export function startDaemon(config = {}, deps = {}) {
2985
3697
  return normalizedPreferredCwd;
2986
3698
  }
2987
3699
 
3700
+ const worktreeCwd = await ensureTaskWorktree({
3701
+ taskId,
3702
+ projectId,
3703
+ launchConfig,
3704
+ });
3705
+ if (worktreeCwd) {
3706
+ return worktreeCwd;
3707
+ }
3708
+
2988
3709
  const boundPath = await getProjectLocalPath(projectId);
2989
3710
  if (boundPath) {
2990
3711
  return boundPath;
@@ -2994,8 +3715,13 @@ export function startDaemon(config = {}, deps = {}) {
2994
3715
  const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
2995
3716
  if (normalizedSessionId && normalizedBackend && normalizedBackend !== "opencode") {
2996
3717
  try {
3718
+ const configuredBackend = await resolveConfiguredRuntimeBackend(normalizedBackend, ALLOW_CLI_LIST, {
3719
+ configFilePath: config.CONFIG_FILE,
3720
+ });
3721
+ const resumeBackend = configuredBackend?.runtimeBackend ||
3722
+ await normalizeRuntimeBackendAlias(normalizedBackend, { configFilePath: config.CONFIG_FILE });
2997
3723
  const resumeContext = await (deps.resolveResumeContext || resolveResumeContext)(
2998
- normalizedBackend,
3724
+ resumeBackend,
2999
3725
  normalizedSessionId,
3000
3726
  {
3001
3727
  cwd: process.cwd(),
@@ -3036,6 +3762,7 @@ export function startDaemon(config = {}, deps = {}) {
3036
3762
  request_id: requestIdRaw,
3037
3763
  } =
3038
3764
  payload || {};
3765
+ const launchConfig = normalizeLaunchConfig(payload?.launch_config);
3039
3766
  const requestId = requestIdRaw ? String(requestIdRaw) : "";
3040
3767
 
3041
3768
  if (!taskId || !projectId) {
@@ -3082,11 +3809,22 @@ export function startDaemon(config = {}, deps = {}) {
3082
3809
  pendingTaskStarts.add(taskId);
3083
3810
  let acceptedAckSent = false;
3084
3811
  try {
3085
- const effectiveBackend = await normalizeRuntimeBackendAlias(backendType || SUPPORTED_BACKENDS[0], {
3812
+ const requestedBackend = normalizeRuntimeBackendName(backendType || SUPPORTED_BACKENDS[0]);
3813
+ const configuredBackend = await resolveConfiguredRuntimeBackend(requestedBackend, ALLOW_CLI_LIST, {
3086
3814
  configFilePath: config.CONFIG_FILE,
3087
3815
  });
3088
- if (!(await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE }))) {
3089
- logError(`Unsupported backend: ${effectiveBackend}. Supported: ${SUPPORTED_BACKENDS.join(", ")}`);
3816
+ const effectiveBackend = configuredBackend?.runtimeBackend ||
3817
+ await normalizeRuntimeBackendAlias(requestedBackend, { configFilePath: config.CONFIG_FILE });
3818
+ const hasConfiguredEntry = Boolean(configuredBackend?.commandLine);
3819
+ const selectedBackend = configuredBackend?.commandLine
3820
+ ? configuredBackend.requestedBackend
3821
+ : effectiveBackend;
3822
+ const isAdvertisedBackend = SUPPORTED_BACKENDS.includes(selectedBackend);
3823
+ const isAllowedExternalBackend =
3824
+ !isBuiltInRuntimeBackend(effectiveBackend) &&
3825
+ await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE });
3826
+ if (!isAdvertisedBackend || (!hasConfiguredEntry && !isAllowedExternalBackend)) {
3827
+ logError(`Unsupported backend: ${selectedBackend}. Supported: ${SUPPORTED_BACKENDS.join(", ")}`);
3090
3828
  sendAgentCommandAck({
3091
3829
  requestId,
3092
3830
  taskId,
@@ -3100,7 +3838,7 @@ export function startDaemon(config = {}, deps = {}) {
3100
3838
  task_id: taskId,
3101
3839
  project_id: projectId,
3102
3840
  status: "KILLED",
3103
- summary: `Unsupported backend: ${effectiveBackend}`,
3841
+ summary: `Unsupported backend: ${selectedBackend}`,
3104
3842
  },
3105
3843
  })
3106
3844
  .catch(() => {});
@@ -3117,10 +3855,10 @@ export function startDaemon(config = {}, deps = {}) {
3117
3855
  });
3118
3856
  acceptedAckSent = true;
3119
3857
 
3120
- const cliCommand = ALLOW_CLI_LIST[effectiveBackend] || "";
3858
+ const cliCommand = ALLOW_CLI_LIST[selectedBackend] || ALLOW_CLI_LIST[effectiveBackend] || "";
3121
3859
 
3122
3860
  log("");
3123
- log(`Creating task ${taskId} for project ${projectId} (${effectiveBackend})`);
3861
+ log(`Creating task ${taskId} for project ${projectId} (${selectedBackend})`);
3124
3862
  log(`CLI command: ${formatBackendLaunchCommand(cliCommand)}`);
3125
3863
  client
3126
3864
  .sendJson({
@@ -3145,9 +3883,18 @@ export function startDaemon(config = {}, deps = {}) {
3145
3883
  let logPath;
3146
3884
  let runTimestampPart = null;
3147
3885
 
3148
- if (boundPath) {
3149
- taskDir = boundPath;
3150
- log(`Using project bound path: ${taskDir}`);
3886
+ const resolvedTaskWorkspace =
3887
+ (await ensureTaskWorktree({
3888
+ taskId,
3889
+ projectId,
3890
+ launchConfig,
3891
+ })) ||
3892
+ normalizeOptionalString(launchConfig.cwd) ||
3893
+ boundPath;
3894
+
3895
+ if (resolvedTaskWorkspace) {
3896
+ taskDir = resolvedTaskWorkspace;
3897
+ log(`Using task workspace: ${taskDir}`);
3151
3898
  logPath = path.join(taskDir, "conductor.log");
3152
3899
  } else {
3153
3900
  const now = new Date();
@@ -3160,8 +3907,8 @@ export function startDaemon(config = {}, deps = {}) {
3160
3907
  }
3161
3908
 
3162
3909
  const args = [];
3163
- if (effectiveBackend) {
3164
- args.push("--backend", effectiveBackend);
3910
+ if (selectedBackend) {
3911
+ args.push("--backend", selectedBackend);
3165
3912
  }
3166
3913
  if (initialContent) {
3167
3914
  args.push("--prefill", initialContent);
@@ -3351,6 +4098,7 @@ export function startDaemon(config = {}, deps = {}) {
3351
4098
  const normalizedTargetTaskId = targetTaskId ? String(targetTaskId) : "";
3352
4099
  const normalizedProjectId = projectId ? String(projectId) : "";
3353
4100
  const normalizedSourceSessionId = sourceSessionId ? String(sourceSessionId).trim() : "";
4101
+ const targetLaunchConfig = normalizeLaunchConfig(payload?.target_launch_config);
3354
4102
 
3355
4103
  if (
3356
4104
  !normalizedMode ||
@@ -3405,16 +4153,27 @@ export function startDaemon(config = {}, deps = {}) {
3405
4153
  return;
3406
4154
  }
3407
4155
 
3408
- const effectiveBackend = await normalizeRuntimeBackendAlias(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0], {
4156
+ const requestedBackend = normalizeRuntimeBackendName(targetBackendType || sourceBackendType || SUPPORTED_BACKENDS[0]);
4157
+ const configuredBackend = await resolveConfiguredRuntimeBackend(requestedBackend, ALLOW_CLI_LIST, {
3409
4158
  configFilePath: config.CONFIG_FILE,
3410
4159
  });
3411
- if (!(await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE }))) {
4160
+ const effectiveBackend = configuredBackend?.runtimeBackend ||
4161
+ await normalizeRuntimeBackendAlias(requestedBackend, { configFilePath: config.CONFIG_FILE });
4162
+ const hasConfiguredEntry = Boolean(configuredBackend?.commandLine);
4163
+ const selectedBackend = configuredBackend?.commandLine
4164
+ ? configuredBackend.requestedBackend
4165
+ : effectiveBackend;
4166
+ const isAdvertisedBackend = SUPPORTED_BACKENDS.includes(selectedBackend);
4167
+ const isAllowedExternalBackend =
4168
+ !isBuiltInRuntimeBackend(effectiveBackend) &&
4169
+ await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE });
4170
+ if (!isAdvertisedBackend || (!hasConfiguredEntry && !isAllowedExternalBackend)) {
3412
4171
  reportRestartFailure({
3413
4172
  taskId: normalizedTargetTaskId,
3414
4173
  projectId: normalizedProjectId,
3415
4174
  requestId,
3416
4175
  mode: normalizedMode,
3417
- error: new Error(`Unsupported backend: ${effectiveBackend}`),
4176
+ error: new Error(`Unsupported backend: ${selectedBackend}`),
3418
4177
  });
3419
4178
  return;
3420
4179
  }
@@ -3430,7 +4189,16 @@ export function startDaemon(config = {}, deps = {}) {
3430
4189
  });
3431
4190
  return;
3432
4191
  }
3433
- if (effectiveBackend !== sourceBackendType) {
4192
+ const normalizedSourceBackend = normalizeRuntimeBackendName(sourceBackendType);
4193
+ const configuredSourceBackend = await resolveConfiguredRuntimeBackend(normalizedSourceBackend, ALLOW_CLI_LIST, {
4194
+ configFilePath: config.CONFIG_FILE,
4195
+ });
4196
+ const sourceRuntimeBackend = configuredSourceBackend?.runtimeBackend ||
4197
+ await normalizeRuntimeBackendAlias(normalizedSourceBackend, { configFilePath: config.CONFIG_FILE });
4198
+ const sourceSelectedBackend = configuredSourceBackend?.commandLine
4199
+ ? configuredSourceBackend.requestedBackend
4200
+ : sourceRuntimeBackend;
4201
+ if (selectedBackend !== sourceSelectedBackend) {
3434
4202
  reportRestartFailure({
3435
4203
  taskId: normalizedTargetTaskId,
3436
4204
  projectId: normalizedProjectId,
@@ -3451,23 +4219,32 @@ export function startDaemon(config = {}, deps = {}) {
3451
4219
  logError(`Failed to report agent_command_ack(restart_task) for ${normalizedTargetTaskId}: ${err?.message || err}`);
3452
4220
  });
3453
4221
 
4222
+ const normalizedSourceBackend = normalizeRuntimeBackendName(sourceBackendType);
4223
+ const configuredSourceBackend = await resolveConfiguredRuntimeBackend(normalizedSourceBackend, ALLOW_CLI_LIST, {
4224
+ configFilePath: config.CONFIG_FILE,
4225
+ });
4226
+ const sourceRuntimeBackend = configuredSourceBackend?.runtimeBackend ||
4227
+ await normalizeRuntimeBackendAlias(normalizedSourceBackend, { configFilePath: config.CONFIG_FILE });
4228
+
3454
4229
  let resolvedResumeSessionId = normalizedSourceSessionId;
3455
4230
  let resolvedResumeCwd = "";
3456
4231
  try {
3457
4232
  if (normalizedMode === "bridge_to_new_task" || normalizedMode === "fork_to_new_task") {
3458
4233
  const sourceResumeCwd = await resolveRestartCwd({
4234
+ taskId: normalizedTargetTaskId,
3459
4235
  projectId: normalizedProjectId,
3460
4236
  backendType: sourceBackendType,
4237
+ launchConfig: targetLaunchConfig,
3461
4238
  sessionId: normalizedSourceSessionId,
3462
4239
  sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
3463
4240
  });
3464
4241
  const bridgeSession = await getBridgeSessionHelper();
3465
4242
  const bridgeResult = await bridgeSession({
3466
- sourceTool: sourceBackendType,
4243
+ sourceTool: sourceRuntimeBackend,
3467
4244
  sourceSessionId: normalizedSourceSessionId,
3468
4245
  sourceSessionPath: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
3469
4246
  sourceSessionInfo: {
3470
- tool: sourceBackendType,
4247
+ tool: sourceRuntimeBackend,
3471
4248
  sessionId: normalizedSourceSessionId,
3472
4249
  path: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
3473
4250
  cwd: sourceResumeCwd || undefined,
@@ -3477,15 +4254,19 @@ export function startDaemon(config = {}, deps = {}) {
3477
4254
  });
3478
4255
  resolvedResumeSessionId = bridgeResult.sessionId;
3479
4256
  resolvedResumeCwd = await resolveRestartCwd({
4257
+ taskId: normalizedTargetTaskId,
3480
4258
  projectId: normalizedProjectId,
3481
4259
  preferredCwd: bridgeResult.cwd,
4260
+ launchConfig: targetLaunchConfig,
3482
4261
  backendType: effectiveBackend,
3483
4262
  sessionId: bridgeResult.sessionId,
3484
4263
  sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
3485
4264
  });
3486
4265
  } else if (normalizedMode === "resume_inplace") {
3487
4266
  resolvedResumeCwd = await resolveRestartCwd({
4267
+ taskId: normalizedTargetTaskId,
3488
4268
  projectId: normalizedProjectId,
4269
+ launchConfig: targetLaunchConfig,
3489
4270
  backendType: effectiveBackend,
3490
4271
  sessionId: normalizedSourceSessionId,
3491
4272
  sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
@@ -3517,11 +4298,11 @@ export function startDaemon(config = {}, deps = {}) {
3517
4298
  return;
3518
4299
  }
3519
4300
 
3520
- const cliCommand = ALLOW_CLI_LIST[effectiveBackend] || "";
4301
+ const cliCommand = ALLOW_CLI_LIST[selectedBackend] || ALLOW_CLI_LIST[effectiveBackend] || "";
3521
4302
 
3522
4303
  log("");
3523
4304
  log(
3524
- `Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${effectiveBackend})`,
4305
+ `Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${selectedBackend})`,
3525
4306
  );
3526
4307
  log(`CLI command: ${formatBackendLaunchCommand(cliCommand)}`);
3527
4308
 
@@ -3570,8 +4351,8 @@ export function startDaemon(config = {}, deps = {}) {
3570
4351
  }
3571
4352
 
3572
4353
  const args = [];
3573
- if (effectiveBackend) {
3574
- args.push("--backend", effectiveBackend);
4354
+ if (selectedBackend) {
4355
+ args.push("--backend", selectedBackend);
3575
4356
  }
3576
4357
  args.push("--resume", resolvedResumeSessionId);
3577
4358
  args.push("--");