@love-moon/conductor-cli 0.5.0 → 0.6.0
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/CHANGELOG.md +35 -0
- package/bin/conductor-fire.js +55 -1
- package/package.json +5 -5
- package/src/daemon.js +130 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @love-moon/conductor-cli
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bcc80b5: Initialize Git submodules automatically when preparing task worktrees.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- @love-moon/conductor-sdk@0.6.0
|
|
12
|
+
- @love-moon/ai-sdk@0.6.0
|
|
13
|
+
|
|
14
|
+
## 0.5.1
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 39a49fc: fix: reclaim orphaned chat-web browser and cap chat-web task lifetime
|
|
19
|
+
|
|
20
|
+
chat-web persists one Chromium profile per provider, guarded by a per-profile
|
|
21
|
+
SingletonLock. A task whose browser was not cleaned up (e.g. the ai-sdk worker
|
|
22
|
+
was SIGKILLed) left an orphaned Chromium holding that lock, so the next task for
|
|
23
|
+
the same provider failed to launch with `Opening in existing browser session`.
|
|
24
|
+
|
|
25
|
+
- chat-web now reclaims stale/orphaned profile locks before launching (kills an
|
|
26
|
+
orphan whose owner process is gone, clears dead locks) and refuses with a
|
|
27
|
+
clear `ProfileLockedError` when a genuine live chat still holds the profile.
|
|
28
|
+
- The ai-sdk worker now closes its session (and browser) on SIGTERM/SIGINT and
|
|
29
|
+
bounds the close so it can't hang, preventing browser leaks on shutdown.
|
|
30
|
+
- conductor fire caps a chat-web task's active lifetime (default 24h,
|
|
31
|
+
`CONDUCTOR_CHATWEB_MAX_ACTIVE_MS`) and auto-stops it as
|
|
32
|
+
`KILLED / max_active_duration`; chat history is preserved.
|
|
33
|
+
|
|
34
|
+
- Updated dependencies [39a49fc]
|
|
35
|
+
- @love-moon/ai-sdk@0.5.1
|
|
36
|
+
- @love-moon/conductor-sdk@0.5.1
|
|
37
|
+
|
|
3
38
|
## 0.5.0
|
|
4
39
|
|
|
5
40
|
### Patch Changes
|
package/bin/conductor-fire.js
CHANGED
|
@@ -246,6 +246,18 @@ const DEFAULT_POLL_INTERVAL_MS = parseInt(
|
|
|
246
246
|
const DEFAULT_ERROR_LOOP_WINDOW_MS = 2 * 60 * 1000;
|
|
247
247
|
const DEFAULT_ERROR_LOOP_BACKOFF_MS = 3 * 60 * 1000;
|
|
248
248
|
const DEFAULT_ERROR_LOOP_THRESHOLD = 3;
|
|
249
|
+
// Runtime backend tokens (first word of the resolved command line) that mean
|
|
250
|
+
// "this task drives a chat-web Chromium browser". Mirrors ai-sdk's chat-web
|
|
251
|
+
// aliases; user-facing aliases like `web-chatgpt` resolve to one of these.
|
|
252
|
+
const CHAT_WEB_RUNTIME_BACKEND_TOKENS = new Set(["chat-web", "chatweb", "chat_web", "web-chat"]);
|
|
253
|
+
// Max active lifetime for a chat-web task before it is auto-stopped (default
|
|
254
|
+
// 24h). Bounded to [1min, 7d]; override via CONDUCTOR_CHATWEB_MAX_ACTIVE_MS.
|
|
255
|
+
const CHAT_WEB_MAX_ACTIVE_MS = getBoundedEnvInt(
|
|
256
|
+
"CONDUCTOR_CHATWEB_MAX_ACTIVE_MS",
|
|
257
|
+
24 * 60 * 60 * 1000,
|
|
258
|
+
60_000,
|
|
259
|
+
7 * 24 * 60 * 60 * 1000,
|
|
260
|
+
);
|
|
249
261
|
const SESSION_BOOTSTRAP_LOCK_TIMEOUT_MS = 15_000;
|
|
250
262
|
const SESSION_BOOTSTRAP_LOCK_RETRY_MS = 50;
|
|
251
263
|
const FIRE_WATCHDOG_INTERVAL_MS = getBoundedEnvInt(
|
|
@@ -866,6 +878,18 @@ async function main() {
|
|
|
866
878
|
env: process.env,
|
|
867
879
|
});
|
|
868
880
|
|
|
881
|
+
// chat-web tasks drive a real Chromium browser that holds a per-profile
|
|
882
|
+
// singleton lock for as long as the task lives. A task that never ends
|
|
883
|
+
// pins that browser indefinitely (and blocks other chats for the same
|
|
884
|
+
// provider), so we cap its active lifetime and auto-stop it when exceeded.
|
|
885
|
+
// The conversation itself is preserved (it lives in the provider account
|
|
886
|
+
// and the persisted profile; closing the browser does not delete it).
|
|
887
|
+
const runtimeBackendToken = String(sessionCommandLine || "")
|
|
888
|
+
.trim()
|
|
889
|
+
.split(/\s+/)[0]
|
|
890
|
+
?.toLowerCase() || "";
|
|
891
|
+
const isChatWebTask = CHAT_WEB_RUNTIME_BACKEND_TOKENS.has(runtimeBackendToken);
|
|
892
|
+
|
|
869
893
|
log(`Using backend: ${cliArgs.backend}`);
|
|
870
894
|
|
|
871
895
|
try {
|
|
@@ -880,6 +904,7 @@ async function main() {
|
|
|
880
904
|
|
|
881
905
|
const signals = new AbortController();
|
|
882
906
|
let shutdownSignal = null;
|
|
907
|
+
let autoStopReason = null;
|
|
883
908
|
let backendShutdownRequested = false;
|
|
884
909
|
const requestBackendShutdown = (source) => {
|
|
885
910
|
if (backendShutdownRequested) {
|
|
@@ -911,6 +936,27 @@ async function main() {
|
|
|
911
936
|
process.on("SIGINT", onSigint);
|
|
912
937
|
process.on("SIGTERM", onSigterm);
|
|
913
938
|
|
|
939
|
+
// Cap the active lifetime of chat-web tasks (default 24h). On expiry we
|
|
940
|
+
// stop the task gracefully — abort the runner and close the backend
|
|
941
|
+
// session so the Chromium browser is released — and mark it KILLED with a
|
|
942
|
+
// max_active_duration reason. Chat history is retained on the provider side.
|
|
943
|
+
let maxActiveTimer = null;
|
|
944
|
+
if (isChatWebTask) {
|
|
945
|
+
maxActiveTimer = setTimeout(() => {
|
|
946
|
+
log(
|
|
947
|
+
`chat-web task exceeded max active duration (${CHAT_WEB_MAX_ACTIVE_MS}ms); ` +
|
|
948
|
+
`auto-stopping task (chat history is preserved).`,
|
|
949
|
+
);
|
|
950
|
+
autoStopReason = "max_active_duration";
|
|
951
|
+
fireShuttingDown = true;
|
|
952
|
+
signals.abort();
|
|
953
|
+
requestBackendShutdown("max_active_duration");
|
|
954
|
+
}, CHAT_WEB_MAX_ACTIVE_MS);
|
|
955
|
+
if (typeof maxActiveTimer.unref === "function") {
|
|
956
|
+
maxActiveTimer.unref();
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
914
960
|
if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "running" })) {
|
|
915
961
|
try {
|
|
916
962
|
await conductor.sendTaskStatus(taskContext.taskId, {
|
|
@@ -1030,7 +1076,12 @@ async function main() {
|
|
|
1030
1076
|
// SDK durable outbox would retry forever, preventing the process from
|
|
1031
1077
|
// exiting.
|
|
1032
1078
|
const taskDeletedByUser = remoteStopReason === "deleted_by_user";
|
|
1033
|
-
const finalStatus =
|
|
1079
|
+
const finalStatus = autoStopReason
|
|
1080
|
+
? {
|
|
1081
|
+
status: "KILLED",
|
|
1082
|
+
summary: `${autoStopReason}: chat-web task exceeded its max active duration`,
|
|
1083
|
+
}
|
|
1084
|
+
: shutdownSignal
|
|
1034
1085
|
? {
|
|
1035
1086
|
status: "KILLED",
|
|
1036
1087
|
summary: `terminated by ${shutdownSignal}`,
|
|
@@ -1079,6 +1130,9 @@ async function main() {
|
|
|
1079
1130
|
} finally {
|
|
1080
1131
|
process.off("SIGINT", onSigint);
|
|
1081
1132
|
process.off("SIGTERM", onSigterm);
|
|
1133
|
+
if (maxActiveTimer) {
|
|
1134
|
+
clearTimeout(maxActiveTimer);
|
|
1135
|
+
}
|
|
1082
1136
|
if (shutdownSignal === "SIGINT") {
|
|
1083
1137
|
process.exitCode = 130;
|
|
1084
1138
|
} else if (shutdownSignal === "SIGTERM") {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"gitCommitId": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"gitCommitId": "2f914a7",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"test": "node --test test/*.test.js"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@love-moon/ai-sdk": "0.
|
|
27
|
-
"@love-moon/conductor-sdk": "0.
|
|
26
|
+
"@love-moon/ai-sdk": "0.6.0",
|
|
27
|
+
"@love-moon/conductor-sdk": "0.6.0",
|
|
28
28
|
"@github/copilot-sdk": "^0.3.0",
|
|
29
29
|
"chrome-launcher": "^1.2.1",
|
|
30
30
|
"chrome-remote-interface": "^0.33.0",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
39
|
"@roamhq/wrtc": "^0.10.0",
|
|
40
|
-
"@love-moon/chat-web": "0.
|
|
40
|
+
"@love-moon/chat-web": "0.6.0"
|
|
41
41
|
},
|
|
42
42
|
"pnpm": {
|
|
43
43
|
"onlyBuiltDependencies": [
|
package/src/daemon.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
listAdvertisedBackends,
|
|
29
29
|
resolveConfiguredRuntimeBackend,
|
|
30
30
|
isBuiltInRuntimeBackend,
|
|
31
|
+
isCommandOptionalBuiltInRuntimeBackend,
|
|
31
32
|
isRuntimeSupportedBackend,
|
|
32
33
|
normalizeRuntimeBackendAlias,
|
|
33
34
|
normalizeRuntimeBackendName,
|
|
@@ -1255,6 +1256,8 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1255
1256
|
"",
|
|
1256
1257
|
"worktree:",
|
|
1257
1258
|
" sync_branch: false",
|
|
1259
|
+
" sync_submodules: true",
|
|
1260
|
+
" # When .gitmodules exists, initialize/update submodules in task worktrees.",
|
|
1258
1261
|
" symlink: []",
|
|
1259
1262
|
" # Example: symlink paths from the parent workspace into each worktree",
|
|
1260
1263
|
" # symlink:",
|
|
@@ -1263,6 +1266,72 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1263
1266
|
"",
|
|
1264
1267
|
].join("\n");
|
|
1265
1268
|
|
|
1269
|
+
const MAX_PROJECT_ICON_IMAGE_BYTES = 128 * 1024;
|
|
1270
|
+
const PROJECT_ICON_MIME_BY_EXTENSION = {
|
|
1271
|
+
".svg": "image/svg+xml",
|
|
1272
|
+
".png": "image/png",
|
|
1273
|
+
".jpg": "image/jpeg",
|
|
1274
|
+
".jpeg": "image/jpeg",
|
|
1275
|
+
".gif": "image/gif",
|
|
1276
|
+
".webp": "image/webp",
|
|
1277
|
+
".ico": "image/x-icon",
|
|
1278
|
+
".avif": "image/avif",
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
function getProjectSettingsCandidates(projectWorkspacePath) {
|
|
1282
|
+
return [
|
|
1283
|
+
path.join(projectWorkspacePath, ".conductor", "settings.yaml"),
|
|
1284
|
+
path.join(projectWorkspacePath, ".conductor", "settings.yml"),
|
|
1285
|
+
path.join(projectWorkspacePath, ".conductor", "setttings.yaml"),
|
|
1286
|
+
path.join(projectWorkspacePath, ".conductor", "setttings.yml"),
|
|
1287
|
+
];
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function extractProjectIconFromSettings(parsed) {
|
|
1291
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
const direct = normalizeOptionalString(parsed.icon);
|
|
1295
|
+
if (direct) return direct;
|
|
1296
|
+
const projectSettings = parsed.project;
|
|
1297
|
+
if (
|
|
1298
|
+
projectSettings &&
|
|
1299
|
+
typeof projectSettings === "object" &&
|
|
1300
|
+
!Array.isArray(projectSettings)
|
|
1301
|
+
) {
|
|
1302
|
+
return normalizeOptionalString(projectSettings.icon);
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function projectIconLooksLikeFilesystemPath(iconValue) {
|
|
1308
|
+
if (/^(https?:\/\/|data:)/i.test(iconValue)) return false;
|
|
1309
|
+
if (iconValue.startsWith("/") || iconValue.startsWith("./") || iconValue.startsWith("../")) {
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
if (!iconValue.includes("/")) return false;
|
|
1313
|
+
const ext = path.extname(iconValue).toLowerCase();
|
|
1314
|
+
return Object.prototype.hasOwnProperty.call(PROJECT_ICON_MIME_BY_EXTENSION, ext);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function readProjectIconAsDataUri(settingsDir, iconValue) {
|
|
1318
|
+
const resolvedPath = path.isAbsolute(iconValue)
|
|
1319
|
+
? iconValue
|
|
1320
|
+
: path.resolve(settingsDir, iconValue);
|
|
1321
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
1322
|
+
const mime = PROJECT_ICON_MIME_BY_EXTENSION[ext];
|
|
1323
|
+
if (!mime) return null;
|
|
1324
|
+
try {
|
|
1325
|
+
const stat = statSyncFn(resolvedPath);
|
|
1326
|
+
if (!stat.isFile() || stat.size === 0 || stat.size > MAX_PROJECT_ICON_IMAGE_BYTES) {
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
return `data:${mime};base64,${readFileSyncFn(resolvedPath).toString("base64")}`;
|
|
1330
|
+
} catch {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1266
1335
|
function ensureProjectSettingsTemplate(projectWorkspacePath) {
|
|
1267
1336
|
const settingsPath = path.join(projectWorkspacePath, ".conductor", "settings.yaml");
|
|
1268
1337
|
if (existsSyncFn(settingsPath)) {
|
|
@@ -1276,15 +1345,28 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1276
1345
|
}
|
|
1277
1346
|
}
|
|
1278
1347
|
|
|
1279
|
-
function
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1348
|
+
function readProjectIconSetting(projectWorkspacePath) {
|
|
1349
|
+
for (const settingsPath of getProjectSettingsCandidates(projectWorkspacePath)) {
|
|
1350
|
+
if (!existsSyncFn(settingsPath)) {
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
let rawIcon = null;
|
|
1354
|
+
try {
|
|
1355
|
+
rawIcon = extractProjectIconFromSettings(yaml.load(readFileSyncFn(settingsPath, "utf8")));
|
|
1356
|
+
} catch {
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
if (!rawIcon) return null;
|
|
1360
|
+
if (!projectIconLooksLikeFilesystemPath(rawIcon)) {
|
|
1361
|
+
return rawIcon;
|
|
1362
|
+
}
|
|
1363
|
+
return readProjectIconAsDataUri(path.dirname(settingsPath), rawIcon);
|
|
1364
|
+
}
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1286
1367
|
|
|
1287
|
-
|
|
1368
|
+
function readProjectWorktreeSettings(projectWorkspacePath) {
|
|
1369
|
+
for (const settingsPath of getProjectSettingsCandidates(projectWorkspacePath)) {
|
|
1288
1370
|
if (!existsSyncFn(settingsPath)) {
|
|
1289
1371
|
continue;
|
|
1290
1372
|
}
|
|
@@ -1298,6 +1380,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1298
1380
|
return {
|
|
1299
1381
|
symlinkPaths: normalizeConfiguredPathList(worktreeSettings.symlink, projectWorkspacePath),
|
|
1300
1382
|
syncBranch: worktreeSettings.sync_branch === true || worktreeSettings.syncBranch === true,
|
|
1383
|
+
syncSubmodules: worktreeSettings.sync_submodules !== false && worktreeSettings.syncSubmodules !== false,
|
|
1301
1384
|
settingsPath,
|
|
1302
1385
|
};
|
|
1303
1386
|
} catch (error) {
|
|
@@ -1308,6 +1391,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1308
1391
|
return {
|
|
1309
1392
|
symlinkPaths: [],
|
|
1310
1393
|
syncBranch: false,
|
|
1394
|
+
syncSubmodules: true,
|
|
1311
1395
|
settingsPath: null,
|
|
1312
1396
|
};
|
|
1313
1397
|
}
|
|
@@ -1377,6 +1461,28 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1377
1461
|
}
|
|
1378
1462
|
}
|
|
1379
1463
|
|
|
1464
|
+
async function ensureTaskWorktreeSubmodules({ taskId, projectWorkspacePath, worktreeRoot }) {
|
|
1465
|
+
const { syncSubmodules } = readProjectWorktreeSettings(projectWorkspacePath);
|
|
1466
|
+
if (!syncSubmodules || !existsSyncFn(path.join(worktreeRoot, ".gitmodules"))) {
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
try {
|
|
1471
|
+
await runSpawnProcess(
|
|
1472
|
+
"git",
|
|
1473
|
+
["-C", worktreeRoot, "submodule", "sync", "--recursive"],
|
|
1474
|
+
{ cwd: worktreeRoot, timeoutMs: WORKTREE_SUBMODULE_SYNC_TIMEOUT_MS },
|
|
1475
|
+
);
|
|
1476
|
+
await runSpawnProcess(
|
|
1477
|
+
"git",
|
|
1478
|
+
["-C", worktreeRoot, "submodule", "update", "--init", "--recursive"],
|
|
1479
|
+
{ cwd: worktreeRoot, timeoutMs: WORKTREE_SUBMODULE_SYNC_TIMEOUT_MS },
|
|
1480
|
+
);
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
throw new Error(`Failed to sync git submodules for ${taskId}: ${error?.message || error}`);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1380
1486
|
async function runSpawnProcess(command, args, options = {}) {
|
|
1381
1487
|
let child;
|
|
1382
1488
|
const { timeoutMs, ...spawnOptions } = options || {};
|
|
@@ -1565,6 +1671,11 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1565
1671
|
}
|
|
1566
1672
|
}
|
|
1567
1673
|
|
|
1674
|
+
await ensureTaskWorktreeSubmodules({
|
|
1675
|
+
taskId,
|
|
1676
|
+
projectWorkspacePath: worktreeConfig.projectWorkspacePath,
|
|
1677
|
+
worktreeRoot,
|
|
1678
|
+
});
|
|
1568
1679
|
mkdirSyncFn(finalCwd, { recursive: true });
|
|
1569
1680
|
await ensureTaskWorktreeSymlinks({
|
|
1570
1681
|
projectRepoRoot: worktreeConfig.projectRepoRoot,
|
|
@@ -1600,6 +1711,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1600
1711
|
process.env.CONDUCTOR_WORKTREE_SYNC_TIMEOUT_MS,
|
|
1601
1712
|
5_000,
|
|
1602
1713
|
);
|
|
1714
|
+
const WORKTREE_SUBMODULE_SYNC_TIMEOUT_MS = parsePositiveInt(
|
|
1715
|
+
process.env.CONDUCTOR_WORKTREE_SUBMODULE_SYNC_TIMEOUT_MS,
|
|
1716
|
+
120_000,
|
|
1717
|
+
);
|
|
1603
1718
|
const SHUTDOWN_STATUS_REPORT_TIMEOUT_MS = parsePositiveInt(
|
|
1604
1719
|
process.env.CONDUCTOR_SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
|
|
1605
1720
|
1000,
|
|
@@ -4337,6 +4452,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
4337
4452
|
lastCommit: null,
|
|
4338
4453
|
lastCommitAt: null,
|
|
4339
4454
|
fileCount: null,
|
|
4455
|
+
icon: null,
|
|
4340
4456
|
error: null,
|
|
4341
4457
|
errorCode: null,
|
|
4342
4458
|
validatedAt,
|
|
@@ -4390,6 +4506,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
4390
4506
|
typeof snapshot?.fileCount === "number" && Number.isInteger(snapshot.fileCount)
|
|
4391
4507
|
? snapshot.fileCount
|
|
4392
4508
|
: null,
|
|
4509
|
+
icon: readProjectIconSetting(effectiveWorkspace),
|
|
4393
4510
|
};
|
|
4394
4511
|
}
|
|
4395
4512
|
} catch (error) {
|
|
@@ -4413,6 +4530,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
4413
4530
|
last_commit_at: result.lastCommitAt,
|
|
4414
4531
|
git_remote_url: result.gitRemoteUrl,
|
|
4415
4532
|
file_count: result.fileCount,
|
|
4533
|
+
icon: result.icon,
|
|
4416
4534
|
error: result.error,
|
|
4417
4535
|
error_code: result.errorCode,
|
|
4418
4536
|
validated_at: result.validatedAt,
|
|
@@ -5055,7 +5173,8 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
5055
5173
|
const isAllowedExternalBackend =
|
|
5056
5174
|
!isBuiltInRuntimeBackend(effectiveBackend) &&
|
|
5057
5175
|
await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE });
|
|
5058
|
-
|
|
5176
|
+
const isCommandOptionalBuiltIn = isCommandOptionalBuiltInRuntimeBackend(effectiveBackend);
|
|
5177
|
+
if (!isAdvertisedBackend || (!hasConfiguredEntry && !isAllowedExternalBackend && !isCommandOptionalBuiltIn)) {
|
|
5059
5178
|
logError(`Unsupported backend: ${selectedBackend}. Supported: ${SUPPORTED_BACKENDS.join(", ")}`);
|
|
5060
5179
|
sendAgentCommandAck({
|
|
5061
5180
|
requestId,
|
|
@@ -5526,7 +5645,8 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
5526
5645
|
const isAllowedExternalBackend =
|
|
5527
5646
|
!isBuiltInRuntimeBackend(effectiveBackend) &&
|
|
5528
5647
|
await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE });
|
|
5529
|
-
|
|
5648
|
+
const isCommandOptionalBuiltIn = isCommandOptionalBuiltInRuntimeBackend(effectiveBackend);
|
|
5649
|
+
if (!isAdvertisedBackend || (!hasConfiguredEntry && !isAllowedExternalBackend && !isCommandOptionalBuiltIn)) {
|
|
5530
5650
|
reportRestartFailure({
|
|
5531
5651
|
taskId: normalizedTargetTaskId,
|
|
5532
5652
|
projectId: normalizedProjectId,
|