@claudexor/workspace 4.8.1 → 5.0.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.
Files changed (136) hide show
  1. package/dist/anchor-store.d.ts +20 -9
  2. package/dist/anchor-store.d.ts.map +1 -1
  3. package/dist/anchor-store.js +96 -16
  4. package/dist/anchor-store.js.map +1 -1
  5. package/dist/capture-failure.d.ts.map +1 -1
  6. package/dist/capture-failure.js +7 -2
  7. package/dist/capture-failure.js.map +1 -1
  8. package/dist/capture-inventory.d.ts +10 -0
  9. package/dist/capture-inventory.d.ts.map +1 -0
  10. package/dist/capture-inventory.js +63 -0
  11. package/dist/capture-inventory.js.map +1 -0
  12. package/dist/capture-objects.d.ts +32 -0
  13. package/dist/capture-objects.d.ts.map +1 -0
  14. package/dist/capture-objects.js +129 -0
  15. package/dist/capture-objects.js.map +1 -0
  16. package/dist/cleanup-fault.d.ts +18 -0
  17. package/dist/cleanup-fault.d.ts.map +1 -0
  18. package/dist/cleanup-fault.js +45 -0
  19. package/dist/cleanup-fault.js.map +1 -0
  20. package/dist/continuation-evidence.d.ts +61 -15
  21. package/dist/continuation-evidence.d.ts.map +1 -1
  22. package/dist/continuation-evidence.js +151 -36
  23. package/dist/continuation-evidence.js.map +1 -1
  24. package/dist/continuation-facts.d.ts +93 -0
  25. package/dist/continuation-facts.d.ts.map +1 -0
  26. package/dist/continuation-facts.js +162 -0
  27. package/dist/continuation-facts.js.map +1 -0
  28. package/dist/directory-workspace.d.ts.map +1 -1
  29. package/dist/directory-workspace.js +1 -3
  30. package/dist/directory-workspace.js.map +1 -1
  31. package/dist/files-io.d.ts.map +1 -1
  32. package/dist/files-io.js +1 -7
  33. package/dist/files-io.js.map +1 -1
  34. package/dist/fs-presence.d.ts +12 -0
  35. package/dist/fs-presence.d.ts.map +1 -0
  36. package/dist/fs-presence.js +35 -0
  37. package/dist/fs-presence.js.map +1 -0
  38. package/dist/git-nested-repositories.d.ts.map +1 -1
  39. package/dist/git-nested-repositories.js +11 -2
  40. package/dist/git-nested-repositories.js.map +1 -1
  41. package/dist/git-observed.d.ts +75 -0
  42. package/dist/git-observed.d.ts.map +1 -0
  43. package/dist/git-observed.js +172 -0
  44. package/dist/git-observed.js.map +1 -0
  45. package/dist/git-result.d.ts +36 -0
  46. package/dist/git-result.d.ts.map +1 -0
  47. package/dist/git-result.js +96 -0
  48. package/dist/git-result.js.map +1 -0
  49. package/dist/git-run.d.ts +11 -0
  50. package/dist/git-run.d.ts.map +1 -1
  51. package/dist/git-run.js +24 -0
  52. package/dist/git-run.js.map +1 -1
  53. package/dist/git-snapshot.d.ts +40 -0
  54. package/dist/git-snapshot.d.ts.map +1 -0
  55. package/dist/git-snapshot.js +238 -0
  56. package/dist/git-snapshot.js.map +1 -0
  57. package/dist/git-transient-capture.d.ts +63 -16
  58. package/dist/git-transient-capture.d.ts.map +1 -1
  59. package/dist/git-transient-capture.js +170 -57
  60. package/dist/git-transient-capture.js.map +1 -1
  61. package/dist/git.d.ts +46 -21
  62. package/dist/git.d.ts.map +1 -1
  63. package/dist/git.js +164 -103
  64. package/dist/git.js.map +1 -1
  65. package/dist/index.d.ts +9 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +9 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/lanes.d.ts +2 -1
  70. package/dist/lanes.d.ts.map +1 -1
  71. package/dist/lanes.js +7 -6
  72. package/dist/lanes.js.map +1 -1
  73. package/dist/manager.d.ts +21 -7
  74. package/dist/manager.d.ts.map +1 -1
  75. package/dist/manager.js +30 -17
  76. package/dist/manager.js.map +1 -1
  77. package/dist/object-compose.d.ts +58 -0
  78. package/dist/object-compose.d.ts.map +1 -0
  79. package/dist/object-compose.js +444 -0
  80. package/dist/object-compose.js.map +1 -0
  81. package/dist/object-materialize.d.ts +52 -0
  82. package/dist/object-materialize.d.ts.map +1 -0
  83. package/dist/object-materialize.js +467 -0
  84. package/dist/object-materialize.js.map +1 -0
  85. package/dist/object-occupied.d.ts +27 -0
  86. package/dist/object-occupied.d.ts.map +1 -0
  87. package/dist/object-occupied.js +131 -0
  88. package/dist/object-occupied.js.map +1 -0
  89. package/dist/object-revert.d.ts +45 -0
  90. package/dist/object-revert.d.ts.map +1 -0
  91. package/dist/object-revert.js +265 -0
  92. package/dist/object-revert.js.map +1 -0
  93. package/dist/object-saved-files.d.ts +50 -0
  94. package/dist/object-saved-files.d.ts.map +1 -0
  95. package/dist/object-saved-files.js +176 -0
  96. package/dist/object-saved-files.js.map +1 -0
  97. package/dist/object-sparse.d.ts +22 -0
  98. package/dist/object-sparse.d.ts.map +1 -0
  99. package/dist/object-sparse.js +72 -0
  100. package/dist/object-sparse.js.map +1 -0
  101. package/dist/object-store-leases.d.ts +41 -0
  102. package/dist/object-store-leases.d.ts.map +1 -0
  103. package/dist/object-store-leases.js +277 -0
  104. package/dist/object-store-leases.js.map +1 -0
  105. package/dist/object-store-roots.d.ts +25 -0
  106. package/dist/object-store-roots.d.ts.map +1 -0
  107. package/dist/object-store-roots.js +114 -0
  108. package/dist/object-store-roots.js.map +1 -0
  109. package/dist/object-store.d.ts +110 -0
  110. package/dist/object-store.d.ts.map +1 -0
  111. package/dist/object-store.js +404 -0
  112. package/dist/object-store.js.map +1 -0
  113. package/dist/object-transfer.d.ts +41 -0
  114. package/dist/object-transfer.d.ts.map +1 -0
  115. package/dist/object-transfer.js +267 -0
  116. package/dist/object-transfer.js.map +1 -0
  117. package/dist/plain-diff-capture.d.ts +1 -2
  118. package/dist/plain-diff-capture.d.ts.map +1 -1
  119. package/dist/plain-diff-capture.js +2 -7
  120. package/dist/plain-diff-capture.js.map +1 -1
  121. package/dist/plain-diff.d.ts +0 -4
  122. package/dist/plain-diff.d.ts.map +1 -1
  123. package/dist/plain-diff.js +2 -44
  124. package/dist/plain-diff.js.map +1 -1
  125. package/dist/raw-patch.d.ts.map +1 -1
  126. package/dist/raw-patch.js +5 -8
  127. package/dist/raw-patch.js.map +1 -1
  128. package/dist/run-expiry.d.ts +3 -0
  129. package/dist/run-expiry.d.ts.map +1 -0
  130. package/dist/run-expiry.js +15 -0
  131. package/dist/run-expiry.js.map +1 -0
  132. package/dist/thread-tree.d.ts +36 -2
  133. package/dist/thread-tree.d.ts.map +1 -1
  134. package/dist/thread-tree.js +87 -11
  135. package/dist/thread-tree.js.map +1 -1
  136. package/package.json +5 -5
