@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
@@ -1,58 +1,33 @@
1
+ import { asProjectId } from '@reticlehq/core';
1
2
  import { refusedResult } from './page-commands/undeclared-command.js';
2
3
  import { recordImpact } from '../../memory/impact/impact-recorder.js';
3
4
  import { LastAct } from './last-act.js';
4
5
  import { GapLedger } from '@reticlehq/engine/evidence/gap-ledger.js';
5
6
  import { CaptureLedger } from '../../surface/tools/feature-capture.js';
6
7
  import { commandTimeoutMessage } from './timing/command-timeout.js';
7
- import { readHealthEvent, pendingNavigationMs } from './session-health.js';
8
+ import { readHealthEvent, healthNotices } from './session-health.js';
8
9
  import { MIRRORED_COMMANDS, mirroredNarration } from './presence/session-mirror.js';
9
10
  /** The HUD does not need 200 frames to watch a counter climb; the last state always lands. */
10
11
  const IMPACT_PUSH_DEBOUNCE_MS = 700;
11
12
  import { PendingCommands, CommandTimeoutError } from './page-commands/pending-commands.js';
12
13
  import { span } from '../../trace.js';
13
14
  import { keepCallerContextInDaemon, noteToDaemonLog } from './daemon-answers.js';
14
- import { isKnownRealm, EventType, HumanControlDataSchema, HumanControlKind, HumanMarkDataSchema, ReticleCommand, MessageKind, parseEventPayload, PresenterTone, SESSION_HEALTH, SESSION_LEASE, SESSION_LIFECYCLE, SessionState, } from '@reticlehq/core';
15
+ import { isKnownRealm, EventType, HumanControlDataSchema, HumanControlKind, HumanMarkDataSchema, HudUseDataSchema, ReticleCommand, MessageKind, parseEventPayload, PresenterTone, SESSION_HEALTH, SESSION_LEASE, SESSION_LIFECYCLE, SessionState, } from '@reticlehq/core';
15
16
  import { RingBuffer } from '@reticlehq/engine/window/ring-buffer.js';
16
- import { filterEvents, mergeEventsBySeq, } from '../../memory/journal/journal-query.js';
17
+ import { readJournalWriteLoss, readQueryEvents, } from '../../memory/journal/journal-query.js';
17
18
  import {} from '@reticlehq/engine/window/ambient.js';
18
19
  import { ObservedState } from './facts/observed-state.js';
19
- import { recordBrowserLatency, recordSdkFailure } from '../../telemetry/session-metrics.js';
20
+ import { recordBrowserLatency, recordHudUse, recordSdkFailure, } from '../../telemetry/session-metrics.js';
20
21
  import { LiveControl } from './human/live-control.js';
21
22
  import { ReviewStore } from './human/review-store.js';
22
23
  import { buildSessionRecommendation } from './presence/session-recommendation.js';
23
24
  import { buildPresenterArgs } from './human/presenter-args.js';
24
25
  import { buildSessionLease } from './presence/session-lease.js';
25
26
  import { buildSessionInfo } from './session-info.js';
27
+ import { MAX_SUCCESSOR_HOPS, REBINDABLE_COMMANDS } from './rebind.js';
26
28
  const DEFAULT_COMMAND_TIMEOUT_MS = 8000;
27
29
  /** Prefix on correlated command ids (c1, c2, …) — distinguishes them from mark ids. */
28
30
  const COMMAND_ID_PREFIX = 'c';
29
- /**
30
- * Commands that may be re-issued against the connection that replaced this session.
31
- *
32
- * Reads only, and the boundary is the point. Re-reading a page costs nothing and answers the same
33
- * question the caller asked. An act cannot be replayed: it may already have been dispatched in the
34
- * page that went away, and performing it a second time behind the caller's back is a double submit
35
- * nobody asked for. A replaced act still errors, and the caller's own retry — with the same id,
36
- * which is still the right one — is the safe path.
37
- */
38
- /**
39
- * How far to walk a chain of replacements before giving up.
40
- *
41
- * Generous for the real case (a page reloading a handful of times while a call is in flight) and
42
- * small enough that a cycle costs nothing. The number is a backstop, not a policy: a chain longer
43
- * than this means something is re-dialling in a loop, and that is a different problem.
44
- */
45
- const MAX_SUCCESSOR_HOPS = 32;
46
- const REBINDABLE_COMMANDS = new Set([
47
- ReticleCommand.SNAPSHOT,
48
- ReticleCommand.QUERY,
49
- ReticleCommand.MATCH,
50
- ReticleCommand.INSPECT,
51
- ReticleCommand.STATE_READ,
52
- ReticleCommand.STORAGE_READ,
53
- ReticleCommand.CAPABILITIES,
54
- ReticleCommand.ANIMATIONS,
55
- ]);
56
31
  /** Prefix on minted action ids (a1, a2, …) — the journal's action identity, independent of commands. */
57
32
  const ACTION_ID_PREFIX = 'a';
58
33
  /**
@@ -69,7 +44,13 @@ const WS_OPEN = 1;
69
44
  */
