@clawos-dev/clawd 0.2.9 → 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/dist/cli.cjs +72 -29
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -13048,7 +13048,7 @@ function applyCommand(state, command, deps) {
13048
13048
  next.pendingPermissions = {};
13049
13049
  if (next.procAlive) {
13050
13050
  next.status = "stopping";
13051
- effects.push({ kind: "kill", signal: "SIGTERM", graceMs: 5e3 });
13051
+ effects.push({ kind: "kill", signal: "SIGKILL" });
13052
13052
  }
13053
13053
  return { state: next, effects };
13054
13054
  }
@@ -13073,7 +13073,7 @@ function applyCommand(state, command, deps) {
13073
13073
  case "delete": {
13074
13074
  next.pendingPermissions = {};
13075
13075
  if (next.procAlive) {
13076
- effects.push({ kind: "kill", signal: "SIGTERM", graceMs: 5e3 });
13076
+ effects.push({ kind: "kill", signal: "SIGKILL" });
13077
13077
  }
13078
13078
  effects.push({ kind: "delete-file" });
13079
13079
  effects.push({
@@ -13091,7 +13091,7 @@ function applyCommand(state, command, deps) {
13091
13091
  effects.push({ kind: "persist-file", file: next.file });
13092
13092
  effects.push(sessionInfoFrame(next.file));
13093
13093
  if (runtimePatch && next.procAlive) {
13094
- effects.push({ kind: "kill", signal: "SIGTERM", graceMs: 5e3 });
13094
+ effects.push({ kind: "kill", signal: "SIGKILL" });
13095
13095
  }
13096
13096
  return { state: next, effects };
13097
13097
  }
@@ -13105,7 +13105,7 @@ function applyCommand(state, command, deps) {
13105
13105
  effects.push({ kind: "persist-file", file: nextFile });
13106
13106
  effects.push(sessionInfoFrame(nextFile));
13107
13107
  if (next.procAlive) {
13108
- effects.push({ kind: "kill", signal: "SIGTERM", graceMs: 5e3 });
13108
+ effects.push({ kind: "kill", signal: "SIGKILL" });
13109
13109
  }
13110
13110
  return { state: next, effects };
13111
13111
  }
@@ -13135,7 +13135,7 @@ function applyCommand(state, command, deps) {
13135
13135
  effects.push({ kind: "persist-file", file: nextFile });
13136
13136
  effects.push(sessionInfoFrame(nextFile));
13137
13137
  if (next.procAlive) {
13138
- effects.push({ kind: "kill", signal: "SIGTERM", graceMs: 5e3 });
13138
+ effects.push({ kind: "kill", signal: "SIGKILL" });
13139
13139
  }
13140
13140
  return { state: next, effects };
13141
13141
  }
@@ -13257,6 +13257,7 @@ function reduceSession(state, input, deps) {
13257
13257
 
13258
13258
  // src/session/runner.ts
13259
13259
  var DEFAULT_CONTROL_REQUEST_TIMEOUT_MS = 1e4;
13260
+ var DEFAULT_WAIT_STOP_TIMEOUT_MS = 3e3;
13260
13261
  var SessionRunner = class {
13261
13262
  constructor(initial, hooks) {
13262
13263
  this.hooks = hooks;
@@ -13268,6 +13269,8 @@ var SessionRunner = class {
13268
13269
  stdoutBuf = "";
13269
13270
  // 未决 control_request 表;key = request_id
13270
13271
  pendingControlRequests = /* @__PURE__ */ new Map();
13272
+ // waitUntilStopped 排队的 resolve 回调;reducer 把 procAlive 翻成 false 后批量触发
13273
+ stopWaiters = [];
13271
13274
  getState() {
13272
13275
  return this.state;
13273
13276
  }
@@ -13284,6 +13287,40 @@ var SessionRunner = class {
13284
13287
  const { state, effects } = reduceSession(this.state, inputMsg, deps);
13285
13288
  this.state = state;
13286
13289
  for (const e of effects) this.applyEffect(e);
13290
+ if (!this.state.procAlive && this.stopWaiters.length > 0) {
13291
+ const waiters = this.stopWaiters;
13292
+ this.stopWaiters = [];
13293
+ for (const w of waiters) w();
13294
+ }
13295
+ }
13296
+ // 等子进程退出(procAlive=false)。manager.stop 在发完 SIGTERM 后调它确保
13297
+ // 真停下来再 ack 给前端,从而避免"删 worktree 时 CC 还活着持有文件锁"的 race。
13298
+ // 已经停止则立即 resolve;超时(默认 3000ms)抛错由上层透传成 RPC error
13299
+ async waitUntilStopped(timeoutMs = DEFAULT_WAIT_STOP_TIMEOUT_MS) {
13300
+ if (!this.state.procAlive) return;
13301
+ return new Promise((resolve, reject) => {
13302
+ let settled = false;
13303
+ const onStop = () => {
13304
+ if (settled) return;
13305
+ settled = true;
13306
+ clearTimeout(timer);
13307
+ resolve();
13308
+ };
13309
+ const timer = setTimeout(() => {
13310
+ if (settled) return;
13311
+ settled = true;
13312
+ const i = this.stopWaiters.indexOf(onStop);
13313
+ if (i >= 0) this.stopWaiters.splice(i, 1);
13314
+ const pid = this.proc?.pid ?? "n/a";
13315
+ reject(
13316
+ new Error(
13317
+ `wait stop timeout ${timeoutMs}ms: status=${this.state.status} procAlive=${this.state.procAlive} pid=${pid}`
13318
+ )
13319
+ );
13320
+ }, timeoutMs);
13321
+ timer.unref?.();
13322
+ this.stopWaiters.push(onStop);
13323
+ });
13287
13324
  }
13288
13325
  // 观察者 / 测试直接喂单行 stdout 进 reducer:保证 seq 分配走统一路径
13289
13326
  // 等价于往内部 stdout line buffer 里写完整一行
@@ -13374,7 +13411,7 @@ var SessionRunner = class {
13374
13411
  this.doSpawn(effect.ctx);
13375
13412
  break;
13376
13413
  case "kill":
13377
- this.doKill(effect.signal, effect.graceMs);
13414
+ this.doKill(effect.signal);
13378
13415
  break;
13379
13416
  case "write-stdin":
13380
13417
  this.proc?.stdin?.write(effect.payload);
@@ -13431,22 +13468,16 @@ var SessionRunner = class {
13431
13468
  this.input({ kind: "proc-error", message: err.message });
13432
13469
  });
13433
13470
  }
13434
- // kill 当前 proc,graceMs 后强杀
13435
- doKill(signal, graceMs) {
13471
+ // kill 当前 proc:单发 signal,没有"先 TERM 后 KILL"的兜底。
13472
+ // reducer 推下来的 signal SIGKILL(见 reducer.ts 各 kill 推送处)——选择 SIGKILL
13473
+ // 是因为 CC 在 streaming hang / 等 control_response / API 半连接 等场景下不响应 SIGTERM,
13474
+ // 直接 SIGKILL 一次到位。代价是 CC 来不及落盘当前轮 jsonl,但 observer 路径已经回灌过。
13475
+ doKill(signal) {
13436
13476
  if (!this.proc) return;
13437
- const p = this.proc;
13438
13477
  try {
13439
- p.kill(signal);
13478
+ this.proc.kill(signal);
13440
13479
  } catch {
13441
13480
  }
13442
- if (graceMs && graceMs > 0) {
13443
- setTimeout(() => {
13444
- try {
13445
- p.kill("SIGKILL");
13446
- } catch {
13447
- }
13448
- }, graceMs).unref();
13449
- }
13450
13481
  }
13451
13482
  };
13452
13483
 
@@ -13679,12 +13710,17 @@ var SessionManager = class {
13679
13710
  });
13680
13711
  return { response: { ok: true }, broadcast };
13681
13712
  }
13682
- stop(args) {
13713
+ // 等子进程真退出再 ack。reducer 推 SIGKILL effect(见 reducer.ts),内核立即回收子进程,
13714
+ // proc.on('exit') 几乎瞬时触发;waitUntilStopped 的 3s 超时只在子进程已是 zombie 或
13715
+ // 极端 OS 异常时才会用上。这样修掉了"前端 sessionStop 还没等 CC 退就去删 worktree
13716
+ // 撞文件锁"的 race,同时也覆盖 CC streaming hang / 幽灵进程这类 SIGTERM 杀不动的 case
13717
+ async stop(args) {
13683
13718
  const runner = this.runners.get(args.sessionId);
13684
13719
  if (!runner) return { response: { ok: true }, broadcast: [] };
13685
13720
  const { broadcast } = this.withCollector(() => {
13686
13721
  runner.input({ kind: "command", command: { kind: "stop" } });
13687
13722
  });
13723
+ await runner.waitUntilStopped();
13688
13724
  return { response: { ok: true }, broadcast };
13689
13725
  }
13690
13726
  // 温柔中断当前轮次:不杀进程,仅发 CC IPC `control_request.subtype=interrupt`
@@ -17246,7 +17282,7 @@ function buildSessionHandlers(deps) {
17246
17282
  };
17247
17283
  const stop = async (frame) => {
17248
17284
  const args = SessionIdArgs.parse(frame);
17249
- const { broadcast } = manager.stop(args);
17285
+ const { broadcast } = await manager.stop(args);
17250
17286
  return { response: { type: "session:stop", ok: true }, broadcast };
17251
17287
  };
17252
17288
  const interrupt = async (frame) => {
@@ -17460,6 +17496,17 @@ var import_node_os10 = __toESM(require("os"), 1);
17460
17496
  var import_node_path15 = __toESM(require("path"), 1);
17461
17497
  var import_node_util = require("util");
17462
17498
  var pexec = (0, import_node_util.promisify)(import_node_child_process5.execFile);
17499
+ function formatChildProcessError(err) {
17500
+ const e = err;
17501
+ const stderrLine = (e.stderr ?? "").split("\n").map((s) => s.trim()).find((s) => s.length > 0);
17502
+ if (stderrLine) return stderrLine;
17503
+ const parts = [];
17504
+ if (e.killed) parts.push("killed");
17505
+ if (e.signal) parts.push(`signal=${e.signal}`);
17506
+ if (e.code !== void 0 && e.code !== null) parts.push(`code=${e.code}`);
17507
+ if (parts.length > 0) return parts.join(" ");
17508
+ return e.message ?? "unknown error";
17509
+ }
17463
17510
  function normalizePath(p) {
17464
17511
  const resolved = import_node_path15.default.resolve(p);
17465
17512
  try {
@@ -17625,9 +17672,7 @@ async function createWorktree(input) {
17625
17672
  { timeout: 15e3 }
17626
17673
  );
17627
17674
  } catch (err) {
17628
- const stderr = err.stderr || err.message;
17629
- const firstLine = stderr.split("\n")[0].trim();
17630
- throw new Error(`\u521B\u5EFA worktree \u5931\u8D25\uFF1A${firstLine}`);
17675
+ throw new Error(`\u521B\u5EFA worktree \u5931\u8D25\uFF1A${formatChildProcessError(err)}`);
17631
17676
  }
17632
17677
  return { worktreeRoot, branch, baseBranch };
17633
17678
  }
@@ -17656,23 +17701,21 @@ async function removeWorktree(input) {
17656
17701
  timeout: 1e4
17657
17702
  });
17658
17703
  } catch (err) {
17659
- const stderr = err.stderr || err.message;
17704
+ const stderr = err.stderr ?? "";
17660
17705
  const lower = stderr.toLowerCase();
17661
17706
  const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !import_node_fs16.default.existsSync(worktreeRoot);
17662
17707
  if (!vanished) {
17663
- const firstLine = stderr.split("\n")[0].trim();
17664
- throw new Error(`\u6E05\u7406 worktree \u5931\u8D25\uFF1A${firstLine}`);
17708
+ throw new Error(`\u6E05\u7406 worktree \u5931\u8D25\uFF1A${formatChildProcessError(err)}`);
17665
17709
  }
17666
17710
  }
17667
17711
  try {
17668
17712
  await pexec("git", ["-C", repoRoot, "branch", "-D", worktreeBranch], { timeout: 5e3 });
17669
17713
  } catch (err) {
17670
- const stderr = err.stderr || err.message;
17714
+ const stderr = err.stderr ?? "";
17671
17715
  const lower = stderr.toLowerCase();
17672
17716
  const vanished = lower.includes("not found") || lower.includes("no such");
17673
17717
  if (!vanished) {
17674
- const firstLine = stderr.split("\n")[0].trim();
17675
- throw new Error(`\u6E05\u7406\u5206\u652F\u5931\u8D25\uFF1A${firstLine}`);
17718
+ throw new Error(`\u6E05\u7406\u5206\u652F\u5931\u8D25\uFF1A${formatChildProcessError(err)}`);
17676
17719
  }
17677
17720
  }
17678
17721
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",