@love-moon/conductor-cli 0.2.8 → 0.2.10

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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/daemon.js +70 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "conductor": "bin/conductor.js"
@@ -16,8 +16,8 @@
16
16
  "test": "node --test"
17
17
  },
18
18
  "dependencies": {
19
- "@love-moon/tui-driver": "0.2.8",
20
- "@love-moon/conductor-sdk": "0.2.8",
19
+ "@love-moon/tui-driver": "0.2.10",
20
+ "@love-moon/conductor-sdk": "0.2.10",
21
21
  "dotenv": "^16.4.5",
22
22
  "enquirer": "^2.4.1",
23
23
  "js-yaml": "^4.1.1",
package/src/daemon.js CHANGED
@@ -61,6 +61,7 @@ export function startDaemon(config = {}, deps = {}) {
61
61
  const killFn = deps.kill || process.kill;
62
62
  let requestShutdown = async () => {};
63
63
  let shutdownSignalHandled = false;
64
+ let forcedSignalExitHandled = false;
64
65
 
65
66
  const exitAndReturn = (code) => {
66
67
  exitFn(code);
@@ -145,6 +146,14 @@ export function startDaemon(config = {}, deps = {}) {
145
146
  process.env.CONDUCTOR_STOP_FORCE_KILL_TIMEOUT_MS,
146
147
  5000,
147
148
  );
149
+ const SHUTDOWN_STATUS_REPORT_TIMEOUT_MS = parsePositiveInt(
150
+ process.env.CONDUCTOR_SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
151
+ 1000,
152
+ );
153
+ const SHUTDOWN_DISCONNECT_TIMEOUT_MS = parsePositiveInt(
154
+ process.env.CONDUCTOR_SHUTDOWN_DISCONNECT_TIMEOUT_MS,
155
+ 1000,
156
+ );
148
157
 
149
158
  try {
150
159
  mkdirSyncFn(WORKSPACE_ROOT, { recursive: true });
@@ -216,8 +225,16 @@ export function startDaemon(config = {}, deps = {}) {
216
225
  };
217
226
 
218
227
  process.on("exit", cleanupLock);
228
+ const signalExitCode = (signal) => (signal === "SIGINT" ? 130 : 143);
219
229
  const handleSignal = (signal) => {
220
- if (shutdownSignalHandled) return;
230
+ if (shutdownSignalHandled) {
231
+ if (forcedSignalExitHandled) return;
232
+ forcedSignalExitHandled = true;
233
+ log(`Received ${signal} again, forcing exit now`);
234
+ cleanupLock();
235
+ exitFn(signalExitCode(signal));
236
+ return;
237
+ }
221
238
  shutdownSignalHandled = true;
222
239
  void (async () => {
223
240
  try {
@@ -227,7 +244,7 @@ export function startDaemon(config = {}, deps = {}) {
227
244
  logError(`Graceful shutdown failed on ${signal}: ${err?.message || err}`);
228
245
  } finally {
229
246
  cleanupLock();
230
- exitFn(0);
247
+ exitFn(signalExitCode(signal));
231
248
  }
232
249
  })();
233
250
  };
@@ -639,6 +656,20 @@ export function startDaemon(config = {}, deps = {}) {
639
656
  return;
640
657
  }
641
658
 
659
+ const existingTaskRecord = activeTaskProcesses.get(taskId);
660
+ if (existingTaskRecord?.child) {
661
+ log(
662
+ `Duplicate create_task ignored for ${taskId}: task already active (pid=${existingTaskRecord.child.pid ?? "unknown"})`,
663
+ );
664
+ sendAgentCommandAck({
665
+ requestId,
666
+ taskId,
667
+ eventType: "create_task",
668
+ accepted: true,
669
+ }).catch(() => {});
670
+ return;
671
+ }
672
+
642
673
  // Validate and get CLI command for the backend
643
674
  const effectiveBackend = backendType || SUPPORTED_BACKENDS[0];
644
675
  if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
@@ -892,15 +923,19 @@ export function startDaemon(config = {}, deps = {}) {
892
923
  activeEntries.map(async ([taskId, record]) => {
893
924
  suppressedExitStatusReports.add(taskId);
894
925
  try {
895
- await client.sendJson({
896
- type: "task_status_update",
897
- payload: {
898
- task_id: taskId,
899
- project_id: record.projectId,
900
- status: "KILLED",
901
- summary: `daemon shutdown (${reason})`,
902
- },
903
- });
926
+ await withTimeout(
927
+ client.sendJson({
928
+ type: "task_status_update",
929
+ payload: {
930
+ task_id: taskId,
931
+ project_id: record.projectId,
932
+ status: "KILLED",
933
+ summary: `daemon shutdown (${reason})`,
934
+ },
935
+ }),
936
+ SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
937
+ `report shutdown status for ${taskId}`,
938
+ );
904
939
  } catch (err) {
905
940
  logError(`Failed to report shutdown status (KILLED) for ${taskId}: ${err?.message || err}`);
906
941
  }
@@ -923,7 +958,11 @@ export function startDaemon(config = {}, deps = {}) {
923
958
  activeTaskProcesses.clear();
924
959
 
925
960
  try {
926
- await Promise.resolve(client.disconnect());
961
+ await withTimeout(
962
+ Promise.resolve(client.disconnect()),
963
+ SHUTDOWN_DISCONNECT_TIMEOUT_MS,
964
+ "disconnect daemon websocket",
965
+ );
927
966
  } catch (error) {
928
967
  logError(`Failed to disconnect client on daemon close: ${error?.message || error}`);
929
968
  }
@@ -989,6 +1028,25 @@ function parsePositiveInt(value, fallback) {
989
1028
  return fallback;
990
1029
  }
991
1030
 
1031
+ async function withTimeout(promise, timeoutMs, label) {
1032
+ let timer = null;
1033
+ const timeoutPromise = new Promise((_, reject) => {
1034
+ timer = setTimeout(() => {
1035
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
1036
+ }, timeoutMs);
1037
+ if (typeof timer?.unref === "function") {
1038
+ timer.unref();
1039
+ }
1040
+ });
1041
+ try {
1042
+ return await Promise.race([promise, timeoutPromise]);
1043
+ } finally {
1044
+ if (timer) {
1045
+ clearTimeout(timer);
1046
+ }
1047
+ }
1048
+ }
1049
+
992
1050
  function expandHomePath(inputPath, homeDir) {
993
1051
  if (typeof inputPath !== "string" || !inputPath) {
994
1052
  return inputPath;