sealkeep 0.9.0 → 0.11.0

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 (274) hide show
  1. package/ARCHITECTURE.md +163 -14
  2. package/CHANGELOG.md +223 -1
  3. package/CONTROL_PLANE.md +2 -2
  4. package/LICENSE +1 -1
  5. package/README.md +108 -22
  6. package/THIRD_PARTY.md +2 -2
  7. package/THREAT_MODEL.md +23 -4
  8. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
  9. package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
  10. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
  11. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
  12. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
  13. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
  14. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
  15. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
  16. package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
  17. package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
  18. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
  19. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
  20. package/dist/site/index.html +1808 -1904
  21. package/dist/site/llms.txt +67 -0
  22. package/dist/site/trust/architecture-data-flow.html +53 -0
  23. package/dist/site/trust/audit-roadmap.html +37 -0
  24. package/dist/site/trust/deployment-responsibility.html +11 -0
  25. package/dist/site/trust/dpa-sample.html +30 -0
  26. package/dist/site/trust/release-provenance.html +21 -0
  27. package/dist/site/trust/subprocessors.html +15 -0
  28. package/dist/site/trust/threat-model.html +34 -0
  29. package/dist/site/trust/toms.html +41 -0
  30. package/dist/site/trust-document.css +32 -0
  31. package/dist/site/trust.html +73 -0
  32. package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
  33. package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
  34. package/dist/site/visual/index.html +18 -0
  35. package/dist/site.zip +0 -0
  36. package/dist/src/activity.d.ts +9 -0
  37. package/dist/src/activity.js +90 -1
  38. package/dist/src/adapters.d.ts +175 -5
  39. package/dist/src/adapters.js +961 -73
  40. package/dist/src/agent-context.d.ts +135 -0
  41. package/dist/src/agent-context.js +1059 -0
  42. package/dist/src/archive-copies.d.ts +47 -0
  43. package/dist/src/archive-copies.js +179 -0
  44. package/dist/src/audit.d.ts +1 -1
  45. package/dist/src/audit.js +29 -4
  46. package/dist/src/autopilot.d.ts +52 -7
  47. package/dist/src/autopilot.js +143 -25
  48. package/dist/src/background-bandwidth.d.ts +46 -0
  49. package/dist/src/background-bandwidth.js +301 -0
  50. package/dist/src/background-cpu.d.ts +82 -0
  51. package/dist/src/background-cpu.js +212 -0
  52. package/dist/src/background-worker-error.d.ts +12 -0
  53. package/dist/src/background-worker-error.js +18 -0
  54. package/dist/src/branding.d.ts +24 -1
  55. package/dist/src/branding.js +31 -1
  56. package/dist/src/bridge.d.ts +233 -0
  57. package/dist/src/bridge.js +604 -0
  58. package/dist/src/byte-stream.d.ts +91 -0
  59. package/dist/src/byte-stream.js +385 -0
  60. package/dist/src/chunk-store.d.ts +41 -8
  61. package/dist/src/chunk-store.js +161 -65
  62. package/dist/src/cli.js +1698 -163
  63. package/dist/src/cloud.d.ts +841 -31
  64. package/dist/src/cloud.js +3196 -277
  65. package/dist/src/context-background.d.ts +37 -0
  66. package/dist/src/context-background.js +309 -0
  67. package/dist/src/context-drain-child.d.ts +1 -0
  68. package/dist/src/context-drain-child.js +98 -0
  69. package/dist/src/context-reader.d.ts +118 -0
  70. package/dist/src/context-reader.js +447 -0
  71. package/dist/src/control-plane/auth.d.ts +32 -4
  72. package/dist/src/control-plane/auth.js +85 -24
  73. package/dist/src/control-plane/server.js +19 -6
  74. package/dist/src/control-plane.d.ts +17 -1
  75. package/dist/src/control-plane.js +32 -6
  76. package/dist/src/crypto.d.ts +1 -1
  77. package/dist/src/crypto.js +5 -5
  78. package/dist/src/daemon-lease.d.ts +70 -0
  79. package/dist/src/daemon-lease.js +420 -0
  80. package/dist/src/daemon.d.ts +94 -1
  81. package/dist/src/daemon.js +1082 -105
  82. package/dist/src/darwin-service-policy.d.ts +41 -0
  83. package/dist/src/darwin-service-policy.js +60 -0
  84. package/dist/src/dashboard-cli.js +15 -15
  85. package/dist/src/device-authorization.d.ts +37 -0
  86. package/dist/src/device-authorization.js +199 -0
  87. package/dist/src/device-enrollment.d.ts +91 -0
  88. package/dist/src/device-enrollment.js +349 -0
  89. package/dist/src/disk.d.ts +17 -12
  90. package/dist/src/disk.js +43 -17
  91. package/dist/src/doctor.d.ts +35 -1
  92. package/dist/src/doctor.js +316 -41
  93. package/dist/src/durable-ticket-lock.d.ts +24 -0
  94. package/dist/src/durable-ticket-lock.js +232 -0
  95. package/dist/src/enroll.d.ts +1 -1
  96. package/dist/src/enroll.js +13 -7
  97. package/dist/src/env.d.ts +10 -1
  98. package/dist/src/env.js +11 -3
  99. package/dist/src/errors.d.ts +8 -8
  100. package/dist/src/errors.js +6 -6
  101. package/dist/src/flush.d.ts +12 -0
  102. package/dist/src/flush.js +37 -0
  103. package/dist/src/heartbeat.d.ts +86 -12
  104. package/dist/src/heartbeat.js +415 -29
  105. package/dist/src/index-background-watchdog.d.ts +1 -0
  106. package/dist/src/index-background-watchdog.js +94 -0
  107. package/dist/src/index-background-work.d.ts +21 -0
  108. package/dist/src/index-background-work.js +25 -0
  109. package/dist/src/index-background.d.ts +64 -0
  110. package/dist/src/index-background.js +394 -0
  111. package/dist/src/index-build-child.d.ts +1 -0
  112. package/dist/src/index-build-child.js +109 -0
  113. package/dist/src/index-manifest.d.ts +52 -0
  114. package/dist/src/index-manifest.js +444 -0
  115. package/dist/src/index-publication-proof.d.ts +84 -0
  116. package/dist/src/index-publication-proof.js +380 -0
  117. package/dist/src/index-publication-state.d.ts +149 -0
  118. package/dist/src/index-publication-state.js +696 -0
  119. package/dist/src/index-publication-verifier.d.ts +89 -0
  120. package/dist/src/index-publication-verifier.js +341 -0
  121. package/dist/src/index-publish.d.ts +62 -0
  122. package/dist/src/index-publish.js +540 -0
  123. package/dist/src/index-scratch-cleanup.d.ts +19 -0
  124. package/dist/src/index-scratch-cleanup.js +166 -0
  125. package/dist/src/index-segment-types.d.ts +132 -0
  126. package/dist/src/index-segment-types.js +21 -0
  127. package/dist/src/index-segments.d.ts +9 -0
  128. package/dist/src/index-segments.js +516 -0
  129. package/dist/src/index-store.d.ts +123 -0
  130. package/dist/src/index-store.js +495 -0
  131. package/dist/src/index-sync.d.ts +91 -15
  132. package/dist/src/index-sync.js +286 -53
  133. package/dist/src/index-upgrade-publication.d.ts +30 -0
  134. package/dist/src/index-upgrade-publication.js +179 -0
  135. package/dist/src/integration-manager.d.ts +32 -0
  136. package/dist/src/integration-manager.js +394 -0
  137. package/dist/src/leakscan.js +1 -1
  138. package/dist/src/local-api.d.ts +56 -15
  139. package/dist/src/local-api.js +4979 -541
  140. package/dist/src/machine-settings.d.ts +51 -0
  141. package/dist/src/machine-settings.js +166 -0
  142. package/dist/src/managed-chunks.d.ts +5 -2
  143. package/dist/src/managed-chunks.js +14 -14
  144. package/dist/src/mcp-install.d.ts +11 -9
  145. package/dist/src/mcp-install.js +73 -28
  146. package/dist/src/mcp-workspace.d.ts +18 -0
  147. package/dist/src/mcp-workspace.js +50 -0
  148. package/dist/src/mcp.js +294 -25
  149. package/dist/src/migrate.js +27 -21
  150. package/dist/src/notify.d.ts +1 -1
  151. package/dist/src/notify.js +5 -5
  152. package/dist/src/offload.d.ts +201 -14
  153. package/dist/src/offload.js +1848 -140
  154. package/dist/src/onboarding.d.ts +8 -1
  155. package/dist/src/onboarding.js +4 -4
  156. package/dist/src/packages.d.ts +2 -2
  157. package/dist/src/packages.js +10 -2
  158. package/dist/src/passkey.d.ts +0 -1
  159. package/dist/src/passkey.js +2 -7
  160. package/dist/src/password-lock.d.ts +2 -2
  161. package/dist/src/password-lock.js +6 -6
  162. package/dist/src/paths.d.ts +2 -0
  163. package/dist/src/paths.js +2 -0
  164. package/dist/src/presence.d.ts +86 -0
  165. package/dist/src/presence.js +240 -0
  166. package/dist/src/progress-deadline.d.ts +21 -0
  167. package/dist/src/progress-deadline.js +91 -0
  168. package/dist/src/project-repair.d.ts +55 -0
  169. package/dist/src/project-repair.js +131 -0
  170. package/dist/src/providers/gcs.d.ts +28 -7
  171. package/dist/src/providers/gcs.js +35 -24
  172. package/dist/src/providers/gdrive.d.ts +71 -8
  173. package/dist/src/providers/gdrive.js +223 -50
  174. package/dist/src/providers/index.d.ts +11 -3
  175. package/dist/src/providers/index.js +30 -10
  176. package/dist/src/providers/s3.d.ts +30 -8
  177. package/dist/src/providers/s3.js +41 -30
  178. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  179. package/dist/src/providers/safe-storage-fetch.js +72 -0
  180. package/dist/src/queue.d.ts +136 -19
  181. package/dist/src/queue.js +862 -96
  182. package/dist/src/reclaim-transaction.d.ts +156 -0
  183. package/dist/src/reclaim-transaction.js +1027 -0
  184. package/dist/src/recovery-codes.d.ts +32 -0
  185. package/dist/src/recovery-codes.js +338 -0
  186. package/dist/src/recovery.js +12 -9
  187. package/dist/src/rehydrate.d.ts +25 -22
  188. package/dist/src/rehydrate.js +319 -23
  189. package/dist/src/restore.d.ts +57 -4
  190. package/dist/src/restore.js +272 -36
  191. package/dist/src/resume-stub.d.ts +92 -0
  192. package/dist/src/resume-stub.js +417 -0
  193. package/dist/src/retention.d.ts +98 -7
  194. package/dist/src/retention.js +1066 -62
  195. package/dist/src/rotate.js +3 -3
  196. package/dist/src/search.d.ts +566 -8
  197. package/dist/src/search.js +5897 -290
  198. package/dist/src/secrets.d.ts +51 -7
  199. package/dist/src/secrets.js +316 -24
  200. package/dist/src/service.d.ts +49 -11
  201. package/dist/src/service.js +776 -35
  202. package/dist/src/share.js +3 -3
  203. package/dist/src/shared-spaces.d.ts +98 -0
  204. package/dist/src/shared-spaces.js +214 -0
  205. package/dist/src/source-reader.d.ts +73 -0
  206. package/dist/src/source-reader.js +715 -0
  207. package/dist/src/spool.d.ts +1 -1
  208. package/dist/src/spool.js +1 -1
  209. package/dist/src/start-tui.js +2 -1
  210. package/dist/src/start.js +2 -2
  211. package/dist/src/storage-endpoint.d.ts +21 -0
  212. package/dist/src/storage-endpoint.js +122 -0
  213. package/dist/src/storage-setup.js +12 -12
  214. package/dist/src/storage-targets.d.ts +109 -6
  215. package/dist/src/storage-targets.js +975 -67
  216. package/dist/src/stream-to-cloud.d.ts +5 -1
  217. package/dist/src/stream-to-cloud.js +34 -14
  218. package/dist/src/sync-rules.d.ts +31 -6
  219. package/dist/src/sync-rules.js +153 -14
  220. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  221. package/dist/src/team-backfill-scheduling.js +33 -0
  222. package/dist/src/team-backfill.d.ts +116 -0
  223. package/dist/src/team-backfill.js +1429 -0
  224. package/dist/src/team-index-cache.d.ts +16 -0
  225. package/dist/src/team-index-cache.js +152 -0
  226. package/dist/src/team-offboarding.d.ts +38 -0
  227. package/dist/src/team-offboarding.js +1043 -0
  228. package/dist/src/team-presence.d.ts +127 -0
  229. package/dist/src/team-presence.js +904 -0
  230. package/dist/src/team-publication-policy.d.ts +20 -0
  231. package/dist/src/team-publication-policy.js +140 -0
  232. package/dist/src/team-realtime.d.ts +68 -0
  233. package/dist/src/team-realtime.js +816 -0
  234. package/dist/src/team-source-facts-cache.d.ts +23 -0
  235. package/dist/src/team-source-facts-cache.js +255 -0
  236. package/dist/src/trash.d.ts +1 -1
  237. package/dist/src/trash.js +2 -2
  238. package/dist/src/tui.js +11 -12
  239. package/dist/src/types.d.ts +173 -7
  240. package/dist/src/types.js +20 -0
  241. package/dist/src/ui-server.d.ts +163 -35
  242. package/dist/src/ui-server.js +712 -72
  243. package/dist/src/ui.d.ts +1 -2
  244. package/dist/src/ui.js +1 -2
  245. package/dist/src/upload.d.ts +27 -0
  246. package/dist/src/upload.js +383 -43
  247. package/dist/src/vault.d.ts +226 -30
  248. package/dist/src/vault.js +1776 -192
  249. package/dist/src/watcher.d.ts +7 -1
  250. package/dist/src/watcher.js +198 -55
  251. package/dist/src/worker.d.ts +27 -3
  252. package/dist/src/worker.js +274 -55
  253. package/package.json +33 -12
  254. package/scripts/native-reboot-rehearsal.mjs +90 -0
  255. package/web/app.js +6032 -343
  256. package/web/bootstrap.js +17 -0
  257. package/web/index.html +255 -57
  258. package/web/rail.js +317 -40
  259. package/web/retention.html +2 -2
  260. package/web/rules-view.js +188 -16
  261. package/web/sessions-view.js +485 -62
  262. package/web/sessions.html +2 -2
  263. package/web/setup-api.js +152 -29
  264. package/web/setup-logic.js +68 -9
  265. package/web/setup.html +113 -44
  266. package/web/setup.js +604 -71
  267. package/web/style.css +513 -98
  268. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  269. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
  273. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
  274. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
