chatroom-cli 1.43.1 → 1.43.3

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/dist/index.js CHANGED
@@ -69493,6 +69493,7 @@ var init_errorCodes = __esm(() => {
69493
69493
  COMMAND_NOT_RUNNING: "COMMAND_NOT_RUNNING",
69494
69494
  TOO_MANY_COMMANDS: "TOO_MANY_COMMANDS",
69495
69495
  INVALID_STATE_TRANSITION: "INVALID_STATE_TRANSITION",
69496
+ INVALID_RUN_STATE_TRANSITION: "INVALID_RUN_STATE_TRANSITION",
69496
69497
  OUTPUT_CHUNK_TOO_LARGE: "OUTPUT_CHUNK_TOO_LARGE",
69497
69498
  NOT_FOUND: "NOT_FOUND",
69498
69499
  INVALID_BOT_TOKEN: "INVALID_BOT_TOKEN",
@@ -69573,6 +69574,7 @@ var init_errorCodes = __esm(() => {
69573
69574
  BACKEND_ERROR_CODES.INVALID_STDIN_FORMAT,
69574
69575
  BACKEND_ERROR_CODES.INVALID_BOT_TOKEN,
69575
69576
  BACKEND_ERROR_CODES.INVALID_STATE_TRANSITION,
69577
+ BACKEND_ERROR_CODES.INVALID_RUN_STATE_TRANSITION,
69576
69578
  BACKEND_ERROR_CODES.TASK_INVALID_TRANSITION,
69577
69579
  BACKEND_ERROR_CODES.TASK_MISSING_REQUIRED_FIELD,
69578
69580
  BACKEND_ERROR_CODES.TASK_VALIDATION_FAILED,
@@ -75936,31 +75938,248 @@ var init_file_tree_subscription = __esm(() => {
75936
75938
  init_convex_error();
75937
75939
  });
75938
75940
 
75939
- // src/commands/machine/daemon-start/handlers/command-runner.ts
75940
- import { spawn as spawn4 } from "node:child_process";
75941
- import { access as access3 } from "node:fs/promises";
75942
- function evictStalePendingStops() {
75943
- const evictBefore = Date.now() - PENDING_STOP_TTL_MS;
75944
- for (const [runId, ts] of pendingStops) {
75945
- if (ts < evictBefore)
75946
- pendingStops.delete(runId);
75941
+ // src/commands/machine/daemon-start/handlers/orphan-tracker.ts
75942
+ import { createHash as createHash6 } from "node:crypto";
75943
+ import {
75944
+ appendFileSync,
75945
+ existsSync as existsSync3,
75946
+ mkdirSync as mkdirSync3,
75947
+ readFileSync as readFileSync5,
75948
+ renameSync,
75949
+ unlinkSync as unlinkSync2,
75950
+ writeFileSync as writeFileSync3
75951
+ } from "node:fs";
75952
+ import { homedir as homedir6 } from "node:os";
75953
+ import { join as join15 } from "node:path";
75954
+ function getUrlHash2() {
75955
+ const url2 = getConvexUrl();
75956
+ return createHash6("sha256").update(url2).digest("hex").substring(0, 8);
75957
+ }
75958
+ function getChildPidsFilePath() {
75959
+ const dir = join15(homedir6(), ".chatroom");
75960
+ return join15(dir, `daemon-children-${getUrlHash2()}.pids`);
75961
+ }
75962
+ function ensureChatroomDir3() {
75963
+ const dir = join15(homedir6(), ".chatroom");
75964
+ if (!existsSync3(dir)) {
75965
+ mkdirSync3(dir, { recursive: true, mode: 448 });
75947
75966
  }
75948
75967
  }
75949
- function buildCommandKey(machineId, workingDir, commandName) {
75950
- return `${machineId}|${workingDir}|${commandName}`;
75968
+ function readPids() {
75969
+ const filePath = getChildPidsFilePath();
75970
+ if (!existsSync3(filePath))
75971
+ return [];
75972
+ try {
75973
+ return readFileSync5(filePath, "utf-8").split(`
75974
+ `).map((line) => parseInt(line.trim(), 10)).filter((n) => Number.isFinite(n) && n > 0);
75975
+ } catch {
75976
+ return [];
75977
+ }
75951
75978
  }
75952
- async function reportRunFailed(ctx, runId, reason) {
75979
+ function trackChildPid(pid) {
75953
75980
  try {
75954
- await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
75955
- sessionId: ctx.sessionId,
75956
- machineId: ctx.machineId,
75957
- runId,
75958
- status: "failed"
75981
+ ensureChatroomDir3();
75982
+ appendFileSync(getChildPidsFilePath(), `${pid}
75983
+ `, "utf-8");
75984
+ } catch (err) {
75985
+ console.warn(`[orphan-tracker] Failed to track child PID ${pid}: ${err}`);
75986
+ }
75987
+ }
75988
+ function untrackChildPid(pid) {
75989
+ try {
75990
+ const filePath = getChildPidsFilePath();
75991
+ if (!existsSync3(filePath))
75992
+ return;
75993
+ const remaining = readFileSync5(filePath, "utf-8").split(`
75994
+ `).filter((line) => {
75995
+ const n = parseInt(line.trim(), 10);
75996
+ return Number.isFinite(n) && n > 0 && n !== pid;
75959
75997
  });
75998
+ const tmpPath = `${filePath}.tmp`;
75999
+ writeFileSync3(tmpPath, remaining.join(`
76000
+ `) + (remaining.length > 0 ? `
76001
+ ` : ""), "utf-8");
76002
+ renameSync(tmpPath, filePath);
75960
76003
  } catch (err) {
75961
- console.warn(`[${formatTimestamp()}] ⚠️ Failed to report run failure (${reason}): ${getErrorMessage(err)}`);
76004
+ console.warn(`[orphan-tracker] Failed to untrack child PID ${pid}: ${err}`);
75962
76005
  }
75963
76006
  }
76007
+ function clearTrackedPids() {
76008
+ try {
76009
+ const filePath = getChildPidsFilePath();
76010
+ if (existsSync3(filePath)) {
76011
+ unlinkSync2(filePath);
76012
+ }
76013
+ } catch {}
76014
+ }
76015
+ async function reapOrphanedProcessGroups() {
76016
+ const pids = readPids();
76017
+ let reaped = 0;
76018
+ const checked = pids.length;
76019
+ for (const pgid of pids) {
76020
+ if (process.platform === "win32")
76021
+ continue;
76022
+ try {
76023
+ process.kill(-pgid, 0);
76024
+ } catch {
76025
+ continue;
76026
+ }
76027
+ try {
76028
+ process.kill(-pgid, "SIGTERM");
76029
+ } catch {
76030
+ continue;
76031
+ }
76032
+ const deadline = Date.now() + 500;
76033
+ let alive = true;
76034
+ while (Date.now() < deadline) {
76035
+ await new Promise((r) => setTimeout(r, 50));
76036
+ try {
76037
+ process.kill(-pgid, 0);
76038
+ } catch {
76039
+ alive = false;
76040
+ break;
76041
+ }
76042
+ }
76043
+ if (alive) {
76044
+ try {
76045
+ process.kill(-pgid, "SIGKILL");
76046
+ } catch {}
76047
+ }
76048
+ console.log(`[orphan-tracker] Reaped orphan process group ${pgid}`);
76049
+ reaped++;
76050
+ }
76051
+ clearTrackedPids();
76052
+ return { reaped, checked };
76053
+ }
76054
+ var CHATROOM_DIR5;
76055
+ var init_orphan_tracker = __esm(() => {
76056
+ init_client2();
76057
+ CHATROOM_DIR5 = join15(homedir6(), ".chatroom");
76058
+ });
76059
+
76060
+ // src/commands/machine/daemon-start/handlers/process/state.ts
76061
+ function deriveTerminalStatus(code2, signal, terminationIntent) {
76062
+ if (terminationIntent !== null)
76063
+ return terminationIntent;
76064
+ if (code2 === 0)
76065
+ return "completed";
76066
+ if (signal !== null)
76067
+ return "stopped";
76068
+ return "failed";
76069
+ }
76070
+ var TERMINAL_STATES, PENDING_STOP_TTL_MS = 60000, SIGTERM_GRACE_PERIOD_MS = 5000, SOFT_TIMEOUT_MS, OUTPUT_FLUSH_INTERVAL_MS = 3000, MAX_BUFFER_SIZE;
76071
+ var init_state2 = __esm(() => {
76072
+ TERMINAL_STATES = new Set(["completed", "failed", "stopped", "killed"]);
76073
+ SOFT_TIMEOUT_MS = 24 * 60 * 60 * 1000;
76074
+ MAX_BUFFER_SIZE = 100 * 1024;
76075
+ });
76076
+
76077
+ // src/commands/machine/daemon-start/handlers/process/manager.ts
76078
+ class ProcessManager {
76079
+ runningProcesses = new Map;
76080
+ runningProcessesByCommand = new Map;
76081
+ pendingStops = new Map;
76082
+ has(runId) {
76083
+ return this.runningProcesses.has(runId);
76084
+ }
76085
+ get(runId) {
76086
+ return this.runningProcesses.get(runId);
76087
+ }
76088
+ getByCommand(commandKey) {
76089
+ const runId = this.runningProcessesByCommand.get(commandKey);
76090
+ if (runId === undefined)
76091
+ return;
76092
+ return this.runningProcesses.get(runId);
76093
+ }
76094
+ getAll() {
76095
+ return [...this.runningProcesses.entries()];
76096
+ }
76097
+ get size() {
76098
+ return this.runningProcesses.size;
76099
+ }
76100
+ register(runId, commandKey, process2) {
76101
+ this.runningProcesses.set(runId, process2);
76102
+ this.runningProcessesByCommand.set(commandKey, runId);
76103
+ }
76104
+ unregister(runId, commandKey) {
76105
+ this.runningProcesses.delete(runId);
76106
+ if (this.runningProcessesByCommand.get(commandKey) === runId) {
76107
+ this.runningProcessesByCommand.delete(commandKey);
76108
+ }
76109
+ }
76110
+ markPendingStop(runId) {
76111
+ this.pendingStops.set(runId, Date.now());
76112
+ }
76113
+ hasPendingStop(runId) {
76114
+ return this.pendingStops.has(runId);
76115
+ }
76116
+ consumePendingStop(runId) {
76117
+ const has5 = this.pendingStops.has(runId);
76118
+ if (has5)
76119
+ this.pendingStops.delete(runId);
76120
+ return has5;
76121
+ }
76122
+ evictStalePendingStops() {
76123
+ const evictBefore = Date.now() - PENDING_STOP_TTL_MS;
76124
+ for (const [runId, ts] of this.pendingStops) {
76125
+ if (ts < evictBefore)
76126
+ this.pendingStops.delete(runId);
76127
+ }
76128
+ }
76129
+ clear() {
76130
+ this.runningProcesses.clear();
76131
+ this.runningProcessesByCommand.clear();
76132
+ this.pendingStops.clear();
76133
+ }
76134
+ waitForExit(runId, ms) {
76135
+ return new Promise((resolve5) => {
76136
+ const interval = 100;
76137
+ let elapsed3 = 0;
76138
+ const timer = setInterval(() => {
76139
+ if (!this.runningProcesses.has(runId)) {
76140
+ clearInterval(timer);
76141
+ resolve5(true);
76142
+ return;
76143
+ }
76144
+ elapsed3 += interval;
76145
+ if (elapsed3 >= ms) {
76146
+ clearInterval(timer);
76147
+ resolve5(false);
76148
+ }
76149
+ }, interval);
76150
+ });
76151
+ }
76152
+ }
76153
+ var processManager;
76154
+ var init_manager = __esm(() => {
76155
+ init_state2();
76156
+ processManager = new ProcessManager;
76157
+ });
76158
+
76159
+ // src/commands/machine/daemon-start/handlers/process/killer.ts
76160
+ function killProcess(child, signal) {
76161
+ if (child.pid == null)
76162
+ return;
76163
+ try {
76164
+ process.kill(-child.pid, signal);
76165
+ } catch {}
76166
+ }
76167
+ async function killTrackedProcess(tracked) {
76168
+ killProcess(tracked.process, "SIGTERM");
76169
+ const exited = await processManager.waitForExit(tracked.runId, SIGTERM_GRACE_PERIOD_MS);
76170
+ if (!exited) {
76171
+ console.log(`[${formatTimestamp()}] \uD83D\uDD2A Force-killing process: ${tracked.runId}`);
76172
+ killProcess(tracked.process, "SIGKILL");
76173
+ await processManager.waitForExit(tracked.runId, 1000);
76174
+ }
76175
+ }
76176
+ var init_killer = __esm(() => {
76177
+ init_state2();
76178
+ init_manager();
76179
+ });
76180
+
76181
+ // src/commands/machine/daemon-start/handlers/process/spawner.ts
76182
+ import { spawn as spawn4 } from "node:child_process";
75964
76183
  async function flushOutput(ctx, tracked) {
75965
76184
  if (tracked.outputBuffer.length === 0)
75966
76185
  return;
@@ -75986,91 +76205,14 @@ function appendToBuffer(ctx, tracked, data) {
75986
76205
  flushOutput(ctx, tracked).catch(() => {});
75987
76206
  }
75988
76207
  }
75989
- function killProcess(child, signal) {
75990
- try {
75991
- child.kill(signal);
75992
- } catch {}
75993
- }
75994
- function waitForExit(runIdStr, ms) {
75995
- return new Promise((resolve5) => {
75996
- const interval = 100;
75997
- let elapsed3 = 0;
75998
- const timer = setInterval(() => {
75999
- if (!runningProcesses.has(runIdStr)) {
76000
- clearInterval(timer);
76001
- resolve5(true);
76002
- return;
76003
- }
76004
- elapsed3 += interval;
76005
- if (elapsed3 >= ms) {
76006
- clearInterval(timer);
76007
- resolve5(false);
76008
- }
76009
- }, interval);
76010
- });
76011
- }
76012
- async function killTrackedProcess(tracked) {
76013
- killProcess(tracked.process, "SIGTERM");
76014
- const exited = await waitForExit(tracked.runId, SIGTERM_GRACE_PERIOD_MS);
76015
- if (!exited) {
76016
- console.log(`[${formatTimestamp()}] \uD83D\uDD2A Force-killing process: ${tracked.runId}`);
76017
- killProcess(tracked.process, "SIGKILL");
76018
- await waitForExit(tracked.runId, 1000);
76019
- }
76020
- }
76021
- async function onCommandRun(ctx, event) {
76208
+ function spawnCommandProcess(ctx, event, commandKey) {
76022
76209
  const { workingDir, commandName, script, runId } = event;
76023
76210
  const runIdStr = runId.toString();
76024
- const commandKey = buildCommandKey(ctx.machineId, workingDir, commandName);
76025
- if (runningProcesses.has(runIdStr)) {
76026
- console.log(`[${formatTimestamp()}] ⚠️ Command already running: ${runIdStr}`);
76027
- return;
76028
- }
76029
- if (pendingStops.has(runIdStr)) {
76030
- pendingStops.delete(runIdStr);
76031
- console.log(`[${formatTimestamp()}] ⏭️ Skipping command run due to pending stop: ${commandName} (${runIdStr})`);
76032
- try {
76033
- await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76034
- sessionId: ctx.sessionId,
76035
- machineId: ctx.machineId,
76036
- runId,
76037
- status: "stopped"
76038
- });
76039
- } catch (err) {
76040
- console.warn(`[${formatTimestamp()}] ⚠️ Failed to update status to stopped for pending-stop skip: ${getErrorMessage(err)}`);
76041
- }
76042
- return;
76043
- }
76044
- const priorRunId = runningProcessesByCommand.get(commandKey);
76045
- if (priorRunId) {
76046
- const priorTracked = runningProcesses.get(priorRunId);
76047
- if (priorTracked) {
76048
- console.log(`[${formatTimestamp()}] \uD83D\uDD04 Replacing prior run ${priorRunId} with ${runIdStr} for ${commandName}`);
76049
- clearInterval(priorTracked.flushTimer);
76050
- if (priorTracked.softTimeoutTimer)
76051
- clearTimeout(priorTracked.softTimeoutTimer);
76052
- await killTrackedProcess(priorTracked);
76053
- runningProcesses.delete(priorRunId);
76054
- runningProcessesByCommand.delete(commandKey);
76055
- }
76056
- }
76057
- console.log(`[${formatTimestamp()}] \uD83D\uDE80 Running command: ${commandName} → ${script}`);
76058
- if (!workingDir.startsWith("/")) {
76059
- console.error(`[${formatTimestamp()}] ❌ Rejected command: workingDir is not absolute: ${workingDir}`);
76060
- await reportRunFailed(ctx, runId, "Working directory is not an absolute path");
76061
- return;
76062
- }
76063
- try {
76064
- await access3(workingDir);
76065
- } catch {
76066
- console.error(`[${formatTimestamp()}] ❌ Rejected command: workingDir not found: ${workingDir}`);
76067
- await reportRunFailed(ctx, runId, "Working directory not found");
76068
- return;
76069
- }
76070
76211
  const child = spawn4("sh", ["-c", script], {
76071
76212
  cwd: workingDir,
76072
76213
  env: { ...process.env },
76073
- stdio: ["ignore", "pipe", "pipe"]
76214
+ stdio: ["ignore", "pipe", "pipe"],
76215
+ detached: true
76074
76216
  });
76075
76217
  const tracked = {
76076
76218
  process: child,
@@ -76081,16 +76223,20 @@ async function onCommandRun(ctx, event) {
76081
76223
  flushTimer: setInterval(() => {
76082
76224
  flushOutput(ctx, tracked).catch(() => {});
76083
76225
  }, OUTPUT_FLUSH_INTERVAL_MS),
76084
- softTimeoutTimer: null
76226
+ softTimeoutTimer: null,
76227
+ terminationIntent: null
76085
76228
  };
76086
76229
  tracked.flushTimer.unref?.();
76087
- runningProcesses.set(runIdStr, tracked);
76088
- runningProcessesByCommand.set(commandKey, runIdStr);
76230
+ processManager.register(runIdStr, commandKey, tracked);
76231
+ if (child.pid != null) {
76232
+ trackChildPid(child.pid);
76233
+ }
76089
76234
  const softTimeoutTimer = setTimeout(async () => {
76090
76235
  console.log(`[${formatTimestamp()}] ⏰ Command soft timeout (24h): ${commandName} (runId: ${runIdStr})`);
76091
- const currentTracked = runningProcesses.get(runIdStr);
76236
+ const currentTracked = processManager.get(runIdStr);
76092
76237
  if (!currentTracked)
76093
76238
  return;
76239
+ currentTracked.terminationIntent = "killed";
76094
76240
  try {
76095
76241
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76096
76242
  sessionId: ctx.sessionId,
@@ -76104,7 +76250,7 @@ async function onCommandRun(ctx, event) {
76104
76250
  }
76105
76251
  killProcess(child, "SIGTERM");
76106
76252
  setTimeout(() => {
76107
- if (!runningProcesses.has(runIdStr))
76253
+ if (!processManager.has(runIdStr))
76108
76254
  return;
76109
76255
  console.log(`[${formatTimestamp()}] \uD83D\uDD2A Force-killing timed-out process: ${runIdStr}`);
76110
76256
  killProcess(child, "SIGKILL");
@@ -76112,17 +76258,6 @@ async function onCommandRun(ctx, event) {
76112
76258
  }, SOFT_TIMEOUT_MS);
76113
76259
  softTimeoutTimer.unref?.();
76114
76260
  tracked.softTimeoutTimer = softTimeoutTimer;
76115
- try {
76116
- await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76117
- sessionId: ctx.sessionId,
76118
- machineId: ctx.machineId,
76119
- runId,
76120
- status: "running",
76121
- pid: child.pid
76122
- });
76123
- } catch (err) {
76124
- console.warn(`[${formatTimestamp()}] ⚠️ Failed to update run status to running: ${getErrorMessage(err)}`);
76125
- }
76126
76261
  child.stdout?.on("data", (data) => {
76127
76262
  appendToBuffer(ctx, tracked, data.toString());
76128
76263
  });
@@ -76132,14 +76267,14 @@ async function onCommandRun(ctx, event) {
76132
76267
  child.on("exit", async (code2, signal) => {
76133
76268
  console.log(`[${formatTimestamp()}] \uD83C\uDFC1 Command exited: ${commandName} (code=${code2}, signal=${signal})`);
76134
76269
  await flushOutput(ctx, tracked).catch(() => {});
76270
+ if (tracked.process.pid != null) {
76271
+ untrackChildPid(tracked.process.pid);
76272
+ }
76135
76273
  clearInterval(tracked.flushTimer);
76136
76274
  if (tracked.softTimeoutTimer)
76137
76275
  clearTimeout(tracked.softTimeoutTimer);
76138
- runningProcesses.delete(runIdStr);
76139
- if (runningProcessesByCommand.get(commandKey) === runIdStr) {
76140
- runningProcessesByCommand.delete(commandKey);
76141
- }
76142
- const status3 = code2 === 0 ? "completed" : signal ? "stopped" : "failed";
76276
+ processManager.unregister(runIdStr, commandKey);
76277
+ const status3 = deriveTerminalStatus(code2, signal, tracked.terminationIntent);
76143
76278
  try {
76144
76279
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76145
76280
  sessionId: ctx.sessionId,
@@ -76154,13 +76289,13 @@ async function onCommandRun(ctx, event) {
76154
76289
  });
76155
76290
  child.on("error", async (err) => {
76156
76291
  console.error(`[${formatTimestamp()}] ❌ Command spawn failed: ${commandName}: ${err.message}`);
76292
+ if (tracked.process.pid != null) {
76293
+ untrackChildPid(tracked.process.pid);
76294
+ }
76157
76295
  clearInterval(tracked.flushTimer);
76158
76296
  if (tracked.softTimeoutTimer)
76159
76297
  clearTimeout(tracked.softTimeoutTimer);
76160
- runningProcesses.delete(runIdStr);
76161
- if (runningProcessesByCommand.get(commandKey) === runIdStr) {
76162
- runningProcessesByCommand.delete(commandKey);
76163
- }
76298
+ processManager.unregister(runIdStr, commandKey);
76164
76299
  try {
76165
76300
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76166
76301
  sessionId: ctx.sessionId,
@@ -76172,13 +76307,111 @@ async function onCommandRun(ctx, event) {
76172
76307
  console.warn(`[${formatTimestamp()}] ⚠️ Failed to update run status on error: ${getErrorMessage(updateErr)}`);
76173
76308
  }
76174
76309
  });
76310
+ return tracked;
76311
+ }
76312
+ var init_spawner = __esm(() => {
76313
+ init_api3();
76314
+ init_convex_error();
76315
+ init_orphan_tracker();
76316
+ init_state2();
76317
+ init_manager();
76318
+ init_killer();
76319
+ });
76320
+
76321
+ // src/commands/machine/daemon-start/handlers/command-runner.ts
76322
+ import { access as access3 } from "node:fs/promises";
76323
+ function buildCommandKey(machineId, workingDir, commandName) {
76324
+ return `${machineId}|${workingDir}|${commandName}`;
76325
+ }
76326
+ async function reportRunFailed(ctx, runId, reason) {
76327
+ try {
76328
+ await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76329
+ sessionId: ctx.sessionId,
76330
+ machineId: ctx.machineId,
76331
+ runId,
76332
+ status: "failed"
76333
+ });
76334
+ } catch (err) {
76335
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to report run failure (${reason}): ${getErrorMessage(err)}`);
76336
+ }
76337
+ }
76338
+ async function onCommandRun(ctx, event) {
76339
+ const { workingDir, commandName, script, runId } = event;
76340
+ const runIdStr = runId.toString();
76341
+ const commandKey = buildCommandKey(ctx.machineId, workingDir, commandName);
76342
+ if (processManager.has(runIdStr)) {
76343
+ console.log(`[${formatTimestamp()}] ⚠️ Command already running: ${runIdStr}`);
76344
+ return;
76345
+ }
76346
+ if (processManager.consumePendingStop(runIdStr)) {
76347
+ console.log(`[${formatTimestamp()}] ⏭️ Skipping command run due to pending stop: ${commandName} (${runIdStr})`);
76348
+ try {
76349
+ await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76350
+ sessionId: ctx.sessionId,
76351
+ machineId: ctx.machineId,
76352
+ runId,
76353
+ status: "stopped"
76354
+ });
76355
+ } catch (err) {
76356
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to update status to stopped for pending-stop skip: ${getErrorMessage(err)}`);
76357
+ }
76358
+ return;
76359
+ }
76360
+ try {
76361
+ const currentRun = await ctx.deps.backend.query(api.commands.getRunStatus, {
76362
+ sessionId: ctx.sessionId,
76363
+ machineId: ctx.machineId,
76364
+ runId
76365
+ });
76366
+ if (currentRun && TERMINAL_STATES.has(currentRun.status)) {
76367
+ console.log(`[${formatTimestamp()}] ⏭️ Skipping command run — row already ${currentRun.status}: ${commandName} (${runIdStr})`);
76368
+ return;
76369
+ }
76370
+ } catch (err) {
76371
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to check run status before spawn: ${getErrorMessage(err)}`);
76372
+ }
76373
+ const priorTracked = processManager.getByCommand(commandKey);
76374
+ if (priorTracked) {
76375
+ console.log(`[${formatTimestamp()}] \uD83D\uDD04 Replacing prior run ${priorTracked.runId} with ${runIdStr} for ${commandName}`);
76376
+ priorTracked.terminationIntent = "killed";
76377
+ clearInterval(priorTracked.flushTimer);
76378
+ if (priorTracked.softTimeoutTimer)
76379
+ clearTimeout(priorTracked.softTimeoutTimer);
76380
+ await killTrackedProcess(priorTracked);
76381
+ processManager.unregister(priorTracked.runId, commandKey);
76382
+ }
76383
+ console.log(`[${formatTimestamp()}] \uD83D\uDE80 Running command: ${commandName} → ${script}`);
76384
+ if (!workingDir.startsWith("/")) {
76385
+ console.error(`[${formatTimestamp()}] ❌ Rejected command: workingDir is not absolute: ${workingDir}`);
76386
+ await reportRunFailed(ctx, runId, "Working directory is not an absolute path");
76387
+ return;
76388
+ }
76389
+ try {
76390
+ await access3(workingDir);
76391
+ } catch {
76392
+ console.error(`[${formatTimestamp()}] ❌ Rejected command: workingDir not found: ${workingDir}`);
76393
+ await reportRunFailed(ctx, runId, "Working directory not found");
76394
+ return;
76395
+ }
76396
+ const tracked = spawnCommandProcess(ctx, event, commandKey);
76397
+ try {
76398
+ await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76399
+ sessionId: ctx.sessionId,
76400
+ machineId: ctx.machineId,
76401
+ runId,
76402
+ status: "running",
76403
+ pid: tracked.process.pid
76404
+ });
76405
+ } catch (err) {
76406
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to update run status to running: ${getErrorMessage(err)}`);
76407
+ }
76175
76408
  }
76176
76409
  async function onCommandStop(ctx, event) {
76177
76410
  const runIdStr = event.runId.toString();
76178
- const tracked = runningProcesses.get(runIdStr);
76411
+ const tracked = processManager.get(runIdStr);
76179
76412
  if (!tracked) {
76180
76413
  console.log(`[${formatTimestamp()}] ⚠️ No running process found for run: ${runIdStr} — marking as stopped`);
76181
- pendingStops.set(runIdStr, Date.now());
76414
+ processManager.markPendingStop(runIdStr);
76182
76415
  try {
76183
76416
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76184
76417
  sessionId: ctx.sessionId,
@@ -76197,13 +76430,8 @@ async function onCommandStop(ctx, event) {
76197
76430
  clearTimeout(tracked.softTimeoutTimer);
76198
76431
  tracked.softTimeoutTimer = null;
76199
76432
  }
76200
- killProcess(tracked.process, "SIGTERM");
76201
- const exitedAfterSigterm = await waitForExit(runIdStr, SIGTERM_GRACE_PERIOD_MS);
76202
- if (!exitedAfterSigterm) {
76203
- console.log(`[${formatTimestamp()}] \uD83D\uDD2A Force-killing process: ${runIdStr}`);
76204
- killProcess(tracked.process, "SIGKILL");
76205
- await waitForExit(runIdStr, 1000);
76206
- }
76433
+ tracked.terminationIntent = "stopped";
76434
+ await killTrackedProcess(tracked);
76207
76435
  try {
76208
76436
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76209
76437
  sessionId: ctx.sessionId,
@@ -76216,15 +76444,14 @@ async function onCommandStop(ctx, event) {
76216
76444
  }
76217
76445
  }
76218
76446
  async function shutdownAllCommands(ctx) {
76219
- if (runningProcesses.size === 0)
76447
+ if (processManager.size === 0)
76220
76448
  return;
76221
- console.log(`[${formatTimestamp()}] Shutting down ${runningProcesses.size} running command(s)...`);
76222
- const trackedEntries = [...runningProcesses.entries()];
76449
+ console.log(`[${formatTimestamp()}] Shutting down ${processManager.size} running command(s)...`);
76450
+ const trackedEntries = processManager.getAll();
76223
76451
  for (const [, tracked] of trackedEntries) {
76224
76452
  clearInterval(tracked.flushTimer);
76225
76453
  if (tracked.softTimeoutTimer)
76226
76454
  clearTimeout(tracked.softTimeoutTimer);
76227
- await flushOutput(ctx, tracked).catch(() => {});
76228
76455
  try {
76229
76456
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76230
76457
  sessionId: ctx.sessionId,
@@ -76243,23 +76470,22 @@ async function shutdownAllCommands(ctx) {
76243
76470
  t.unref?.();
76244
76471
  });
76245
76472
  for (const [, tracked] of trackedEntries) {
76246
- if (runningProcesses.has(tracked.runId)) {
76473
+ if (processManager.has(tracked.runId)) {
76247
76474
  killProcess(tracked.process, "SIGKILL");
76248
76475
  }
76249
76476
  }
76250
- runningProcesses.clear();
76251
- runningProcessesByCommand.clear();
76477
+ processManager.clear();
76478
+ clearTrackedPids();
76252
76479
  console.log(`[${formatTimestamp()}] All commands stopped`);
76253
76480
  }
76254
- var runningProcesses, runningProcessesByCommand, pendingStops, PENDING_STOP_TTL_MS = 60000, OUTPUT_FLUSH_INTERVAL_MS = 3000, MAX_BUFFER_SIZE, SOFT_TIMEOUT_MS, SIGTERM_GRACE_PERIOD_MS = 5000;
76255
76481
  var init_command_runner = __esm(() => {
76256
76482
  init_api3();
76257
76483
  init_convex_error();
76258
- runningProcesses = new Map;
76259
- runningProcessesByCommand = new Map;
76260
- pendingStops = new Map;
76261
- MAX_BUFFER_SIZE = 100 * 1024;
76262
- SOFT_TIMEOUT_MS = 24 * 60 * 60 * 1000;
76484
+ init_orphan_tracker();
76485
+ init_state2();
76486
+ init_manager();
76487
+ init_spawner();
76488
+ init_killer();
76263
76489
  });
76264
76490
 
76265
76491
  // src/commands/machine/daemon-start/handlers/ping.ts
@@ -77228,21 +77454,25 @@ async function recoverState(ctx) {
77228
77454
  console.log(` ⚠️ Failed to clear stale PIDs: ${getErrorMessage(e)}`);
77229
77455
  }
77230
77456
  try {
77231
- const runResult = await ctx.deps.backend.mutation(api.commands.clearStaleCommandRuns, {
77457
+ const runResult = await ctx.deps.backend.mutation(api.commands.reapOrphansForDaemonRestart, {
77232
77458
  sessionId: ctx.sessionId,
77233
77459
  machineId: ctx.machineId
77234
77460
  });
77235
- if (runResult.clearedCount > 0) {
77236
- console.log(` \uD83E\uDDF9 Cleared ${runResult.clearedCount} stale command run(s) from backend`);
77461
+ if (runResult.reapedCount > 0) {
77462
+ console.log(` \uD83E\uDDF9 Reaped ${runResult.reapedCount} command run(s) from previous daemon run (marked as daemon-restart)`);
77237
77463
  }
77238
77464
  } catch (e) {
77239
- console.log(` ⚠️ Failed to clear stale command runs: ${getErrorMessage(e)}`);
77465
+ console.warn(` ⚠️ Failed to reap orphan command runs: ${getErrorMessage(e)}`);
77240
77466
  }
77241
77467
  }
77242
77468
  async function initDaemon() {
77243
77469
  if (!acquireLock()) {
77244
77470
  process.exit(1);
77245
77471
  }
77472
+ const { reaped } = await reapOrphanedProcessGroups();
77473
+ if (reaped > 0) {
77474
+ console.log(`[${formatTimestamp()}] Reaped ${reaped} orphaned process group(s) from previous daemon run`);
77475
+ }
77246
77476
  const convexUrl = getConvexUrl();
77247
77477
  const sessionId = await validateAuthentication(convexUrl);
77248
77478
  const client4 = await getConvexClient();
@@ -77330,6 +77560,7 @@ var init_init2 = __esm(() => {
77330
77560
  init_error_formatting();
77331
77561
  init_version();
77332
77562
  init_pid();
77563
+ init_orphan_tracker();
77333
77564
  AUTH_WAIT_TIMEOUT_MS = 5 * 60 * 1000;
77334
77565
  });
77335
77566
 
@@ -78075,7 +78306,7 @@ function evictStaleDedupEntries(tracker) {
78075
78306
  if (ts < evictBefore)
78076
78307
  tracker.commandStopIds.delete(id3);
78077
78308
  }
78078
- evictStalePendingStops();
78309
+ processManager.evictStalePendingStops();
78079
78310
  }
78080
78311
  async function dispatchCommandEvent(ctx, event, tracker) {
78081
78312
  const eventId = event._id.toString();
@@ -78331,6 +78562,7 @@ var init_command_loop = __esm(() => {
78331
78562
  init_file_tree_subscription();
78332
78563
  init_git_subscription();
78333
78564
  init_command_runner();
78565
+ init_manager();
78334
78566
  init_init2();
78335
78567
  init_observed_sync();
78336
78568
  init_api3();
@@ -79446,5 +79678,5 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
79446
79678
  });
79447
79679
  program2.parse();
79448
79680
 
79449
- //# debugId=1151E4B582DED8E964756E2164756E21
79681
+ //# debugId=A50E5218AF2CBB4A64756E2164756E21
79450
79682
  //# sourceMappingURL=index.js.map