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,4 +1,4 @@
1
- import { type AgentId } from "./adapters.js";
1
+ import { detectAgents, findTranscripts, type AgentId } from "./adapters.js";
2
2
  import { ArchiveQueue, type ArchiveJob } from "./queue.js";
3
3
  export type WatcherEvent = {
4
4
  agent: AgentId;
@@ -10,10 +10,16 @@ export type WatcherEvent = {
10
10
  export type WatcherOptions = {
11
11
  home?: string;
12
12
  agents?: AgentId[];
13
+ /** Overrides saved path exclusions, principally for isolated callers/tests. */
14
+ exclusions?: string[];
13
15
  /** Wait for a file to stop changing before queuing it. */
14
16
  debounceMs?: number;
15
17
  queue?: ArchiveQueue;
16
18
  onEvent?: (event: WatcherEvent) => void;
19
+ /** Dependency seam for deterministic close/reconciliation tests. */
20
+ detectAgents?: typeof detectAgents;
21
+ /** Dependency seam for deterministic scan lifecycle tests. */
22
+ findTranscripts?: typeof findTranscripts;
17
23
  };
18
24
  export type TranscriptWatcher = {
19
25
  roots: string[];
@@ -1,8 +1,9 @@
1
1
  import { watch } from "node:fs";
2
2
  import { stat } from "node:fs/promises";
3
- import { join, resolve } from "node:path";
4
- import { detectAgents } from "./adapters.js";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { detectAgents, findTranscripts } from "./adapters.js";
5
5
  import { ArchiveQueue } from "./queue.js";
6
+ import { localSettingsPath, readLocalSettings, transcriptPathIsExcluded } from "./machine-settings.js";
6
7
  import { applySyncRules, resolveSyncRules } from "./sync-rules.js";
7
8
  /**
8
9
  * Watches adapter transcript roots and records archive intents.
@@ -14,20 +15,120 @@ import { applySyncRules, resolveSyncRules } from "./sync-rules.js";
14
15
  export async function startTranscriptWatcher(dataDir, options = {}) {
15
16
  const queue = options.queue ?? new ArchiveQueue(dataDir);
16
17
  const debounceMs = options.debounceMs ?? 2000;
17
- const installs = (await detectAgents(options.home)).filter((install) => !options.agents || options.agents.includes(install.agent));
18
- const watchers = [];
18
+ const settings = await readLocalSettings(dataDir);
19
+ let selectedAgents = new Set(options.agents ?? settings.watchedAgents);
20
+ const watchers = new Map();
19
21
  const timers = new Map();
22
+ // Returned by reference and mutated during reconciliation, so daemon status
23
+ // reflects a Settings change without replacing the TranscriptWatcher object.
20
24
  const roots = [];
21
25
  let closed = false;
26
+ let settingsWatcher;
27
+ let settingsTimer;
28
+ let reconciliation = Promise.resolve();
29
+ let closeInFlight;
22
30
  // Rules are resolved once at startup and refreshed only on scan — the daemon's
23
31
  // periodic reconciliation point — rather than per event, so a burst of writes
24
32
  // cannot turn into a burst of account lookups.
25
33
  let rules = (await resolveSyncRules(dataDir)).rules;
34
+ let exclusions = options.exclusions ?? settings.exclusions;
35
+ const excluded = (path) => transcriptPathIsExcluded(path, exclusions, options.home);
36
+ function schedule(agent, path) {
37
+ const existing = timers.get(path);
38
+ if (existing)
39
+ clearTimeout(existing.timer);
40
+ const timer = setTimeout(() => { timers.delete(path); void admitAndEnqueue(agent, path); }, debounceMs);
41
+ timer.unref?.();
42
+ timers.set(path, { agent, timer });
43
+ }
44
+ function attach(agent, root) {
45
+ if (closed)
46
+ return;
47
+ try {
48
+ const watcher = watch(root, { recursive: true }, (_event, filename) => {
49
+ if (!filename || !selectedAgents.has(agent))
50
+ return;
51
+ const path = resolve(join(root, filename.toString()));
52
+ if (!path.endsWith(".jsonl") || excluded(path))
53
+ return;
54
+ schedule(agent, path);
55
+ });
56
+ watcher.on("error", (error) => options.onEvent?.({ agent, path: root, error: error.message }));
57
+ watchers.set(root, { agent, watcher });
58
+ }
59
+ catch (error) {
60
+ options.onEvent?.({ agent, path: root, error: error instanceof Error ? error.message : "watch failed" });
61
+ }
62
+ }
63
+ /**
64
+ * Settings are live daemon policy, not merely restart configuration. Rebuild
65
+ * the small set of native roots at the same cadence as the safety scan: stop
66
+ * observing disabled agents, attach newly enabled/created stores, and cancel
67
+ * debounce work that was admitted under the previous selection.
68
+ */
69
+ async function reconcileRoots(refreshSettings) {
70
+ if (closed)
71
+ return;
72
+ if (refreshSettings) {
73
+ const latest = await readLocalSettings(dataDir);
74
+ if (options.agents === undefined)
75
+ selectedAgents = new Set(latest.watchedAgents);
76
+ if (options.exclusions === undefined)
77
+ exclusions = latest.exclusions;
78
+ }
79
+ const installs = await (options.detectAgents ?? detectAgents)(options.home);
80
+ const desired = new Map();
81
+ for (const install of installs) {
82
+ if (!selectedAgents.has(install.agent))
83
+ continue;
84
+ for (const root of install.transcriptRoots) {
85
+ const entry = await stat(root).catch(() => null);
86
+ if (entry?.isDirectory())
87
+ desired.set(root, install.agent);
88
+ }
89
+ }
90
+ // close() may have run while settings, agent detection, or root stats were
91
+ // awaiting I/O. Mutating the native watcher map after that point would
92
+ // resurrect handles that no caller can close.
93
+ if (closed)
94
+ return;
95
+ for (const [root, current] of watchers) {
96
+ if (desired.get(root) === current.agent)
97
+ continue;
98
+ current.watcher.close();
99
+ watchers.delete(root);
100
+ }
101
+ for (const [root, agent] of desired)
102
+ if (!watchers.has(root))
103
+ attach(agent, root);
104
+ for (const [path, pending] of timers) {
105
+ if (selectedAgents.has(pending.agent))
106
+ continue;
107
+ clearTimeout(pending.timer);
108
+ timers.delete(path);
109
+ }
110
+ roots.splice(0, roots.length, ...[...watchers.keys()].sort());
111
+ }
112
+ function requestReconcile(refreshSettings) {
113
+ if (closed)
114
+ return Promise.resolve();
115
+ // A settings rename and a periodic scan can arrive together. Serialize
116
+ // their close/attach mutations so neither resurrects a root the other just
117
+ // disabled or loses a newly enabled one.
118
+ const run = reconciliation.then(() => reconcileRoots(refreshSettings));
119
+ reconciliation = run.catch(() => undefined);
120
+ return run;
121
+ }
26
122
  async function discover() {
27
- const { findTranscripts } = await import("./adapters.js");
28
123
  const found = [];
29
- for (const install of installs)
30
- found.push(...await findTranscripts(install.agent, options.home, 500));
124
+ for (const agent of selectedAgents) {
125
+ if (closed)
126
+ break;
127
+ found.push(...await (options.findTranscripts ?? findTranscripts)(agent, options.home, undefined, {
128
+ exclude: excluded,
129
+ includeProjects: false,
130
+ }));
131
+ }
31
132
  return found;
32
133
  }
33
134
  /**
@@ -45,7 +146,7 @@ export async function startTranscriptWatcher(dataDir, options = {}) {
45
146
  return;
46
147
  try {
47
148
  const info = await stat(path).catch(() => null);
48
- if (!info?.isFile())
149
+ if (closed || !info?.isFile())
49
150
  return;
50
151
  const { job, deduped } = await queue.enqueue({ sourcePath: path, agent, event: "watcher" });
51
152
  options.onEvent?.({ agent, path, job, deduped });
@@ -58,64 +159,106 @@ export async function startTranscriptWatcher(dataDir, options = {}) {
58
159
  // live events, enqueueNow — so a session the sync rules exclude is never queued
59
160
  // no matter which of those triggered it.
60
161
  async function admitAndEnqueue(agent, path) {
61
- if (closed || !(await admitted(path)))
162
+ if (closed || !selectedAgents.has(agent) || excluded(path) || !(await admitted(path)))
62
163
  return;
63
164
  await enqueue(agent, path);
64
165
  }
65
- function schedule(agent, path) {
66
- const existing = timers.get(path);
67
- if (existing)
68
- clearTimeout(existing);
69
- const timer = setTimeout(() => { timers.delete(path); void admitAndEnqueue(agent, path); }, debounceMs);
70
- timer.unref?.();
71
- timers.set(path, timer);
72
- }
73
- for (const install of installs) {
74
- for (const root of install.transcriptRoots) {
75
- if (!(await stat(root).catch(() => null)))
76
- continue;
77
- roots.push({ agent: install.agent, root });
78
- try {
79
- const watcher = watch(root, { recursive: true }, (_event, filename) => {
80
- if (!filename)
81
- return;
82
- const path = resolve(join(root, filename.toString()));
83
- if (!path.endsWith(".jsonl"))
84
- return;
85
- schedule(install.agent, path);
86
- });
87
- watcher.on("error", (error) => options.onEvent?.({ agent: install.agent, path: root, error: error.message }));
88
- watchers.push(watcher);
166
+ async function scanCurrent() {
167
+ if (closed)
168
+ return 0;
169
+ // Reconcile identity/policy first: a settings-triggered scan must detach a
170
+ // disabled store before it walks anything, and must attach an enabled store
171
+ // even if the account rules lookup is temporarily slow.
172
+ await requestReconcile(true);
173
+ if (closed)
174
+ return 0;
175
+ rules = (await resolveSyncRules(dataDir)).rules;
176
+ if (closed)
177
+ return 0;
178
+ const found = await discover();
179
+ if (closed)
180
+ return 0;
181
+ const included = new Set(applySyncRules(found, rules).included.map((item) => item.path));
182
+ const candidates = found.filter((candidate) => included.has(candidate.path));
183
+ const outcomes = await queue.enqueueMany(candidates.map((candidate) => ({
184
+ sourcePath: candidate.path, agent: candidate.agent, event: "watcher"
185
+ })));
186
+ for (const outcome of outcomes) {
187
+ const agent = outcome.request.agent;
188
+ if (outcome.ok) {
189
+ options.onEvent?.({ agent, path: outcome.request.sourcePath, job: outcome.job, deduped: outcome.deduped });
89
190
  }
90
- catch (error) {
91
- options.onEvent?.({ agent: install.agent, path: root, error: error instanceof Error ? error.message : "watch failed" });
191
+ else {
192
+ options.onEvent?.({ agent, path: outcome.request.sourcePath, error: outcome.error instanceof Error ? outcome.error.message : "enqueue failed" });
92
193
  }
93
194
  }
195
+ return found.length;
196
+ }
197
+ await requestReconcile(false);
198
+ // Settings are written by atomic rename, so watch the containing directory
199
+ // rather than the old inode. This lane is independent of the archive tick:
200
+ // disabling an agent closes its native watcher promptly even on a five-minute
201
+ // daemon schedule, and enabling one attaches as soon as its store exists.
202
+ if (options.agents === undefined || options.exclusions === undefined) {
203
+ const wanted = basename(localSettingsPath(dataDir));
204
+ try {
205
+ settingsWatcher = watch(dataDir, (_event, filename) => {
206
+ if (closed || (filename && basename(filename.toString()) !== wanted))
207
+ return;
208
+ if (settingsTimer)
209
+ clearTimeout(settingsTimer);
210
+ settingsTimer = setTimeout(() => {
211
+ settingsTimer = undefined;
212
+ // Enabling a store should also pick up transcripts already present;
213
+ // otherwise the new root attaches promptly but its backlog still
214
+ // waits for the daemon's long safety-scan interval.
215
+ void scanCurrent().catch((error) => options.onEvent?.({
216
+ agent: "codex",
217
+ path: localSettingsPath(dataDir),
218
+ error: error instanceof Error ? error.message : "settings reconciliation failed",
219
+ }));
220
+ }, 25);
221
+ settingsTimer.unref?.();
222
+ });
223
+ settingsWatcher.on("error", (error) => options.onEvent?.({ agent: "codex", path: dataDir, error: error.message }));
224
+ }
225
+ catch (error) {
226
+ options.onEvent?.({ agent: "codex", path: dataDir, error: error instanceof Error ? error.message : "settings watch failed" });
227
+ }
94
228
  }
95
229
  return {
96
- roots: roots.map((entry) => entry.root),
230
+ roots,
97
231
  enqueueNow: (agent, path) => admitAndEnqueue(agent, resolve(path)),
98
- async scan() {
99
- // The reconciliation point: pick up any change to the rules themselves,
100
- // then decide admission once for the whole batch rather than once per
101
- // candidate the way a live event has to.
102
- rules = (await resolveSyncRules(dataDir)).rules;
103
- const found = await discover();
104
- const included = new Set(applySyncRules(found, rules).included.map((item) => item.path));
105
- for (const candidate of found) {
106
- if (included.has(candidate.path))
107
- await enqueue(candidate.agent, candidate.path);
108
- }
109
- return found.length;
110
- },
232
+ scan: scanCurrent,
111
233
  pending: () => timers.size,
112
- async close() {
234
+ close() {
235
+ if (closeInFlight)
236
+ return closeInFlight;
113
237
  closed = true;
114
- for (const timer of timers.values())
115
- clearTimeout(timer);
116
- timers.clear();
117
- for (const watcher of watchers)
118
- watcher.close();
238
+ closeInFlight = (async () => {
239
+ const closeObservedHandles = () => {
240
+ if (settingsTimer)
241
+ clearTimeout(settingsTimer);
242
+ settingsTimer = undefined;
243
+ settingsWatcher?.close();
244
+ settingsWatcher = undefined;
245
+ for (const { timer } of timers.values())
246
+ clearTimeout(timer);
247
+ timers.clear();
248
+ for (const { watcher } of watchers.values())
249
+ watcher.close();
250
+ watchers.clear();
251
+ roots.splice(0, roots.length);
252
+ };
253
+ closeObservedHandles();
254
+ // A reconciliation which passed its first closed check before this
255
+ // call owns no handles after its final check, but join it before
256
+ // returning so close is a complete lifecycle boundary. The second
257
+ // sweep also protects older/injected reconciliation implementations.
258
+ await reconciliation;
259
+ closeObservedHandles();
260
+ })();
261
+ return closeInFlight;
119
262
  }
120
263
  };
121
264
  }
@@ -1,11 +1,24 @@
1
1
  import { ArchiveQueue, type ArchiveJob } from "./queue.js";
2
+ import { archiveFile } from "./vault.js";
2
3
  import { Reservations } from "./disk.js";
3
4
  /** Parallel seals are capped whatever the config asks: sealing is disk- and CPU-bound, and a machine short of space is rarely long on either. */
4
5
  export declare const MAX_SEAL_CONCURRENCY = 4;
6
+ /** Long enough for a slow local chunk, short enough to expire before the
7
+ * ordinary five-minute queue lease can be mistaken for useful work. */
8
+ export declare const DEFAULT_SEAL_IDLE_MS: number;
9
+ export type ActiveSeal = {
10
+ project: string;
11
+ projectKey: string | null;
12
+ sourcePath: string;
13
+ };
5
14
  export type DrainOptions = {
6
15
  queue?: ArchiveQueue;
7
16
  owner?: string;
8
17
  max?: number;
18
+ /** Off to seal without the end-of-drain active-project announcement (index sync + finished lines). */
19
+ announce?: boolean;
20
+ /** Daemon handoff: report active seals without starting another index writer. */
21
+ onActiveSealed?: (seals: readonly ActiveSeal[]) => void;
9
22
  /** Off only for tests that want to archive without a real filesystem behind them. */
10
23
  requireSpace?: boolean;
11
24
  /**
@@ -16,9 +29,8 @@ export type DrainOptions = {
16
29
  */
17
30
  reserveBytes?: number;
18
31
  /**
19
- * How many seals may run at once. Defaults to a `sealConcurrency` field in
20
- * the vault's config.json when one is present (readConfig tolerates and
21
- * preserves fields it does not know), and to 1 otherwise. Clamped to
32
+ * Internal/test seam for how many seals may run at once. Production callers
33
+ * omit it and use the safe default of one. Clamped to
22
34
  * 1..MAX_SEAL_CONCURRENCY.
23
35
  */
24
36
  concurrency?: number;
@@ -35,6 +47,18 @@ export type DrainOptions = {
35
47
  * and off by default in archiveFile itself, so nothing else changes.
36
48
  */
37
49
  delta?: boolean;
50
+ /** Cancels the owned seal and leaves its queue row retryable. */
51
+ signal?: AbortSignal;
52
+ /** Maximum silence between source/provider byte progress events. */
53
+ sealIdleMs?: number;
54
+ /** Background-only duty cycle. Manual/foreground callers omit it. */
55
+ backgroundCpuTarget?: number;
56
+ /** Deterministic CPU scheduling seams. */
57
+ cpuUsage?: typeof process.cpuUsage;
58
+ now?: () => number;
59
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
60
+ /** Deterministic liveness seam. Production always uses archiveFile. */
61
+ archive?: typeof archiveFile;
38
62
  };
39
63
  /**
40
64
  * Encrypts queued transcripts. The recovery phrase lives only in this process: