@reticlehq/server 3.7.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (578) hide show
  1. package/SKILL.md +59 -68
  2. package/bin/reticle.js +9 -2
  3. package/dist/command/cli/adhoc-suite.js +1 -1
  4. package/dist/command/cli/adhoc-verdict.d.ts +12 -0
  5. package/dist/command/cli/adhoc-verdict.js +62 -4
  6. package/dist/command/cli/answers/gate-hook-message.d.ts +2 -0
  7. package/dist/command/cli/answers/gate-hook-message.js +5 -1
  8. package/dist/command/cli/auth/cloud-session.d.ts +11 -19
  9. package/dist/command/cli/auth/cloud-session.js +23 -0
  10. package/dist/command/cli/auth/device-flow.d.ts +3 -37
  11. package/dist/command/cli/cli-doctor.js +66 -21
  12. package/dist/command/cli/cli-flow-commands.js +53 -11
  13. package/dist/command/cli/cli-kill.d.ts +8 -0
  14. package/dist/command/cli/cli-kill.js +43 -3
  15. package/dist/command/cli/cli-parse-feedback.d.ts +4 -0
  16. package/dist/command/cli/cli-parse-feedback.js +13 -1
  17. package/dist/command/cli/cli-parse-grammar.d.ts +1 -2
  18. package/dist/command/cli/cli-parse-grammar.js +1 -6
  19. package/dist/command/cli/cli-parse-serve.d.ts +24 -0
  20. package/dist/command/cli/cli-parse-serve.js +124 -0
  21. package/dist/command/cli/cli-parse.d.ts +12 -11
  22. package/dist/command/cli/cli-parse.js +100 -123
  23. package/dist/command/cli/cli-usage.d.ts +0 -11
  24. package/dist/command/cli/cli-usage.js +43 -16
  25. package/dist/command/cli/cli-verify.d.ts +1 -1
  26. package/dist/command/cli/cli-verify.js +29 -14
  27. package/dist/command/cli/cloud-cli.js +27 -9
  28. package/dist/command/cli/cloud-kit.js +3 -3
  29. package/dist/command/cli/cloud-link.d.ts +1 -20
  30. package/dist/command/cli/cloud-link.js +2 -0
  31. package/dist/command/cli/cloud-login.d.ts +6 -2
  32. package/dist/command/cli/cloud-login.js +13 -41
  33. package/dist/command/cli/config/checkout.d.ts +10 -0
  34. package/dist/command/cli/config/checkout.js +78 -0
  35. package/dist/command/cli/doctor/doctor-agent-link.d.ts +23 -0
  36. package/dist/command/cli/doctor/doctor-agent-link.js +28 -0
  37. package/dist/command/cli/doctor/doctor-coverage-line.js +1 -7
  38. package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
  39. package/dist/command/cli/doctor/doctor-rows.js +4 -0
  40. package/dist/command/cli/drive/drive-attach.d.ts +5 -0
  41. package/dist/command/cli/drive/drive-attach.js +21 -1
  42. package/dist/command/cli/hook/codex-transcript.d.ts +20 -0
  43. package/dist/command/cli/hook/codex-transcript.js +224 -0
  44. package/dist/command/cli/hook/episode-structure.d.ts +24 -0
  45. package/dist/command/cli/hook/episode-structure.js +138 -0
  46. package/dist/command/cli/hook/hook-command.d.ts +9 -0
  47. package/dist/command/cli/hook/hook-command.js +211 -0
  48. package/dist/command/cli/hook/hook-fast.d.ts +31 -0
  49. package/dist/command/cli/hook/hook-fast.js +136 -0
  50. package/dist/command/cli/hook/prompt-hook.d.ts +30 -0
  51. package/dist/command/cli/hook/prompt-hook.js +199 -0
  52. package/dist/command/cli/hook/tool-hook.d.ts +21 -0
  53. package/dist/command/cli/hook/tool-hook.js +259 -0
  54. package/dist/command/cli/launch/cli-launch.d.ts +2 -0
  55. package/dist/command/cli/launch/cli-launch.js +22 -2
  56. package/dist/command/cli/lifecycle/daemon-lifecycle.js +28 -2
  57. package/dist/command/cli/mcp-command.d.ts +9 -0
  58. package/dist/command/cli/mcp-command.js +127 -41
  59. package/dist/command/cli/open-navigate.d.ts +43 -0
  60. package/dist/command/cli/open-navigate.js +85 -0
  61. package/dist/command/cli/ports/port-holder.d.ts +16 -4
  62. package/dist/command/cli/ports/port-holder.js +58 -12
  63. package/dist/command/cli/ports/resolve/cli-port.js +18 -2
  64. package/dist/command/cli/project-name.d.ts +1 -7
  65. package/dist/command/cli/project-name.js +1 -0
  66. package/dist/command/cli/report-command.d.ts +8 -2
  67. package/dist/command/cli/report-command.js +95 -18
  68. package/dist/command/cli/status-fields.d.ts +9 -2
  69. package/dist/command/cli/status-fields.js +148 -4
  70. package/dist/command/cli/try-command.d.ts +3 -4
  71. package/dist/command/cli/try-command.js +6 -11
  72. package/dist/command/cli/verify-results.d.ts +4 -1
  73. package/dist/command/cli/verify-results.js +3 -1
  74. package/dist/command/cli.d.ts +1 -7
  75. package/dist/command/cli.js +79 -160
  76. package/dist/command/daemon/daemon-resilience.d.ts +6 -1
  77. package/dist/command/daemon/daemon-resilience.js +31 -18
  78. package/dist/command/daemon/daemon.d.ts +11 -0
  79. package/dist/command/daemon/daemon.js +70 -2
  80. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +2 -0
  81. package/dist/command/daemon/lifetime/daemon-usefulness.js +13 -2
  82. package/dist/command/daemon/wake-decision.js +4 -2
  83. package/dist/command/daemon/win32-escape.d.ts +32 -0
  84. package/dist/command/daemon/win32-escape.js +119 -0
  85. package/dist/command/setup/agent-configs.d.ts +9 -0
  86. package/dist/command/setup/agent-configs.js +9 -0
  87. package/dist/command/setup/auto-approve.d.ts +10 -3
  88. package/dist/command/setup/auto-approve.js +51 -3
  89. package/dist/command/setup/bringup/dev-server-files.d.ts +1 -9
  90. package/dist/command/setup/bringup/dev-server-wait.js +5 -1
  91. package/dist/command/setup/bringup/ensure-daemon.js +4 -7
  92. package/dist/command/setup/init/init-host.js +6 -0
  93. package/dist/command/setup/init/init-port.js +5 -4
  94. package/dist/command/setup/init/init-runtime.js +7 -1
  95. package/dist/command/setup/node-effects.d.ts +8 -0
  96. package/dist/command/setup/node-effects.js +36 -4
  97. package/dist/command/setup/probe/dev-port.d.ts +26 -0
  98. package/dist/command/setup/probe/dev-port.js +117 -0
  99. package/dist/command/setup/probe/page-probe.d.ts +1 -1
  100. package/dist/command/setup/probe/page-probe.js +11 -3
  101. package/dist/command/setup/remaining-steps.d.ts +0 -7
  102. package/dist/command/setup/remaining-steps.js +3 -2
  103. package/dist/command/setup/run-setup.d.ts +11 -0
  104. package/dist/command/setup/run-setup.js +33 -5
  105. package/dist/command/setup/setup-command.d.ts +2 -0
  106. package/dist/command/setup/setup-command.js +22 -4
  107. package/dist/command/update/updater.d.ts +1 -6
  108. package/dist/command/update/updater.js +4 -5
  109. package/dist/command/update/version-seen.d.ts +24 -0
  110. package/dist/command/update/version-seen.js +65 -0
  111. package/dist/command/version/peer-announce.d.ts +9 -1
  112. package/dist/command/version/peer-announce.js +25 -2
  113. package/dist/command/version/sdk-fix.d.ts +5 -6
  114. package/dist/command/version/sdk-fix.js +17 -32
  115. package/dist/command/version/version-skew.d.ts +16 -1
  116. package/dist/command/version/version-skew.js +84 -10
  117. package/dist/features/crawl/crawl-tools.js +2 -2
  118. package/dist/features/crawl/crawl.js +2 -0
  119. package/dist/features/exhaust/ledger.d.ts +15 -96
  120. package/dist/features/exhaust/ledger.js +3 -3
  121. package/dist/features/harness/drive-runs.d.ts +28 -4
  122. package/dist/features/harness/drive-runs.js +99 -7
  123. package/dist/features/harness/drivers.d.ts +1 -1
  124. package/dist/features/harness/drivers.js +1 -1
  125. package/dist/features/harness/harness.d.ts +5 -0
  126. package/dist/features/harness/harness.js +24 -12
  127. package/dist/features/harness/platform/platform-drives.d.ts +7 -1
  128. package/dist/features/harness/platform/platform-drives.js +12 -7
  129. package/dist/features/harness/platform/refusal.d.ts +1 -1
  130. package/dist/features/harness/platform/refusal.js +4 -4
  131. package/dist/features/harness/platform/remote-drive.js +70 -28
  132. package/dist/features/harness/platform/server-driver.js +22 -1
  133. package/dist/features/harness/platform-config.d.ts +1 -10
  134. package/dist/features/harness/platform-config.js +0 -20
  135. package/dist/features/license/license.d.ts +2 -14
  136. package/dist/features/visual/step-shots.d.ts +33 -1
  137. package/dist/features/visual/step-shots.js +155 -1
  138. package/dist/features/visual/visual-tools.js +12 -4
  139. package/dist/hooks/coding-agents.d.ts +6 -0
  140. package/dist/hooks/coding-agents.js +24 -3
  141. package/dist/hooks/episode-stamp.d.ts +32 -0
  142. package/dist/hooks/episode-stamp.js +61 -0
  143. package/dist/hooks/hook-commands.js +19 -9
  144. package/dist/index.d.ts +5 -1
  145. package/dist/index.js +66 -24
  146. package/dist/judgement/app-graph/journal-steps.d.ts +8 -0
  147. package/dist/judgement/app-graph/journal-steps.js +92 -0
  148. package/dist/judgement/capsule/capsule-store.d.ts +2 -39
  149. package/dist/judgement/domain/domain-model.js +1 -10
  150. package/dist/judgement/domain/domain-tools.js +4 -2
  151. package/dist/judgement/runs/artifact/run-store.d.ts +28 -8
  152. package/dist/judgement/runs/artifact/run-store.js +153 -28
  153. package/dist/judgement/runs/default-run-id.d.ts +3 -1
  154. package/dist/judgement/runs/default-run-id.js +15 -3
  155. package/dist/judgement/runs/drive-run.d.ts +3 -3
  156. package/dist/judgement/runs/drive-run.js +11 -7
  157. package/dist/judgement/runs/run-ledger.d.ts +21 -0
  158. package/dist/judgement/runs/run-ledger.js +143 -0
  159. package/dist/judgement/runs/runner-port.d.ts +1 -1
  160. package/dist/judgement/runs/runner-port.js +8 -2
  161. package/dist/judgement/runs/verification-sync.d.ts +4 -3
  162. package/dist/judgement/runs/verification-sync.js +27 -5
  163. package/dist/language/flows/affected-tools.js +2 -1
  164. package/dist/language/flows/annotate-notes/annotate.js +8 -3
  165. package/dist/language/flows/annotate-tools.js +1 -1
  166. package/dist/language/flows/change/affected.d.ts +8 -0
  167. package/dist/language/flows/change/affected.js +20 -7
  168. package/dist/language/flows/decision.js +19 -4
  169. package/dist/language/flows/fields/flow-secret-field.d.ts +9 -0
  170. package/dist/language/flows/fields/flow-secret-field.js +67 -5
  171. package/dist/language/flows/flow-anchor.d.ts +5 -0
  172. package/dist/language/flows/flow-anchor.js +4 -0
  173. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  174. package/dist/language/flows/flow-cross-step.js +9 -0
  175. package/dist/language/flows/flow-expect-grammar.js +15 -10
  176. package/dist/language/flows/flow-intent.js +6 -1
  177. package/dist/language/flows/flow-journey.d.ts +8 -0
  178. package/dist/language/flows/flow-journey.js +13 -0
  179. package/dist/language/flows/flow-lease.d.ts +27 -0
  180. package/dist/language/flows/flow-lease.js +59 -0
  181. package/dist/language/flows/flow-replay-run.d.ts +1 -87
  182. package/dist/language/flows/flow-replay-run.js +111 -310
  183. package/dist/language/flows/flow-replay.js +21 -19
  184. package/dist/language/flows/flow-replayable.d.ts +38 -0
  185. package/dist/language/flows/flow-replayable.js +179 -0
  186. package/dist/language/flows/flow-start-path.d.ts +108 -0
  187. package/dist/language/flows/flow-start-path.js +313 -0
  188. package/dist/language/flows/flow-step-runners.d.ts +23 -2
  189. package/dist/language/flows/flow-step-runners.js +191 -32
  190. package/dist/language/flows/flow-store-for-session.d.ts +8 -1
  191. package/dist/language/flows/flow-store-for-session.js +6 -0
  192. package/dist/language/flows/flow-tools.d.ts +2 -23
  193. package/dist/language/flows/flow-tools.js +63 -113
  194. package/dist/language/flows/flows.d.ts +4 -6
  195. package/dist/language/flows/flows.js +90 -15
  196. package/dist/language/flows/heal-run.d.ts +10 -4
  197. package/dist/language/flows/heal-run.js +26 -9
  198. package/dist/language/flows/recording/flake.d.ts +3 -27
  199. package/dist/language/flows/recording/flake.js +1 -1
  200. package/dist/language/flows/recording/recording-backtrack.js +1 -4
  201. package/dist/language/flows/recording/tape/recordings.d.ts +11 -1
  202. package/dist/language/flows/recording/tape/recordings.js +9 -3
  203. package/dist/language/flows/replay.d.ts +25 -5
  204. package/dist/language/flows/replay.js +83 -16
  205. package/dist/language/flows/stores/assertion-tiers-store.js +1 -1
  206. package/dist/language/flows/suite/coverage.d.ts +9 -0
  207. package/dist/language/flows/suite/coverage.js +12 -1
  208. package/dist/language/flows/suite-selection.d.ts +25 -0
  209. package/dist/language/flows/suite-selection.js +41 -9
  210. package/dist/machine/git-tracks.d.ts +6 -0
  211. package/dist/machine/git-tracks.js +36 -0
  212. package/dist/memory/cloud/background-sync.d.ts +25 -0
  213. package/dist/memory/cloud/background-sync.js +79 -0
  214. package/dist/memory/cloud/cloud-config.d.ts +12 -1
  215. package/dist/memory/cloud/cloud-config.js +39 -13
  216. package/dist/memory/cloud/cloud-sync.d.ts +3 -24
  217. package/dist/memory/cloud/cloud-sync.js +5 -2
  218. package/dist/memory/cloud/harness-config.d.ts +0 -7
  219. package/dist/memory/cloud/memory-scope.d.ts +19 -0
  220. package/dist/memory/cloud/memory-scope.js +48 -0
  221. package/dist/memory/cloud/sync-answer.d.ts +43 -0
  222. package/dist/memory/cloud/sync-answer.js +42 -0
  223. package/dist/memory/cloud/sync-cycle.d.ts +40 -10
  224. package/dist/memory/cloud/sync-cycle.js +97 -144
  225. package/dist/memory/cloud/sync-daemon.d.ts +50 -3
  226. package/dist/memory/cloud/sync-daemon.js +473 -83
  227. package/dist/memory/cloud/sync-describe.d.ts +28 -0
  228. package/dist/memory/cloud/sync-describe.js +92 -0
  229. package/dist/memory/cloud/sync-disk.d.ts +9 -1
  230. package/dist/memory/cloud/sync-disk.js +137 -33
  231. package/dist/memory/cloud/sync-flow-versions.d.ts +26 -0
  232. package/dist/memory/cloud/sync-flow-versions.js +42 -0
  233. package/dist/memory/impact/impact-store.js +32 -15
  234. package/dist/memory/intent/intent-shard.d.ts +41 -266
  235. package/dist/memory/intent/intent-tools.js +20 -61
  236. package/dist/memory/intent/record-request.d.ts +27 -0
  237. package/dist/memory/intent/record-request.js +88 -0
  238. package/dist/memory/intent/settled-words.js +9 -1
  239. package/dist/memory/journal/drive-flow.d.ts +3 -0
  240. package/dist/memory/journal/drive-flow.js +11 -41
  241. package/dist/memory/journal/envelope-store.js +1 -1
  242. package/dist/memory/journal/envelope.d.ts +6 -154
  243. package/dist/memory/journal/episode-log.d.ts +107 -0
  244. package/dist/memory/journal/episode-log.js +302 -0
  245. package/dist/memory/journal/journal-recorder.d.ts +7 -0
  246. package/dist/memory/journal/journal-recorder.js +22 -9
  247. package/dist/memory/journal/on-disk/ambient-file.d.ts +1 -7
  248. package/dist/memory/journal/on-disk/ambient-file.js +1 -1
  249. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +24 -3
  250. package/dist/memory/journal/on-disk/workspace-gitignore.js +38 -7
  251. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -0
  252. package/dist/memory/journal/self-observation.js +3 -1
  253. package/dist/memory/journal/session-end.js +23 -9
  254. package/dist/memory/journal/session-journal.js +39 -20
  255. package/dist/memory/project/artifact-root-resolver.d.ts +14 -1
  256. package/dist/memory/project/artifact-root-resolver.js +68 -23
  257. package/dist/memory/project/artifact-root.d.ts +17 -3
  258. package/dist/memory/project/artifact-root.js +47 -9
  259. package/dist/memory/project/fs/fs-port.d.ts +5 -0
  260. package/dist/memory/project/fs/fs-port.js +13 -0
  261. package/dist/memory/project/learned-routes.d.ts +3 -4
  262. package/dist/memory/project/learned-routes.js +3 -11
  263. package/dist/memory/project/link-directory.js +5 -2
  264. package/dist/memory/project/root-claims.d.ts +3 -1
  265. package/dist/memory/project/root-claims.js +41 -7
  266. package/dist/memory/project/session-root.d.ts +1 -1
  267. package/dist/memory/project/session-root.js +1 -15
  268. package/dist/memory/project/sync-status.js +5 -6
  269. package/dist/memory/recall/memory-tools.js +2 -2
  270. package/dist/memory/recall/platform-memory.d.ts +3 -1
  271. package/dist/memory/recall/platform-memory.js +9 -1
  272. package/dist/memory/recall/project-memory.js +15 -19
  273. package/dist/portal/bridge/auth-failure-reason.d.ts +13 -0
  274. package/dist/portal/bridge/auth-failure-reason.js +19 -0
  275. package/dist/portal/bridge/bridge.d.ts +19 -4
  276. package/dist/portal/bridge/bridge.js +150 -177
  277. package/dist/portal/bridge/harness-request.d.ts +15 -0
  278. package/dist/portal/bridge/harness-request.js +19 -0
  279. package/dist/portal/bridge/origin/origin-follow.d.ts +46 -0
  280. package/dist/portal/bridge/origin/origin-follow.js +110 -0
  281. package/dist/portal/bridge/pairing-token.d.ts +6 -0
  282. package/dist/portal/bridge/pairing-token.js +47 -16
  283. package/dist/portal/bridge/protocol-skew.d.ts +27 -2
  284. package/dist/portal/bridge/protocol-skew.js +66 -2
  285. package/dist/portal/bridge/session-nonces.d.ts +46 -0
  286. package/dist/portal/bridge/session-nonces.js +72 -0
  287. package/dist/portal/input/viewport-tools.d.ts +31 -0
  288. package/dist/portal/input/viewport-tools.js +106 -15
  289. package/dist/portal/pool/bounded-close.d.ts +23 -0
  290. package/dist/portal/pool/bounded-close.js +38 -0
  291. package/dist/portal/pool/browser-pool.d.ts +44 -39
  292. package/dist/portal/pool/browser-pool.js +247 -95
  293. package/dist/portal/pool/context-permissions.d.ts +9 -1
  294. package/dist/portal/pool/context-permissions.js +26 -0
  295. package/dist/portal/pool/lease-emulation.d.ts +37 -0
  296. package/dist/portal/pool/lease-emulation.js +29 -0
  297. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  298. package/dist/portal/pool/playwright-launcher.js +74 -7
  299. package/dist/portal/pool/pool-contract.d.ts +23 -1
  300. package/dist/portal/pool/pool-lease.d.ts +128 -0
  301. package/dist/portal/pool/pool-lease.js +49 -0
  302. package/dist/portal/pool/zero-install.d.ts +8 -1
  303. package/dist/portal/pool/zero-install.js +24 -2
  304. package/dist/portal/production/production-activation.d.ts +136 -0
  305. package/dist/portal/production/production-activation.js +173 -0
  306. package/dist/portal/production/production-drive.d.ts +52 -0
  307. package/dist/portal/production/production-drive.js +301 -0
  308. package/dist/portal/production/production-hello.d.ts +23 -0
  309. package/dist/portal/production/production-hello.js +23 -0
  310. package/dist/portal/realm/web-realm.js +42 -15
  311. package/dist/portal/session/control-envelope.d.ts +1 -1
  312. package/dist/portal/session/dev-server/served-document.d.ts +0 -5
  313. package/dist/portal/session/dev-server/served-document.js +6 -8
  314. package/dist/portal/session/facts/handshake-facts.d.ts +8 -1
  315. package/dist/portal/session/first-run-wiring.d.ts +3 -36
  316. package/dist/portal/session/first-run-wiring.js +10 -103
  317. package/dist/portal/session/live-control-tools.js +61 -19
  318. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  319. package/dist/portal/session/no-session-next-action.js +40 -15
  320. package/dist/portal/session/no-session-watch.d.ts +7 -0
  321. package/dist/portal/session/no-session-watch.js +67 -23
  322. package/dist/portal/session/output-budget.d.ts +10 -0
  323. package/dist/portal/session/output-budget.js +16 -0
  324. package/dist/portal/session/presence/session-lease.d.ts +2 -0
  325. package/dist/portal/session/presence/session-lease.js +8 -0
  326. package/dist/portal/session/recorded-gaps.d.ts +15 -11
  327. package/dist/portal/session/recorded-gaps.js +39 -18
  328. package/dist/portal/session/serving-directory.d.ts +24 -2
  329. package/dist/portal/session/serving-directory.js +93 -5
  330. package/dist/portal/session/session-info.d.ts +26 -0
  331. package/dist/portal/session/session-info.js +20 -0
  332. package/dist/portal/session/session-manager.d.ts +35 -3
  333. package/dist/portal/session/session-manager.js +69 -14
  334. package/dist/portal/session/session-successor.js +1 -9
  335. package/dist/portal/session/session-tools.js +22 -4
  336. package/dist/portal/session/session.d.ts +10 -10
  337. package/dist/portal/session/session.js +37 -29
  338. package/dist/portal/session/zero-install-first-call.d.ts +7 -2
  339. package/dist/portal/session/zero-install-first-call.js +43 -20
  340. package/dist/start-wiring.d.ts +2 -2
  341. package/dist/start-wiring.js +17 -3
  342. package/dist/status-payload.d.ts +11 -1
  343. package/dist/status-payload.js +7 -1
  344. package/dist/surface/agent-presence-grace.d.ts +11 -0
  345. package/dist/surface/agent-presence-grace.js +34 -0
  346. package/dist/surface/http-server.d.ts +12 -1
  347. package/dist/surface/http-server.js +64 -15
  348. package/dist/surface/mcp/faults/mcp-outage.d.ts +12 -2
  349. package/dist/surface/mcp/faults/mcp-outage.js +19 -1
  350. package/dist/surface/mcp/mcp-proxy.d.ts +17 -74
  351. package/dist/surface/mcp/mcp-proxy.js +194 -267
  352. package/dist/surface/mcp/mcp.d.ts +20 -3
  353. package/dist/surface/mcp/mcp.js +88 -25
  354. package/dist/surface/mcp/proxy/drive-calls.d.ts +44 -0
  355. package/dist/surface/mcp/proxy/drive-calls.js +114 -5
  356. package/dist/surface/mcp/proxy/proxy-boot.d.ts +11 -0
  357. package/dist/surface/mcp/proxy/proxy-boot.js +1 -0
  358. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +6 -0
  359. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +14 -1
  360. package/dist/surface/mcp/proxy/proxy-handshake.d.ts +34 -2
  361. package/dist/surface/mcp/proxy/proxy-handshake.js +76 -8
  362. package/dist/surface/mcp/server-instructions.js +35 -16
  363. package/dist/surface/mcp/shared-params.d.ts +8 -0
  364. package/dist/surface/mcp/shared-params.js +15 -1
  365. package/dist/surface/mcp/sse-frame-parser.d.ts +11 -0
  366. package/dist/surface/mcp/sse-frame-parser.js +18 -0
  367. package/dist/surface/mcp/surface-vocabulary.d.ts +2 -0
  368. package/dist/surface/mcp/surface-vocabulary.js +13 -0
  369. package/dist/surface/mcp/tool-catalog-cache.d.ts +2 -0
  370. package/dist/surface/mcp/tool-catalog-cache.js +14 -4
  371. package/dist/surface/tools/account-tool.d.ts +4 -3
  372. package/dist/surface/tools/account-tool.js +20 -31
  373. package/dist/surface/tools/act/act-danger.d.ts +2 -0
  374. package/dist/surface/tools/act/act-danger.js +41 -14
  375. package/dist/surface/tools/act/act-observation.d.ts +11 -0
  376. package/dist/surface/tools/act/act-observation.js +33 -0
  377. package/dist/surface/tools/act/act-preflight.js +10 -3
  378. package/dist/surface/tools/act/act-sequence-retry.js +2 -1
  379. package/dist/surface/tools/act/act-target.js +19 -5
  380. package/dist/surface/tools/act/durable.js +1 -1
  381. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +9 -7
  382. package/dist/surface/tools/act/navigation/navigate-arrival.js +11 -14
  383. package/dist/surface/tools/act/unsettled-find.d.ts +36 -0
  384. package/dist/surface/tools/act/unsettled-find.js +71 -0
  385. package/dist/surface/tools/act-sequence-tool.js +53 -24
  386. package/dist/surface/tools/act-tools.d.ts +2 -3
  387. package/dist/surface/tools/act-tools.js +49 -47
  388. package/dist/surface/tools/agent/compact-envelope.d.ts +2 -2
  389. package/dist/surface/tools/agent/for-the-agent.js +41 -3
  390. package/dist/surface/tools/assert/assert-grade.js +8 -3
  391. package/dist/surface/tools/assert/assert-verdict.js +4 -2
  392. package/dist/surface/tools/browser-tools.js +7 -4
  393. package/dist/surface/tools/chat-drives.js +50 -25
  394. package/dist/surface/tools/coverage-tools.js +1 -1
  395. package/dist/surface/tools/dynamic-tools.d.ts +8 -0
  396. package/dist/surface/tools/dynamic-tools.js +89 -34
  397. package/dist/surface/tools/empty-session-list.d.ts +18 -25
  398. package/dist/surface/tools/empty-session-list.js +67 -38
  399. package/dist/surface/tools/error-recovery.d.ts +17 -7
  400. package/dist/surface/tools/error-recovery.js +53 -20
  401. package/dist/surface/tools/explore-tools.d.ts +10 -0
  402. package/dist/surface/tools/explore-tools.js +72 -19
  403. package/dist/surface/tools/feedback-tools.js +23 -2
  404. package/dist/surface/tools/gap-novelty.d.ts +6 -0
  405. package/dist/surface/tools/gap-novelty.js +9 -0
  406. package/dist/surface/tools/gaps/capability-absences.js +4 -3
  407. package/dist/surface/tools/harness-explore.d.ts +1 -1
  408. package/dist/surface/tools/harness-explore.js +6 -4
  409. package/dist/surface/tools/harness-gate.d.ts +43 -21
  410. package/dist/surface/tools/harness-gate.js +55 -81
  411. package/dist/surface/tools/harness-script.d.ts +2 -0
  412. package/dist/surface/tools/harness-script.js +11 -6
  413. package/dist/surface/tools/harness-toolset.js +3 -4
  414. package/dist/surface/tools/invoke-tool.d.ts +0 -8
  415. package/dist/surface/tools/invoke-tool.js +164 -178
  416. package/dist/surface/tools/lease-hint.d.ts +9 -0
  417. package/dist/surface/tools/lease-hint.js +22 -3
  418. package/dist/surface/tools/lease-permissions.d.ts +1 -1
  419. package/dist/surface/tools/lease-readiness.d.ts +55 -0
  420. package/dist/surface/tools/lease-readiness.js +150 -2
  421. package/dist/surface/tools/lease-tools.d.ts +3 -75
  422. package/dist/surface/tools/lease-tools.js +124 -188
  423. package/dist/surface/tools/map-tools.d.ts +20 -0
  424. package/dist/surface/tools/map-tools.js +145 -0
  425. package/dist/surface/tools/merge-tools.js +11 -7
  426. package/dist/surface/tools/next-step.js +6 -3
  427. package/dist/surface/tools/observe-tools.js +46 -35
  428. package/dist/surface/tools/platform-moment.d.ts +3 -12
  429. package/dist/surface/tools/platform-moment.js +12 -24
  430. package/dist/surface/tools/read/complete-snapshot.d.ts +7 -1
  431. package/dist/surface/tools/read/complete-snapshot.js +13 -2
  432. package/dist/surface/tools/read/query-paginate.js +6 -0
  433. package/dist/surface/tools/read/query-shape.d.ts +9 -0
  434. package/dist/surface/tools/read/query-shape.js +15 -0
  435. package/dist/surface/tools/read/snapshot-unmounted-note.d.ts +1 -0
  436. package/dist/surface/tools/read/snapshot-unmounted-note.js +73 -0
  437. package/dist/surface/tools/read-tools.js +5 -5
  438. package/dist/surface/tools/real-input-attempt.d.ts +11 -0
  439. package/dist/surface/tools/real-input-attempt.js +206 -38
  440. package/dist/surface/tools/reconcile-tools.js +21 -8
  441. package/dist/surface/tools/reticle-result.d.ts +3 -0
  442. package/dist/surface/tools/reticle-result.js +8 -1
  443. package/dist/surface/tools/run-report.d.ts +22 -0
  444. package/dist/surface/tools/run-report.js +141 -0
  445. package/dist/surface/tools/session-ports.d.ts +57 -0
  446. package/dist/surface/tools/session-ports.js +102 -0
  447. package/dist/surface/tools/tool-kit.d.ts +58 -4
  448. package/dist/surface/tools/tool-kit.js +140 -6
  449. package/dist/surface/tools/tool-surface.d.ts +2 -0
  450. package/dist/surface/tools/tool-surface.js +45 -0
  451. package/dist/surface/tools/tools.js +49 -87
  452. package/dist/telemetry/automation-hint.d.ts +5 -0
  453. package/dist/telemetry/automation-hint.js +7 -0
  454. package/dist/telemetry/cli-telemetry.d.ts +22 -0
  455. package/dist/telemetry/cli-telemetry.js +80 -11
  456. package/dist/telemetry/daemon-telemetry.d.ts +3 -1
  457. package/dist/telemetry/daemon-telemetry.js +9 -2
  458. package/dist/telemetry/feedback-cli.d.ts +1 -1
  459. package/dist/telemetry/feedback-cli.js +8 -3
  460. package/dist/telemetry/feedback.d.ts +1 -1
  461. package/dist/telemetry/feedback.js +51 -4
  462. package/dist/telemetry/harness-run.d.ts +3 -3
  463. package/dist/telemetry/harness-run.js +18 -7
  464. package/dist/telemetry/hud-events.d.ts +1 -1
  465. package/dist/telemetry/hud-metrics.d.ts +6 -3
  466. package/dist/telemetry/hud-metrics.js +20 -8
  467. package/dist/telemetry/install-source.js +1 -1
  468. package/dist/telemetry/mcp-connection.d.ts +1 -1
  469. package/dist/telemetry/mcp-connection.js +21 -2
  470. package/dist/telemetry/onboarding-firsts.d.ts +6 -1
  471. package/dist/telemetry/onboarding-firsts.js +41 -1
  472. package/dist/telemetry/proxy-telemetry.d.ts +42 -0
  473. package/dist/telemetry/proxy-telemetry.js +70 -1
  474. package/dist/telemetry/session-metrics.d.ts +1 -3
  475. package/dist/telemetry/session-metrics.js +16 -12
  476. package/dist/telemetry/telemetry.d.ts +9 -1
  477. package/dist/telemetry/telemetry.js +16 -1
  478. package/dist/telemetry/verification-of.d.ts +3 -1
  479. package/dist/telemetry/verification-of.js +7 -1
  480. package/dist/wire-journal.js +12 -2
  481. package/docs/README.md +4 -2
  482. package/docs/actions.mdx +7 -6
  483. package/docs/adapters.md +2 -1
  484. package/docs/agent-cheatsheet.md +19 -5
  485. package/docs/architecture.md +13 -10
  486. package/docs/autodrive.md +6 -5
  487. package/docs/benchmarks.md +2 -2
  488. package/docs/capabilities.mdx +1 -1
  489. package/docs/cli/affected.mdx +1 -1
  490. package/docs/cli/cloud.mdx +6 -6
  491. package/docs/cli/doctor.mdx +3 -1
  492. package/docs/cli/feedback.mdx +11 -7
  493. package/docs/cli/gate.mdx +1 -1
  494. package/docs/cli/help.mdx +1 -1
  495. package/docs/cli/init.mdx +20 -16
  496. package/docs/cli/mcp.mdx +5 -2
  497. package/docs/cli/open.mdx +5 -2
  498. package/docs/cli/report.mdx +1 -1
  499. package/docs/cli/restart.mdx +1 -1
  500. package/docs/cli/rollback.mdx +4 -4
  501. package/docs/cli/serve.mdx +7 -5
  502. package/docs/cli/setup.mdx +1 -1
  503. package/docs/cli/status.mdx +6 -2
  504. package/docs/cli/stop.mdx +3 -3
  505. package/docs/cli/telemetry.mdx +5 -1
  506. package/docs/cli/try.mdx +8 -8
  507. package/docs/cli/update.mdx +8 -5
  508. package/docs/cli/verify.mdx +12 -5
  509. package/docs/cli/version.mdx +1 -1
  510. package/docs/cli.mdx +10 -6
  511. package/docs/debugging.md +1 -1
  512. package/docs/deploy-checks.md +8 -6
  513. package/docs/desktop.mdx +14 -18
  514. package/docs/enterprise.md +6 -6
  515. package/docs/environment.mdx +5 -3
  516. package/docs/faq.mdx +9 -6
  517. package/docs/first-drive.md +19 -9
  518. package/docs/flows.md +3 -4
  519. package/docs/for-agents.md +1 -1
  520. package/docs/frameworks.mdx +34 -11
  521. package/docs/getting-started.md +46 -29
  522. package/docs/http-transport.md +2 -2
  523. package/docs/human-control.md +8 -8
  524. package/docs/index.mdx +2 -2
  525. package/docs/install-agentic.mdx +18 -19
  526. package/docs/install-manual.mdx +8 -4
  527. package/docs/integration-patterns.md +5 -5
  528. package/docs/multi-agent-testing.md +7 -5
  529. package/docs/packages/browser.mdx +34 -2
  530. package/docs/packages/core.mdx +2 -1
  531. package/docs/packages/electron.mdx +15 -3
  532. package/docs/packages/eslint-plugin.mdx +1 -1
  533. package/docs/packages/next.mdx +17 -5
  534. package/docs/packages/react.mdx +8 -1
  535. package/docs/packages/server.mdx +3 -3
  536. package/docs/packages/tauri.mdx +3 -3
  537. package/docs/packages/vite-plugin.mdx +1 -1
  538. package/docs/packages.mdx +19 -6
  539. package/docs/platform-integration.md +12 -4
  540. package/docs/predicates.mdx +18 -7
  541. package/docs/production-mode.md +108 -0
  542. package/docs/prompt-capture.md +99 -0
  543. package/docs/quickstart.mdx +14 -12
  544. package/docs/recipes.mdx +9 -8
  545. package/docs/required-checks.md +3 -2
  546. package/docs/skill-file.mdx +4 -3
  547. package/docs/skill-setup.md +2 -2
  548. package/docs/skill.md +62 -70
  549. package/docs/state-files.md +1 -0
  550. package/docs/state-management.mdx +11 -9
  551. package/docs/system-map.md +14 -13
  552. package/docs/telemetry.md +32 -24
  553. package/docs/testing.md +9 -5
  554. package/docs/tools/act-and-wait.mdx +17 -13
  555. package/docs/tools/act-sequence.mdx +40 -66
  556. package/docs/tools/act.mdx +7 -5
  557. package/docs/tools/assert.mdx +1 -1
  558. package/docs/tools/look.mdx +1 -1
  559. package/docs/tools/navigate.mdx +1 -1
  560. package/docs/tools/network.mdx +13 -11
  561. package/docs/tools/overview.mdx +3 -3
  562. package/docs/tools/query.mdx +3 -3
  563. package/docs/tools/session-and-feedback.mdx +9 -6
  564. package/docs/tools/sessions.mdx +7 -0
  565. package/docs/tools/snapshot.mdx +9 -7
  566. package/docs/tools/tools-and-run.mdx +11 -10
  567. package/docs/tools/verify.mdx +1 -1
  568. package/docs/troubleshooting.mdx +21 -9
  569. package/docs/usage.md +58 -43
  570. package/docs/use-cases.mdx +2 -2
  571. package/docs/vs-chrome-devtools-mcp.mdx +5 -5
  572. package/docs/vs-playwright-mcp.mdx +5 -4
  573. package/docs/what-is-recorded.md +12 -7
  574. package/docs/what-is-reticle.mdx +2 -2
  575. package/docs/why-reticle.mdx +1 -1
  576. package/package.json +9 -12
  577. package/dist/command/dev/stale-issue-guard.d.ts +0 -34
  578. package/dist/command/dev/stale-issue-guard.js +0 -69
