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
@@ -1,18 +1,21 @@
1
- import { mkdir, rm, stat, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { join } from "node:path";
4
- import { detectAgents } from "./adapters.js";
5
- import { isVaultlineError } from "./errors.js";
4
+ import { detectSetupAgents } from "./adapters.js";
5
+ import { isSealkeepError } from "./errors.js";
6
6
  import { localApiTokenPath } from "./paths.js";
7
7
  import { ArchiveQueue } from "./queue.js";
8
8
  import { signerEnabled } from "./control-plane.js";
9
+ import { liveness, readHeartbeat } from "./heartbeat.js";
9
10
  import { quotaMessage, quotaState } from "./packages.js";
10
11
  import { loadProviderCredentials, resolveRecoveryPhrase } from "./secrets.js";
11
- import { ENVELOPE_VERSION, SUITES } from "../packages/vaultline-crypto/src/index.js";
12
+ import { ENVELOPE_VERSION, SUITES } from "../packages/sealkeep-crypto/src/index.js";
12
13
  import { readConfig } from "./vault.js";
13
14
  import { envVar } from "./env.js";
15
+ import { providerLabel } from "./branding.js";
16
+ import { readUntouchedCodexResumeStub } from "./resume-stub.js";
14
17
  async function writable(directory) {
15
- const probe = join(directory, `.vaultline-write-probe-${randomUUID()}`);
18
+ const probe = join(directory, `.sealkeep-write-probe-${randomUUID()}`);
16
19
  try {
17
20
  await mkdir(directory, { recursive: true, mode: 0o700 });
18
21
  await writeFile(probe, "");
@@ -25,6 +28,164 @@ async function writable(directory) {
25
28
  await rm(probe, { force: true });
26
29
  }
27
30
  }
31
+ function archiveMetadataId(raw) {
32
+ try {
33
+ const record = JSON.parse(raw);
34
+ return record && typeof record === "object" && typeof record.id === "string" && record.id
35
+ ? record.id
36
+ : null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ async function mapIntegrityMetadata(names, concurrency, work) {
43
+ const output = new Array(names.length);
44
+ let cursor = 0;
45
+ const workers = Array.from({ length: Math.min(Math.max(1, concurrency), names.length) }, async () => {
46
+ for (;;) {
47
+ const index = cursor++;
48
+ if (index >= names.length)
49
+ return;
50
+ output[index] = await work(names[index]);
51
+ if ((index + 1) % 16 === 0)
52
+ await new Promise((resolve) => setImmediate(resolve));
53
+ }
54
+ });
55
+ await Promise.all(workers);
56
+ return output;
57
+ }
58
+ function sameNames(left, right) {
59
+ return left.length === right.length && left.every((name, index) => name === right[index]);
60
+ }
61
+ /**
62
+ * Read one sidecar without turning descriptor pressure into a corruption report.
63
+ * A malformed document is called invalid only when the exact bytes remain
64
+ * malformed across a second read. I/O failures and changing bytes stay in the
65
+ * transient bucket; Health remains red, but does not tell the user the archive
66
+ * metadata itself is corrupt.
67
+ */
68
+ async function readIntegrityMetadata(directory, name, read, readAttempts) {
69
+ const path = join(directory, name);
70
+ let raw = null;
71
+ for (let attempt = 0; attempt < readAttempts; attempt += 1) {
72
+ try {
73
+ raw = await read(path);
74
+ break;
75
+ }
76
+ catch {
77
+ if (attempt + 1 < readAttempts)
78
+ await new Promise((resolve) => setImmediate(resolve));
79
+ }
80
+ }
81
+ if (raw === null)
82
+ return { kind: "transient", name };
83
+ const id = archiveMetadataId(raw);
84
+ if (id)
85
+ return { kind: "valid", name, id };
86
+ // Atomic record updates should never expose partial JSON. Confirm stable bad
87
+ // bytes anyway, so a manual repair racing this read is not branded damage.
88
+ await new Promise((resolve) => setImmediate(resolve));
89
+ let confirmation;
90
+ try {
91
+ confirmation = await read(path);
92
+ }
93
+ catch {
94
+ return { kind: "transient", name };
95
+ }
96
+ const confirmedId = archiveMetadataId(confirmation);
97
+ if (confirmedId)
98
+ return { kind: "valid", name, id: confirmedId };
99
+ return confirmation === raw ? { kind: "invalid", name } : { kind: "transient", name };
100
+ }
101
+ export async function archiveIntegritySummary(directory, options = {}) {
102
+ const listDirectory = options.listDirectory ?? readdir;
103
+ const readMetadata = options.readMetadata ?? ((path) => readFile(path, "utf8"));
104
+ const concurrency = Math.min(64, Math.max(1, Math.trunc(options.concurrency ?? 24) || 1));
105
+ const readAttempts = Math.min(5, Math.max(1, Math.trunc(options.readAttempts ?? 2) || 1));
106
+ const snapshotAttempts = Math.min(5, Math.max(1, Math.trunc(options.snapshotAttempts ?? 3) || 1));
107
+ let names = [];
108
+ let reads = [];
109
+ let scanIncomplete = false;
110
+ for (let attempt = 0; attempt < snapshotAttempts; attempt += 1) {
111
+ let before;
112
+ try {
113
+ before = (await listDirectory(directory)).filter((name) => name.endsWith(".json") || name.endsWith(".skarchive")).sort();
114
+ }
115
+ catch {
116
+ return {
117
+ ok: false,
118
+ unreadableMetadataCount: 0,
119
+ invalidMetadataCount: 0,
120
+ transientMetadataReadErrorCount: 0,
121
+ detachedArchiveCount: 0,
122
+ scanIncomplete: true,
123
+ detail: "Sealkeep cannot list archive metadata. Nothing was changed.",
124
+ };
125
+ }
126
+ const metadata = before.filter((name) => name.endsWith(".json"));
127
+ reads = await mapIntegrityMetadata(metadata, concurrency, (name) => readIntegrityMetadata(directory, name, readMetadata, readAttempts));
128
+ let after;
129
+ try {
130
+ after = (await listDirectory(directory)).filter((name) => name.endsWith(".json") || name.endsWith(".skarchive")).sort();
131
+ }
132
+ catch {
133
+ names = before;
134
+ scanIncomplete = true;
135
+ break;
136
+ }
137
+ names = after;
138
+ if (sameNames(before, after)) {
139
+ scanIncomplete = false;
140
+ break;
141
+ }
142
+ scanIncomplete = true;
143
+ if (attempt + 1 < snapshotAttempts)
144
+ await new Promise((resolve) => setImmediate(resolve));
145
+ }
146
+ const validIds = new Set(reads.filter((item) => item.kind === "valid").map((item) => item.id));
147
+ const invalid = reads.filter((item) => item.kind === "invalid").map((item) => item.name);
148
+ const transient = reads.filter((item) => item.kind === "transient").map((item) => item.name);
149
+ const transientStems = new Set(transient.map((name) => name.slice(0, -".json".length)));
150
+ // A sidecar that could not be read may still be perfectly valid. Do not turn
151
+ // the same temporary I/O failure into a second, invented detached-file error.
152
+ const detached = scanIncomplete ? [] : names
153
+ .filter((name) => name.endsWith(".skarchive"))
154
+ .map((name) => name.slice(0, -".skarchive".length))
155
+ .filter((id) => !validIds.has(id) && !transientStems.has(id));
156
+ const unreadableMetadataCount = invalid.length + transient.length;
157
+ if (unreadableMetadataCount === 0 && detached.length === 0 && !scanIncomplete) {
158
+ return {
159
+ ok: true,
160
+ unreadableMetadataCount: 0,
161
+ invalidMetadataCount: 0,
162
+ transientMetadataReadErrorCount: 0,
163
+ detachedArchiveCount: 0,
164
+ scanIncomplete: false,
165
+ detail: `${validIds.size} archive metadata record${validIds.size === 1 ? " is" : "s are"} readable; no sealed files are detached from their metadata.`
166
+ };
167
+ }
168
+ const parts = [
169
+ invalid.length > 0 ? `${invalid.length} archive metadata file${invalid.length === 1 ? " is" : "s are"} invalid` : null,
170
+ transient.length > 0 ? `${transient.length} archive metadata file${transient.length === 1 ? " could" : "s could"} not be checked after retrying` : null,
171
+ detached.length > 0 ? `${detached.length} sealed file${detached.length === 1 ? " has" : "s have"} no usable metadata` : null,
172
+ scanIncomplete ? "the archive folder changed throughout the check, so the snapshot is incomplete" : null,
173
+ ].filter((part) => Boolean(part));
174
+ const first = (invalid[0] ?? transient[0] ?? (detached[0] ? `${detached[0]}.skarchive` : null));
175
+ return {
176
+ ok: false,
177
+ unreadableMetadataCount,
178
+ invalidMetadataCount: invalid.length,
179
+ transientMetadataReadErrorCount: transient.length,
180
+ detachedArchiveCount: detached.length,
181
+ scanIncomplete,
182
+ detail: `${parts.join("; ")}. Sealkeep left every file untouched. Invalid metadata and detached sealed files need repair; a temporary read or changing-folder result should be checked again before treating it as damage.${first ? ` First affected file: ${first}` : ""}`
183
+ };
184
+ }
185
+ async function archiveIntegrityCheck(directory) {
186
+ const summary = await archiveIntegritySummary(directory);
187
+ return { name: "archive-integrity", status: summary.ok ? "pass" : "fail", detail: summary.detail };
188
+ }
28
189
  /**
29
190
  * A read-mostly environment report. It writes only a probe file it removes, and
30
191
  * it never reads transcript contents, prints a recovery phrase, or shows the API token.
@@ -59,7 +220,7 @@ function quotaCheck(managed) {
59
220
  */
60
221
  async function sealedCopyCheck(dataDir, signedIn) {
61
222
  if (!signedIn)
62
- return { name: "sealed-key-copy", status: "warn", detail: "Not signed in, so no sealed copy of your key is stored" };
223
+ return { name: "sealed-key-copy", status: "warn", detail: "Could not verify the account connection or its key backup. Review your Recovery Kit; an existing backup may still be stored." };
63
224
  try {
64
225
  const { hasSealedCopy } = await import("./cloud.js");
65
226
  return await hasSealedCopy(dataDir)
@@ -70,19 +231,21 @@ async function sealedCopyCheck(dataDir, signedIn) {
70
231
  return { name: "sealed-key-copy", status: "warn", detail: `Could not check for a sealed copy: ${error instanceof Error ? error.message.split("\n")[0] : "unknown"}` };
71
232
  }
72
233
  }
73
- export async function runDoctor(dataDir, env = process.env) {
234
+ export async function runDoctor(dataDir, env = process.env, home = env.HOME ?? env.USERPROFILE ?? env.home ?? ".", options = {}) {
74
235
  const checks = [];
236
+ let managed = null;
75
237
  const config = await readConfig(dataDir).catch((error) => {
76
238
  // This renders in the Health tab, where a raw "config_unsupported_version:"
77
239
  // slug or a bare "unreadable" reads, to someone told there is no reset, as
78
240
  // "your vault is gone." Say what is and is not affected, and never a code.
79
- const detail = isVaultlineError(error) && error.code === "vault_not_initialized"
241
+ const detail = isSealkeepError(error) && error.code === "vault_not_initialized"
80
242
  ? "No vault has been set up on this machine yet. Open Sealkeep's setup page to create one."
81
243
  : "Sealkeep can't read this vault's settings file. Your sealed archives are separate files and are not affected — don't create a new vault; restore this machine's vault folder from a backup, or contact support.";
82
244
  checks.push({ name: "config", status: "fail", detail });
83
245
  return null;
84
246
  });
85
247
  if (config) {
248
+ managed = await managedAccount(dataDir);
86
249
  checks.push({ name: "config", status: "pass", detail: `Vault ${config.vaultId} (config v${config.version})` });
87
250
  // A standing warning, like the crypto row: nothing about a healthy plain
88
251
  // vault makes this go away, because the exposure is the choice itself.
@@ -92,59 +255,149 @@ export async function runDoctor(dataDir, env = process.env) {
92
255
  checks.push(await writable(config.storage.root)
93
256
  ? { name: "archive-store", status: "pass", detail: `Writable: ${config.storage.root}` }
94
257
  : { name: "archive-store", status: "fail", detail: `Not writable: ${config.storage.root}` });
258
+ checks.push(await archiveIntegrityCheck(config.storage.root));
95
259
  // A machine on the managed tier has no remoteStorage of its own — the
96
260
  // bucket is ours. Reporting "archives stay on this device" to someone who
97
261
  // has been pushing to managed storage all week is worse than saying
98
262
  // nothing: it is a health check telling them a comforting falsehood.
99
263
  if (config.remoteStorage) {
100
- checks.push({ name: "remote-storage", status: "pass", detail: `Target ${config.remoteStorage.provider}://${config.remoteStorage.bucket}/${config.remoteStorage.prefix}` });
264
+ checks.push({ name: "remote-storage", status: "pass", detail: `Target ${providerLabel(config.remoteStorage.provider)} · ${config.remoteStorage.bucket}/${config.remoteStorage.prefix}` });
101
265
  }
102
266
  else {
103
- const managed = await managedAccount(dataDir);
104
267
  checks.push(managed
105
268
  // The cloud token lives in this machine's keychain, not in the vault,
106
269
  // so being signed in says archives from here *can* go to managed
107
270
  // storage — not that any have. Say the weaker, true thing.
108
271
  ? { name: "remote-storage", status: "pass", detail: `Signed in to managed storage as ${managed.email} (${managed.plan})` }
109
- : { name: "remote-storage", status: "warn", detail: "No remote target configured; archives stay on this device" });
272
+ : config.storageTargets?.length
273
+ ? { name: "remote-storage", status: "warn", detail: `Configured storage: ${config.storageTargets.map((target) => providerLabel(target.provider)).join(", ")}. The account connection could not be verified by this check. Review storage connections for current availability.` }
274
+ : { name: "remote-storage", status: "warn", detail: "No remote target configured. Add a storage destination to send new archives off this machine." });
110
275
  checks.push(await sealedCopyCheck(dataDir, managed !== null));
111
276
  // A zero quota is a plan with no managed entitlement at all — nothing to warn about.
112
277
  if (managed && managed.quotaBytes > 0)
113
278
  checks.push(quotaCheck(managed));
114
279
  }
115
280
  }
281
+ // The native service can deliberately stay alive while waiting for a phrase:
282
+ // its supervised UI is the recovery path that lets Settings store one and
283
+ // wake the worker. Keep this context separate from the heartbeat because the
284
+ // locked process has not started a daemon tick yet, and its startup heartbeat
285
+ // only proves that the child exists.
286
+ const phraseAvailable = config
287
+ ? await resolveRecoveryPhrase(dataDir, config.vaultId, undefined, env).then((phrase) => Boolean(phrase)).catch(() => false)
288
+ : Boolean(envVar("RECOVERY_PHRASE", env));
289
+ const waitingForPhrase = config !== null && options.nativeServicePresent === true && !phraseAvailable;
116
290
  const stats = await new ArchiveQueue(dataDir).stats();
117
291
  checks.push(stats.failed > 0
118
292
  ? { name: "queue", status: "warn", detail: `${stats.failed} session${stats.failed === 1 ? "" : "s"} could not be archived. See the Queue tab for why, and retry from there.` }
119
293
  : { name: "queue", status: "pass", detail: `${stats.ready} ready, ${stats.leased} in flight, ${stats.done} archived` });
120
- const agents = await detectAgents();
121
- const detected = agents.filter((agent) => agent.detected).map((agent) => agent.agent);
294
+ const agents = await detectSetupAgents(home, dataDir);
295
+ const supported = agents.filter((agent) => agent.detected && agent.archivable);
296
+ const detected = supported.map((agent) => agent.agent);
122
297
  checks.push(detected.length > 0
123
298
  ? { name: "agents", status: "pass", detail: `Detected: ${detected.join(", ")}` }
124
299
  : { name: "agents", status: "warn", detail: "No supported agent directory found on this machine" });
300
+ // A command in hooks.json is not proof that Codex will dispatch it. Codex
301
+ // keeps enabled/trust state separately in config.toml and deliberately lets
302
+ // the user disable a reviewed hook. Report that state without changing it;
303
+ // the filesystem watcher remains the independent preservation path.
304
+ if (supported.length > 0) {
305
+ const inactive = supported.filter((agent) => agent.hooksInstalled && !agent.hooksActive);
306
+ const missing = supported.filter((agent) => !agent.hooksInstalled);
307
+ // Routed elsewhere is worse than missing: it passes every presence check
308
+ // while recall and hook capture silently do nothing for this vault.
309
+ const aimedElsewhere = supported.filter((agent) => agent.hooksAimedElsewhere);
310
+ if (aimedElsewhere.length > 0) {
311
+ checks.push({
312
+ name: "agent-memory-hooks", status: "fail",
313
+ detail: aimedElsewhere.map((agent) => `${agent.label}'s memory hooks route to ${agent.hooksDataDir ?? "another data directory"}, not this vault (${dataDir}): automatic recall and hook capture are silently off here. Run \`sealkeep autopilot\` to re-target them.`).join(" ")
314
+ });
315
+ }
316
+ else if (inactive.length === 0 && missing.length === 0) {
317
+ checks.push({
318
+ name: "agent-memory-hooks", status: "pass",
319
+ detail: `${supported.map((agent) => agent.label).join(" and ")} lifecycle hooks are configured and active.`
320
+ });
321
+ }
322
+ else {
323
+ const details = [
324
+ ...inactive.map((agent) => agent.note ?? `${agent.label}'s configured lifecycle hooks are not active.`),
325
+ ...missing.map((agent) => `${agent.label}'s complete Sealkeep lifecycle is not configured.`),
326
+ "The background watcher remains the independent archive path while Automatic archiving is running."
327
+ ];
328
+ checks.push({ name: "agent-memory-hooks", status: "warn", detail: details.join(" ") });
329
+ }
330
+ }
331
+ // Hooks fail open, so a recall lane whose passes keep dying is invisible at
332
+ // the agent: memory is simply absent. The service's heartbeat is the one
333
+ // place that records it.
334
+ const recallLane = (await readHeartbeat(dataDir))?.context;
335
+ const { readHookUnlockFailure } = await import("./agent-context.js");
336
+ const unlockFailure = await readHookUnlockFailure(dataDir);
337
+ if (unlockFailure && Date.now() - Date.parse(unlockFailure.at) < 24 * 60 * 60_000) {
338
+ checks.push({
339
+ name: "automatic-recall", status: "fail",
340
+ detail: `The ${unlockFailure.agent === "codex" ? "Codex" : "Claude Code"} memory hook could not unlock this vault at ${unlockFailure.at.slice(0, 16).replace("T", " ")}: automatic recall and hook capture were silently off for that session, while archiving may have kept working. The hook ran with SEALKEEP_SECRET_BACKEND ${unlockFailure.backend ? `= ${unlockFailure.backend}` : "unset"}; the hooks and the background service must find the recovery phrase in the same secret backend (Settings › Unlocking, or \`sealkeep autopilot\`).`,
341
+ });
342
+ }
343
+ else if (recallLane?.state === "attention" && recallLane.failures >= 3) {
344
+ checks.push({
345
+ name: "automatic-recall", status: "warn",
346
+ detail: `Automatic recall passes keep failing (${recallLane.failures} in a row, last: ${recallLane.lastError ?? "unknown"}${recallLane.lastSuccessAt ? `, last success ${recallLane.lastSuccessAt.slice(0, 16).replace("T", " ")}` : ""}). Memory hooks still run, but recall arrives late or not at all until this clears; the background service retries every half minute.`,
347
+ });
348
+ }
349
+ else if (recallLane?.lastSuccessAt) {
350
+ checks.push({
351
+ name: "automatic-recall", status: "pass",
352
+ detail: `Automatic recall last prepared memory at ${recallLane.lastSuccessAt.slice(0, 16).replace("T", " ")}.`,
353
+ });
354
+ }
125
355
  // The failure Paul hit in the wild: an agent's own catalog still points at
126
356
  // a session file that is gone — reclaimed by us, or deleted by anything —
127
357
  // and "resume" breaks with no hint that the bytes are one command away.
128
358
  // This check counts archived sessions whose source file is missing and
129
359
  // hands over the exact command that puts each back where resume expects it.
360
+ // A reclaimed Codex transcript can deliberately leave a tiny valid rollout
361
+ // at that path. Do not call that pointer the original source: name what it
362
+ // is and give the overwrite mode required while it occupies the destination.
130
363
  try {
131
364
  const { stat } = await import("node:fs/promises");
132
365
  const { listArchives } = await import("./vault.js");
133
366
  const records = (await listArchives(dataDir)).filter((record) => record.version === 2);
134
367
  const missing = [];
368
+ const pointers = [];
135
369
  for (const record of records) {
136
370
  const there = await stat(record.source.path).then((entry) => entry.isFile()).catch(() => false);
137
- if (!there)
138
- missing.push(record.source.path.split("/").pop() ?? record.id);
371
+ const filename = record.source.path.split("/").pop() ?? record.id;
372
+ if (!there) {
373
+ missing.push({ id: record.id, filename });
374
+ continue;
375
+ }
376
+ if (record.reclaimed?.resumeStub?.version === 1) {
377
+ const pointer = await readUntouchedCodexResumeStub(record.source.path);
378
+ if (pointer
379
+ && pointer.archiveId === record.id
380
+ && pointer.sessionId === record.reclaimed.resumeStub.sessionId
381
+ && pointer.bytes === record.reclaimed.resumeStub.bytes
382
+ && pointer.sha256 === record.reclaimed.resumeStub.sha256) {
383
+ pointers.push({ id: record.id, filename, sessionId: pointer.sessionId });
384
+ }
385
+ }
139
386
  }
140
387
  if (missing.length > 0) {
141
388
  checks.push({
142
389
  name: "resumable-history", status: "pass",
143
- detail: `${missing.length} archived session${missing.length === 1 ? " file is" : " files are"} no longer on disk (reclaimed or removed). An agent trying to resume one will say the file is missing — \`sealkeep recover <that filename>\` puts it back exactly where resume expects it. First: ${missing[0]}`
390
+ detail: `${missing.length} archived session${missing.length === 1 ? " file is" : " files are"} no longer on disk (reclaimed or removed). Restore the exact archive to its native path before resuming. First: ${missing[0].filename} — \`sealkeep recover ${missing[0].id} --native\``
391
+ });
392
+ }
393
+ else if (pointers.length > 0) {
394
+ checks.push({
395
+ name: "resumable-history", status: "pass",
396
+ detail: `${pointers.length} reclaimed Codex session${pointers.length === 1 ? " keeps" : "s keep"} a tiny native resume pointer, so \`codex resume ${pointers[0].sessionId}\` remains discoverable without downloading the full transcript. Use its Sealkeep MCP search instruction for relevant memory. To restore every byte explicitly while preserving the pointer as a backup, run \`sealkeep recover ${pointers[0].id} --native --overwrite backup\`, then resume again. First: ${pointers[0].filename}`
144
397
  });
145
398
  }
146
399
  else if (records.length > 0) {
147
- checks.push({ name: "resumable-history", status: "pass", detail: "Every archived session's file is still on disk; agent resume needs nothing from us" });
400
+ checks.push({ name: "resumable-history", status: "pass", detail: "Every archived session remains discoverable at its native path; agent resume needs nothing from Sealkeep" });
148
401
  }
149
402
  }
150
403
  catch { /* an unreadable archive store already failed its own check above */ }
@@ -159,12 +412,31 @@ export async function runDoctor(dataDir, env = process.env) {
159
412
  // nothing archived yet — nothing to say
160
413
  }
161
414
  else if (coverage.missing.length === 0) {
162
- checks.push({ name: "search-index", status: "pass", detail: `All ${coverage.total} archives are in the content search index${coverage.builtAt ? ` (updated ${coverage.builtAt.slice(0, 10)})` : ""}.` });
415
+ // "All N archives" means all N THIS MACHINE sealed. Saying so keeps the
416
+ // sentence true on a team, where the account holds more than this
417
+ // machine has ever seen.
418
+ checks.push({ name: "search-index", status: "pass", detail: `All ${coverage.total} archives sealed on this machine are in the content search index${coverage.builtAt ? ` (updated ${coverage.builtAt.slice(0, 10)})` : ""}.` });
163
419
  }
164
420
  else {
421
+ const daemonBeat = await readHeartbeat(dataDir);
422
+ const service = liveness(daemonBeat);
423
+ const indexing = service.state === "running" ? daemonBeat?.indexing : undefined;
424
+ let automation;
425
+ if (waitingForPhrase) {
426
+ automation = "Automatic catch-up is waiting for this machine's recovery phrase; it will resume after you store it in Settings › Unlocking.";
427
+ }
428
+ else if (indexing?.state === "building") {
429
+ automation = `Automatic catch-up is running in the background (attempt ${indexing.attempts}).`;
430
+ }
431
+ else if (indexing?.state === "retry-wait") {
432
+ automation = `Automatic catch-up is waiting to retry${indexing.nextAttemptAt ? ` after ${indexing.nextAttemptAt}` : ""}${indexing.lastError ? ` (${indexing.lastError})` : ""}.`;
433
+ }
434
+ else {
435
+ automation = "Automatic catch-up is not currently active; repair or restart automatic archiving in Settings.";
436
+ }
165
437
  checks.push({
166
- name: "search-index", status: "pass",
167
- detail: `${coverage.indexed} of ${coverage.total} archives are searchable; ${coverage.missing.length} not yet indexed (new seals index themselves — \`sealkeep index build\` adds the rest). First missing: ${coverage.missing[0].path.split("/").pop() ?? coverage.missing[0].id}`
438
+ name: "search-index", status: "warn",
439
+ detail: `${coverage.indexed} of ${coverage.total} archives sealed on this machine are searchable; ${coverage.missing.length} not yet indexed. ${automation}`
168
440
  });
169
441
  }
170
442
  }
@@ -179,20 +451,13 @@ export async function runDoctor(dataDir, env = process.env) {
179
451
  const pinned = [...new Set(targets.flatMap((target) => target.projects ?? []))];
180
452
  if (pinned.length > 0) {
181
453
  const { listArchives } = await import("./vault.js");
182
- const { claudeProjectFromPath, codexProjectFromRollout } = await import("./adapters.js");
454
+ const { projectOfRecord } = await import("./adapters.js");
183
455
  const records = await listArchives(dataDir).catch(() => []);
184
456
  const known = new Set();
185
457
  for (const record of records) {
186
- try {
187
- const name = record.source.agent === "claude"
188
- ? claudeProjectFromPath(record.source.path).project
189
- : record.source.agent === "codex"
190
- ? (await codexProjectFromRollout(record.source.path, record.createdAt)).project
191
- : null;
192
- if (name)
193
- known.add(name);
194
- }
195
- catch { /* a project we cannot read is not evidence of a bad pin */ }
458
+ const name = await projectOfRecord(record).catch(() => null);
459
+ if (name)
460
+ known.add(name);
196
461
  }
197
462
  const unmatched = pinned.filter((name) => !known.has(name));
198
463
  if (unmatched.length === 0) {
@@ -212,25 +477,35 @@ export async function runDoctor(dataDir, env = process.env) {
212
477
  // so a machine set up correctly through the window — phrase in the keystore,
213
478
  // sealing away happily — was told on its Health tab that its phrase was
214
479
  // missing and named an environment variable as the cure.
215
- const phraseAvailable = config
216
- ? await resolveRecoveryPhrase(dataDir, config.vaultId, undefined, env).then((phrase) => Boolean(phrase)).catch(() => false)
217
- : Boolean(envVar("RECOVERY_PHRASE", env));
218
480
  checks.push(phraseAvailable
219
481
  ? { name: "recovery-phrase", status: "pass", detail: "This machine can unlock the vault on its own, so archiving runs unattended." }
220
482
  : { name: "recovery-phrase", status: "warn", detail: "This machine doesn't hold your recovery phrase, so it can't archive on its own. Add it under Settings › Unlocking to let archiving run unattended. Your vault and archives are unaffected." });
221
483
  const tokenMode = await stat(localApiTokenPath(dataDir)).then((info) => info.mode & 0o777).catch(() => null);
222
484
  checks.push(tokenMode === null
223
- ? { name: "local-api-token", status: "warn", detail: "No local API token yet; it is created when `sealkeep api` first starts" }
485
+ ? { name: "local-api-token", status: "warn", detail: "No local API token yet; Autopilot creates it with the local UI, as do `sealkeep ui` and `sealkeep api`" }
224
486
  : tokenMode === 0o600
225
487
  ? { name: "local-api-token", status: "pass", detail: "Local API token is owner-only" }
226
488
  : { name: "local-api-token", status: "fail", detail: `Local API token mode is ${tokenMode.toString(8)}; it must be 600` });
227
- const signerOn = signerEnabled(env);
489
+ // The doctor normally runs inside the local dashboard process, while the
490
+ // uploader runs inside a separately installed launchd/systemd service. Its
491
+ // environment is intentionally not inherited by the dashboard. Inspecting
492
+ // this process's signer flag therefore produced the exact opposite of the
493
+ // truth on healthy machines. The daemon publishes its effective capability
494
+ // in the heartbeat; the local flag remains a compatibility fallback for a
495
+ // foreground `sealkeep doctor` invocation.
496
+ const daemonBeat = await readHeartbeat(dataDir);
497
+ const daemonLive = liveness(daemonBeat);
498
+ const daemonUploads = daemonLive.state !== "stopped" && daemonLive.state !== "never-started"
499
+ && daemonBeat?.capabilities?.uploads === true;
500
+ const signerOn = daemonUploads || signerEnabled(env);
228
501
  const credentials = config ? await loadProviderCredentials(dataDir, config.vaultId).then(() => true).catch(() => false) : false;
229
- checks.push(!signerOn
230
- ? { name: "signer", status: "warn", detail: "New archives aren't being uploaded from this machine right now. Anything already shown as verified in your storage is safely there. Turn on automatic archiving in Settings to resume uploads." }
231
- : credentials
232
- ? { name: "signer", status: "pass", detail: "Signer enabled and provider credentials are available from the secret store" }
233
- : { name: "signer", status: "warn", detail: "Uploads are on, but this machine has no key for the bucket. Add the bucket's credentials on the storage step of setup." });
502
+ checks.push(waitingForPhrase
503
+ ? { name: "signer", status: "warn", detail: "Automatic uploads are waiting for this machine's recovery phrase; they will resume after you store it in Settings › Unlocking." }
504
+ : !signerOn
505
+ ? { name: "signer", status: "warn", detail: `${daemonLive.state === "late" ? "The background service has stopped checking in on time." : "Automatic uploads are not running on this machine."} Anything already shown as verified in your storage is safely there. Repair automatic uploads in Settings.` }
506
+ : managed || credentials
507
+ ? { name: "signer", status: "pass", detail: daemonUploads ? "The background service reports automatic uploads enabled." : "Uploads are enabled in this process and storage credentials are available." }
508
+ : { name: "signer", status: "warn", detail: "Uploads are on, but this machine has no usable sign-in for its configured storage. Reconnect that destination in Settings." });
234
509
  // Deliberately a standing warning: the envelope is unaudited, and no local check can change that.
235
510
  checks.push({ name: "crypto", status: "warn", detail: `Archive envelope v${ENVELOPE_VERSION} (${SUITES[0]}); implemented against published primitives and not independently audited` });
236
511
  return { ok: checks.every((check) => check.status !== "fail"), checks };
@@ -0,0 +1,24 @@
1
+ export type DurableTicketLock = {
2
+ readonly queuePath: string;
3
+ readonly claimPath: string;
4
+ readonly ticket: string;
5
+ readonly token: string;
6
+ assertOwned(): Promise<void>;
7
+ release(): Promise<void>;
8
+ };
9
+ export type DurableTicketLockOptions = {
10
+ timeoutMs?: number;
11
+ pollMs?: number;
12
+ signal?: AbortSignal;
13
+ isProcessAlive?: (pid: number) => Promise<boolean>;
14
+ };
15
+ /**
16
+ * Cross-process FIFO lock without stale-owner unlink.
17
+ *
18
+ * Every contender atomically allocates a monotonically increasing immutable
19
+ * ticket. Released and dead-owner tickets stay as tiny tombstones, so no later
20
+ * process can recreate an older pathname or race a compare-then-unlink stale
21
+ * takeover. A dead process is skipped by later tickets; a malformed earlier
22
+ * ticket fails closed until an operator repairs it.
23
+ */
24
+ export declare function acquireDurableTicketLock(requestedQueuePath: string, resource: string, options?: DurableTicketLockOptions): Promise<DurableTicketLock>;