chatroom-cli 1.43.2 → 1.44.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/dist/index.js CHANGED
@@ -27123,6 +27123,207 @@ var init_claude = __esm(() => {
27123
27123
  init_claude_code_agent_service();
27124
27124
  });
27125
27125
 
27126
+ // src/infrastructure/services/remote-agents/commandcode/command-code-stream-reader.ts
27127
+ import { createInterface as createInterface5 } from "node:readline";
27128
+
27129
+ class CommandCodeStreamReader {
27130
+ textCallbacks = [];
27131
+ agentEndCallbacks = [];
27132
+ anyEventCallbacks = [];
27133
+ constructor(stream) {
27134
+ const rl = createInterface5({ input: stream, crlfDelay: Infinity });
27135
+ rl.on("line", (line) => {
27136
+ const trimmed = line.trim();
27137
+ if (!trimmed)
27138
+ return;
27139
+ for (const cb of this.anyEventCallbacks)
27140
+ cb();
27141
+ for (const cb of this.textCallbacks)
27142
+ cb(trimmed);
27143
+ });
27144
+ stream.on("close", () => {
27145
+ for (const cb of this.agentEndCallbacks)
27146
+ cb();
27147
+ });
27148
+ }
27149
+ onText(cb) {
27150
+ this.textCallbacks.push(cb);
27151
+ }
27152
+ onAnyEvent(cb) {
27153
+ this.anyEventCallbacks.push(cb);
27154
+ }
27155
+ onAgentEnd(cb) {
27156
+ this.agentEndCallbacks.push(cb);
27157
+ }
27158
+ }
27159
+ var init_command_code_stream_reader = () => {};
27160
+
27161
+ // src/infrastructure/services/remote-agents/commandcode/command-code-agent-service.ts
27162
+ var COMMANDCODE_COMMAND = "cmd", COMMANDCODE_MODELS, CommandCodeAgentService;
27163
+ var init_command_code_agent_service = __esm(() => {
27164
+ init_base_cli_agent_service();
27165
+ init_command_code_stream_reader();
27166
+ COMMANDCODE_MODELS = [
27167
+ "claude-sonnet-4-6",
27168
+ "claude-opus-4-7",
27169
+ "claude-opus-4-6",
27170
+ "claude-haiku-4-5",
27171
+ "gpt-5.5",
27172
+ "gpt-5.4",
27173
+ "gpt-5.3-codex",
27174
+ "gpt-5.4-mini",
27175
+ "google/gemini-3.5-flash",
27176
+ "google/gemini-3.1-flash-lite",
27177
+ "moonshotai/Kimi-K2.6",
27178
+ "moonshotai/Kimi-K2.5",
27179
+ "zai-org/GLM-5.1",
27180
+ "zai-org/GLM-5",
27181
+ "MiniMaxAI/MiniMax-M2.7",
27182
+ "MiniMaxAI/MiniMax-M2.5",
27183
+ "deepseek/deepseek-v4-pro",
27184
+ "deepseek/deepseek-v4-flash",
27185
+ "Qwen/Qwen3.6-Max-Preview",
27186
+ "Qwen/Qwen3.6-Plus",
27187
+ "Qwen/Qwen3.7-Max",
27188
+ "stepfun/Step-3.5-Flash"
27189
+ ];
27190
+ CommandCodeAgentService = class CommandCodeAgentService extends BaseCLIAgentService {
27191
+ id = "commandcode";
27192
+ displayName = "CommandCode";
27193
+ command = COMMANDCODE_COMMAND;
27194
+ constructor(deps) {
27195
+ super(deps);
27196
+ }
27197
+ async isInstalled() {
27198
+ return this.checkInstalled(COMMANDCODE_COMMAND);
27199
+ }
27200
+ async getVersion() {
27201
+ return this.checkVersion(COMMANDCODE_COMMAND);
27202
+ }
27203
+ async listModels() {
27204
+ return COMMANDCODE_MODELS;
27205
+ }
27206
+ async spawn(options) {
27207
+ const args2 = ["-p", "--skip-onboarding", "--yolo", "--max-turns", "999999"];
27208
+ if (options.model) {
27209
+ args2.push("--model", options.model);
27210
+ }
27211
+ const fullPrompt = options.systemPrompt ? `${options.systemPrompt}
27212
+
27213
+ ${options.prompt}` : options.prompt;
27214
+ const childProcess = this.deps.spawn(COMMANDCODE_COMMAND, args2, {
27215
+ cwd: options.workingDir,
27216
+ stdio: ["pipe", "pipe", "pipe"],
27217
+ shell: false,
27218
+ detached: true,
27219
+ env: {
27220
+ ...process.env,
27221
+ GIT_EDITOR: "true",
27222
+ GIT_SEQUENCE_EDITOR: "true"
27223
+ }
27224
+ });
27225
+ childProcess.stdin?.write(fullPrompt);
27226
+ childProcess.stdin?.end();
27227
+ await new Promise((resolve) => setTimeout(resolve, 500));
27228
+ if (childProcess.killed || childProcess.exitCode !== null) {
27229
+ throw new Error(`Agent process exited immediately (exit code: ${childProcess.exitCode})`);
27230
+ }
27231
+ if (!childProcess.pid) {
27232
+ throw new Error("Agent process started but has no PID");
27233
+ }
27234
+ const pid = childProcess.pid;
27235
+ const context4 = options.context;
27236
+ const entry = this.registerProcess(pid, context4);
27237
+ const roleTag = context4.role ?? "unknown";
27238
+ const chatroomSuffix = context4.chatroomId ? `@${context4.chatroomId.slice(-6)}` : "";
27239
+ const logPrefix = `[commandcode:${roleTag}${chatroomSuffix}`;
27240
+ const outputCallbacks = [];
27241
+ if (childProcess.stdout) {
27242
+ const reader = new CommandCodeStreamReader(childProcess.stdout);
27243
+ let textBuffer = "";
27244
+ const flushText = () => {
27245
+ if (!textBuffer)
27246
+ return;
27247
+ for (const line of textBuffer.split(`
27248
+ `)) {
27249
+ if (line)
27250
+ process.stdout.write(`${logPrefix} text] ${line}
27251
+ `);
27252
+ }
27253
+ textBuffer = "";
27254
+ };
27255
+ reader.onText((text) => {
27256
+ textBuffer += text;
27257
+ if (textBuffer.includes(`
27258
+ `))
27259
+ flushText();
27260
+ entry.lastOutputAt = Date.now();
27261
+ for (const cb of outputCallbacks)
27262
+ cb();
27263
+ });
27264
+ reader.onAnyEvent(() => {
27265
+ entry.lastOutputAt = Date.now();
27266
+ for (const cb of outputCallbacks)
27267
+ cb();
27268
+ });
27269
+ reader.onAgentEnd(() => {
27270
+ flushText();
27271
+ process.stdout.write(`${logPrefix} agent_end]
27272
+ `);
27273
+ });
27274
+ if (childProcess.stderr) {
27275
+ childProcess.stderr.pipe(process.stderr, { end: false });
27276
+ childProcess.stderr.on("data", () => {
27277
+ entry.lastOutputAt = Date.now();
27278
+ for (const cb of outputCallbacks)
27279
+ cb();
27280
+ });
27281
+ }
27282
+ return {
27283
+ pid,
27284
+ onExit: (cb) => {
27285
+ childProcess.on("exit", (code2, signal) => {
27286
+ this.deleteProcess(pid);
27287
+ cb({ code: code2, signal, context: context4 });
27288
+ });
27289
+ },
27290
+ onOutput: (cb) => {
27291
+ outputCallbacks.push(cb);
27292
+ },
27293
+ onAgentEnd: (cb) => {
27294
+ reader.onAgentEnd(cb);
27295
+ }
27296
+ };
27297
+ }
27298
+ if (childProcess.stderr) {
27299
+ childProcess.stderr.pipe(process.stderr, { end: false });
27300
+ childProcess.stderr.on("data", () => {
27301
+ entry.lastOutputAt = Date.now();
27302
+ for (const cb of outputCallbacks)
27303
+ cb();
27304
+ });
27305
+ }
27306
+ return {
27307
+ pid,
27308
+ onExit: (cb) => {
27309
+ childProcess.on("exit", (code2, signal) => {
27310
+ this.deleteProcess(pid);
27311
+ cb({ code: code2, signal, context: context4 });
27312
+ });
27313
+ },
27314
+ onOutput: (cb) => {
27315
+ outputCallbacks.push(cb);
27316
+ }
27317
+ };
27318
+ }
27319
+ };
27320
+ });
27321
+
27322
+ // src/infrastructure/services/remote-agents/commandcode/index.ts
27323
+ var init_commandcode = __esm(() => {
27324
+ init_command_code_agent_service();
27325
+ });
27326
+
27126
27327
  // ../../node_modules/.pnpm/@opencode-ai+sdk@1.14.22/node_modules/@opencode-ai/sdk/dist/gen/types.gen.js