@@ -10,9 +10,13 @@
10
10
  * work is happening" costs nobody anything.
11
11
  *
12
12
  * ── THE THREE RULES THAT KEEP IT INVISIBLE ────────────────────────────────────────────────────
13
- * QUIET WHEN IDLE. A cycle on an unchanged session is one small GET and nothing else. That is what
14
- * makes a one-minute timer affordable; it is also why the timer is not adaptive — a scheme that
15
- * "backs off when idle" is complexity paid for a cost that is already near zero.
13
+ * QUIET WHEN IDLE. A cycle runs because something under `.reticle` CHANGED: the whole root is
14
+ * watched, and a change schedules one cycle after a short debounce, so a drive that writes forty
15
+ * files costs one cycle. Nothing changed, nothing sent. A slow timer (ten minutes) stays as the
16
+ * backstop — for a filesystem that cannot be watched and the team's decisions to pull down — and a
17
+ * cycle that moved something still earns the fast active rate. A FAILED cycle retries after a
18
+ * minute, doubling per failure up to the backstop, and a success returns it to the idle rhythm.
19
+ * The sync's own bookkeeping writes are ignored, or every cycle would schedule the next.
16
20
  *
17
21
  * NEVER IN THE WAY. Nothing here is awaited by a tool call. A sync that can slow down or fail a
