@reticlehq/server 3.1.0 → 3.3.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 (384) hide show
  1. package/SKILL.md +30 -28
  2. package/dist/command/cli/adhoc-suite.d.ts +19 -0
  3. package/dist/command/cli/adhoc-suite.js +88 -0
  4. package/dist/command/cli/adhoc-verdict.d.ts +23 -2
  5. package/dist/command/cli/adhoc-verdict.js +18 -10
  6. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  7. package/dist/command/cli/cli-doctor.js +37 -5
  8. package/dist/command/cli/cli-flow-commands.d.ts +2 -1
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  11. package/dist/command/cli/cli-parse-grammar.js +7 -0
  12. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  13. package/dist/command/cli/cli-parse-verify.js +86 -13
  14. package/dist/command/cli/cli-parse.d.ts +8 -1
  15. package/dist/command/cli/cli-parse.js +38 -7
  16. package/dist/command/cli/cli-verify.d.ts +59 -0
  17. package/dist/command/cli/cli-verify.js +131 -19
  18. package/dist/command/cli/cloud-cli.js +7 -6
  19. package/dist/command/cli/cloud-kit.d.ts +0 -2
  20. package/dist/command/cli/cloud-kit.js +3 -7
  21. package/dist/command/cli/config/config-discovery.d.ts +12 -0
  22. package/dist/command/cli/config/config-discovery.js +19 -0
  23. package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
  24. package/dist/command/cli/doctor/doctor-rows.js +12 -0
  25. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  26. package/dist/command/cli/drive/drive-command.js +1 -1
  27. package/dist/command/cli/launch/cli-launch.d.ts +3 -23
  28. package/dist/command/cli/launch/cli-launch.js +29 -70
  29. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
  30. package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
  31. package/dist/command/cli/mcp-command.js +5 -3
  32. package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
  33. package/dist/command/cli/ports/resolve/cli-port.js +50 -2
  34. package/dist/command/cli/report-command.d.ts +16 -0
  35. package/dist/command/cli/report-command.js +97 -0
  36. package/dist/command/cli/status/status-lines.d.ts +28 -0
  37. package/dist/command/cli/status/status-lines.js +117 -0
  38. package/dist/command/cli.d.ts +1 -1
  39. package/dist/command/cli.js +29 -177
  40. package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
  41. package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
  42. package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
  43. package/dist/command/daemon/binding/loopback-reach.js +63 -0
  44. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  45. package/dist/command/daemon/daemon-resilience.d.ts +13 -1
  46. package/dist/command/daemon/daemon-resilience.js +12 -1
  47. package/dist/command/daemon/daemon.d.ts +2 -11
  48. package/dist/command/daemon/daemon.js +8 -11
  49. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  50. package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
  51. package/dist/command/daemon/lifetime/heartbeat.js +10 -0
  52. package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
  53. package/dist/command/daemon/lifetime/log-cap.js +115 -0
  54. package/dist/command/setup/agent-notice-memory.d.ts +16 -0
  55. package/dist/command/setup/agent-notice-memory.js +65 -0
  56. package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
  57. package/dist/command/setup/bringup/dev-server-wait.js +16 -0
  58. package/dist/command/setup/bringup/ensure-daemon.js +1 -1
  59. package/dist/command/setup/init/init-host.js +8 -0
  60. package/dist/command/setup/init/init-runtime.js +13 -1
  61. package/dist/command/setup/node-effects.js +1 -1
  62. package/dist/command/setup/probe/page-probe.js +9 -2
  63. package/dist/command/setup/run-setup.d.ts +25 -7
  64. package/dist/command/setup/run-setup.js +51 -9
  65. package/dist/command/setup/setup-command.js +31 -3
  66. package/dist/command/setup/terminal/confirm.js +2 -1
  67. package/dist/command/update/updater.d.ts +27 -0
  68. package/dist/command/update/updater.js +35 -2
  69. package/dist/features/crawl/crawl-tools.js +3 -1
  70. package/dist/features/crawl/crawl.d.ts +2 -0
  71. package/dist/features/crawl/crawl.js +3 -1
  72. package/dist/features/harness/drive-report.d.ts +73 -0
  73. package/dist/features/harness/drive-report.js +196 -0
  74. package/dist/features/harness/driver.js +29 -8
  75. package/dist/features/harness/drivers.d.ts +19 -0
  76. package/dist/features/harness/drivers.js +19 -0
  77. package/dist/features/harness/fill-values.d.ts +67 -0
  78. package/dist/features/harness/fill-values.js +153 -0
  79. package/dist/features/harness/jev-driver.d.ts +188 -0
  80. package/dist/features/harness/jev-driver.js +661 -0
  81. package/dist/features/harness/openai-driver.d.ts +46 -0
  82. package/dist/features/harness/openai-driver.js +197 -0
  83. package/dist/features/harness/platform-config.d.ts +62 -0
  84. package/dist/features/harness/platform-config.js +97 -0
  85. package/dist/features/visual/visual-tools.js +26 -2
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +80 -130
  88. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  89. package/dist/judgement/capsule/capsule-store.js +58 -6
  90. package/dist/judgement/capsule/capsule.js +5 -0
  91. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  92. package/dist/judgement/capsule/causal-summary.js +7 -0
  93. package/dist/judgement/domain/domain-model.js +38 -9
  94. package/dist/judgement/domain/domain-tools.js +4 -2
  95. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  96. package/dist/judgement/phenomena/phenomena.d.ts +2 -1
  97. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  98. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  99. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  100. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  101. package/dist/judgement/runs/artifact/render-report.js +4 -1
  102. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  103. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  104. package/dist/judgement/runs/context-tools.js +21 -3
  105. package/dist/judgement/runs/drive-run.js +7 -2
  106. package/dist/judgement/runs/verification-sync.d.ts +2 -2
  107. package/dist/judgement/runs/verification-sync.js +9 -2
  108. package/dist/language/flows/affected-tools.js +6 -3
  109. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  110. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  111. package/dist/language/flows/flow-anchor.d.ts +12 -0
  112. package/dist/language/flows/flow-anchor.js +20 -0
  113. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  114. package/dist/language/flows/flow-expect-grammar.js +92 -11
  115. package/dist/language/flows/flow-intent.js +1 -1
  116. package/dist/language/flows/flow-learning.d.ts +3 -0
  117. package/dist/language/flows/flow-learning.js +39 -8
  118. package/dist/language/flows/flow-replay-run.d.ts +55 -10
  119. package/dist/language/flows/flow-replay-run.js +217 -58
  120. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  121. package/dist/language/flows/flow-replay.d.ts +29 -11
  122. package/dist/language/flows/flow-replay.js +221 -203
  123. package/dist/language/flows/flow-result.d.ts +18 -1
  124. package/dist/language/flows/flow-result.js +18 -2
  125. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  126. package/dist/language/flows/flow-step-runners.js +20 -4
  127. package/dist/language/flows/flow-store-for-session.d.ts +2 -1
  128. package/dist/language/flows/flow-success.d.ts +21 -6
  129. package/dist/language/flows/flow-success.js +50 -138
  130. package/dist/language/flows/flow-tools.js +8 -4
  131. package/dist/language/flows/flows.d.ts +15 -10
  132. package/dist/language/flows/flows.js +17 -4
  133. package/dist/language/flows/heal.d.ts +9 -1
  134. package/dist/language/flows/heal.js +24 -2
  135. package/dist/language/flows/learned-sources.d.ts +21 -0
  136. package/dist/language/flows/learned-sources.js +84 -0
  137. package/dist/language/flows/narrow-write.d.ts +4 -4
  138. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  139. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  140. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  141. package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
  142. package/dist/language/flows/replay.js +13 -9
  143. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  144. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  145. package/dist/language/flows/stores/flake-store.js +2 -3
  146. package/dist/language/flows/suite/coverage.d.ts +1 -1
  147. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  148. package/dist/language/flows/suite-selection.d.ts +15 -1
  149. package/dist/language/flows/suite-selection.js +1 -1
  150. package/dist/language/flows/verify-change-tools.js +21 -4
  151. package/dist/memory/cloud/account-state.d.ts +1 -1
  152. package/dist/memory/cloud/account-state.js +5 -4
  153. package/dist/memory/cloud/cloud-config.d.ts +9 -0
  154. package/dist/memory/cloud/cloud-config.js +9 -0
  155. package/dist/memory/cloud/cloud-sync.d.ts +7 -4
  156. package/dist/memory/cloud/cloud-sync.js +11 -6
  157. package/dist/memory/cloud/harness-config.d.ts +12 -0
  158. package/dist/memory/cloud/harness-config.js +58 -0
  159. package/dist/memory/cloud/harness-offer.d.ts +44 -0
  160. package/dist/memory/cloud/harness-offer.js +146 -0
  161. package/dist/memory/cloud/harness-switch.d.ts +11 -0
  162. package/dist/memory/cloud/harness-switch.js +53 -0
  163. package/dist/memory/cloud/sync-cycle.d.ts +12 -0
  164. package/dist/memory/cloud/sync-cycle.js +29 -4
  165. package/dist/memory/cloud/sync-daemon.d.ts +35 -2
  166. package/dist/memory/cloud/sync-daemon.js +45 -0
  167. package/dist/memory/cloud/sync-disk.js +11 -6
  168. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  169. package/dist/memory/impact/impact-recorder.js +2 -1
  170. package/dist/memory/impact/impact-store.d.ts +5 -0
  171. package/dist/memory/impact/impact-store.js +25 -0
  172. package/dist/memory/intent/inline-intent.js +8 -3
  173. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  174. package/dist/memory/intent/intent-shard-store.js +253 -141
  175. package/dist/memory/intent/intent-shard.d.ts +22 -23
  176. package/dist/memory/intent/intent-shard.js +1 -1
  177. package/dist/memory/intent/intent-store.d.ts +13 -8
  178. package/dist/memory/intent/intent-store.js +46 -90
  179. package/dist/memory/intent/intent-tools.js +34 -23
  180. package/dist/memory/journal/ambient-store.js +2 -3
  181. package/dist/memory/journal/attach-journal.d.ts +11 -0
  182. package/dist/memory/journal/attach-journal.js +1 -1
  183. package/dist/memory/journal/deviation-report.js +4 -1
  184. package/dist/memory/journal/deviation-service.js +10 -2
  185. package/dist/memory/journal/drive-flow.d.ts +2 -2
  186. package/dist/memory/journal/drive-flow.js +21 -12
  187. package/dist/memory/journal/envelope-store.js +2 -3
  188. package/dist/memory/journal/journal-query.d.ts +35 -1
  189. package/dist/memory/journal/journal-query.js +46 -0
  190. package/dist/memory/journal/journal-recorder.d.ts +71 -2
  191. package/dist/memory/journal/journal-recorder.js +45 -7
  192. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  193. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  194. package/dist/memory/journal/on-disk/retention.d.ts +91 -2
  195. package/dist/memory/journal/on-disk/retention.js +181 -11
  196. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  197. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  198. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
  199. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
  200. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  201. package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
  202. package/dist/memory/journal/route-template.d.ts +22 -0
  203. package/dist/memory/journal/route-template.js +44 -0
  204. package/dist/memory/journal/self-observation.d.ts +3 -0
  205. package/dist/memory/journal/self-observation.js +67 -0
  206. package/dist/memory/journal/session-end.d.ts +17 -4
  207. package/dist/memory/journal/session-end.js +85 -7
  208. package/dist/memory/journal/session-journal.d.ts +122 -5
  209. package/dist/memory/journal/session-journal.js +489 -32
  210. package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
  211. package/dist/memory/project/artifact-root-resolver.js +89 -0
  212. package/dist/memory/project/artifact-root.d.ts +39 -2
  213. package/dist/memory/project/artifact-root.js +58 -1
  214. package/dist/memory/project/baselines.d.ts +1 -1
  215. package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
  216. package/dist/memory/project/dir/fill-value-store.js +88 -0
  217. package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
  218. package/dist/memory/project/dir/reticle-dir.js +11 -0
  219. package/dist/memory/project/fs/fs-port.d.ts +22 -1
  220. package/dist/memory/project/fs/fs-port.js +72 -5
  221. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  222. package/dist/memory/project/fs/write-atomic.js +48 -0
  223. package/dist/memory/project/learned-routes.d.ts +16 -2
  224. package/dist/memory/project/learned-routes.js +26 -10
  225. package/dist/memory/project/project-for-root.d.ts +30 -0
  226. package/dist/memory/project/project-for-root.js +43 -0
  227. package/dist/memory/project/project-tools.js +10 -5
  228. package/dist/memory/project/session-root.d.ts +35 -6
  229. package/dist/memory/project/session-root.js +70 -8
  230. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  231. package/dist/memory/recall/prior/connection-memory.js +8 -0
  232. package/dist/memory/recall/registered-projects.d.ts +2 -1
  233. package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
  234. package/dist/portal/bridge/auth-failure-reason.js +20 -3
  235. package/dist/portal/bridge/bridge.d.ts +6 -1
  236. package/dist/portal/bridge/bridge.js +36 -2
  237. package/dist/portal/input/real-input.js +2 -1
  238. package/dist/portal/pool/browser-pool.d.ts +10 -0
  239. package/dist/portal/pool/browser-pool.js +10 -0
  240. package/dist/portal/pool/playwright-launcher.js +1 -0
  241. package/dist/portal/pool/pool-contract.d.ts +2 -0
  242. package/dist/portal/pool/storage-seed.d.ts +2 -1
  243. package/dist/portal/pool/zero-install.d.ts +27 -0
  244. package/dist/portal/pool/zero-install.js +54 -0
  245. package/dist/portal/realm/web-realm.js +6 -0
  246. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  247. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  248. package/dist/portal/session/dev-server/served-document.js +57 -0
  249. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  250. package/dist/portal/session/facts/background-traffic.js +25 -0
  251. package/dist/portal/session/facts/session-replaced.d.ts +19 -0
  252. package/dist/portal/session/facts/session-replaced.js +22 -0
  253. package/dist/portal/session/live-control-tools.js +48 -3
  254. package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
  255. package/dist/portal/session/no-session-diagnosis.js +67 -60
  256. package/dist/portal/session/no-session-next-action.js +3 -2
  257. package/dist/portal/session/no-session-watch.js +14 -10
  258. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  259. package/dist/portal/session/rebind.d.ts +18 -0
  260. package/dist/portal/session/rebind.js +38 -0
  261. package/dist/portal/session/session-health.d.ts +61 -0
  262. package/dist/portal/session/session-health.js +40 -1
  263. package/dist/portal/session/session-info.d.ts +6 -0
  264. package/dist/portal/session/session-manager.d.ts +13 -0
  265. package/dist/portal/session/session-manager.js +42 -3
  266. package/dist/portal/session/session-successor.d.ts +11 -0
  267. package/dist/portal/session/session-successor.js +14 -1
  268. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  269. package/dist/portal/session/session-verdict-facts.js +15 -0
  270. package/dist/portal/session/session.d.ts +26 -10
  271. package/dist/portal/session/session.js +48 -49
  272. package/dist/status-payload.d.ts +12 -1
  273. package/dist/status-payload.js +4 -1
  274. package/dist/surface/http-server.js +3 -5
  275. package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
  276. package/dist/surface/mcp/faults/mcp-outage.js +20 -4
  277. package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
  278. package/dist/surface/mcp/mcp-proxy.js +23 -12
  279. package/dist/surface/mcp/mcp.d.ts +10 -1
  280. package/dist/surface/mcp/mcp.js +59 -29
  281. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
  282. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
  283. package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
  284. package/dist/surface/mcp/proxy/proxy-log.js +7 -22
  285. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  286. package/dist/surface/mcp/server-instructions.js +12 -1
  287. package/dist/surface/tools/act/act-preflight.js +6 -1
  288. package/dist/surface/tools/act/already-true.d.ts +40 -0
  289. package/dist/surface/tools/act/already-true.js +36 -0
  290. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
  291. package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
  292. package/dist/surface/tools/act/sequence-grade.js +13 -0
  293. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  294. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  295. package/dist/surface/tools/act-capsule.js +49 -8
  296. package/dist/surface/tools/act-tools.js +25 -24
  297. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  298. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  299. package/dist/surface/tools/assert/assert-grade.js +14 -2
  300. package/dist/surface/tools/assert/assert-verdict.js +74 -21
  301. package/dist/surface/tools/browser-tools.js +4 -3
  302. package/dist/surface/tools/coverage-tools.js +5 -2
  303. package/dist/surface/tools/dynamic-tools.js +43 -46
  304. package/dist/surface/tools/error-recovery.d.ts +6 -0
  305. package/dist/surface/tools/error-recovery.js +15 -0
  306. package/dist/surface/tools/explore-tools.js +80 -7
  307. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  308. package/dist/surface/tools/feedback-tools.js +50 -3
  309. package/dist/surface/tools/first-sentence.d.ts +24 -0
  310. package/dist/surface/tools/first-sentence.js +0 -0
  311. package/dist/surface/tools/harness-explore.d.ts +125 -1
  312. package/dist/surface/tools/harness-explore.js +401 -15
  313. package/dist/surface/tools/harness-plan.d.ts +76 -0
  314. package/dist/surface/tools/harness-plan.js +156 -0
  315. package/dist/surface/tools/harness-toolset.js +12 -1
  316. package/dist/surface/tools/invoke-tool.js +82 -9
  317. package/dist/surface/tools/lease-hint.d.ts +8 -0
  318. package/dist/surface/tools/lease-hint.js +10 -0
  319. package/dist/surface/tools/lease-tools.d.ts +16 -28
  320. package/dist/surface/tools/lease-tools.js +53 -5
  321. package/dist/surface/tools/lineage-tools.js +1 -1
  322. package/dist/surface/tools/live-call-text.d.ts +6 -0
  323. package/dist/surface/tools/live-call-text.js +36 -2
  324. package/dist/surface/tools/observe-tools.js +69 -15
  325. package/dist/surface/tools/read/query-windowed.d.ts +1 -1
  326. package/dist/surface/tools/read/query-windowed.js +1 -1
  327. package/dist/surface/tools/read-tools.js +1 -2
  328. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  329. package/dist/surface/tools/real-input-attempt.js +1 -1
  330. package/dist/surface/tools/tool-kit.d.ts +23 -3
  331. package/dist/surface/tools/tool-kit.js +6 -1
  332. package/dist/surface/tools/tool-surface.js +17 -5
  333. package/dist/surface/tools/tools-helpers.d.ts +8 -7
  334. package/dist/surface/tools/tools-helpers.js +11 -13
  335. package/dist/surface/tools/tools.js +70 -30
  336. package/dist/telemetry/bug-found.js +32 -2
  337. package/dist/telemetry/feedback-context.js +3 -3
  338. package/dist/telemetry/feedback-local.d.ts +1 -0
  339. package/dist/telemetry/feedback-local.js +1 -3
  340. package/dist/telemetry/feedback.d.ts +1 -1
  341. package/dist/telemetry/harness-drive.d.ts +6 -0
  342. package/dist/telemetry/harness-drive.js +39 -0
  343. package/dist/telemetry/hud-metrics.d.ts +32 -0
  344. package/dist/telemetry/hud-metrics.js +95 -0
  345. package/dist/telemetry/mcp-connection.d.ts +14 -1
  346. package/dist/telemetry/mcp-connection.js +17 -2
  347. package/dist/telemetry/onboarding-firsts.d.ts +30 -1
  348. package/dist/telemetry/onboarding-firsts.js +54 -12
  349. package/dist/telemetry/session-metrics.d.ts +5 -0
  350. package/dist/telemetry/session-metrics.js +15 -1
  351. package/dist/telemetry/verification-of.js +31 -3
  352. package/dist/wire-journal.d.ts +31 -0
  353. package/dist/wire-journal.js +100 -0
  354. package/docs/README.md +1 -0
  355. package/docs/agent-cheatsheet.md +7 -3
  356. package/docs/autodrive.md +42 -2
  357. package/docs/benchmarks.md +1 -1
  358. package/docs/capabilities.mdx +1 -1
  359. package/docs/cli/cloud.mdx +11 -9
  360. package/docs/cli/doctor.mdx +1 -0
  361. package/docs/cli/init.mdx +1 -0
  362. package/docs/cli/report.mdx +44 -0
  363. package/docs/cli/status.mdx +35 -20
  364. package/docs/cli.mdx +8 -18
  365. package/docs/desktop.mdx +1 -1
  366. package/docs/faq.mdx +1 -1
  367. package/docs/flows.md +11 -0
  368. package/docs/frameworks.mdx +113 -1
  369. package/docs/install-agentic.mdx +36 -0
  370. package/docs/install-manual.mdx +1 -1
  371. package/docs/packages/tauri.mdx +1 -1
  372. package/docs/packages.mdx +1 -1
  373. package/docs/predicates.mdx +144 -4
  374. package/docs/system-map.md +6 -4
  375. package/docs/telemetry.md +2 -1
  376. package/docs/token-efficiency.md +2 -2
  377. package/docs/tools/overview.mdx +1 -1
  378. package/docs/tools/query.mdx +10 -10
  379. package/docs/troubleshooting.mdx +8 -2
  380. package/docs/usage.md +2 -2
  381. package/docs/what-is-recorded.md +62 -0
  382. package/package.json +5 -5
  383. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  384. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
