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/client.js +1 -1
- package/lib/index.js +223 -192
- package/lib/types/snapshot-format.d.ts +479 -0
- package/lib/types/snapshot-fs.d.ts +85 -0
- package/lib/types/snapshot.d.ts +4 -372
- package/package.json +1 -1
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
|
|
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 {
|
|
494
|
-
import {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
852
|
+
async function isUnsafeTarget(path) {
|
|
835
853
|
try {
|
|
836
|
-
const
|
|
837
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
865
|
-
const escapes = inside === ".." || inside.startsWith(`..${
|
|
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(
|
|
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 ??
|
|
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
|
|
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
|
|
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 =
|
|
1068
|
+
const dir = join3(this.sessionDir(sessionId), PENDING_DIR);
|
|
1040
1069
|
await mkdir(dir, { recursive: true });
|
|
1041
|
-
return
|
|
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(
|
|
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
|
|
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
|
|
1126
|
-
else if (source.kind === "text") await
|
|
1127
|
-
else await
|
|
1128
|
-
await
|
|
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 =
|
|
1139
|
-
if (content.staged !== void 0) await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
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 = `
|
|
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
|
|
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 =
|
|
1497
|
+
const file = join3(dir, name2);
|
|
1469
1498
|
try {
|
|
1470
|
-
await
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
1646
|
-
const current = (await
|
|
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
|
|
1654
|
-
else if (content.kind === "text") await
|
|
1655
|
-
else await
|
|
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 = `
|
|
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 = `
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
2030
|
+
const dest = join3(dirname3(entryFile), sidecarName(basename2(entryFile)));
|
|
2001
2031
|
await this.writeSidecar(dest, source);
|
|
2002
|
-
const st = await
|
|
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
|
|
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 =
|
|
2041
|
-
const st = await
|
|
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
|
|
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 =
|
|
2119
|
+
const file = join3(sessionDir, name2);
|
|
2090
2120
|
try {
|
|
2091
|
-
const parsed = JSON.parse(await
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
2187
|
+
const full = join3(this.root, name2);
|
|
2158
2188
|
let st;
|
|
2159
2189
|
try {
|
|
2160
|
-
st = await
|
|
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
|
|
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 =
|
|
2241
|
-
const st = await
|
|
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
|
|
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
|
|
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 =
|
|
2360
|
+
const file = join3(dir, _SnapshotStore.FORMAT_FILE);
|
|
2331
2361
|
const tmp = `${file}.tmp`;
|
|
2332
|
-
await
|
|
2333
|
-
await
|
|
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
|
|
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 =
|
|
2398
|
+
const file = join3(dir, _SnapshotStore.STORE_FILE);
|
|
2369
2399
|
const tmp = `${file}.tmp`;
|
|
2370
|
-
await
|
|
2371
|
-
await
|
|
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,
|
|
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.
|
|
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
|
|
2457
|
-
import { dirname as
|
|
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
|
|
2514
|
+
return join4(resolveDshHome2(dshHome), STATE_FILENAME);
|
|
2485
2515
|
}
|
|
2486
2516
|
async function loadLastSweepAt(path) {
|
|
2487
2517
|
try {
|
|
2488
|
-
const raw = JSON.parse(await
|
|
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(
|
|
2498
|
-
await
|
|
2499
|
-
await
|
|
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
|
|
2693
|
-
const st = await
|
|
2694
|
-
const source = await
|
|
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,
|