18
22
  * verification would be a sync worth switching off, and the local record is authoritative anyway.
@@ -21,23 +25,80 @@
21
25
  * laptop on a train would otherwise fill the log with the same line four hundred times and teach
22
26
  * everyone to ignore it.
23
27
  */
28
+ import { ReticleDir } from '@reticlehq/core';
24
29
  import { reticleDirPaths } from '../project/dir/reticle-dir.js';
25
30
  import { syncRequest } from './cloud-sync.js';
26
31
  import { log } from '../../log.js';
27
32
  import { homedir } from 'node:os';
28
33
  import { emitSyncHook } from '../../hooks/hook-emit.js';
29
- import { MemoryReadHeader, MemoryReadPurpose, memoryReadUrl, scopedMemoryEntries, } from './memory-scope.js';
34
+ import { MemoryReadHeader, MemoryReadPurpose, MemoryScopeField, MEMORY_PAGE_LIMIT, memoryReadUrl, readMemoryPages, scopedMemoryEntries, } from './memory-scope.js';
30
35
  import { IntentShardStore } from '../intent/intent-shard-store.js';
31
36
  import { applySettledWords, keptWordsInRequest, settledWordsIn, } from '../intent/settled-words.js';
32
37
  import { createNodeFileSystem } from '../project/fs/fs-port.js';