@@ -0,0 +1,417 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { link, lstat, open, realpath, rm, stat, utimes, writeFile } from "node:fs/promises";
3
+ import { constants as fsConstants } from "node:fs";
4
+ import { openForFlush } from "./flush.js";
5
+ import { homedir } from "node:os";
6
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { validateNoFollowPath } from "./reclaim-transaction.js";
8
+ /**
9
+ * A stable, model-visible marker. It deliberately lives in an ordinary Codex
10
+ * message rather than in a made-up rollout record type: old and new Codex
11
+ * builds may ignore fields they do not know, but the conversation loader has
12
+ * always understood user/assistant messages.
13
+ */
14
+ export const CODEX_RESUME_STUB_MARKER = "sealkeep://codex-resume-stub/v1";
15
+ const MAX_STUB_BYTES = 1024 * 1024;
16
+ const MAX_META_BYTES = 1024 * 1024;
17
+ const PREPARED_STUB_BASENAME = /^\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.sealkeep-resume-stub\.partial$/i;
18
+ const POINTER_OPEN_FLAGS = fsConstants.O_RDONLY
19
+ | (typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0)
20
+ | (typeof fsConstants.O_NONBLOCK === "number" ? fsConstants.O_NONBLOCK : 0);
21
+ function samePointerSnapshot(left, right) {
22
+ return right.isFile() && left.dev === right.dev && left.ino === right.ino && left.size === right.size
23
+ && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
24
+ }
25
+ /** A growing rollout must not turn a <=1 MiB stat into an unbounded readFile. */
26
+ async function readStablePointer(handle, path, before) {
27
+ if (!before.isFile() || before.size < 0n || before.size > BigInt(MAX_STUB_BYTES)) {
28
+ throw new Error(`Codex resume pointer candidate exceeds the regular-file read bound and was retained: ${path}`);
29
+ }
30
+ // One extra byte detects growth even on filesystems with coarse timestamps.
31
+ // The allocation and total bytes read remain bounded regardless of appends.
32
+ const buffer = Buffer.alloc(Number(before.size) + 1);
33
+ let offset = 0;
34
+ while (offset < buffer.length) {
35
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
36
+ if (!bytesRead)
37
+ break;
38
+ offset += bytesRead;
39
+ }
40
+ const after = await handle.stat({ bigint: true });
41
+ const named = await lstat(path, { bigint: true });
42
+ if (offset !== Number(before.size) || !samePointerSnapshot(before, after) || !samePointerSnapshot(before, named)) {
43
+ throw new Error(`Codex resume pointer candidate changed while it was validated and was retained: ${path}`);
44
+ }
45
+ return buffer.subarray(0, offset);
46
+ }
47
+ /**
48
+ * Only real, non-symlinked native Codex rollout locations participate.
49
+ *
50
+ * Lexical containment is insufficient here: a symlinked `sessions` directory
51
+ * can make a path spell `~/.codex/sessions/...` while naming an unrelated file
52
+ * elsewhere. Check every user-controlled path component with lstat, then check
53
+ * canonical containment as a second independent condition.
54
+ */
55
+ export async function isNativeCodexSessionPath(path, home = homedir()) {
56
+ return validateNativeCodexSessionPath(path, home, "file").then(() => true, () => false);
57
+ }
58
+ /** Canonical no-follow validation shared by reclaim and crash reconciliation. */
59
+ export async function validateNativeCodexSessionPath(path, home = homedir(), leaf = "file") {
60
+ const lexicalHome = resolve(home);
61
+ const suffix = relative(lexicalHome, resolve(path));
62
+ if (suffix === "" || suffix.startsWith(`..${sep}`) || suffix === ".." || isAbsolute(suffix)) {
63
+ throw new Error(`Codex session path is outside the selected home: ${path}`);
64
+ }
65
+ const canonicalHome = await realpath(lexicalHome).catch(() => null);
66
+ if (!canonicalHome)
67
+ throw new Error(`Codex home is unavailable: ${lexicalHome}`);
68
+ const candidate = join(canonicalHome, suffix);
69
+ await validateNoFollowPath(candidate, [join(canonicalHome, ".codex", "sessions"), join(canonicalHome, ".codex", "archived_sessions")], { leaf });
70
+ return resolve(path);
71
+ }
72
+ async function firstLine(path) {
73
+ const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
74
+ const handle = await open(path, fsConstants.O_RDONLY | noFollow);
75
+ try {
76
+ const chunks = [];
77
+ let offset = 0;
78
+ while (offset <= MAX_META_BYTES) {
79
+ const chunk = Buffer.alloc(Math.min(64 * 1024, MAX_META_BYTES + 1 - offset));
80
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, offset);
81
+ if (bytesRead === 0)
82
+ break;
83
+ const part = chunk.subarray(0, bytesRead);
84
+ const newline = part.indexOf(0x0a);
85
+ if (newline !== -1) {
86
+ chunks.push(part.subarray(0, newline));
87
+ return Buffer.concat(chunks).toString("utf8");
88
+ }
89
+ chunks.push(part);
90
+ offset += bytesRead;
91
+ }
92
+ if (offset > MAX_META_BYTES)
93
+ throw new Error(`Codex session metadata exceeds ${MAX_META_BYTES} bytes`);
94
+ return Buffer.concat(chunks).toString("utf8");
95
+ }
96
+ finally {
97
+ await handle.close();
98
+ }
99
+ }
100
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
101
+ const UUID_IN_NAME = /(?:^|[^0-9a-f])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?=$|[^0-9a-f])/ig;
102
+ /** The native filename, not a parent directory or a longer hex token, owns the session id. */
103
+ export function codexSessionIdMatchesFilename(path, sessionId) {
104
+ // Index rows are searchable across operating systems, so recognize both
105
+ // separator styles even when this process did not create the path.
106
+ const filename = path.split(/[\\/]/).at(-1) ?? basename(path);
107
+ return [...filename.matchAll(UUID_IN_NAME)]
108
+ .some((match) => match[1]?.toLowerCase() === sessionId.toLowerCase());
109
+ }
110
+ function parseSessionMeta(line, path, options = {}) {
111
+ let value;
112
+ try {
113
+ value = JSON.parse(line);
114
+ }
115
+ catch {
116
+ throw new Error(`Codex session ${path} has unreadable session metadata; keeping the original on disk`);
117
+ }
118
+ const payloadId = value?.payload?.id;
119
+ const payloadSessionId = value?.payload?.session_id;
120
+ const source = value?.payload?.source;
121
+ const subagentThreadSpawn = source && typeof source === "object" && !Array.isArray(source)
122
+ && source.subagent
123
+ && typeof source.subagent === "object"
124
+ && !Array.isArray(source.subagent)
125
+ && (source.subagent.thread_spawn)
126
+ && typeof (source.subagent.thread_spawn) === "object"
127
+ && !Array.isArray(source.subagent.thread_spawn)
128
+ ? source.subagent.thread_spawn
129
+ : null;
130
+ if (subagentThreadSpawn) {
131
+ // Direct resume is unproven for every structurally recognized child—not
132
+ // just today's dual-id shape. Installed Codex v0.152 rejects a real v2
133
+ // child until its parent is loaded, so variants with one/equal IDs must
134
+ // remain full transcripts until a parent-first protocol is proven.
135
+ const parentThreadId = subagentThreadSpawn.parent_thread_id;
136
+ const recognized = typeof payloadId === "string" && UUID.test(payloadId)
137
+ && typeof payloadSessionId === "string" && UUID.test(payloadSessionId)
138
+ && typeof parentThreadId === "string" && UUID.test(parentThreadId)
139
+ && parentThreadId.toLowerCase() === payloadSessionId.toLowerCase();
140
+ if (!options.allowRecognizedSubagent || !recognized) {
141
+ throw new Error(`Codex subagent session ${path} cannot be independently resumed by the installed Codex; keeping the original on disk`);
142
+ }
143
+ }
144
+ if (typeof payloadId === "string" && typeof payloadSessionId === "string"
145
+ && payloadId.toLowerCase() !== payloadSessionId.toLowerCase()
146
+ && !subagentThreadSpawn) {
147
+ throw new Error(`Codex session ${path} has conflicting session ids; keeping the original on disk`);
148
+ }
149
+ const id = payloadId ?? payloadSessionId;
150
+ const cwd = value?.payload?.cwd;
151
+ const rawTimestamp = value.timestamp ?? value?.payload?.timestamp;
152
+ if (value?.type !== "session_meta" || typeof id !== "string" || !UUID.test(id)
153
+ || typeof cwd !== "string" || !cwd.trim()
154
+ || typeof rawTimestamp !== "string" || !Number.isFinite(Date.parse(rawTimestamp))) {
155
+ throw new Error(`Codex session ${path} has unsupported session metadata; keeping the original on disk`);
156
+ }
157
+ // The catalog and the rollout must agree. A pointer under the wrong UUID
158
+ // would make `codex resume <id>` open another session, which is worse than
159
+ // declining to reclaim a file.
160
+ if (!codexSessionIdMatchesFilename(path, id)) {
161
+ throw new Error(`Codex session id ${id} does not match an exact UUID token in its rollout filename; keeping the original on disk`);
162
+ }
163
+ const ordinal = typeof value.ordinal === "number" && Number.isSafeInteger(value.ordinal) && value.ordinal >= 0
164
+ ? value.ordinal
165
+ : undefined;
166
+ const relatedProcessSessionIds = [id];
167
+ if (typeof payloadSessionId === "string" && UUID.test(payloadSessionId)
168
+ && !relatedProcessSessionIds.some((candidate) => candidate.toLowerCase() === payloadSessionId.toLowerCase())) {
169
+ relatedProcessSessionIds.push(payloadSessionId);
170
+ }
171
+ return { sessionId: id, relatedProcessSessionIds, timestamp: rawTimestamp, cwd, ...(ordinal === undefined ? {} : { ordinal }) };
172
+ }
173
+ /** Validates that replacing this exact native rollout with a pointer is supported. */
174
+ export async function assertCodexResumePointerSupported(path, home = homedir()) {
175
+ const canonical = await validateNativeCodexSessionPath(path, home, "file");
176
+ await assertCodexResumePointerMetadataSupported(canonical, canonical);
177
+ }
178
+ /**
179
+ * Re-validates a staged original against its logical native filename.
180
+ * Crash reconciliation uses this after upgrades: staging lives inside the
181
+ * vault, but the rollout UUID contract still belongs to the original path.
182
+ */
183
+ export async function assertCodexResumePointerMetadataSupported(contentsPath, logicalNativePath) {
184
+ parseSessionMeta(await firstLine(resolve(contentsPath)), resolve(logicalNativePath));
185
+ }
186
+ /**
187
+ * A reclaim journal may outlive the binary that wrote it. Do not let such a
188
+ * journal turn an arbitrary neighbouring file into a replacement temp: only
189
+ * the private basename generated by prepareCodexResumeStub is eligible.
190
+ */
191
+ export function assertCodexResumePointerTempPath(tempPath) {
192
+ if (!PREPARED_STUB_BASENAME.test(basename(resolve(tempPath)))) {
193
+ throw new Error(`Codex resume pointer temp has an unsupported name and was retained: ${tempPath}`);
194
+ }
195
+ }
196
+ function parseUntouchedPointerContents(raw, logicalNativePath, options = {}) {
197
+ const lines = raw.endsWith("\n") ? raw.slice(0, -1).split("\n") : raw.split("\n");
198
+ if (lines.length !== 3)
199
+ return null;
200
+ try {
201
+ const meta = parseSessionMeta(lines[0], resolve(logicalNativePath), options);
202
+ const message = JSON.parse(lines[1]);
203
+ const content = Array.isArray(message?.payload?.content) ? message.payload.content : [];
204
+ const text = content.length === 1 && content[0] && typeof content[0] === "object"
205
+ ? content[0].text
206
+ : null;
207
+ if (message.type !== "response_item" || message.payload?.type !== "message" || message.payload.role !== "user"
208
+ || typeof text !== "string" || !text.startsWith(`${CODEX_RESUME_STUB_MARKER}\n`))
209
+ return null;
210
+ const match = / as archive ([0-9a-f-]{36}), then reclaimed/.exec(text);
211
+ if (!match)
212
+ return null;
213
+ const final = JSON.parse(lines[2]);
214
+ if (final.type !== "response_item" || final.payload?.type !== "message" || final.payload.role !== "assistant")
215
+ return null;
216
+ return { version: 1, archiveId: match[1], sessionId: meta.sessionId };
217
+ }
218
+ catch {
219
+ return null;
220
+ }
221
+ }
222
+ /**
223
+ * Return true only when an existing pathname is the exact journaled pointer
224
+ * and its structured contents bind it to this native session and archive.
225
+ * A missing or fingerprint-mismatched pathname returns false because generic
226
+ * reconciliation already retains it. A matching-but-invalid pathname throws:
227
+ * deleting or publishing it based only on a journal-supplied hash would let a
228
+ * corrupt journal target an unrelated neighbouring file.
229
+ */
230
+ export async function validateCodexResumePointerBinding(contentsPath, logicalNativePath, archiveId, expected) {
231
+ let handle;
232
+ try {
233
+ handle = await open(resolve(contentsPath), POINTER_OPEN_FLAGS);
234
+ }
235
+ catch (error) {
236
+ if (error.code === "ENOENT")
237
+ return false;
238
+ throw error;
239
+ }
240
+ try {
241
+ const before = await handle.stat({ bigint: true });
242
+ if (!before.isFile())
243
+ throw new Error(`Codex resume pointer candidate is not a regular file and was retained: ${contentsPath}`);
244
+ if (before.size !== BigInt(expected.bytes))
245
+ return false;
246
+ if (before.size > BigInt(MAX_STUB_BYTES)) {
247
+ throw new Error(`Codex resume pointer candidate exceeds ${MAX_STUB_BYTES} bytes and was retained: ${contentsPath}`);
248
+ }
249
+ const rawBuffer = await readStablePointer(handle, contentsPath, before);
250
+ const sha256 = createHash("sha256").update(rawBuffer).digest("hex");
251
+ if (sha256 !== expected.sha256)
252
+ return false;
253
+ // Binding is not an eligibility decision. An older release could have
254
+ // generated a structurally valid child pointer before direct child resume
255
+ // was disproven. Recognize only that narrow, internally consistent shape
256
+ // here so reconciliation may safely remove the pointer while restoring the
257
+ // full child rollout; normal pointer creation/reading still rejects it.
258
+ const pointer = parseUntouchedPointerContents(rawBuffer.toString("utf8"), logicalNativePath, {
259
+ allowRecognizedSubagent: true,
260
+ });
261
+ if (!pointer || pointer.archiveId.toLowerCase() !== archiveId.toLowerCase()) {
262
+ throw new Error(`Codex resume pointer candidate is not bound to archive ${archiveId} and was retained: ${contentsPath}`);
263
+ }
264
+ return true;
265
+ }
266
+ finally {
267
+ await handle.close();
268
+ }
269
+ }
270
+ /** Own and validated parent IDs that may identify a live Codex process. */
271
+ export async function codexSessionProcessIds(path) {
272
+ return parseSessionMeta(await firstLine(path), resolve(path)).relatedProcessSessionIds;
273
+ }
274
+ /** IDs plus project cwd used when a live Codex process has no UUID in argv. */
275
+ export async function codexSessionProcessIdentity(path) {
276
+ const meta = parseSessionMeta(await firstLine(path), resolve(path));
277
+ return { ids: meta.relatedProcessSessionIds, cwd: meta.cwd };
278
+ }
279
+ function inputMessage(timestamp, text, ordinal) {
280
+ return {
281
+ ...(ordinal === undefined ? {} : { ordinal }),
282
+ timestamp,
283
+ type: "response_item",
284
+ payload: { type: "message", role: "user", content: [{ type: "input_text", text }] },
285
+ };
286
+ }
287
+ function outputMessage(timestamp, text, ordinal) {
288
+ return {
289
+ ...(ordinal === undefined ? {} : { ordinal }),
290
+ timestamp,
291
+ type: "response_item",
292
+ payload: { type: "message", role: "assistant", phase: "final_answer", content: [{ type: "output_text", text }] },
293
+ };
294
+ }
295
+ /**
296
+ * Builds the replacement beside the source, while the original is still
297
+ * present and fully verified. The caller transaction-stages the original first
298
+ * and then publishes this prepared file into its native path without
299
+ * overwriting a path the agent recreated in the meantime.
300
+ *
301
+ * The original session-meta line is kept byte-for-byte. It is the format
302
+ * contract of the installed Codex build and carries the stable id, cwd, git
303
+ * project, source kind and original timestamp. Even a large modern metadata
304
+ * line is tiny beside a multi-GB rollout and avoids guessing at vendor fields.
305
+ */
306
+ export async function prepareCodexResumeStub(sourcePath, archiveId, options = {}) {
307
+ const path = resolve(sourcePath);
308
+ if (!(await isNativeCodexSessionPath(path, options.home)))
309
+ return null;
310
+ const sourceStat = await stat(path);
311
+ if (!sourceStat.isFile())
312
+ throw new Error(`Codex session ${path} is not a regular file; keeping it on disk`);
313
+ const metaLine = await firstLine(path);
314
+ const meta = parseSessionMeta(metaLine, path);
315
+ const project = options.project?.trim();
316
+ const scope = project ? ` for project ${JSON.stringify(project)}` : "";
317
+ const pointer = [
318
+ CODEX_RESUME_STUB_MARKER,
319
+ `Sealkeep preserved a searchable snapshot of Codex session ${meta.sessionId}${scope} as archive ${archiveId}, then reclaimed only the bulky local source. Other snapshots of this session may have different archive ids.`,
320
+ `Use the local MCP tool \`sealkeep_search\` with \`{"query":"<current question>","agent":"codex","sessionId":"${meta.sessionId}"}\` to retrieve relevant knowledge across every preserved snapshot of this Codex session.`,
321
+ "For a matching result, pass its context object unchanged to `sealkeep_read_context` to read the cited source passage, actual user prompt when recorded, and neighboring messages. Follow its continuation with the same source if needed; a search snippet is not the full earlier conversation.",
322
+ `To restore this exact archived snapshot, run \`sealkeep recover ${archiveId} --native --overwrite backup\`, then run \`codex resume ${meta.sessionId}\` again.`,
323
+ "Do not claim that earlier context was recovered until the search or restore returns it.",
324
+ ].join("\n");
325
+ const display = [
326
+ "This session's preserved history is searchable in Sealkeep.",
327
+ "This lightweight resume intentionally keeps archived snapshots off disk. Use Sealkeep MCP search now, or run the exact restore command above and resume the same session again when you need every byte from that snapshot.",
328
+ ].join(" ");
329
+ const contents = `${metaLine}\n${JSON.stringify(inputMessage(meta.timestamp, pointer, meta.ordinal === undefined ? undefined : meta.ordinal + 1))}\n${JSON.stringify(outputMessage(meta.timestamp, display, meta.ordinal === undefined ? undefined : meta.ordinal + 2))}\n`;
330
+ if (Buffer.byteLength(contents) > MAX_STUB_BYTES) {
331
+ throw new Error(`Codex resume pointer for ${path} exceeds ${MAX_STUB_BYTES} bytes; keeping the original on disk`);
332
+ }
333
+ const tempPath = join(dirname(path), `.${randomUUID()}.sealkeep-resume-stub.partial`);
334
+ try {
335
+ await writeFile(tempPath, contents, { encoding: "utf8", mode: 0o600, flag: "wx" });
336
+ await utimes(tempPath, sourceStat.atime, sourceStat.mtime);
337
+ if (options.syncPreparedFile) {
338
+ await options.syncPreparedFile(tempPath);
339
+ }
340
+ else {
341
+ const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
342
+ const { handle } = await openForFlush(tempPath, noFollow);
343
+ try {
344
+ const info = await handle.stat();
345
+ if (!info.isFile())
346
+ throw new Error(`Codex resume pointer temp is not a regular file: ${tempPath}`);
347
+ await handle.sync();
348
+ }
349
+ finally {
350
+ await handle.close();
351
+ }
352
+ }
353
+ }
354
+ catch (error) {
355
+ await rm(tempPath, { force: true }).catch(() => undefined);
356
+ throw error;
357
+ }
358
+ return {
359
+ version: 1,
360
+ archiveId,
361
+ sessionId: meta.sessionId,
362
+ path,
363
+ bytes: Buffer.byteLength(contents),
364
+ sha256: createHash("sha256").update(contents).digest("hex"),
365
+ tempPath,
366
+ atime: sourceStat.atime,
367
+ mtime: sourceStat.mtime,
368
+ };
369
+ }
370
+ /** Installs an already-prepared pointer without clobbering a recreated path. */
371
+ export async function installCodexResumeStub(stub) {
372
+ // `rename(temp, path)` overwrites on POSIX. Codex can recreate its rollout
373
+ // after the staging move but before this call; a hard-link publication is the
374
+ // same-filesystem atomic create-if-absent primitive and returns EEXIST rather
375
+ // than discarding those newly written bytes. The extra temp link is harmless
376
+ // after a crash and is removed best-effort once the native name is durable.
377
+ await link(stub.tempPath, stub.path);
378
+ await rm(stub.tempPath, { force: true }).catch(() => undefined);
379
+ }
380
+ export async function discardCodexResumeStub(stub) {
381
+ if (stub)
382
+ await rm(stub.tempPath, { force: true }).catch(() => undefined);
383
+ }
384
+ /**
385
+ * Recognises only the complete, untouched pointer format. Once Codex appends
386
+ * a new turn there are more than the three expected lines, so the worker
387
+ * archives that new work normally instead of permanently excluding the file.
388
+ */
389
+ export async function readUntouchedCodexResumeStub(path) {
390
+ let handle;
391
+ try {
392
+ // lstat before open also avoids opening a FIFO or other special file.
393
+ const named = await lstat(path, { bigint: true });
394
+ if (!named.isFile() || named.size <= 0n || named.size > BigInt(MAX_STUB_BYTES))
395
+ return null;
396
+ // Nonblocking is ignored for regular files, but prevents a FIFO swapped
397
+ // into the pathname after lstat from hanging the background worker.
398
+ handle = await open(path, POINTER_OPEN_FLAGS);
399
+ const before = await handle.stat({ bigint: true });
400
+ if (!samePointerSnapshot(named, before))
401
+ return null;
402
+ const raw = await readStablePointer(handle, path, before);
403
+ const pointer = parseUntouchedPointerContents(raw.toString("utf8"), path);
404
+ return pointer ? {
405
+ ...pointer,
406
+ path: resolve(path),
407
+ bytes: raw.length,
408
+ sha256: createHash("sha256").update(raw).digest("hex"),
409
+ } : null;
410
+ }
411
+ catch {
412
+ return null;
413
+ }
414
+ finally {
415
+ await handle?.close();
416
+ }
417
+ }
@@ -1,6 +1,10 @@
1
1
  import { type ActivityOptions } from "./activity.js";
