@clawos-dev/clawd 0.2.201-beta.402.b896f3d → 0.2.201

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
@@ -29153,7 +29153,7 @@ var require_websocket = __commonJS({
29153
29153
  var http3 = require("http");
29154
29154
  var net4 = require("net");
29155
29155
  var tls = require("tls");
29156
- var { randomBytes, createHash: createHash2 } = require("crypto");
29156
+ var { randomBytes, createHash: createHash3 } = require("crypto");
29157
29157
  var { Duplex, Readable: Readable3 } = require("stream");
29158
29158
  var { URL: URL2 } = require("url");
29159
29159
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -29813,7 +29813,7 @@ var require_websocket = __commonJS({
29813
29813
  abortHandshake(websocket, socket, "Invalid Upgrade header");
29814
29814
  return;
29815
29815
  }
29816
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
29816
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
29817
29817
  if (res.headers["sec-websocket-accept"] !== digest) {
29818
29818
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
29819
29819
  return;
@@ -30180,7 +30180,7 @@ var require_websocket_server = __commonJS({
30180
30180
  var EventEmitter3 = require("events");
30181
30181
  var http3 = require("http");
30182
30182
  var { Duplex } = require("stream");
30183
- var { createHash: createHash2 } = require("crypto");
30183
+ var { createHash: createHash3 } = require("crypto");
30184
30184
  var extension2 = require_extension();
30185
30185
  var PerMessageDeflate2 = require_permessage_deflate();
30186
30186
  var subprotocol2 = require_subprotocol();
@@ -30481,7 +30481,7 @@ var require_websocket_server = __commonJS({
30481
30481
  );
30482
30482
  }
30483
30483
  if (this._state > RUNNING) return abortHandshake(socket, 503);
30484
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
30484
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
30485
30485
  const headers = [
30486
30486
  "HTTP/1.1 101 Switching Protocols",
30487
30487
  "Upgrade: websocket",
@@ -41144,6 +41144,18 @@ function createLogger(opts = {}) {
41144
41144
  );
41145
41145
  return wrap(base);
41146
41146
  }
41147
+ function createFileOnlyLogger(opts) {
41148
+ const level = opts.level ?? "debug";
41149
+ try {
41150
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(opts.file), { recursive: true });
41151
+ } catch {
41152
+ }
41153
+ const base = (0, import_pino.default)(
41154
+ { level, base: {} },
41155
+ import_pino.default.destination({ dest: opts.file, mkdir: true, sync: true })
41156
+ );
41157
+ return wrap(base);
41158
+ }
41147
41159
  function pinoLevelToString(n) {
41148
41160
  if (typeof n !== "number") return null;
41149
41161
  if (n >= 50) return "error";
@@ -43338,9 +43350,18 @@ var SessionManager = class {
43338
43350
  // 由 observer 监听 jsonl user 行后调 recordRealUserUuid 建立映射;rewind 系列 RPC 在
43339
43351
  // 入参 / 出参做转译,保证 UI 看到的 uuid 始终是 events 流里的 synth uuid
43340
43352
  realUuidBySynth = /* @__PURE__ */ new Map();
43341
- // observeScreenIdle 复合条件闸用:sessionIdobserver 上次喂出业务事件的时刻(deps.now())。
43342
- // observerIdleWaitMs 据此判 observer 是否也静止 idleMs(屏幕静止 AND observer 静止才补 turn_end)。
43343
- lastObserverEventAt = /* @__PURE__ */ new Map();
43353
+ // observer 收到 `turn_duration` 信号但屏幕还没稳定 5s 挂进 pending,等 `notifyScreenIdle`
43354
+ // (屏幕 armed=true 触发点)flush 成真正的 turn_end reducer。
43355
+ //
43356
+ // 语义澄清:`turn_duration` 是 CC 报的原始信号("本轮 API 调用完了"),**不代表 turn 真的结束**
43357
+ //(背景 agent 可能还在跑)。屏幕 5s 稳定 + 无 popup 才是"确认信号"。
43358
+ // 两者 AND 后 daemon 才产生真正的 `turn_end` 事件送给 reducer。
43359
+ //
43360
+ // pending 不设截止时间:屏幕稳定的时机由 CC UI 决定,可能几秒也可能几分钟。观察者以
43361
+ // "屏幕真的稳定"作为触发点,signal-driven 而非 timer-driven。
43362
+ //
43363
+ // 清理点:newSession / stop / session-delete / stopAll。
43364
+ pendingTurnDurationSignals = /* @__PURE__ */ new Set();
43344
43365
  // SessionStore 按 scope 派生(root = <dataDir>/sessions/<scopeSubPath>/)。
43345
43366
  // default scope 直接复用 deps.store;persona scope(owner / listener)第一次访问时按需创建并缓存。
43346
43367
  // 取代旧的 storesByAgent —— agentId 概念由 SessionScope 取代,路径即身份,
@@ -43680,6 +43701,14 @@ var SessionManager = class {
43680
43701
  routeFromRunner(frame, target) {
43681
43702
  const compressed = compressFrameForWire(frame);
43682
43703
  if (!compressed) return;
43704
+ if (compressed.type === "session:status") {
43705
+ const s = compressed;
43706
+ this.deps.screenIdleProbeLogger?.info("session:status wire emit", {
43707
+ sessionId: s.sessionId,
43708
+ status: s.status,
43709
+ target
43710
+ });
43711
+ }
43683
43712
  if (compressed.type === "session:event" || compressed.type === "session:status") {
43684
43713
  const sid = compressed.sessionId;
43685
43714
  if (sid) {
@@ -43953,7 +43982,7 @@ var SessionManager = class {
43953
43982
  this.runners.delete(args.sessionId);
43954
43983
  this.realUuidBySynth.delete(args.sessionId);
43955
43984
  this.lastUiSizeBySessionId.delete(args.sessionId);
43956
- this.lastObserverEventAt.delete(args.sessionId);
43985
+ this.clearPendingTurnEnd(args.sessionId);
43957
43986
  return { response: { sessionId: args.sessionId }, broadcast };
43958
43987
  }
43959
43988
  this.deleteOwned(args.sessionId);
@@ -43983,6 +44012,7 @@ var SessionManager = class {
43983
44012
  async stop(args) {
43984
44013
  const runner = this.runners.get(args.sessionId);
43985
44014
  if (!runner) return { response: { ok: true }, broadcast: [] };
44015
+ this.clearPendingTurnEnd(args.sessionId);
43986
44016
  const { broadcast } = this.withCollector(() => {
43987
44017
  runner.input({ kind: "command", command: { kind: "stop" } });
43988
44018
  });
@@ -44116,6 +44146,7 @@ var SessionManager = class {
44116
44146
  newSession(args) {
44117
44147
  const existingFile = this.getFile(args.sessionId);
44118
44148
  const nextToolSessionId = this.deps.mode === "tui" && (existingFile.tool ?? "claude") === "claude" ? v4_default() : void 0;
44149
+ this.clearPendingTurnEnd(args.sessionId);
44119
44150
  const runner = this.runners.get(args.sessionId);
44120
44151
  if (runner) {
44121
44152
  const { value, broadcast } = this.withCollector(() => {
@@ -44216,6 +44247,7 @@ var SessionManager = class {
44216
44247
  for (const r of this.runners.values()) {
44217
44248
  r.input({ kind: "command", command: { kind: "stop" } });
44218
44249
  }
44250
+ this.pendingTurnDurationSignals.clear();
44219
44251
  }
44220
44252
  // 给 observer 用:拿已存在的 runner
44221
44253
  getActive(sessionId) {
@@ -44432,7 +44464,7 @@ var SessionManager = class {
44432
44464
  this.runners.delete(args.sessionId);
44433
44465
  this.realUuidBySynth.delete(args.sessionId);
44434
44466
  this.lastUiSizeBySessionId.delete(args.sessionId);
44435
- this.lastObserverEventAt.delete(args.sessionId);
44467
+ this.clearPendingTurnEnd(args.sessionId);
44436
44468
  return { response: { sessionId: args.sessionId }, broadcast };
44437
44469
  }
44438
44470
  this.storeFor(args.scope).delete(args.sessionId);
@@ -44678,23 +44710,93 @@ var SessionManager = class {
44678
44710
  return;
44679
44711
  }
44680
44712
  }
44681
- this.lastObserverEventAt.set(sessionId, (this.deps.now ?? Date.now)());
44682
44713
  let feedEvents = outEvents;
44683
44714
  if (outEvents.some((e) => e.kind === "turn_end")) {
44684
- const ev = this.peekTurnEvidence(runner);
44685
- if (!ev.turnHasContent) {
44715
+ const runnerState = runner.getState();
44716
+ const toolSessionId = runnerState.file.toolSessionId;
44717
+ const adapter = this.deps.getAdapter(runnerState.file.tool ?? "claude");
44718
+ const gateOpen = !adapter.canAcceptTurnEnd || !toolSessionId ? true : adapter.canAcceptTurnEnd(toolSessionId);
44719
+ this.deps.screenIdleProbeLogger?.info("turn_duration signal received", {
44720
+ sessionId,
44721
+ toolSessionId,
44722
+ batchKinds: outEvents.map((e) => e.kind),
44723
+ gateOpen,
44724
+ hasCanAcceptGate: !!adapter.canAcceptTurnEnd
44725
+ });
44726
+ if (gateOpen) {
44727
+ this.deps.screenIdleProbeLogger?.info(
44728
+ "turn_duration \u2192 turn_end confirmed (gate open) \u2192 fed to reducer",
44729
+ { sessionId, toolSessionId }
44730
+ );
44731
+ } else {
44686
44732
  feedEvents = outEvents.filter((e) => e.kind !== "turn_end");
44687
- this.deps.logger?.info("[TE-PROBE] drop spurious observer turn_end", {
44688
- sessionId,
44689
- src: "observer",
44690
- ...ev,
44691
- batchKinds: outEvents.map((e) => e.kind)
44692
- });
44733
+ if (this.pendingTurnDurationSignals.has(sessionId)) {
44734
+ this.deps.screenIdleProbeLogger?.info(
44735
+ "turn_duration dedup: pending already set (repeated observer signal)",
44736
+ { sessionId, toolSessionId }
44737
+ );
44738
+ } else {
44739
+ this.pendingTurnDurationSignals.add(sessionId);
44740
+ this.deps.screenIdleProbeLogger?.info(
44741
+ "turn_duration pending: gate closed \u2192 waiting for screen-idle signal",
44742
+ { sessionId, toolSessionId }
44743
+ );
44744
+ }
44693
44745
  }
44694
44746
  }
44695
44747
  if (feedEvents.length === 0) return;
44696
44748
  runner.feedObserverEvents(feedEvents);
44697
44749
  }
44750
+ /**
44751
+ * `ClaudeTuiAdapter.observeScreenIdle` fire triggered → armed=true 时调用(一次性触发点)。
44752
+ *
44753
+ * 语义:屏幕真的稳定了 5s。查有没有 pending 的 turn_duration 信号:
44754
+ * - 有 → 之前收到的 turn_duration 被屏幕稳定**确认**了,flush 成 turn_end 进 reducer
44755
+ * - 无 → 屏幕稳定但从没收到 turn_duration → noop(不生成 turn_end;补偿路径的错误做法)
44756
+ *
44757
+ * pending 集合是**必要前提**:turn_end 只能来自"observer 收 turn_duration + 屏幕后续稳定"
44758
+ * 双源确认,任何一个缺少都不能 emit。这跟 PR #962 拆掉的 dispatchTurnIdle "屏幕静止就补
44759
+ * turn_end" 语义不同。
44760
+ *
44761
+ * 仅 TUI 模式;SDK / codex 没有屏幕信号也就不会触发本方法。
44762
+ */
44763
+ notifyScreenIdle(toolSessionId) {
44764
+ if (this.deps.mode !== "tui") return;
44765
+ const sid = this.sessionIdByToolSid(toolSessionId);
44766
+ if (!sid) {
44767
+ this.deps.screenIdleProbeLogger?.warn("notifyScreenIdle: no session for toolSessionId", {
44768
+ toolSessionId
44769
+ });
44770
+ return;
44771
+ }
44772
+ if (!this.pendingTurnDurationSignals.has(sid)) {
44773
+ this.deps.screenIdleProbeLogger?.info(
44774
+ "notifyScreenIdle: no pending turn_duration \u2192 noop",
44775
+ { sessionId: sid, toolSessionId }
44776
+ );
44777
+ return;
44778
+ }
44779
+ const runner = this.runners.get(sid);
44780
+ if (!runner) {
44781
+ this.pendingTurnDurationSignals.delete(sid);
44782
+ this.deps.screenIdleProbeLogger?.warn(
44783
+ "notifyScreenIdle: pending but no runner \u2192 cleared without inject",
44784
+ { sessionId: sid, toolSessionId }
44785
+ );
44786
+ return;
44787
+ }
44788
+ this.pendingTurnDurationSignals.delete(sid);
44789
+ this.deps.screenIdleProbeLogger?.info(
44790
+ "notifyScreenIdle: pending turn_duration + screen idle confirmed \u2192 inject turn_end",
44791
+ { sessionId: sid, toolSessionId }
44792
+ );
44793
+ runner.input({ kind: "inject-events", events: [{ kind: "turn_end" }] });
44794
+ }
44795
+ clearPendingTurnEnd(sessionId) {
44796
+ if (this.pendingTurnDurationSignals.delete(sessionId)) {
44797
+ this.deps.screenIdleProbeLogger?.info("pending turn_duration cleared", { sessionId });
44798
+ }
44799
+ }
44698
44800
  // AskUserQuestion 表单回写(plan: clawd-ask-user-question):UI 答完所有 question 后调用。
44699
44801
  // - session 不存在 / 无 runner → noop 幂等返回 ok(first-decider-wins)
44700
44802
  // - reducer noop(toolUseId 不存在或已答过)也保持幂等返回,handler 不抛
@@ -44953,70 +45055,6 @@ var SessionManager = class {
44953
45055
  if (!runner) return;
44954
45056
  runner.input({ kind: "ready-detected" });
44955
45057
  }
44956
- /**
44957
- * ClaudeTuiAdapter onTurnIdle callback:屏幕内容静止时**复发**本轮已出现过的权威 turn_end。
44958
- * 本意:turn_duration 写盘早于尾段正文 → observer 把尾随 text 推进 buffer 盖掉 lastEventKind →
44959
- * spinner 不熄;屏幕静止时再补一条 turn_end 排到尾随 text 之后。
44960
- *
44961
- * Fix A(修 bug1 "UI 还在变却显示结束" / bug2 "发消息后无 spinner"):补偿**只复发不 originate**——
44962
- * 仅当本轮已出现过 turn_end(turnEndSeenThisTurn,即真有过尾随 text 覆盖场景)才补。turnEndSeenThisTurn
44963
- * ===false 说明本轮 CC 从未结束过(仍在工作 / 刚发消息没产出 / 漏检弹框等用户),屏幕静止 ≠ turn 结束,
44964
- * 凭空 inject turn_end 会误灭 spinner——故跳过,让真正的 turn_duration 来时再正常收。
44965
- * 仅 tui 模式;runner 缺失 noop。turn_end 无 uuid 不参与 dedup。
44966
- */
44967
- dispatchTurnIdle(toolSessionId) {
44968
- if (this.deps.mode !== "tui") return;
44969
- const sid = this.sessionIdByToolSid(toolSessionId);
44970
- const runner = sid ? this.runners.get(sid) : void 0;
44971
- if (!runner) return;
44972
- const ev = this.peekTurnEvidence(runner);
44973
- const willInject = ev.turnEndSeenThisTurn;
44974
- this.deps.logger?.info("[TE-PROBE] screen-idle compensation", {
44975
- sessionId: sid,
44976
- src: "screen-idle",
44977
- willInject,
44978
- ...ev
44979
- });
44980
- if (!willInject) return;
44981
- runner.input({ kind: "inject-events", events: [{ kind: "turn_end" }] });
44982
- }
44983
- /**
44984
- * 读 runner 当前 turn 态快照,供两个 turn_end 注入源(屏幕静止补偿 / observer turn_duration)
44985
- * 判断误发 + 打 [TE-PROBE] 日志。
44986
- * turnEndSeenThisTurn:从 buffer 末尾回扫到最近 user_text 期间是否已出现过 turn_end
44987
- * (已出现=本轮真结束过、合法尾随;未出现=本轮还没结束过)→ Fix A 复发闸。
44988
- * turnHasContent:末条是否 assistant 产出(非 user_text/turn_end/空)→ Fix B 空 turn 守卫闸。
44989
- */
44990
- peekTurnEvidence(runner) {
44991
- const st = runner.getState();
44992
- const buf = st.buffer;
44993
- const lastEventKindBefore = buf.length > 0 ? buf[buf.length - 1].event.kind : null;
44994
- let turnEndSeenThisTurn = false;
44995
- for (let i = buf.length - 1; i >= 0; i--) {
44996
- const k2 = buf[i].event.kind;
44997
- if (k2 === "user_text") break;
44998
- if (k2 === "turn_end") {
44999
- turnEndSeenThisTurn = true;
45000
- break;
45001
- }
45002
- }
45003
- const turnHasContent = lastEventKindBefore !== null && lastEventKindBefore !== "user_text" && lastEventKindBefore !== "turn_end";
45004
- return { turnOpenBefore: st.turnOpen, lastEventKindBefore, turnEndSeenThisTurn, turnHasContent };
45005
- }
45006
- /**
45007
- * observer 还需静止多久(ms)才满 idleMs,0 = 已满。observeScreenIdle 复合条件闸:屏幕静止后
45008
- * 精确等这段剩余再补 turn_end —— turn_duration 写盘早于尾段正文,observer 把尾随 text poll 落盘
45009
- * 期间屏幕可能已静止,仅看屏幕会早 fire(补的 turn_end 盖不到尾随 text 之后)。
45010
- * 找不到 runner / 从无事件 → 0(不阻塞 fire)。idleMs 由装配处传 SCREEN_IDLE_MS。
45011
- */
45012
- observerIdleWaitMs(toolSessionId, idleMs) {
45013
- const sid = this.sessionIdByToolSid(toolSessionId);
45014
- if (!sid) return 0;
45015
- const last = this.lastObserverEventAt.get(sid);
45016
- if (last === void 0) return 0;
45017
- const elapsed = (this.deps.now ?? Date.now)() - last;
45018
- return Math.max(0, idleMs - elapsed);
45019
- }
45020
45058
  /** toolSessionId → sessionId 反查(遍历 runners);session 数典型 < 10,O(n) 可接受 */
45021
45059
  sessionIdByToolSid(toolSessionId) {
45022
45060
  for (const [sid, runner] of this.runners) {
@@ -46755,6 +46793,7 @@ var CodexAdapter = class {
46755
46793
  };
46756
46794
 
46757
46795
  // src/tools/claude-tui.ts
46796
+ var import_node_crypto5 = require("crypto");
46758
46797
  var import_node_fs16 = __toESM(require("fs"), 1);
46759
46798
  var import_node_os7 = __toESM(require("os"), 1);
46760
46799
  var import_node_path14 = __toESM(require("path"), 1);
@@ -47565,22 +47604,56 @@ function observeScreenIdle(surface, opts) {
47565
47604
  timer = null;
47566
47605
  if (disposed) return;
47567
47606
  if (opts.getPopupVisible()) {
47607
+ opts.probeLogger?.info("screen-idle fire suppressed: popup visible", {
47608
+ label: opts.probeLabel
47609
+ });
47568
47610
  timer = setTimeout(fire, opts.idleMs);
47569
47611
  return;
47570
47612
  }
47571
47613
  const obsWait = opts.getObserverWaitMs?.() ?? 0;
47572
47614
  if (obsWait > 0) {
47615
+ opts.probeLogger?.info("screen-idle fire suppressed: observer not idle", {
47616
+ label: opts.probeLabel,
47617
+ obsWait
47618
+ });
47573
47619
  timer = setTimeout(fire, Math.max(obsWait, REWAIT_MIN_MS));
47574
47620
  return;
47575
47621
  }
47576
- if (armed) return;
47622
+ if (armed) {
47623
+ opts.probeLogger?.debug("screen-idle fire noop: already armed", {
47624
+ label: opts.probeLabel
47625
+ });
47626
+ return;
47627
+ }
47577
47628
  armed = true;
47629
+ opts.probeLogger?.info("screen-idle fire triggered \u2192 armed=true, calling onIdle", {
47630
+ label: opts.probeLabel
47631
+ });
47578
47632
  opts.onIdle();
47579
47633
  };
47580
47634
  const unsub = surface.onTick((lines) => {
47581
47635
  if (disposed) return;
47582
47636
  const snap = snapOf(lines);
47583
47637
  if (snap === lastSnap) return;
47638
+ if (opts.probeLogger) {
47639
+ const prev = lastSnap;
47640
+ const meta = {
47641
+ label: opts.probeLabel,
47642
+ prevHash: prev === null ? null : shortHash(prev),
47643
+ nextHash: shortHash(snap),
47644
+ prevLen: prev?.length ?? 0,
47645
+ nextLen: snap.length
47646
+ };
47647
+ if (prev !== null) {
47648
+ const diff2 = firstLineDiff(prev, snap);
47649
+ if (diff2) {
47650
+ meta.diffRow = diff2.row;
47651
+ meta.prevRow = diff2.prev;
47652
+ meta.nextRow = diff2.next;
47653
+ }
47654
+ }
47655
+ opts.probeLogger.info("screen-idle tick snap changed", meta);
47656
+ }
47584
47657
  lastSnap = snap;
47585
47658
  armed = false;
47586
47659
  clear();
@@ -47591,9 +47664,38 @@ function observeScreenIdle(surface, opts) {
47591
47664
  disposed = true;
47592
47665
  unsub();
47593
47666
  clear();
47667
+ },
47668
+ isIdle() {
47669
+ const popupVisible = opts.getPopupVisible();
47670
+ const idle = armed && !popupVisible;
47671
+ if (opts.probeLogger) {
47672
+ opts.probeLogger.info("screen-idle isIdle check", {
47673
+ label: opts.probeLabel,
47674
+ idle,
47675
+ armed,
47676
+ popupVisible
47677
+ });
47678
+ }
47679
+ return idle;
47594
47680
  }
47595
47681
  };
47596
47682
  }
47683
+ function shortHash(s) {
47684
+ return (0, import_node_crypto5.createHash)("sha1").update(s).digest("hex").slice(0, 8);
47685
+ }
47686
+ function firstLineDiff(prev, next) {
47687
+ const p2 = prev.split("\n");
47688
+ const n = next.split("\n");
47689
+ const rows = Math.max(p2.length, n.length);
47690
+ for (let i = 0; i < rows; i++) {
47691
+ const pl = p2[i] ?? "";
47692
+ const nl = n[i] ?? "";
47693
+ if (pl !== nl) {
47694
+ return { row: i, prev: pl.slice(0, 60), next: nl.slice(0, 60) };
47695
+ }
47696
+ }
47697
+ return null;
47698
+ }
47597
47699
  var BYPASS_SETTLE_MS = 300;
47598
47700
  var SCREEN_IDLE_MS = 5e3;
47599
47701
  function createBootGate(pty, logger) {
@@ -47668,11 +47770,42 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47668
47770
  // 用于 spawn / PtyChildProcess 链路打日志
47669
47771
  tuiLogger;
47670
47772
  tuiOpts;
47773
+ /**
47774
+ * per-toolSessionId 的 tui 观察者句柄,仅用于 turn_end gate 查询(`canAcceptTurnEnd`)。
47775
+ * onIdle / onPopupTransition 等回调仍走原有闭包(不复用这份 map),本 map 只承担
47776
+ * "manager 需要跨模块查屏幕/弹框状态"这单一职责。
47777
+ */
47778
+ tuiStates = /* @__PURE__ */ new Map();
47671
47779
  constructor(opts = {}) {
47672
47780
  super(opts);
47673
47781
  this.tuiLogger = opts.logger;
47674
47782
  this.tuiOpts = opts;
47675
47783
  }
47784
+ /**
47785
+ * TUI adapter 的 turn_end 权威判定:屏幕已 idle 且非弹框态才放行。
47786
+ *
47787
+ * `feedObserverEvents` 收到 observer 回灌 `turn_end` 时调用。屏幕仍在变(如后台 agent 在跑)
47788
+ * 时 drop 掉 turn_end,避免 `system/turn_duration` JSONL 帧误触发 running-idle 状态转换。
47789
+ *
47790
+ * 未跟踪的 toolSessionId(spawn 前 / spawn 失败 / 已 dispose)视为 pass —— gate 只 drop
47791
+ * "有证据判定为伪信号"的场景,不做 unknown → block。
47792
+ */
47793
+ canAcceptTurnEnd(toolSessionId) {
47794
+ const state = this.tuiStates.get(toolSessionId);
47795
+ if (!state) {
47796
+ this.tuiOpts.screenIdleProbeLogger?.info(
47797
+ "canAcceptTurnEnd: no tuiState \u2192 pass (\u672A\u8DDF\u8E2A)",
47798
+ { toolSessionId }
47799
+ );
47800
+ return true;
47801
+ }
47802
+ const result = state.screenIdle.isIdle();
47803
+ this.tuiOpts.screenIdleProbeLogger?.info("canAcceptTurnEnd", {
47804
+ toolSessionId,
47805
+ result
47806
+ });
47807
+ return result;
47808
+ }
47676
47809
  spawn(ctx) {
47677
47810
  const args = buildTuiSpawnArgs(ctx, jsonlExistsForCtx(ctx));
47678
47811
  const cmd = process.env.CLAUDE_BIN ?? "claude";
@@ -47730,18 +47863,26 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47730
47863
  const screenIdleObserver = observeScreenIdle(surface, {
47731
47864
  idleMs: SCREEN_IDLE_MS,
47732
47865
  onIdle: () => {
47733
- if (!ctx.toolSessionId || !this.tuiOpts.onTurnIdle) return;
47734
- this.tuiLogger?.debug("screen-idle \u2192 turn_end", { toolSessionId: ctx.toolSessionId });
47735
- this.tuiOpts.onTurnIdle(ctx.toolSessionId);
47866
+ if (!ctx.toolSessionId || !this.tuiOpts.onScreenIdle) return;
47867
+ this.tuiLogger?.debug("screen-idle \u2192 notifyScreenIdle", { toolSessionId: ctx.toolSessionId });
47868
+ this.tuiOpts.onScreenIdle(ctx.toolSessionId);
47736
47869
  },
47737
47870
  getPopupVisible: () => popupObserver.visibleKind !== null,
47738
- // observer 还需静止多久才满 SCREEN_IDLE_MS(复合条件 AND):屏幕静止后精确等这段剩余再补
47739
- // turn_end,确保它排在尾段 text + turn_duration 全部 poll 落盘之后 = buffer 末条。
47740
- getObserverWaitMs: () => ctx.toolSessionId ? this.tuiOpts.getObserverWaitMs?.(ctx.toolSessionId, SCREEN_IDLE_MS) ?? 0 : 0
47871
+ // 取证 probe(可选,装配处传独立 file-only logger,跟主 daemon.log 解耦)
47872
+ ...this.tuiOpts.screenIdleProbeLogger ? {
47873
+ probeLogger: this.tuiOpts.screenIdleProbeLogger,
47874
+ probeLabel: ctx.toolSessionId ?? "<no-tsid>"
47875
+ } : {}
47741
47876
  });
47742
47877
  if (ctx.toolSessionId && this.tuiOpts.onSurfaceRegister) {
47743
47878
  this.tuiOpts.onSurfaceRegister(ctx.toolSessionId, surface);
47744
47879
  }
47880
+ if (ctx.toolSessionId) {
47881
+ this.tuiStates.set(ctx.toolSessionId, {
47882
+ screenIdle: screenIdleObserver,
47883
+ popup: popupObserver
47884
+ });
47885
+ }
47745
47886
  let chunkSeq = 0;
47746
47887
  if (ctx.toolSessionId && this.tuiOpts.onPtyReplayRegister) {
47747
47888
  this.tuiOpts.onPtyReplayRegister(ctx.toolSessionId, async () => {
@@ -47787,6 +47928,9 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47787
47928
  readyObserver.dispose();
47788
47929
  popupObserver.dispose();
47789
47930
  screenIdleObserver.dispose();
47931
+ if (ctx.toolSessionId) {
47932
+ this.tuiStates.delete(ctx.toolSessionId);
47933
+ }
47790
47934
  if (ctx.toolSessionId && this.tuiOpts.onSurfaceUnregister) {
47791
47935
  this.tuiOpts.onSurfaceUnregister(ctx.toolSessionId);
47792
47936
  }
@@ -48069,7 +48213,7 @@ async function writeInboxMcpConfig(args) {
48069
48213
  // src/shift/store.ts
48070
48214
  var import_promises = __toESM(require("fs/promises"), 1);
48071
48215
  var import_node_path19 = __toESM(require("path"), 1);
48072
- var import_node_crypto5 = require("crypto");
48216
+ var import_node_crypto6 = require("crypto");
48073
48217
 
48074
48218
  // src/shift/constants.ts
48075
48219
  var MAX_RUNS_PER_SHIFT = 30;
@@ -48165,7 +48309,7 @@ function createShiftStore(deps) {
48165
48309
  const nextRunAtMs = computeNextRunAtMs(input.schedule, now) ?? void 0;
48166
48310
  const shift = {
48167
48311
  ...input,
48168
- id: (0, import_node_crypto5.randomUUID)(),
48312
+ id: (0, import_node_crypto6.randomUUID)(),
48169
48313
  createdAtMs: now,
48170
48314
  updatedAtMs: now,
48171
48315
  state: { nextRunAtMs },
@@ -51076,7 +51220,7 @@ function lookupMime(filePathOrName) {
51076
51220
  }
51077
51221
 
51078
51222
  // src/attachment/sign-url.ts
51079
- var import_node_crypto6 = __toESM(require("crypto"), 1);
51223
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
51080
51224
  var HMAC_ALGO = "sha256";
51081
51225
  function base64urlEncode(buf) {
51082
51226
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -51093,7 +51237,7 @@ function decodeAbsPathFromUrl(encoded) {
51093
51237
  }
51094
51238
  function computeSig(secret, absPath, e) {
51095
51239
  const msg = e === null ? absPath : `${absPath}|${e}`;
51096
- return import_node_crypto6.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
51240
+ return import_node_crypto7.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
51097
51241
  }
51098
51242
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
51099
51243
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -51128,7 +51272,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
51128
51272
  if (provided.length !== expected.length) {
51129
51273
  return { ok: false, code: "BAD_SIG" };
51130
51274
  }
51131
- if (!import_node_crypto6.default.timingSafeEqual(provided, expected)) {
51275
+ if (!import_node_crypto7.default.timingSafeEqual(provided, expected)) {
51132
51276
  return { ok: false, code: "BAD_SIG" };
51133
51277
  }
51134
51278
  if (e !== null && now() / 1e3 > e) {
@@ -51140,7 +51284,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
51140
51284
  // src/attachment/upload.ts
51141
51285
  var import_node_fs25 = __toESM(require("fs"), 1);
51142
51286
  var import_node_path25 = __toESM(require("path"), 1);
51143
- var import_node_crypto7 = __toESM(require("crypto"), 1);
51287
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
51144
51288
  var import_promises2 = require("stream/promises");
51145
51289
  var UploadError = class extends Error {
51146
51290
  constructor(code, message) {
@@ -51164,11 +51308,11 @@ async function writeUploadedAttachment(args) {
51164
51308
  } catch (err) {
51165
51309
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
51166
51310
  }
51167
- const hasher = import_node_crypto7.default.createHash("sha256");
51311
+ const hasher = import_node_crypto8.default.createHash("sha256");
51168
51312
  let actualSize = 0;
51169
51313
  const tmpPath = import_node_path25.default.join(
51170
51314
  attachmentsRoot,
51171
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto7.default.randomBytes(4).toString("hex")}`
51315
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto8.default.randomBytes(4).toString("hex")}`
51172
51316
  );
51173
51317
  try {
51174
51318
  await (0, import_promises2.pipeline)(
@@ -52045,7 +52189,7 @@ function runAttachmentGc(args) {
52045
52189
  // src/attachment/group.ts
52046
52190
  var import_node_fs28 = __toESM(require("fs"), 1);
52047
52191
  var import_node_path29 = __toESM(require("path"), 1);
52048
- var import_node_crypto8 = __toESM(require("crypto"), 1);
52192
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
52049
52193
  init_protocol();
52050
52194
  var GroupFileStore = class {
52051
52195
  dataDir;
@@ -52134,7 +52278,7 @@ var GroupFileStore = class {
52134
52278
  entries[idx] = next;
52135
52279
  } else {
52136
52280
  next = {
52137
- id: `gf-${import_node_crypto8.default.randomBytes(6).toString("base64url")}`,
52281
+ id: `gf-${import_node_crypto9.default.randomBytes(6).toString("base64url")}`,
52138
52282
  relPath: input.relPath,
52139
52283
  from: input.from,
52140
52284
  label: input.label,
@@ -52253,7 +52397,7 @@ function readDaemonSourceFromEnv(env = process.env) {
52253
52397
  // src/tunnel/tunnel-manager.ts
52254
52398
  var import_node_fs33 = __toESM(require("fs"), 1);
52255
52399
  var import_node_path34 = __toESM(require("path"), 1);
52256
- var import_node_crypto9 = __toESM(require("crypto"), 1);
52400
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
52257
52401
  var import_node_child_process9 = require("child_process");
52258
52402
 
52259
52403
  // src/tunnel/tunnel-store.ts
@@ -52752,7 +52896,7 @@ var TunnelManager = class {
52752
52896
  override: this.deps.frpcBinaryOverride ?? void 0
52753
52897
  });
52754
52898
  const tomlPath = import_node_path34.default.join(this.deps.dataDir, "frpc.toml");
52755
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto9.default.randomBytes(3).toString("hex")}`;
52899
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto10.default.randomBytes(3).toString("hex")}`;
52756
52900
  const toml = buildFrpcToml({
52757
52901
  serverAddr: t.frpsHost,
52758
52902
  serverPort: t.frpsPort,
@@ -53617,7 +53761,7 @@ function pumpWsToSshd(ws, deps, clientAddr) {
53617
53761
  // src/tunnel/device-key.ts
53618
53762
  var import_node_os14 = __toESM(require("os"), 1);
53619
53763
  var import_node_path41 = __toESM(require("path"), 1);
53620
- var import_node_crypto10 = __toESM(require("crypto"), 1);
53764
+ var import_node_crypto11 = __toESM(require("crypto"), 1);
53621
53765
  var DERIVE_SALT = "clawd-tunnel-device-v1";
53622
53766
  function deriveStableDeviceKey(opts = {}) {
53623
53767
  const hostname = opts.hostname ?? import_node_os14.default.hostname();
@@ -53627,13 +53771,13 @@ function deriveStableDeviceKey(opts = {}) {
53627
53771
  const normalizedDataDir = opts.dataDir ? import_node_path41.default.resolve(opts.dataDir) : null;
53628
53772
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
53629
53773
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
53630
- return import_node_crypto10.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
53774
+ return import_node_crypto11.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
53631
53775
  }
53632
53776
 
53633
53777
  // src/auth-store.ts
53634
53778
  var import_node_fs40 = __toESM(require("fs"), 1);
53635
53779
  var import_node_path42 = __toESM(require("path"), 1);
53636
- var import_node_crypto11 = __toESM(require("crypto"), 1);
53780
+ var import_node_crypto12 = __toESM(require("crypto"), 1);
53637
53781
  var AUTH_FILE_NAME = "auth.json";
53638
53782
  function authFilePath(dataDir) {
53639
53783
  return import_node_path42.default.join(dataDir, AUTH_FILE_NAME);
@@ -53665,10 +53809,10 @@ function loadOrCreateAuthFile(opts) {
53665
53809
  return next;
53666
53810
  }
53667
53811
  function defaultGenerateToken() {
53668
- return import_node_crypto11.default.randomBytes(32).toString("base64url");
53812
+ return import_node_crypto12.default.randomBytes(32).toString("base64url");
53669
53813
  }
53670
53814
  function defaultGenerateOwnerPrincipalId() {
53671
- return `owner-${import_node_crypto11.default.randomUUID()}`;
53815
+ return `owner-${import_node_crypto12.default.randomUUID()}`;
53672
53816
  }
53673
53817
  function readAuthFile(file) {
53674
53818
  try {
@@ -53790,7 +53934,7 @@ var OwnerIdentityStore = class {
53790
53934
  };
53791
53935
 
53792
53936
  // src/feishu-auth/login-flow.ts
53793
- var import_node_crypto12 = __toESM(require("crypto"), 1);
53937
+ var import_node_crypto13 = __toESM(require("crypto"), 1);
53794
53938
  var STATE_TTL_MS = 5 * 60 * 1e3;
53795
53939
  var LoginFlow = class {
53796
53940
  constructor(deps) {
@@ -53799,7 +53943,7 @@ var LoginFlow = class {
53799
53943
  deps;
53800
53944
  pendingStates = /* @__PURE__ */ new Map();
53801
53945
  start() {
53802
- const state = import_node_crypto12.default.randomBytes(16).toString("base64url");
53946
+ const state = import_node_crypto13.default.randomBytes(16).toString("base64url");
53803
53947
  const now = (this.deps.now ?? Date.now)();
53804
53948
  this.pendingStates.set(state, now);
53805
53949
  this.gcExpired(now);
@@ -55834,7 +55978,7 @@ init_src();
55834
55978
  // src/extension/bundle-zip.ts
55835
55979
  var import_promises5 = __toESM(require("fs/promises"), 1);
55836
55980
  var import_node_path49 = __toESM(require("path"), 1);
55837
- var import_node_crypto13 = __toESM(require("crypto"), 1);
55981
+ var import_node_crypto14 = __toESM(require("crypto"), 1);
55838
55982
  var import_jszip2 = __toESM(require_lib3(), 1);
55839
55983
  async function bundleExtensionDir(dir) {
55840
55984
  const entries = await listFilesSorted(dir);
@@ -55849,7 +55993,7 @@ async function bundleExtensionDir(dir) {
55849
55993
  compression: "DEFLATE",
55850
55994
  compressionOptions: { level: 6 }
55851
55995
  });
55852
- const sha256 = import_node_crypto13.default.createHash("sha256").update(buffer).digest("hex");
55996
+ const sha256 = import_node_crypto14.default.createHash("sha256").update(buffer).digest("hex");
55853
55997
  return { buffer, sha256 };
55854
55998
  }
55855
55999
  var FIXED_DATE = /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
@@ -55919,7 +56063,7 @@ function computePublishCheck(args) {
55919
56063
  var import_promises6 = __toESM(require("fs/promises"), 1);
55920
56064
  var import_node_path51 = __toESM(require("path"), 1);
55921
56065
  var import_node_os19 = __toESM(require("os"), 1);
55922
- var import_node_crypto14 = __toESM(require("crypto"), 1);
56066
+ var import_node_crypto15 = __toESM(require("crypto"), 1);
55923
56067
  var import_jszip3 = __toESM(require_lib3(), 1);
55924
56068
  init_src();
55925
56069
 
@@ -55950,7 +56094,7 @@ var InstallError = class extends Error {
55950
56094
  };
55951
56095
  async function installFromChannel(args, deps) {
55952
56096
  const { channelRef, snapshotHash, bundleZip } = args;
55953
- const computed = import_node_crypto14.default.createHash("sha256").update(bundleZip).digest("hex");
56097
+ const computed = import_node_crypto15.default.createHash("sha256").update(bundleZip).digest("hex");
55954
56098
  if (computed !== snapshotHash) {
55955
56099
  throw new InstallError(
55956
56100
  "HASH_MISMATCH",
@@ -56042,7 +56186,7 @@ async function installFromChannel(args, deps) {
56042
56186
  var import_promises7 = __toESM(require("fs/promises"), 1);
56043
56187
  var import_node_path52 = __toESM(require("path"), 1);
56044
56188
  var import_node_os20 = __toESM(require("os"), 1);
56045
- var import_node_crypto15 = __toESM(require("crypto"), 1);
56189
+ var import_node_crypto16 = __toESM(require("crypto"), 1);
56046
56190
  var import_jszip4 = __toESM(require_lib3(), 1);
56047
56191
  init_src();
56048
56192
  var UpdateError = class extends Error {
@@ -56081,7 +56225,7 @@ async function updateFromChannel(args, deps) {
56081
56225
  if (e instanceof UpdateError) throw e;
56082
56226
  throw e;
56083
56227
  }
56084
- const computed = import_node_crypto15.default.createHash("sha256").update(bundleZip).digest("hex");
56228
+ const computed = import_node_crypto16.default.createHash("sha256").update(bundleZip).digest("hex");
56085
56229
  if (computed !== snapshotHash) {
56086
56230
  throw new UpdateError(
56087
56231
  "HASH_MISMATCH",
@@ -56787,7 +56931,7 @@ function listPidsOnPort(port) {
56787
56931
  }
56788
56932
 
56789
56933
  // src/app-builder/publish-registry.ts
56790
- var import_node_crypto16 = require("crypto");
56934
+ var import_node_crypto17 = require("crypto");
56791
56935
  var PublishJobRegistry = class {
56792
56936
  jobs = /* @__PURE__ */ new Map();
56793
56937
  has(name) {
@@ -56804,7 +56948,7 @@ var PublishJobRegistry = class {
56804
56948
  if (this.jobs.has(args.name)) {
56805
56949
  throw new Error(`already publishing: ${args.name}`);
56806
56950
  }
56807
- const jobId = args.jobId ?? `job-${(0, import_node_crypto16.randomUUID)()}`;
56951
+ const jobId = args.jobId ?? `job-${(0, import_node_crypto17.randomUUID)()}`;
56808
56952
  this.jobs.set(args.name, {
56809
56953
  jobId,
56810
56954
  name: args.name,
@@ -57765,7 +57909,7 @@ async function uninstall(deps) {
57765
57909
  }
57766
57910
 
57767
57911
  // src/handlers/index.ts
57768
- var import_node_crypto17 = require("crypto");
57912
+ var import_node_crypto18 = require("crypto");
57769
57913
  init_peer_forward();
57770
57914
  function buildMethodHandlers(deps) {
57771
57915
  return {
@@ -57799,7 +57943,7 @@ function buildMethodHandlers(deps) {
57799
57943
  const c = deps.contactStore.get(deviceId);
57800
57944
  return c ? { deviceId: c.deviceId, remoteUrl: c.remoteUrl, connectToken: c.connectToken } : null;
57801
57945
  },
57802
- genId: () => (0, import_node_crypto17.randomUUID)(),
57946
+ genId: () => (0, import_node_crypto18.randomUUID)(),
57803
57947
  now: () => Date.now(),
57804
57948
  forwardInboxPostToPeer,
57805
57949
  logger: deps.logger
@@ -58715,6 +58859,13 @@ async function startDaemon(config) {
58715
58859
  logClient
58716
58860
  });
58717
58861
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
58862
+ const screenIdleProbeLogger = createFileOnlyLogger({
58863
+ file: import_node_path62.default.join(config.dataDir, "screen-idle-probe.log"),
58864
+ level: "debug"
58865
+ });
58866
+ logger.info("screen-idle probe logger enabled", {
58867
+ file: import_node_path62.default.join(config.dataDir, "screen-idle-probe.log")
58868
+ });
58718
58869
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
58719
58870
  const pre = stateMgr.preflight();
58720
58871
  if (pre.status === "active") {
@@ -58991,6 +59142,10 @@ async function startDaemon(config) {
58991
59142
  // 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
58992
59143
  storeFactory: sessionStoreFactory,
58993
59144
  logger,
59145
+ // 取证 probe(可选,CLAWD_SCREEN_IDLE_PROBE=1 时启用):manager turn_end 判定链
59146
+ // 的所有决策点打到独立文件,跟 adapter 的 observeScreenIdle probe 共用同一份 file logger,
59147
+ // 便于 grep sessionId 时 tui 层 + manager 层交叉时序都在同一文件里
59148
+ ...screenIdleProbeLogger ? { screenIdleProbeLogger } : {},
58994
59149
  getAdapter,
58995
59150
  historyReader: history,
58996
59151
  dataDir: config.dataDir,
@@ -59108,10 +59263,10 @@ async function startDaemon(config) {
59108
59263
  onSurfaceUnregister: (tsid) => manager.unregisterSurface(tsid),
59109
59264
  // ReadyGate v2:ReadyDetector emit ready 时投递 reducer 'ready-detected' input
59110
59265
  onReady: (tsid) => manager.dispatchReadyDetected(tsid),
59111
- // 屏幕静止补权威 turn_end(修 turn_duration 尾随 text 覆盖 lastEventKind)
59112
- onTurnIdle: (tsid) => manager.dispatchTurnIdle(tsid),
59113
- // 复合条件闸:observer 还需静止多久才满 idleMs(屏幕静止后精确等这段剩余再补 turn_end
59114
- getObserverWaitMs: (tsid, idleMs) => manager.observerIdleWaitMs(tsid, idleMs)
59266
+ // 屏幕真稳定 5s 的一次性信号 manager pending turn_duration flush turn_end
59267
+ onScreenIdle: (tsid) => manager.notifyScreenIdle(tsid),
59268
+ // 取证 probe(默认无条件启用;见 createFileOnlyLogger
59269
+ screenIdleProbeLogger
59115
59270
  }) : new ClaudeAdapter({ logger, historyReader: new ClaudeHistoryReader() });
59116
59271
  registerAdapter("claude", claudeAdapter);
59117
59272
  registerAdapter("codex", new CodexAdapter({ logger, historyReader: new CodexHistoryReader() }));