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,64 @@
1
+ export type ContentIndexBuildResult = {
2
+ indexedNow: number;
3
+ skipped: number;
4
+ };
5
+ export type BackgroundIndexBuild = {
6
+ /** Useful for diagnostics and for proving this work is not on the daemon's PID. */
7
+ pid: number | undefined;
8
+ /** Secret-free sibling; diagnostic identity, never the build owner. */
9
+ watchdogPid?: number;
10
+ /** Resolves once the child has accepted the secret and is beginning the build. */
11
+ started: Promise<void>;
12
+ result: Promise<ContentIndexBuildResult>;
13
+ /** Stops this exact child with bounded escalation. Its durable barrier remains
14
+ * if the kernel has not confirmed exit within the join window. Idempotent. */
15
+ stop(): Promise<void>;
16
+ };
17
+ /** Hard V8-heap boundary for best-effort derived work. Plaintext and index
18
+ * serialization are streamed, so exceeding this means the retained index is
19
+ * itself unreasonable; the child fails without taking the interactive daemon
20
+ * or the rest of the desktop down with it. */
21
+ export declare const BACKGROUND_INDEX_MAX_OLD_SPACE_MB = 1024;
22
+ export declare const BACKGROUND_INDEX_MAX_SEMI_SPACE_MB = 16;
23
+ /** Checked cooperatively every 8 MiB of plaintext in addition to V8's heap
24
+ * boundary. This includes native buffers, decompression, and token storage. */
25
+ export declare const BACKGROUND_INDEX_MAX_RSS_BYTES: number;
26
+ /** Configuration and credential-store discovery only; credentials are loaded
27
+ * from the vault by the worker, never inherited from the daemon environment. */
28
+ export declare function backgroundIndexWorkerEnvironment(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
29
+ export declare function backgroundIndexExecArgv(parent: readonly string[]): string[];
30
+ /**
31
+ * Runs a historical content-index build outside the daemon's event loop.
32
+ *
33
+ * Index extraction is not merely asynchronous I/O: archive authentication,
34
+ * decompression, token scanning, index serialisation, gzip and encryption all
35
+ * contain long synchronous sections. A Promise around those sections still
36
+ * blocks heartbeat timers. A child also gives the large derived index its own
37
+ * heap, and lets the OS prefer interactive Codex/Claude hook processes while a
38
+ * backlog is being rebuilt.
39
+ *
40
+ * The recovery phrase travels only over the already-owned IPC channel. It is
41
+ * deliberately absent from argv and the environment, which are routinely
42
+ * exposed by process inspectors and service diagnostics.
43
+ */
44
+ export declare function startBackgroundIndexBuild(dataDir: string, phrase: string, options: {
45
+ sync: boolean;
46
+ idleMs?: number;
47
+ /** Child boot gets a small floor; once it says started, idleMs governs real work. */
48
+ startupMs?: number;
49
+ stopGraceMs?: number;
50
+ /** Historical work defaults to one lane and 30% of one core. */
51
+ lanes?: number;
52
+ cpuTarget?: number;
53
+ /** Deterministic process fixture; production always uses the package child. */
54
+ childPath?: string;
55
+ /** Deterministic failure fixture; production always uses the package watchdog. */
56
+ watchdogPath?: string;
57
+ /** Supervised cleanup only after the exact worker has actually exited. */
58
+ onWorkerExit?: (worker: {
59
+ pid: number;
60
+ processIdentity: string;
61
+ }) => Promise<void>;
62
+ /** Exact owning daemon's current-vault upload capability; never inferred globally. */
63
+ signerAuthorized?: boolean;
64
+ }): BackgroundIndexBuild;
@@ -0,0 +1,394 @@
1
+ import { fork } from "node:child_process";
2
+ import { constants, setPriority } from "node:os";
3
+ import { fileURLToPath } from "node:url";
4
+ import { SealkeepError } from "./errors.js";
5
+ import { backgroundDaemonCpuBudget, BACKGROUND_CPU_TOTAL_TARGET, signalOwnedBackgroundProcess, onOwnedBackgroundProcessExit, startBackgroundProcessCpuGovernor } from "./background-cpu.js";
6
+ import { daemonProcessIdentity, registerDaemonIndexWorker } from "./daemon-lease.js";
7
+ import { envVar } from "./env.js";
8
+ import { backgroundWorkerErrorFromWire } from "./background-worker-error.js";
9
+ /** Hard V8-heap boundary for best-effort derived work. Plaintext and index
10
+ * serialization are streamed, so exceeding this means the retained index is
11
+ * itself unreasonable; the child fails without taking the interactive daemon
12
+ * or the rest of the desktop down with it. */
13
+ export const BACKGROUND_INDEX_MAX_OLD_SPACE_MB = 1024;
14
+ export const BACKGROUND_INDEX_MAX_SEMI_SPACE_MB = 16;
15
+ /** Checked cooperatively every 8 MiB of plaintext in addition to V8's heap
16
+ * boundary. This includes native buffers, decompression, and token storage. */
17
+ export const BACKGROUND_INDEX_MAX_RSS_BYTES = 1536 * 1024 * 1024;
18
+ const PROCESS_RUNTIME_ENV = ["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP", "TMPDIR"];
19
+ /** Configuration and credential-store discovery only; credentials are loaded
20
+ * from the vault by the worker, never inherited from the daemon environment. */
21
+ export function backgroundIndexWorkerEnvironment(env = process.env) {
22
+ const result = {};
23
+ for (const key of [...PROCESS_RUNTIME_ENV, "HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA",
24
+ "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "LANG", "LC_ALL", "TZ"]) {
25
+ if (env[key] !== undefined)
26
+ result[key] = env[key];
27
+ }
28
+ for (const prefix of ["SEALKEEP_", "VAULTLINE_"]) {
29
+ for (const name of ["SECRET_BACKEND", "CLOUD_URL", "ALLOW_PRIVATE_STORAGE_ENDPOINT", "GDRIVE_CLIENT_ID", "MAX_ARCHIVE_BYTES"]) {
30
+ if (env[prefix + name] !== undefined)
31
+ result[prefix + name] = env[prefix + name];
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ export function backgroundIndexExecArgv(parent) {
37
+ // A parent may have --eval source, inspector authentication, or arbitrary
38
+ // preload arguments. Only our development TS loader must cross this boundary.
39
+ const loaders = [];
40
+ for (let index = 0; index < parent.length; index += 1) {
41
+ const argument = parent[index];
42
+ const match = /^(--import|--require|-r)(?:=(.*))?$/.exec(argument);
43
+ if (!match)
44
+ continue;
45
+ const value = match[2] ?? parent[++index];
46
+ if (value && (value === "tsx" || /[/\\]tsx[/\\]dist[/\\](?:loader\.mjs|preflight\.cjs)$/.test(value))) {
47
+ loaders.push(match[1], value);
48
+ }
49
+ }
50
+ return [
51
+ ...loaders,
52
+ `--max-old-space-size=${BACKGROUND_INDEX_MAX_OLD_SPACE_MB}`,
53
+ `--max-semi-space-size=${BACKGROUND_INDEX_MAX_SEMI_SPACE_MB}`,
54
+ // V8's default concurrent collector can add two or more CPU cores while
55
+ // the JavaScript lane is correctly sleeping. This child is explicitly
56
+ // background work; serial GC trades a little wall time for a bounded,
57
+ // non-interactive process footprint.
58
+ "--single-threaded-gc",
59
+ ];
60
+ }
61
+ /**
62
+ * Runs a historical content-index build outside the daemon's event loop.
63
+ *
64
+ * Index extraction is not merely asynchronous I/O: archive authentication,
65
+ * decompression, token scanning, index serialisation, gzip and encryption all
66
+ * contain long synchronous sections. A Promise around those sections still
67
+ * blocks heartbeat timers. A child also gives the large derived index its own
68
+ * heap, and lets the OS prefer interactive Codex/Claude hook processes while a
69
+ * backlog is being rebuilt.
70
+ *
71
+ * The recovery phrase travels only over the already-owned IPC channel. It is
72
+ * deliberately absent from argv and the environment, which are routinely
73
+ * exposed by process inspectors and service diagnostics.
74
+ */
75
+ export function startBackgroundIndexBuild(dataDir, phrase, options) {
76
+ // Tests execute the TypeScript source through tsx; packaged installs execute
77
+ // dist JavaScript. Match the entry point to the module that created us.
78
+ const extension = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
79
+ const childPath = options.childPath ?? fileURLToPath(new URL(`./index-build-child${extension}`, import.meta.url));
80
+ // Explicit per-daemon denial overrides ambient operator enablement. Direct
81
+ // callers retain their explicit operator setting, but never another daemon's
82
+ // process-local grant. The resulting capability travels only through IPC.
83
+ const signerAuthorized = options.signerAuthorized ?? (envVar("ENABLE_SIGNER") === "1");
84
+ const child = fork(childPath, [], {
85
+ stdio: ["ignore", "ignore", "inherit", "ipc"],
86
+ execArgv: backgroundIndexExecArgv(process.execArgv),
87
+ env: backgroundIndexWorkerEnvironment(),
88
+ });
89
+ // The daemon and lifecycle hooks are interactive control lanes; a historical
90
+ // rebuild is best-effort derived work. Priority adjustment is advisory and
91
+ // unsupported on a few hosts, where ordinary scheduler fairness still holds.
92
+ if (child.pid !== undefined) {
93
+ try {
94
+ setPriority(child.pid, constants.priority.PRIORITY_BELOW_NORMAL);
95
+ }
96
+ catch { /* priority is an optimisation, never a reason to lose indexing */ }
97
+ }
98
+ const cpuTarget = Math.min(1, Math.max(0.05, options.cpuTarget ?? 0.30));
99
+ // The in-child limiter handles ordinary parse/compression work. Its event
100
+ // loop cannot interrupt V8's own GC, so this owned parent also governs the
101
+ // whole process on POSIX. This is what makes the ceiling apply to native
102
+ // worker/collector CPU rather than just JavaScript checkpoints.
103
+ // Do not pause or send any work until the independent orphan watchdog has
104
+ // verified this exact worker generation and acknowledged ownership.
105
+ let cpuGovernor = { stop() { } };
106
+ const watchdogEnv = {};
107
+ for (const key of PROCESS_RUNTIME_ENV) {
108
+ if (process.env[key] !== undefined)
109
+ watchdogEnv[key] = process.env[key];
110
+ }
111
+ let watchdog;
112
+ try {
113
+ watchdog = fork(options.watchdogPath ?? fileURLToPath(new URL(`./index-background-watchdog${extension}`, import.meta.url)), [], {
114
+ stdio: ["ignore", "ignore", "inherit", "ipc"],
115
+ env: watchdogEnv,
116
+ execArgv: [...backgroundIndexExecArgv(process.execArgv).filter((arg) => !arg.startsWith("--max-old-space-size")), "--max-old-space-size=64"],
117
+ });
118
+ }
119
+ catch (error) {
120
+ // A synchronous spawn/argument failure occurs before the usual listeners
121
+ // exist. This exact worker has received neither work nor a pause signal.
122
+ child.once("error", () => { });
123
+ signalOwnedBackgroundProcess(child, "SIGKILL");
124
+ throw error;
125
+ }
126
+ let watchdogDisarmed = false;
127
+ let watchdogFailure;
128
+ let watchdogExited = false;
129
+ let watchdogExitTimer;
130
+ let watchdogClosedResolve;
131
+ const watchdogClosed = new Promise((resolve) => { watchdogClosedResolve = resolve; });
132
+ // Keep this owned child and its IPC channel referenced until it exits. A bare
133
+ // Promise does not keep Node alive; unref'ing here can abandon a build before
134
+ // daemon.close() gets the chance to terminate it and wait for the exact PID.
135
+ let startedResolve;
136
+ let startedSettled = false;
137
+ const started = new Promise((resolve) => { startedResolve = resolve; });
138
+ const markStarted = () => {
139
+ if (startedSettled)
140
+ return;
141
+ startedSettled = true;
142
+ startedResolve();
143
+ };
144
+ let resultSettled = false;
145
+ let resultResolve;
146
+ let resultReject;
147
+ const result = new Promise((resolve, reject) => {
148
+ resultResolve = resolve;
149
+ resultReject = reject;
150
+ });
151
+ let reportedResult;
152
+ let reportedError;
153
+ let workerIdentity;
154
+ let workerRegistration;
155
+ let closedResolve;
156
+ let closedSettled = false;
157
+ const closed = new Promise((resolve) => { closedResolve = resolve; });
158
+ const idleMs = Math.max(1, options.idleMs ?? 2 * 60_000);
159
+ // A busy machine can take longer to load Node (and the TypeScript loader in
160
+ // development) than a deliberately tiny test/work idle window. Treat boot
161
+ // and work as separate liveness phases: a child that never starts is still
162
+ // bounded, while a healthy child is judged by actual progress only after its
163
+ // first IPC acknowledgement. Production's two-minute idle window remains
164
+ // the startup window too; the floor matters only for shorter overrides.
165
+ const startupMs = Math.max(idleMs, options.startupMs ?? 10_000);
166
+ const stopGraceMs = Math.max(1, options.stopGraceMs ?? 2_000);
167
+ let timedOut = false;
168
+ let idleTimer;
169
+ let forceTimer;
170
+ const clearDeadlines = () => {
171
+ if (idleTimer)
172
+ clearTimeout(idleTimer);
173
+ if (forceTimer)
174
+ clearTimeout(forceTimer);
175
+ idleTimer = undefined;
176
+ forceTimer = undefined;
177
+ };
178
+ const armIdleDeadline = (windowMs = idleMs, phase = "running") => {
179
+ if (closedSettled || timedOut)
180
+ return;
181
+ if (idleTimer)
182
+ clearTimeout(idleTimer);
183
+ idleTimer = setTimeout(() => {
184
+ timedOut = true;
185
+ reportedError = new SealkeepError("operation_timeout", phase === "starting"
186
+ ? `Historical index child did not start within ${windowMs} ms`
187
+ : `Historical index build made no progress for ${windowMs} ms`, { operation: "historical_index", phase, idleMs: windowMs, retryable: true });
188
+ cpuGovernor.stop();
189
+ signalOwnedBackgroundProcess(child, "SIGTERM");
190
+ forceTimer = setTimeout(() => { if (!closedSettled)
191
+ signalOwnedBackgroundProcess(child, "SIGKILL"); }, stopGraceMs);
192
+ }, windowMs);
193
+ };
194
+ onOwnedBackgroundProcessExit(child, (code, signal) => {
195
+ cpuGovernor.stop();
196
+ clearDeadlines();
197
+ closedSettled = true;
198
+ watchdogDisarmed = true;
199
+ if (!watchdogExited) {
200
+ if (watchdog.connected)
201
+ watchdog.send({ type: "disarm" }, () => { });
202
+ watchdogExitTimer = setTimeout(() => signalOwnedBackgroundProcess(watchdog, "SIGKILL"), stopGraceMs);
203
+ }
204
+ void (async () => {
205
+ try {
206
+ const registration = await workerRegistration;
207
+ const cleanupErrors = [];
208
+ try {
209
+ if (workerIdentity && child.pid !== undefined) {
210
+ await options.onWorkerExit?.({ pid: child.pid, processIdentity: workerIdentity });
211
+ }
212
+ }
213
+ catch (error) {
214
+ cleanupErrors.push(error);
215
+ }
216
+ // A shared-lane recovery failure must not skip cleanup of this dead
217
+ // generation's private scratch or leave its startup barrier stranded.
218
+ try {
219
+ await registration?.retireAfterExit();
220
+ }
221
+ catch (error) {
222
+ cleanupErrors.push(error);
223
+ }
224
+ if (cleanupErrors.length)
225
+ throw new AggregateError(cleanupErrors, "Historical index worker cleanup failed");
226
+ }
227
+ catch (error) {
228
+ reportedError = error instanceof Error ? error : new Error(String(error));
229
+ reportedResult = undefined;
230
+ }
231
+ closedResolve();
232
+ markStarted();
233
+ if (resultSettled)
234
+ return;
235
+ resultSettled = true;
236
+ if (watchdogFailure)
237
+ resultReject(watchdogFailure);
238
+ else if (governorFailure)
239
+ resultReject(governorFailure);
240
+ else if (reportedResult)
241
+ resultResolve(reportedResult);
242
+ else
243
+ resultReject(reportedError ?? new Error(`Historical index child exited before reporting a result (${signal ?? code ?? "unknown"})`));
244
+ })();
245
+ });
246
+ child.once("error", (error) => { reportedError ??= error; });
247
+ child.on("message", (wire) => {
248
+ if (!wire || typeof wire !== "object" || !("type" in wire))
249
+ return;
250
+ const message = wire;
251
+ if (message.type === "started" || message.type === "progress") {
252
+ markStarted();
253
+ armIdleDeadline();
254
+ return;
255
+ }
256
+ if (message.type === "result") {
257
+ markStarted();
258
+ if (!timedOut)
259
+ reportedResult = message.result;
260
+ return;
261
+ }
262
+ if (message.type === "error") {
263
+ reportedError = backgroundWorkerErrorFromWire(message.error);
264
+ }
265
+ });
266
+ let governorFailure;
267
+ const beginBuild = (scratchGeneration) => {
268
+ if (closedSettled || timedOut || stopInFlight)
269
+ return;
270
+ cpuGovernor = startBackgroundProcessCpuGovernor({
271
+ target: cpuTarget,
272
+ budget: backgroundDaemonCpuBudget,
273
+ signal: (signal) => signalOwnedBackgroundProcess(child, signal),
274
+ verifySignals: true,
275
+ onFailure: (error) => {
276
+ governorFailure ??= error;
277
+ reportedError = error;
278
+ reportedResult = undefined;
279
+ if (child.connected)
280
+ child.disconnect();
281
+ void stop();
282
+ },
283
+ });
284
+ if (governorFailure)
285
+ return;
286
+ child.send({
287
+ type: "build", dataDir, phrase, sync: options.sync,
288
+ lanes: Math.max(1, Math.floor(options.lanes ?? 1)),
289
+ // POSIX has a parent-side hard process governor, which can pause even a
290
+ // synchronous GC. Let that be the one clock instead of compounding it with
291
+ // child sleeps. Windows has no SIGSTOP and keeps cooperative accounting.
292
+ cpuTarget: process.platform === "win32" ? Math.min(cpuTarget, BACKGROUND_CPU_TOTAL_TARGET / 3) : 1,
293
+ maxRssBytes: BACKGROUND_INDEX_MAX_RSS_BYTES,
294
+ scratchGeneration,
295
+ signerAuthorized,
296
+ }, (error) => {
297
+ if (error) {
298
+ reportedError ??= error;
299
+ void stop();
300
+ }
301
+ });
302
+ };
303
+ let stopInFlight;
304
+ const stop = () => {
305
+ if (stopInFlight)
306
+ return stopInFlight;
307
+ stopInFlight = (async () => {
308
+ if (closedSettled) {
309
+ await closed;
310
+ await watchdogClosed;
311
+ return;
312
+ }
313
+ clearDeadlines();
314
+ cpuGovernor.stop();
315
+ signalOwnedBackgroundProcess(child, "SIGTERM");
316
+ let force;
317
+ await Promise.race([
318
+ closed,
319
+ new Promise((resolve) => {
320
+ force = setTimeout(() => {
321
+ if (!closedSettled)
322
+ signalOwnedBackgroundProcess(child, "SIGKILL");
323
+ resolve();
324
+ }, stopGraceMs);
325
+ }),
326
+ ]);
327
+ if (force)
328
+ clearTimeout(force);
329
+ // SIGKILL is asynchronous too. Give the exact PID another join window;
330
+ // its durable barrier still excludes replacement if the kernel stalls.
331
+ if (!closedSettled) {
332
+ let final;
333
+ await Promise.race([
334
+ closed,
335
+ new Promise((resolve) => { final = setTimeout(resolve, stopGraceMs); }),
336
+ ]);
337
+ if (final)
338
+ clearTimeout(final);
339
+ }
340
+ if (closedSettled) {
341
+ await closed;
342
+ await watchdogClosed;
343
+ }
344
+ })();
345
+ return stopInFlight;
346
+ };
347
+ const watchdogFailed = (cause) => {
348
+ if (closedSettled || watchdogDisarmed)
349
+ return;
350
+ watchdogFailure ??= cause;
351
+ reportedError = cause;
352
+ reportedResult = undefined;
353
+ void stop();
354
+ };
355
+ watchdog.once("error", (error) => { watchdogFailed(error); });
356
+ watchdog.once("disconnect", () => {
357
+ watchdogFailed(new Error("Historical index orphan watchdog exited unexpectedly (IPC disconnected)"));
358
+ });
359
+ onOwnedBackgroundProcessExit(watchdog, (code, signal) => {
360
+ watchdogExited = true;
361
+ if (watchdogExitTimer)
362
+ clearTimeout(watchdogExitTimer);
363
+ watchdogClosedResolve();
364
+ watchdogFailed(new Error(`Historical index orphan watchdog exited unexpectedly (${signal ?? code ?? "unknown"})`));
365
+ });
366
+ watchdog.once("message", (wire) => {
367
+ if (wire && typeof wire === "object" && "type" in wire && wire.type === "armed") {
368
+ if (closedSettled || timedOut || stopInFlight || !workerIdentity || child.pid === undefined)
369
+ return;
370
+ workerRegistration = registerDaemonIndexWorker(dataDir, child.pid, workerIdentity);
371
+ void workerRegistration.then((registration) => beginBuild(registration.scratchGeneration), watchdogFailed);
372
+ }
373
+ else
374
+ watchdogFailed(new Error("Historical index orphan watchdog did not acknowledge ownership"));
375
+ });
376
+ // Send only after all failure listeners are installed. Neither the phrase
377
+ // nor the vault path ever enters the watchdog's IPC, argv or environment.
378
+ void (async () => {
379
+ if (child.pid === undefined)
380
+ throw new Error("Historical index child has no process identity");
381
+ const identity = await daemonProcessIdentity(child.pid);
382
+ if (closedSettled || timedOut || stopInFlight)
383
+ return;
384
+ if (identity.state !== "identified")
385
+ throw new Error("Historical index child process generation could not be verified");
386
+ workerIdentity = identity.identity;
387
+ watchdog.send({ type: "arm", pid: child.pid, identity: identity.identity, graceMs: stopGraceMs }, (error) => {
388
+ if (error)
389
+ watchdogFailed(error);
390
+ });
391
+ })().catch((error) => watchdogFailed(error));
392
+ armIdleDeadline(startupMs, "starting");
393
+ return { pid: child.pid, watchdogPid: watchdog.pid, started, result, stop };
394
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,109 @@
1
+ import { backgroundUploadBytesPerSecond, readLocalSettings } from "./machine-settings.js";
2
+ import { createBackgroundCpuYield } from "./background-cpu.js";
3
+ import { runBackgroundIndexWork } from "./index-background-work.js";
4
+ import { SealkeepError } from "./errors.js";
5
+ import { acquireDaemonSignerGrant } from "./control-plane.js";
6
+ import { backgroundWorkerErrorToWire } from "./background-worker-error.js";
7
+ function send(message) {
8
+ return new Promise((resolve) => {
9
+ if (!process.send || !process.connected) {
10
+ resolve();
11
+ return;
12
+ }
13
+ process.send(message, () => resolve());
14
+ });
15
+ }
16
+ let completed = false;
17
+ const cancellation = new AbortController();
18
+ const cancel = () => {
19
+ if (completed || cancellation.signal.aborted)
20
+ return;
21
+ const error = new Error("Historical index owner requested shutdown");
22
+ error.name = "AbortError";
23
+ cancellation.abort(error);
24
+ };
25
+ // A normal daemon stop first asks the child to abort. Chunk-independent remote
26
+ // archives are indexed directly from their authenticated stream, so cancellation
27
+ // closes that read without owning archive-sized scratch space. The parent's
28
+ // existing two-second SIGKILL fallback remains the hard bound for an unresponsive
29
+ // provider or synchronous legacy decrypt.
30
+ process.once("SIGTERM", cancel);
31
+ process.once("SIGINT", cancel);
32
+ // If the daemon dies without reaching close(), the IPC pipe still tells this
33
+ // child it has lost its sole owner. Give an active remote read one short
34
+ // graceful window to close. The independent, never-paused sibling watchdog
35
+ // supplies the hard orphan bound even when this event loop is SIGSTOP-paused
36
+ // or stuck in synchronous work and cannot observe the disconnect itself.
37
+ process.once("disconnect", () => {
38
+ if (completed)
39
+ return;
40
+ cancel();
41
+ setTimeout(() => process.exit(0), 2_000).unref();
42
+ });
43
+ // One IPC request, one build, one result. Keeping this child single-purpose is
44
+ // what makes daemon cancellation exact: there is never a second queued build
45
+ // left behind after the parent replaces or shuts down the service.
46
+ process.once("message", async (wire) => {
47
+ const request = wire && typeof wire === "object" ? wire : {};
48
+ if (request.type !== "build" || typeof request.dataDir !== "string" || typeof request.phrase !== "string" || typeof request.sync !== "boolean"
49
+ || typeof request.scratchGeneration !== "string"
50
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(request.scratchGeneration)) {
51
+ await send({ type: "error", error: { message: "Historical index child received an invalid build request" } });
52
+ completed = true;
53
+ if (process.connected)
54
+ process.disconnect();
55
+ return;
56
+ }
57
+ // Only an explicit capability from this exact owning daemon crosses IPC.
58
+ // Other live daemons in the parent's process cannot implicitly authorize us.
59
+ const signerGrant = request.signerAuthorized === true ? acquireDaemonSignerGrant() : undefined;
60
+ try {
61
+ await send({ type: "started" });
62
+ const backgroundTransferBytesPerSecond = backgroundUploadBytesPerSecond(await readLocalSettings(request.dataDir));
63
+ const cpuTarget = typeof request.cpuTarget === "number" ? request.cpuTarget : 0.30;
64
+ const maxRssBytes = typeof request.maxRssBytes === "number" ? request.maxRssBytes : 1536 * 1024 * 1024;
65
+ const yieldForUpgrade = createBackgroundCpuYield({
66
+ target: cpuTarget,
67
+ budget: null,
68
+ signal: cancellation.signal,
69
+ });
70
+ const reportProgress = () => { void send({ type: "progress" }); };
71
+ const result = await runBackgroundIndexWork(request.dataDir, request.phrase, {
72
+ sync: request.sync,
73
+ signal: cancellation.signal,
74
+ upgradeProgress: reportProgress,
75
+ upgradeYield: async () => {
76
+ if (process.memoryUsage().rss > maxRssBytes) {
77
+ throw new SealkeepError("operation_timeout", `Historical index publication reached its ${Math.round(maxRssBytes / (1024 * 1024))} MiB memory safety ceiling. Sealkeep will retry later.`, { operation: "historical_index_publication", reason: "memory_ceiling", maxRssBytes, retryable: true });
78
+ }
79
+ await yieldForUpgrade();
80
+ reportProgress();
81
+ },
82
+ scheduling: {
83
+ lanes: Number.isInteger(request.lanes) ? request.lanes : 1,
84
+ cpuTarget: typeof request.cpuTarget === "number" ? request.cpuTarget : 0.30,
85
+ maxRssBytes,
86
+ backgroundTransferBytesPerSecond,
87
+ scratchGeneration: request.scratchGeneration,
88
+ // Segments mode only: this background child cannot hold a merged
89
+ // index above this many tokens, so publishFromSegments abandons
90
+ // publication (pushed: false) rather than producing one and failing.
91
+ publishMaxTokens: 400_000,
92
+ },
93
+ onActivity: () => { void send({ type: "progress" }); },
94
+ onProgress: () => { void send({ type: "progress" }); },
95
+ });
96
+ await send({ type: "result", result });
97
+ }
98
+ catch (cause) {
99
+ await send({ type: "error", error: backgroundWorkerErrorToWire(cause) });
100
+ }
101
+ finally {
102
+ signerGrant?.release();
103
+ // Closing the IPC handle lets the otherwise listener-free child exit on
104
+ // its own; no secret or build worker remains resident after completion.
105
+ completed = true;
106
+ if (process.connected)
107
+ process.disconnect();
108
+ }
109
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The segment manifest — the list of immutable sealed segments that make up
3
+ * the content index — and the compaction that keeps that list short.
4
+ *
5
+ * Sealed segments never change, so the index over them need not be rewritten
6
+ * either: appending memory means appending a segment, removing an archive
7
+ * means tombstoning it, and staying fast means occasionally streaming a few
8
+ * small segments into one bigger immutable one. The manifest itself is the
9
+ * only thing ever rewritten, and every rewrite is temp-write + rename under a
10
+ * lock, so two processes can never interleave a read-modify-write.
11
+ */
12
+ import { type SegmentEntry, type SegmentManifest, type SegmentReader, type SegmentRow, type SegmentWriter } from "./index-segment-types.js";
13
+ export declare function readManifest(dataDir: string): Promise<SegmentManifest>;
14
+ /** Read-modify-write under the manifest lock. Every successful write gets a
15
+ * fresh `generation`, whether or not `mutate` actually changed anything. */
16
+ export declare function updateManifest(dataDir: string, mutate: (current: SegmentManifest) => SegmentManifest | Promise<SegmentManifest>): Promise<SegmentManifest>;
17
+ export declare function appendSegment(dataDir: string, entry: SegmentEntry): Promise<SegmentManifest>;
18
+ export declare function retireArchives(dataDir: string, archiveIds: readonly string[]): Promise<SegmentManifest>;
19
+ /**
20
+ * Ids to merge, or `[]`. A "remote" segment (the whole account index pulled
21
+ * from the cloud) is never a candidate: it never merges with a locally-built
22
+ * one, and it does not count toward the segment-count trigger either.
23
+ */
24
+ export declare function planCompaction(manifest: SegmentManifest, options?: {
25
+ maxSegments?: number;
26
+ maxSmallBytes?: number;
27
+ }): string[];
28
+ /**
29
+ * The compaction merge on its own: the union/cap/tombstone/summary rules that
30
+ * turn several sealed segments' rows into one segment's worth, streamed so no
31
+ * more than one batch of archive or token rows is held at once.
32
+ *
33
+ * `readers` must be given oldest first (manifest order) — every "later wins"
34
+ * rule below is defined relative to that order. Meta rows are the one
35
+ * exception to streaming: a segment holds only a few hundred bytes of
36
+ * metadata per archive, so the whole merged map is kept in memory (also
37
+ * needed to rank token postings by `archivedAt`) and emitted last.
38
+ */
39
+ export declare function mergeSegmentRows(readers: readonly SegmentReader[], tombstones: ReadonlySet<string>, options?: {
40
+ signal?: AbortSignal;
41
+ onYield?: () => void | Promise<void>;
42
+ }): AsyncGenerator<SegmentRow>;
43
+ export declare function compactSegments(dataDir: string, phrase: string, segmentIds: readonly string[], tools: {
44
+ open: (dataDir: string, phrase: string, segmentId: string) => Promise<SegmentReader>;
45
+ write: SegmentWriter;
46
+ remove: (dataDir: string, segmentId: string) => Promise<void>;
47
+ }, options?: {
48
+ signal?: AbortSignal;
49
+ onYield?: () => void | Promise<void>;
50
+ }): Promise<SegmentEntry>;
51
+ /** Segment ids present on disk that the manifest does not list. */
52
+ export declare function orphanSegmentIds(dataDir: string, listed: (dataDir: string) => Promise<string[]>): Promise<string[]>;