33
38
  import { flushPendingWrites } from '../../hooks/hook-bus.js';
34
- import { describeSync, runSyncCycle } from './sync-cycle.js';
35
- import { diskSink, diskSource, readCloudState } from './sync-disk.js';
36
- import { CLOUD_LINK_FILE, resolveProjectCloud } from './cloud-config.js';
39
+ import { runSyncCycle } from './sync-cycle.js';
40
+ import { describeSync } from './sync-describe.js';
41
+ import { diskSink, diskSource, policyScreenshots, readCloudState } from './sync-disk.js';
42
+ import { resolveProjectCloud } from './cloud-config.js';
37
43
  import { watch } from 'node:fs';
44
+ import { BackgroundSyncState, backgroundSyncGate, } from './background-sync.js';
38
45
  import { unmatchedRoots, unsyncedRoots } from './unsynced-roots.js';
39
- /** How often the daemon cycles. See the note above on why this is a constant and not a curve. */
40
- const DAEMON_SYNC_INTERVAL_MS = 60_000;
46
+ /**
47
+ * The backstop: how often the daemon cycles when nothing under `.reticle` has changed. Long, because
48
+ * a change wakes the sync on its own; this exists for what a watch cannot see (an unwatchable
49
+ * filesystem, decisions made on the dashboard). A failed cycle retries sooner: FAILURE_RETRY_MS.
50
+ */
51
+ const DAEMON_SYNC_INTERVAL_MS = 600_000;
52
+ /**
53
+ * How long a change under `.reticle` waits before cycling. Coalesces the burst a drive writes, and
54
+ * is never extended by a later write, so a stream of writes cannot postpone the sync forever.
55
+ */
56
+ const CHANGE_DEBOUNCE_MS = 2_000;
57
+ /**
58
+ * What the sync itself writes into a root, plus `sessions/` (the per-event journal, appended on
59
+ * every browser event and never sent) and `episodes/` (every tool call, likewise never sent). A
60
+ * change here must not wake the sync: the first would make every cycle schedule the next, the others
61
+ * would turn an open tab or a working agent into a cycle every two seconds.
62
+ * The atomic writes' `<file>.<pid>.tmp` siblings are matched by prefix.
63
+ */
64
+ const NOT_A_CHANGE = [ReticleDir.CLOUD_STATE_FILE, ReticleDir.ISSUES_FILE];
65
+ /**
66
+ * The longest the team's memory decisions go unread while a drive keeps the sync busy. They must come
67
+ * down before changed INTENT goes up (or a word a maintainer set aside is sent back as a new
68
+ * disagreement), and nothing else waits on them: paging the whole memory before every run cost a
69
+ * real project nine round trips per cycle, the larger part of a run's time to the dashboard.
70
+ */
71
+ const SETTLED_WORDS_MAX_AGE_MS = 60_000;
72
+ /**
73
+ * How often the OTHER linked repos are walked while this one keeps changing. They are not watched, and
74
+ * each is a full cycle of round trips: walking them on every change here made a run landing in this
75
+ * repo wait behind all of them. The backstop timer and a requested sync always walk them.
76
+ */
77
+ const OTHER_ROOTS_EVERY_MS = 60_000;
78
+ /**
79
+ * How often an idle machine asks whether the team's memory moved. Conditional (If-None-Match on the
80
+ * first page), so an unchanged minute is one bodiless 304, and a dashboard decision lands within it
81
+ * rather than on the ten-minute backstop.
82
+ */
83
+ const MEMORY_POLL_MS = 50_000;
84
+ /**
85
+ * How often the poll looks at the clock. A read is due once MEMORY_POLL_MS have passed since the last
86
+ * one (any read, a cycle's too), so a decision waits at most the two together: one minute. Measured
87
+ * with a plain one-minute timer: 74 s and 96 s, because a cycle's read just before it pushed the
88
+ * next ask a whole minute on.
89
+ */
90
+ const MEMORY_POLL_TICK_MS = 10_000;
91
+ const IF_NONE_MATCH = 'if-none-match';
92
+ const isIntent = (path) => {
93
+ const [head = ''] = path.split(/[\\/]/);
94
+ return head === ReticleDir.INTENT_SUBDIR || head.startsWith(ReticleDir.INTENT_FILE);
95
+ };
96
+ const isOwnWrite = (path) => {
97
+ const [head = ''] = path.split(/[\\/]/);
98
+ return (head === ReticleDir.SESSIONS_SUBDIR ||
99
+ head === ReticleDir.EPISODES_SUBDIR ||
100
+ NOT_A_CHANGE.some((file) => head === file || head.startsWith(`${file}.`)));
101
+ };
41
102
  /**
42
103
  * The cadence while something is actually happening.
43
104
  *
@@ -53,6 +114,11 @@ const DAEMON_SYNC_INTERVAL_MS = 60_000;
53
114
  * loop, and verification working with the network down is a promise this product makes.
54
115
  */
