@clawos-dev/clawd 0.2.198 → 0.2.199-beta.399.3cb813c

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 +91 -17
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -42956,6 +42956,18 @@ var SessionManager = class {
42956
42956
  // 由 observer 监听 jsonl user 行后调 recordRealUserUuid 建立映射;rewind 系列 RPC 在
42957
42957
  // 入参 / 出参做转译,保证 UI 看到的 uuid 始终是 events 流里的 synth uuid
42958
42958
  realUuidBySynth = /* @__PURE__ */ new Map();
42959
+ // observer 收到 `turn_duration` 信号但屏幕还没稳定 5s → 挂进 pending,等 `notifyScreenIdle`
42960
+ // (屏幕 armed=true 触发点)flush 成真正的 turn_end 进 reducer。
42961
+ //
42962
+ // 语义澄清:`turn_duration` 是 CC 报的原始信号("本轮 API 调用完了"),**不代表 turn 真的结束**
42963
+ //(背景 agent 可能还在跑)。屏幕 5s 稳定 + 无 popup 才是"确认信号"。
42964
+ // 两者 AND 后 daemon 才产生真正的 `turn_end` 事件送给 reducer。
42965
+ //
42966
+ // pending 不设截止时间:屏幕稳定的时机由 CC UI 决定,可能几秒也可能几分钟。观察者以
42967
+ // "屏幕真的稳定"作为触发点,signal-driven 而非 timer-driven。
42968
+ //
42969
+ // 清理点:newSession / stop / session-delete / stopAll。
42970
+ pendingTurnDurationSignals = /* @__PURE__ */ new Set();
42959
42971
  // SessionStore 按 scope 派生(root = <dataDir>/sessions/<scopeSubPath>/)。
42960
42972
  // default scope 直接复用 deps.store;persona scope(owner / listener)第一次访问时按需创建并缓存。
42961
42973
  // 取代旧的 storesByAgent —— agentId 概念由 SessionScope 取代,路径即身份,
@@ -43576,6 +43588,7 @@ var SessionManager = class {
43576
43588
  this.runners.delete(args.sessionId);
43577
43589
  this.realUuidBySynth.delete(args.sessionId);
43578
43590
  this.lastUiSizeBySessionId.delete(args.sessionId);
43591
+ this.clearPendingTurnEnd(args.sessionId);
43579
43592
  return { response: { sessionId: args.sessionId }, broadcast };
43580
43593
  }
43581
43594
  this.deleteOwned(args.sessionId);
@@ -43605,6 +43618,7 @@ var SessionManager = class {
43605
43618
  async stop(args) {
43606
43619
  const runner = this.runners.get(args.sessionId);
43607
43620
  if (!runner) return { response: { ok: true }, broadcast: [] };
43621
+ this.clearPendingTurnEnd(args.sessionId);
43608
43622
  const { broadcast } = this.withCollector(() => {
43609
43623
  runner.input({ kind: "command", command: { kind: "stop" } });
43610
43624
  });
@@ -43738,6 +43752,7 @@ var SessionManager = class {
43738
43752
  newSession(args) {
43739
43753
  const existingFile = this.getFile(args.sessionId);
43740
43754
  const nextToolSessionId = this.deps.mode === "tui" && (existingFile.tool ?? "claude") === "claude" ? v4_default() : void 0;
43755
+ this.clearPendingTurnEnd(args.sessionId);
43741
43756
  const runner = this.runners.get(args.sessionId);
43742
43757
  if (runner) {
43743
43758
  const { value, broadcast } = this.withCollector(() => {
@@ -43838,6 +43853,7 @@ var SessionManager = class {
43838
43853
  for (const r of this.runners.values()) {
43839
43854
  r.input({ kind: "command", command: { kind: "stop" } });
43840
43855
  }
43856
+ this.pendingTurnDurationSignals.clear();
43841
43857
  }
43842
43858
  // 给 observer 用:拿已存在的 runner
43843
43859
  getActive(sessionId) {
@@ -44054,6 +44070,7 @@ var SessionManager = class {
44054
44070
  this.runners.delete(args.sessionId);
44055
44071
  this.realUuidBySynth.delete(args.sessionId);
44056
44072
  this.lastUiSizeBySessionId.delete(args.sessionId);
44073
+ this.clearPendingTurnEnd(args.sessionId);
44057
44074
  return { response: { sessionId: args.sessionId }, broadcast };
44058
44075
  }
44059
44076
  this.storeFor(args.scope).delete(args.sessionId);
@@ -44305,35 +44322,87 @@ var SessionManager = class {
44305
44322
  const toolSessionId = runnerState.file.toolSessionId;
44306
44323
  const adapter = this.deps.getAdapter(runnerState.file.tool ?? "claude");
44307
44324
  const gateOpen = !adapter.canAcceptTurnEnd || !toolSessionId ? true : adapter.canAcceptTurnEnd(toolSessionId);
44308
- this.deps.screenIdleProbeLogger?.info("feedObserverEvents turn_end batch received", {
44325
+ this.deps.screenIdleProbeLogger?.info("turn_duration signal received", {
44309
44326
  sessionId,
44310
44327
  toolSessionId,
44311
44328
  batchKinds: outEvents.map((e) => e.kind),
44312
44329
  gateOpen,
44313
44330
  hasCanAcceptGate: !!adapter.canAcceptTurnEnd
44314
44331
  });
44315
- if (!gateOpen) {
44316
- this.deps.logger?.info("drop turn_end (adapter gate closed)", {
44317
- sessionId,
44318
- toolSessionId,
44319
- reason: "tui-screen-not-idle"
44320
- });
44321
- this.deps.screenIdleProbeLogger?.info("drop turn_end: adapter gate closed", {
44322
- sessionId,
44323
- toolSessionId,
44324
- reason: "tui-screen-not-idle"
44325
- });
44326
- feedEvents = outEvents.filter((e) => e.kind !== "turn_end");
44332
+ if (gateOpen) {
44333
+ this.deps.screenIdleProbeLogger?.info(
44334
+ "turn_duration \u2192 turn_end confirmed (gate open) \u2192 fed to reducer",
44335
+ { sessionId, toolSessionId }
44336
+ );
44327
44337
  } else {
44328
- this.deps.screenIdleProbeLogger?.info("turn_end passed gate \u2192 fed to reducer", {
44329
- sessionId,
44330
- toolSessionId
44331
- });
44338
+ feedEvents = outEvents.filter((e) => e.kind !== "turn_end");
44339
+ if (this.pendingTurnDurationSignals.has(sessionId)) {
44340
+ this.deps.screenIdleProbeLogger?.info(
44341
+ "turn_duration dedup: pending already set (repeated observer signal)",
44342
+ { sessionId, toolSessionId }
44343
+ );
44344
+ } else {
44345
+ this.pendingTurnDurationSignals.add(sessionId);
44346
+ this.deps.screenIdleProbeLogger?.info(
44347
+ "turn_duration pending: gate closed \u2192 waiting for screen-idle signal",
44348
+ { sessionId, toolSessionId }
44349
+ );
44350
+ }
44332
44351
  }
44333
44352
  }
44334
44353
  if (feedEvents.length === 0) return;
44335
44354
  runner.feedObserverEvents(feedEvents);
44336
44355
  }
44356
+ /**
44357
+ * `ClaudeTuiAdapter.observeScreenIdle` fire triggered → armed=true 时调用(一次性触发点)。
44358
+ *
44359
+ * 语义:屏幕真的稳定了 5s。查有没有 pending 的 turn_duration 信号:
44360
+ * - 有 → 之前收到的 turn_duration 被屏幕稳定**确认**了,flush 成 turn_end 进 reducer
44361
+ * - 无 → 屏幕稳定但从没收到 turn_duration → noop(不生成 turn_end;补偿路径的错误做法)
44362
+ *
44363
+ * pending 集合是**必要前提**:turn_end 只能来自"observer 收 turn_duration + 屏幕后续稳定"
44364
+ * 双源确认,任何一个缺少都不能 emit。这跟 PR #962 拆掉的 dispatchTurnIdle "屏幕静止就补
44365
+ * turn_end" 语义不同。
44366
+ *
44367
+ * 仅 TUI 模式;SDK / codex 没有屏幕信号也就不会触发本方法。
44368
+ */
44369
+ notifyScreenIdle(toolSessionId) {
44370
+ if (this.deps.mode !== "tui") return;
44371
+ const sid = this.sessionIdByToolSid(toolSessionId);
44372
+ if (!sid) {
44373
+ this.deps.screenIdleProbeLogger?.warn("notifyScreenIdle: no session for toolSessionId", {
44374
+ toolSessionId
44375
+ });
44376
+ return;
44377
+ }
44378
+ if (!this.pendingTurnDurationSignals.has(sid)) {
44379
+ this.deps.screenIdleProbeLogger?.info(
44380
+ "notifyScreenIdle: no pending turn_duration \u2192 noop",
44381
+ { sessionId: sid, toolSessionId }
44382
+ );
44383
+ return;
44384
+ }
44385
+ const runner = this.runners.get(sid);
44386
+ if (!runner) {
44387
+ this.pendingTurnDurationSignals.delete(sid);
44388
+ this.deps.screenIdleProbeLogger?.warn(
44389
+ "notifyScreenIdle: pending but no runner \u2192 cleared without inject",
44390
+ { sessionId: sid, toolSessionId }
44391
+ );
44392
+ return;
44393
+ }
44394
+ this.pendingTurnDurationSignals.delete(sid);
44395
+ this.deps.screenIdleProbeLogger?.info(
44396
+ "notifyScreenIdle: pending turn_duration + screen idle confirmed \u2192 inject turn_end",
44397
+ { sessionId: sid, toolSessionId }
44398
+ );
44399
+ runner.input({ kind: "inject-events", events: [{ kind: "turn_end" }] });
44400
+ }
44401
+ clearPendingTurnEnd(sessionId) {
44402
+ if (this.pendingTurnDurationSignals.delete(sessionId)) {
44403
+ this.deps.screenIdleProbeLogger?.info("pending turn_duration cleared", { sessionId });
44404
+ }
44405
+ }
44337
44406
  // AskUserQuestion 表单回写(plan: clawd-ask-user-question):UI 答完所有 question 后调用。
44338
44407
  // - session 不存在 / 无 runner → noop 幂等返回 ok(first-decider-wins)
44339
44408
  // - reducer noop(toolUseId 不存在或已答过)也保持幂等返回,handler 不抛
@@ -47404,6 +47473,9 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47404
47473
  const screenIdleObserver = observeScreenIdle(surface, {
47405
47474
  idleMs: SCREEN_IDLE_MS,
47406
47475
  onIdle: () => {
47476
+ if (!ctx.toolSessionId || !this.tuiOpts.onScreenIdle) return;
47477
+ this.tuiLogger?.debug("screen-idle \u2192 notifyScreenIdle", { toolSessionId: ctx.toolSessionId });
47478
+ this.tuiOpts.onScreenIdle(ctx.toolSessionId);
47407
47479
  },
47408
47480
  getPopupVisible: () => popupObserver.visibleKind !== null,
47409
47481
  // 取证 probe(可选,装配处传独立 file-only logger,跟主 daemon.log 解耦)
@@ -57896,6 +57968,8 @@ async function startDaemon(config) {
57896
57968
  onSurfaceUnregister: (tsid) => manager.unregisterSurface(tsid),
57897
57969
  // ReadyGate v2:ReadyDetector emit ready 时投递 reducer 'ready-detected' input
57898
57970
  onReady: (tsid) => manager.dispatchReadyDetected(tsid),
57971
+ // 屏幕真稳定 5s 的一次性信号 → manager 查 pending turn_duration 并 flush 成 turn_end
57972
+ onScreenIdle: (tsid) => manager.notifyScreenIdle(tsid),
57899
57973
  // 取证 probe(默认无条件启用;见 createFileOnlyLogger)
57900
57974
  screenIdleProbeLogger
57901
57975
  }) : new ClaudeAdapter({ logger, historyReader: new ClaudeHistoryReader() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.198",
3
+ "version": "0.2.199-beta.399.3cb813c",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",