@clawos-dev/clawd 0.2.199-beta.400.ba99f40 → 0.2.200

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 CHANGED
@@ -29030,7 +29030,7 @@ var require_websocket = __commonJS({
29030
29030
  var http3 = require("http");
29031
29031
  var net3 = require("net");
29032
29032
  var tls = require("tls");
29033
- var { randomBytes, createHash: createHash2 } = require("crypto");
29033
+ var { randomBytes, createHash: createHash3 } = require("crypto");
29034
29034
  var { Duplex, Readable: Readable3 } = require("stream");
29035
29035
  var { URL: URL2 } = require("url");
29036
29036
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -29690,7 +29690,7 @@ var require_websocket = __commonJS({
29690
29690
  abortHandshake(websocket, socket, "Invalid Upgrade header");
29691
29691
  return;
29692
29692
  }
29693
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
29693
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
29694
29694
  if (res.headers["sec-websocket-accept"] !== digest) {
29695
29695
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
29696
29696
  return;
@@ -30057,7 +30057,7 @@ var require_websocket_server = __commonJS({
30057
30057
  var EventEmitter3 = require("events");
30058
30058
  var http3 = require("http");
30059
30059
  var { Duplex } = require("stream");
30060
- var { createHash: createHash2 } = require("crypto");
30060
+ var { createHash: createHash3 } = require("crypto");
30061
30061
  var extension2 = require_extension();
30062
30062
  var PerMessageDeflate2 = require_permessage_deflate();
30063
30063
  var subprotocol2 = require_subprotocol();
@@ -30358,7 +30358,7 @@ var require_websocket_server = __commonJS({
30358
30358
  );
30359
30359
  }
30360
30360
  if (this._state > RUNNING) return abortHandshake(socket, 503);
30361
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
30361
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
30362
30362
  const headers = [
30363
30363
  "HTTP/1.1 101 Switching Protocols",
30364
30364
  "Upgrade: websocket",
@@ -40843,6 +40843,18 @@ function createLogger(opts = {}) {
40843
40843
  );
40844
40844
  return wrap(base);
40845
40845
  }
40846
+ function createFileOnlyLogger(opts) {
40847
+ const level = opts.level ?? "debug";
40848
+ try {
40849
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(opts.file), { recursive: true });
40850
+ } catch {
40851
+ }
40852
+ const base = (0, import_pino.default)(
40853
+ { level, base: {} },
40854
+ import_pino.default.destination({ dest: opts.file, mkdir: true, sync: true })
40855
+ );
40856
+ return wrap(base);
40857
+ }
40846
40858
  function pinoLevelToString(n) {
40847
40859
  if (typeof n !== "number") return null;
40848
40860
  if (n >= 50) return "error";
@@ -43008,9 +43020,18 @@ var SessionManager = class {
43008
43020
  // 由 observer 监听 jsonl user 行后调 recordRealUserUuid 建立映射;rewind 系列 RPC 在
43009
43021
  // 入参 / 出参做转译,保证 UI 看到的 uuid 始终是 events 流里的 synth uuid
43010
43022
  realUuidBySynth = /* @__PURE__ */ new Map();
43011
- // observeScreenIdle 复合条件闸用:sessionIdobserver 上次喂出业务事件的时刻(deps.now())。
43012
- // observerIdleWaitMs 据此判 observer 是否也静止 idleMs(屏幕静止 AND observer 静止才补 turn_end)。
43013
- lastObserverEventAt = /* @__PURE__ */ new Map();
43023
+ // observer 收到 `turn_duration` 信号但屏幕还没稳定 5s 挂进 pending,等 `notifyScreenIdle`
43024
+ // (屏幕 armed=true 触发点)flush 成真正的 turn_end reducer。
43025
+ //
43026
+ // 语义澄清:`turn_duration` 是 CC 报的原始信号("本轮 API 调用完了"),**不代表 turn 真的结束**
43027
+ //(背景 agent 可能还在跑)。屏幕 5s 稳定 + 无 popup 才是"确认信号"。
43028
+ // 两者 AND 后 daemon 才产生真正的 `turn_end` 事件送给 reducer。
43029
+ //
43030
+ // pending 不设截止时间:屏幕稳定的时机由 CC UI 决定,可能几秒也可能几分钟。观察者以
43031
+ // "屏幕真的稳定"作为触发点,signal-driven 而非 timer-driven。
43032
+ //
43033
+ // 清理点:newSession / stop / session-delete / stopAll。
43034
+ pendingTurnDurationSignals = /* @__PURE__ */ new Set();
43014
43035
  // SessionStore 按 scope 派生(root = <dataDir>/sessions/<scopeSubPath>/)。
43015
43036
  // default scope 直接复用 deps.store;persona scope(owner / listener)第一次访问时按需创建并缓存。
43016
43037
  // 取代旧的 storesByAgent —— agentId 概念由 SessionScope 取代,路径即身份,
@@ -43350,6 +43371,14 @@ var SessionManager = class {
43350
43371
  routeFromRunner(frame, target) {
43351
43372
  const compressed = compressFrameForWire(frame);
43352
43373
  if (!compressed) return;
43374
+ if (compressed.type === "session:status") {
43375
+ const s = compressed;
43376
+ this.deps.screenIdleProbeLogger?.info("session:status wire emit", {
43377
+ sessionId: s.sessionId,
43378
+ status: s.status,
43379
+ target
43380
+ });
43381
+ }
43353
43382
  if (compressed.type === "session:event" || compressed.type === "session:status") {
43354
43383
  const sid = compressed.sessionId;
43355
43384
  if (sid) {
@@ -43623,7 +43652,7 @@ var SessionManager = class {
43623
43652
  this.runners.delete(args.sessionId);
43624
43653
  this.realUuidBySynth.delete(args.sessionId);
43625
43654
  this.lastUiSizeBySessionId.delete(args.sessionId);
43626
- this.lastObserverEventAt.delete(args.sessionId);
43655
+ this.clearPendingTurnEnd(args.sessionId);
43627
43656
  return { response: { sessionId: args.sessionId }, broadcast };
43628
43657
  }
43629
43658
  this.deleteOwned(args.sessionId);
@@ -43653,6 +43682,7 @@ var SessionManager = class {
43653
43682
  async stop(args) {
43654
43683
  const runner = this.runners.get(args.sessionId);
43655
43684
  if (!runner) return { response: { ok: true }, broadcast: [] };
43685
+ this.clearPendingTurnEnd(args.sessionId);
43656
43686
  const { broadcast } = this.withCollector(() => {
43657
43687
  runner.input({ kind: "command", command: { kind: "stop" } });
43658
43688
  });
@@ -43786,6 +43816,7 @@ var SessionManager = class {
43786
43816
  newSession(args) {
43787
43817
  const existingFile = this.getFile(args.sessionId);
43788
43818
  const nextToolSessionId = this.deps.mode === "tui" && (existingFile.tool ?? "claude") === "claude" ? v4_default() : void 0;
43819
+ this.clearPendingTurnEnd(args.sessionId);
43789
43820
  const runner = this.runners.get(args.sessionId);
43790
43821
  if (runner) {
43791
43822
  const { value, broadcast } = this.withCollector(() => {
@@ -43886,6 +43917,7 @@ var SessionManager = class {
43886
43917
  for (const r of this.runners.values()) {
43887
43918
  r.input({ kind: "command", command: { kind: "stop" } });
43888
43919
  }
43920
+ this.pendingTurnDurationSignals.clear();
43889
43921
  }
43890
43922
  // 给 observer 用:拿已存在的 runner
43891
43923
  getActive(sessionId) {
@@ -44102,7 +44134,7 @@ var SessionManager = class {
44102
44134
  this.runners.delete(args.sessionId);
44103
44135
  this.realUuidBySynth.delete(args.sessionId);
44104
44136
  this.lastUiSizeBySessionId.delete(args.sessionId);
44105
- this.lastObserverEventAt.delete(args.sessionId);
44137
+ this.clearPendingTurnEnd(args.sessionId);
44106
44138
  return { response: { sessionId: args.sessionId }, broadcast };
44107
44139
  }
44108
44140
  this.storeFor(args.scope).delete(args.sessionId);
@@ -44348,23 +44380,93 @@ var SessionManager = class {
44348
44380
  return;
44349
44381
  }
44350
44382
  }
44351
- this.lastObserverEventAt.set(sessionId, (this.deps.now ?? Date.now)());
44352
44383
  let feedEvents = outEvents;
44353
44384
  if (outEvents.some((e) => e.kind === "turn_end")) {
44354
- const ev = this.peekTurnEvidence(runner);
44355
- if (!ev.turnHasContent) {
44385
+ const runnerState = runner.getState();
44386
+ const toolSessionId = runnerState.file.toolSessionId;
44387
+ const adapter = this.deps.getAdapter(runnerState.file.tool ?? "claude");
44388
+ const gateOpen = !adapter.canAcceptTurnEnd || !toolSessionId ? true : adapter.canAcceptTurnEnd(toolSessionId);
44389
+ this.deps.screenIdleProbeLogger?.info("turn_duration signal received", {
44390
+ sessionId,
44391
+ toolSessionId,
44392
+ batchKinds: outEvents.map((e) => e.kind),
44393
+ gateOpen,
44394
+ hasCanAcceptGate: !!adapter.canAcceptTurnEnd
44395
+ });
44396
+ if (gateOpen) {
44397
+ this.deps.screenIdleProbeLogger?.info(
44398
+ "turn_duration \u2192 turn_end confirmed (gate open) \u2192 fed to reducer",
44399
+ { sessionId, toolSessionId }
44400
+ );
44401
+ } else {
44356
44402
  feedEvents = outEvents.filter((e) => e.kind !== "turn_end");
44357
- this.deps.logger?.info("[TE-PROBE] drop spurious observer turn_end", {
44358
- sessionId,
44359
- src: "observer",
44360
- ...ev,
44361
- batchKinds: outEvents.map((e) => e.kind)
44362
- });
44403
+ if (this.pendingTurnDurationSignals.has(sessionId)) {
44404
+ this.deps.screenIdleProbeLogger?.info(
44405
+ "turn_duration dedup: pending already set (repeated observer signal)",
44406
+ { sessionId, toolSessionId }
44407
+ );
44408
+ } else {
44409
+ this.pendingTurnDurationSignals.add(sessionId);
44410
+ this.deps.screenIdleProbeLogger?.info(
44411
+ "turn_duration pending: gate closed \u2192 waiting for screen-idle signal",
44412
+ { sessionId, toolSessionId }
44413
+ );
44414
+ }
44363
44415
  }
44364
44416
  }
44365
44417
  if (feedEvents.length === 0) return;
44366
44418
  runner.feedObserverEvents(feedEvents);
44367
44419
  }
44420
+ /**
44421
+ * `ClaudeTuiAdapter.observeScreenIdle` fire triggered → armed=true 时调用(一次性触发点)。
44422
+ *
44423
+ * 语义:屏幕真的稳定了 5s。查有没有 pending 的 turn_duration 信号:
44424
+ * - 有 → 之前收到的 turn_duration 被屏幕稳定**确认**了,flush 成 turn_end 进 reducer
44425
+ * - 无 → 屏幕稳定但从没收到 turn_duration → noop(不生成 turn_end;补偿路径的错误做法)
44426
+ *
44427
+ * pending 集合是**必要前提**:turn_end 只能来自"observer 收 turn_duration + 屏幕后续稳定"
44428
+ * 双源确认,任何一个缺少都不能 emit。这跟 PR #962 拆掉的 dispatchTurnIdle "屏幕静止就补
44429
+ * turn_end" 语义不同。
44430
+ *
44431
+ * 仅 TUI 模式;SDK / codex 没有屏幕信号也就不会触发本方法。
44432
+ */
44433
+ notifyScreenIdle(toolSessionId) {
44434
+ if (this.deps.mode !== "tui") return;
44435
+ const sid = this.sessionIdByToolSid(toolSessionId);
44436
+ if (!sid) {
44437
+ this.deps.screenIdleProbeLogger?.warn("notifyScreenIdle: no session for toolSessionId", {
44438
+ toolSessionId
44439
+ });
44440
+ return;
44441
+ }
44442
+ if (!this.pendingTurnDurationSignals.has(sid)) {
44443
+ this.deps.screenIdleProbeLogger?.info(
44444
+ "notifyScreenIdle: no pending turn_duration \u2192 noop",
44445
+ { sessionId: sid, toolSessionId }
44446
+ );
44447
+ return;
44448
+ }
44449
+ const runner = this.runners.get(sid);
44450
+ if (!runner) {
44451
+ this.pendingTurnDurationSignals.delete(sid);
44452
+ this.deps.screenIdleProbeLogger?.warn(
44453
+ "notifyScreenIdle: pending but no runner \u2192 cleared without inject",
44454
+ { sessionId: sid, toolSessionId }
44455
+ );
44456
+ return;
44457
+ }
44458
+ this.pendingTurnDurationSignals.delete(sid);
44459
+ this.deps.screenIdleProbeLogger?.info(
44460
+ "notifyScreenIdle: pending turn_duration + screen idle confirmed \u2192 inject turn_end",
44461
+ { sessionId: sid, toolSessionId }
44462
+ );
44463
+ runner.input({ kind: "inject-events", events: [{ kind: "turn_end" }] });
44464
+ }
44465
+ clearPendingTurnEnd(sessionId) {
44466
+ if (this.pendingTurnDurationSignals.delete(sessionId)) {
44467
+ this.deps.screenIdleProbeLogger?.info("pending turn_duration cleared", { sessionId });
44468
+ }
44469
+ }
44368
44470
  // AskUserQuestion 表单回写(plan: clawd-ask-user-question):UI 答完所有 question 后调用。
44369
44471
  // - session 不存在 / 无 runner → noop 幂等返回 ok(first-decider-wins)
44370
44472
  // - reducer noop(toolUseId 不存在或已答过)也保持幂等返回,handler 不抛
@@ -44623,70 +44725,6 @@ var SessionManager = class {
44623
44725
  if (!runner) return;
44624
44726
  runner.input({ kind: "ready-detected" });
44625
44727
  }
44626
- /**
44627
- * ClaudeTuiAdapter onTurnIdle callback:屏幕内容静止时**复发**本轮已出现过的权威 turn_end。
44628
- * 本意:turn_duration 写盘早于尾段正文 → observer 把尾随 text 推进 buffer 盖掉 lastEventKind →
44629
- * spinner 不熄;屏幕静止时再补一条 turn_end 排到尾随 text 之后。
44630
- *
44631
- * Fix A(修 bug1 "UI 还在变却显示结束" / bug2 "发消息后无 spinner"):补偿**只复发不 originate**——
44632
- * 仅当本轮已出现过 turn_end(turnEndSeenThisTurn,即真有过尾随 text 覆盖场景)才补。turnEndSeenThisTurn
44633
- * ===false 说明本轮 CC 从未结束过(仍在工作 / 刚发消息没产出 / 漏检弹框等用户),屏幕静止 ≠ turn 结束,
44634
- * 凭空 inject turn_end 会误灭 spinner——故跳过,让真正的 turn_duration 来时再正常收。
44635
- * 仅 tui 模式;runner 缺失 noop。turn_end 无 uuid 不参与 dedup。
44636
- */
44637
- dispatchTurnIdle(toolSessionId) {
44638
- if (this.deps.mode !== "tui") return;
44639
- const sid = this.sessionIdByToolSid(toolSessionId);
44640
- const runner = sid ? this.runners.get(sid) : void 0;
44641
- if (!runner) return;
44642
- const ev = this.peekTurnEvidence(runner);
44643
- const willInject = ev.turnEndSeenThisTurn;
44644
- this.deps.logger?.info("[TE-PROBE] screen-idle compensation", {
44645
- sessionId: sid,
44646
- src: "screen-idle",
44647
- willInject,
44648
- ...ev
44649
- });
44650
- if (!willInject) return;
44651
- runner.input({ kind: "inject-events", events: [{ kind: "turn_end" }] });
44652
- }
44653
- /**
44654
- * 读 runner 当前 turn 态快照,供两个 turn_end 注入源(屏幕静止补偿 / observer turn_duration)
44655
- * 判断误发 + 打 [TE-PROBE] 日志。
44656
- * turnEndSeenThisTurn:从 buffer 末尾回扫到最近 user_text 期间是否已出现过 turn_end
44657
- * (已出现=本轮真结束过、合法尾随;未出现=本轮还没结束过)→ Fix A 复发闸。
44658
- * turnHasContent:末条是否 assistant 产出(非 user_text/turn_end/空)→ Fix B 空 turn 守卫闸。
44659
- */
44660
- peekTurnEvidence(runner) {
44661
- const st = runner.getState();
44662
- const buf = st.buffer;
44663
- const lastEventKindBefore = buf.length > 0 ? buf[buf.length - 1].event.kind : null;
44664
- let turnEndSeenThisTurn = false;
44665
- for (let i = buf.length - 1; i >= 0; i--) {
44666
- const k2 = buf[i].event.kind;
44667
- if (k2 === "user_text") break;
44668
- if (k2 === "turn_end") {
44669
- turnEndSeenThisTurn = true;
44670
- break;
44671
- }
44672
- }
44673
- const turnHasContent = lastEventKindBefore !== null && lastEventKindBefore !== "user_text" && lastEventKindBefore !== "turn_end";
44674
- return { turnOpenBefore: st.turnOpen, lastEventKindBefore, turnEndSeenThisTurn, turnHasContent };
44675
- }
44676
- /**
44677
- * observer 还需静止多久(ms)才满 idleMs,0 = 已满。observeScreenIdle 复合条件闸:屏幕静止后
44678
- * 精确等这段剩余再补 turn_end —— turn_duration 写盘早于尾段正文,observer 把尾随 text poll 落盘
44679
- * 期间屏幕可能已静止,仅看屏幕会早 fire(补的 turn_end 盖不到尾随 text 之后)。
44680
- * 找不到 runner / 从无事件 → 0(不阻塞 fire)。idleMs 由装配处传 SCREEN_IDLE_MS。
44681
- */
44682
- observerIdleWaitMs(toolSessionId, idleMs) {
44683
- const sid = this.sessionIdByToolSid(toolSessionId);
44684
- if (!sid) return 0;
44685
- const last = this.lastObserverEventAt.get(sid);
44686
- if (last === void 0) return 0;
44687
- const elapsed = (this.deps.now ?? Date.now)() - last;
44688
- return Math.max(0, idleMs - elapsed);
44689
- }
44690
44728
  /** toolSessionId → sessionId 反查(遍历 runners);session 数典型 < 10,O(n) 可接受 */
44691
44729
  sessionIdByToolSid(toolSessionId) {
44692
44730
  for (const [sid, runner] of this.runners) {
@@ -46429,6 +46467,7 @@ var CodexAdapter = class {
46429
46467
  };
46430
46468
 
46431
46469
  // src/tools/claude-tui.ts
46470
+ var import_node_crypto5 = require("crypto");
46432
46471
  var import_node_fs16 = __toESM(require("fs"), 1);
46433
46472
  var import_node_os7 = __toESM(require("os"), 1);
46434
46473
  var import_node_path14 = __toESM(require("path"), 1);
@@ -47239,22 +47278,56 @@ function observeScreenIdle(surface, opts) {
47239
47278
  timer = null;
47240
47279
  if (disposed) return;
47241
47280
  if (opts.getPopupVisible()) {
47281
+ opts.probeLogger?.info("screen-idle fire suppressed: popup visible", {
47282
+ label: opts.probeLabel
47283
+ });
47242
47284
  timer = setTimeout(fire, opts.idleMs);
47243
47285
  return;
47244
47286
  }
47245
47287
  const obsWait = opts.getObserverWaitMs?.() ?? 0;
47246
47288
  if (obsWait > 0) {
47289
+ opts.probeLogger?.info("screen-idle fire suppressed: observer not idle", {
47290
+ label: opts.probeLabel,
47291
+ obsWait
47292
+ });
47247
47293
  timer = setTimeout(fire, Math.max(obsWait, REWAIT_MIN_MS));
47248
47294
  return;
47249
47295
  }
47250
- if (armed) return;
47296
+ if (armed) {
47297
+ opts.probeLogger?.debug("screen-idle fire noop: already armed", {
47298
+ label: opts.probeLabel
47299
+ });
47300
+ return;
47301
+ }
47251
47302
  armed = true;
47303
+ opts.probeLogger?.info("screen-idle fire triggered \u2192 armed=true, calling onIdle", {
47304
+ label: opts.probeLabel
47305
+ });
47252
47306
  opts.onIdle();
47253
47307
  };
47254
47308
  const unsub = surface.onTick((lines) => {
47255
47309
  if (disposed) return;
47256
47310
  const snap = snapOf(lines);
47257
47311
  if (snap === lastSnap) return;
47312
+ if (opts.probeLogger) {
47313
+ const prev = lastSnap;
47314
+ const meta = {
47315
+ label: opts.probeLabel,
47316
+ prevHash: prev === null ? null : shortHash(prev),
47317
+ nextHash: shortHash(snap),
47318
+ prevLen: prev?.length ?? 0,
47319
+ nextLen: snap.length
47320
+ };
47321
+ if (prev !== null) {
47322
+ const diff2 = firstLineDiff(prev, snap);
47323
+ if (diff2) {
47324
+ meta.diffRow = diff2.row;
47325
+ meta.prevRow = diff2.prev;
47326
+ meta.nextRow = diff2.next;
47327
+ }
47328
+ }
47329
+ opts.probeLogger.info("screen-idle tick snap changed", meta);
47330
+ }
47258
47331
  lastSnap = snap;
47259
47332
  armed = false;
47260
47333
  clear();
@@ -47265,9 +47338,38 @@ function observeScreenIdle(surface, opts) {
47265
47338
  disposed = true;
47266
47339
  unsub();
47267
47340
  clear();
47341
+ },
47342
+ isIdle() {
47343
+ const popupVisible = opts.getPopupVisible();
47344
+ const idle = armed && !popupVisible;
47345
+ if (opts.probeLogger) {
47346
+ opts.probeLogger.info("screen-idle isIdle check", {
47347
+ label: opts.probeLabel,
47348
+ idle,
47349
+ armed,
47350
+ popupVisible
47351
+ });
47352
+ }
47353
+ return idle;
47268
47354
  }
47269
47355
  };
47270
47356
  }
47357
+ function shortHash(s) {
47358
+ return (0, import_node_crypto5.createHash)("sha1").update(s).digest("hex").slice(0, 8);
47359
+ }
47360
+ function firstLineDiff(prev, next) {
47361
+ const p2 = prev.split("\n");
47362
+ const n = next.split("\n");
47363
+ const rows = Math.max(p2.length, n.length);
47364
+ for (let i = 0; i < rows; i++) {
47365
+ const pl = p2[i] ?? "";
47366
+ const nl = n[i] ?? "";
47367
+ if (pl !== nl) {
47368
+ return { row: i, prev: pl.slice(0, 60), next: nl.slice(0, 60) };
47369
+ }
47370
+ }
47371
+ return null;
47372
+ }
47271
47373
  var BYPASS_SETTLE_MS = 300;
47272
47374
  var SCREEN_IDLE_MS = 5e3;
47273
47375
  function createBootGate(pty, logger) {
@@ -47342,11 +47444,42 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47342
47444
  // 用于 spawn / PtyChildProcess 链路打日志
47343
47445
  tuiLogger;
47344
47446
  tuiOpts;
47447
+ /**
47448
+ * per-toolSessionId 的 tui 观察者句柄,仅用于 turn_end gate 查询(`canAcceptTurnEnd`)。
47449
+ * onIdle / onPopupTransition 等回调仍走原有闭包(不复用这份 map),本 map 只承担
47450
+ * "manager 需要跨模块查屏幕/弹框状态"这单一职责。
47451
+ */
47452
+ tuiStates = /* @__PURE__ */ new Map();
47345
47453
  constructor(opts = {}) {
47346
47454
  super(opts);
47347
47455
  this.tuiLogger = opts.logger;
47348
47456
  this.tuiOpts = opts;
47349
47457
  }
47458
+ /**
47459
+ * TUI adapter 的 turn_end 权威判定:屏幕已 idle 且非弹框态才放行。
47460
+ *
47461
+ * `feedObserverEvents` 收到 observer 回灌 `turn_end` 时调用。屏幕仍在变(如后台 agent 在跑)
47462
+ * 时 drop 掉 turn_end,避免 `system/turn_duration` JSONL 帧误触发 running-idle 状态转换。
47463
+ *
47464
+ * 未跟踪的 toolSessionId(spawn 前 / spawn 失败 / 已 dispose)视为 pass —— gate 只 drop
47465
+ * "有证据判定为伪信号"的场景,不做 unknown → block。
47466
+ */
47467
+ canAcceptTurnEnd(toolSessionId) {
47468
+ const state = this.tuiStates.get(toolSessionId);
47469
+ if (!state) {
47470
+ this.tuiOpts.screenIdleProbeLogger?.info(
47471
+ "canAcceptTurnEnd: no tuiState \u2192 pass (\u672A\u8DDF\u8E2A)",
47472
+ { toolSessionId }
47473
+ );
47474
+ return true;
47475
+ }
47476
+ const result = state.screenIdle.isIdle();
47477
+ this.tuiOpts.screenIdleProbeLogger?.info("canAcceptTurnEnd", {
47478
+ toolSessionId,
47479
+ result
47480
+ });
47481
+ return result;
47482
+ }
47350
47483
  spawn(ctx) {
47351
47484
  const args = buildTuiSpawnArgs(ctx, jsonlExistsForCtx(ctx));
47352
47485
  const cmd = process.env.CLAUDE_BIN ?? "claude";
@@ -47404,18 +47537,26 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47404
47537
  const screenIdleObserver = observeScreenIdle(surface, {
47405
47538
  idleMs: SCREEN_IDLE_MS,
47406
47539
  onIdle: () => {
47407
- if (!ctx.toolSessionId || !this.tuiOpts.onTurnIdle) return;
47408
- this.tuiLogger?.debug("screen-idle \u2192 turn_end", { toolSessionId: ctx.toolSessionId });
47409
- this.tuiOpts.onTurnIdle(ctx.toolSessionId);
47540
+ if (!ctx.toolSessionId || !this.tuiOpts.onScreenIdle) return;
47541
+ this.tuiLogger?.debug("screen-idle \u2192 notifyScreenIdle", { toolSessionId: ctx.toolSessionId });
47542
+ this.tuiOpts.onScreenIdle(ctx.toolSessionId);
47410
47543
  },
47411
47544
  getPopupVisible: () => popupObserver.visibleKind !== null,
47412
- // observer 还需静止多久才满 SCREEN_IDLE_MS(复合条件 AND):屏幕静止后精确等这段剩余再补
47413
- // turn_end,确保它排在尾段 text + turn_duration 全部 poll 落盘之后 = buffer 末条。
47414
- getObserverWaitMs: () => ctx.toolSessionId ? this.tuiOpts.getObserverWaitMs?.(ctx.toolSessionId, SCREEN_IDLE_MS) ?? 0 : 0
47545
+ // 取证 probe(可选,装配处传独立 file-only logger,跟主 daemon.log 解耦)
47546
+ ...this.tuiOpts.screenIdleProbeLogger ? {
47547
+ probeLogger: this.tuiOpts.screenIdleProbeLogger,
47548
+ probeLabel: ctx.toolSessionId ?? "<no-tsid>"
47549
+ } : {}
47415
47550
  });
47416
47551
  if (ctx.toolSessionId && this.tuiOpts.onSurfaceRegister) {
47417
47552
  this.tuiOpts.onSurfaceRegister(ctx.toolSessionId, surface);
47418
47553
  }
47554
+ if (ctx.toolSessionId) {
47555
+ this.tuiStates.set(ctx.toolSessionId, {
47556
+ screenIdle: screenIdleObserver,
47557
+ popup: popupObserver
47558
+ });
47559
+ }
47419
47560
  let chunkSeq = 0;
47420
47561
  if (ctx.toolSessionId && this.tuiOpts.onPtyReplayRegister) {
47421
47562
  this.tuiOpts.onPtyReplayRegister(ctx.toolSessionId, async () => {
@@ -47461,6 +47602,9 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47461
47602
  readyObserver.dispose();
47462
47603
  popupObserver.dispose();
47463
47604
  screenIdleObserver.dispose();
47605
+ if (ctx.toolSessionId) {
47606
+ this.tuiStates.delete(ctx.toolSessionId);
47607
+ }
47464
47608
  if (ctx.toolSessionId && this.tuiOpts.onSurfaceUnregister) {
47465
47609
  this.tuiOpts.onSurfaceUnregister(ctx.toolSessionId);
47466
47610
  }
@@ -47743,7 +47887,7 @@ async function writeInboxMcpConfig(args) {
47743
47887
  // src/shift/store.ts
47744
47888
  var import_promises = __toESM(require("fs/promises"), 1);
47745
47889
  var import_node_path19 = __toESM(require("path"), 1);
47746
- var import_node_crypto5 = require("crypto");
47890
+ var import_node_crypto6 = require("crypto");
47747
47891
 
47748
47892
  // src/shift/constants.ts
47749
47893
  var MAX_RUNS_PER_SHIFT = 30;
@@ -47839,7 +47983,7 @@ function createShiftStore(deps) {
47839
47983
  const nextRunAtMs = computeNextRunAtMs(input.schedule, now) ?? void 0;
47840
47984
  const shift = {
47841
47985
  ...input,
47842
- id: (0, import_node_crypto5.randomUUID)(),
47986
+ id: (0, import_node_crypto6.randomUUID)(),
47843
47987
  createdAtMs: now,
47844
47988
  updatedAtMs: now,
47845
47989
  state: { nextRunAtMs },
@@ -50810,7 +50954,7 @@ function lookupMime(filePathOrName) {
50810
50954
  }
50811
50955
 
50812
50956
  // src/attachment/sign-url.ts
50813
- var import_node_crypto6 = __toESM(require("crypto"), 1);
50957
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
50814
50958
  var HMAC_ALGO = "sha256";
50815
50959
  function base64urlEncode(buf) {
50816
50960
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -50827,7 +50971,7 @@ function decodeAbsPathFromUrl(encoded) {
50827
50971
  }
50828
50972
  function computeSig(secret, absPath, e) {
50829
50973
  const msg = e === null ? absPath : `${absPath}|${e}`;
50830
- return import_node_crypto6.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
50974
+ return import_node_crypto7.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
50831
50975
  }
50832
50976
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
50833
50977
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -50862,7 +51006,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
50862
51006
  if (provided.length !== expected.length) {
50863
51007
  return { ok: false, code: "BAD_SIG" };
50864
51008
  }
50865
- if (!import_node_crypto6.default.timingSafeEqual(provided, expected)) {
51009
+ if (!import_node_crypto7.default.timingSafeEqual(provided, expected)) {
50866
51010
  return { ok: false, code: "BAD_SIG" };
50867
51011
  }
50868
51012
  if (e !== null && now() / 1e3 > e) {
@@ -50874,7 +51018,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
50874
51018
  // src/attachment/upload.ts
50875
51019
  var import_node_fs25 = __toESM(require("fs"), 1);
50876
51020
  var import_node_path25 = __toESM(require("path"), 1);
50877
- var import_node_crypto7 = __toESM(require("crypto"), 1);
51021
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
50878
51022
  var import_promises2 = require("stream/promises");
50879
51023
  var UploadError = class extends Error {
50880
51024
  constructor(code, message) {
@@ -50898,11 +51042,11 @@ async function writeUploadedAttachment(args) {
50898
51042
  } catch (err) {
50899
51043
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
50900
51044
  }
50901
- const hasher = import_node_crypto7.default.createHash("sha256");
51045
+ const hasher = import_node_crypto8.default.createHash("sha256");
50902
51046
  let actualSize = 0;
50903
51047
  const tmpPath = import_node_path25.default.join(
50904
51048
  attachmentsRoot,
50905
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto7.default.randomBytes(4).toString("hex")}`
51049
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto8.default.randomBytes(4).toString("hex")}`
50906
51050
  );
50907
51051
  try {
50908
51052
  await (0, import_promises2.pipeline)(
@@ -51778,7 +51922,7 @@ function runAttachmentGc(args) {
51778
51922
  // src/attachment/group.ts
51779
51923
  var import_node_fs28 = __toESM(require("fs"), 1);
51780
51924
  var import_node_path29 = __toESM(require("path"), 1);
51781
- var import_node_crypto8 = __toESM(require("crypto"), 1);
51925
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
51782
51926
  init_protocol();
51783
51927
  var GroupFileStore = class {
51784
51928
  dataDir;
@@ -51867,7 +52011,7 @@ var GroupFileStore = class {
51867
52011
  entries[idx] = next;
51868
52012
  } else {
51869
52013
  next = {
51870
- id: `gf-${import_node_crypto8.default.randomBytes(6).toString("base64url")}`,
52014
+ id: `gf-${import_node_crypto9.default.randomBytes(6).toString("base64url")}`,
51871
52015
  relPath: input.relPath,
51872
52016
  from: input.from,
51873
52017
  label: input.label,
@@ -51986,7 +52130,7 @@ function readDaemonSourceFromEnv(env = process.env) {
51986
52130
  // src/tunnel/tunnel-manager.ts
51987
52131
  var import_node_fs33 = __toESM(require("fs"), 1);
51988
52132
  var import_node_path34 = __toESM(require("path"), 1);
51989
- var import_node_crypto9 = __toESM(require("crypto"), 1);
52133
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
51990
52134
  var import_node_child_process9 = require("child_process");
51991
52135
 
51992
52136
  // src/tunnel/tunnel-store.ts
@@ -52485,7 +52629,7 @@ var TunnelManager = class {
52485
52629
  override: this.deps.frpcBinaryOverride ?? void 0
52486
52630
  });
52487
52631
  const tomlPath = import_node_path34.default.join(this.deps.dataDir, "frpc.toml");
52488
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto9.default.randomBytes(3).toString("hex")}`;
52632
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto10.default.randomBytes(3).toString("hex")}`;
52489
52633
  const toml = buildFrpcToml({
52490
52634
  serverAddr: t.frpsHost,
52491
52635
  serverPort: t.frpsPort,
@@ -53065,7 +53209,7 @@ function readIssuedPubkey(sshdDir, deviceId) {
53065
53209
  // src/tunnel/device-key.ts
53066
53210
  var import_node_os14 = __toESM(require("os"), 1);
53067
53211
  var import_node_path39 = __toESM(require("path"), 1);
53068
- var import_node_crypto10 = __toESM(require("crypto"), 1);
53212
+ var import_node_crypto11 = __toESM(require("crypto"), 1);
53069
53213
  var DERIVE_SALT = "clawd-tunnel-device-v1";
53070
53214
  function deriveStableDeviceKey(opts = {}) {
53071
53215
  const hostname = opts.hostname ?? import_node_os14.default.hostname();
@@ -53075,13 +53219,13 @@ function deriveStableDeviceKey(opts = {}) {
53075
53219
  const normalizedDataDir = opts.dataDir ? import_node_path39.default.resolve(opts.dataDir) : null;
53076
53220
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
53077
53221
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
53078
- return import_node_crypto10.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
53222
+ return import_node_crypto11.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
53079
53223
  }
53080
53224
 
53081
53225
  // src/auth-store.ts
53082
53226
  var import_node_fs38 = __toESM(require("fs"), 1);
53083
53227
  var import_node_path40 = __toESM(require("path"), 1);
53084
- var import_node_crypto11 = __toESM(require("crypto"), 1);
53228
+ var import_node_crypto12 = __toESM(require("crypto"), 1);
53085
53229
  var AUTH_FILE_NAME = "auth.json";
53086
53230
  function authFilePath(dataDir) {
53087
53231
  return import_node_path40.default.join(dataDir, AUTH_FILE_NAME);
@@ -53113,10 +53257,10 @@ function loadOrCreateAuthFile(opts) {
53113
53257
  return next;
53114
53258
  }
53115
53259
  function defaultGenerateToken() {
53116
- return import_node_crypto11.default.randomBytes(32).toString("base64url");
53260
+ return import_node_crypto12.default.randomBytes(32).toString("base64url");
53117
53261
  }
53118
53262
  function defaultGenerateOwnerPrincipalId() {
53119
- return `owner-${import_node_crypto11.default.randomUUID()}`;
53263
+ return `owner-${import_node_crypto12.default.randomUUID()}`;
53120
53264
  }
53121
53265
  function readAuthFile(file) {
53122
53266
  try {
@@ -53238,7 +53382,7 @@ var OwnerIdentityStore = class {
53238
53382
  };
53239
53383
 
53240
53384
  // src/feishu-auth/login-flow.ts
53241
- var import_node_crypto12 = __toESM(require("crypto"), 1);
53385
+ var import_node_crypto13 = __toESM(require("crypto"), 1);
53242
53386
  var STATE_TTL_MS = 5 * 60 * 1e3;
53243
53387
  var LoginFlow = class {
53244
53388
  constructor(deps) {
@@ -53247,7 +53391,7 @@ var LoginFlow = class {
53247
53391
  deps;
53248
53392
  pendingStates = /* @__PURE__ */ new Map();
53249
53393
  start() {
53250
- const state = import_node_crypto12.default.randomBytes(16).toString("base64url");
53394
+ const state = import_node_crypto13.default.randomBytes(16).toString("base64url");
53251
53395
  const now = (this.deps.now ?? Date.now)();
53252
53396
  this.pendingStates.set(state, now);
53253
53397
  this.gcExpired(now);
@@ -55249,7 +55393,7 @@ init_protocol();
55249
55393
  // src/extension/bundle-zip.ts
55250
55394
  var import_promises5 = __toESM(require("fs/promises"), 1);
55251
55395
  var import_node_path47 = __toESM(require("path"), 1);
55252
- var import_node_crypto13 = __toESM(require("crypto"), 1);
55396
+ var import_node_crypto14 = __toESM(require("crypto"), 1);
55253
55397
  var import_jszip2 = __toESM(require_lib3(), 1);
55254
55398
  async function bundleExtensionDir(dir) {
55255
55399
  const entries = await listFilesSorted(dir);
@@ -55264,7 +55408,7 @@ async function bundleExtensionDir(dir) {
55264
55408
  compression: "DEFLATE",
55265
55409
  compressionOptions: { level: 6 }
55266
55410
  });
55267
- const sha256 = import_node_crypto13.default.createHash("sha256").update(buffer).digest("hex");
55411
+ const sha256 = import_node_crypto14.default.createHash("sha256").update(buffer).digest("hex");
55268
55412
  return { buffer, sha256 };
55269
55413
  }
55270
55414
  var FIXED_DATE = /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
@@ -55332,7 +55476,7 @@ function computePublishCheck(args) {
55332
55476
  var import_promises6 = __toESM(require("fs/promises"), 1);
55333
55477
  var import_node_path49 = __toESM(require("path"), 1);
55334
55478
  var import_node_os19 = __toESM(require("os"), 1);
55335
- var import_node_crypto14 = __toESM(require("crypto"), 1);
55479
+ var import_node_crypto15 = __toESM(require("crypto"), 1);
55336
55480
  var import_jszip3 = __toESM(require_lib3(), 1);
55337
55481
 
55338
55482
  // src/extension/paths.ts
@@ -55361,7 +55505,7 @@ var InstallError = class extends Error {
55361
55505
  };
55362
55506
  async function installFromChannel(args, deps) {
55363
55507
  const { channelRef, snapshotHash, bundleZip } = args;
55364
- const computed = import_node_crypto14.default.createHash("sha256").update(bundleZip).digest("hex");
55508
+ const computed = import_node_crypto15.default.createHash("sha256").update(bundleZip).digest("hex");
55365
55509
  if (computed !== snapshotHash) {
55366
55510
  throw new InstallError(
55367
55511
  "HASH_MISMATCH",
@@ -55453,7 +55597,7 @@ async function installFromChannel(args, deps) {
55453
55597
  var import_promises7 = __toESM(require("fs/promises"), 1);
55454
55598
  var import_node_path50 = __toESM(require("path"), 1);
55455
55599
  var import_node_os20 = __toESM(require("os"), 1);
55456
- var import_node_crypto15 = __toESM(require("crypto"), 1);
55600
+ var import_node_crypto16 = __toESM(require("crypto"), 1);
55457
55601
  var import_jszip4 = __toESM(require_lib3(), 1);
55458
55602
  var UpdateError = class extends Error {
55459
55603
  constructor(code, message) {
@@ -55491,7 +55635,7 @@ async function updateFromChannel(args, deps) {
55491
55635
  if (e instanceof UpdateError) throw e;
55492
55636
  throw e;
55493
55637
  }
55494
- const computed = import_node_crypto15.default.createHash("sha256").update(bundleZip).digest("hex");
55638
+ const computed = import_node_crypto16.default.createHash("sha256").update(bundleZip).digest("hex");
55495
55639
  if (computed !== snapshotHash) {
55496
55640
  throw new UpdateError(
55497
55641
  "HASH_MISMATCH",
@@ -56196,7 +56340,7 @@ function listPidsOnPort(port) {
56196
56340
  }
56197
56341
 
56198
56342
  // src/app-builder/publish-registry.ts
56199
- var import_node_crypto16 = require("crypto");
56343
+ var import_node_crypto17 = require("crypto");
56200
56344
  var PublishJobRegistry = class {
56201
56345
  jobs = /* @__PURE__ */ new Map();
56202
56346
  has(name) {
@@ -56213,7 +56357,7 @@ var PublishJobRegistry = class {
56213
56357
  if (this.jobs.has(args.name)) {
56214
56358
  throw new Error(`already publishing: ${args.name}`);
56215
56359
  }
56216
- const jobId = args.jobId ?? `job-${(0, import_node_crypto16.randomUUID)()}`;
56360
+ const jobId = args.jobId ?? `job-${(0, import_node_crypto17.randomUUID)()}`;
56217
56361
  this.jobs.set(args.name, {
56218
56362
  jobId,
56219
56363
  name: args.name,
@@ -57173,7 +57317,7 @@ async function uninstall(deps) {
57173
57317
  }
57174
57318
 
57175
57319
  // src/handlers/index.ts
57176
- var import_node_crypto17 = require("crypto");
57320
+ var import_node_crypto18 = require("crypto");
57177
57321
  function buildMethodHandlers(deps) {
57178
57322
  return {
57179
57323
  ...buildSessionHandlers({
@@ -57206,7 +57350,7 @@ function buildMethodHandlers(deps) {
57206
57350
  const c = deps.contactStore.get(deviceId);
57207
57351
  return c ? { deviceId: c.deviceId, remoteUrl: c.remoteUrl, connectToken: c.connectToken } : null;
57208
57352
  },
57209
- genId: () => (0, import_node_crypto17.randomUUID)(),
57353
+ genId: () => (0, import_node_crypto18.randomUUID)(),
57210
57354
  now: () => Date.now(),
57211
57355
  forwardInboxPostToPeer,
57212
57356
  logger: deps.logger
@@ -58119,6 +58263,13 @@ async function startDaemon(config) {
58119
58263
  logClient
58120
58264
  });
58121
58265
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
58266
+ const screenIdleProbeLogger = createFileOnlyLogger({
58267
+ file: import_node_path60.default.join(config.dataDir, "screen-idle-probe.log"),
58268
+ level: "debug"
58269
+ });
58270
+ logger.info("screen-idle probe logger enabled", {
58271
+ file: import_node_path60.default.join(config.dataDir, "screen-idle-probe.log")
58272
+ });
58122
58273
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
58123
58274
  const pre = stateMgr.preflight();
58124
58275
  if (pre.status === "active") {
@@ -58395,6 +58546,10 @@ async function startDaemon(config) {
58395
58546
  // 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
58396
58547
  storeFactory: sessionStoreFactory,
58397
58548
  logger,
58549
+ // 取证 probe(可选,CLAWD_SCREEN_IDLE_PROBE=1 时启用):manager turn_end 判定链
58550
+ // 的所有决策点打到独立文件,跟 adapter 的 observeScreenIdle probe 共用同一份 file logger,
58551
+ // 便于 grep sessionId 时 tui 层 + manager 层交叉时序都在同一文件里
58552
+ ...screenIdleProbeLogger ? { screenIdleProbeLogger } : {},
58398
58553
  getAdapter,
58399
58554
  historyReader: history,
58400
58555
  dataDir: config.dataDir,
@@ -58512,10 +58667,10 @@ async function startDaemon(config) {
58512
58667
  onSurfaceUnregister: (tsid) => manager.unregisterSurface(tsid),
58513
58668
  // ReadyGate v2:ReadyDetector emit ready 时投递 reducer 'ready-detected' input
58514
58669
  onReady: (tsid) => manager.dispatchReadyDetected(tsid),
58515
- // 屏幕静止补权威 turn_end(修 turn_duration 尾随 text 覆盖 lastEventKind)
58516
- onTurnIdle: (tsid) => manager.dispatchTurnIdle(tsid),
58517
- // 复合条件闸:observer 还需静止多久才满 idleMs(屏幕静止后精确等这段剩余再补 turn_end
58518
- getObserverWaitMs: (tsid, idleMs) => manager.observerIdleWaitMs(tsid, idleMs)
58670
+ // 屏幕真稳定 5s 的一次性信号 manager pending turn_duration flush turn_end
58671
+ onScreenIdle: (tsid) => manager.notifyScreenIdle(tsid),
58672
+ // 取证 probe(默认无条件启用;见 createFileOnlyLogger
58673
+ screenIdleProbeLogger
58519
58674
  }) : new ClaudeAdapter({ logger, historyReader: new ClaudeHistoryReader() });
58520
58675
  registerAdapter("claude", claudeAdapter);
58521
58676
  registerAdapter("codex", new CodexAdapter({ logger, historyReader: new CodexHistoryReader() }));