55
116
  const DAEMON_SYNC_ACTIVE_INTERVAL_MS = 5_000;
117
+ /**
118
+ * The first retry after a failed cycle, doubling with each failure after it up to the backstop. A
119
+ * failed upload is owed work, so it does not wait ten minutes for the idle timer.
120
+ */
121
+ const FAILURE_RETRY_MS = 60_000;
56
122
  /** Given to the first cycle so a freshly-started daemon does not race the session that woke it. */
57
123
  const FIRST_CYCLE_DELAY_MS = 5_000;
58
124
  /**
@@ -65,14 +131,16 @@ const FIRST_CYCLE_DELAY_MS = 5_000;
65
131
  */
66
132
  const NUDGE_DELAY_MS = 1_500;
67
133
  /**
68
- * How long a flush waits for a cycle already in flight before running its own.
134
+ * The whole time a flush may take: the wait for a cycle in flight, and its own cycle.
69
135
  *
70
- * Shutdown budget, not a sync budget: every call inside a cycle already carries its own timeout, so
71
- * this only bounds the WAIT. Generous enough for an ordinary cycle to finish, short enough that a
72
- * stalled one cannot hold a daemon open.
136
+ * Shutdown budget, not a sync budget. `reticle stop` sends SIGKILL 5 s after SIGTERM, and the daemon
137
+ * sends its stop event (up to 2 s) before it flushes. A flush past this budget let the kill come
138
+ * first. Runs that are not sent stay on disk, and the next daemon sends them.
73
139
  */
74
- const FLUSH_WAIT_MS = 3_000;
140
+ const FLUSH_BUDGET_MS = 1_500;
75
141
  const FLUSH_POLL_MS = 25;
142
+ /** A failed status, push or pull that says the platform refused the credential. */
143
+ const AUTH_REFUSED = /^(?:status|sync|pull) 40[13]\b/;
76
144
  /** Whether a cycle landed or collected anything — what earns the fast interval and a log line. */
77
145
  const movedAnything = (report) => report.runsSent > 0 ||
78
146
  report.flowsSent > 0 ||
