@love-moon/conductor-cli 0.2.32 → 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";
@@ -207,14 +215,12 @@ function filterConfiguredAllowCliList(allowCliList) {
207
215
  if (!allowCliList || typeof allowCliList !== "object") {
208
216
  return {};
209
217
  }
210
- const builtInBackends = new Set(RUNTIME_SUPPORTED_BACKENDS);
211
218
  const filtered = {};
212
219
  for (const [backend, command] of Object.entries(allowCliList)) {
213
220
  const normalizedBackend = normalizeRuntimeBackendName(backend);
214
221
  if (
215
222
  !normalizedBackend ||
216
- LEGACY_RUNTIME_BACKEND_ALIASES.has(normalizedBackend) ||
217
- !builtInBackends.has(normalizedBackend)
223
+ LEGACY_RUNTIME_BACKEND_ALIASES.has(normalizedBackend)
218
224
  ) {
219
225
  continue;
220
226
  }
@@ -237,10 +243,28 @@ function getAllowCliList(userConfig) {
237
243
  return DEFAULT_CLI_LIST;
238
244
  }
239
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
+
240
253
  function formatBackendLaunchCommand(cliCommand) {
241
254
  return typeof cliCommand === "string" && cliCommand.trim() ? cliCommand.trim() : "ai-sdk-managed";
242
255
  }
243
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
+
244
268
  async function defaultCreatePty(command, args, options) {
245
269
  if (!nodePtySpawnPromise) {
246
270
  const spawnHelperInfo = ensureNodePtySpawnHelperExecutable();
@@ -408,6 +432,64 @@ function normalizeLaunchConfig(value) {
408
432
  return value;
409
433
  }
410
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
+
411
493
  function normalizeTerminalEnv(value) {
412
494
  if (!value || typeof value !== "object" || Array.isArray(value)) {
413
495
  return {};
@@ -425,6 +507,36 @@ function normalizeTerminalEnv(value) {
425
507
  return env;
426
508
  }
427
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
+
428
540
  export function startDaemon(config = {}, deps = {}) {
429
541
  const exitFn = deps.exit || process.exit;
430
542
  const killFn = deps.kill || process.kill;
@@ -510,8 +622,10 @@ export function startDaemon(config = {}, deps = {}) {
510
622
  const CLI_PATH_VAL = config.CLI_PATH || CLI_PATH;
511
623
 
512
624
  // Get allow_cli_list from config
513
- const ALLOW_CLI_LIST = getAllowCliList(userConfig);
514
- 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 = {};
515
629
  const fetchLatestVersionFn = deps.fetchLatestVersion || fetchLatestVersion;
516
630
  const isNewerVersionFn = deps.isNewerVersion || isNewerVersion;
517
631
  const detectPackageManagerFn = deps.detectPackageManager || detectPackageManager;
@@ -554,7 +668,11 @@ export function startDaemon(config = {}, deps = {}) {
554
668
  const mkdirSyncFn = deps.mkdirSync || fs.mkdirSync;
555
669
  const writeFileSyncFn = deps.writeFileSync || fs.writeFileSync;
556
670
  const existsSyncFn = deps.existsSync || fs.existsSync;
671
+ const statSyncFn = deps.statSync || fs.statSync;
672
+ const lstatSyncFn = deps.lstatSync || fs.lstatSync;
557
673
  const readFileSyncFn = deps.readFileSync || fs.readFileSync;
674
+ const readlinkSyncFn = deps.readlinkSync || fs.readlinkSync;
675
+ const symlinkSyncFn = deps.symlinkSync || fs.symlinkSync;
558
676
  const unlinkSyncFn = deps.unlinkSync || fs.unlinkSync;
559
677
  const renameSyncFn = deps.renameSync || fs.renameSync;
560
678
  const createWriteStreamFn = deps.createWriteStream || fs.createWriteStream;
@@ -565,6 +683,263 @@ export function startDaemon(config = {}, deps = {}) {
565
683
  deps.createWebSocketClient ||
566
684
  ((clientConfig, options) => new ConductorWebSocketClient(clientConfig, options));
567
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
+
568
943
  const RTC_MODULE_CANDIDATES = resolveRtcModuleCandidates(process.env.CONDUCTOR_PTY_RTC_MODULES);
569
944
  const RTC_DIRECT_DISABLED = parseBooleanEnv(process.env.CONDUCTOR_DISABLE_PTY_DIRECT_RTC);
570
945
  const PROJECT_PATH_LOOKUP_TIMEOUT_MS = parsePositiveInt(
@@ -939,8 +1314,12 @@ export function startDaemon(config = {}, deps = {}) {
939
1314
  "x-conductor-backends": SUPPORTED_BACKENDS.join(","),
940
1315
  "x-conductor-version": cliVersion,
941
1316
  };
1317
+ const advertisedCapabilities = ["project_path_validation"];
942
1318
  if (ptyTaskCapabilityEnabled) {
943
- 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(",");
944
1323
  }
945
1324
  const client = createWebSocketClient(sdkConfig, {
946
1325
  extraHeaders,
@@ -998,14 +1377,31 @@ export function startDaemon(config = {}, deps = {}) {
998
1377
 
999
1378
  void (async () => {
1000
1379
  try {
1001
- const runtimeBackends = await listRuntimeSupportedBackends({ configFilePath: config.CONFIG_FILE });
1002
- const externalBackends = runtimeBackends.filter((backend) => !RUNTIME_SUPPORTED_BACKENDS.includes(backend));
1003
- 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
+ });
1004
1383
  } catch (error) {
1005
- 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}`);
1006
1401
  }
1007
1402
 
1008
1403
  extraHeaders["x-conductor-backends"] = SUPPORTED_BACKENDS.join(",");
1404
+ extraHeaders["x-conductor-backend-runtime-map"] = serializeRuntimeBackendMap(SUPPORTED_BACKEND_RUNTIME_MAP);
1009
1405
  if (typeof client?.setExtraHeaders === "function") {
1010
1406
  client.setExtraHeaders(extraHeaders);
1011
1407
  }
@@ -1642,6 +2038,30 @@ export function startDaemon(config = {}, deps = {}) {
1642
2038
  });
1643
2039
  }
1644
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
+
1645
2065
  function sendPtyTransportStatus(payload) {
1646
2066
  return client.sendJson({
1647
2067
  type: "pty_transport_status",
@@ -2263,7 +2683,9 @@ export function startDaemon(config = {}, deps = {}) {
2263
2683
  rejectCreatePtyTaskDuringShutdown(payload);
2264
2684
  return;
2265
2685
  }
2266
- let taskDir = normalizeOptionalString(launchConfig.cwd) || boundPath;
2686
+ let taskDir =
2687
+ normalizeOptionalString(launchConfig.cwd) ||
2688
+ boundPath;
2267
2689
  if (!taskDir) {
2268
2690
  const now = new Date();
2269
2691
  const dayDir = path.join(WORKSPACE_ROOT, formatWorkspaceDate(now));
@@ -2316,22 +2738,7 @@ export function startDaemon(config = {}, deps = {}) {
2316
2738
  logError(`Failed to report agent_command_ack(create_pty_task) for ${taskId}: ${err?.message || err}`);
2317
2739
  });
2318
2740
 
2319
- const env = {
2320
- ...process.env,
2321
- ...launchSpec.env,
2322
- CONDUCTOR_PROJECT_ID: projectId,
2323
- CONDUCTOR_TASK_ID: taskId,
2324
- CONDUCTOR_PTY_SESSION_ID: ptySessionId,
2325
- };
2326
- if (config.CONFIG_FILE) {
2327
- env.CONDUCTOR_CONFIG = config.CONFIG_FILE;
2328
- }
2329
- if (AGENT_TOKEN) {
2330
- env.CONDUCTOR_AGENT_TOKEN = AGENT_TOKEN;
2331
- }
2332
- if (BACKEND_HTTP) {
2333
- env.CONDUCTOR_BACKEND_URL = BACKEND_HTTP;
2334
- }
2741
+ const env = buildPtyTaskEnv(process.env, launchSpec.env);
2335
2742
 
2336
2743
  const logPath = path.join(launchSpec.cwd, "conductor-terminal.log");
2337
2744
  let logStream;
@@ -2421,6 +2828,119 @@ export function startDaemon(config = {}, deps = {}) {
2421
2828
  }
2422
2829
  }
2423
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
+
2424
2944
  async function handleTerminalAttach(payload) {
2425
2945
  const taskId = payload?.task_id ? String(payload.task_id) : "";
2426
2946
  if (!taskId) return;
@@ -2671,6 +3191,23 @@ export function startDaemon(config = {}, deps = {}) {
2671
3191
  rejectCreatePtyTaskDuringShutdown(event.payload);
2672
3192
  return;
2673
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
+ }
2674
3211
  }
2675
3212
 
2676
3213
  if (event.type === "create_task") {
@@ -2687,6 +3224,10 @@ export function startDaemon(config = {}, deps = {}) {
2687
3224
  void handleCreatePtyTask(event.payload);
2688
3225
  return;
2689
3226
  }
3227
+ if (event.type === "cleanup_task_worktree") {
3228
+ void handleCleanupTaskWorktree(event.payload);
3229
+ return;
3230
+ }
2690
3231
  if (event.type === "stop_task") {
2691
3232
  handleStopTask(event.payload);
2692
3233
  return;
@@ -2713,6 +3254,10 @@ export function startDaemon(config = {}, deps = {}) {
2713
3254
  }
2714
3255
  if (event.type === "collect_logs") {
2715
3256
  void handleCollectLogs(event.payload);
3257
+ return;
3258
+ }
3259
+ if (event.type === "validate_project_path") {
3260
+ void handleValidateProjectPath(event.payload);
2716
3261
  }
2717
3262
  }
2718
3263
 
@@ -2785,6 +3330,96 @@ export function startDaemon(config = {}, deps = {}) {
2785
3330
  }
2786
3331
  }
2787
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
+
2788
3423
  function handleStopTask(payload) {
2789
3424
  const taskId = payload?.task_id;
2790
3425
  if (!taskId) return;
@@ -2913,6 +3548,20 @@ export function startDaemon(config = {}, deps = {}) {
2913
3548
  return null;
2914
3549
  }
2915
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
+ }
2916
3565
  if (!project.metadata) {
2917
3566
  return null;
2918
3567
  }
@@ -3035,8 +3684,10 @@ export function startDaemon(config = {}, deps = {}) {
3035
3684
  }
3036
3685
 
3037
3686
  async function resolveRestartCwd({
3687
+ taskId,
3038
3688
  projectId,
3039
3689
  preferredCwd = "",
3690
+ launchConfig = null,
3040
3691
  backendType,
3041
3692
  sessionId,
3042
3693
  sourceSessionFilePath = "",
@@ -3046,6 +3697,15 @@ export function startDaemon(config = {}, deps = {}) {
3046
3697
  return normalizedPreferredCwd;
3047
3698
  }
3048
3699
 
3700
+ const worktreeCwd = await ensureTaskWorktree({
3701
+ taskId,
3702
+ projectId,
3703
+ launchConfig,
3704
+ });
3705
+ if (worktreeCwd) {
3706
+ return worktreeCwd;
3707
+ }
3708
+
3049
3709
  const boundPath = await getProjectLocalPath(projectId);
3050
3710
  if (boundPath) {
3051
3711
  return boundPath;
@@ -3055,8 +3715,13 @@ export function startDaemon(config = {}, deps = {}) {
3055
3715
  const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
3056
3716
  if (normalizedSessionId && normalizedBackend && normalizedBackend !== "opencode") {
3057
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 });
3058
3723
  const resumeContext = await (deps.resolveResumeContext || resolveResumeContext)(
3059
- normalizedBackend,
3724
+ resumeBackend,
3060
3725
  normalizedSessionId,
3061
3726
  {
3062
3727
  cwd: process.cwd(),
@@ -3097,6 +3762,7 @@ export function startDaemon(config = {}, deps = {}) {
3097
3762
  request_id: requestIdRaw,
3098
3763
  } =
3099
3764
  payload || {};
3765
+ const launchConfig = normalizeLaunchConfig(payload?.launch_config);
3100
3766
  const requestId = requestIdRaw ? String(requestIdRaw) : "";
3101
3767
 
3102
3768
  if (!taskId || !projectId) {
@@ -3143,11 +3809,22 @@ export function startDaemon(config = {}, deps = {}) {
3143
3809
  pendingTaskStarts.add(taskId);
3144
3810
  let acceptedAckSent = false;
3145
3811
  try {
3146
- 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, {
3147
3814
  configFilePath: config.CONFIG_FILE,
3148
3815
  });
3149
- if (!(await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE }))) {
3150
- 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(", ")}`);
3151
3828
  sendAgentCommandAck({
3152
3829
  requestId,
3153
3830
  taskId,
@@ -3161,7 +3838,7 @@ export function startDaemon(config = {}, deps = {}) {
3161
3838
  task_id: taskId,
3162
3839
  project_id: projectId,
3163
3840
  status: "KILLED",
3164
- summary: `Unsupported backend: ${effectiveBackend}`,
3841
+ summary: `Unsupported backend: ${selectedBackend}`,
3165
3842
  },
3166
3843
  })
3167
3844
  .catch(() => {});
@@ -3178,10 +3855,10 @@ export function startDaemon(config = {}, deps = {}) {
3178
3855
  });
3179
3856
  acceptedAckSent = true;
3180
3857
 
3181
- const cliCommand = ALLOW_CLI_LIST[effectiveBackend] || "";
3858
+ const cliCommand = ALLOW_CLI_LIST[selectedBackend] || ALLOW_CLI_LIST[effectiveBackend] || "";
3182
3859
 
3183
3860
  log("");
3184
- log(`Creating task ${taskId} for project ${projectId} (${effectiveBackend})`);
3861
+ log(`Creating task ${taskId} for project ${projectId} (${selectedBackend})`);
3185
3862
  log(`CLI command: ${formatBackendLaunchCommand(cliCommand)}`);
3186
3863
  client
3187
3864
  .sendJson({
@@ -3206,9 +3883,18 @@ export function startDaemon(config = {}, deps = {}) {
3206
3883
  let logPath;
3207
3884
  let runTimestampPart = null;
3208
3885
 
3209
- if (boundPath) {
3210
- taskDir = boundPath;
3211
- 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}`);
3212
3898
  logPath = path.join(taskDir, "conductor.log");
3213
3899
  } else {
3214
3900
  const now = new Date();
@@ -3221,8 +3907,8 @@ export function startDaemon(config = {}, deps = {}) {
3221
3907
  }
3222
3908
 
3223
3909
  const args = [];
3224
- if (effectiveBackend) {
3225
- args.push("--backend", effectiveBackend);
3910
+ if (selectedBackend) {
3911
+ args.push("--backend", selectedBackend);
3226
3912
  }
3227
3913
  if (initialContent) {
3228
3914
  args.push("--prefill", initialContent);
@@ -3412,6 +4098,7 @@ export function startDaemon(config = {}, deps = {}) {
3412
4098
  const normalizedTargetTaskId = targetTaskId ? String(targetTaskId) : "";
3413
4099
  const normalizedProjectId = projectId ? String(projectId) : "";
3414
4100
  const normalizedSourceSessionId = sourceSessionId ? String(sourceSessionId).trim() : "";
4101
+ const targetLaunchConfig = normalizeLaunchConfig(payload?.target_launch_config);
3415
4102
 
3416
4103
  if (
3417
4104
  !normalizedMode ||
@@ -3466,16 +4153,27 @@ export function startDaemon(config = {}, deps = {}) {
3466
4153
  return;
3467
4154
  }
3468
4155
 
3469
- 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, {
3470
4158
  configFilePath: config.CONFIG_FILE,
3471
4159
  });
3472
- 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)) {
3473
4171
  reportRestartFailure({
3474
4172
  taskId: normalizedTargetTaskId,
3475
4173
  projectId: normalizedProjectId,
3476
4174
  requestId,
3477
4175
  mode: normalizedMode,
3478
- error: new Error(`Unsupported backend: ${effectiveBackend}`),
4176
+ error: new Error(`Unsupported backend: ${selectedBackend}`),
3479
4177
  });
3480
4178
  return;
3481
4179
  }
@@ -3491,7 +4189,16 @@ export function startDaemon(config = {}, deps = {}) {
3491
4189
  });
3492
4190
  return;
3493
4191
  }
3494
- 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) {
3495
4202
  reportRestartFailure({
3496
4203
  taskId: normalizedTargetTaskId,
3497
4204
  projectId: normalizedProjectId,
@@ -3512,23 +4219,32 @@ export function startDaemon(config = {}, deps = {}) {
3512
4219
  logError(`Failed to report agent_command_ack(restart_task) for ${normalizedTargetTaskId}: ${err?.message || err}`);
3513
4220
  });
3514
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
+
3515
4229
  let resolvedResumeSessionId = normalizedSourceSessionId;
3516
4230
  let resolvedResumeCwd = "";
3517
4231
  try {
3518
4232
  if (normalizedMode === "bridge_to_new_task" || normalizedMode === "fork_to_new_task") {
3519
4233
  const sourceResumeCwd = await resolveRestartCwd({
4234
+ taskId: normalizedTargetTaskId,
3520
4235
  projectId: normalizedProjectId,
3521
4236
  backendType: sourceBackendType,
4237
+ launchConfig: targetLaunchConfig,
3522
4238
  sessionId: normalizedSourceSessionId,
3523
4239
  sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
3524
4240
  });
3525
4241
  const bridgeSession = await getBridgeSessionHelper();
3526
4242
  const bridgeResult = await bridgeSession({
3527
- sourceTool: sourceBackendType,
4243
+ sourceTool: sourceRuntimeBackend,
3528
4244
  sourceSessionId: normalizedSourceSessionId,
3529
4245
  sourceSessionPath: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
3530
4246
  sourceSessionInfo: {
3531
- tool: sourceBackendType,
4247
+ tool: sourceRuntimeBackend,
3532
4248
  sessionId: normalizedSourceSessionId,
3533
4249
  path: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
3534
4250
  cwd: sourceResumeCwd || undefined,
@@ -3538,15 +4254,19 @@ export function startDaemon(config = {}, deps = {}) {
3538
4254
  });
3539
4255
  resolvedResumeSessionId = bridgeResult.sessionId;
3540
4256
  resolvedResumeCwd = await resolveRestartCwd({
4257
+ taskId: normalizedTargetTaskId,
3541
4258
  projectId: normalizedProjectId,
3542
4259
  preferredCwd: bridgeResult.cwd,
4260
+ launchConfig: targetLaunchConfig,
3543
4261
  backendType: effectiveBackend,
3544
4262
  sessionId: bridgeResult.sessionId,
3545
4263
  sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
3546
4264
  });
3547
4265
  } else if (normalizedMode === "resume_inplace") {
3548
4266
  resolvedResumeCwd = await resolveRestartCwd({
4267
+ taskId: normalizedTargetTaskId,
3549
4268
  projectId: normalizedProjectId,
4269
+ launchConfig: targetLaunchConfig,
3550
4270
  backendType: effectiveBackend,
3551
4271
  sessionId: normalizedSourceSessionId,
3552
4272
  sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
@@ -3578,11 +4298,11 @@ export function startDaemon(config = {}, deps = {}) {
3578
4298
  return;
3579
4299
  }
3580
4300
 
3581
- const cliCommand = ALLOW_CLI_LIST[effectiveBackend] || "";
4301
+ const cliCommand = ALLOW_CLI_LIST[selectedBackend] || ALLOW_CLI_LIST[effectiveBackend] || "";
3582
4302
 
3583
4303
  log("");
3584
4304
  log(
3585
- `Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${effectiveBackend})`,
4305
+ `Restarting task ${normalizedTargetTaskId} from ${normalizedSourceTaskId} (${normalizedMode} -> ${selectedBackend})`,
3586
4306
  );
3587
4307
  log(`CLI command: ${formatBackendLaunchCommand(cliCommand)}`);
3588
4308
 
@@ -3631,8 +4351,8 @@ export function startDaemon(config = {}, deps = {}) {
3631
4351
  }
3632
4352
 
3633
4353
  const args = [];
3634
- if (effectiveBackend) {
3635
- args.push("--backend", effectiveBackend);
4354
+ if (selectedBackend) {
4355
+ args.push("--backend", selectedBackend);
3636
4356
  }
3637
4357
  args.push("--resume", resolvedResumeSessionId);
3638
4358
  args.push("--");