@@ -0,0 +1,72 @@
1
+ import { lstatPresent } from "./fs-presence.js";
2
+ import { classifyGitFailure, gitObserved, GitOperationError } from "./git-observed.js";
3
+ /** `git sparse-checkout check-rules` arrived in Git 2.41; an older Git answers its
4
+ * usage error (exit 129: an unknown subcommand) instead. */
5
+ const CHECK_RULES_UNKNOWN = 129;
6
+ /**
7
+ * The paths among `paths` that the worktree's sparse-checkout patterns leave out
8
+ * (none when it is not a sparse checkout), from one `check-rules` call, or null when
9
+ * this Git cannot evaluate the patterns (older than 2.41). A snapshot's `add -A`
10
+ * skips such a path, a tracked one keeping HEAD's blob whether its file is absent,
11
+ * as a sparse checkout usually leaves it, or present, so an operation that addresses
12
+ * one could not tell what it holds before or after writing. With
13
+ * `core.sparseCheckout` set but no patterns file, Git checks out everything, and so
14
+ * nothing is left out here either. A worktree that is not sparse costs one
15
+ * `git config` and never asks `check-rules`.
16
+ */
17
+ export async function outsideSparseCheckout(worktree, paths, env) {
18
+ if (paths.length === 0)
19
+ return [];
20
+ const sparse = await gitObserved(worktree, ["config", "--type=bool", "--get", "core.sparseCheckout"], { env, label: "config core.sparseCheckout", allowFailure: true });
21
+ if (sparse.stdout.trim() !== "true")
22
+ return [];
23
+ const patterns = await gitObserved(worktree, ["rev-parse", "--path-format=absolute", "--git-path", "info/sparse-checkout"], { env, label: "rev-parse --git-path info/sparse-checkout" });
24
+ if (!lstatPresent(patterns.stdout.trim()))
25
+ return [];
26
+ const kept = await gitObserved(worktree, ["sparse-checkout", "check-rules", "-z"], {
27
+ env,
28
+ label: "sparse-checkout check-rules",
29
+ input: paths.map((path) => `${path}\0`).join(""),
30
+ allowFailure: true,
31
+ });
32
+ if (kept.code === CHECK_RULES_UNKNOWN)
33
+ return null;
34
+ if (kept.code !== 0)
35
+ throw new GitOperationError(classifyGitFailure(kept.stderr), `git sparse-checkout check-rules failed: ${kept.stderr.trim().slice(0, 2_000)}`);
36
+ const inside = new Set(kept.stdout.split("\0"));
37
+ return paths.filter((path) => !inside.has(path));
38
+ }
39
+ /**
40
+ * The refusal of an operation that must write or remove a path outside the
41
+ * sparse-checkout patterns, naming those paths, or of any operation in a sparse
42
+ * checkout whose Git cannot evaluate its patterns; nothing is written. Widening the
43
+ * sparse checkout (`git sparse-checkout add`) lets the same operation run. Paths it
44
+ * does not address keep their files: the checkout runs with the patterns off.
45
+ */
46
+ export async function sparseCheckoutRefusal(worktree, paths, operation, env = {}) {
47
+ const found = await outsideSparseCheckout(worktree, paths, env);
48
+ if (found === null) {
49
+ const version = await gitObserved(worktree, ["version"], {
50
+ env,
51
+ label: "version",
52
+ allowFailure: true,
53
+ });
54
+ return {
55
+ code: "git_failed",
56
+ message: `this checkout is sparse, and its Git (${version.stdout.trim() || "version unknown"}) cannot evaluate the sparse-checkout patterns: Claudexor asks \`git sparse-checkout check-rules\`, which Git 2.41 added, which of the paths this ${operation} changes are inside them; nothing was written. Use Git 2.41 or newer, or turn the sparse checkout off (git sparse-checkout disable), and try again`,
57
+ paths: [],
58
+ pathCount: 0,
59
+ };
60
+ }
61
+ const outside = [...new Set(found)].sort();
62
+ if (outside.length === 0)
63
+ return null;
64
+ const what = operation === "delivery" ? "this result changes" : "this revert must change";
65
+ return {
66
+ code: "delivery_outside_sparse_checkout",
67
+ message: `${outside.length} path(s) ${what} are outside this checkout's sparse-checkout patterns, so nothing was written; widen the sparse checkout (git sparse-checkout add) to include them and try again: ${outside.slice(0, 20).join(", ")}`.slice(0, 4_000),
68
+ paths: outside.slice(0, 200),
69
+ pathCount: outside.length,
70
+ };
71
+ }
72
+ //# sourceMappingURL=object-sparse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-sparse.js","sourceRoot":"","sources":["../src/object-sparse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /** The store's private control directories (pending, roots, tmp, the prune lock). */
2
+ export declare function controlDir(store: string, name: string): string;
3
+ export interface PendingRoots {
4
+ store: string;
5
+ path: string;
6
+ roots: string[];
7
+ /** Renews the record's mtime while the import runs. */
8
+ renewal?: ReturnType<typeof setInterval>;
9
+ }
10
+ /**
11
+ * Two-flag protocol with prune: an import first records its pending roots, then
12
+ * waits out a prune already holding the lock; a prune first takes the lock,
13
+ * then yields when any live pending record exists. One of the two always sees
14
+ * the other, so a prune never deletes an object a concurrent import relies on.
15
+ * The record names this process by PID and kernel birth: it stays live for as
16
+ * long as this process runs, however long the import takes.
17
+ */
18
+ export declare function beginPendingRoots(store: string, roots: string[]): Promise<PendingRoots>;
19
+ /**
20
+ * End a pending lease. Never throws: what the leased operation answered stays its
21
+ * answer, and its caller's remaining cleanup still runs. A record that cannot be
22
+ * removed (a directory that refuses the unlink) is not released: it stays, naming
23
+ * this process, and a prune counts it as it counts any record (see
24
+ * `livePendingRecords`), so it can only delay pruning. Returns that removal fault
25
+ * as the caller's cleanup diagnostic, else undefined.
26
+ */
27
+ export declare function endPendingRoots(pending: PendingRoots | null | undefined): unknown;
28
+ /**
29
+ * Take the prune lock atomically: the owner record is written into a private
30
+ * directory that becomes the lock by one rename, so no reader ever sees a lock
31
+ * without its owner. A stale lock (dead or reused-PID owner, or ownerless
32
+ * debris) is moved aside by one rename — of racing reapers only one moves it —
33
+ * and checked to be the lock judged stale before it is removed. Returns the
34
+ * hold, or null when another live pruner holds the lock.
35
+ */
36
+ export declare function acquirePruneLock(store: string): {
37
+ stillHeld: () => boolean;
38
+ release: () => void;
39
+ } | null;
40
+ export declare function livePendingRecords(store: string, graceMs: number): string[][];
41
+ //# sourceMappingURL=object-store-leases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-store-leases.d.ts","sourceRoot":"","sources":["../src/object-store-leases.ts"],"names":[],"mappings":"AAgCA,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9D;AA6DD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,uDAAuD;IACvD,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;CAC1C;AAKD;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAoC7F;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CASjF;AAqCD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,GACZ;IAAE,SAAS,EAAE,MAAM,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,GAAG,IAAI,CAmD1D;AAOD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAoC7E"}
@@ -0,0 +1,277 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, utimesSync, writeFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { setTimeout as sleep } from "node:timers/promises";
5
+ import { compareProcessBirth, defaultProcessIdentityService, isKnownProcessIdentity, WorkspaceError, } from "@claudexor/core";
6
+ import { noteCleanupFault } from "./cleanup-fault.js";
7
+ import { existsStrict } from "./fs-presence.js";
8
+ /*
9
+ * Leases of the project object store (#535): the pending records that keep a
10
+ * running import's objects from a concurrent prune, and the prune lock. Both
11
+ * name their process by PID and, where the platform reports one, its kernel
12
+ * birth, so a reused PID never keeps a lease alive.
13
+ */
14
+ /** The store's private control directories (pending, roots, tmp, the prune lock). */
15
+ export function controlDir(store, name) {
16
+ return join(store, "claudexor", name);
17
+ }
18
+ function processAlive(pid) {
19
+ try {
20
+ process.kill(pid, 0);
21
+ return true;
22
+ }
23
+ catch (error) {
24
+ return error.code === "EPERM";
25
+ }
26
+ }
27
+ function selfOwner() {
28
+ const identity = defaultProcessIdentityService.self();
29
+ return { pid: process.pid, ...(identity.status === "known" ? { identity } : {}) };
30
+ }
31
+ /**
32
+ * `alive`: the recorded process still runs (same PID and same kernel birth);
33
+ * `dead`: it exited, or its PID now names another process; `unverified`: the
34
+ * PID is in use but no birth was recorded or it cannot be read now.
35
+ */
36
+ function ownerState(owner) {
37
+ const pid = Number(owner.pid);
38
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !processAlive(pid))
39
+ return "dead";
40
+ if (!isKnownProcessIdentity(owner.identity))
41
+ return "unverified";
42
+ const birth = compareProcessBirth(owner.identity, defaultProcessIdentityService.read(pid));
43
+ if (birth === "same")
44
+ return "alive";
45
+ return birth === "unknown" ? "unverified" : "dead";
46
+ }
47
+ /** Milliseconds since `path` was last written; vanished paths are infinitely old. */
48
+ function ageOf(path) {
49
+ try {
50
+ return Date.now() - statSync(path).mtimeMs;
51
+ }
52
+ catch {
53
+ return Number.POSITIVE_INFINITY;
54
+ }
55
+ }
56
+ /** Keep `path`'s mtime fresh while its holder runs (the bounded liveness of an
57
+ * unverifiable owner); unref'd, so it never keeps a process alive. */
58
+ function renewWhileHeld(path, everyMs, held = () => true) {
59
+ const timer = setInterval(() => {
60
+ try {
61
+ if (!held())
62
+ return;
63
+ const now = new Date();
64
+ utimesSync(path, now, now);
65
+ }
66
+ catch {
67
+ // Gone: its holder released it.
68
+ }
69
+ }, everyMs);
70
+ timer.unref?.();
71
+ return timer;
72
+ }
73
+ /** Bound of a prune-lock wait before an import gives up. */
74
+ const PRUNE_WAIT_MS = 600_000;
75
+ /**
76
+ * Two-flag protocol with prune: an import first records its pending roots, then
77
+ * waits out a prune already holding the lock; a prune first takes the lock,
78
+ * then yields when any live pending record exists. One of the two always sees
79
+ * the other, so a prune never deletes an object a concurrent import relies on.
80
+ * The record names this process by PID and kernel birth: it stays live for as
81
+ * long as this process runs, however long the import takes.
82
+ */
83
+ export async function beginPendingRoots(store, roots) {
84
+ const dir = controlDir(store, "pending");
85
+ mkdirSync(dir, { recursive: true });
86
+ const path = join(dir, `${process.pid}-${randomUUID()}.json`);
87
+ writeFileSync(path, JSON.stringify({ ...selfOwner(), created_at: new Date().toISOString(), roots }) + "\n", { mode: 0o600 });
88
+ const pending = {
89
+ store,
90
+ path,
91
+ roots,
92
+ renewal: renewWhileHeld(path, PENDING_RENEW_MS),
93
+ };
94
+ try {
95
+ const lock = controlDir(store, "prune.lock");
96
+ // A holder verified once (by kernel birth) needs only a PID check per poll.
97
+ let verified;
98
+ const held = () => {
99
+ const owner = existsSync(lock) ? readLockOwner(lock) : null;
100
+ if (owner?.nonce && owner.nonce === verified && processAlive(Number(owner.pid)))
101
+ return true;
102
+ const live = pruneLockHeld(lock);
103
+ verified = live ? owner?.nonce : undefined;
104
+ return live;
105
+ };
106
+ for (let waited = 0; held(); waited += 100) {
107
+ if (waited > PRUNE_WAIT_MS)
108
+ throw new WorkspaceError("object store prune did not finish in time");
109
+ await sleep(100);
110
+ }
111
+ return pending;
112
+ }
113
+ catch (error) {
114
+ noteCleanupFault(error, endPendingRoots(pending));
115
+ throw error;
116
+ }
117
+ }
118
+ /**
119
+ * End a pending lease. Never throws: what the leased operation answered stays its
120
+ * answer, and its caller's remaining cleanup still runs. A record that cannot be
121
+ * removed (a directory that refuses the unlink) is not released: it stays, naming
122
+ * this process, and a prune counts it as it counts any record (see
123
+ * `livePendingRecords`), so it can only delay pruning. Returns that removal fault
124
+ * as the caller's cleanup diagnostic, else undefined.
125
+ */
126
+ export function endPendingRoots(pending) {
127
+ if (!pending)
128
+ return undefined;
129
+ if (pending.renewal)
130
+ clearInterval(pending.renewal);
131
+ try {
132
+ rmSync(pending.path, { force: true });
133
+ return undefined;
134
+ }
135
+ catch (error) {
136
+ return error;
137
+ }
138
+ }
139
+ function readLockOwner(lock) {
140
+ try {
141
+ const owner = JSON.parse(readFileSync(join(lock, "owner.json"), "utf8"));
142
+ return owner && typeof owner === "object" ? owner : null;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ /** A lock appears whole (owner included) by one rename; one without an owner
149
+ * is debris of an interrupted writer, held only while this young. */
150
+ const LOCK_SETTLE_MS = 60_000;
151
+ /** A pruner renews its lock this often; an unverifiable holder silent for
152
+ * LOCK_STALE_MS is gone (a verified live holder is never stale). */
153
+ const LOCK_RENEW_MS = 30_000;
154
+ const LOCK_STALE_MS = 10 * 60_000;
155
+ function pruneLockHeld(lock) {
156
+ if (!existsSync(lock))
157
+ return false;
158
+ const owner = readLockOwner(lock);
159
+ if (owner === null)
160
+ return ageOf(lock) < LOCK_SETTLE_MS;
161
+ const state = ownerState(owner);
162
+ return (state === "alive" || (state === "unverified" && ageOf(join(lock, "owner.json")) < LOCK_STALE_MS));
163
+ }
164
+ /** Errors meaning "the lock path is taken" (a directory rename onto an existing one). */
165
+ const LOCK_TAKEN = new Set(["EEXIST", "ENOTEMPTY", "EPERM", "EACCES", "EISDIR"]);
166
+ /**
167
+ * Take the prune lock atomically: the owner record is written into a private
168
+ * directory that becomes the lock by one rename, so no reader ever sees a lock
169
+ * without its owner. A stale lock (dead or reused-PID owner, or ownerless
170
+ * debris) is moved aside by one rename — of racing reapers only one moves it —
171
+ * and checked to be the lock judged stale before it is removed. Returns the
172
+ * hold, or null when another live pruner holds the lock.
173
+ */
174
+ export function acquirePruneLock(store) {
175
+ const lock = controlDir(store, "prune.lock");
176
+ const tmp = controlDir(store, "tmp");
177
+ mkdirSync(tmp, { recursive: true });
178
+ const nonce = randomUUID();
179
+ const staged = join(tmp, `prune-lock-${nonce}`);
180
+ mkdirSync(staged);
181
+ try {
182
+ writeFileSync(join(staged, "owner.json"), JSON.stringify({ ...selfOwner(), nonce }) + "\n");
183
+ for (let attempt = 0; attempt < 2; attempt += 1) {
184
+ // An existing (even empty) lock is never replaced by the rename below.
185
+ if (!existsSync(lock)) {
186
+ try {
187
+ renameSync(staged, lock);
188
+ const stillHeld = () => readLockOwner(lock)?.nonce === nonce;
189
+ const renewal = renewWhileHeld(join(lock, "owner.json"), LOCK_RENEW_MS, stillHeld);
190
+ return {
191
+ stillHeld,
192
+ release: () => {
193
+ clearInterval(renewal);
194
+ if (stillHeld())
195
+ rmSync(lock, { recursive: true, force: true });
196
+ },
197
+ };
198
+ }
199
+ catch (error) {
200
+ if (!LOCK_TAKEN.has(error.code ?? "") || !existsSync(lock))
201
+ throw error;
202
+ }
203
+ }
204
+ if (pruneLockHeld(lock))
205
+ return null;
206
+ const judged = readLockOwner(lock)?.nonce;
207
+ const aside = join(tmp, `prune-lock-stale-${randomUUID()}`);
208
+ try {
209
+ renameSync(lock, aside);
210
+ }
211
+ catch {
212
+ return null; // another pruner reaped or replaced it first
213
+ }
214
+ if (readLockOwner(aside)?.nonce !== judged) {
215
+ // A fresh lock replaced the stale one meanwhile: hand it back and yield.
216
+ try {
217
+ renameSync(aside, lock);
218
+ }
219
+ catch {
220
+ // The slot is taken again; that holder's own checks see the change.
221
+ }
222
+ return null;
223
+ }
224
+ rmSync(aside, { recursive: true, force: true });
225
+ }
226
+ return null;
227
+ }
228
+ finally {
229
+ rmSync(staged, { recursive: true, force: true });
230
+ }
231
+ }
232
+ /** Without a readable kernel birth a pending record is live this long after
233
+ * its last renewal, so a leaked record can delay pruning but never block it. */
234
+ const PENDING_MAX_AGE_MS = 60 * 60 * 1000;
235
+ const PENDING_RENEW_MS = 60_000;
236
+ export function livePendingRecords(store, graceMs) {
237
+ const dir = controlDir(store, "pending");
238
+ // Only ENOENT and ENOTDIR say there are none: an unreadable one may hold a live lease.
239
+ if (!existsStrict(dir))
240
+ return [];
241
+ const live = [];
242
+ for (const name of readdirSync(dir)) {
243
+ const path = join(dir, name);
244
+ try {
245
+ const record = JSON.parse(readFileSync(path, "utf8"));
246
+ const age = Date.now() - statSync(path).mtimeMs;
247
+ // A running import has no wall-clock limit: its record stays live while
248
+ // its own process (PID and kernel birth) runs. A dead owner's or a
249
+ // reused PID's record is reclaimed once past the grace window.
250
+ const state = ownerState(record);
251
+ if (age < graceMs ||
252
+ state === "alive" ||
253
+ (state === "unverified" && age < PENDING_MAX_AGE_MS))
254
+ live.push(record.roots ?? []);
255
+ else
256
+ unlinkSync(path);
257
+ }
258
+ catch {
259
+ // A record being written is young: it counts as live. An unreadable one
260
+ // past the maximum age is debris and must not block pruning forever.
261
+ const age = (() => {
262
+ try {
263
+ return Date.now() - statSync(path).mtimeMs;
264
+ }
265
+ catch {
266
+ return 0;
267
+ }
268
+ })();
269
+ if (age < PENDING_MAX_AGE_MS)
270
+ live.push([]);
271
+ else
272
+ rmSync(path, { force: true });
273
+ }
274
+ }
275
+ return live;
276
+ }
277
+ //# sourceMappingURL=object-store-leases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-store-leases.js","sourceRoot":"","sources":["../src/object-store-leases.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { GitDeliveryPair } from "@claudexor/schema";
2
+ /**
3
+ * Durable custody: a root record names the objects it keeps and the path that
4
+ * owns them (a run directory, a thread directory, an envelope, an anchor). It
5
+ * is live while its owner exists, so retention of the owner is retention of its
6
+ * objects, and roots are durable the moment an import is verified (no caller
7
+ * has to finish later). A run directory retention retired keeps only
8
+ * `tombstone.yaml`: its payload is gone, and so is its custody. A record appears
9
+ * whole by one rename: one torn by a crash or a full disk would refuse every
10
+ * later prune of its project.
11
+ */
12
+ export declare function writeRootRecord(store: string, owner: string, roots: string[], pair?: GitDeliveryPair): string;
13
+ /**
14
+ * The newest delivery pair kept under `owner` whose post-tree is `postTree`:
15
+ * a delivery retains its pair before it writes, so a replay that finds the
16
+ * result already applied (its caller died before recording it) recovers the
17
+ * pair revert needs. Null when no such record exists; a record directory that
18
+ * cannot be looked at throws, since it may hold that pair.
19
+ */
20
+ export declare function retainedPairOf(store: string, owner: string, postTree: string): GitDeliveryPair | null;
21
+ /** Roots of every record whose owner still holds custody; dead records are
22
+ * removed unless `dryRun`. `retired` names owners a retention pass retires now
23
+ * (a dry run has not written their tombstones, yet must count the same set). */
24
+ export declare function liveRootRecords(store: string, dryRun?: boolean, retired?: ReadonlySet<string>): string[];
25
+ //# sourceMappingURL=object-store-roots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-store-roots.d.ts","sourceRoot":"","sources":["../src/object-store-roots.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAMpD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,CAAC,EAAE,eAAe,GACrB,MAAM,CAuBR;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,eAAe,GAAG,IAAI,CAkBxB;AAcD;;gFAEgF;AAChF,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,UAAQ,EACd,OAAO,GAAE,WAAW,CAAC,MAAM,CAAa,GACvC,MAAM,EAAE,CAoBV"}
@@ -0,0 +1,114 @@
1
+ import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { GitDeliveryPair } from "@claudexor/schema";
5
+ import { WorkspaceError } from "@claudexor/core";
6
+ import { existsStrict } from "./fs-presence.js";
7
+ import { controlDir } from "./object-store-leases.js";
8
+ import { runFilesExpired } from "./run-expiry.js";
9
+ /**
10
+ * Durable custody: a root record names the objects it keeps and the path that
11
+ * owns them (a run directory, a thread directory, an envelope, an anchor). It
12
+ * is live while its owner exists, so retention of the owner is retention of its
13
+ * objects, and roots are durable the moment an import is verified (no caller
14
+ * has to finish later). A run directory retention retired keeps only
15
+ * `tombstone.yaml`: its payload is gone, and so is its custody. A record appears
16
+ * whole by one rename: one torn by a crash or a full disk would refuse every
17
+ * later prune of its project.
18
+ */
19
+ export function writeRootRecord(store, owner, roots, pair) {
20
+ const dir = controlDir(store, "roots");
21
+ const tmp = controlDir(store, "tmp");
22
+ mkdirSync(dir, { recursive: true });
23
+ mkdirSync(tmp, { recursive: true });
24
+ const name = `${Date.now()}-${process.pid}-${randomUUID()}.json`;
25
+ const staged = join(tmp, `root-${name}`);
26
+ try {
27
+ writeFileSync(staged, JSON.stringify({
28
+ owner,
29
+ roots,
30
+ created_at: new Date().toISOString(),
31
+ ...(pair ? { pair } : {}),
32
+ }) + "\n", { mode: 0o600 });
33
+ renameSync(staged, join(dir, name));
34
+ }
35
+ finally {
36
+ rmSync(staged, { force: true });
37
+ }
38
+ return join(dir, name);
39
+ }
40
+ /**
41
+ * The newest delivery pair kept under `owner` whose post-tree is `postTree`:
42
+ * a delivery retains its pair before it writes, so a replay that finds the
43
+ * result already applied (its caller died before recording it) recovers the
44
+ * pair revert needs. Null when no such record exists; a record directory that
45
+ * cannot be looked at throws, since it may hold that pair.
46
+ */
47
+ export function retainedPairOf(store, owner, postTree) {
48
+ const dir = controlDir(store, "roots");
49
+ if (!existsStrict(dir))
50
+ return null;
51
+ let newest = null;
52
+ for (const name of readdirSync(dir)) {
53
+ let record;
54
+ try {
55
+ record = JSON.parse(readFileSync(join(dir, name), "utf8"));
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ if (record.owner !== owner || record.pair === undefined)
61
+ continue;
62
+ const pair = GitDeliveryPair.safeParse(record.pair);
63
+ if (!pair.success || pair.data.post_tree !== postTree)
64
+ continue;
65
+ const at = Date.parse(String(record.created_at));
66
+ if (!newest || at >= newest.at)
67
+ newest = { at, pair: pair.data };
68
+ }
69
+ return newest?.pair ?? null;
70
+ }
71
+ /** An owner path that still holds custody: it exists and is not a retired run. */
72
+ function ownerHoldsCustody(owner, retired) {
73
+ if (retired.has(owner))
74
+ return false;
75
+ try {
76
+ return existsStrict(owner) && !runFilesExpired(owner);
77
+ }
78
+ catch {
79
+ // An owner whose presence or retirement cannot be read (only ENOENT and ENOTDIR
80
+ // say it is gone) keeps its objects.
81
+ return true;
82
+ }
83
+ }
84
+ /** Roots of every record whose owner still holds custody; dead records are
85
+ * removed unless `dryRun`. `retired` names owners a retention pass retires now
86
+ * (a dry run has not written their tombstones, yet must count the same set). */
87
+ export function liveRootRecords(store, dryRun = false, retired = new Set()) {
88
+ const dir = controlDir(store, "roots");
89
+ // A record directory that cannot be looked at may name live roots: it throws.
90
+ if (!existsStrict(dir))
91
+ return [];
92
+ const roots = [];
93
+ for (const name of readdirSync(dir)) {
94
+ const path = join(dir, name);
95
+ let record;
96
+ try {
97
+ record = JSON.parse(readFileSync(path, "utf8"));
98
+ }
99
+ catch {
100
+ // Unreadable custody is never treated as absent: keep everything it could name.
101
+ throw new WorkspaceError(`object store root record ${path} is unreadable; pruning refused`);
102
+ }
103
+ if (typeof record.owner === "string" && ownerHoldsCustody(record.owner, retired)) {
104
+ if (Array.isArray(record.roots))
105
+ for (const root of record.roots)
106
+ if (typeof root === "string")
107
+ roots.push(root);
108
+ }
109
+ else if (!dryRun)
110
+ unlinkSync(path);
111
+ }
112
+ return roots;
113
+ }
114
+ //# sourceMappingURL=object-store-roots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-store-roots.js","sourceRoot":"","sources":["../src/object-store-roots.ts"],"names":[],"mappings":"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"}