@clawos-dev/clawd 0.2.414 → 0.2.415

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 +82 -20
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -45085,6 +45085,9 @@ function cloneState(s) {
45085
45085
  procAlive: s.procAlive,
45086
45086
  seenUuids: s.seenUuids,
45087
45087
  subSessionMeta: s.subSessionMeta,
45088
+ // 浅拷贝:下方 trackBackgroundTask 在 next 上原地增删,不能共享调用方数组(纯函数契约)
45089
+ pendingBgTasks: s.pendingBgTasks ? s.pendingBgTasks.slice() : void 0,
45090
+ stdinClosed: s.stdinClosed,
45088
45091
  // ReadyGate v2:readyForSend 是基础类型直接复制;pendingSend 必须浅拷贝防止
45089
45092
  // reduce 内 push/shift 在调用方原 state 上副作用(reducer 纯函数契约)
45090
45093
  readyForSend: s.readyForSend,
@@ -45372,6 +45375,16 @@ function pushEventToBufferInPlace(next, event, deps) {
45372
45375
  const isTurnEnd = withSeq.kind === "turn_end";
45373
45376
  const isUserText = withSeq.kind === "user_text";
45374
45377
  const isBackgroundEvent = withSeq.kind === "subagent_progress";
45378
+ if (isBackgroundEvent) {
45379
+ const bg = withSeq;
45380
+ const done = bg.status === "completed" || bg.status === "failed";
45381
+ const pending = next.pendingBgTasks ?? [];
45382
+ if (done) {
45383
+ next.pendingBgTasks = pending.filter((id) => id !== bg.agentId);
45384
+ } else if (bg.agentId && !pending.includes(bg.agentId)) {
45385
+ next.pendingBgTasks = [...pending, bg.agentId];
45386
+ }
45387
+ }
45375
45388
  if (isTurnEnd && next.turnOpen && isLateTurnEnd(withSeq, next.turnStartedAt, deps.now())) {
45376
45389
  return [emitSessionEvent(next.file.sessionId, withSeq)];
45377
45390
  }
@@ -45427,13 +45440,22 @@ function pushEventToBufferInPlace(next, event, deps) {
45427
45440
  status: "running-idle"
45428
45441
  }
45429
45442
  });
45430
- const idleKillMs = idleKillDelayOf(next);
45431
- if (idleKillMs !== void 0) {
45432
- effects.push({
45433
- kind: "schedule-idle-kill",
45434
- sessionId: next.file.sessionId,
45435
- ms: idleKillMs
45436
- });
45443
+ const tool = next.file.tool ?? "claude";
45444
+ const canCloseStdin = tool === "claude" && deps.mode !== "tui" && (next.pendingBgTasks?.length ?? 0) === 0;
45445
+ if (canCloseStdin) {
45446
+ next.stdinClosed = true;
45447
+ effects.push({ kind: "close-stdin" });
45448
+ } else if (tool === "codex") {
45449
+ effects.push({ kind: "kill", signal: "SIGTERM" });
45450
+ } else {
45451
+ const idleKillMs = idleKillDelayOf(next);
45452
+ if (idleKillMs !== void 0) {
45453
+ effects.push({
45454
+ kind: "schedule-idle-kill",
45455
+ sessionId: next.file.sessionId,
45456
+ ms: idleKillMs
45457
+ });
45458
+ }
45437
45459
  }
45438
45460
  }
45439
45461
  return effects;
@@ -45468,12 +45490,16 @@ function applyCommand(state, command, deps) {
45468
45490
  });
45469
45491
  }
45470
45492
  }