70
45
  export class Session {
71
46
  id;
72
- /** Stable build-stamped project identity; undefined for v1.0 SDKs that omit it. */
47
+ /**
48
+ * Stable build-stamped project identity; undefined for SDKs old enough to omit it.
49
+ *
50
+ * MINTED HERE. This is one of the two places a projectId enters the daemon with its provenance
51
+ * intact: the field is `projectId` on a zod-parsed HELLO, so the page said what it is. Every
52
+ * downstream reader takes the brand from this property rather than re-blessing a string.
53
+ */
73
54
  projectId;
74
55
  /**
75
56
  * The `.reticle` directory this session's evidence belongs in — impact counters AND capsules.
@@ -82,6 +63,11 @@ export class Session {
82
63
  * root, which is what every counter did unconditionally before this existed.
83
64
  */
84
65
  artifactRoot;
66
+ /**
67
+ * Same-origin endpoints this session's project declared as background (`.reticle.json`
68
+ * `background`). Stamped with `artifactRoot`, from the same project. Empty excludes nothing.
69
+ */
70
+ background = [];
85
71
  url;
86
72
  title;
87
73
  adapters;
@@ -145,7 +131,7 @@ export class Session {
145
131
  #editEpoch;
146
132
  constructor(hello, socket, clock) {
147
133
  this.id = hello.sessionId;
148
- this.projectId = hello.projectId;
134
+ this.projectId = hello.projectId === undefined ? undefined : asProjectId(hello.projectId);
149
135
  this.url = hello.url;
150
136
  this.title = hello.title;
151
137
  this.adapters = hello.adapters;
@@ -207,17 +193,16 @@ export class Session {
207
193
  }
208
194
  /** The attachable health block — single source of truth for the tools. */
209
195
  health() {
210
- // From event t=0, not a cursor: a wedge that began before the current action is exactly the
211
- // case a per-window reading cannot see, and is the one both reporters hit.
212
- const stuck = pendingNavigationMs(this.eventsSince(0), this.elapsed());
213
196
  const base = {
214
197
  lastSeenMs: this.lastSeenMs(),
215
198
  throttled: this.throttled(),
216
199
  focused: this.#focused,
217
- ...(stuck === undefined ? {} : { pendingNavigationMs: stuck }),
218
200
  // Carried onto every act/assert result, not just reticle_sessions: skew drops actions
219
201
  // SILENTLY, and the fields it contradicts are on the act verdict.
220
202
  ...(this.versionSkew === undefined ? {} : { versionSkew: this.versionSkew }),
203
+ // The absence-gated facts: a stuck navigation and an ledger worth naming. Both are absent on
204
+ // a healthy session, and `healthNotices` owns which is which.
205
+ ...healthNotices(this.eventsSince(0), this.elapsed(), this.#journalReader?.ledger?.()),
221
206
  };
222
207
  // attach the escape-hatch hint only when un-scriptable (keeps field absent otherwise).
223
208
  const recommendation = buildSessionRecommendation({
@@ -280,6 +265,14 @@ export class Session {
280
265
  keepCallerContext = keepCallerContextInDaemon;
281
266
  /** Re-stamp an incoming event with server-relative time, buffer it, and fan out. */
282
267
  pushEvent(event, byteSize) {
268
+ // Telemetry about the HUD, not evidence about the app: taken off here, before the buffer, so no
269
+ // verdict, settle window or agent read can ever see a person's clicks on Reticle's own panel.
270
+ if (event.type === EventType.HUD_USED) {
271
+ const use = HudUseDataSchema.safeParse(event.data);
272
+ if (use.success)
273
+ recordHudUse(use.data);
274
+ return;
275
+ }
283
276
  if (event.documentId !== undefined)
284
277
  this.#documentId = event.documentId;
285
278
  if (event.editEpoch !== undefined)
@@ -408,17 +401,16 @@ export class Session {
408
401
  this.#journalReader = reader;
409
402
  }
410
403
  /**
411
- * Journal-backed event query: the ring buffer's events, merged with the durable journal **only when
412
- * the buffer has evicted** (so a healthy session pays no disk cost), then filtered by since/until/
413
- * actionId. This is how "what did action N cause" is answered after the buffer has dropped the
414
- * evidence — the substrate's whole point. The sync `eventsSince`/`window` stay for the hot path.
404
+ * Journal-backed event query — the rule itself lives in `readQueryEvents`, beside the merge and
405
+ * filter it is made of. This is how "what did action N cause" is answered after the buffer has
406
+ * dropped the evidence. The sync `eventsSince`/`window` stay for the hot path.
415
407
  */
416
408
  async queryEvents(options) {
417
- if (this.#journalReader !== undefined && this.#buffer.bufferHealth().dropped > 0) {
418
- const durable = await this.#journalReader.readEvents();
419
- return filterEvents(mergeEventsBySeq(durable, this.#buffer.since(0)), options);
420
- }
421
- return filterEvents(this.#buffer.since(options.since ?? 0), options);
409
+ return readQueryEvents(this.#journalReader, this.#buffer, options);
410
+ }
411
+ /** What the durable ledger could not WRITE — see `readJournalWriteLoss` for the rule and why. */
412
+ async journalWriteLoss() {
413
+ return readJournalWriteLoss(this.#journalReader, this.#buffer);
422
414
  }
423
415
  /**
424
416
  * Open an action-attribution window: events observed until finishAction attribute to the returned
@@ -541,12 +533,19 @@ export class Session {
541
533
  return this.#buffer.bufferHealth();
542
534
  }
543
535
  /**
544
- * Did the buffer lose scarce evidence from a window opened at `cursor`? The input to whether a
536
+ * Did the EVENT STORE lose scarce evidence from a window opened at `cursor`? The input to whether a
545
537
  * verdict's capture was clean — see `RingBuffer.lostSince`, and never the raw drop counter, which
546
- * moves for the age and churn evictions that every live page produces continuously.
538
+ * moves for the age and churn evictions that every live page produces continuously. The journal is
539
+ * the other half of that store, so a durable read that could not reach back to `cursor` lost
540
+ * exactly what an eviction would have. Both boundaries are INCLUSIVE (`t` is a millisecond many
541
+ * records share) and an absent `lostThroughT` impeaches every window rather than none — the
542
+ * conservative direction, both times.
547
543
  */
548
544
  lostSince(cursor) {
549
- return this.#buffer.lostSince(cursor);
545
+ if (this.#buffer.lostSince(cursor))
546
+ return true;
547
+ const lost = this.#journalReader?.readLoss?.();
548
+ return lost !== undefined && (lost.lostThroughT === undefined || lost.lostThroughT >= cursor);
550
549
  }
551
550
  onEvent(listener) {
552
551
  this.#listeners.add(listener);
@@ -19,12 +19,23 @@ interface StatusPayload {
19
19
  * beside a live session.
20
20
  */
21
21
  why?: string;
22
+ /**
23
+ * The same diagnosis with the differential left off: the lead and the next action.
24
+ *
25
+ * Two fields because there are two readers. `why` is for an agent about to spend a drive on a
26
+ * guess, and carries the ranked causes, the port-scan caveat and the lease offer. `whyLead` is
27
+ * for a surface a PERSON reads: `reticle init` prints the daemon's account of a failed connect,
28
+ * and was printing roughly five hundred words of it in one paragraph.
29
+ */
30
+ whyLead?: string;
22
31
  /** Port of the verify HTTP endpoint this daemon serves — present only when started with `--http`. */
23
32
  verifyPort?: number;
24
33
  }
25
34
  export declare function statusPayload(sessionCount: number, sessions: SessionInfo[],
26
35
  /** The no-session diagnosis, injected so this stays pure and the port probe stays off this path. */
27
- why?: string, verifyPort?: number): StatusPayload;
36
+ why?: string, verifyPort?: number,
37
+ /** The short rendering of the same diagnosis. Absent on a daemon that has no provider wired. */
38
+ whyLead?: string): StatusPayload;
28
39
  /**
29
40
  * Why `serve --http` cannot claim success against an already-running daemon — or undefined when
30
41
  * that daemon already serves the verify HTTP endpoint on the wanted port.
@@ -2,7 +2,9 @@ import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
2
2
  import { SERVER_VERSION } from './command/version/identity/server-version.js';
3
3
  export function statusPayload(sessionCount, sessions,
4
4
  /** The no-session diagnosis, injected so this stays pure and the port probe stays off this path. */
5
- why, verifyPort) {
5
+ why, verifyPort,
6
+ /** The short rendering of the same diagnosis. Absent on a daemon that has no provider wired. */
7
+ whyLead) {
6
8
  return {
7
9
  running: true,
8
10
  version: SERVER_VERSION,
@@ -12,6 +14,7 @@ why, verifyPort) {
12
14
  // Only when there is nothing to explain away: a diagnosis printed beside a live session would
13
15
  // contradict it.
14
16
  ...(0 === sessionCount && why !== undefined ? { why } : {}),
17
+ ...(0 === sessionCount && whyLead !== undefined ? { whyLead } : {}),
15
18
  ...(verifyPort === undefined ? {} : { verifyPort }),
16
19
  };
17
20
  }
@@ -4,7 +4,6 @@ import { MCP_SSE_PATH, MCP_MESSAGE_PATH, MCP_SHUTDOWN_EVENT, STATUS_PATH, DRIVE_
4
4
  import { getSessionMetrics } from '../telemetry/session-metrics.js';
5
5
  import { noteAgentPeer, PEER_VERSION_PARAM, PEER_CONTRACT_PARAM, } from '../command/version/peer-announce.js';
6
6
  import { log } from '../log.js';
7
- import { reportMcpConnected } from '../telemetry/mcp-connection.js';
8
7
  import { isLoopbackHost, isLocalWebOrigin, isLoopbackPeer, requestToken, tokensMatch, } from '../portal/bridge/token-auth.js';
9
8
  import { LOOPBACK_IDLE_MS } from './loopback-agent.js';
10
9
  /**
@@ -177,10 +176,9 @@ export function createSharedServer(options = {}) {
177
176
  .connect(transport)
178
177
  .then(() => {
179
178
  log('mcp_client_connected', { sessionId: sid });
180
- // The one signal that separates "Reticle is running" from "somebody is USING it": a daemon
181
- // can sit up for days with nothing attached. It also exposes reconnect churn, which is
182
- // indistinguishable from healthy usage in every other metric.
183
- reportMcpConnected();
179
+ // The telemetry for this moved to `oninitialized`, where the client has said its name.
180
+ // An SSE stream that opens here and never handshakes is a probe, not a client, and
181
+ // reporting it from this point produced a row that could never name who attached.
184
182
  })
185
183
  .catch((err) => {
186
184
  const message = err instanceof Error ? err.message : String(err);
@@ -6,7 +6,8 @@
6
6
  * often does a real user's MCP server go down, and does it come back — had no answer outside the
7
7
  * machine it happened on.
8
8
  *
9
- * Capped at two events per proxy process, and the cap is the design rather than a limitation. One
9
+ * Capped at one benign and one fault event PER STAGE per proxy process, and the cap is the design
10
+ * rather than a limitation. One
10
11
  * measured afternoon produced 547 proxy reconnects; an event per reconnect would bill for the
11
12
  * pathology instead of measuring it, which is the same mistake the per-call `tool` event made before
12
13
  * it was removed. What a dashboard needs is the SHARE OF SESSIONS that lose MCP at all (the first
@@ -6,7 +6,8 @@
6
6
  * often does a real user's MCP server go down, and does it come back — had no answer outside the
7
7
  * machine it happened on.
8
8
  *
9
- * Capped at two events per proxy process, and the cap is the design rather than a limitation. One
9
+ * Capped at one benign and one fault event PER STAGE per proxy process, and the cap is the design
10
+ * rather than a limitation. One
10
11
  * measured afternoon produced 547 proxy reconnects; an event per reconnect would bill for the
11
12
  * pathology instead of measuring it, which is the same mistake the per-call `tool` event made before
12
13
  * it was removed. What a dashboard needs is the SHARE OF SESSIONS that lose MCP at all (the first
@@ -28,6 +29,19 @@ const KNOWN_REASONS = new Set(Object.values(OutageReason));
28
29
  function outageReason(raw) {
29
30
  return KNOWN_REASONS.has(raw) ? raw : OutageReason.OTHER;
30
31
  }
32
+ /**
33
+ * The daemon retiring on schedule is not a fault, and it is by far the commonest drop.
34
+ *
35
+ * Kept as the CLASS rather than the reason itself, because the cap is keyed on it: one benign drop
36
+ * and one fault per stage, not one event per reason. Keying on the reason would put the volume the
37
+ * cap exists to prevent straight back — a flapping proxy has several reasons available to it and
38
+ * would bill for each of them — while keying on the stage alone is what let a scheduled shutdown at
39
+ * minute one permanently suppress a real outage at minute forty, biasing the metric toward exactly
40
+ * the rows `daemon_shutdown` was introduced to exclude.
41
+ */
42
+ function isBenign(reason) {
43
+ return OutageReason.DAEMON_SHUTDOWN === reason;
44
+ }
31
45
  const reported = new Set();
32
46
  /** Reset between tests; a real process reports each stage at most once. */
33
47
  export function resetOutageReporting() {
@@ -39,13 +53,15 @@ export function resetOutageReporting() {
39
53
  * fails — the reconnect it is describing.
40
54
  */
41
55
  export function reportMcpOutage(stage, facts) {
42
- if (reported.has(stage))
56
+ const reason = outageReason(facts.reason);
57
+ const slot = `${stage}:${isBenign(reason) ? 'benign' : 'fault'}`;
58
+ if (reported.has(slot))
43
59
  return;
44
- reported.add(stage);
60
+ reported.add(slot);
45
61
  void getTelemetry().emit(TelemetryEventKind.MCP_CONNECTION_LOST, {
46
62
  outage: {
47
63
  stage,
48
- reason: outageReason(facts.reason),
64
+ reason,
49
65
  attempts: facts.attempts,
50
66
  // Always sent, including zero: zero is the finding — a drop no agent could feel. See
51
67
  // McpOutageSchema.pendingLost.
@@ -107,4 +107,13 @@ export declare function startMcpProxy(port: number,
107
107
  */
108
108
  ensureDaemon?: () => Promise<void>,
109
109
  /** Observe the real shutdown boundary in transport tests without terminating their worker. */
110
- exitProcess?: (code: number) => void): Promise<never>;
110
+ exitProcess?: (code: number) => void,
111
+ /**
112
+ * Tell the resilience layer this session proved itself usable.
113
+ *
114
+ * The same `endpoint` frame that earns a fresh retry budget below also clears the disconnect-storm
115
+ * counter, and for the same reason: response headers are produced by a daemon that accepts SSE and
116
+ * drops it, and an `endpoint` frame is not. Optional, because a transport test drives this
117
+ * function with no resilience installed.
118
+ */
119
+ onUsableSession?: () => void): Promise<never>;
@@ -10,9 +10,9 @@ export { probeDaemon, waitForDaemon } from './proxy/proxy-daemon-probe.js';
10
10
  import { probeDaemon } from './proxy/proxy-daemon-probe.js';
11
11
  import { SERVER_VERSION } from '../../command/version/identity/server-version.js';
12
12
  import { buildServerInstructions } from './server-instructions.js';
13
- import { hasProjectConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
13
+ import { hasAnyProjectConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
14
14
  import { reticleStateHome } from '../../command/daemon/daemon.js';
15
- import { readProjectId } from '../../command/cli/ports/resolve/cli-port.js';
15
+ import { projectIdsAt } from '../../command/cli/ports/resolve/cli-port.js';
16
16
  import { PEER_VERSION_PARAM, PEER_CONTRACT_PARAM } from '../../command/version/peer-announce.js';
17
17
  import { onStreamDrop, onClientRequest, onReconnectBudgetSpent, onQueueExpired, OnDrop, OnRequest, } from './proxy/proxy-lifecycle.js';
18
18
  import { describePresence, probePresence } from '../../command/daemon/binding/port-presence.js';
@@ -22,7 +22,7 @@ import { flushProxySessionMetrics } from '../../telemetry/proxy-telemetry.js';
22
22
  * the whole defect this import closes was the proxy answering a DIFFERENT question from every other
23
23
  * surface, and a second copy of the probe is how that comes back.
24
24
  */
25
- import { fetchStatus as fetchDaemonStatus } from '../../command/cli/launch/cli-launch.js';
25
+ import { fetchStatus as fetchDaemonStatus } from '../../command/daemon/binding/daemon-status-probe.js';
26
26
  /**
27
27
  * Re-exported so every existing caller (and every spec) keeps importing the proxy's log from the
28
28
  * proxy. Where the code lives is a file-size decision; where it is imported from is an API.
@@ -289,7 +289,7 @@ export function buildSessionUrl(rawData, port) {
289
289
  function proxyInstructions(port) {
290
290
  try {
291
291
  return buildServerInstructions({
292
- previouslyConnected: hasProjectConnectedBefore(reticleStateHome(), port, readProjectId(process.cwd())),
292
+ previouslyConnected: hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd())),
293
293
  });
294
294
  }
295
295
  catch {
@@ -304,7 +304,16 @@ export function startMcpProxy(port,
304
304
  */
305
305
  ensureDaemon,
306
306
  /** Observe the real shutdown boundary in transport tests without terminating their worker. */
307
- exitProcess = (code) => process.exit(code)) {
307
+ exitProcess = (code) => process.exit(code),
308
+ /**
309
+ * Tell the resilience layer this session proved itself usable.
310
+ *
311
+ * The same `endpoint` frame that earns a fresh retry budget below also clears the disconnect-storm
312
+ * counter, and for the same reason: response headers are produced by a daemon that accepts SSE and
313
+ * drops it, and an `endpoint` frame is not. Optional, because a transport test drives this
314
+ * function with no resilience installed.
315
+ */
316
+ onUsableSession = () => undefined) {
308
317
  return new Promise((_resolve, reject) => {
309
318
  // A client just started us, which is the only honest evidence that Reticle is registered with
310
319
  // one at all: a config entry proves somebody wrote JSON, not that a client ever read it. Paired
@@ -452,13 +461,11 @@ exitProcess = (code) => process.exit(code)) {
452
461
  const msg = parseJsonRpc(line);
453
462
  if (null === msg || msg.id === undefined || !pending.take(msg.id))
454
463
  return;
455
- if (failure.transport) {
456
- reportMcpOutage(OutageStage.FIRST, {
457
- reason: OutageReason.CONNECT_ERROR,
458
- attempts: failure.attempts,
459
- pendingLost: 1,
460
- });
461
- }
464
+ // No outage is reported here, deliberately. A POST leg that died while the SSE stream is
465
+ // still up is not the agent losing its tools: `postSocketFailures` on the session summary
466
+ // already counts it, and reporting it as a lost stream both double-counted and — because
467
+ // the outage cap is per process — let a transient POST failure in the first minute suppress
468
+ // the real stream outage in the fortieth.
462
469
  proxyLog('reticle_mcp_proxy_post_unanswered', {
463
470
  port,
464
471
  method: String(msg.method),
@@ -490,6 +497,10 @@ exitProcess = (code) => process.exit(code)) {
490
497
  reportMcpOutage(OutageStage.RECOVERED, { reason: lastDropReason, attempts: cost });
491
498
  attempts = 0;
492
499
  attemptsBeforeDormant = 0;
500
+ // The disconnects behind us were reconnects, not a runaway. Counting them for the life of
501
+ // the process made twenty daemon restarts over a working week indistinguishable from twenty
502
+ // in eight seconds, and the proxy left on the former.
503
+ onUsableSession();
493
504
  // The new session's McpServer has never seen the client's initialize — replay it first, then
494
505
  // flush whatever the client sent while we were reconnecting.
495
506
  for (const line of replay.replayLines())
@@ -12,7 +12,16 @@ export declare const CALL_TOOL_METHOD = "tools/call";
12
12
  * Non-session-bound tools (and tools with no outputSchema) are returned unchanged.
13
13
  */
14
14
  export declare function withSessionEnvelope(name: string, outputSchema: z.ZodRawShape | undefined): z.ZodRawShape | undefined;
15
- export declare function firstSentence(description: string): string;
15
+ /**
16
+ * Abbreviations whose internal period is NOT a sentence end.
17
+ *
18
+ * The splitter looked for the first `". "`, which "e.g. " satisfies — so every lean-profile
19
+ * description containing one was cut off mid-abbreviation. `reticle_act`'s ref parameter reached the
20
+ * agent as "Element ref from reticle_snapshot or reticle_query (e.g." and stopped: the example gone,
21
+ * and any contract stated after it gone too. Silent, and it degraded the DEFAULT profile only, which
22
+ * is the one nobody reads the raw strings for.
23
+ */
24
+ export { firstSentence } from '../tools/first-sentence.js';
16
25
  /**
17
26
  * Serialize a tool result to the MCP `text` content block. The agent consumes this text — the
18
27
  * typed contract travels separately as `structuredContent`, unchanged by encoding — so the text
@@ -36,6 +36,7 @@ import { log } from '../../log.js';
36
36
  import { SERVER_VERSION } from '../../command/version/identity/server-version.js';
37
37
  import { setMcpClientNameHook } from '../../telemetry/feedback-context.js';
38
38
  import { getSessionMetrics } from '../../telemetry/session-metrics.js';
39
+ import { reportMcpConnected } from '../../telemetry/mcp-connection.js';
39
40
  import { parsePredicate } from '@reticlehq/engine/question/predicate/predicate-parse.js';
40
41
  /**
41
42
  * Merge the runtime-spliced envelope (health/lease/age/control) into a session-bound tool's declared
@@ -70,16 +71,9 @@ function withExample(description, example) {
70
71
  * and any contract stated after it gone too. Silent, and it degraded the DEFAULT profile only, which
71
72
  * is the one nobody reads the raw strings for.
72
73
  */
73
- const ABBREVIATIONS = ['e.g.', 'i.e.', 'etc.', 'vs.', 'cf.'];
74
- export function firstSentence(description) {
75
- const nl = description.indexOf('\n');
76
- const base = nl >= 0 ? description.slice(0, nl) : description;
77
- // Mask abbreviations with an equal-length filler so offsets stay valid in the original string.
78
- const masked = ABBREVIATIONS.reduce((text, abbr) => text.split(abbr).join('\u0000'.repeat(abbr.length)), base);
79
- const dot = masked.search(/\.\s/);
80
- const sentence = dot >= 0 ? base.slice(0, dot + 1) : base;
81
- return sentence.length > 160 ? `${sentence.slice(0, 159)}…` : sentence;
82
- }
74
+ // Re-exported because callers (and `mcp.test.ts`) have imported it from here since it lived here.
75
+ export { firstSentence } from '../tools/first-sentence.js';
76
+ import { firstSentence } from '../tools/first-sentence.js';
83
77
  /**
84
78
  * Parameters carrying the recursive predicate grammar. Advertised compactly in lean profiles.
85
79
  *
@@ -124,8 +118,8 @@ const PREDICATE_KINDS = 'Predicate object: { kind, ...fields }. kind is one of e
124
118
  * paying for six.
125
119
  */
126
120
  const PREDICATE_FIELD_GRAMMAR_HINT = ' Bug-catching options: net.count (exact request count — catches double-submit), ' +
127
- 'net.bodyContains (a substring of what the SERVER answered — catches a UI that echoes the input ' +
128
- 'it sent instead of the result it got back), net.requestBodyMatches (a shallow JSON match on what ' +
121
+ 'net.bodyMatches (a shallow JSON match on what the SERVER answered — say the FIELD and its value, ' +
122
+ 'because a substring of the body matches key names too), net.requestBodyMatches (the same on what ' +
129
123
  'the UI SENT — the verdict for "applying this filter actually puts it in the payload"), ' +
130
124
  'console.absent:true (action completed with a CLEAN ' +
131
125
  'console), absent:true on element/text (it should be gone). Call reticle_tools for the full field ' +
@@ -218,21 +212,33 @@ tools = tableForSurface(surface)) {
218
212
  const shipped = new Set(tableForSurface(surface).map((tool) => tool.name));
219
213
  const ours = tools.filter((tool) => shipped.has(tool.name));
220
214
  const theirs = tools.filter((tool) => !shipped.has(tool.name));
221
- // The `merged` surface keeps `reticle_tools` and drops `reticle_run`, which is not symmetry for
222
- // its own sake. `reticle_run` exists to DISPATCH to a tool the surface does not advertise, and
223
- // this surface advertises everything, so it has no job left. `reticle_tools` has a second job that
224
- // survives: every input schema on a trimmed surface is LEAN, and `{ names: [...] }` is the only
225
- // way to get a tool's full parameters — which matters more here than anywhere, because a merged
226
- // tool's schema is the union of its members' fields with all of them optional. Recovery messages
227
- // across this server also say "Call reticle_tools", and a surface without it turns that advice
228
- // into a dead end (see surface-sizes.test.ts).
229
- // A surface with no `reticle_run` can invoke only what it advertises, so the catalogue is told to
230
- // list exactly that. Anything else is a menu of names the agent cannot order from.
215
+ /*
216
+ * `merged` used to drop `reticle_run`, on the reasoning that the hatch "exists to DISPATCH to a
217
+ * tool the surface does not advertise, and this surface advertises everything, so it has no job
218
+ * left".
219
+ *
220
+ * THE PREMISE WAS FALSE, and it had been false for some time. `MERGED_TOOL_NAMES` is
221
+ * `CORE_TOOL_NAMES` minus the merged-away names plus `LOOK`; `EXTENDED_TOOL_NAMES` — screenshot,
222
+ * visual diff, clock, network mock, storage, record, the flow pair, intent, context, capabilities
223
+ * — is not in it. So eleven registered tools were advertised by nothing, callable by nothing, and
224
+ * catalogued by nothing: `reticle_tools { names: ["reticle_screenshot"] }` answered `unknown
225
+ * tool`, on a build where the tool exists and is registered.
226
+ *
227
+ * The extended set's own comments say each demoted tool is "still one `reticle_run` hop from any
228
+ * agent that wants it". That is the trade the demotion was justified by, and dropping the hatch
229
+ * silently cancelled the half of it we owed the caller. One of the casualties was
230
+ * `reticle_visual_diff`, which covers the only bug class the registry marks as one Reticle cannot
231
+ * otherwise see.
232
+ *
233
+ * So the hatch stays, and the catalogue lists everything the hatch can reach. `reticle_tools`
234
+ * keeps its second job either way: every input schema on a trimmed surface is LEAN, and
235
+ * `{ names: [...] }` is the only way to get full parameters — which matters most here, because a
236
+ * merged tool's schema is the union of its members' fields with all of them optional. Recovery
237
+ * messages across this server also say "Call reticle_tools", and a surface without it turns that
238
+ * advice into a dead end (see surface-sizes.test.ts).
239
+ */
231
240
  const advertisedHere = filterTools([...ours], surface);
232
- const callable = surface === TOOL_SURFACE.MERGED
233
- ? new Set([...advertisedHere, ...theirs].map((tool) => tool.name))
234
- : undefined;
235
- const meta = buildDynamicTools([...tools], origin, callable).filter((tool) => surface !== TOOL_SURFACE.MERGED || tool.name !== ReticleTool.RUN);
241
+ const meta = buildDynamicTools([...tools], origin, undefined);
236
242
  return [...advertisedHere, ...theirs, ...meta];
237
243
  }
238
244
  /**
@@ -512,8 +518,14 @@ tools = tableForSurface(profile)) {
512
518
  */
513
519
  server.server.oninitialized = () => {
514
520
  const info = server.server.getClientVersion();
515
- if (info?.name !== undefined)
516
- getSessionMetrics().recordClient(info.name, info.version);
521
+ if (info?.name === undefined)
522
+ return;
523
+ getSessionMetrics().recordClient(info.name, info.version);
524
+ // The one signal that separates "Reticle is running" from "somebody is USING it", reported from
525
+ // the first moment it can carry WHO. It used to fire from the SSE `connect()` resolution, which
526
+ // happens before the handshake — so every connect row was anonymous, and the field meant to say
527
+ // which agent client converts best was empty on every event ever sent.
528
+ reportMcpConnected(info.name);
517
529
  };
518
530
  // Which agent is on the other end, for a feedback report. Registered as a lazy hook rather than
519
531
  // read here: the handshake has not happened yet at construction time, and a report filed twenty
@@ -540,7 +552,25 @@ tools = tableForSurface(profile)) {
540
552
  const advertised = advertisedTools(profile, tools);
541
553
  // The names this surface actually hands the agent. Every piece of advice leaving this server is
542
554
  // rewritten against it, so a static string cannot route a reader to a tool they were not given.
543
- const advertisedNames = new Set(advertised.map((tool) => tool.name));
555
+ /**
556
+ * What a name in advice can actually REACH, which is not the same as what is advertised.
557
+ *
558
+ * `liveCallText` rewrites mentions of tools this surface cannot reach — it exists because advice
559
+ * naming `reticle_flow_save` on a surface without it sent agents at a tool that answered "not
560
+ * reachable on this tool surface". Gated on the ADVERTISED set, it kept firing once the dispatch
561
+ * hatch came back: `reticle_flow_save` and `reticle_record` are reachable through
562
+ * `reticle_run` but not advertised, so every mention of them — INCLUDING their own names and the
563
+ * parameter docs that name the recording to save — was rewritten to
564
+ * `reticle_verify { action: "explore", persona }`. Two tools with the same wrong name, and a
565
+ * `flowName` description telling the reader to pass the name that a tool which is not the one
566
+ * they called was called with.
567
+ *
568
+ * With a hatch, every registered tool is reachable and no rewriting is owed. Without one,
569
+ * advertised IS reachable, which is the case this was written for.
570
+ */
571
+ const advertisedNames = advertised.some((tool) => ReticleTool.RUN === tool.name)
572
+ ? new Set(tools.map((tool) => tool.name))
573
+ : new Set(advertised.map((tool) => tool.name));
544
574
  installFriendlyArgErrors(server, new Map(advertised
545
575
  .filter((tool) => tool.example !== undefined)
546
576
  .map((tool) => [tool.name, JSON.stringify(tool.example)])),
@@ -5,5 +5,18 @@ export declare function daemonPollDelayMs(attempt: number): number;
5
5
  * inside the daemon just to check reachability.
6
6
  */
7
7
  export declare function probeDaemon(port: number): Promise<boolean>;
8
- /** Poll until the daemon's HTTP port accepts connections or the deadline is reached. */
9
- export declare function waitForDaemon(port: number): Promise<void>;
8
+ /**
9
+ * Poll until a Reticle daemon is SERVING on `port`, or the deadline passes.
10
+ *
11
+ * "Accepts a TCP connection" is not the question. A daemon wedged mid-start accepts and never
12
+ * serves, and so does a stranger holding the port — `port-presence.ts` calls both FOREIGN for
13
+ * exactly this reason, and every other decision point in the codebase was migrated onto it. This
14
+ * one was not, so the wake path spawned a daemon, waited on a bare connect, and dialled a corpse.
15
+ *
16
+ * Probes are injected (defaulting to the real pair) so the rule is testable without a socket, which
17
+ * is the same shape `probePresence`'s own callers use.
18
+ */
19
+ export declare function waitForDaemon(port: number, probes?: {
20
+ tcpOpen: (port: number) => Promise<boolean>;
21
+ status: (port: number) => Promise<unknown>;
22
+ }): Promise<void>;
@@ -1,5 +1,7 @@
1
1
  import * as net from 'node:net';
2
2
  import { LOOPBACK_HOST } from '@reticlehq/core';
3
+ import { describePresence, presenceIsUsable, probePresence, PortPresence, } from '../../../command/daemon/binding/port-presence.js';
4
+ import { fetchStatus } from '../../../command/daemon/binding/daemon-status-probe.js';
3
5
  const DEFAULT_DAEMON_READY_TIMEOUT_MS = 10_000;
4
6
  /**
5
7
  * How long to wait for the spawned daemon's port to accept connections before giving up. The default
@@ -39,16 +41,31 @@ export function probeDaemon(port) {
39
41
  socket.connect(port, LOOPBACK_HOST);
40
42
  });
41
43
  }
42
- /** Poll until the daemon's HTTP port accepts connections or the deadline is reached. */
43
- export async function waitForDaemon(port) {
44
+ /**
45
+ * Poll until a Reticle daemon is SERVING on `port`, or the deadline passes.
46
+ *
47
+ * "Accepts a TCP connection" is not the question. A daemon wedged mid-start accepts and never
48
+ * serves, and so does a stranger holding the port — `port-presence.ts` calls both FOREIGN for
49
+ * exactly this reason, and every other decision point in the codebase was migrated onto it. This
50
+ * one was not, so the wake path spawned a daemon, waited on a bare connect, and dialled a corpse.
51
+ *
52
+ * Probes are injected (defaulting to the real pair) so the rule is testable without a socket, which
53
+ * is the same shape `probePresence`'s own callers use.
54
+ */
55
+ export async function waitForDaemon(port, probes = { tcpOpen: probeDaemon, status: fetchStatus }) {
44
56
  const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
45
57
  let attempt = 0;
58
+ let presence = PortPresence.FREE;
46
59
  while (Date.now() < deadline) {
47
- const reachable = await probeDaemon(port);
48
- if (reachable)
60
+ presence = await probePresence(port, probes);
61
+ if (presenceIsUsable(presence))
49
62
  return;
50
63
  attempt++;
51
64
  await delay(daemonPollDelayMs(attempt));
52
65
  }
53
- throw new Error(`reticle daemon did not become ready on port ${port} within ${DAEMON_READY_TIMEOUT_MS}ms`);
66
+ // The state we timed out in is the whole diagnosis — FREE means the daemon never came up, FOREIGN
67
+ // means something is squatting the port. Reporting only the timeout sent readers hunting the
68
+ // wrong one.
69
+ throw new Error(`reticle daemon did not become ready on port ${String(port)} within ` +
70
+ `${String(DAEMON_READY_TIMEOUT_MS)}ms — ${describePresence(presence, port)}`);
54
71
  }