@@ -0,0 +1,32 @@
1
+ /** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
2
+ export declare function fetchStatus(port: number): Promise<unknown>;
3
+ /** One connected tab as `reticle status` reports it — the at-a-glance health line. */
4
+ interface StatusSession {
5
+ sessionId: string;
6
+ url: string;
7
+ projectId?: string;
8
+ throttled: boolean;
9
+ /**
10
+ * The tab is backgrounded. Reported by the daemon on every session; `false` on one too old to say,
11
+ * which reads as visible — the conservative answer, since it keeps the reuse this command has
12
+ * always done rather than opening a tab on a guess.
13
+ */
14
+ hidden: boolean;
15
+ stale: boolean;
16
+ pendingMarks: number;
17
+ /** The tab is attached and answering nothing — see Session.unresponsive. Absent on an older daemon. */
18
+ unresponsive?: true;
19
+ }
20
+ /**
21
+ * Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
22
+ * untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
23
+ * degrades to "running, 0 sessions" instead of throwing.
24
+ */
25
+ export declare function summarizeStatus(payload: unknown): {
26
+ sessionCount: number;
27
+ sessions: StatusSession[];
28
+ why?: string;
29
+ /** The same diagnosis without the differential, for a surface a person reads. */
30
+ whyLead?: string;
31
+ };
32
+ export {};
@@ -0,0 +1,93 @@
1
+ import * as http from 'node:http';
2
+ import { LOOPBACK_HOST, STATUS_PATH } from '@reticlehq/core';
3
+ import { loopbackAgent } from '../../../surface/loopback-agent.js';
4
+ /**
5
+ * The impure half of `probePresence`: ask the port whether a Reticle daemon is answering.
6
+ *
7
+ * It lives beside `port-presence.ts` rather than in `launch/` because every caller of
8
+ * `probePresence` passes it — status, doctor, kill, verify, drive, the daemon lifecycle and the
9
+ * MCP wake path all spell `probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus })`.
10
+ * Filed under `launch/` it read as a launcher helper, and it is not one; the proxy could not reach
11
+ * it there without inventing a dependency on a directory it has no other business with.
12
+ *
13
+ * The rule it feeds stays pure and socket-free in `port-presence.ts`. That split is deliberate: a
14
+ * probe that starts lying is a different failure from a rule that does.
15
+ */
16
+ /** How long the daemon /status probe waits before giving up — a local loopback call is near-instant. */
17
+ const STATUS_PROBE_TIMEOUT_MS = 1000;
18
+ /** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
19
+ export function fetchStatus(port) {
20
+ return new Promise((resolve) => {
21
+ const req = http.get({
22
+ host: LOOPBACK_HOST,
23
+ port,
24
+ path: STATUS_PATH,
25
+ timeout: STATUS_PROBE_TIMEOUT_MS,
26
+ // Shares the proxy's keep-alive agent: the proxy calls this on every reconnect, and a socket
27
+ // per probe is the same churn the POST leg was fixed for.
28
+ agent: loopbackAgent,
29
+ }, (res) => {
30
+ let body = '';
31
+ res.setEncoding('utf8');
32
+ res.on('data', (chunk) => (body += chunk));
33
+ res.on('end', () => {
34
+ try {
35
+ resolve(JSON.parse(body));
36
+ }
37
+ catch {
38
+ resolve(undefined);
39
+ }
40
+ });
41
+ });
42
+ req.on('error', () => resolve(undefined));
43
+ req.on('timeout', () => {
44
+ req.destroy();
45
+ resolve(undefined);
46
+ });
47
+ });
48
+ }
49
+ /**
50
+ * Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
51
+ * untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
52
+ * degrades to "running, 0 sessions" instead of throwing.
53
+ */
54
+ export function summarizeStatus(payload) {
55
+ if (typeof payload !== 'object' || null === payload)
56
+ return { sessionCount: 0, sessions: [] };
57
+ const obj = payload;
58
+ // Carried through to the printed line: with no sessions this is the whole answer, and dropping it
59
+ // here would silently undo the reason it is on the wire.
60
+ const why = 'string' === typeof obj['why'] ? obj['why'] : undefined;
61
+ // Absent on a daemon older than this field, which is why every reader falls back to `why`.
62
+ const whyLead = 'string' === typeof obj['whyLead'] ? obj['whyLead'] : undefined;
63
+ const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
64
+ const sessions = raw
65
+ .map((s) => {
66
+ if (typeof s !== 'object' || null === s)
67
+ return null;
68
+ const r = s;
69
+ const sessionId = 'string' === typeof r['sessionId'] ? r['sessionId'] : '';
70
+ if ('' === sessionId)
71
+ return null;
72
+ return {
73
+ sessionId,
74
+ url: 'string' === typeof r['url'] ? r['url'] : '',
75
+ ...('string' === typeof r['projectId'] && 0 < r['projectId'].length
76
+ ? { projectId: r['projectId'] }
77
+ : {}),
78
+ throttled: true === r['throttled'],
79
+ hidden: true === r['hidden'],
80
+ stale: true === r['stale'],
81
+ pendingMarks: 'number' === typeof r['pendingMarks'] ? r['pendingMarks'] : 0,
82
+ ...(true === r['unresponsive'] ? { unresponsive: true } : {}),
83
+ };
84
+ })
85
+ .filter((s) => s !== null);
86
+ const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
87
+ return {
88
+ sessionCount,
89
+ sessions,
90
+ ...(why === undefined ? {} : { why }),
91
+ ...(whyLead === undefined ? {} : { whyLead }),
92
+ };
93
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Which of `localhost`'s two answers can actually reach the bridge.
3
+ *
4
+ * `localhost` is a name with two answers, and Windows Chrome tries the IPv6 one first. The daemon
5
+ * binds `127.0.0.1`, so `loopback-alias.ts` forwards `[::1]:port` to it — best-effort by contract,
6
+ * because a machine with IPv6 off or something already holding `[::1]:port` must not stop the daemon
7
+ * serving. When that alias does not open, the daemon is serving perfectly on IPv4 and the page's
8
+ * documented default URL cannot reach it. The daemon's own view is "the bridge was up for the whole
9
+ * window", and it is right.
10
+ *
11
+ * Nothing said which half was missing. The one record was a field on the `mcp_daemon_started` log
12
+ * line, so the condition that makes `localhost` unreachable on the platform with most of our
13
+ * installs was invisible unless somebody read the daemon's JSON. The install gate's Windows cell
14
+ * reports exactly this shape — `init` exits 1 with "the SDK IS in the page and never dialled", while
15
+ * the page's own console says it could not open a websocket to `ws://localhost:<port>` after three
16
+ * attempts, and the very next boot of the same scaffold connects — and none of the three causes that
17
+ * diagnosis offers is this one.
18
+ *
19
+ * This answers it by connecting, at the moment of the diagnosis, rather than by remembering what
20
+ * happened at startup: a probe is true about now, and a startup flag is true about startup.
21
+ */
22
+ /** Which loopback addresses answered on the bridge port. */
23
+ export interface LoopbackReach {
24
+ /** `127.0.0.1` answered — what the daemon binds. */
25
+ readonly v4: boolean;
26
+ /** `[::1]` answered — what `localhost` resolves to first on Windows. */
27
+ readonly v6: boolean;
28
+ }
29
+ /**
30
+ * Probe both halves of loopback on the bridge port.
31
+ *
32
+ * Both are attempted even when the first answers: the interesting result is the DISAGREEMENT, and
33
+ * learning that v4 works tells you nothing about the address the browser will actually pick.
34
+ */
35
+ export declare function probeLoopbackReach(port: number): Promise<LoopbackReach>;
36
+ /**
37
+ * True when the bridge is reachable as an IPv4 address and NOT as the name the SDK is told to dial.
38
+ *
39
+ * This is the whole condition worth naming. Both up is healthy; both down is an ordinary "the daemon
40
+ * is not there" that every other check already reports; v6-only cannot happen on a `127.0.0.1` bind.
41
+ */
42
+ export declare function isLocalhostSplit(reach: LoopbackReach): boolean;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Which of `localhost`'s two answers can actually reach the bridge.
3
+ *
4
+ * `localhost` is a name with two answers, and Windows Chrome tries the IPv6 one first. The daemon
5
+ * binds `127.0.0.1`, so `loopback-alias.ts` forwards `[::1]:port` to it — best-effort by contract,
6
+ * because a machine with IPv6 off or something already holding `[::1]:port` must not stop the daemon
7
+ * serving. When that alias does not open, the daemon is serving perfectly on IPv4 and the page's
8
+ * documented default URL cannot reach it. The daemon's own view is "the bridge was up for the whole
9
+ * window", and it is right.
10
+ *
11
+ * Nothing said which half was missing. The one record was a field on the `mcp_daemon_started` log
12
+ * line, so the condition that makes `localhost` unreachable on the platform with most of our
13
+ * installs was invisible unless somebody read the daemon's JSON. The install gate's Windows cell
14
+ * reports exactly this shape — `init` exits 1 with "the SDK IS in the page and never dialled", while
15
+ * the page's own console says it could not open a websocket to `ws://localhost:<port>` after three
16
+ * attempts, and the very next boot of the same scaffold connects — and none of the three causes that
17
+ * diagnosis offers is this one.
18
+ *
19
+ * This answers it by connecting, at the moment of the diagnosis, rather than by remembering what
20
+ * happened at startup: a probe is true about now, and a startup flag is true about startup.
21
+ */
22
+ import net from 'node:net';
23
+ import { LOOPBACK_HOST } from '@reticlehq/core';
24
+ import { IPV6_LOOPBACK } from './loopback-alias.js';
25
+ /** Long enough for a loopback TCP handshake, short enough to sit inside a diagnosis. */
26
+ const REACH_TIMEOUT_MS = 500;
27
+ /** Can a TCP connection be opened to this address and port at all. */
28
+ function canConnect(host, port) {
29
+ return new Promise((resolve) => {
30
+ const socket = net.connect({ host, port });
31
+ const settle = (answer) => {
32
+ socket.destroy();
33
+ resolve(answer);
34
+ };
35
+ socket.setTimeout(REACH_TIMEOUT_MS, () => settle(false));
36
+ socket.once('connect', () => settle(true));
37
+ // Refused, unreachable, IPv6 disabled entirely — all of them are "this address does not answer",
38
+ // which is the only question being asked. An unhandled 'error' here would take down the caller.
39
+ socket.once('error', () => settle(false));
40
+ });
41
+ }
42
+ /**
43
+ * Probe both halves of loopback on the bridge port.
44
+ *
45
+ * Both are attempted even when the first answers: the interesting result is the DISAGREEMENT, and
46
+ * learning that v4 works tells you nothing about the address the browser will actually pick.
47
+ */
48
+ export async function probeLoopbackReach(port) {
49
+ const [v4, v6] = await Promise.all([
50
+ canConnect(LOOPBACK_HOST, port),
51
+ canConnect(IPV6_LOOPBACK, port),
52
+ ]);
53
+ return { v4, v6 };
54
+ }
55
+ /**
56
+ * True when the bridge is reachable as an IPv4 address and NOT as the name the SDK is told to dial.
57
+ *
58
+ * This is the whole condition worth naming. Both up is healthy; both down is an ordinary "the daemon
59
+ * is not there" that every other check already reports; v6-only cannot happen on a `127.0.0.1` bind.
60
+ */
61
+ export function isLocalhostSplit(reach) {
62
+ return reach.v4 && !reach.v6;
63
+ }
@@ -65,7 +65,7 @@ export declare function probePresence(port: number, probes: {
65
65
  * these lets the sentence tell them apart when — and only when — the recorded pid for the port
66
66
  * matches the process actually holding it.
67
67
  */
68
- export interface PortHolder {
68
+ interface PortHolder {
69
69
  /** The pid we recorded for this port, from the daemon pid file. `null` when there is none. */
70
70
  ourPid: number | null;
71
71
  /** The pid actually holding the port, when the caller could determine it. */
@@ -57,6 +57,18 @@ export type DaemonExitReason = (typeof DaemonExitReason)[keyof typeof DaemonExit
57
57
  export declare function recordExitReason(reason: DaemonExitReason): void;
58
58
  export declare function installExitTrace(proc: ProcessLike, log: LogFn): void;
59
59
  export declare function installDaemonResilience(proc: ProcessLike, log: LogFn, onFatal: () => void): void;
60
+ /** The handle the proxy keeps, so it can say when a session proved itself usable. */
61
+ export interface ProxyResilience {
62
+ /**
63
+ * A stream reached an `endpoint` frame: there is a working session, so whatever disconnects came
64
+ * before it were reconnects rather than a runaway.
65
+ *
66
+ * The same signal that already earns a fresh RETRY budget in `mcp-proxy.ts`, and chosen there for
67
+ * the same reason: response headers are produced by a daemon that accepts SSE and drops it, and an
68
+ * `endpoint` frame is not.
69
+ */
70
+ noteUsableSession(): void;
71
+ }
60
72
  /**
61
73
  * Process-level resilience for the MCP PROXY, whose rule is the opposite of the daemon's.
62
74
  *
@@ -70,5 +82,5 @@ export declare function installDaemonResilience(proc: ProcessLike, log: LogFn, o
70
82
  * `onCrash` is for reporting only — it must not end the process. `onGone` is the ONE exception, and
71
83
  * it is the subject of DISCONNECT_STORM below.
72
84
  */
73
- export declare function installProxyResilience(proc: ProcessLike, log: LogFn, onCrash?: (kind: CrashKind, cause: unknown) => void, onGone?: () => void): void;
85
+ export declare function installProxyResilience(proc: ProcessLike, log: LogFn, onCrash?: (kind: CrashKind, cause: unknown) => void, onGone?: () => void): ProxyResilience;
74
86
  export {};
@@ -290,7 +290,10 @@ export function installProxyResilience(proc, log, onCrash = reportCrash, onGone
290
290
  unreachable: 'reticle_mcp_proxy_daemon_unreachable',
291
291
  };
292
292
  // "Absorb the disconnect and keep serving" is right for ONE disconnect and catastrophic for a
293
- // stream of them — see DISCONNECT_STORM.
293
+ // stream of them — see DISCONNECT_STORM. Counted since the last USABLE SESSION rather than since
294
+ // this process started: a lifetime tally reaches the threshold on a working week, because the
295
+ // classifier reads `ECONNRESET` as "the peer vanished" and on this leg the peer is the DAEMON,
296
+ // which restarts routinely and is the thing the proxy exists to survive.
294
297
  let disconnects = 0;
295
298
  let gone = false;
296
299
  const absorb = (value) => {
@@ -309,6 +312,13 @@ export function installProxyResilience(proc, log, onCrash = reportCrash, onGone
309
312
  }
310
313
  return true;
311
314
  };
315
+ const noteUsableSession = () => {
316
+ // Not after `gone`: the process is already leaving, and a late frame must not look like a
317
+ // reason to stay. Nothing brings the proxy back from `onGone` by design.
318
+ if (gone)
319
+ return;
320
+ disconnects = 0;
321
+ };
312
322
  proc.on('unhandledRejection', (reason) => {
313
323
  if (absorb(reason))
314
324
  return;
@@ -325,4 +335,5 @@ export function installProxyResilience(proc, log, onCrash = reportCrash, onGone
325
335
  });
326
336
  onCrash(CrashKind.UNCAUGHT_EXCEPTION, err);
327
337
  });
338
+ return { noteUsableSession };
328
339
  }
@@ -1,3 +1,4 @@
1
+ export { MAX_DAEMON_LOG_BYTES, recoverOversizedLog } from './lifetime/log-cap.js';
1
2
  /** Env override for the whole state directory — see ReticleEnv.STATE_DIR. */
2
3
  export declare const STATE_DIR_ENV: "RETICLE_STATE_DIR";
3
4
  /**
@@ -10,20 +11,10 @@ export declare const STATE_DIR_ENV: "RETICLE_STATE_DIR";
10
11
  */
11
12
  export declare function reticleStateHome(): string;
12
13
  export declare function logPath(port: number): string;
13
- /**
14
- * How large a daemon log may get before it is rolled over to `<name>.1`.
15
- *
16
- * It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
17
- * is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
18
- * nothing ever reclaims. One previous generation is kept, because the question people bring to this
19
- * file ("what happened just now?") is answered by the current one and the question they bring next
20
- * ("and just before that?") is answered by the other.
21
- */
22
- export declare const MAX_DAEMON_LOG_BYTES: number;
23
14
  /**
24
15
  * Roll the log over if it has grown past the cap. Best-effort: a daemon must still start when the
25
16
  * log cannot be rotated, because failing to launch over housekeeping is strictly worse than a large
26
- * file.
17
+ * file. The cap itself, the mid-run check and the prune live in lifetime/log-cap.ts.
27
18
  */
28
19
  export declare function rotateDaemonLog(path: string, deps: {
29
20
  fileSize(p: string): number;
@@ -5,6 +5,10 @@ import { spawn } from 'node:child_process';
5
5
  import { daemonRegistryFileName, ReticleEnv } from '@reticlehq/core';
6
6
  import { log } from '../../log.js';
7
7
  import { uncleanPredecessor } from './lifetime/unclean-predecessor.js';
8
+ import { MAX_DAEMON_LOG_BYTES, pruneOldDaemonLogs } from './lifetime/log-cap.js';
9
+ // Re-exported from their owner so callers outside this directory — the proxy log, the rotation
10
+ // tests — keep reaching `daemon` rather than `lifetime`, which is a sink. See lifetime/log-cap.ts.
11
+ export { MAX_DAEMON_LOG_BYTES, recoverOversizedLog } from './lifetime/log-cap.js';
8
12
  /** Env override for the whole state directory — see ReticleEnv.STATE_DIR. */
9
13
  export const STATE_DIR_ENV = ReticleEnv.STATE_DIR;
10
14
  /**
@@ -30,20 +34,10 @@ function registryPath(port) {
30
34
  export function logPath(port) {
31
35
  return join(reticleStateHome(), `daemon-${port}.log`);
32
36
  }
33
- /**
34
- * How large a daemon log may get before it is rolled over to `<name>.1`.
35
- *
36
- * It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
37
- * is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
38
- * nothing ever reclaims. One previous generation is kept, because the question people bring to this
39
- * file ("what happened just now?") is answered by the current one and the question they bring next
40
- * ("and just before that?") is answered by the other.
41
- */
42
- export const MAX_DAEMON_LOG_BYTES = 8 * 1024 * 1024;
43
37
  /**
44
38
  * Roll the log over if it has grown past the cap. Best-effort: a daemon must still start when the
45
39
  * log cannot be rotated, because failing to launch over housekeeping is strictly worse than a large
46
- * file.
40
+ * file. The cap itself, the mid-run check and the prune live in lifetime/log-cap.ts.
47
41
  */
48
42
  export function rotateDaemonLog(path, deps, max = MAX_DAEMON_LOG_BYTES) {
49
43
  try {
@@ -259,6 +253,9 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
259
253
  // Roll the log BEFORE opening the append handle, or the daemon writes into the renamed file
260
254
  // through an fd that no longer matches any name anyone can find.
261
255
  rotateDaemonLog(logFilePath, deps);
256
+ // Every OTHER port's leftovers, while we are already here and holding the directory. The clock is
257
+ // read at this I/O boundary rather than injected, same as the timestamp written below.
258
+ pruneOldDaemonLogs(deps.home, Date.now());
262
259
  let logFd;
263
260
  try {
264
261
  logFd = deps.openFile(logFilePath, 'a');
@@ -1,4 +1,4 @@
1
- export interface IdleShutdownOptions {
1
+ interface IdleShutdownOptions {
2
2
  /** True when nothing is using the daemon: no agent connected, no browser session, no pool lease. */
3
3
  isIdle: () => boolean;
4
4
  /** Clean teardown + process exit. Called at most once. */
@@ -52,3 +52,4 @@ export declare function resolveIdleCheckMs(raw: string | undefined): number;
52
52
  * broken.
53
53
  */
54
54
  export declare function resolveIdleShutdownMs(raw: string | undefined): number;
55
+ export {};
@@ -51,6 +51,14 @@ interface DaemonHeartbeatOptions {
51
51
  intervalMs?: number;
52
52
  /** Injected, per the repo rule: never call Date.now() inside logic. */
53
53
  clock?: () => number;
54
+ /**
55
+ * The log file this beat writes into, for the size check that rides along with it. Injected only
56
+ * by tests; the default is the daemon's own stderr. See guardDaemonLog.
57
+ */
58
+ logGuard?: {
59
+ size(): number;
60
+ truncate(): void;
61
+ };
54
62
  }
55
63
  export declare class DaemonHeartbeat {
56
64
  #private;
@@ -17,6 +17,7 @@
17
17
  *
18
18
  * A heartbeat nobody interprets is just log volume.
19
19
  */
20
+ import { DAEMON_LOG_TRUNCATED_EVENT, guardDaemonLog } from './log-cap.js';
20
21
  /** The event name a beat carries. One `grep` separates liveness from everything else in the file. */
21
22
  export const DAEMON_HEARTBEAT_EVENT = 'reticle_daemon_alive';
22
23
  /** Written by installExitTrace when the process exits through Node. */
@@ -55,6 +56,7 @@ export class DaemonHeartbeat {
55
56
  #facts;
56
57
  #intervalMs;
57
58
  #clock;
59
+ #logGuard;
58
60
  #startedAt;
59
61
  #timer;
60
62
  constructor(opts) {
@@ -62,6 +64,7 @@ export class DaemonHeartbeat {
62
64
  this.#facts = opts.facts;
63
65
  this.#intervalMs = opts.intervalMs ?? DAEMON_HEARTBEAT_MS;
64
66
  this.#clock = opts.clock ?? (() => Date.now());
67
+ this.#logGuard = opts.logGuard;
65
68
  this.#startedAt = this.#clock();
66
69
  }
67
70
  /**
@@ -72,6 +75,13 @@ export class DaemonHeartbeat {
72
75
  * ambiguity this exists to remove.
73
76
  */
74
77
  beat() {
78
+ // The one thing a running daemon does on a timer, so it is also where the log it is filling gets
79
+ // looked at. `rotateDaemonLog` runs at spawn and never again: a detached daemon then writes for
80
+ // days with nothing checking the size, which is how a log reaches its cap with no mechanism left
81
+ // that would ever trim it. The beat is itself one of those writes.
82
+ const reclaimed = guardDaemonLog(this.#logGuard);
83
+ if (0 < reclaimed)
84
+ this.#log(DAEMON_LOG_TRUNCATED_EVENT, { reclaimedBytes: reclaimed });
75
85
  const facts = this.#facts();
76
86
  this.#log(DAEMON_HEARTBEAT_EVENT, {
77
87
  uptimeMs: this.#clock() - this.#startedAt,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * How big the daemon's log may get, and the three things that keep it there.
3
+ *
4
+ * `rotateDaemonLog` runs at spawn. `guardDaemonLog` runs while the daemon lives, because a detached
5
+ * daemon writes for days — a beat every thirty seconds plus every session, tool call and reconnect —
6
+ * and a size checked once at start is a size nothing will ever check again. A log measured at 98% of
7
+ * the cap and still climbing had no remaining mechanism that would trim it, and the tracker carries
8
+ * the same failure with a bigger number: the daemon log filling a disk, and a user who left over it.
9
+ * `pruneOldDaemonLogs` is the other half of the disk — one file per port ever bound, forever.
10
+ *
11
+ * Here rather than in daemon.ts because the size check has to be called from the heartbeat, and
12
+ * `lifetime` is a sink: nothing in it may reach back into its parent. Same reason nothing here
13
+ * logs — the caller that has a logger emits `DAEMON_LOG_TRUNCATED_EVENT`.
14
+ */
15
+ /**
16
+ * How large a daemon log may get before it is rolled over to `<name>.1`.
17
+ *
18
+ * It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
19
+ * is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
20
+ * nothing ever reclaims. One previous generation is kept, because the question people bring to this
21
+ * file ("what happened just now?") is answered by the current one and the question they bring next
22
+ * ("and just before that?") is answered by the other.
23
+ */
24
+ export declare const MAX_DAEMON_LOG_BYTES: number;
25
+ /**
26
+ * Reclaim a log that has already run past `max`, by TRUNCATING IT IN PLACE. Returns bytes reclaimed.
27
+ *
28
+ * The one definition behind both logs Reticle writes: the daemon log, and the MCP proxy log in
29
+ * surface/mcp/proxy/proxy-log.ts, which delegates to this. An uncapped proxy log has filled a disk
30
+ * and broken unrelated builds, Docker and ordinary shell commands with ENOSPC; the daemon log is
31
+ * larger, longer-lived and had the same hole, so it gets the same answer rather than a second one.
32
+ *
33
+ * In place, never renamed or unlinked: a rename moves the bytes without reclaiming a byte, and a
34
+ * file a running process still holds open keeps its blocks allocated after an unlink until that
35
+ * handle closes. `: > ~/.reticle/daemon-4400.log` is the operation being copied here.
36
+ *
37
+ * Best-effort, in the same spirit as `rotateDaemonLog`: dying over housekeeping is strictly worse
38
+ * than a large file.
39
+ */
40
+ export declare function recoverOversizedLog(deps: {
41
+ size(): number;
42
+ truncate(): void;
43
+ }, max?: number): number;
44
+ /** Emitted into the fresh log immediately after a truncation, so the gap explains itself. */
45
+ export declare const DAEMON_LOG_TRUNCATED_EVENT = "reticle_daemon_log_truncated";
46
+ /** Keep a RUNNING daemon's log under the cap. Returns the bytes reclaimed, 0 when it was fine. */
47
+ export declare function guardDaemonLog(deps?: {
48
+ size(): number;
49
+ truncate(): void;
50
+ }): number;
51
+ /**
52
+ * How long a daemon log outlives its daemon.
53
+ *
54
+ * Every port ever bound leaves a `daemon-<port>.log` behind and nothing removed any of them — one
55
+ * machine held 128 files in `~/.reticle`, logs the large majority. Nothing enumerates them either:
56
+ * every reader (`doctor`, the startup-cause reader, the lifecycle messages) opens the log of the
57
+ * port it is asking about right now, so a log for a port that will never be bound again is read by
58
+ * nobody. A week is long enough to still have last Tuesday's crash when somebody asks about it.
59
+ */
60
+ export declare const DAEMON_LOG_RETENTION_MS: number;
61
+ /**
62
+ * Delete daemon logs nothing has written to in the retention window. Returns the names removed.
63
+ *
64
+ * Age is the whole test, and it doubles as the liveness probe: a live daemon beats into its log
65
+ * every thirty seconds, so a log older than a week has no daemon behind it by construction.
66
+ * Best-effort throughout — a log that will not delete is not a reason to fail a daemon start.
67
+ */
68
+ export declare function pruneOldDaemonLogs(home: string, nowMs: number): string[];
@@ -0,0 +1,115 @@
1
+ /**
2
+ * How big the daemon's log may get, and the three things that keep it there.
3
+ *
4
+ * `rotateDaemonLog` runs at spawn. `guardDaemonLog` runs while the daemon lives, because a detached
5
+ * daemon writes for days — a beat every thirty seconds plus every session, tool call and reconnect —
6
+ * and a size checked once at start is a size nothing will ever check again. A log measured at 98% of
7
+ * the cap and still climbing had no remaining mechanism that would trim it, and the tracker carries
8
+ * the same failure with a bigger number: the daemon log filling a disk, and a user who left over it.
9
+ * `pruneOldDaemonLogs` is the other half of the disk — one file per port ever bound, forever.
10
+ *
11
+ * Here rather than in daemon.ts because the size check has to be called from the heartbeat, and
12
+ * `lifetime` is a sink: nothing in it may reach back into its parent. Same reason nothing here
13
+ * logs — the caller that has a logger emits `DAEMON_LOG_TRUNCATED_EVENT`.
14
+ */
15
+ import { fstatSync, ftruncateSync, readdirSync, statSync, unlinkSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ /**
18
+ * How large a daemon log may get before it is rolled over to `<name>.1`.
19
+ *
20
+ * It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
21
+ * is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
22
+ * nothing ever reclaims. One previous generation is kept, because the question people bring to this
23
+ * file ("what happened just now?") is answered by the current one and the question they bring next
24
+ * ("and just before that?") is answered by the other.
25
+ */
26
+ export const MAX_DAEMON_LOG_BYTES = 8 * 1024 * 1024;
27
+ /**
28
+ * Reclaim a log that has already run past `max`, by TRUNCATING IT IN PLACE. Returns bytes reclaimed.
29
+ *
30
+ * The one definition behind both logs Reticle writes: the daemon log, and the MCP proxy log in
31
+ * surface/mcp/proxy/proxy-log.ts, which delegates to this. An uncapped proxy log has filled a disk
32
+ * and broken unrelated builds, Docker and ordinary shell commands with ENOSPC; the daemon log is
33
+ * larger, longer-lived and had the same hole, so it gets the same answer rather than a second one.
34
+ *
35
+ * In place, never renamed or unlinked: a rename moves the bytes without reclaiming a byte, and a
36
+ * file a running process still holds open keeps its blocks allocated after an unlink until that
37
+ * handle closes. `: > ~/.reticle/daemon-4400.log` is the operation being copied here.
38
+ *
39
+ * Best-effort, in the same spirit as `rotateDaemonLog`: dying over housekeeping is strictly worse
40
+ * than a large file.
41
+ */
42
+ export function recoverOversizedLog(deps, max = MAX_DAEMON_LOG_BYTES) {
43
+ try {
44
+ const size = deps.size();
45
+ if (max >= size)
46
+ return 0;
47
+ deps.truncate();
48
+ return size;
49
+ }
50
+ catch {
51
+ return 0;
52
+ }
53
+ }
54
+ /**
55
+ * The daemon's log, as the daemon itself can address it: its own stderr.
56
+ *
57
+ * `spawnDaemon` hands the child `stdio: ['ignore', logFd, logFd]`, so inside a running daemon fd 2
58
+ * IS `daemon-<port>.log`, opened with O_APPEND — which is what makes truncating it safe: an
59
+ * appending write always lands at the new end of file, so there is no sparse hole and no lost
60
+ * handle. No path, no port and no second open are needed to reach it. When the daemon runs in the
61
+ * foreground fd 2 is a terminal, whose reported size is 0, so this is a no-op there.
62
+ */
63
+ const DAEMON_LOG_FD = 2;
64
+ /** Emitted into the fresh log immediately after a truncation, so the gap explains itself. */
65
+ export const DAEMON_LOG_TRUNCATED_EVENT = 'reticle_daemon_log_truncated';
66
+ const daemonLogFdOps = {
67
+ size: () => fstatSync(DAEMON_LOG_FD).size,
68
+ truncate: () => ftruncateSync(DAEMON_LOG_FD, 0),
69
+ };
70
+ /** Keep a RUNNING daemon's log under the cap. Returns the bytes reclaimed, 0 when it was fine. */
71
+ export function guardDaemonLog(deps = daemonLogFdOps) {
72
+ return recoverOversizedLog(deps);
73
+ }
74
+ /**
75
+ * How long a daemon log outlives its daemon.
76
+ *
77
+ * Every port ever bound leaves a `daemon-<port>.log` behind and nothing removed any of them — one
78
+ * machine held 128 files in `~/.reticle`, logs the large majority. Nothing enumerates them either:
79
+ * every reader (`doctor`, the startup-cause reader, the lifecycle messages) opens the log of the
80
+ * port it is asking about right now, so a log for a port that will never be bound again is read by
81
+ * nobody. A week is long enough to still have last Tuesday's crash when somebody asks about it.
82
+ */
83
+ export const DAEMON_LOG_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
84
+ /**
85
+ * Delete daemon logs nothing has written to in the retention window. Returns the names removed.
86
+ *
87
+ * Age is the whole test, and it doubles as the liveness probe: a live daemon beats into its log
88
+ * every thirty seconds, so a log older than a week has no daemon behind it by construction.
89
+ * Best-effort throughout — a log that will not delete is not a reason to fail a daemon start.
90
+ */
91
+ export function pruneOldDaemonLogs(home, nowMs) {
92
+ const pruned = [];
93
+ let files;
94
+ try {
95
+ files = readdirSync(home);
96
+ }
97
+ catch {
98
+ return pruned; // no ~/.reticle yet → nothing to prune
99
+ }
100
+ for (const file of files) {
101
+ if (null === /^daemon-\d+\.log(\.1)?$/.exec(file))
102
+ continue;
103
+ const path = join(home, file);
104
+ try {
105
+ if (DAEMON_LOG_RETENTION_MS > nowMs - statSync(path).mtimeMs)
106
+ continue;
107
+ unlinkSync(path);
108
+ pruned.push(file);
109
+ }
110
+ catch {
111
+ // unreadable or already gone — either way it is not this process's problem
112
+ }
113
+ }
114
+ return pruned;
115
+ }
@@ -0,0 +1,16 @@
1
+ /** One client we could not write to, and why. */
2
+ export interface ManualNotice {
3
+ readonly name: string;
4
+ readonly file: string;
5
+ readonly why: string;
6
+ }
7
+ /** Where the stamp lives, under the state home the caller resolves. */
8
+ export declare const NOTICE_MEMORY_BASENAME = "agent-notices.json";
9
+ /** Identity of a notice: the same client, the same file, for the same reason. */
10
+ export declare function noticeKey(n: ManualNotice): string;
11
+ /** Pure. The notices `said` has not already recorded, in order, without repeats. */
12
+ export declare function unsaidNotices(said: readonly string[], notices: readonly ManualNotice[]): ManualNotice[];
13
+ /** Every key recorded before. An unreadable or malformed stamp reads as "nothing was said". */
14
+ export declare function readSaid(stateHome: string): string[];
15
+ /** Record these keys beside whatever was there. A failed write costs a repeated line, nothing more. */
16
+ export declare function rememberSaid(stateHome: string, keys: readonly string[]): void;