45493
+ if (next.procAlive && next.stdinClosed) {
45494
+ effects.push({ kind: "kill", signal: "SIGKILL" });
45495
+ next.procAlive = false;
45496
+ next.stdinClosed = false;
45497
+ }
45471
45498
  if (!next.procAlive) {
45472
45499
  next.procAlive = true;
45473
45500
  next.freshSpawn = true;
45501
+ next.stdinClosed = false;
45474
45502
  next.buffer = [];
45475
- next.bufferStartSeq = 0;
45476
- next.nextSeq = 0;
45477
45503
  next.turnOpen = false;
45478
45504
  next.turnStartedAt = null;
45479
45505
  next.status = "running";
@@ -45559,8 +45585,6 @@ function applyCommand(state, command, deps) {
45559
45585
  next.procAlive = true;
45560
45586
  next.freshSpawn = true;
45561
45587
  next.buffer = [];
45562
- next.bufferStartSeq = 0;
45563
- next.nextSeq = 0;
45564
45588
  next.turnOpen = false;
45565
45589
  next.turnStartedAt = null;
45566
45590
  next.status = "running";
@@ -45741,6 +45765,8 @@ function reduceSessionInner(state, input, deps) {
45741
45765
  const next = cloneState(state);
45742
45766
  next.procAlive = false;
45743
45767
  next.status = "stopped";
45768
+ next.stdinClosed = false;
45769
+ next.pendingBgTasks = [];
45744
45770
  const interruptEffects = next.turnOpen ? pushEventToBufferInPlace(next, { kind: "turn_end", reason: "interrupted" }, deps) : [];
45745
45771
  resetReadyGate(next);
45746
45772
  const sessionId = next.file.sessionId;
@@ -46234,8 +46260,7 @@ var SessionRunner = class {
46234
46260
  timer.unref?.();
46235
46261
  this.pendingControlRequests.set(requestId, { resolve: resolve9, reject, timer });
46236
46262
  try {
46237
- proc.stdin?.write(payload);
46238
- this.recorder?.tapStdinWrite(payload);
46263
+ if (!this.writeStdin(payload)) throw new Error(`stdin closed: ${subtype}`);
46239
46264
  } catch (err) {
46240
46265
  clearTimeout(timer);
46241
46266
  this.pendingControlRequests.delete(requestId);
@@ -46293,6 +46318,9 @@ var SessionRunner = class {
46293
46318
  }
46294
46319
  this.doKill(effect.signal);
46295
46320
  break;
46321
+ case "close-stdin":
46322
+ this.proc?.stdin?.end();
46323
+ break;
46296
46324
  case "write-stdin":
46297
46325
  if (this.session) {
46298
46326
  this.session.startTurn(effect.payload);
@@ -46304,8 +46332,7 @@ var SessionRunner = class {
46304
46332
  len: effect.payload.length,
46305
46333
  preview: effect.payload.replace(/\x1b\[20[01]~/g, "\xB7paste\xB7").slice(0, 60)
46306
46334
  });
46307
- this.proc?.stdin?.write(effect.payload);
46308
- this.recorder?.tapStdinWrite(effect.payload);
46335
+ this.writeStdin(effect.payload);
46309
46336
  break;
46310
46337
  case "respond-permission": {
46311
46338
  if (this.session) {
@@ -46313,14 +46340,12 @@ var SessionRunner = class {
46313
46340
  break;
46314
46341
  }
46315
46342
  const payload = encodePermissionResponse(effect.requestId, effect.allow, effect.input, effect.message);
46316
- this.proc?.stdin?.write(payload);
46317
- this.recorder?.tapStdinWrite(payload);
46343
+ this.writeStdin(payload);
46318
46344
  break;
46319
46345
  }
46320
46346
  case "send-control-response-allow-with-input": {
46321
46347
  const payload = encodeAllowWithInputControlResponse(effect.requestId, effect.updatedInput);
46322
- this.proc?.stdin?.write(payload);
46323
- this.recorder?.tapStdinWrite(payload);
46348
+ this.writeStdin(payload);
46324
46349
  break;
46325
46350
  }
46326
46351
  case "persist-file":
@@ -46427,6 +46452,7 @@ var SessionRunner = class {
46427
46452
  now: this.hooks.now
46428
46453
  });
46429
46454
  proc.stdout?.on("data", (chunk) => {
46455
+ if (this.proc !== proc) return;
46430
46456
  const { newBuf, lines } = splitStdoutChunk(this.stdoutBuf, chunk);
46431
46457
  this.stdoutBuf = newBuf;
46432
46458
  for (const line of lines) {
@@ -46435,10 +46461,21 @@ var SessionRunner = class {
46435
46461
  }
46436
46462
  });
46437
46463
  proc.stderr?.on("data", (chunk) => {
46464
+ if (this.proc !== proc) return;
46438
46465
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
46439
46466
  this.hooks.logger?.warn("tool stderr", { line: text.slice(0, 500) });
46440
46467
  });
46468
+ const stdin = proc.stdin;
46469
+ if (stdin && typeof stdin.on === "function") {
46470
+ stdin.on("error", (err) => {
46471
+ this.hooks.logger?.warn("stdin error (write dropped)", {
46472
+ sessionId: this.state.file.sessionId,
46473
+ message: err.message
46474
+ });
46475
+ });
46476
+ }
46441
46477
  proc.on("exit", (code) => {
46478
+ if (this.proc !== proc) return;
46442
46479
  this.proc = null;
46443
46480
  this.recorder = null;
46444
46481
  this.rejectAllPending(new Error("session gone"));
@@ -46446,11 +46483,33 @@ var SessionRunner = class {
46446
46483
  this.input({ kind: "proc-exit", code });
46447
46484
  });
46448
46485
  proc.on("error", (err) => {
46486
+ if (this.proc !== proc) return;
46449
46487
  const message = err.message;
46450
46488
  this.hooks.logger?.error("claude spawn failed", { message });
46451
46489
  this.input({ kind: "proc-error", message });
46452
46490
  });
46453
46491
  }
46492
+ /**
46493
+ * stdin 唯一写入口。stdin 已 EOF(close-stdin 收进程收尾中)时丢弃并告警——写进已关闭的
46494
+ * 管道会抛 EPIPE 把 daemon 打崩。reducer 侧保证正常路径不会撞上(send 遇 stdinClosed 先
46495
+ * kill 再 spawn),这道闸兜的是 permission / 问答回写等异步路径:它们的 effect 可能在
46496
+ * turn_end 关 stdin 之后才被执行。
46497
+ *
46498
+ * recorder tap 收在这里,保证"写成功才录",录制流与真实字节流不分叉。
46499
+ */
46500
+ writeStdin(payload) {
46501
+ const stdin = this.proc?.stdin;
46502
+ if (!stdin || stdin.writableEnded) {
46503
+ this.hooks.logger?.warn("stdin write dropped: pipe already closed", {
46504
+ sessionId: this.state.file.sessionId,
46505
+ len: payload.length
46506
+ });
46507
+ return false;
46508
+ }
46509
+ stdin.write(payload);
46510
+ this.recorder?.tapStdinWrite(payload);
46511
+ return true;
46512
+ }
46454
46513
  // kill 当前 proc:单发 signal,没有"先 TERM 后 KILL"的兜底。
46455
46514
  // reducer 推下来的 signal 是 SIGKILL(见 reducer.ts 各 kill 推送处)——选择 SIGKILL
46456
46515
  // 是因为 CC 在 streaming hang / 等 control_response / API 半连接 等场景下不响应 SIGTERM,
@@ -46550,6 +46609,9 @@ function makeInitialState(file, subSessionMeta) {
46550
46609
  procAlive: false,
46551
46610
  seenUuids: [],
46552
46611
  subSessionMeta,
46612
+ // 后台任务台账(close-stdin 判据);由 subagent_progress 事件流维护,spawn 时天然为空
46613
+ pendingBgTasks: [],
46614
+ stdinClosed: false,
46553
46615
  // ReadyGate v2:默认值兼容 SDK happy path(直写 stdin,不走队列)。
46554
46616
  // TUI 模式由 reducer send 分支按 deps.mode='tui' 主动翻 readyForSend=false
46555
46617
  readyForSend: true,
@@ -62216,7 +62278,7 @@ function computeMethodAccess(args) {
62216
62278
  }
62217
62279
 
62218
62280
  // src/version.ts
62219
- var version = "0.2.414".length > 0 ? "0.2.414" : "dev";
62281
+ var version = "0.2.415".length > 0 ? "0.2.415" : "dev";
62220
62282
 
62221
62283
  // src/cli-probe/probe.ts
62222
62284
  var fs65 = __toESM(require("fs"), 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.414",
3
+ "version": "0.2.415",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",