@@ -91,6 +159,13 @@ export function startSyncDaemon(deps) {
91
159
  const activeIntervalMs = Math.min(DAEMON_SYNC_ACTIVE_INTERVAL_MS, intervalMs);
92
160
  const now = deps.now ?? (() => Date.now());
93
161
  const request = deps.request ?? defaultRequest;
162
+ /** Per root, the credential the platform refused (401/403). Stopped until the sign-in changes. */
163
+ const refusedUnder = new Map();
164
+ /** Per root, the gate as of this tick. */
165
+ const gates = new Map();
166
+ /** Per root, the state last logged: one line per change, never one per tick. */
167
+ const loggedState = new Map();
168
+ let lastPushAt;
94
169
  let timer;
95
170
  let stopped = false;
96
171
  let running = false;
@@ -131,8 +206,9 @@ export function startSyncDaemon(deps) {
131
206
  [...linked.keys(), ...unmatchedRoots(homedir())], (r) => Promise.resolve(true === linked.get(r)), (r) => Promise.resolve(sendsFlows.has(r)));
132
207
  for (const entry of unsynced) {
133
208
  const held = entry.runs + entry.flows;
134
- if (entry.linked || announced.get(entry.root) === held)
209
+ if (entry.linked || announced.get(entry.root) === held) {
135
210
  continue;
211
+ }
136
212
  announced.set(entry.root, held);
137
213
  log('reticle_cloud_unsynced_root', {
138
214
  root: entry.root,
@@ -143,15 +219,63 @@ export function startSyncDaemon(deps) {
143
219
  });
144
220
  }
145
221
  };
222
+ /**
223
+ * The cloud as this tick may use it: unchanged when the gate lets the root send, else with no
224
+ * config, which every caller below already reads as "send nothing". A credential the platform
225
+ * refused stays refused until the sign-in or the key changes.
226
+ */
227
+ const admit = (root, cloud) => {
228
+ if (deps.gate === undefined) {
229
+ return cloud;
230
+ }
231
+ let gate = deps.gate(root, cloud);
232
+ if (BackgroundSyncState.ON === gate.state && gate.credential !== undefined) {
233
+ if (refusedUnder.get(root) === gate.credential) {
234
+ gate = {
235
+ state: BackgroundSyncState.REFUSED,
236
+ why: 'the platform refused this sign-in: run `reticle connect` again',
237
+ };
238
+ }
239
+ else {
240
+ refusedUnder.delete(root);
241
+ }
242
+ }
243
+ gates.set(root, gate);
244
+ // A root starts as unlinked: `reticle_cloud_unsynced_root` already names one that never linked.
245
+ if ((loggedState.get(root) ?? BackgroundSyncState.UNLINKED) !== gate.state) {
246
+ loggedState.set(root, gate.state);
247
+ log('reticle_background_sync', { root, state: gate.state, why: gate.why });
248
+ }
249
+ return BackgroundSyncState.ON === gate.state ? cloud : { ...cloud, config: null };
250
+ };
251
+ /** Stop sending for this root until its sign-in changes. */
252
+ const refuse = (root) => {
253
+ const credential = gates.get(root)?.credential;
254
+ if (credential !== undefined) {
255
+ refusedUnder.set(root, credential);
256
+ }
257
+ };
146
258
  /** One root's bundle. The state, source and sink are already per-root; only the caller was not. */
147
- const pushRoot = async (root, cloud) => {
148
- if (null === cloud.config)
259
+ const pushRoot = async (root, cloud, readMemory = true) => {
260
+ if (null === cloud.config) {
149
261
  return undefined;
262
+ }
263
+ const report = await sendRoot(root, cloud, readMemory);
264
+ if (report.error !== undefined && AUTH_REFUSED.test(report.error)) {
265
+ refuse(root);
266
+ }
267
+ return report;
268
+ };
269
+ const sendRoot = async (root, cloud, readMemory) => {
270
+ if (null === cloud.config) {
271
+ throw new Error('sendRoot needs a linked cloud');
272
+ }
150
273
  const full = diskSource(root);
151
274
  // The team's decisions come down too: an author's record kept the words a maintainer set aside,
152
275
  // and re-sent them every cycle as a new disagreement.
153
- if (cloud.policy.memory)
276
+ if (cloud.policy.memory && readMemory) {
154
277
  await pullSettledWords(root, cloud.config, cloud.projectId).catch(() => 0);
278
+ }
155
279
  return runSyncCycle({
156
280
  config: cloud.config,
157
281
  ...(cloud.link === undefined ? {} : { link: cloud.link }),
@@ -162,33 +286,97 @@ export function startSyncDaemon(deps) {
162
286
  flows: () => (cloud.policy.flows ? full.flows() : []),
163
287
  capsules: () => (cloud.policy.capsules ? full.capsules() : []),
164
288
  derived: (kind) => (cloud.policy.memory ? full.derived(kind) : undefined),
165
- ...(cloud.policy.screenshots && full.screenshot !== undefined
166
- ? { screenshot: full.screenshot }
167
- : {}),
289
+ ...policyScreenshots(full.screenshot, cloud.policy),
168
290
  },
169
291
  sink: diskSink(root),
170
292
  state: readCloudState(root),
171
293
  now,
294
+ ...(deps.clientVersion === undefined ? {} : { clientVersion: deps.clientVersion }),
172
295
  request,
173
296
  });
174
297
  };
298
+ /** The tag of this root's first memory page as last read: what a conditional read names. */
299
+ let memoryTag;
300
+ let polling = false;
301
+ let lastCloud;
175
302
  /** Bring the dashboard's memory decisions down to this root's records. Returns how many changed. */
176
- const pullSettledWords = async (root, config, projectId) => {
177
- const res = await request(memoryReadUrl(config.url, { projectId }), {
178
- method: 'GET',
179
- // The sync, not an agent: without this every cycle counted as an agent consulting memory.
180
- headers: {
181
- authorization: `Bearer ${config.apiKey}`,
182
- [MemoryReadHeader.PURPOSE]: MemoryReadPurpose.SYNC,
183
- },
303
+ const pullSettledWords = async (root, config, projectId,
304
+ /** A first page already in hand (the poll's 200), so it is not asked for twice. */
305
+ first) => {
306
+ const body = await readMemoryPages(config.url, { projectId }, async (url) => {
307
+ if (first !== undefined && url === first.url) {
308
+ return first.body;
309
+ }
310
+ const res = await request(url, {
311
+ method: 'GET',
312
+ // The sync, not an agent: without this every cycle counted as an agent consulting memory.
313
+ headers: {
314
+ authorization: `Bearer ${config.apiKey}`,
315
+ [MemoryReadHeader.PURPOSE]: MemoryReadPurpose.SYNC,
316
+ },
317
+ });
318
+ if (root === deps.reticleRoot && !url.includes(`${MemoryScopeField.CURSOR}=`)) {
319
+ memoryTag = res.etag;
320
+ }
321
+ return 200 === res.status ? JSON.parse(res.text) : undefined;
184
322
  });
185
- if (200 !== res.status)
323
+ if (body === undefined) {
186
324
  return 0;
187
- const entries = scopedMemoryEntries(JSON.parse(res.text), projectId) ?? [];
325
+ }
326
+ const entries = scopedMemoryEntries(body, projectId) ?? [];
188
327
  const settled = settledWordsIn(entries);
189
328
  await keptWordsInRequest(reticleDirPaths(root).request, settled);
190
329
  return applySettledWords(new IntentShardStore(createNodeFileSystem(), root, { now }), settled);
191
330
  };
331
+ /**
332
+ * Ask whether the team's memory moved and, when it did, bring its decisions down. Skipped while a
333
+ * cycle runs or one read it within the minute.
334
+ * ponytail: the tag covers the first page, which is ordered newest-updated first, so any decision
335
+ * moves it; a fact deleted past the first page waits for the backstop.
336
+ */
337
+ const pollMemory = async () => {
338
+ if (stopped || running || polling || now() - memoryReadAt < MEMORY_POLL_MS) {
339
+ return;
340
+ }
341
+ polling = true;
342
+ try {
343
+ // The link the last cycle resolved: the poll costs no link read of its own.
344
+ const cloud = lastCloud;
345
+ if (cloud === undefined || null === cloud.config || !cloud.policy.memory) {
346
+ return;
347
+ }
348
+ const url = memoryReadUrl(cloud.config.url, {
349
+ projectId: cloud.projectId,
350
+ limit: MEMORY_PAGE_LIMIT,
351
+ });
352
+ const res = await request(url, {
353
+ method: 'GET',
354
+ headers: {
355
+ authorization: `Bearer ${cloud.config.apiKey}`,
356
+ [MemoryReadHeader.PURPOSE]: MemoryReadPurpose.SYNC,
357
+ ...(memoryTag === undefined ? {} : { [IF_NONE_MATCH]: memoryTag }),
358
+ },
359
+ });
360
+ // Answered either way, 304 included: the memory is known current as of now.
361
+ if (200 === res.status || 304 === res.status) {
362
+ memoryReadAt = now();
363
+ }
364
+ if (200 !== res.status) {
365
+ return;
366
+ }
367
+ memoryTag = res.etag;
368
+ await pullSettledWords(deps.reticleRoot, cloud.config, cloud.projectId, {
369
+ url,
370
+ body: JSON.parse(res.text),
371
+ });
372
+ }
373
+ catch {
374
+ // Offline is not news; the next minute asks again.
375
+ }
376
+ finally {
377
+ polling = false;
378
+ }
379
+ };
192
380
  /**
193
381
  * Push every other linked root this machine knows about.
194
382
  *
@@ -199,8 +387,9 @@ export function startSyncDaemon(deps) {
199
387
  const syncOtherRoots = async () => {
200
388
  const roots = deps.otherRoots;
201
389
  const cloudFor = deps.cloudFor;
202
- if (roots === undefined || cloudFor === undefined)
390
+ if (roots === undefined || cloudFor === undefined) {
203
391
  return;
392
+ }
204
393
  let list = [];
205
394
  try {
206
395
  list = await roots();
@@ -211,35 +400,41 @@ export function startSyncDaemon(deps) {
211
400
  return;
212
401
  }
213
402
  for (const root of list) {
214
- if (root === deps.reticleRoot)
403
+ if (root === deps.reticleRoot) {
215
404
  continue;
405
+ }
216
406
  rootsLinked.set(root, false);
217
407
  try {
218
- const cloud = await cloudFor(root);
408
+ const cloud = admit(root, await cloudFor(root));
219
409
  // Linked HERE, by its own link file. An unlinked root resolves to the environment's key,
220
410
  // which is the right answer for the root this daemon stands in and somebody else's project
221
411
  // for every other one: a daemon started with RETICLE_API_KEY pushed every project this
222
412
  // machine had seen into that one, a status and a pull per root on every cycle.
223
413
  // Never silently: `refreshUnsynced` names this root if it holds runs.
224
- if (null === cloud.config || null === cloud.projectId)
414
+ if (null === cloud.config || null === cloud.projectId) {
225
415
  continue;
416
+ }
226
417
  rootsLinked.set(root, true);
227
- if (cloud.policy.flows)
418
+ if (cloud.policy.flows) {
228
419
  rootsSendFlows.add(root);
420
+ }
229
421
  const report = await pushRoot(root, cloud);
230
- if (report === undefined)
422
+ if (report === undefined) {
231
423
  continue;
424
+ }
232
425
  if (report.error !== undefined) {
233
426
  log('reticle_cloud_sync_failed', { root, error: report.error });
234
427
  continue;
235
428
  }
236
- if (!report.ok)
429
+ if (!report.ok) {
237
430
  log('reticle_cloud_sync_failed', { root, error: describeSync(report) });
431
+ }
238
432
  const moved = movedAnything(report);
239
433
  // The root is NAMED here and not in the single-root log below, because with several repos
240
434
  // reporting, "synced 3 runs" without a directory is not an answer to "synced from where".
241
- if (moved)
435
+ if (moved) {
242
436
  log('reticle_cloud_synced', { root, summary: describeSync(report) });
437
+ }
243
438
  }
244
439
  catch (error) {
245
440
  log('reticle_cloud_sync_failed', {
@@ -249,46 +444,84 @@ export function startSyncDaemon(deps) {
249
444
  }
250
445
  }
251
446
  };
252
- const cycle = async () => {
447
+ /** When this root last read the team's memory, and whether its intent moved since. */
448
+ let memoryReadAt = Number.NEGATIVE_INFINITY;
449
+ /** When the other linked repos were last walked. */
450
+ let othersAt = Number.NEGATIVE_INFINITY;
451
+ let intentMoved = true;
452
+ /**
453
+ * A change landed that no cycle has delivered yet. Work saved while offline used to wait out the
454
+ * longest backoff after the network returned (each save's failed cycle doubled it): owed work
455
+ * retries every minute, and only an idle machine backs off.
456
+ */
457
+ let owed = false;
458
+ const memoryDue = (forced) => {
459
+ if (!forced && !intentMoved && now() - memoryReadAt < SETTLED_WORDS_MAX_AGE_MS) {
460
+ return false;
461
+ }
462
+ intentMoved = false;
463
+ memoryReadAt = now();
464
+ return true;
465
+ };
466
+ /** `asked`: a sync somebody requested: read the memory and walk every repo, whatever is due. */
467
+ const cycle = async (asked = false) => {
253
468
  // Overlap guard: a slow cycle must not have a second one started on top of it, or two bundles
254
469
  // race and the cursor written by the loser silently rewinds the winner's progress.
255
- if (running)
470
+ if (running) {
256
471
  return undefined;
472
+ }
257
473
  running = true;
258
- rootsLinked = new Map();
259
- rootsSendFlows = new Set();
474
+ // Owed work this cycle carries; a failed exchange leaves it owed, and a change meanwhile adds to it.
475
+ const carried = owed;
476
+ owed = false;
477
+ // The other repos' entries stand until their next pass, so what they owe is still reported.
478
+ const othersDue = asked || now() - othersAt >= OTHER_ROOTS_EVERY_MS;
479
+ if (othersDue) {
480
+ othersAt = now();
481
+ rootsLinked = new Map();
482
+ rootsSendFlows = new Set();
483
+ }
484
+ const own = deps.reticleRoot;
485
+ rootsSendFlows.delete(own);
260
486
  try {
261
- const cloud = await deps.cloud();
487
+ const cloud = admit(deps.reticleRoot, await deps.cloud());
488
+ lastCloud = cloud;
262
489
  const linked = null !== cloud.config;
263
- const own = deps.reticleRoot;
264
490
  rootsLinked.set(own, linked);
265
- if (linked && cloud.policy.flows)
491
+ if (linked && cloud.policy.flows) {
266
492
  rootsSendFlows.add(own);
267
- if (linked !== wasLinked || (linked && cloud.projectId !== wasProject)) {
268
- wasLinked = linked;
269
- wasProject = cloud.projectId;
270
- if (linked)
493
+ }
494
+ // Another project's memory is another project's decisions: a re-link reads it at once.
495
+ const relinked = cloud.projectId !== wasProject;
496
+ const linkChanged = linked !== wasLinked || (linked && relinked);
497
+ wasLinked = linked;
498
+ wasProject = cloud.projectId;
499
+ // With a gate, `admit` already said why once per change; this would say "run connect" to
500
+ // somebody who only signed out or switched sync off.
501
+ if (deps.gate === undefined && linkChanged) {
502
+ if (linked) {
271
503
  log('reticle_cloud_linked', {
272
504
  projectId: cloud.projectId,
273
505
  root: deps.reticleRoot,
274
506
  });
507
+ }
275
508
  // Names the ROOT, because the answer is almost always "the daemon is not where you think it
276
509
  // is" — and a message that omits the directory sends people to check their key instead.
277
- else
510
+ else {
278
511
  log('reticle_cloud_unlinked', {
279
512
  root: deps.reticleRoot,
280
513
  fix: 'run `reticle connect` in this directory, or start the daemon in the linked one — nothing is being synced from here',
281
514
  });
515
+ }
282
516
  }
283
- // Every OTHER linked root, first. Their failures are logged and never abort this daemon's own
284
- // push: one repo whose credential was revoked must not silence the rest of the machine.
285
- await syncOtherRoots();
286
- if (null === cloud.config)
517
+ if (null === cloud.config) {
287
518
  return undefined;
288
- const report = await pushRoot(deps.reticleRoot, cloud);
519
+ }
520
+ const report = await pushRoot(deps.reticleRoot, cloud, memoryDue(asked || relinked));
289
521
  // `pushRoot` returns undefined only for an unlinked root, which the guard above has ruled out.
290
- if (report === undefined)
522
+ if (report === undefined) {
291
523
  return undefined;
524
+ }
292
525
  // Said out loud on EVERY cycle, including the ones that moved nothing and the ones that
293
526
  // failed. The log below is deliberately quiet when idle, which is right for a log and wrong
294
527
  // for a hook: from outside, "nothing changed" and "sync has been broken since Tuesday" are
@@ -307,21 +540,36 @@ export function startSyncDaemon(deps) {
307
540
  reportedError = failure;
308
541
  log('reticle_cloud_sync_failed', { error: failure });
309
542
  }
310
- if (report.error === undefined) {
311
- if (failure === undefined)
543
+ if (report.error === undefined && report.ok) {
544
+ lastPushAt = now();
545
+ }
546
+ if (report.error !== undefined) {
547
+ owed ||= carried;
548
+ failedCycle();
549
+ }
550
+ else {
551
+ if (failure === undefined) {
312
552
  reportedError = undefined;
553
+ }
313
554
  // Only when something actually moved. A per-minute "nothing to send" is noise that trains
314
555
  // people to stop reading the log.
315
556
  const moved = movedAnything(report);
316
- if (moved && failure === undefined)
557
+ if (moved && failure === undefined) {
317
558
  log('reticle_cloud_synced', { summary: describeSync(report) });
559
+ }
318
560
  /*
319
561
  * A cycle that moved something means a drive is in progress, so the next one comes sooner;
320
562
  * one that moved nothing means the machine is idle, so it backs straight off. The rate
321
563
  * follows the work without needing to be told about sessions, and an idle laptop settles at
322
564
  * the same cost it had before this existed.
323
565
  */
324
- nextDelay = moved ? activeIntervalMs : intervalMs;
566
+ if (failure !== undefined && !moved) {
567
+ failedCycle();
568
+ }
569
+ else {
570
+ failures = 0;
571
+ nextDelay = moved ? activeIntervalMs : intervalMs;
572
+ }
325
573
  }
326
574
  return report;
327
575
  }
@@ -332,66 +580,177 @@ export function startSyncDaemon(deps) {
332
580
  reportedError = message;
333
581
  log('reticle_cloud_sync_failed', { error: message });
334
582
  }
583
+ owed ||= carried;
584
+ failedCycle();
335
585
  return undefined;
336
586
  }
337
587
  finally {
588
+ // Every OTHER linked root, after this one: the root that changed is the one somebody is waiting
589
+ // on. Their failures are logged and never touch this root's push, whichever ran first.
590
+ if (othersDue) {
591
+ await syncOtherRoots().catch(() => undefined);
592
+ }
338
593
  await refreshUnsynced().catch(() => undefined);
339
594
  running = false;
595
+ startWatching();
340
596
  }
341
597
  };
342
598
  /** The delay for the NEXT cycle: fast while the last one moved something, slow once it stops. */
343
599
  let nextDelay = intervalMs;
600
+ /** Consecutive failed cycles: the retry waits a minute, then doubles, never past the backstop. */
601
+ let failures = 0;
602
+ function failedCycle() {
603
+ if (owed) {
604
+ nextDelay = FAILURE_RETRY_MS;
605
+ return;
606
+ }
607
+ nextDelay = Math.min(FAILURE_RETRY_MS * 2 ** failures, intervalMs);
608
+ failures += 1;
609
+ }
610
+ /** When the armed timer fires; Infinity when none is armed. */
611
+ let dueAt = Number.POSITIVE_INFINITY;
612
+ /** A change landed while a cycle was running: that cycle may have read the disk before it. */
613
+ let dirty = false;
344
614
  const schedule = (delay) => {
345
- if (stopped)
615
+ if (stopped) {
346
616
  return;
617
+ }
347
618
  // Replace, never stack. `nudge` and the interval both schedule, and leaving the old timer armed
348
619
  // would let every nudge add a permanent extra cycle per minute for the life of the process.
349
- if (timer !== undefined)
620
+ if (timer !== undefined) {
350
621
  clearTimeout(timer);
622
+ }
623
+ dueAt = now() + delay;
351
624
  timer = setTimeout(() => {
352
- void cycle().finally(() => schedule(nextDelay));
625
+ dueAt = Number.POSITIVE_INFINITY;
626
+ void cycle().finally(() => {
627
+ schedule(nextDelay);
628
+ afterCycle();
629
+ });
353
630
  }, delay);
354
631
  // Unref'd: a pending sync must never be the reason a process refuses to exit.
355
632
  timer.unref?.();
356
633
  };
634
+ /** Cycle within `delay`, but never LATER than a cycle already armed. */
635
+ const soon = (delay) => {
636
+ if (now() + delay < dueAt) {
637
+ schedule(delay);
638
+ }
639
+ };
640
+ /** One follow-up for the changes a cycle may have missed, however many there were. */
641
+ const afterCycle = () => {
642
+ if (!dirty) {
643
+ return;
644
+ }
645
+ dirty = false;
646
+ soon(CHANGE_DEBOUNCE_MS);
647
+ };
648
+ const onChange = (path) => {
649
+ if (stopped || isOwnWrite(path)) {
650
+ return;
651
+ }
652
+ if (isIntent(path)) {
653
+ intentMoved = true;
654
+ }
655
+ owed = true;
656
+ if (running) {
657
+ dirty = true;
658
+ }
659
+ else {
660
+ soon(CHANGE_DEBOUNCE_MS);
661
+ }
662
+ };
663
+ let unwatch;
664
+ /** Watch the root once it exists; a root created mid-session is picked up after the next cycle. */
665
+ const startWatching = () => {
666
+ if (stopped || unwatch !== undefined || deps.watch === undefined) {
667
+ return;
668
+ }
669
+ unwatch = deps.watch(onChange);
670
+ };
671
+ const stopWatching = () => {
672
+ unwatch?.();
673
+ unwatch = undefined;
674
+ };
357
675
  schedule(FIRST_CYCLE_DELAY_MS);
358
- // A nudge, not an immediate cycle: the link file and its key are written one after the other.
359
- const unwatch = deps.watchLink?.(() => schedule(NUDGE_DELAY_MS));
676
+ startWatching();
677
+ const poll = setInterval(() => void pollMemory(), MEMORY_POLL_TICK_MS);
678
+ poll.unref?.();
360
679
  return {
361
680
  // A sync somebody asked for writes what is held on a timer first: a drive's run waits a few
362
681
  // seconds after its last verdict, and a push straight after `session end` sent "0 of 0".
363
682
  syncNow: async () => {
364
683
  await flushPendingWrites();
365
- return cycle();
684
+ const report = await cycle(true);
685
+ afterCycle();
686
+ return report;
366
687
  },
367
688
  unsyncedRoots: () => unsynced,
689
+ background: () => {
690
+ const gate = gates.get(deps.reticleRoot);
691
+ return {
692
+ state: gate?.state ?? BackgroundSyncState.ON,
693
+ why: gate?.why ?? 'not checked yet',
694
+ failures,
695
+ ...(reportedError === undefined ? {} : { lastError: reportedError }),
696
+ ...(lastPushAt === undefined ? {} : { lastPushAt }),
697
+ };
698
+ },
368
699
  nudge: () => {
369
700
  // A cycle already in flight will not pick this up, so the nudge is still scheduled behind it
370
701
  // rather than dropped — otherwise the run that arrived during a slow cycle waits a full tick.
371
702
  schedule(NUDGE_DELAY_MS);
372
703
  },
373
704
  stop: () => {
374
- unwatch?.();
705
+ stopWatching();
706
+ clearInterval(poll);
375
707
  stopped = true;
376
- if (timer !== undefined)
708
+ if (timer !== undefined) {
377
709
  clearTimeout(timer);
710
+ }
378
711
  timer = undefined;
379
712
  },
380
713
  flush: async () => {
381
- unwatch?.();
714
+ stopWatching();
715
+ clearInterval(poll);
382
716
  stopped = true;
383
- if (timer !== undefined)
717
+ if (timer !== undefined) {
384
718
  clearTimeout(timer);
719
+ }
385
720
  timer = undefined;
386
721
  // A cycle already running may have started BEFORE the write being flushed, so its report says
387
722
  // nothing about that artifact. Wait it out rather than skipping on the overlap guard.
388
- const deadline = Date.now() + FLUSH_WAIT_MS;
389
- while (running && Date.now() < deadline)
723
+ const deadline = now() + FLUSH_BUDGET_MS;
724
+ while (running && now() < deadline) {
390
725
  await new Promise((resolve) => setTimeout(resolve, FLUSH_POLL_MS));
391
- await cycle();
726
+ }
727
+ // Not `asked`: on the way out, the run goes first; the memory and the other repos only if due.
728
+ // Bounded by what is left of the budget: a cycle can wait on a network that does not answer.
729
+ let lateTimer;
730
+ const late = new Promise((resolve) => {
731
+ lateTimer = setTimeout(resolve, Math.max(0, deadline - now()));
732
+ });
733
+ await Promise.race([cycle().then(() => undefined), late]);
734
+ clearTimeout(lateTimer);
392
735
  },
393
736
  };
394
737
  }
738
+ /**
739
+ * Flush, but return after `budgetMs` at the latest, and never throw.
740
+ *
741
+ * For a process on its way out. The cycle has per-call timeouts, but a stalled network still adds
742
+ * them up, and an exit must not wait on that. A cycle cut short loses nothing: the artifacts stay
743
+ * on disk, and the next daemon or `reticle sync` sends them.
744
+ */
745
+ export async function flushWithin(daemon, budgetMs) {
746
+ let timer;
747
+ const deadline = new Promise((resolve) => {
748
+ timer = setTimeout(resolve, budgetMs);
749
+ timer.unref?.();
750
+ });
751
+ await Promise.race([daemon.flush().catch(() => undefined), deadline]);
752
+ clearTimeout(timer);
753
+ }
395
754
  /**
396
755
  * Start cloud sync for a daemon and hand back the two hooks the daemon wires to it.
397
756
  *
@@ -405,30 +764,61 @@ export function startSyncDaemon(deps) {
405
764
  * resolving a credential — and the part a bootstrap file has no business spelling out.
406
765
  */
407
766
  export function attachCloudSync(deps) {
767
+ const signedIn = deps.signedIn;
408
768
  return startSyncDaemon({
409
769
  reticleRoot: deps.reticleRoot,
770
+ clientVersion: deps.clientVersion,
410
771
  cloud: () => resolveProjectCloud(deps.fs, deps.reticleRoot, deps.homeDir, deps.env),
411
772
  // Every OTHER linked repo on this machine, not just the directory the daemon was started in.
412
773
  // One daemon serves many projects, and pushing only its own root left the rest silently
413
774
  // reporting nothing — indistinguishable, on the dashboard, from nobody having verified anything.
414
775
  otherRoots: () => Promise.resolve(deps.otherRoots()),
415
776
  cloudFor: (root) => resolveProjectCloud(deps.fs, root, deps.homeDir, deps.env),
416
- // ponytail: this root only; a sibling root's re-link is still read on the next tick.
417
- watchLink: (onChange) => watchLinkFile(deps.reticleRoot, onChange),
777
+ // ponytail: this root only; sibling roots sync at most once a minute, when this one changes,
778
+ // and on the backstop timer.
779
+ // Watching them too means one recursive watcher per linked repo — add it if siblings lag.
780
+ watch: (onChange) => watchReticleRoot(deps.reticleRoot, onChange),
781
+ // Sent only while somebody is signed in, to a linked project that did not switch it off.
782
+ ...(signedIn === undefined
783
+ ? {}
784
+ : {
785
+ gate: (root, cloud) => backgroundSyncGate({ reticleRoot: root, cloud, env: deps.env, signedIn }),
786
+ }),
787
+ ...(deps.request === undefined ? {} : { request: deps.request }),
788
+ ...(deps.intervalMs === undefined ? {} : { intervalMs: deps.intervalMs }),
418
789
  });
419
790
  }
420
- /** Watch a root for its link file changing. A root with no `.reticle` yet is picked up per tick. */
421
- function watchLinkFile(reticleRoot, onChange) {
791
+ /**
792
+ * Watch a whole `.reticle` root, reporting each change by its path under the root.
793
+ *
794
+ * Returns undefined only when the root does not exist yet, so the caller tries again later. Where a
795
+ * recursive watch is unsupported (an old Node on Linux, a network filesystem) or fails later (too
796
+ * many files), it says so ONCE and returns a no-op: the backstop timer is the whole sync from then on.
797
+ */
798
+ export function watchReticleRoot(reticleRoot, onChange, watchFn = (root, listener) => watch(root, { recursive: true }, listener)) {
799
+ const unavailable = (error) => log('reticle_cloud_watch_unavailable', {
800
+ root: reticleRoot,
801
+ error: error instanceof Error ? error.message : String(error),
802
+ fallback: 'syncing on the timer only',
803
+ });
422
804
  try {
423
- const watcher = watch(reticleRoot, (_event, name) => {
424
- if (CLOUD_LINK_FILE === String(name))
425
- onChange();
805
+ const watcher = watchFn(reticleRoot, (_event, name) => {
806
+ if (name !== null) {
807
+ onChange(name);
808
+ }
809
+ });
810
+ watcher.on('error', (error) => {
811
+ unavailable(error);
812
+ watcher.close();
426
813
  });
427
- watcher.on('error', () => watcher.close());
428
814
  watcher.unref();
429
815
  return () => watcher.close();
430
816
  }
431
- catch {
817
+ catch (error) {
818
+ if ('ENOENT' === error.code) {
819
+ return undefined;
820
+ }
821
+ unavailable(error);
432
822
  return () => undefined;
433
823
  }
434
824
  }