@love-moon/conductor-cli 0.2.32 → 0.2.34
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/bin/conductor-fire.js +199 -79
- package/package.json +4 -4
- package/src/daemon.js +772 -52
- package/src/fire/resume.js +67 -7
- package/src/runtime-backends.js +306 -3
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 {
|
|
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
|
-
|
|
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
|
|
514
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1380
|
+
ALLOW_CLI_LIST = await filterRuntimeSupportedAllowCliList(RAW_ALLOW_CLI_LIST, {
|
|
1381
|
+
configFilePath: config.CONFIG_FILE,
|
|
1382
|
+
});
|
|
1004
1383
|
} catch (error) {
|
|
1005
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3150
|
-
|
|
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: ${
|
|
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} (${
|
|
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
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
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 (
|
|
3225
|
-
args.push("--backend",
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
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:
|
|
4243
|
+
sourceTool: sourceRuntimeBackend,
|
|
3528
4244
|
sourceSessionId: normalizedSourceSessionId,
|
|
3529
4245
|
sourceSessionPath: sourceSessionFilePath ? String(sourceSessionFilePath) : undefined,
|
|
3530
4246
|
sourceSessionInfo: {
|
|
3531
|
-
tool:
|
|
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} -> ${
|
|
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 (
|
|
3635
|
-
args.push("--backend",
|
|
4354
|
+
if (selectedBackend) {
|
|
4355
|
+
args.push("--backend", selectedBackend);
|
|
3636
4356
|
}
|
|
3637
4357
|
args.push("--resume", resolvedResumeSessionId);
|
|
3638
4358
|
args.push("--");
|