@clawos-dev/clawd 0.2.224-beta.437.7f84075 → 0.2.225

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
@@ -11379,6 +11379,45 @@ function hashDirToCwd(hash) {
11379
11379
  const body = hash.startsWith("-") ? hash.slice(1) : hash;
11380
11380
  return "/" + body.replace(/-/g, "/");
11381
11381
  }
11382
+ function findTranscriptByToolSessionId(projectsRoot, toolSessionId) {
11383
+ let entries;
11384
+ try {
11385
+ entries = import_node_fs5.default.readdirSync(projectsRoot, { withFileTypes: true });
11386
+ } catch {
11387
+ return null;
11388
+ }
11389
+ let best = null;
11390
+ for (const ent of entries) {
11391
+ if (!ent.isDirectory()) continue;
11392
+ const candidate = import_node_path5.default.join(projectsRoot, ent.name, `${toolSessionId}.jsonl`);
11393
+ try {
11394
+ const st = import_node_fs5.default.statSync(candidate);
11395
+ if (!best || st.mtimeMs > best.mtime) best = { p: candidate, mtime: st.mtimeMs };
11396
+ } catch {
11397
+ }
11398
+ }
11399
+ return best?.p ?? null;
11400
+ }
11401
+ function deriveCwdFromTranscript(filePath) {
11402
+ const dirName = import_node_path5.default.basename(import_node_path5.default.dirname(filePath));
11403
+ let raw;
11404
+ try {
11405
+ raw = import_node_fs5.default.readFileSync(filePath, "utf8");
11406
+ } catch {
11407
+ return null;
11408
+ }
11409
+ let hit = null;
11410
+ for (const line of raw.split("\n")) {
11411
+ const t = line.trim();
11412
+ if (!t) continue;
11413
+ try {
11414
+ const o = JSON.parse(t);
11415
+ if (typeof o.cwd === "string" && cwdToHashDir(o.cwd) === dirName) hit = o.cwd;
11416
+ } catch {
11417
+ }
11418
+ }
11419
+ return hit;
11420
+ }
11382
11421
  function safeStatMtime(p2) {
11383
11422
  try {
11384
11423
  return import_node_fs5.default.statSync(p2).mtimeMs;
@@ -42357,6 +42396,13 @@ function applyCommand(state, command, deps) {
42357
42396
  }
42358
42397
  return { state: next, effects };
42359
42398
  }
42399
+ case "sync-cwd": {
42400
+ if (next.file.cwd === command.cwd) return { state: next, effects };
42401
+ next.file = { ...next.file, cwd: command.cwd, updatedAt: nowIso(deps) };
42402
+ effects.push({ kind: "persist-file", file: next.file });
42403
+ effects.push(sessionInfoFrame(next.file));
42404
+ return { state: next, effects };
42405
+ }
42360
42406
  case "resume": {
42361
42407
  const sameToolSession = next.file.toolSessionId === command.toolSessionId;
42362
42408
  const nextFile = {
@@ -43955,6 +44001,23 @@ var SessionManager = class {
43955
44001
  this.writeOwned(updated);
43956
44002
  return { response: updated, broadcast: [] };
43957
44003
  }
44004
+ /**
44005
+ * [T-17] observer 检测到 CC 挪走 transcript(cwd 中途变化)后的 cwd 校正入口。
44006
+ * 不进 withCollector:observer 回调没有 in-flight RPC,session:info 帧走
44007
+ * routeFromRunner 的异步 broadcast 路径。有 runner 走 reducer sync-cwd(无 kill,
44008
+ * 区别于 update 的 RUNTIME_PATCH_KEYS 重启语义);无 runner 直写 store。
44009
+ * 幂等:reducer 同值 noop;session 不存在静默返回(observer 生命周期与 store 解耦)。
44010
+ */
44011
+ syncSessionCwd(sessionId, cwd) {
44012
+ const runner = this.runners.get(sessionId);
44013
+ if (runner) {
44014
+ runner.input({ kind: "command", command: { kind: "sync-cwd", cwd } });
44015
+ return;
44016
+ }
44017
+ const existing = this.findOwnedSession(sessionId);
44018
+ if (!existing || existing.cwd === cwd) return;
44019
+ this.writeOwned({ ...existing, cwd, updatedAt: nowIso2(this.deps) });
44020
+ }
43958
44021
  /**
43959
44022
  * 启动清扫:删除所有磁盘残留的 ephemeral(快速问答临时)session。
43960
44023
  * 客户端崩溃 / 没关抽屉会留下永久不可见的孤儿(ephemeral 被 sidebar 过滤,用户自己也删不掉)。
@@ -49640,14 +49703,19 @@ var SessionObserver = class {
49640
49703
  lastSize: size,
49641
49704
  buf: "",
49642
49705
  adapter: args.adapter,
49643
- prevIsRejectSentinel: false
49706
+ prevIsRejectSentinel: false,
49707
+ toolSessionId: args.toolSessionId,
49708
+ sawFile: import_node_fs25.default.existsSync(filePath),
49709
+ lastRelocateScanMs: 0,
49710
+ pendingCwdDerive: false,
49711
+ lastDeriveMs: 0
49644
49712
  };
49645
49713
  try {
49646
49714
  import_node_fs25.default.mkdirSync(import_node_path24.default.dirname(filePath), { recursive: true });
49647
49715
  } catch {
49648
49716
  }
49649
49717
  w2.watcher = import_node_fs25.default.watch(import_node_path24.default.dirname(filePath), { persistent: false }, (_event, changedName) => {
49650
- if (!changedName || !filePath.endsWith(changedName)) return;
49718
+ if (!changedName || !w2.filePath.endsWith(changedName)) return;
49651
49719
  this.poll(w2);
49652
49720
  });
49653
49721
  w2.pollTimer = setInterval(() => this.poll(w2), 200);
@@ -49695,8 +49763,10 @@ var SessionObserver = class {
49695
49763
  try {
49696
49764
  size = import_node_fs25.default.statSync(w2.filePath).size;
49697
49765
  } catch {
49766
+ if (w2.sawFile) this.maybeRelocate(w2);
49698
49767
  return;
49699
49768
  }
49769
+ w2.sawFile = true;
49700
49770
  if (size < w2.lastSize) {
49701
49771
  w2.lastSize = 0;
49702
49772
  w2.buf = "";
@@ -49731,6 +49801,57 @@ var SessionObserver = class {
49731
49801
  } finally {
49732
49802
  import_node_fs25.default.closeSync(fd);
49733
49803
  }
49804
+ if (w2.pendingCwdDerive) this.maybeDeriveCwd(w2);
49805
+ }
49806
+ // [T-17] 文件消失后的兜底重定位:按 tsid 全局扫描 → 切 watcher → lastSize/buf 保留接续读
49807
+ // (CC 是 move,内容前缀不变,不重复喂;异常变小走 poll 的截断重建分支,上游 uuid dedup 兜底)。
49808
+ // 检测点唯一(poll ENOENT),fs.watch rename 事件也 funnel 到 poll,天然幂等;scan 加 1s
49809
+ // rate-limit 防 ENOENT 持续期(如 cleanupPeriodDays 真删除)反复 readdir 空转。
49810
+ maybeRelocate(w2) {
49811
+ const now = Date.now();
49812
+ if (now - w2.lastRelocateScanMs < 1e3) return;
49813
+ w2.lastRelocateScanMs = now;
49814
+ const projectsRoot = import_node_path24.default.join(this.home, ".claude", "projects");
49815
+ const found = findTranscriptByToolSessionId(projectsRoot, w2.toolSessionId);
49816
+ if (!found || found === w2.filePath) return;
49817
+ try {
49818
+ w2.watcher?.close();
49819
+ } catch {
49820
+ }
49821
+ w2.filePath = found;
49822
+ try {
49823
+ w2.watcher = import_node_fs25.default.watch(import_node_path24.default.dirname(found), { persistent: false }, (_event, changedName) => {
49824
+ if (!changedName || !w2.filePath.endsWith(changedName)) return;
49825
+ this.poll(w2);
49826
+ });
49827
+ } catch {
49828
+ w2.watcher = null;
49829
+ }
49830
+ const cwd = deriveCwdFromTranscript(found);
49831
+ if (cwd) {
49832
+ this.applyDerivedCwd(w2, cwd);
49833
+ } else {
49834
+ w2.pendingCwdDerive = true;
49835
+ }
49836
+ this.opts.onJsonlRelocated?.(w2.sessionId, { filePath: found, cwd });
49837
+ this.poll(w2);
49838
+ }
49839
+ maybeDeriveCwd(w2) {
49840
+ const now = Date.now();
49841
+ if (now - w2.lastDeriveMs < 500) return;
49842
+ w2.lastDeriveMs = now;
49843
+ const cwd = deriveCwdFromTranscript(w2.filePath);
49844
+ if (!cwd) return;
49845
+ w2.pendingCwdDerive = false;
49846
+ this.applyDerivedCwd(w2, cwd);
49847
+ this.opts.onJsonlRelocated?.(w2.sessionId, { filePath: w2.filePath, cwd });
49848
+ }
49849
+ applyDerivedCwd(w2, cwd) {
49850
+ this.metaObserver?.start({
49851
+ sessionId: w2.sessionId,
49852
+ cwd,
49853
+ toolSessionId: w2.toolSessionId
49854
+ });
49734
49855
  }
49735
49856
  // 解析 JSONL 单行:仅当是主链 user 文本行(非 sidechain / 非 sub-agent / message.role='user'
49736
49857
  // 且 content 含 text part)时,抽出 real uuid + 第一段 text + timestamp 上报。
@@ -54681,10 +54802,18 @@ function readJsonlEntries(file) {
54681
54802
  }
54682
54803
  function forkSession(input) {
54683
54804
  const baseDir = input.baseDir ?? import_node_path47.default.join(import_node_os16.default.homedir(), ".claude");
54684
- const projectDir = import_node_path47.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
54685
- const sourceFile = import_node_path47.default.join(projectDir, `${input.toolSessionId}.jsonl`);
54805
+ let projectDir = import_node_path47.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
54806
+ let sourceFile = import_node_path47.default.join(projectDir, `${input.toolSessionId}.jsonl`);
54686
54807
  if (!import_node_fs45.default.existsSync(sourceFile)) {
54687
- throw new Error(`fork: source transcript not found: ${sourceFile}`);
54808
+ const found = findTranscriptByToolSessionId(
54809
+ import_node_path47.default.join(baseDir, "projects"),
54810
+ input.toolSessionId
54811
+ );
54812
+ if (!found) {
54813
+ throw new Error(`fork: source transcript not found: ${sourceFile}`);
54814
+ }
54815
+ sourceFile = found;
54816
+ projectDir = import_node_path47.default.dirname(found);
54688
54817
  }
54689
54818
  const entries = readJsonlEntries(sourceFile);
54690
54819
  const mainEntries = entries.filter((e) => e.isSidechain !== true);
@@ -59686,6 +59815,13 @@ async function startDaemon(config) {
59686
59815
  // daemon 内部转译,UI 看到的始终是 events 流里的 synth uuid(详见 manager 注释)
59687
59816
  onUserMessageObserved: (sessionId, realUuid, text) => {
59688
59817
  manager.recordRealUserUuid({ sessionId, realUuid, text });
59818
+ },
59819
+ // [T-17] CC 中途改 cwd 挪走 transcript:observer 重定位后校正 SessionFile.cwd
59820
+ // (persist + session:info,不 kill),fork / respawn / UI 缓存随之对齐。
59821
+ // cwd 可能两段式到达(挪走瞬间新 cwd 行未落盘 → 先 null 后补),回调幂等
59822
+ onJsonlRelocated: (sessionId, { filePath, cwd }) => {
59823
+ logger.info("observer jsonl relocated", { sessionId, filePath, cwd });
59824
+ if (cwd) manager.syncSessionCwd(sessionId, cwd);
59689
59825
  }
59690
59826
  });
59691
59827
  manager.attachObserver(observer);