dsh-rewind-plugin 0.21.0 → 0.21.1

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/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
3
- import { copyFile as copyFile2, rm as rm2, stat as stat2, unlink } from "node:fs/promises";
3
+ import { copyFile as copyFile3, rm as rm2, stat as stat3, unlink } from "node:fs/promises";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
 
6
6
  // src/locales.ts
@@ -490,21 +490,20 @@ function planRewind(events, surface, target) {
490
490
  }
491
491
 
492
492
  // src/snapshot.ts
493
- import { createHash } from "node:crypto";
494
- import { chmod, copyFile, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
495
- import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
493
+ import { chmod, copyFile as copyFile2, lstat as lstat2, mkdir, readFile as readFile3, readdir as readdir2, rename as rename2, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
494
+ import { basename as basename2, dirname as dirname3, join as join3 } from "node:path";
496
495
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
497
- var SNAPSHOT_DIR_NAME = "rewind-snapshots";
496
+
497
+ // src/snapshot-format.ts
498
+ import { createHash } from "node:crypto";
499
+ import { readFile } from "node:fs/promises";
500
+ import { basename, dirname, join, relative, sep } from "node:path";
498
501
  var SIDECAR_SUFFIX = ".before";
499
502
  var PENDING_DIR = ".pending";
500
503
  var RESCUE_DIR = "rescue";
501
504
  var JOURNAL_PREFIX = "journal-";
502
505
  var LEGACY_JOURNAL_PREFIX = "restore-journal-";
503
- var PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
504
- var COMPARE_CHUNK_BYTES = 64 * 1024;
505
506
  var REPLACEMENT_CHAR = "\uFFFD";
506
- var DEFAULT_MAX_FILE_BYTES = 8 * 1024 * 1024;
507
- var MAX_ANCHOR_GROUPS = 100;
508
507
  var CURRENT_STORE_VERSION = 2;
509
508
  var UnknownStoreVersionError = class extends Error {
510
509
  constructor(version, source) {
@@ -522,84 +521,6 @@ function textSourceOf(text) {
522
521
  function isLinkEntry(entry) {
523
522
  return "ref" in entry;
524
523
  }
525
- async function sameFileBytes(aPath, bPath) {
526
- const sizes = await Promise.all([stat(aPath), stat(bPath)]);
527
- if (sizes[0].size !== sizes[1].size) return false;
528
- const [a, b] = await Promise.all([open(aPath, "r"), open(bPath, "r")]);
529
- try {
530
- const aChunk = Buffer.allocUnsafe(COMPARE_CHUNK_BYTES);
531
- const bChunk = Buffer.allocUnsafe(COMPARE_CHUNK_BYTES);
532
- for (; ; ) {
533
- const [ra, rb] = await Promise.all([
534
- a.read(aChunk, 0, COMPARE_CHUNK_BYTES, null),
535
- b.read(bChunk, 0, COMPARE_CHUNK_BYTES, null)
536
- ]);
537
- if (ra.bytesRead !== rb.bytesRead) return false;
538
- if (ra.bytesRead === 0) return true;
539
- if (!aChunk.subarray(0, ra.bytesRead).equals(bChunk.subarray(0, rb.bytesRead))) return false;
540
- }
541
- } finally {
542
- await Promise.all([a.close(), b.close()]);
543
- }
544
- }
545
- async function sameFileBuffer(path, bytes) {
546
- const st = await stat(path);
547
- if (st.size !== bytes.length) return false;
548
- const handle = await open(path, "r");
549
- try {
550
- const chunk = Buffer.allocUnsafe(COMPARE_CHUNK_BYTES);
551
- let offset = 0;
552
- for (; ; ) {
553
- const read = await handle.read(chunk, 0, Math.min(COMPARE_CHUNK_BYTES, bytes.length - offset), offset);
554
- if (read.bytesRead === 0) return offset === bytes.length;
555
- if (!chunk.subarray(0, read.bytesRead).equals(bytes.subarray(offset, offset + read.bytesRead))) return false;
556
- offset += read.bytesRead;
557
- }
558
- } finally {
559
- await handle.close();
560
- }
561
- }
562
- function isEnoent(error) {
563
- return error?.code === "ENOENT";
564
- }
565
- var defaultProbe = {
566
- async matches(source, path) {
567
- try {
568
- if (source === null) {
569
- await stat(path);
570
- return false;
571
- }
572
- if (source.kind === "blob") return await sameFileBytes(path, source.path);
573
- if (source.kind === "text") return await sameFileBuffer(path, source.bytes);
574
- return (await readFile(path)).toString("utf8") === source.text;
575
- } catch (error) {
576
- if (isEnoent(error)) return source === null ? true : false;
577
- return void 0;
578
- }
579
- },
580
- async copy(path, dest) {
581
- try {
582
- await copyFile(path, dest);
583
- const st = await stat(dest);
584
- return { kind: "copied", size: st.size };
585
- } catch (error) {
586
- if (isEnoent(error)) {
587
- const source = await stat(path).catch(() => void 0);
588
- if (source === void 0) return { kind: "absent" };
589
- }
590
- return { kind: "failed", message: error instanceof Error ? error.message : String(error) };
591
- }
592
- },
593
- async size(path) {
594
- try {
595
- const st = await stat(path);
596
- return st.isFile() ? st.size : void 0;
597
- } catch {
598
- return void 0;
599
- }
600
- },
601
- isLink: isLinkPath
602
- };
603
524
  function safeFileId(callId) {
604
525
  return callId.replace(/[^a-zA-Z0-9._-]/g, "_");
605
526
  }
@@ -607,12 +528,6 @@ function safeSessionId(sessionId) {
607
528
  const safe = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
608
529
  return safe === ".." || safe === "." ? "session" : safe;
609
530
  }
610
- async function writeJsonAtomic(file, data, afterTempWrite) {
611
- const tmp = `${file}.tmp`;
612
- await writeFile(tmp, JSON.stringify(data), "utf8");
613
- afterTempWrite?.();
614
- await rename(tmp, file);
615
- }
616
531
  var RESTORE_JOURNAL_STATES = /* @__PURE__ */ new Set(["running", "rollback-running", "completed", "rolled-back", "recovery-required"]);
617
532
  function isRawJournalRef(value) {
618
533
  if (value === null) return true;
@@ -814,6 +729,109 @@ async function readEntry(file, anchorSeq) {
814
729
  if (parsed.before === null) return { ...base, before: null, size: 0 };
815
730
  return { ...base, before: textSourceOf(parsed.before), size: Buffer.byteLength(parsed.before, "utf8") };
816
731
  }
732
+ function isSafeLinkRef(ref) {
733
+ return /^[0-9]+\/[a-zA-Z0-9._-]+\.json$/.test(ref);
734
+ }
735
+ function isSafeBackupRef(ref) {
736
+ if (ref.length === 0 || ref.startsWith("/") || ref.includes("\\")) return false;
737
+ const segments = ref.split("/");
738
+ if (segments.length !== 2 && segments.length !== 3) return false;
739
+ if (segments.length === 3 && segments[0] !== RESCUE_DIR) return false;
740
+ return segments.every((segment) => segment !== "." && segment !== ".." && /^[a-zA-Z0-9._-]+$/.test(segment));
741
+ }
742
+
743
+ // src/snapshot-fs.ts
744
+ import { copyFile, lstat, open, readFile as readFile2, readdir, realpath, rename, stat, writeFile } from "node:fs/promises";
745
+ import { dirname as dirname2, isAbsolute, join as join2, relative as relative2, sep as sep2 } from "node:path";
746
+ var COMPARE_CHUNK_BYTES = 64 * 1024;
747
+ async function sameFileBytes(aPath, bPath) {
748
+ const sizes = await Promise.all([stat(aPath), stat(bPath)]);
749
+ if (sizes[0].size !== sizes[1].size) return false;
750
+ const [a, b] = await Promise.all([open(aPath, "r"), open(bPath, "r")]);
751
+ try {
752
+ const aChunk = Buffer.allocUnsafe(COMPARE_CHUNK_BYTES);
753
+ const bChunk = Buffer.allocUnsafe(COMPARE_CHUNK_BYTES);
754
+ for (; ; ) {
755
+ const [ra, rb] = await Promise.all([
756
+ a.read(aChunk, 0, COMPARE_CHUNK_BYTES, null),
757
+ b.read(bChunk, 0, COMPARE_CHUNK_BYTES, null)
758
+ ]);
759
+ if (ra.bytesRead !== rb.bytesRead) return false;
760
+ if (ra.bytesRead === 0) return true;
761
+ if (!aChunk.subarray(0, ra.bytesRead).equals(bChunk.subarray(0, rb.bytesRead))) return false;
762
+ }
763
+ } finally {
764
+ await Promise.all([a.close(), b.close()]);
765
+ }
766
+ }
767
+ async function sameFileBuffer(path, bytes) {
768
+ const st = await stat(path);
769
+ if (st.size !== bytes.length) return false;
770
+ const handle = await open(path, "r");
771
+ try {
772
+ const chunk = Buffer.allocUnsafe(COMPARE_CHUNK_BYTES);
773
+ let offset = 0;
774
+ for (; ; ) {
775
+ const read = await handle.read(chunk, 0, Math.min(COMPARE_CHUNK_BYTES, bytes.length - offset), offset);
776
+ if (read.bytesRead === 0) return offset === bytes.length;
777
+ if (!chunk.subarray(0, read.bytesRead).equals(bytes.subarray(offset, offset + read.bytesRead))) return false;
778
+ offset += read.bytesRead;
779
+ }
780
+ } finally {
781
+ await handle.close();
782
+ }
783
+ }
784
+ function isEnoent(error) {
785
+ return error?.code === "ENOENT";
786
+ }
787
+ var defaultProbe = {
788
+ async matches(source, path) {
789
+ try {
790
+ if (source === null) {
791
+ await stat(path);
792
+ return false;
793
+ }
794
+ if (source.kind === "blob") return await sameFileBytes(path, source.path);
795
+ if (source.kind === "text") return await sameFileBuffer(path, source.bytes);
796
+ return (await readFile2(path)).toString("utf8") === source.text;
797
+ } catch (error) {
798
+ if (isEnoent(error)) return source === null ? true : false;
799
+ return void 0;
800
+ }
801
+ },
802
+ async copy(path, dest) {
803
+ try {
804
+ const live = await lstat(path).catch(() => void 0);
805
+ if (live !== void 0 && !live.isFile() && !live.isSymbolicLink()) {
806
+ return { kind: "failed", message: `not a regular file: ${path}` };
807
+ }
808
+ await copyFile(path, dest);
809
+ const st = await stat(dest);
810
+ return { kind: "copied", size: st.size };
811
+ } catch (error) {
812
+ if (isEnoent(error)) {
813
+ const source = await stat(path).catch(() => void 0);
814
+ if (source === void 0) return { kind: "absent" };
815
+ }
816
+ return { kind: "failed", message: error instanceof Error ? error.message : String(error) };
817
+ }
818
+ },
819
+ async size(path) {
820
+ try {
821
+ const st = await stat(path);
822
+ return st.isFile() ? st.size : void 0;
823
+ } catch {
824
+ return void 0;
825
+ }
826
+ },
827
+ isUnsafeTarget
828
+ };
829
+ async function writeJsonAtomic(file, data, afterTempWrite) {
830
+ const tmp = `${file}.tmp`;
831
+ await writeFile(tmp, JSON.stringify(data), "utf8");
832
+ afterTempWrite?.();
833
+ await rename(tmp, file);
834
+ }
817
835
  async function dirBytes(dir) {
818
836
  let names;
819
837
  try {
@@ -823,7 +841,7 @@ async function dirBytes(dir) {
823
841
  }
824
842
  let total = 0;
825
843
  for (const name2 of names) {
826
- const full = join(dir, name2);
844
+ const full = join2(dir, name2);
827
845
  const st = await lstat(full).catch(() => void 0);
828
846
  if (st === void 0) continue;
829
847
  if (st.isDirectory()) total += await dirBytes(full);
@@ -831,10 +849,10 @@ async function dirBytes(dir) {
831
849
  }
832
850
  return total;
833
851
  }
834
- async function isLinkPath(path) {
852
+ async function isUnsafeTarget(path) {
835
853
  try {
836
- const stat3 = await lstat(path);
837
- return stat3.isSymbolicLink() || stat3.nlink > 1;
854
+ const st = await lstat(path);
855
+ return !st.isFile() || st.nlink > 1;
838
856
  } catch {
839
857
  return false;
840
858
  }
@@ -844,14 +862,29 @@ async function nearestExistingAncestor(dir) {
844
862
  for (; ; ) {
845
863
  const st = await lstat(current).catch(() => void 0);
846
864
  if (st !== void 0) return current;
847
- const parent = dirname(current);
865
+ const parent = dirname2(current);
848
866
  if (parent === current) return void 0;
849
867
  current = parent;
850
868
  }
851
869
  }
870
+ async function locationPin(path) {
871
+ const dir = dirname2(path);
872
+ try {
873
+ return await realpath(dir);
874
+ } catch (error) {
875
+ if (!isEnoent(error)) return void 0;
876
+ }
877
+ const ancestor = await nearestExistingAncestor(dir);
878
+ if (ancestor === void 0) return void 0;
879
+ try {
880
+ return join2(await realpath(ancestor), relative2(ancestor, dir));
881
+ } catch {
882
+ return void 0;
883
+ }
884
+ }
852
885
  async function parentStillMatches(path, recorded) {
853
886
  if (recorded === void 0) return true;
854
- const dir = dirname(path);
887
+ const dir = dirname2(path);
855
888
  try {
856
889
  return await realpath(dir) === recorded;
857
890
  } catch (error) {
@@ -861,23 +894,13 @@ async function parentStillMatches(path, recorded) {
861
894
  if (ancestor === void 0) return false;
862
895
  try {
863
896
  const real = await realpath(ancestor);
864
- const inside = relative(real, recorded);
865
- const escapes = inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
897
+ const inside = relative2(real, recorded);
898
+ const escapes = inside === ".." || inside.startsWith(`..${sep2}`) || isAbsolute(inside);
866
899
  return inside !== "" && !escapes;
867
900
  } catch {
868
901
  return false;
869
902
  }
870
903
  }
871
- function isSafeLinkRef(ref) {
872
- return /^[0-9]+\/[a-zA-Z0-9._-]+\.json$/.test(ref);
873
- }
874
- function isSafeBackupRef(ref) {
875
- if (ref.length === 0 || ref.startsWith("/") || ref.includes("\\")) return false;
876
- const segments = ref.split("/");
877
- if (segments.length !== 2 && segments.length !== 3) return false;
878
- if (segments.length === 3 && segments[0] !== RESCUE_DIR) return false;
879
- return segments.every((segment) => segment !== "." && segment !== ".." && /^[a-zA-Z0-9._-]+$/.test(segment));
880
- }
881
904
  async function dirSizeAndLastActive(dir) {
882
905
  let size = 0;
883
906
  let lastActiveMs = 0;
@@ -901,12 +924,18 @@ async function dirSizeAndLastActive(dir) {
901
924
  }
902
925
  for (const name2 of names) {
903
926
  if (name2.startsWith(".") && name2 !== PENDING_DIR) continue;
904
- await visit(join(current, name2));
927
+ await visit(join2(current, name2));
905
928
  }
906
929
  };
907
930
  await visit(dir);
908
931
  return { size, lastActiveMs };
909
932
  }
933
+
934
+ // src/snapshot.ts
935
+ var SNAPSHOT_DIR_NAME = "rewind-snapshots";
936
+ var PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
937
+ var DEFAULT_MAX_FILE_BYTES = 8 * 1024 * 1024;
938
+ var MAX_ANCHOR_GROUPS = 100;
910
939
  var SnapshotStore = class _SnapshotStore {
911
940
  /** Debounce window for the per-commit prune (keeps the readdir+sort off the hot path). */
912
941
  static PRUNE_INTERVAL_MS = 1e3;
@@ -967,11 +996,11 @@ var SnapshotStore = class _SnapshotStore {
967
996
  this.dedup = opts?.dedup ?? true;
968
997
  this.maxFileBytes = opts?.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
969
998
  this.maxAnchorGroups = opts?.maxAnchorGroups ?? MAX_ANCHOR_GROUPS;
970
- this.root = root ?? join(resolveDshHome(opts?.dshHome), SNAPSHOT_DIR_NAME);
999
+ this.root = root ?? join3(resolveDshHome(opts?.dshHome), SNAPSHOT_DIR_NAME);
971
1000
  }
972
1001
  /** Absolute path of one session's snapshot directory (id sanitized). */
973
1002
  sessionDir(sessionId) {
974
- return join(this.root, safeSessionId(sessionId));
1003
+ return join3(this.root, safeSessionId(sessionId));
975
1004
  }
976
1005
  /**
977
1006
  * True when `size` bytes is over the per-file cap, so it must not be backed
@@ -987,7 +1016,7 @@ var SnapshotStore = class _SnapshotStore {
987
1016
  }
988
1017
  /** Absolute path of one anchor group directory. */
989
1018
  anchorDir(sessionId, anchorSeq) {
990
- return join(this.sessionDir(sessionId), String(anchorSeq));
1019
+ return join3(this.sessionDir(sessionId), String(anchorSeq));
991
1020
  }
992
1021
  /** Absolute file ref (relative to the session dir) of an entry. */
993
1022
  entryRefOf(sessionId, callId, anchorSeq) {
@@ -1036,9 +1065,9 @@ var SnapshotStore = class _SnapshotStore {
1036
1065
  * never committed is either unlinked by its caller or collected by `prune`.
1037
1066
  */
1038
1067
  async stageCapture(sessionId, key) {
1039
- const dir = join(this.sessionDir(sessionId), PENDING_DIR);
1068
+ const dir = join3(this.sessionDir(sessionId), PENDING_DIR);
1040
1069
  await mkdir(dir, { recursive: true });
1041
- return join(dir, `${safeFileId(key)}-${shortHash(key)}${SIDECAR_SUFFIX}`);
1070
+ return join3(dir, `${safeFileId(key)}-${shortHash(key)}${SIDECAR_SUFFIX}`);
1042
1071
  }
1043
1072
  /**
1044
1073
  * Seed a session's dedup state from the existing (bounded) on-disk window:
@@ -1073,7 +1102,7 @@ var SnapshotStore = class _SnapshotStore {
1073
1102
  if (seen.has(key)) throw new Error(`link cycle at ${entry.path} (${key})`);
1074
1103
  seen.add(key);
1075
1104
  if (!isSafeLinkRef(entry.ref)) throw new Error(`unsafe link ref ${entry.ref} for ${entry.path}`);
1076
- const referenced = await readEntry(join(this.sessionDir(sessionId), entry.ref), refAnchorOf(entry.ref));
1105
+ const referenced = await readEntry(join3(this.sessionDir(sessionId), entry.ref), refAnchorOf(entry.ref));
1077
1106
  if (referenced === void 0) throw new Error(`dangling link ${entry.ref} for ${entry.path}`);
1078
1107
  return this.resolveBefore(sessionId, referenced, seen);
1079
1108
  }
@@ -1087,7 +1116,7 @@ var SnapshotStore = class _SnapshotStore {
1087
1116
  async validatedSource(entry) {
1088
1117
  const source = entry.before;
1089
1118
  if (source === null || source.kind !== "blob") return source;
1090
- const st = await stat(source.path).catch((error) => {
1119
+ const st = await stat2(source.path).catch((error) => {
1091
1120
  if (isEnoent(error)) throw new Error(`missing backup sidecar ${source.path} for ${entry.path}`);
1092
1121
  throw error;
1093
1122
  });
@@ -1122,10 +1151,10 @@ var SnapshotStore = class _SnapshotStore {
1122
1151
  */
1123
1152
  async writeSidecar(dest, source) {
1124
1153
  const tmp = `${dest}.tmp`;
1125
- if (source.kind === "blob") await copyFile(source.path, tmp);
1126
- else if (source.kind === "text") await writeFile(tmp, source.bytes);
1127
- else await writeFile(tmp, Buffer.from(source.text, "utf8"));
1128
- await rename(tmp, dest);
1154
+ if (source.kind === "blob") await copyFile2(source.path, tmp);
1155
+ else if (source.kind === "text") await writeFile2(tmp, source.bytes);
1156
+ else await writeFile2(tmp, Buffer.from(source.text, "utf8"));
1157
+ await rename2(tmp, dest);
1129
1158
  }
1130
1159
  /**
1131
1160
  * Place one entry's sidecar next to its entry file: MOVE a staged capture
@@ -1135,10 +1164,10 @@ var SnapshotStore = class _SnapshotStore {
1135
1164
  */
1136
1165
  async placeSidecar(entryFile, content) {
1137
1166
  if (content.source === null) return null;
1138
- const dest = join(dirname(entryFile), sidecarName(basename(entryFile)));
1139
- if (content.staged !== void 0) await rename(content.staged.file, dest);
1167
+ const dest = join3(dirname3(entryFile), sidecarName(basename2(entryFile)));
1168
+ if (content.staged !== void 0) await rename2(content.staged.file, dest);
1140
1169
  else await this.writeSidecar(dest, content.source);
1141
- const st = await stat(dest);
1170
+ const st = await stat2(dest);
1142
1171
  return { source: { kind: "blob", path: dest }, size: st.size };
1143
1172
  }
1144
1173
  /**
@@ -1148,12 +1177,12 @@ var SnapshotStore = class _SnapshotStore {
1148
1177
  async commit(sessionId, entry, content, opts) {
1149
1178
  const time = Math.max(Date.now(), this.lastEntryTime + 1);
1150
1179
  this.lastEntryTime = time;
1151
- const parent = await realpath(dirname(entry.path)).catch(() => void 0);
1180
+ const parent = await locationPin(entry.path);
1152
1181
  await this.ensureDedupSeeded(sessionId);
1153
1182
  await this.assertKnownStoreVersion(sessionId);
1154
1183
  const dir = this.anchorDir(sessionId, entry.anchorSeq);
1155
1184
  await mkdir(dir, { recursive: true });
1156
- const file = join(dir, entryFileName(entry.callId));
1185
+ const file = join3(dir, entryFileName(entry.callId));
1157
1186
  const selfRef = this.entryRefOf(sessionId, entry.callId, entry.anchorSeq);
1158
1187
  const key = `${sessionId}\0${entry.path}`;
1159
1188
  const prior = this.lastEntry.get(key);
@@ -1249,7 +1278,7 @@ var SnapshotStore = class _SnapshotStore {
1249
1278
  const sessionDir = this.sessionDir(sessionId);
1250
1279
  let names;
1251
1280
  try {
1252
- names = await readdir(sessionDir);
1281
+ names = await readdir2(sessionDir);
1253
1282
  } catch (error) {
1254
1283
  if (error.code === "ENOENT") return [];
1255
1284
  throw error;
@@ -1258,10 +1287,10 @@ var SnapshotStore = class _SnapshotStore {
1258
1287
  for (const name2 of names) {
1259
1288
  const anchorSeq = Number(name2);
1260
1289
  if (!Number.isSafeInteger(anchorSeq) || anchorSeq < targetSeq) continue;
1261
- const files = await readdir(this.anchorDir(sessionId, anchorSeq)).catch(() => []);
1290
+ const files = await readdir2(this.anchorDir(sessionId, anchorSeq)).catch(() => []);
1262
1291
  for (const file of files) {
1263
1292
  if (!file.endsWith(".json")) continue;
1264
- const entry = await readEntry(join(this.anchorDir(sessionId, anchorSeq), file), anchorSeq);
1293
+ const entry = await readEntry(join3(this.anchorDir(sessionId, anchorSeq), file), anchorSeq);
1265
1294
  if (entry !== void 0) entries.push(entry);
1266
1295
  }
1267
1296
  }
@@ -1329,7 +1358,7 @@ var SnapshotStore = class _SnapshotStore {
1329
1358
  continue;
1330
1359
  }
1331
1360
  try {
1332
- if (await probe.isLink(entry.path)) {
1361
+ if (await probe.isUnsafeTarget(entry.path)) {
1333
1362
  skipped.push(entry.path);
1334
1363
  continue;
1335
1364
  }
@@ -1424,7 +1453,7 @@ var SnapshotStore = class _SnapshotStore {
1424
1453
  }
1425
1454
  );
1426
1455
  if (applied === "skipped") {
1427
- journalAction.failed = `parent directory moved or repointed: ${dirname(action.path)}`;
1456
+ journalAction.failed = `path moved or repointed: ${action.path}`;
1428
1457
  await this.saveJournal(journal);
1429
1458
  failed.push({ path: action.path, message: journalAction.failed });
1430
1459
  continue;
@@ -1455,7 +1484,7 @@ var SnapshotStore = class _SnapshotStore {
1455
1484
  }
1456
1485
  /** Absolute path of one restore-op journal file (the current prefix). */
1457
1486
  journalPath(sessionId, opId) {
1458
- return join(this.sessionDir(sessionId), `${JOURNAL_PREFIX}${safeFileId(opId)}.json`);
1487
+ return join3(this.sessionDir(sessionId), `${JOURNAL_PREFIX}${safeFileId(opId)}.json`);
1459
1488
  }
1460
1489
  /**
1461
1490
  * Locate an existing journal file for an op: the current prefix first, then
@@ -1465,9 +1494,9 @@ var SnapshotStore = class _SnapshotStore {
1465
1494
  async findJournalFile(sessionId, opId) {
1466
1495
  const dir = this.sessionDir(sessionId);
1467
1496
  for (const name2 of [`${JOURNAL_PREFIX}${safeFileId(opId)}.json`, `${LEGACY_JOURNAL_PREFIX}${safeFileId(opId)}.json`]) {
1468
- const file = join(dir, name2);
1497
+ const file = join3(dir, name2);
1469
1498
  try {
1470
- await stat(file);
1499
+ await stat2(file);
1471
1500
  return file;
1472
1501
  } catch {
1473
1502
  continue;
@@ -1502,18 +1531,18 @@ var SnapshotStore = class _SnapshotStore {
1502
1531
  async beginRestore(sessionId, targetSeq, actions, probe) {
1503
1532
  const sessionDir = this.sessionDir(sessionId);
1504
1533
  try {
1505
- await this.pruneTerminalJournals(sessionDir, await readdir(sessionDir));
1534
+ await this.pruneTerminalJournals(sessionDir, await readdir2(sessionDir));
1506
1535
  } catch (error) {
1507
1536
  if (!isEnoent(error)) throw error;
1508
1537
  }
1509
1538
  const id = `op-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1510
- const rescueDir = join(sessionDir, RESCUE_DIR, safeFileId(id));
1539
+ const rescueDir = join3(sessionDir, RESCUE_DIR, safeFileId(id));
1511
1540
  let rescueDirReady = false;
1512
1541
  const journalActions = [];
1513
1542
  for (const [index, action] of actions.entries()) {
1514
1543
  let rescue = null;
1515
1544
  let rescueError;
1516
- const dest = join(rescueDir, `${index}${SIDECAR_SUFFIX}`);
1545
+ const dest = join3(rescueDir, `${index}${SIDECAR_SUFFIX}`);
1517
1546
  try {
1518
1547
  if (!rescueDirReady) {
1519
1548
  await mkdir(rescueDir, { recursive: true });
@@ -1525,7 +1554,7 @@ var SnapshotStore = class _SnapshotStore {
1525
1554
  } catch (error) {
1526
1555
  rescueError = error instanceof Error ? error.message : String(error);
1527
1556
  }
1528
- const rescueMode = (await stat(action.path).catch(() => void 0))?.mode;
1557
+ const rescueMode = (await stat2(action.path).catch(() => void 0))?.mode;
1529
1558
  const journalAction = {
1530
1559
  path: action.path,
1531
1560
  action: action.action,
@@ -1561,7 +1590,7 @@ var SnapshotStore = class _SnapshotStore {
1561
1590
  if (file === void 0) return void 0;
1562
1591
  let parsed;
1563
1592
  try {
1564
- parsed = JSON.parse(await readFile(file, "utf8"));
1593
+ parsed = JSON.parse(await readFile3(file, "utf8"));
1565
1594
  } catch (error) {
1566
1595
  throw new Error(`restore journal ${file} is corrupt: ${error instanceof Error ? error.message : String(error)}`);
1567
1596
  }
@@ -1580,7 +1609,7 @@ var SnapshotStore = class _SnapshotStore {
1580
1609
  const sessionDir = this.sessionDir(sessionId);
1581
1610
  let names;
1582
1611
  try {
1583
- names = await readdir(sessionDir);
1612
+ names = await readdir2(sessionDir);
1584
1613
  } catch (error) {
1585
1614
  if (isEnoent(error)) return { journals: [], corrupt: [] };
1586
1615
  throw error;
@@ -1590,7 +1619,7 @@ var SnapshotStore = class _SnapshotStore {
1590
1619
  for (const name2 of names) {
1591
1620
  if (!isJournalName(name2)) continue;
1592
1621
  try {
1593
- const parsed = JSON.parse(await readFile(join(sessionDir, name2), "utf8"));
1622
+ const parsed = JSON.parse(await readFile3(join3(sessionDir, name2), "utf8"));
1594
1623
  if (!isRestoreJournal(parsed)) {
1595
1624
  corrupt.push({ file: name2, message: "journal failed schema validation" });
1596
1625
  continue;
@@ -1600,7 +1629,7 @@ var SnapshotStore = class _SnapshotStore {
1600
1629
  corrupt.push({ file: name2, message: "journal references are invalid" });
1601
1630
  continue;
1602
1631
  }
1603
- journal.sourceFile = join(sessionDir, name2);
1632
+ journal.sourceFile = join3(sessionDir, name2);
1604
1633
  journals.push(journal);
1605
1634
  } catch (error) {
1606
1635
  corrupt.push({ file: name2, message: error instanceof Error ? error.message : String(error) });
@@ -1632,6 +1661,7 @@ var SnapshotStore = class _SnapshotStore {
1632
1661
  */
1633
1662
  async applyActionToDisk(kind, path, content, deleteFile, opts) {
1634
1663
  if (!await parentStillMatches(path, opts?.parent)) return "skipped";
1664
+ if (await isUnsafeTarget(path)) return "skipped";
1635
1665
  if (kind === "delete") {
1636
1666
  try {
1637
1667
  await deleteFile(path);
@@ -1642,17 +1672,17 @@ var SnapshotStore = class _SnapshotStore {
1642
1672
  }
1643
1673
  }
1644
1674
  if (content === null) throw new Error(`restore of ${path} has no recorded content`);
1645
- await mkdir(dirname(path), { recursive: true });
1646
- const current = (await stat(path).catch(() => void 0))?.mode;
1675
+ await mkdir(dirname3(path), { recursive: true });
1676
+ const current = (await stat2(path).catch(() => void 0))?.mode;
1647
1677
  let widened = false;
1648
1678
  if (current !== void 0 && (current & 128) === 0) {
1649
1679
  await chmod(path, current | 128).catch(() => void 0);
1650
1680
  widened = true;
1651
1681
  }
1652
1682
  try {
1653
- if (content.kind === "blob") await copyFile(content.path, path);
1654
- else if (content.kind === "text") await writeFile(path, content.bytes);
1655
- else await writeFile(path, Buffer.from(content.text, "utf8"));
1683
+ if (content.kind === "blob") await copyFile2(content.path, path);
1684
+ else if (content.kind === "text") await writeFile2(path, content.bytes);
1685
+ else await writeFile2(path, Buffer.from(content.text, "utf8"));
1656
1686
  } catch (error) {
1657
1687
  if (widened && current !== void 0) await chmod(path, current).catch(() => void 0);
1658
1688
  throw error;
@@ -1802,7 +1832,7 @@ var SnapshotStore = class _SnapshotStore {
1802
1832
  }
1803
1833
  );
1804
1834
  if (applied === "skipped") {
1805
- action.failed = `parent directory moved or repointed: ${dirname(action.path)}`;
1835
+ action.failed = `path moved or repointed: ${action.path}`;
1806
1836
  await this.saveJournal(journal);
1807
1837
  failed.push({ path: action.path, message: action.failed });
1808
1838
  continue;
@@ -1892,7 +1922,7 @@ var SnapshotStore = class _SnapshotStore {
1892
1922
  }
1893
1923
  );
1894
1924
  if (applied === "skipped") {
1895
- journal.rollbackError = `parent directory moved or repointed: ${action.path}`;
1925
+ journal.rollbackError = `path moved or repointed: ${action.path}`;
1896
1926
  journal.state = "recovery-required";
1897
1927
  await this.saveJournal(journal);
1898
1928
  failed.push({ path: action.path, message: journal.rollbackError });
@@ -1949,13 +1979,13 @@ var SnapshotStore = class _SnapshotStore {
1949
1979
  const sessionDir = this.sessionDir(sessionId);
1950
1980
  let names;
1951
1981
  try {
1952
- names = await readdir(sessionDir);
1982
+ names = await readdir2(sessionDir);
1953
1983
  } catch (error) {
1954
1984
  if (isEnoent(error)) return;
1955
1985
  throw error;
1956
1986
  }
1957
1987
  await this.pruneTerminalJournals(sessionDir, names);
1958
- await this.prunePendingCaptures(join(sessionDir, PENDING_DIR));
1988
+ await this.prunePendingCaptures(join3(sessionDir, PENDING_DIR));
1959
1989
  const seqs = names.map(Number).filter((seq) => Number.isSafeInteger(seq)).sort((a, b) => a - b);
1960
1990
  const excess = seqs.length - keep;
1961
1991
  if (excess <= 0) return;
@@ -1963,10 +1993,10 @@ var SnapshotStore = class _SnapshotStore {
1963
1993
  for (const seq of await this.pinnedAnchors(sessionDir, names)) doomed.delete(seq);
1964
1994
  if (doomed.size === 0) return;
1965
1995
  for (const seq of seqs.slice(excess)) {
1966
- const files = await readdir(this.anchorDir(sessionId, seq)).catch(() => []);
1996
+ const files = await readdir2(this.anchorDir(sessionId, seq)).catch(() => []);
1967
1997
  for (const file of files) {
1968
1998
  if (!file.endsWith(".json")) continue;
1969
- const entryFile = join(this.anchorDir(sessionId, seq), file);
1999
+ const entryFile = join3(this.anchorDir(sessionId, seq), file);
1970
2000
  let entry;
1971
2001
  try {
1972
2002
  entry = await readEntry(entryFile, seq);
@@ -1997,9 +2027,9 @@ var SnapshotStore = class _SnapshotStore {
1997
2027
  time: entry.time
1998
2028
  };
1999
2029
  } else {
2000
- const dest = join(dirname(entryFile), sidecarName(basename(entryFile)));
2030
+ const dest = join3(dirname3(entryFile), sidecarName(basename2(entryFile)));
2001
2031
  await this.writeSidecar(dest, source);
2002
- const st = await stat(dest);
2032
+ const st = await stat2(dest);
2003
2033
  real = {
2004
2034
  callId: entry.callId,
2005
2035
  anchorSeq: entry.anchorSeq,
@@ -2031,14 +2061,14 @@ var SnapshotStore = class _SnapshotStore {
2031
2061
  async prunePendingCaptures(pendingDir) {
2032
2062
  let names;
2033
2063
  try {
2034
- names = await readdir(pendingDir);
2064
+ names = await readdir2(pendingDir);
2035
2065
  } catch {
2036
2066
  return;
2037
2067
  }
2038
2068
  const cutoff = Date.now() - PENDING_MAX_AGE_MS;
2039
2069
  for (const name2 of names) {
2040
- const file = join(pendingDir, name2);
2041
- const st = await lstat(file).catch(() => void 0);
2070
+ const file = join3(pendingDir, name2);
2071
+ const st = await lstat2(file).catch(() => void 0);
2042
2072
  if (st === void 0 || !st.isFile() || st.mtimeMs >= cutoff) continue;
2043
2073
  await rm(file, { force: true });
2044
2074
  }
@@ -2056,7 +2086,7 @@ var SnapshotStore = class _SnapshotStore {
2056
2086
  if (!isJournalName(name2)) continue;
2057
2087
  let parsed;
2058
2088
  try {
2059
- parsed = JSON.parse(await readFile(join(sessionDir, name2), "utf8"));
2089
+ parsed = JSON.parse(await readFile3(join3(sessionDir, name2), "utf8"));
2060
2090
  } catch {
2061
2091
  continue;
2062
2092
  }
@@ -2086,12 +2116,12 @@ var SnapshotStore = class _SnapshotStore {
2086
2116
  async pruneTerminalJournals(sessionDir, names) {
2087
2117
  for (const name2 of names) {
2088
2118
  if (!isJournalName(name2)) continue;
2089
- const file = join(sessionDir, name2);
2119
+ const file = join3(sessionDir, name2);
2090
2120
  try {
2091
- const parsed = JSON.parse(await readFile(file, "utf8"));
2121
+ const parsed = JSON.parse(await readFile3(file, "utf8"));
2092
2122
  if (parsed.state === "completed" || parsed.state === "rolled-back") {
2093
2123
  await rm(file, { force: true });
2094
- await rm(join(sessionDir, RESCUE_DIR, safeFileId(journalOpIdOf(name2))), { recursive: true, force: true });
2124
+ await rm(join3(sessionDir, RESCUE_DIR, safeFileId(journalOpIdOf(name2))), { recursive: true, force: true });
2095
2125
  }
2096
2126
  } catch {
2097
2127
  }
@@ -2100,7 +2130,7 @@ var SnapshotStore = class _SnapshotStore {
2100
2130
  /** True when a path exists on disk. */
2101
2131
  async exists(path) {
2102
2132
  try {
2103
- await stat(path);
2133
+ await stat2(path);
2104
2134
  return true;
2105
2135
  } catch (error) {
2106
2136
  if (isEnoent(error)) return false;
@@ -2147,17 +2177,17 @@ var SnapshotStore = class _SnapshotStore {
2147
2177
  const report = () => ({ scanned, deleted, freedBytes, kept, remainingBytes, skippedActive, dryRun });
2148
2178
  let names;
2149
2179
  try {
2150
- names = await readdir(this.root);
2180
+ names = await readdir2(this.root);
2151
2181
  } catch (error) {
2152
2182
  if (error.code === "ENOENT") return report();
2153
2183
  throw error;
2154
2184
  }
2155
2185
  for (const name2 of names) {
2156
2186
  if (name2.startsWith(".")) continue;
2157
- const full = join(this.root, name2);
2187
+ const full = join3(this.root, name2);
2158
2188
  let st;
2159
2189
  try {
2160
- st = await lstat(full);
2190
+ st = await lstat2(full);
2161
2191
  } catch {
2162
2192
  continue;
2163
2193
  }
@@ -2226,7 +2256,7 @@ var SnapshotStore = class _SnapshotStore {
2226
2256
  const sessionDir = this.sessionDir(sessionId);
2227
2257
  let names;
2228
2258
  try {
2229
- names = await readdir(sessionDir);
2259
+ names = await readdir2(sessionDir);
2230
2260
  } catch (error) {
2231
2261
  if (isEnoent(error)) return { anchorGroups: 0, entries: 0, journals: 0, bytes: 0 };
2232
2262
  throw error;
@@ -2237,8 +2267,8 @@ var SnapshotStore = class _SnapshotStore {
2237
2267
  let bytes = 0;
2238
2268
  for (const name2 of names) {
2239
2269
  if (name2.startsWith(".") && name2 !== PENDING_DIR) continue;
2240
- const full = join(sessionDir, name2);
2241
- const st = await lstat(full).catch(() => void 0);
2270
+ const full = join3(sessionDir, name2);
2271
+ const st = await lstat2(full).catch(() => void 0);
2242
2272
  if (st === void 0) continue;
2243
2273
  if (st.isDirectory()) {
2244
2274
  if (name2 === PENDING_DIR || name2 === RESCUE_DIR) {
@@ -2247,7 +2277,7 @@ var SnapshotStore = class _SnapshotStore {
2247
2277
  }
2248
2278
  if (!Number.isSafeInteger(Number(name2))) continue;
2249
2279
  anchorGroups++;
2250
- for (const file of await readdir(full).catch(() => [])) {
2280
+ for (const file of await readdir2(full).catch(() => [])) {
2251
2281
  if (file.endsWith(".json")) entries++;
2252
2282
  }
2253
2283
  bytes += await dirBytes(full);
@@ -2308,7 +2338,7 @@ var SnapshotStore = class _SnapshotStore {
2308
2338
  */
2309
2339
  async readFormatVersion(sessionId) {
2310
2340
  try {
2311
- const raw = await readFile(join(this.sessionDir(sessionId), _SnapshotStore.FORMAT_FILE), "utf8");
2341
+ const raw = await readFile3(join3(this.sessionDir(sessionId), _SnapshotStore.FORMAT_FILE), "utf8");
2312
2342
  const parsed = Number(raw.trim());
2313
2343
  return Number.isFinite(parsed) ? parsed : null;
2314
2344
  } catch {
@@ -2327,10 +2357,10 @@ var SnapshotStore = class _SnapshotStore {
2327
2357
  async markFormatVersion(sessionId, sessionVersion) {
2328
2358
  const dir = this.sessionDir(sessionId);
2329
2359
  await mkdir(dir, { recursive: true });
2330
- const file = join(dir, _SnapshotStore.FORMAT_FILE);
2360
+ const file = join3(dir, _SnapshotStore.FORMAT_FILE);
2331
2361
  const tmp = `${file}.tmp`;
2332
- await writeFile(tmp, `${sessionVersion}`, "utf8");
2333
- await rename(tmp, file);
2362
+ await writeFile2(tmp, `${sessionVersion}`, "utf8");
2363
+ await rename2(tmp, file);
2334
2364
  }
2335
2365
  /**
2336
2366
  * Set the session-format version the store stamps onto every snapshot it
@@ -2350,7 +2380,7 @@ var SnapshotStore = class _SnapshotStore {
2350
2380
  */
2351
2381
  async readStoreVersion(sessionId) {
2352
2382
  try {
2353
- const raw = await readFile(join(this.sessionDir(sessionId), _SnapshotStore.STORE_FILE), "utf8");
2383
+ const raw = await readFile3(join3(this.sessionDir(sessionId), _SnapshotStore.STORE_FILE), "utf8");
2354
2384
  const parsed = Number(raw.trim());
2355
2385
  return Number.isFinite(parsed) ? parsed : null;
2356
2386
  } catch {
@@ -2365,10 +2395,10 @@ var SnapshotStore = class _SnapshotStore {
2365
2395
  async markStoreVersion(sessionId, storeVersion) {
2366
2396
  const dir = this.sessionDir(sessionId);
2367
2397
  await mkdir(dir, { recursive: true });
2368
- const file = join(dir, _SnapshotStore.STORE_FILE);
2398
+ const file = join3(dir, _SnapshotStore.STORE_FILE);
2369
2399
  const tmp = `${file}.tmp`;
2370
- await writeFile(tmp, `${storeVersion}`, "utf8");
2371
- await rename(tmp, file);
2400
+ await writeFile2(tmp, `${storeVersion}`, "utf8");
2401
+ await rename2(tmp, file);
2372
2402
  }
2373
2403
  /**
2374
2404
  * Refuse to plan against (or write into) a session whose store format is
@@ -2380,7 +2410,7 @@ var SnapshotStore = class _SnapshotStore {
2380
2410
  async assertKnownStoreVersion(sessionId) {
2381
2411
  const version = await this.readStoreVersion(sessionId);
2382
2412
  if (version !== null && version > CURRENT_STORE_VERSION) {
2383
- throw new UnknownStoreVersionError(version, join(this.sessionDir(sessionId), _SnapshotStore.STORE_FILE));
2413
+ throw new UnknownStoreVersionError(version, join3(this.sessionDir(sessionId), _SnapshotStore.STORE_FILE));
2384
2414
  }
2385
2415
  }
2386
2416
  /**
@@ -2425,7 +2455,7 @@ async function reconcileTracked(store, sessionId, anchorSeq, tracked, probe = de
2425
2455
  let recorded = 0;
2426
2456
  for (const path of tracked) {
2427
2457
  try {
2428
- if (await probe.isLink(path)) continue;
2458
+ if (await probe.isUnsafeTarget(path)) continue;
2429
2459
  if (store.isOverFileCap(await probe.size?.(path))) continue;
2430
2460
  const last = await store.lastKnownContent(sessionId, path);
2431
2461
  if (last !== void 0 && last !== null && last.kind !== "lossyText") {
@@ -2453,8 +2483,8 @@ async function reconcileTracked(store, sessionId, anchorSeq, tracked, probe = de
2453
2483
  }
2454
2484
 
2455
2485
  // src/snapshot-cleanup.ts
2456
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
2457
- import { dirname as dirname2, join as join2 } from "node:path";
2486
+ import { mkdir as mkdir2, readFile as readFile4, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
2487
+ import { dirname as dirname4, join as join4 } from "node:path";
2458
2488
  import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
2459
2489
  var DEFAULT_MAX_AGE_DAYS = 30;
2460
2490
  var DEFAULT_CLEANUP_CONFIG = { enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS };
@@ -2481,11 +2511,11 @@ function volatileCleanupStore(settings, writer) {
2481
2511
  var AUTO_SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2482
2512
  var STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
2483
2513
  function resolveCleanupStatePath(dshHome) {
2484
- return join2(resolveDshHome2(dshHome), STATE_FILENAME);
2514
+ return join4(resolveDshHome2(dshHome), STATE_FILENAME);
2485
2515
  }
2486
2516
  async function loadLastSweepAt(path) {
2487
2517
  try {
2488
- const raw = JSON.parse(await readFile2(path, "utf8"));
2518
+ const raw = JSON.parse(await readFile4(path, "utf8"));
2489
2519
  const value = raw["lastSweepAt"];
2490
2520
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
2491
2521
  } catch {
@@ -2494,9 +2524,9 @@ async function loadLastSweepAt(path) {
2494
2524
  }
2495
2525
  async function saveLastSweepAt(path, ms) {
2496
2526
  const tmp = `${path}.tmp`;
2497
- await mkdir2(dirname2(path), { recursive: true });
2498
- await writeFile2(tmp, JSON.stringify({ lastSweepAt: ms }), "utf8");
2499
- await rename2(tmp, path);
2527
+ await mkdir2(dirname4(path), { recursive: true });
2528
+ await writeFile3(tmp, JSON.stringify({ lastSweepAt: ms }), "utf8");
2529
+ await rename3(tmp, path);
2500
2530
  }
2501
2531
  function isAborted(signal) {
2502
2532
  return signal?.aborted === true;
@@ -2675,6 +2705,7 @@ async function captureBefore(fs, workingDirectory, store, exec, pending) {
2675
2705
  const cwd = await workingDirectory.ensure(agent, exec.signal);
2676
2706
  const target = await resolveTarget(fs, path, cwd, exec.signal);
2677
2707
  if (target === void 0) return;
2708
+ if (fs.processPathFromHostPath(target.displayPath) === void 0) return;
2678
2709
  const info = await fs.stat(target, exec.signal).catch((error) => {
2679
2710
  if (isNotFoundError(error)) return void 0;
2680
2711
  throw error;
@@ -2689,9 +2720,9 @@ async function captureBefore(fs, workingDirectory, store, exec, pending) {
2689
2720
  const staged = await store.stageCapture(agent.session.id, key);
2690
2721
  let backup;
2691
2722
  try {
2692
- await copyFile2(target.displayPath, staged);
2693
- const st = await stat2(staged);
2694
- const source = await stat2(target.displayPath).catch(() => void 0);
2723
+ await copyFile3(target.displayPath, staged);
2724
+ const st = await stat3(staged);
2725
+ const source = await stat3(target.displayPath).catch(() => void 0);
2695
2726
  backup = {
2696
2727
  file: staged,
2697
2728
  size: st.size,