@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.
- package/dist/cli.cjs +72 -29
- 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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
13435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|