2
- import { type TrashResult } from "./trash.js";
3
- import { type VaultConfig } from "./types.js";
2
+ import { recordAudit } from "./audit.js";
3
+ import { moveToTrash, type TrashResult } from "./trash.js";
4
+ import { type ArchiveRecord, type ArchiveRecordV2, type RemoteObject, type VaultConfig } from "./types.js";
5
+ import { mutateArchiveRecord } from "./vault.js";
6
+ import { installCodexResumeStub } from "./resume-stub.js";
7
+ import { type VerifiedManagedAccountIndexPublication } from "./index-publication-verifier.js";
4
8
  export type RetentionPolicy = "local-only" | "sync-only" | "archive-and-reclaim" | "manual-approval";
5
9
  export type RetentionSettings = {
6
10
  policy: RetentionPolicy;
@@ -44,6 +48,8 @@ export type Candidate = {
44
48
  agent: string;
45
49
  path: string;
46
50
  bytes: number;
51
+ /** Filesystem blocks attributed to the source inode at preview time. */
52
+ allocatedBytes: number;
47
53
  createdAt: string;
48
54
  eligible: boolean;
49
55
  /** Every reason this source is still protected. Empty exactly when eligible. */
@@ -60,11 +66,68 @@ export type RetentionOptions = {
60
66
  now?: number;
61
67
  activity?: ActivityOptions;
62
68
  settings?: Partial<RetentionSettings>;
69
+ /** Set by the unattended daemon; foreground/manual actions leave it unset. */
70
+ backgroundCpuTarget?: number;
71
+ /** Aggregate daemon transfer ceiling for full remote identity proof. */
72
+ backgroundRemoteBytesPerSecond?: number;
73
+ signal?: AbortSignal;
74
+ /** Selected home for native Codex/Claude path and resume-format validation. */
75
+ home?: string;
76
+ /** Explicit unlock for foreground use; background work normally uses the OS keystore. */
77
+ phrase?: string;
78
+ };
79
+ /**
80
+ * Narrow operation seams for deterministic failure testing of the destructive
81
+ * boundary. Production callers leave this absent and get the real atomic
82
+ * filesystem, record-lock, and append-only audit operations.
83
+ */
84
+ export type ReclaimOperations = {
85
+ moveToTrash?: typeof moveToTrash;
86
+ installResumeStub?: typeof installCodexResumeStub;
87
+ mutateRecord?: typeof mutateArchiveRecord;
88
+ audit?: typeof recordAudit;
89
+ /** Test/embedding seam; production asks the recorded provider immediately before purge. */
90
+ checkRemoteCopy?: (dataDir: string, archiveId: string, copyId: string) => Promise<{
91
+ ok: boolean;
92
+ detail: string;
93
+ }>;
94
+ /**
95
+ * Stream seam that keeps the production byte-count/SHA-256 proof intact.
96
+ * Unlike checkRemoteCopy, returning a stream here does not assert success.
97
+ */
98
+ fetchRemoteCopy?: (dataDir: string, record: ArchiveRecordV2, copy: RemoteObject, signal?: AbortSignal) => Promise<AsyncIterable<Buffer>>;
99
+ /** Deterministic seam for cooperative hashing; daemon uses the 30% lane. */
100
+ cooperate?: () => Promise<void>;
101
+ /** Exact managed-index proof seam. Production always uses the real verifier. */
102
+ verifyIndexPublication?: (dataDir: string, chain: readonly ArchiveRecord[], requestedHead: ArchiveRecord, phrase: string, signal?: AbortSignal,
103
+ /** Reclaimed snapshots belonging to the same exact native Codex session. */
104
+ searchableHeads?: readonly ArchiveRecord[]) => Promise<VerifiedManagedAccountIndexPublication>;
105
+ /** Cheap exact-head recheck seam used immediately before unlink. */
106
+ recheckIndexPublication?: (dataDir: string, verification: VerifiedManagedAccountIndexPublication, signal?: AbortSignal) => Promise<void>;
63
107
  };
108
+ /**
109
+ * A live permission check for source deletion. Returning anything except
110
+ * literal `true`, or failing to read the current policy, keeps the original.
111
+ * The daemon supplies its machine-local gate; foreground entry points derive a
112
+ * command-scoped gate so crash reconciliation cannot borrow authority from an
113
+ * unrelated command or from a preview.
114
+ */
115
+ export type ReclaimAuthorization = (archiveId: string) => Promise<boolean>;
64
116
  export declare function retentionSettings(config: VaultConfig, overrides?: Partial<RetentionSettings>): RetentionSettings;
65
117
  export declare function readApprovals(dataDir: string): Promise<string[]>;
66
118
  /** Manual-approval policy: an operator names the archives whose sources may be reclaimed. */
67
119
  export declare function approveReclamation(dataDir: string, archiveIds: string[]): Promise<string[]>;
120
+ /**
121
+ * The standing "never take this one" list, honoured by EVERY reclaimer.
122
+ *
123
+ * `<dataDir>/never-reclaim` (one id or path fragment per line, `#` comments)
124
+ * was read by exactly one of the four deleters — the squeeze command — so a
125
+ * pinned session survived a squeeze and was then reclaimed by the daemon's
126
+ * ordinary retention tick, with no user in the loop. A pin only one deleter
127
+ * honours is a hole; it lives here now so every caller inherits it.
128
+ */
129
+ export declare function neverReclaimMarks(dataDir: string): Promise<string[]>;
130
+ export declare function isPinned(path: string, marks: string[]): boolean;
68
131
  /**
69
132
  * Decides, per archive, whether its local source may be reclaimed.
70
133
  *
@@ -86,26 +149,44 @@ export type ReclaimOutcome = {
86
149
  id: string;
87
150
  path: string;
88
151
  bytes: number;
152
+ /** Legacy recoverable-trash destination. Empty after permanent purge. */
89
153
  movedTo: string;
90
- strategy: TrashResult["strategy"];
154
+ strategy: TrashResult["strategy"] | "purged";
155
+ /** Allocated blocks released from the source inode, net of its pointer. */
156
+ freedBytes: number;
157
+ /** Present only once the durable staged original has been permanently purged. */
158
+ purgedAt?: string;
159
+ /** A tiny Codex-native pointer now occupies `path`, so `codex resume <id>` remains discoverable. */
160
+ resumeStub?: {
161
+ sessionId: string;
162
+ bytes: number;
163
+ };
91
164
  };
165
+ /** Resolve kill -9 windows before starting another native source reclaim. */
166
+ export declare function reconcileCodexReclaims(dataDir: string, storageRoot: string, home?: string, remoteCheck?: ReclaimOperations["checkRemoteCopy"], cooperate?: () => Promise<void>, activity?: ActivityOptions, signal?: AbortSignal, remoteBytesPerSecond?: number, remoteFetch?: ReclaimOperations["fetchRemoteCopy"], authorizeReclaim?: ReclaimAuthorization, phrase?: string, indexOperations?: Pick<ReclaimOperations, "verifyIndexPublication" | "recheckIndexPublication">): Promise<void>;
92
167
  /**
93
168
  * Reclaims eligible sources. Dry-run is the default: without `confirm`, nothing on
94
- * disk changes. Reclaimed files go to the OS trash, never to `unlink`.
169
+ * disk changes. Confirmed, verified sources are transaction-staged and then
170
+ * permanently purged; Codex receives a tiny same-id resume pointer.
95
171
  */
96
172
  export declare function applyRetention(dataDir: string, options?: RetentionOptions & {
97
173
  confirm?: boolean;
98
174
  home?: string;
99
175
  strategy?: TrashResult["strategy"];
176
+ operations?: ReclaimOperations;
177
+ /** Set only by the installed daemon; checked repeatedly during a long pass. */
178
+ authorizeReclaim?: ReclaimAuthorization;
100
179
  }): Promise<{
101
180
  mode: "dry-run";
102
181
  policy: RetentionPolicy;
103
182
  wouldReclaim: number;
104
183
  wouldFreeBytes: number;
184
+ wouldMoveBytes: number;
105
185
  candidates: Candidate[];
106
186
  reclaimed?: undefined;
107
187
  failed?: undefined;
108
188
  freedBytes?: undefined;
189
+ movedBytes?: undefined;
109
190
  protected?: undefined;
110
191
  } | {
111
192
  mode: "apply";
@@ -116,9 +197,11 @@ export declare function applyRetention(dataDir: string, options?: RetentionOptio
116
197
  error: string;
117
198
  }[];
118
199
  freedBytes: number;
200
+ movedBytes: number;
119
201
  protected: number;
120
202
  wouldReclaim?: undefined;
121
203
  wouldFreeBytes?: undefined;
204
+ wouldMoveBytes?: undefined;
122
205
  candidates?: undefined;
123
206
  }>;
124
207
  /**
@@ -132,13 +215,16 @@ export declare function applyRetention(dataDir: string, options?: RetentionOptio
132
215
  *
133
216
  * - the archive must have a verified remote copy (and a delta head's whole
134
217
  * chain must be verified — one unverified base and the cloud cannot
135
- * reproduce the file this is about to trash);
218
+ * reproduce the file this is about to reclaim);
219
+ * - this exact archive must be covered by the search index, so durable memory
220
+ * is also memory a fresh agent can actually find;
136
221
  * - the source must be byte-for-byte the file that was archived — a session
137
222
  * that grew since has content in no archive at all;
138
223
  * - nothing may have the file open right now.
139
224
  *
140
- * Policy, age and grace are deliberately not consulted. Trash, never unlink,
141
- * same as everywhere.
225
+ * Policy, age and grace are deliberately not consulted. The explicit click is
226
+ * the irreversible-delete approval; all durability/search/activity gates still
227
+ * run immediately before the transaction commits.
142
228
  */
143
229
  export declare function reclaimOne(dataDir: string, archiveId: string, options?: {
144
230
  confirm?: boolean;
@@ -146,6 +232,9 @@ export declare function reclaimOne(dataDir: string, archiveId: string, options?:
146
232
  strategy?: TrashResult["strategy"];
147
233
  now?: number;
148
234
  activity?: ActivityOptions;
235
+ operations?: ReclaimOperations;
236
+ phrase?: string;
237
+ signal?: AbortSignal;
149
238
  }): Promise<ReclaimOutcome>;
150
239
  /**
151
240
  * `reclaimOne` over a selection. Each row passes or is kept with its reason —
@@ -158,6 +247,7 @@ export declare function reclaimBatch(dataDir: string, ids: readonly string[], op
158
247
  strategy?: TrashResult["strategy"];
159
248
  now?: number;
160
249
  activity?: ActivityOptions;
250
+ operations?: ReclaimOperations;
161
251
  }): Promise<{
162
252
  reclaimed: ReclaimOutcome[];
163
253
  kept: {
@@ -165,6 +255,7 @@ export declare function reclaimBatch(dataDir: string, ids: readonly string[], op
165
255
  reason: string;
166
256
  }[];
167
257
  freedBytes: number;
258
+ movedBytes: number;
168
259
  }>;
169
260
  export declare function setRetentionPolicy(dataDir: string, settings: Partial<RetentionSettings>): Promise<VaultConfig>;
170
261
  export type RedundantArchive = {