@love-moon/conductor-cli 0.2.31 → 0.2.32
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 +32 -2
- package/package.json +4 -4
- package/src/daemon.js +65 -4
- package/src/runtime-backends.js +9 -1
package/bin/conductor-fire.js
CHANGED
|
@@ -66,6 +66,20 @@ export function buildConductorConnectHeaders(version = pkgJson.version) {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export function shouldRunReconnectRecovery({
|
|
70
|
+
isReconnect,
|
|
71
|
+
fireShuttingDown = false,
|
|
72
|
+
runner = null,
|
|
73
|
+
} = {}) {
|
|
74
|
+
if (!isReconnect || fireShuttingDown) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (!runner || typeof runner.shouldSuppressReconnectRecovery !== "function") {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return !runner.shouldSuppressReconnectRecovery();
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
// Load allow_cli_list from config file (no defaults - must be configured)
|
|
70
84
|
async function loadAllowCliList(configFilePath) {
|
|
71
85
|
const home = os.homedir();
|
|
@@ -485,7 +499,13 @@ async function main() {
|
|
|
485
499
|
fireWatchdog.start();
|
|
486
500
|
|
|
487
501
|
const scheduleReconnectRecovery = ({ isReconnect }) => {
|
|
488
|
-
if (
|
|
502
|
+
if (
|
|
503
|
+
!shouldRunReconnectRecovery({
|
|
504
|
+
isReconnect,
|
|
505
|
+
fireShuttingDown,
|
|
506
|
+
runner: reconnectRunner,
|
|
507
|
+
})
|
|
508
|
+
) {
|
|
489
509
|
return;
|
|
490
510
|
}
|
|
491
511
|
log("Conductor connection restored");
|
|
@@ -742,7 +762,13 @@ async function main() {
|
|
|
742
762
|
summary: "conductor fire exited",
|
|
743
763
|
};
|
|
744
764
|
try {
|
|
745
|
-
await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
|
|
765
|
+
const statusResult = await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
|
|
766
|
+
if (statusResult?.pending && typeof conductor.flushPendingUpstreamEvents === "function") {
|
|
767
|
+
await conductor.flushPendingUpstreamEvents({
|
|
768
|
+
timeoutMs: 5_000,
|
|
769
|
+
retryIntervalMs: 250,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
746
772
|
} catch (error) {
|
|
747
773
|
log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
|
|
748
774
|
}
|
|
@@ -1644,6 +1670,10 @@ export class BridgeRunner {
|
|
|
1644
1670
|
this.needsReconnectRecovery = true;
|
|
1645
1671
|
}
|
|
1646
1672
|
|
|
1673
|
+
shouldSuppressReconnectRecovery() {
|
|
1674
|
+
return this.stopped || Boolean(this.remoteStopInfo);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1647
1677
|
getRemoteStopSummary() {
|
|
1648
1678
|
if (!this.remoteStopInfo) {
|
|
1649
1679
|
return null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"gitCommitId": "
|
|
3
|
+
"version": "0.2.32",
|
|
4
|
+
"gitCommitId": "c749d4b",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conductor": "bin/conductor.js"
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@love-moon/ai-bridge": "0.1.4",
|
|
21
|
-
"@love-moon/ai-sdk": "0.2.
|
|
22
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
21
|
+
"@love-moon/ai-sdk": "0.2.32",
|
|
22
|
+
"@love-moon/conductor-sdk": "0.2.32",
|
|
23
23
|
"chrome-launcher": "^1.2.1",
|
|
24
24
|
"chrome-remote-interface": "^0.33.0",
|
|
25
25
|
"dotenv": "^16.4.5",
|
package/src/daemon.js
CHANGED
|
@@ -59,6 +59,7 @@ const DEFAULT_TERMINAL_ROWS = 40;
|
|
|
59
59
|
const DEFAULT_TERMINAL_RING_BUFFER_MAX_BYTES = 2 * 1024 * 1024;
|
|
60
60
|
const DEFAULT_TERMINAL_RESUME_SNAPSHOT_MAX_BYTES = 128 * 1024;
|
|
61
61
|
const DEFAULT_RTC_MODULE_CANDIDATES = ["@roamhq/wrtc", "wrtc"];
|
|
62
|
+
const BLOCKING_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
62
63
|
let nodePtySpawnPromise = null;
|
|
63
64
|
|
|
64
65
|
function resolveNodePtySpawnExport(mod) {
|
|
@@ -117,6 +118,20 @@ function logError(message) {
|
|
|
117
118
|
appendDaemonLog(line);
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
function sleepSync(ms) {
|
|
122
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
Atomics.wait(BLOCKING_SLEEP_BUFFER, 0, 0, ms);
|
|
127
|
+
} catch {
|
|
128
|
+
const deadline = Date.now() + ms;
|
|
129
|
+
while (Date.now() < deadline) {
|
|
130
|
+
// best-effort fallback for runtimes that disallow Atomics.wait
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
120
135
|
function getUserConfig(configFilePath) {
|
|
121
136
|
try {
|
|
122
137
|
const home = os.homedir();
|
|
@@ -560,6 +575,18 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
560
575
|
process.env.CONDUCTOR_STOP_FORCE_KILL_TIMEOUT_MS,
|
|
561
576
|
5000,
|
|
562
577
|
);
|
|
578
|
+
const DAEMON_FORCE_STOP_GRACE_MS = parsePositiveInt(
|
|
579
|
+
process.env.CONDUCTOR_DAEMON_FORCE_STOP_GRACE_MS,
|
|
580
|
+
15_000,
|
|
581
|
+
);
|
|
582
|
+
const DAEMON_FORCE_STOP_POLL_INTERVAL_MS = parsePositiveInt(
|
|
583
|
+
process.env.CONDUCTOR_DAEMON_FORCE_STOP_POLL_INTERVAL_MS,
|
|
584
|
+
100,
|
|
585
|
+
);
|
|
586
|
+
const DAEMON_FORCE_KILL_WAIT_MS = parsePositiveInt(
|
|
587
|
+
process.env.CONDUCTOR_DAEMON_FORCE_KILL_WAIT_MS,
|
|
588
|
+
2_000,
|
|
589
|
+
);
|
|
563
590
|
const SHUTDOWN_STATUS_REPORT_TIMEOUT_MS = parsePositiveInt(
|
|
564
591
|
process.env.CONDUCTOR_SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
|
|
565
592
|
1000,
|
|
@@ -648,6 +675,20 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
648
675
|
return true;
|
|
649
676
|
};
|
|
650
677
|
|
|
678
|
+
const waitForProcessExitSync = (pid, timeoutMs) => {
|
|
679
|
+
const deadline = Date.now() + timeoutMs;
|
|
680
|
+
while (true) {
|
|
681
|
+
if (!isProcessAlive(pid)) {
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
const remainingMs = deadline - Date.now();
|
|
685
|
+
if (remainingMs <= 0) {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
sleepSync(Math.min(DAEMON_FORCE_STOP_POLL_INTERVAL_MS, remainingMs));
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
651
692
|
try {
|
|
652
693
|
mkdirSyncFn(WORKSPACE_ROOT, { recursive: true });
|
|
653
694
|
} catch (err) {
|
|
@@ -670,17 +711,35 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
670
711
|
if (alive) {
|
|
671
712
|
if (config.FORCE) {
|
|
672
713
|
log(`Force enabled: stopping existing daemon PID ${pid}`);
|
|
714
|
+
let alreadyExited = false;
|
|
673
715
|
try {
|
|
674
716
|
killFn(pid, "SIGTERM");
|
|
675
717
|
} catch (killErr) {
|
|
676
|
-
if (
|
|
718
|
+
if (killErr?.code === "ESRCH") {
|
|
719
|
+
alreadyExited = true;
|
|
720
|
+
} else {
|
|
677
721
|
logError(`Failed to stop existing daemon PID ${pid}: ${killErr.message}`);
|
|
678
722
|
return exitAndReturn(1);
|
|
679
723
|
}
|
|
680
724
|
}
|
|
681
725
|
try {
|
|
682
|
-
|
|
683
|
-
|
|
726
|
+
let exited = alreadyExited || waitForProcessExitSync(pid, DAEMON_FORCE_STOP_GRACE_MS);
|
|
727
|
+
if (!exited) {
|
|
728
|
+
log(
|
|
729
|
+
`Existing daemon PID ${pid} did not exit within ${DAEMON_FORCE_STOP_GRACE_MS}ms; sending SIGKILL`,
|
|
730
|
+
);
|
|
731
|
+
try {
|
|
732
|
+
killFn(pid, "SIGKILL");
|
|
733
|
+
} catch (killErr) {
|
|
734
|
+
if (killErr?.code !== "ESRCH") {
|
|
735
|
+
logError(`Failed to force kill existing daemon PID ${pid}: ${killErr.message}`);
|
|
736
|
+
return exitAndReturn(1);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
exited = waitForProcessExitSync(pid, DAEMON_FORCE_KILL_WAIT_MS);
|
|
740
|
+
}
|
|
741
|
+
if (!exited) {
|
|
742
|
+
logError(`Existing daemon PID ${pid} is still running after force restart; please stop it manually.`);
|
|
684
743
|
return exitAndReturn(1);
|
|
685
744
|
}
|
|
686
745
|
} catch (checkErr) {
|
|
@@ -688,7 +747,9 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
688
747
|
return exitAndReturn(1);
|
|
689
748
|
}
|
|
690
749
|
log("Removing lock file after force stop");
|
|
691
|
-
|
|
750
|
+
if (existsSyncFn(LOCK_FILE)) {
|
|
751
|
+
unlinkSyncFn(LOCK_FILE);
|
|
752
|
+
}
|
|
692
753
|
} else {
|
|
693
754
|
logError(`Daemon already running with PID ${pid}`);
|
|
694
755
|
return exitAndReturn(1);
|
package/src/runtime-backends.js
CHANGED
|
@@ -126,6 +126,14 @@ function registerExternalAlias(catalog, alias, backend, sourcePath) {
|
|
|
126
126
|
catalog.aliasToBackend.set(alias, backend);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
function formatExternalProviderLoadError(modulePath, error) {
|
|
130
|
+
const message = error?.message || String(error);
|
|
131
|
+
return [
|
|
132
|
+
`Failed to load external AI SDK provider module ${modulePath}: ${message}`,
|
|
133
|
+
"Help: if this provider comes from a local repo or workspace, did you forget to run pnpm install?",
|
|
134
|
+
].join(" ");
|
|
135
|
+
}
|
|
136
|
+
|
|
129
137
|
async function loadExternalRuntimeCatalog(providerPathEnv) {
|
|
130
138
|
const catalog = createEmptyExternalCatalog();
|
|
131
139
|
for (const modulePath of listProviderModulePaths(providerPathEnv)) {
|
|
@@ -133,7 +141,7 @@ async function loadExternalRuntimeCatalog(providerPathEnv) {
|
|
|
133
141
|
try {
|
|
134
142
|
importedModule = await importExternalProviderModule(modulePath);
|
|
135
143
|
} catch (error) {
|
|
136
|
-
throw new Error(
|
|
144
|
+
throw new Error(formatExternalProviderLoadError(modulePath, error));
|
|
137
145
|
}
|
|
138
146
|
const providers = Array.isArray(importedModule?.providers) ? importedModule.providers : [];
|
|
139
147
|
if (providers.length === 0) {
|