@clawos-dev/clawd 0.2.93-beta.174.78b779b → 0.2.93

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 +135 -38
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -114,8 +114,13 @@ var init_methods = __esm({
114
114
  "session:pty:resize",
115
115
  // session:pty:snapshot — UI XtermPanel 重新挂载(切 session 回来 / 刷新)时主动拉一次当前
116
116
  // 屏幕全量快照,再开始消费 incremental session:pty。daemon 调 manager.getPtyReplay(sessionId)
117
- // 取该 session @xterm/headless detector serialize 结果。response.datasession:pty
118
- // push 帧同格式(base64-utf8);session 不存在 / 非 tui / pty 已 exit 时 data=null。
117
+ // drain detector parse queue serialize,保证 atSeqbuffer 内容严格对齐。
118
+ // response 字段:
119
+ // - data:当前屏幕 + scrollback 的 ANSI 字节串(base64-utf8),与 session:pty push 帧同格式
120
+ // - atSeq:此 snapshot 覆盖到的最后一帧 session:pty 的 seq(即 detector 已 parse 完成的最大 seq)
121
+ // UI 端 flush 时丢弃 pending 中 seq ≤ atSeq 的帧
122
+ // - popupKind:当前 detector 检测到的 popup 类型(null = 无 popup)
123
+ // session 不存在 / 非 tui / pty 已 exit 时 data=null, atSeq=0。
119
124
  "session:pty:snapshot",
120
125
  // ---- attachment.* file-sharing(详见 attachment-schemas.ts) ----
121
126
  // 命名警告:这里的 `attachment.*` RPC 与 CC v2.x 上行 `type:"attachment"` 系统行
@@ -22601,6 +22606,11 @@ var SessionManager = class {
22601
22606
  // 新 client 通过 session:subscribe 接入时由 onSubscribe hook 取 getPtyReplay(sessionId)
22602
22607
  // 把屏幕快照定向 emit,解决"刷新页面 / 第二个 tab 接入时 xterm 白屏"
22603
22608
  ptyReplaysByToolSid = /* @__PURE__ */ new Map();
22609
+ // TUI 模式:toolSessionId → @xterm/headless PopupDetector。
22610
+ // ClaudeTuiAdapter spawn 时通过 onDetectorRegister callback 写入;exit 时 onDetectorUnregister 清理。
22611
+ // manager.resizePty 用此映射把 UI 的 session:pty:resize 同步到 detector 内部 buffer,
22612
+ // 保证 snapshot serialize 时 cursor positioning 与 UI xterm 实际尺寸一致。
22613
+ detectorsByToolSid = /* @__PURE__ */ new Map();
22604
22614
  // 仅 mode='tui' 需要:index.ts 装配阶段在 observer 构造后注入。
22605
22615
  // observer.onEvent 要回灌 manager.feedObserverEvents,所以 observer 必须晚于 manager
22606
22616
  // 构造;反过来 manager 又要在 mode='tui' 下 spawn session 时 attach observer。setter 解循环
@@ -23813,7 +23823,7 @@ var SessionManager = class {
23813
23823
  * - session 存在但 pty 还没 spawn / 已 exit → null
23814
23824
  * 调用方(onSubscribe hook)按返回值非空时构造 session:pty + session:control 帧定向回放
23815
23825
  */
23816
- getPtyReplay(sessionId) {
23826
+ async getPtyReplay(sessionId) {
23817
23827
  if (this.deps.mode !== "tui") {
23818
23828
  probeEvent("replay.request", { sid: sessionId, result: "not-tui-mode" });
23819
23829
  return null;
@@ -23842,7 +23852,7 @@ var SessionManager = class {
23842
23852
  });
23843
23853
  return null;
23844
23854
  }
23845
- const snap = replay();
23855
+ const snap = await replay();
23846
23856
  const dumpFile = probeDumpReplay(sessionId, snap.data);
23847
23857
  probeEvent("replay.dump", {
23848
23858
  sid: sessionId,
@@ -23868,18 +23878,50 @@ var SessionManager = class {
23868
23878
  if (!tsid) return void 0;
23869
23879
  return this.ptyTransports.get(tsid);
23870
23880
  }
23881
+ /** ClaudeTuiAdapter spawn 时注册 detector ref;同一 tsid 重 spawn 会覆盖 */
23882
+ registerDetector(toolSessionId, detector) {
23883
+ this.detectorsByToolSid.set(toolSessionId, detector);
23884
+ }
23885
+ /** spawn 进程 exit 时清理 */
23886
+ unregisterDetector(toolSessionId) {
23887
+ this.detectorsByToolSid.delete(toolSessionId);
23888
+ }
23889
+ /**
23890
+ * 同时 resize pty 子进程和 detector buffer:UI session:pty:resize 入口走这个。
23891
+ * 顺序:pty.resize 在前,detector.resize 在后 —— pty 是真正的 CC 子进程视图,必须先成功;
23892
+ * detector 只是 daemon 端镜像,任一步失败都返 false。
23893
+ * 找不到 runner / toolSessionId / pty 任一缺失 → 返 false,handler 静默给 UI 报 ok=false 不抛错。
23894
+ */
23895
+ resizePty(sessionId, cols, rows) {
23896
+ const runner = this.runners.get(sessionId);
23897
+ if (!runner) return false;
23898
+ const tsid = runner.getState().file.toolSessionId;
23899
+ if (!tsid) return false;
23900
+ const pty = this.ptyTransports.get(tsid);
23901
+ if (!pty) return false;
23902
+ try {
23903
+ pty.resize(cols, rows);
23904
+ } catch {
23905
+ return false;
23906
+ }
23907
+ const detector = this.detectorsByToolSid.get(tsid);
23908
+ if (detector) detector.resize(cols, rows);
23909
+ return true;
23910
+ }
23871
23911
  /**
23872
23912
  * ClaudeTuiAdapter onPtyData callback:把 pty 字节流以 session:pty wire 帧广播给订阅者。
23913
+ * seq 来自 adapter spawn 路径维护的 per-session counter,与 detector.parseSeq 同源。
23914
+ * UI 端 snapshot/incremental dedup 必须靠这个 seq。
23873
23915
  * 仅 mode='tui' 时发——SDK spawn 模式根本没 pty。
23874
- * payload 永远只有 `{ sessionId, data }`,data = base64 UTF-8 字节流。
23916
+ * payload 永远只有 `{ sessionId, data, seq }`,data = base64 UTF-8 字节流。
23875
23917
  * 详见 protocol/src/events.ts session:pty JSDoc
23876
23918
  */
23877
- emitPtyData(toolSessionId, b64) {
23919
+ emitPtyChunk(toolSessionId, b64, seq) {
23878
23920
  if (this.deps.mode !== "tui") return;
23879
23921
  const sid = this.sessionIdByToolSid(toolSessionId);
23880
23922
  if (!sid) return;
23881
23923
  this.routeFromRunner(
23882
- { type: "session:pty", sessionId: sid, data: b64 },
23924
+ { type: "session:pty", sessionId: sid, data: b64, seq },
23883
23925
  "broadcast"
23884
23926
  );
23885
23927
  }
@@ -25226,6 +25268,10 @@ function createPopupDetector(opts) {
25226
25268
  let pendingClear = null;
25227
25269
  let quietTimer = null;
25228
25270
  const QUIET_MS = opts.quietMs ?? 300;
25271
+ let parseSeq = 0;
25272
+ let pendingParse = 0;
25273
+ let drainWaiters = [];
25274
+ let disposed = false;
25229
25275
  const scanScreen = () => {
25230
25276
  const buf = term.buffer.active;
25231
25277
  const lines = [];
@@ -25271,15 +25317,43 @@ function createPopupDetector(opts) {
25271
25317
  get visibleKind() {
25272
25318
  return visible;
25273
25319
  },
25274
- writeBytes(data) {
25320
+ get parseSeq() {
25321
+ return parseSeq;
25322
+ },
25323
+ writeBytes(data, seq) {
25324
+ pendingParse++;
25275
25325
  return new Promise((resolve6) => {
25276
25326
  term.write(data, () => {
25327
+ if (disposed) {
25328
+ resolve6();
25329
+ return;
25330
+ }
25331
+ if (typeof seq === "number" && seq > parseSeq) parseSeq = seq;
25332
+ pendingParse--;
25277
25333
  tick();
25334
+ if (pendingParse === 0 && drainWaiters.length > 0) {
25335
+ const waiters = drainWaiters;
25336
+ drainWaiters = [];
25337
+ for (const w2 of waiters) w2();
25338
+ }
25278
25339
  resolve6();
25279
25340
  });
25280
25341
  });
25281
25342
  },
25343
+ drain() {
25344
+ if (pendingParse === 0) return Promise.resolve();
25345
+ return new Promise((resolve6) => {
25346
+ drainWaiters.push(resolve6);
25347
+ });
25348
+ },
25349
+ resize(cols, rows) {
25350
+ try {
25351
+ term.resize(cols, rows);
25352
+ } catch {
25353
+ }
25354
+ },
25282
25355
  dispose() {
25356
+ disposed = true;
25283
25357
  if (quietTimer) {
25284
25358
  clearTimeout(quietTimer);
25285
25359
  quietTimer = null;
@@ -25288,6 +25362,9 @@ function createPopupDetector(opts) {
25288
25362
  clearTimeout(pendingClear);
25289
25363
  pendingClear = null;
25290
25364
  }
25365
+ const waiters = drainWaiters;
25366
+ drainWaiters = [];
25367
+ for (const w2 of waiters) w2();
25291
25368
  serializeAddon.dispose();
25292
25369
  term.dispose();
25293
25370
  },
@@ -25353,23 +25430,52 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
25353
25430
  this.tuiOpts.onPopupTransition(ctx.toolSessionId, kind, visible);
25354
25431
  }
25355
25432
  });
25433
+ if (ctx.toolSessionId && this.tuiOpts.onDetectorRegister) {
25434
+ this.tuiOpts.onDetectorRegister(ctx.toolSessionId, detector);
25435
+ }
25436
+ let chunkSeq = 0;
25356
25437
  if (ctx.toolSessionId && this.tuiOpts.onPtyReplayRegister) {
25357
- this.tuiOpts.onPtyReplayRegister(ctx.toolSessionId, () => ({
25358
- data: detector.serialize(),
25359
- popupKind: detector.visibleKind
25360
- }));
25438
+ this.tuiOpts.onPtyReplayRegister(ctx.toolSessionId, async () => {
25439
+ await detector.drain();
25440
+ return {
25441
+ data: detector.serialize(),
25442
+ popupKind: detector.visibleKind,
25443
+ atSeq: detector.parseSeq
25444
+ };
25445
+ });
25361
25446
  }
25362
- ptyChild.stdout.on("data", (data) => {
25447
+ let coalesceBuf = [];
25448
+ let coalesceScheduled = false;
25449
+ const flushCoalesced = () => {
25450
+ coalesceScheduled = false;
25451
+ if (coalesceBuf.length === 0) return;
25452
+ const merged = Buffer.concat(coalesceBuf);
25453
+ coalesceBuf = [];
25454
+ chunkSeq++;
25455
+ const seq = chunkSeq;
25363
25456
  if (ctx.toolSessionId && this.tuiOpts.onPtyData) {
25364
- this.tuiOpts.onPtyData(ctx.toolSessionId, data.toString("base64"));
25457
+ this.tuiOpts.onPtyData(ctx.toolSessionId, merged.toString("base64"), seq);
25365
25458
  }
25366
- void detector.writeBytes(data.toString("utf8"));
25459
+ void detector.writeBytes(merged.toString("utf8"), seq);
25367
25460
  if (ctx.toolSessionId) {
25368
- probePtyDataThrottled(ctx.toolSessionId, data.length, data);
25461
+ probePtyDataThrottled(ctx.toolSessionId, merged.length, merged);
25462
+ }
25463
+ };
25464
+ ptyChild.stdout.on("data", (data) => {
25465
+ coalesceBuf.push(data);
25466
+ if (!coalesceScheduled) {
25467
+ coalesceScheduled = true;
25468
+ queueMicrotask(flushCoalesced);
25369
25469
  }
25370
25470
  });
25371
25471
  ptyChild.on("exit", () => {
25372
25472
  probeEvent("pty.exit", { tsid: ctx.toolSessionId });
25473
+ if (coalesceBuf.length > 0) {
25474
+ flushCoalesced();
25475
+ }
25476
+ if (ctx.toolSessionId && this.tuiOpts.onDetectorUnregister) {
25477
+ this.tuiOpts.onDetectorUnregister(ctx.toolSessionId);
25478
+ }
25373
25479
  if (ctx.toolSessionId && this.tuiOpts.onPtyUnregister) {
25374
25480
  this.tuiOpts.onPtyUnregister(ctx.toolSessionId);
25375
25481
  }
@@ -29166,14 +29272,8 @@ function buildSessionHandlers(deps) {
29166
29272
  return { response: { type: "session:pty:resize", ok: false } };
29167
29273
  }
29168
29274
  ensureSessionAccess(ctx, sid, "send");
29169
- const pty = manager.getPtyBySessionId(sid);
29170
- if (!pty) return { response: { type: "session:pty:resize", ok: false } };
29171
- try {
29172
- pty.resize(cols, rows);
29173
- } catch {
29174
- return { response: { type: "session:pty:resize", ok: false } };
29175
- }
29176
- return { response: { type: "session:pty:resize", ok: true } };
29275
+ const ok = manager.resizePty(sid, cols, rows);
29276
+ return { response: { type: "session:pty:resize", ok } };
29177
29277
  };
29178
29278
  const ptySnapshot = async (frame, _client, ctx) => {
29179
29279
  const sid = frame.sessionId;
@@ -29181,13 +29281,14 @@ function buildSessionHandlers(deps) {
29181
29281
  return { response: { type: "session:pty:snapshot", ok: false } };
29182
29282
  }
29183
29283
  ensureSessionAccess(ctx, sid, "read");
29184
- const snap = manager.getPtyReplay(sid);
29284
+ const snap = await manager.getPtyReplay(sid);
29185
29285
  if (!snap) {
29186
29286
  return {
29187
29287
  response: {
29188
29288
  type: "session:pty:snapshot",
29189
29289
  ok: true,
29190
29290
  data: null,
29291
+ atSeq: 0,
29191
29292
  popupKind: null
29192
29293
  }
29193
29294
  };
@@ -29197,6 +29298,7 @@ function buildSessionHandlers(deps) {
29197
29298
  type: "session:pty:snapshot",
29198
29299
  ok: true,
29199
29300
  data: Buffer.from(snap.data, "utf8").toString("base64"),
29301
+ atSeq: snap.atSeq,
29200
29302
  popupKind: snap.popupKind
29201
29303
  }
29202
29304
  };
@@ -30595,9 +30697,11 @@ async function startDaemon(config) {
30595
30697
  historyReader: new ClaudeHistoryReader(),
30596
30698
  onPtyRegister: (tsid, pty) => manager.registerPty(tsid, pty),
30597
30699
  onPtyUnregister: (tsid) => manager.unregisterPty(tsid),
30598
- onPtyData: (tsid, b64) => manager.emitPtyData(tsid, b64),
30700
+ onPtyData: (tsid, b64, seq) => manager.emitPtyChunk(tsid, b64, seq),
30599
30701
  onPopupTransition: (tsid, kind, visible) => manager.emitPopupTransition(tsid, kind, visible),
30600
- onPtyReplayRegister: (tsid, replay) => manager.registerPtyReplay(tsid, replay)
30702
+ onPtyReplayRegister: (tsid, replay) => manager.registerPtyReplay(tsid, replay),
30703
+ onDetectorRegister: (tsid, detector) => manager.registerDetector(tsid, detector),
30704
+ onDetectorUnregister: (tsid) => manager.unregisterDetector(tsid)
30601
30705
  }) : new ClaudeAdapter({ logger, historyReader: new ClaudeHistoryReader() });
30602
30706
  registerAdapter("claude", claudeAdapter);
30603
30707
  const personaRegistry = new PersonaRegistry(personaStore);
@@ -30763,16 +30867,8 @@ async function startDaemon(config) {
30763
30867
  questions: Array.isArray(questions) ? questions : []
30764
30868
  });
30765
30869
  }
30766
- const replay = manager.getPtyReplay(sessionId);
30767
- if (replay) {
30768
- if (replay.data) {
30769
- client.send({
30770
- type: "session:pty",
30771
- sessionId,
30772
- data: Buffer.from(replay.data, "utf8").toString("base64")
30773
- });
30774
- }
30775
- if (replay.popupKind) {
30870
+ void manager.getPtyReplay(sessionId).then((replay) => {
30871
+ if (replay && replay.popupKind) {
30776
30872
  client.send({
30777
30873
  type: "session:control",
30778
30874
  sessionId,
@@ -30780,7 +30876,8 @@ async function startDaemon(config) {
30780
30876
  popupKind: replay.popupKind
30781
30877
  });
30782
30878
  }
30783
- }
30879
+ }).catch(() => {
30880
+ });
30784
30881
  }
30785
30882
  });
30786
30883
  transport = wsServer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.93-beta.174.78b779b",
3
+ "version": "0.2.93",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",