27127
27328
  var init_types_gen = () => {};
27128
27329
 
@@ -29517,12 +29718,14 @@ function initHarnessRegistry() {
29517
29718
  registerHarness(new PiAgentService);
29518
29719
  registerHarness(new CursorAgentService);
29519
29720
  registerHarness(new ClaudeCodeAgentService);
29721
+ registerHarness(new CommandCodeAgentService);
29520
29722
  registerHarness(new CopilotAgentService);
29521
29723
  initialized = true;
29522
29724
  }
29523
29725
  var initialized = false;
29524
29726
  var init_init_registry = __esm(() => {
29525
29727
  init_claude();
29728
+ init_commandcode();
29526
29729
  init_copilot();
29527
29730
  init_cursor();
29528
29731
  init_opencode();
@@ -69493,6 +69696,7 @@ var init_errorCodes = __esm(() => {
69493
69696
  COMMAND_NOT_RUNNING: "COMMAND_NOT_RUNNING",
69494
69697
  TOO_MANY_COMMANDS: "TOO_MANY_COMMANDS",
69495
69698
  INVALID_STATE_TRANSITION: "INVALID_STATE_TRANSITION",
69699
+ INVALID_RUN_STATE_TRANSITION: "INVALID_RUN_STATE_TRANSITION",
69496
69700
  OUTPUT_CHUNK_TOO_LARGE: "OUTPUT_CHUNK_TOO_LARGE",
69497
69701
  NOT_FOUND: "NOT_FOUND",
69498
69702
  INVALID_BOT_TOKEN: "INVALID_BOT_TOKEN",
@@ -69573,6 +69777,7 @@ var init_errorCodes = __esm(() => {
69573
69777
  BACKEND_ERROR_CODES.INVALID_STDIN_FORMAT,
69574
69778
  BACKEND_ERROR_CODES.INVALID_BOT_TOKEN,
69575
69779
  BACKEND_ERROR_CODES.INVALID_STATE_TRANSITION,
69780
+ BACKEND_ERROR_CODES.INVALID_RUN_STATE_TRANSITION,
69576
69781
  BACKEND_ERROR_CODES.TASK_INVALID_TRANSITION,
69577
69782
  BACKEND_ERROR_CODES.TASK_MISSING_REQUIRED_FIELD,
69578
69783
  BACKEND_ERROR_CODES.TASK_VALIDATION_FAILED,
@@ -75936,31 +76141,248 @@ var init_file_tree_subscription = __esm(() => {
75936
76141
  init_convex_error();
75937
76142
  });
75938
76143
 
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);
76144
+ // src/commands/machine/daemon-start/handlers/orphan-tracker.ts
76145
+ import { createHash as createHash6 } from "node:crypto";
76146
+ import {
76147
+ appendFileSync,
76148
+ existsSync as existsSync3,
76149
+ mkdirSync as mkdirSync3,
76150
+ readFileSync as readFileSync5,
76151
+ renameSync,
76152
+ unlinkSync as unlinkSync2,
76153
+ writeFileSync as writeFileSync3
76154
+ } from "node:fs";
76155
+ import { homedir as homedir6 } from "node:os";
76156
+ import { join as join15 } from "node:path";
76157
+ function getUrlHash2() {
76158
+ const url2 = getConvexUrl();
76159
+ return createHash6("sha256").update(url2).digest("hex").substring(0, 8);
76160
+ }
76161
+ function getChildPidsFilePath() {
76162
+ const dir = join15(homedir6(), ".chatroom");
76163
+ return join15(dir, `daemon-children-${getUrlHash2()}.pids`);
76164
+ }
76165
+ function ensureChatroomDir3() {
76166
+ const dir = join15(homedir6(), ".chatroom");
76167
+ if (!existsSync3(dir)) {
76168
+ mkdirSync3(dir, { recursive: true, mode: 448 });
75947
76169
  }
75948
76170
  }
75949
- function buildCommandKey(machineId, workingDir, commandName) {
75950
- return `${machineId}|${workingDir}|${commandName}`;
76171
+ function readPids() {
76172
+ const filePath = getChildPidsFilePath();
76173
+ if (!existsSync3(filePath))
76174
+ return [];
76175
+ try {
76176
+ return readFileSync5(filePath, "utf-8").split(`
76177
+ `).map((line) => parseInt(line.trim(), 10)).filter((n) => Number.isFinite(n) && n > 0);
76178
+ } catch {
76179
+ return [];
76180
+ }
75951
76181
  }
75952
- async function reportRunFailed(ctx, runId, reason) {
76182
+ function trackChildPid(pid) {
75953
76183
  try {
75954
- await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
75955
- sessionId: ctx.sessionId,
75956
- machineId: ctx.machineId,
75957
- runId,
75958
- status: "failed"
76184
+ ensureChatroomDir3();
76185
+ appendFileSync(getChildPidsFilePath(), `${pid}
76186
+ `, "utf-8");
76187
+ } catch (err) {
76188
+ console.warn(`[orphan-tracker] Failed to track child PID ${pid}: ${err}`);
76189
+ }
76190
+ }
76191
+ function untrackChildPid(pid) {
76192
+ try {
76193
+ const filePath = getChildPidsFilePath();
76194
+ if (!existsSync3(filePath))
76195
+ return;
76196
+ const remaining = readFileSync5(filePath, "utf-8").split(`
76197
+ `).filter((line) => {
76198
+ const n = parseInt(line.trim(), 10);
76199
+ return Number.isFinite(n) && n > 0 && n !== pid;
75959
76200
  });
76201
+ const tmpPath = `${filePath}.tmp`;
76202
+ writeFileSync3(tmpPath, remaining.join(`
76203
+ `) + (remaining.length > 0 ? `
76204
+ ` : ""), "utf-8");
76205
+ renameSync(tmpPath, filePath);
75960
76206
  } catch (err) {
75961
- console.warn(`[${formatTimestamp()}] ⚠️ Failed to report run failure (${reason}): ${getErrorMessage(err)}`);
76207
+ console.warn(`[orphan-tracker] Failed to untrack child PID ${pid}: ${err}`);
75962
76208
  }
75963
76209
  }
76210
+ function clearTrackedPids() {
76211
+ try {
76212
+ const filePath = getChildPidsFilePath();
76213
+ if (existsSync3(filePath)) {
76214
+ unlinkSync2(filePath);
76215
+ }
76216
+ } catch {}
76217
+ }
76218
+ async function reapOrphanedProcessGroups() {
76219
+ const pids = readPids();
76220
+ let reaped = 0;
76221
+ const checked = pids.length;
76222
+ for (const pgid of pids) {
76223
+ if (process.platform === "win32")
76224
+ continue;
76225
+ try {
76226
+ process.kill(-pgid, 0);
76227
+ } catch {
76228
+ continue;
76229
+ }
76230
+ try {
76231
+ process.kill(-pgid, "SIGTERM");
76232
+ } catch {
76233
+ continue;
76234
+ }
76235
+ const deadline = Date.now() + 500;
76236
+ let alive = true;
76237
+ while (Date.now() < deadline) {
76238
+ await new Promise((r) => setTimeout(r, 50));
76239
+ try {
76240
+ process.kill(-pgid, 0);
76241
+ } catch {
76242
+ alive = false;
76243
+ break;
76244
+ }
76245
+ }
76246
+ if (alive) {
76247
+ try {
76248
+ process.kill(-pgid, "SIGKILL");
76249
+ } catch {}
76250
+ }
76251
+ console.log(`[orphan-tracker] Reaped orphan process group ${pgid}`);
76252
+ reaped++;
76253
+ }
76254
+ clearTrackedPids();
76255
+ return { reaped, checked };
76256
+ }
76257
+ var CHATROOM_DIR5;
76258
+ var init_orphan_tracker = __esm(() => {
76259
+ init_client2();
76260
+ CHATROOM_DIR5 = join15(homedir6(), ".chatroom");
76261
+ });
76262
+
76263
+ // src/commands/machine/daemon-start/handlers/process/state.ts
76264
+ function deriveTerminalStatus(code2, signal, terminationIntent) {
76265
+ if (terminationIntent !== null)
76266
+ return terminationIntent;
76267
+ if (code2 === 0)
76268
+ return "completed";
76269
+ if (signal !== null)
76270
+ return "stopped";
76271
+ return "failed";
76272
+ }
76273
+ var TERMINAL_STATES, PENDING_STOP_TTL_MS = 60000, SIGTERM_GRACE_PERIOD_MS = 5000, SOFT_TIMEOUT_MS, OUTPUT_FLUSH_INTERVAL_MS = 3000, MAX_BUFFER_SIZE;
76274
+ var init_state2 = __esm(() => {
76275
+ TERMINAL_STATES = new Set(["completed", "failed", "stopped", "killed"]);
76276
+ SOFT_TIMEOUT_MS = 24 * 60 * 60 * 1000;
76277
+ MAX_BUFFER_SIZE = 100 * 1024;
76278
+ });
76279
+
76280
+ // src/commands/machine/daemon-start/handlers/process/manager.ts
76281
+ class ProcessManager {
76282
+ runningProcesses = new Map;
76283
+ runningProcessesByCommand = new Map;
76284
+ pendingStops = new Map;
76285
+ has(runId) {
76286
+ return this.runningProcesses.has(runId);
76287
+ }
76288
+ get(runId) {
76289
+ return this.runningProcesses.get(runId);
76290
+ }
76291
+ getByCommand(commandKey) {
76292
+ const runId = this.runningProcessesByCommand.get(commandKey);
76293
+ if (runId === undefined)
76294
+ return;
76295
+ return this.runningProcesses.get(runId);
76296
+ }
76297
+ getAll() {
76298
+ return [...this.runningProcesses.entries()];
76299
+ }
76300
+ get size() {
76301
+ return this.runningProcesses.size;
76302
+ }
76303
+ register(runId, commandKey, process2) {
76304
+ this.runningProcesses.set(runId, process2);
76305
+ this.runningProcessesByCommand.set(commandKey, runId);
76306
+ }
76307
+ unregister(runId, commandKey) {
76308
+ this.runningProcesses.delete(runId);
76309
+ if (this.runningProcessesByCommand.get(commandKey) === runId) {
76310
+ this.runningProcessesByCommand.delete(commandKey);
76311
+ }
76312
+ }
76313
+ markPendingStop(runId) {
76314
+ this.pendingStops.set(runId, Date.now());
76315
+ }
76316
+ hasPendingStop(runId) {
76317
+ return this.pendingStops.has(runId);
76318
+ }
76319
+ consumePendingStop(runId) {
76320
+ const has5 = this.pendingStops.has(runId);
76321
+ if (has5)
76322
+ this.pendingStops.delete(runId);
76323
+ return has5;
76324
+ }
76325
+ evictStalePendingStops() {
76326
+ const evictBefore = Date.now() - PENDING_STOP_TTL_MS;
76327
+ for (const [runId, ts] of this.pendingStops) {
76328
+ if (ts < evictBefore)
76329
+ this.pendingStops.delete(runId);
76330
+ }
76331
+ }
76332
+ clear() {
76333
+ this.runningProcesses.clear();
76334
+ this.runningProcessesByCommand.clear();
76335
+ this.pendingStops.clear();
76336
+ }
76337
+ waitForExit(runId, ms) {
76338
+ return new Promise((resolve5) => {
76339
+ const interval = 100;
76340
+ let elapsed3 = 0;
76341
+ const timer = setInterval(() => {
76342
+ if (!this.runningProcesses.has(runId)) {
76343
+ clearInterval(timer);
76344
+ resolve5(true);
76345
+ return;
76346
+ }
76347
+ elapsed3 += interval;
76348
+ if (elapsed3 >= ms) {
76349
+ clearInterval(timer);
76350
+ resolve5(false);
76351
+ }
76352
+ }, interval);
76353
+ });
76354
+ }
76355
+ }
76356
+ var processManager;
76357
+ var init_manager = __esm(() => {
76358
+ init_state2();
76359
+ processManager = new ProcessManager;
76360
+ });
76361
+
76362
+ // src/commands/machine/daemon-start/handlers/process/killer.ts
76363
+ function killProcess(child, signal) {
76364
+ if (child.pid == null)
76365
+ return;
76366
+ try {
76367
+ process.kill(-child.pid, signal);
76368
+ } catch {}
76369
+ }
76370
+ async function killTrackedProcess(tracked) {
76371
+ killProcess(tracked.process, "SIGTERM");
76372
+ const exited = await processManager.waitForExit(tracked.runId, SIGTERM_GRACE_PERIOD_MS);
76373
+ if (!exited) {
76374
+ console.log(`[${formatTimestamp()}] \uD83D\uDD2A Force-killing process: ${tracked.runId}`);
76375
+ killProcess(tracked.process, "SIGKILL");
76376
+ await processManager.waitForExit(tracked.runId, 1000);
76377
+ }
76378
+ }
76379
+ var init_killer = __esm(() => {
76380
+ init_state2();
76381
+ init_manager();
76382
+ });
76383
+
76384
+ // src/commands/machine/daemon-start/handlers/process/spawner.ts
76385
+ import { spawn as spawn4 } from "node:child_process";
75964
76386
  async function flushOutput(ctx, tracked) {
75965
76387
  if (tracked.outputBuffer.length === 0)
75966
76388
  return;
@@ -75986,91 +76408,14 @@ function appendToBuffer(ctx, tracked, data) {
75986
76408
  flushOutput(ctx, tracked).catch(() => {});
75987
76409
  }
75988
76410
  }
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) {
76411
+ function spawnCommandProcess(ctx, event, commandKey) {
76022
76412
  const { workingDir, commandName, script, runId } = event;
76023
76413
  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
76414
  const child = spawn4("sh", ["-c", script], {
76071
76415
  cwd: workingDir,
76072
76416
  env: { ...process.env },
76073
- stdio: ["ignore", "pipe", "pipe"]
76417
+ stdio: ["ignore", "pipe", "pipe"],
76418
+ detached: true
76074
76419
  });
76075
76420
  const tracked = {
76076
76421
  process: child,
@@ -76081,16 +76426,20 @@ async function onCommandRun(ctx, event) {
76081
76426
  flushTimer: setInterval(() => {
76082
76427
  flushOutput(ctx, tracked).catch(() => {});
76083
76428
  }, OUTPUT_FLUSH_INTERVAL_MS),
76084
- softTimeoutTimer: null
76429
+ softTimeoutTimer: null,
76430
+ terminationIntent: null
76085
76431
  };
76086
76432
  tracked.flushTimer.unref?.();
76087
- runningProcesses.set(runIdStr, tracked);
76088
- runningProcessesByCommand.set(commandKey, runIdStr);
76433
+ processManager.register(runIdStr, commandKey, tracked);
76434
+ if (child.pid != null) {
76435
+ trackChildPid(child.pid);
76436
+ }
76089
76437
  const softTimeoutTimer = setTimeout(async () => {
76090
76438
  console.log(`[${formatTimestamp()}] ⏰ Command soft timeout (24h): ${commandName} (runId: ${runIdStr})`);
76091
- const currentTracked = runningProcesses.get(runIdStr);
76439
+ const currentTracked = processManager.get(runIdStr);
76092
76440
  if (!currentTracked)
76093
76441
  return;
76442
+ currentTracked.terminationIntent = "killed";
76094
76443
  try {
76095
76444
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76096
76445
  sessionId: ctx.sessionId,
@@ -76104,7 +76453,7 @@ async function onCommandRun(ctx, event) {
76104
76453
  }
76105
76454
  killProcess(child, "SIGTERM");
76106
76455
  setTimeout(() => {
76107
- if (!runningProcesses.has(runIdStr))
76456
+ if (!processManager.has(runIdStr))
76108
76457
  return;
76109
76458
  console.log(`[${formatTimestamp()}] \uD83D\uDD2A Force-killing timed-out process: ${runIdStr}`);
76110
76459
  killProcess(child, "SIGKILL");
@@ -76112,17 +76461,6 @@ async function onCommandRun(ctx, event) {
76112
76461
  }, SOFT_TIMEOUT_MS);
76113
76462
  softTimeoutTimer.unref?.();
76114
76463
  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
76464
  child.stdout?.on("data", (data) => {
76127
76465
  appendToBuffer(ctx, tracked, data.toString());
76128
76466
  });
@@ -76132,14 +76470,14 @@ async function onCommandRun(ctx, event) {
76132
76470
  child.on("exit", async (code2, signal) => {
76133
76471
  console.log(`[${formatTimestamp()}] \uD83C\uDFC1 Command exited: ${commandName} (code=${code2}, signal=${signal})`);
76134
76472
  await flushOutput(ctx, tracked).catch(() => {});
76473
+ if (tracked.process.pid != null) {
76474
+ untrackChildPid(tracked.process.pid);
76475
+ }
76135
76476
  clearInterval(tracked.flushTimer);
76136
76477
  if (tracked.softTimeoutTimer)
76137
76478
  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";
76479
+ processManager.unregister(runIdStr, commandKey);
76480
+ const status3 = deriveTerminalStatus(code2, signal, tracked.terminationIntent);
76143
76481
  try {
76144
76482
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76145
76483
  sessionId: ctx.sessionId,
@@ -76154,13 +76492,13 @@ async function onCommandRun(ctx, event) {
76154
76492
  });
76155
76493
  child.on("error", async (err) => {
76156
76494
  console.error(`[${formatTimestamp()}] ❌ Command spawn failed: ${commandName}: ${err.message}`);
76495
+ if (tracked.process.pid != null) {
76496
+ untrackChildPid(tracked.process.pid);
76497
+ }
76157
76498
  clearInterval(tracked.flushTimer);
76158
76499
  if (tracked.softTimeoutTimer)
76159
76500
  clearTimeout(tracked.softTimeoutTimer);
76160
- runningProcesses.delete(runIdStr);
76161
- if (runningProcessesByCommand.get(commandKey) === runIdStr) {
76162
- runningProcessesByCommand.delete(commandKey);
76163
- }
76501
+ processManager.unregister(runIdStr, commandKey);
76164
76502
  try {
76165
76503
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76166
76504
  sessionId: ctx.sessionId,
@@ -76172,13 +76510,111 @@ async function onCommandRun(ctx, event) {
76172
76510
  console.warn(`[${formatTimestamp()}] ⚠️ Failed to update run status on error: ${getErrorMessage(updateErr)}`);
76173
76511
  }
76174
76512
  });
76513
+ return tracked;
76514
+ }
76515
+ var init_spawner = __esm(() => {
76516
+ init_api3();
76517
+ init_convex_error();
76518
+ init_orphan_tracker();
76519
+ init_state2();
76520
+ init_manager();
76521
+ init_killer();
76522
+ });
76523
+
76524
+ // src/commands/machine/daemon-start/handlers/command-runner.ts
76525
+ import { access as access3 } from "node:fs/promises";
76526
+ function buildCommandKey(machineId, workingDir, commandName) {
76527
+ return `${machineId}|${workingDir}|${commandName}`;
76528
+ }
76529
+ async function reportRunFailed(ctx, runId, reason) {
76530
+ try {
76531
+ await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76532
+ sessionId: ctx.sessionId,
76533
+ machineId: ctx.machineId,
76534
+ runId,
76535
+ status: "failed"
76536
+ });
76537
+ } catch (err) {
76538
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to report run failure (${reason}): ${getErrorMessage(err)}`);
76539
+ }
76540
+ }
76541
+ async function onCommandRun(ctx, event) {
76542
+ const { workingDir, commandName, script, runId } = event;
76543
+ const runIdStr = runId.toString();
76544
+ const commandKey = buildCommandKey(ctx.machineId, workingDir, commandName);
76545
+ if (processManager.has(runIdStr)) {
76546
+ console.log(`[${formatTimestamp()}] ⚠️ Command already running: ${runIdStr}`);
76547
+ return;
76548
+ }
76549
+ if (processManager.consumePendingStop(runIdStr)) {
76550
+ console.log(`[${formatTimestamp()}] ⏭️ Skipping command run due to pending stop: ${commandName} (${runIdStr})`);
76551
+ try {
76552
+ await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76553
+ sessionId: ctx.sessionId,
76554
+ machineId: ctx.machineId,
76555
+ runId,
76556
+ status: "stopped"
76557
+ });
76558
+ } catch (err) {
76559
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to update status to stopped for pending-stop skip: ${getErrorMessage(err)}`);
76560
+ }
76561
+ return;
76562
+ }
76563
+ try {
76564
+ const currentRun = await ctx.deps.backend.query(api.commands.getRunStatus, {
76565
+ sessionId: ctx.sessionId,
76566
+ machineId: ctx.machineId,
76567
+ runId
76568
+ });
76569
+ if (currentRun && TERMINAL_STATES.has(currentRun.status)) {
76570
+ console.log(`[${formatTimestamp()}] ⏭️ Skipping command run — row already ${currentRun.status}: ${commandName} (${runIdStr})`);
76571
+ return;
76572
+ }
76573
+ } catch (err) {
76574
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to check run status before spawn: ${getErrorMessage(err)}`);
76575
+ }
76576
+ const priorTracked = processManager.getByCommand(commandKey);
76577
+ if (priorTracked) {
76578
+ console.log(`[${formatTimestamp()}] \uD83D\uDD04 Replacing prior run ${priorTracked.runId} with ${runIdStr} for ${commandName}`);
76579
+ priorTracked.terminationIntent = "killed";
76580
+ clearInterval(priorTracked.flushTimer);
76581
+ if (priorTracked.softTimeoutTimer)
76582
+ clearTimeout(priorTracked.softTimeoutTimer);
76583
+ await killTrackedProcess(priorTracked);
76584
+ processManager.unregister(priorTracked.runId, commandKey);
76585
+ }
76586
+ console.log(`[${formatTimestamp()}] \uD83D\uDE80 Running command: ${commandName} → ${script}`);
76587
+ if (!workingDir.startsWith("/")) {
76588
+ console.error(`[${formatTimestamp()}] ❌ Rejected command: workingDir is not absolute: ${workingDir}`);
76589
+ await reportRunFailed(ctx, runId, "Working directory is not an absolute path");
76590
+ return;
76591
+ }
76592
+ try {
76593
+ await access3(workingDir);
76594
+ } catch {
76595
+ console.error(`[${formatTimestamp()}] ❌ Rejected command: workingDir not found: ${workingDir}`);
76596
+ await reportRunFailed(ctx, runId, "Working directory not found");
76597
+ return;
76598
+ }
76599
+ const tracked = spawnCommandProcess(ctx, event, commandKey);
76600
+ try {
76601
+ await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76602
+ sessionId: ctx.sessionId,
76603
+ machineId: ctx.machineId,
76604
+ runId,
76605
+ status: "running",
76606
+ pid: tracked.process.pid
76607
+ });
76608
+ } catch (err) {
76609
+ console.warn(`[${formatTimestamp()}] ⚠️ Failed to update run status to running: ${getErrorMessage(err)}`);
76610
+ }
76175
76611
  }
76176
76612
  async function onCommandStop(ctx, event) {
76177
76613
  const runIdStr = event.runId.toString();
76178
- const tracked = runningProcesses.get(runIdStr);
76614
+ const tracked = processManager.get(runIdStr);
76179
76615
  if (!tracked) {
76180
76616
  console.log(`[${formatTimestamp()}] ⚠️ No running process found for run: ${runIdStr} — marking as stopped`);
76181
- pendingStops.set(runIdStr, Date.now());
76617
+ processManager.markPendingStop(runIdStr);
76182
76618
  try {
76183
76619
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76184
76620
  sessionId: ctx.sessionId,
@@ -76197,13 +76633,8 @@ async function onCommandStop(ctx, event) {
76197
76633
  clearTimeout(tracked.softTimeoutTimer);
76198
76634
  tracked.softTimeoutTimer = null;
76199
76635
  }
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
- }
76636
+ tracked.terminationIntent = "stopped";
76637
+ await killTrackedProcess(tracked);
76207
76638
  try {
76208
76639
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76209
76640
  sessionId: ctx.sessionId,
@@ -76216,15 +76647,14 @@ async function onCommandStop(ctx, event) {
76216
76647
  }
76217
76648
  }
76218
76649
  async function shutdownAllCommands(ctx) {
76219
- if (runningProcesses.size === 0)
76650
+ if (processManager.size === 0)
76220
76651
  return;
76221
- console.log(`[${formatTimestamp()}] Shutting down ${runningProcesses.size} running command(s)...`);
76222
- const trackedEntries = [...runningProcesses.entries()];
76652
+ console.log(`[${formatTimestamp()}] Shutting down ${processManager.size} running command(s)...`);
76653
+ const trackedEntries = processManager.getAll();
76223
76654
  for (const [, tracked] of trackedEntries) {
76224
76655
  clearInterval(tracked.flushTimer);
76225
76656
  if (tracked.softTimeoutTimer)
76226
76657
  clearTimeout(tracked.softTimeoutTimer);
76227
- await flushOutput(ctx, tracked).catch(() => {});
76228
76658
  try {
76229
76659
  await ctx.deps.backend.mutation(api.commands.updateRunStatus, {
76230
76660
  sessionId: ctx.sessionId,
@@ -76243,23 +76673,22 @@ async function shutdownAllCommands(ctx) {
76243
76673
  t.unref?.();
76244
76674
  });
76245
76675
  for (const [, tracked] of trackedEntries) {
76246
- if (runningProcesses.has(tracked.runId)) {
76676
+ if (processManager.has(tracked.runId)) {
76247
76677
  killProcess(tracked.process, "SIGKILL");
76248
76678
  }
76249
76679
  }
76250
- runningProcesses.clear();
76251
- runningProcessesByCommand.clear();
76680
+ processManager.clear();
76681
+ clearTrackedPids();
76252
76682
  console.log(`[${formatTimestamp()}] All commands stopped`);
76253
76683
  }
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
76684
  var init_command_runner = __esm(() => {
76256
76685
  init_api3();
76257
76686
  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;
76687
+ init_orphan_tracker();
76688
+ init_state2();
76689
+ init_manager();
76690
+ init_spawner();
76691
+ init_killer();
76263
76692
  });
76264
76693
 
76265
76694
  // src/commands/machine/daemon-start/handlers/ping.ts
@@ -77228,21 +77657,25 @@ async function recoverState(ctx) {
77228
77657
  console.log(` ⚠️ Failed to clear stale PIDs: ${getErrorMessage(e)}`);
77229
77658
  }
77230
77659
  try {
77231
- const runResult = await ctx.deps.backend.mutation(api.commands.clearStaleCommandRuns, {
77660
+ const runResult = await ctx.deps.backend.mutation(api.commands.reapOrphansForDaemonRestart, {
77232
77661
  sessionId: ctx.sessionId,
77233
77662
  machineId: ctx.machineId
77234
77663
  });
77235
- if (runResult.clearedCount > 0) {
77236
- console.log(` \uD83E\uDDF9 Cleared ${runResult.clearedCount} stale command run(s) from backend`);
77664
+ if (runResult.reapedCount > 0) {
77665
+ console.log(` \uD83E\uDDF9 Reaped ${runResult.reapedCount} command run(s) from previous daemon run (marked as daemon-restart)`);
77237
77666
  }
77238
77667
  } catch (e) {
77239
- console.log(` ⚠️ Failed to clear stale command runs: ${getErrorMessage(e)}`);
77668
+ console.warn(` ⚠️ Failed to reap orphan command runs: ${getErrorMessage(e)}`);
77240
77669
  }
77241
77670
  }
77242
77671
  async function initDaemon() {
77243
77672
  if (!acquireLock()) {
77244
77673
  process.exit(1);
77245
77674
  }
77675
+ const { reaped } = await reapOrphanedProcessGroups();
77676
+ if (reaped > 0) {
77677
+ console.log(`[${formatTimestamp()}] Reaped ${reaped} orphaned process group(s) from previous daemon run`);
77678
+ }
77246
77679
  const convexUrl = getConvexUrl();
77247
77680
  const sessionId = await validateAuthentication(convexUrl);
77248
77681
  const client4 = await getConvexClient();
@@ -77330,6 +77763,7 @@ var init_init2 = __esm(() => {
77330
77763
  init_error_formatting();
77331
77764
  init_version();
77332
77765
  init_pid();
77766
+ init_orphan_tracker();
77333
77767
  AUTH_WAIT_TIMEOUT_MS = 5 * 60 * 1000;
77334
77768
  });
77335
77769
 
@@ -78075,7 +78509,7 @@ function evictStaleDedupEntries(tracker) {
78075
78509
  if (ts < evictBefore)
78076
78510
  tracker.commandStopIds.delete(id3);
78077
78511
  }
78078
- evictStalePendingStops();
78512
+ processManager.evictStalePendingStops();
78079
78513
  }
78080
78514
  async function dispatchCommandEvent(ctx, event, tracker) {
78081
78515
  const eventId = event._id.toString();
@@ -78331,6 +78765,7 @@ var init_command_loop = __esm(() => {
78331
78765
  init_file_tree_subscription();
78332
78766
  init_git_subscription();
78333
78767
  init_command_runner();
78768
+ init_manager();
78334
78769
  init_init2();
78335
78770
  init_observed_sync();
78336
78771
  init_api3();
@@ -79446,5 +79881,5 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
79446
79881
  });
79447
79882
  program2.parse();
79448
79883
 
79449
- //# debugId=1151E4B582DED8E964756E2164756E21
79884
+ //# debugId=DFA3745A7992596664756E2164756E21
79450
79885
  //# sourceMappingURL=index.js.map