@reticlehq/server 2.12.0 → 2.13.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 (386) hide show
  1. package/SKILL.md +67 -109
  2. package/dist/bridge/auth-failure-reason.d.ts +6 -2
  3. package/dist/bridge/auth-failure-reason.js +15 -8
  4. package/dist/bridge/bridge.d.ts +18 -1
  5. package/dist/bridge/bridge.js +247 -164
  6. package/dist/bridge/pairing-token.d.ts +5 -0
  7. package/dist/bridge/pairing-token.js +28 -0
  8. package/dist/bridge/protocol-skew.d.ts +37 -0
  9. package/dist/bridge/protocol-skew.js +45 -0
  10. package/dist/capsule/capsule-store.d.ts +6 -6
  11. package/dist/capsule/causal-summary.js +7 -2
  12. package/dist/cli/cli-doctor.js +31 -1
  13. package/dist/cli/cli-launch.d.ts +11 -1
  14. package/dist/cli/cli-launch.js +59 -10
  15. package/dist/cli/cli-parse.d.ts +15 -1
  16. package/dist/cli/cli-parse.js +212 -8
  17. package/dist/cli/cli-verify.d.ts +1 -0
  18. package/dist/cli/cli-verify.js +3 -2
  19. package/dist/cli/cloud-cli.js +428 -259
  20. package/dist/cli/cloud-keystore.d.ts +37 -0
  21. package/dist/cli/cloud-keystore.js +97 -0
  22. package/dist/cli/cloud-kit.d.ts +37 -0
  23. package/dist/cli/cloud-kit.js +121 -0
  24. package/dist/cli/cloud-login.d.ts +17 -0
  25. package/dist/cli/cloud-login.js +183 -0
  26. package/dist/cli/cloud-session.d.ts +48 -0
  27. package/dist/cli/cloud-session.js +53 -0
  28. package/dist/cli/config-discovery.js +1 -0
  29. package/dist/cli/daemon-start-options.d.ts +32 -0
  30. package/dist/cli/daemon-start-options.js +38 -0
  31. package/dist/cli/device-flow.d.ts +76 -0
  32. package/dist/cli/device-flow.js +56 -0
  33. package/dist/cli/doctor-rows.d.ts +9 -0
  34. package/dist/cli/doctor-rows.js +9 -0
  35. package/dist/cli/drive-attach.d.ts +13 -0
  36. package/dist/cli/drive-attach.js +18 -0
  37. package/dist/cli/drive-command.js +7 -3
  38. package/dist/cli/mcp-command.js +51 -20
  39. package/dist/cli/project-name.d.ts +30 -0
  40. package/dist/cli/project-name.js +44 -0
  41. package/dist/cli/status-next-action.d.ts +14 -0
  42. package/dist/cli/status-next-action.js +4 -4
  43. package/dist/cli.d.ts +1 -0
  44. package/dist/cli.js +134 -80
  45. package/dist/cloud/cloud-config.d.ts +22 -1
  46. package/dist/cloud/cloud-config.js +64 -5
  47. package/dist/cloud/cloud-sync.d.ts +5 -5
  48. package/dist/cloud/cloud-sync.js +33 -2
  49. package/dist/cloud/sync-cycle.d.ts +110 -0
  50. package/dist/cloud/sync-cycle.js +270 -0
  51. package/dist/cloud/sync-daemon.d.ts +69 -0
  52. package/dist/cloud/sync-daemon.js +260 -0
  53. package/dist/cloud/sync-disk.d.ts +9 -0
  54. package/dist/cloud/sync-disk.js +191 -0
  55. package/dist/cloud/sync-hash.d.ts +2 -0
  56. package/dist/cloud/sync-hash.js +35 -0
  57. package/dist/crawl/crawl-tools.js +5 -3
  58. package/dist/crawl/crawl.d.ts +3 -0
  59. package/dist/crawl/crawl.js +5 -0
  60. package/dist/daemon/daemon-resolve.d.ts +43 -0
  61. package/dist/daemon/daemon-resolve.js +69 -0
  62. package/dist/daemon/dev-servers.d.ts +15 -0
  63. package/dist/daemon/dev-servers.js +68 -0
  64. package/dist/daemon/wake-decision.d.ts +32 -0
  65. package/dist/daemon/wake-decision.js +38 -0
  66. package/dist/domain/domain-tools.js +2 -2
  67. package/dist/events/contradictions.d.ts +18 -0
  68. package/dist/events/contradictions.js +134 -21
  69. package/dist/events/declared.d.ts +7 -4
  70. package/dist/events/declared.js +15 -6
  71. package/dist/events/echo-mismatch.js +64 -14
  72. package/dist/events/event-filters.d.ts +8 -0
  73. package/dist/events/event-filters.js +19 -2
  74. package/dist/events/predicate-eval.js +53 -5
  75. package/dist/events/predicate-precheck.d.ts +8 -0
  76. package/dist/events/predicate-precheck.js +41 -0
  77. package/dist/events/predicate-route.d.ts +14 -0
  78. package/dist/events/predicate-route.js +36 -8
  79. package/dist/events/predicate.d.ts +5 -0
  80. package/dist/events/predicate.js +37 -5
  81. package/dist/events/split-text-miss.d.ts +30 -0
  82. package/dist/events/split-text-miss.js +44 -0
  83. package/dist/flows/flake.d.ts +6 -6
  84. package/dist/flows/flow-memory-consult.d.ts +55 -0
  85. package/dist/flows/flow-memory-consult.js +38 -0
  86. package/dist/flows/flow-replay-run.d.ts +18 -7
  87. package/dist/flows/flow-replay-run.js +239 -22
  88. package/dist/flows/flow-replay.js +11 -2
  89. package/dist/flows/flow-step-runners.d.ts +8 -0
  90. package/dist/flows/flow-step-runners.js +8 -2
  91. package/dist/flows/flow-store-for-session.d.ts +31 -0
  92. package/dist/flows/flow-store-for-session.js +33 -0
  93. package/dist/flows/flow-tools.js +58 -32
  94. package/dist/flows/flows.d.ts +8 -0
  95. package/dist/flows/flows.js +468 -2
  96. package/dist/flows/heal-run.js +5 -2
  97. package/dist/flows/predicate-to-expect.d.ts +17 -12
  98. package/dist/flows/predicate-to-expect.js +30 -12
  99. package/dist/flows/recordings.d.ts +13 -1
  100. package/dist/flows/recordings.js +17 -2
  101. package/dist/flows/replay.d.ts +22 -2
  102. package/dist/flows/replay.js +28 -2
  103. package/dist/flows/verify-change-tools.js +5 -2
  104. package/dist/honesty/blind-spots.d.ts +55 -2
  105. package/dist/honesty/blind-spots.js +63 -3
  106. package/dist/honesty/honesty.d.ts +24 -0
  107. package/dist/honesty/honesty.js +25 -1
  108. package/dist/honesty/instrumentation-gaps.d.ts +9 -0
  109. package/dist/honesty/instrumentation-gaps.js +29 -2
  110. package/dist/honesty/verified.d.ts +2 -1
  111. package/dist/honesty/verified.js +16 -1
  112. package/dist/http-server.js +12 -0
  113. package/dist/impact/impact-recorder.d.ts +31 -8
  114. package/dist/impact/impact-recorder.js +104 -21
  115. package/dist/impact/impact-store.d.ts +15 -1
  116. package/dist/impact/impact-store.js +43 -2
  117. package/dist/index.js +113 -7
  118. package/dist/init/agent-rules.d.ts +13 -6
  119. package/dist/init/agent-rules.js +23 -12
  120. package/dist/init/astro-patch.d.ts +16 -0
  121. package/dist/init/astro-patch.js +28 -0
  122. package/dist/init/capabilities.d.ts +4 -1
  123. package/dist/init/capabilities.js +22 -2
  124. package/dist/init/confirm.d.ts +31 -7
  125. package/dist/init/confirm.js +48 -8
  126. package/dist/init/cra.d.ts +20 -4
  127. package/dist/init/cra.js +57 -17
  128. package/dist/init/csp-check.js +17 -2
  129. package/dist/init/csp-doctor.d.ts +8 -1
  130. package/dist/init/csp-doctor.js +8 -3
  131. package/dist/init/dev-script.d.ts +3 -1
  132. package/dist/init/dev-script.js +8 -5
  133. package/dist/init/format-generated.d.ts +18 -0
  134. package/dist/init/format-generated.js +39 -0
  135. package/dist/init/install-hint.d.ts +9 -0
  136. package/dist/init/install-hint.js +29 -0
  137. package/dist/init/mcp-clients.d.ts +7 -0
  138. package/dist/init/mcp-clients.js +7 -0
  139. package/dist/init/mcp.d.ts +1 -1
  140. package/dist/init/mcp.js +1 -1
  141. package/dist/init/node-io.js +11 -1
  142. package/dist/init/plan-framework.js +81 -45
  143. package/dist/init/plan.d.ts +20 -7
  144. package/dist/init/plan.js +20 -45
  145. package/dist/init/preflight.d.ts +28 -0
  146. package/dist/init/preflight.js +38 -0
  147. package/dist/init/run.d.ts +32 -9
  148. package/dist/init/run.js +141 -119
  149. package/dist/init/slash-command.d.ts +1 -1
  150. package/dist/init/snippets.js +41 -25
  151. package/dist/init/vite-config.d.ts +1 -1
  152. package/dist/init/vite-config.js +39 -25
  153. package/dist/init/workspace-apps.d.ts +17 -10
  154. package/dist/init/workspace-apps.js +110 -0
  155. package/dist/input/network-mock-tools.js +3 -6
  156. package/dist/input/real-input.js +25 -5
  157. package/dist/input/scroll-tools.js +4 -6
  158. package/dist/input/viewport-tools.js +5 -4
  159. package/dist/intent/inline-intent.d.ts +10 -19
  160. package/dist/intent/inline-intent.js +91 -1
  161. package/dist/intent/intent-shard-store.d.ts +72 -0
  162. package/dist/intent/intent-shard-store.js +203 -0
  163. package/dist/intent/intent-shard.d.ts +411 -0
  164. package/dist/intent/intent-shard.js +176 -0
  165. package/dist/intent/intent-store.d.ts +12 -0
  166. package/dist/intent/intent-store.js +21 -0
  167. package/dist/intent/intent-subject.d.ts +45 -0
  168. package/dist/intent/intent-subject.js +177 -0
  169. package/dist/intent/intent-tools.js +65 -2
  170. package/dist/intent/open-intents.d.ts +21 -0
  171. package/dist/intent/open-intents.js +15 -0
  172. package/dist/license/license-env.d.ts +29 -0
  173. package/dist/license/license-env.js +148 -0
  174. package/dist/loopback-agent.d.ts +31 -0
  175. package/dist/loopback-agent.js +57 -0
  176. package/dist/mcp/attach-memory.d.ts +18 -2
  177. package/dist/mcp/attach-memory.js +50 -7
  178. package/dist/mcp/client-identity.d.ts +31 -0
  179. package/dist/mcp/client-identity.js +51 -0
  180. package/dist/mcp/mcp-post-transport.d.ts +26 -0
  181. package/dist/mcp/mcp-post-transport.js +109 -0
  182. package/dist/mcp/mcp-proxy.d.ts +2 -1
  183. package/dist/mcp/mcp-proxy.js +52 -62
  184. package/dist/mcp/mcp.js +7 -1
  185. package/dist/mcp/proxy-backoff.d.ts +11 -0
  186. package/dist/mcp/proxy-backoff.js +13 -0
  187. package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
  188. package/dist/mcp/proxy-daemon-probe.js +7 -1
  189. package/dist/mcp/proxy-handshake.d.ts +23 -0
  190. package/dist/mcp/proxy-handshake.js +31 -0
  191. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  192. package/dist/mcp/tool-catalog-cache.js +12 -0
  193. package/dist/memory/memory-tools.d.ts +2 -0
  194. package/dist/memory/memory-tools.js +82 -0
  195. package/dist/memory/project-memory.d.ts +34 -0
  196. package/dist/memory/project-memory.js +96 -0
  197. package/dist/oracles/propose-consequences.js +12 -3
  198. package/dist/pool/browser-pool.d.ts +63 -4
  199. package/dist/pool/browser-pool.js +113 -4
  200. package/dist/pool/playwright-launcher.js +9 -0
  201. package/dist/project/project-tools.js +3 -3
  202. package/dist/project/reticle-dir.d.ts +15 -2
  203. package/dist/project/reticle-dir.js +21 -4
  204. package/dist/runs/run-store.d.ts +10 -0
  205. package/dist/runs/run-store.js +10 -0
  206. package/dist/runs/verification-sync.d.ts +0 -11
  207. package/dist/runs/verification-sync.js +22 -1
  208. package/dist/runs/verify-server.d.ts +8 -1
  209. package/dist/runs/verify-server.js +14 -2
  210. package/dist/session/command-timeout.d.ts +5 -2
  211. package/dist/session/command-timeout.js +5 -4
  212. package/dist/session/lease-visibility.d.ts +67 -0
  213. package/dist/session/lease-visibility.js +76 -0
  214. package/dist/session/no-session-diagnosis.d.ts +23 -0
  215. package/dist/session/no-session-diagnosis.js +58 -17
  216. package/dist/session/no-session-next-action.d.ts +7 -0
  217. package/dist/session/no-session-next-action.js +42 -3
  218. package/dist/session/no-session-watch.d.ts +10 -6
  219. package/dist/session/no-session-watch.js +51 -19
  220. package/dist/session/pending-commands.d.ts +11 -1
  221. package/dist/session/pending-commands.js +12 -2
  222. package/dist/session/resolve-within.d.ts +34 -0
  223. package/dist/session/resolve-within.js +53 -0
  224. package/dist/session/session-info.d.ts +18 -0
  225. package/dist/session/session-info.js +11 -0
  226. package/dist/session/session-manager.d.ts +14 -0
  227. package/dist/session/session-manager.js +61 -3
  228. package/dist/session/session-mirror.d.ts +16 -0
  229. package/dist/session/session-mirror.js +22 -0
  230. package/dist/session/session-recommendation.d.ts +19 -5
  231. package/dist/session/session-recommendation.js +25 -9
  232. package/dist/session/session-successor.js +36 -1
  233. package/dist/session/session-tools.js +2 -2
  234. package/dist/session/session.d.ts +22 -2
  235. package/dist/session/session.js +87 -7
  236. package/dist/session/wall-clock.d.ts +12 -0
  237. package/dist/session/wall-clock.js +14 -0
  238. package/dist/setup/agent-configs.d.ts +96 -0
  239. package/dist/setup/agent-configs.js +333 -0
  240. package/dist/setup/agent-io.d.ts +9 -0
  241. package/dist/setup/agent-io.js +16 -0
  242. package/dist/setup/agent-writer.d.ts +51 -0
  243. package/dist/setup/agent-writer.js +115 -0
  244. package/dist/setup/approval-migration.d.ts +47 -0
  245. package/dist/setup/approval-migration.js +68 -0
  246. package/dist/setup/auto-approve.d.ts +85 -0
  247. package/dist/setup/auto-approve.js +150 -0
  248. package/dist/setup/bridge-port.d.ts +15 -0
  249. package/dist/setup/bridge-port.js +24 -0
  250. package/dist/setup/desktop-shape.d.ts +57 -0
  251. package/dist/setup/desktop-shape.js +63 -0
  252. package/dist/setup/dev-server-wait.d.ts +60 -0
  253. package/dist/setup/dev-server-wait.js +65 -0
  254. package/dist/setup/drive-agent.d.ts +71 -0
  255. package/dist/setup/drive-agent.js +171 -0
  256. package/dist/setup/drive-plan.d.ts +68 -0
  257. package/dist/setup/drive-plan.js +130 -0
  258. package/dist/setup/ensure-daemon.d.ts +29 -0
  259. package/dist/setup/ensure-daemon.js +58 -0
  260. package/dist/setup/init-runtime.d.ts +47 -0
  261. package/dist/setup/init-runtime.js +160 -0
  262. package/dist/setup/interrupt.d.ts +25 -0
  263. package/dist/setup/interrupt.js +40 -0
  264. package/dist/setup/license-key.d.ts +39 -0
  265. package/dist/setup/license-key.js +72 -0
  266. package/dist/setup/listeners.d.ts +37 -0
  267. package/dist/setup/listeners.js +85 -0
  268. package/dist/setup/node-effects.d.ts +60 -0
  269. package/dist/setup/node-effects.js +210 -0
  270. package/dist/setup/page-probe.d.ts +39 -0
  271. package/dist/setup/page-probe.js +49 -0
  272. package/dist/setup/relaunch.d.ts +36 -0
  273. package/dist/setup/relaunch.js +57 -0
  274. package/dist/setup/remaining-steps.d.ts +19 -0
  275. package/dist/setup/remaining-steps.js +31 -0
  276. package/dist/setup/run-setup.d.ts +99 -0
  277. package/dist/setup/run-setup.js +187 -0
  278. package/dist/setup/session-pick.d.ts +47 -0
  279. package/dist/setup/session-pick.js +51 -0
  280. package/dist/setup/setup-command.d.ts +45 -0
  281. package/dist/setup/setup-command.js +199 -0
  282. package/dist/setup/setup-options.d.ts +74 -0
  283. package/dist/setup/setup-options.js +67 -0
  284. package/dist/setup/transcripts.d.ts +16 -0
  285. package/dist/setup/transcripts.js +43 -0
  286. package/dist/status-payload.d.ts +14 -1
  287. package/dist/status-payload.js +23 -1
  288. package/dist/telemetry/daemon-telemetry.d.ts +8 -1
  289. package/dist/telemetry/daemon-telemetry.js +52 -5
  290. package/dist/telemetry/error-class.js +1 -1
  291. package/dist/telemetry/feedback-context.d.ts +9 -1
  292. package/dist/telemetry/feedback-context.js +60 -29
  293. package/dist/telemetry/feedback-local.d.ts +15 -0
  294. package/dist/telemetry/feedback-local.js +56 -0
  295. package/dist/telemetry/feedback.d.ts +11 -0
  296. package/dist/telemetry/feedback.js +19 -6
  297. package/dist/telemetry/init-telemetry.d.ts +2 -0
  298. package/dist/telemetry/init-telemetry.js +2 -0
  299. package/dist/telemetry/install-source.d.ts +14 -0
  300. package/dist/telemetry/install-source.js +30 -0
  301. package/dist/telemetry/license-activation.d.ts +7 -1
  302. package/dist/telemetry/license-activation.js +9 -5
  303. package/dist/telemetry/mcp-connection.js +7 -0
  304. package/dist/telemetry/project-profile.d.ts +12 -1
  305. package/dist/telemetry/project-profile.js +11 -6
  306. package/dist/telemetry/proxy-telemetry.d.ts +3 -0
  307. package/dist/telemetry/proxy-telemetry.js +21 -0
  308. package/dist/telemetry/session-metrics.d.ts +12 -0
  309. package/dist/telemetry/session-metrics.js +35 -1
  310. package/dist/telemetry/telemetry.js +17 -2
  311. package/dist/telemetry/tool-refused.d.ts +3 -1
  312. package/dist/telemetry/tool-refused.js +34 -2
  313. package/dist/tools/act-capsule.d.ts +10 -0
  314. package/dist/tools/act-capsule.js +8 -2
  315. package/dist/tools/act-danger.d.ts +2 -1
  316. package/dist/tools/act-danger.js +11 -3
  317. package/dist/tools/act-observation.d.ts +41 -0
  318. package/dist/tools/act-observation.js +36 -0
  319. package/dist/tools/act-preflight.d.ts +20 -0
  320. package/dist/tools/act-preflight.js +98 -0
  321. package/dist/tools/act-sequence-retry.d.ts +45 -0
  322. package/dist/tools/act-sequence-retry.js +58 -0
  323. package/dist/tools/act-sequence-tool.d.ts +13 -0
  324. package/dist/tools/act-sequence-tool.js +135 -0
  325. package/dist/tools/act-target.d.ts +14 -0
  326. package/dist/tools/act-target.js +47 -0
  327. package/dist/tools/act-tools.d.ts +21 -1
  328. package/dist/tools/act-tools.js +100 -213
  329. package/dist/tools/assert-verdict.js +22 -16
  330. package/dist/tools/browser-tools.js +6 -2
  331. package/dist/tools/dynamic-tools.js +24 -5
  332. package/dist/tools/error-recovery.d.ts +1 -0
  333. package/dist/tools/error-recovery.js +8 -0
  334. package/dist/tools/invoke-tool.js +59 -9
  335. package/dist/tools/lease-tools.d.ts +30 -4
  336. package/dist/tools/lease-tools.js +169 -5
  337. package/dist/tools/merged-name-redirect.d.ts +13 -0
  338. package/dist/tools/merged-name-redirect.js +22 -0
  339. package/dist/tools/numeric-bounds.d.ts +100 -0
  340. package/dist/tools/numeric-bounds.js +125 -0
  341. package/dist/tools/observe-tools.js +47 -40
  342. package/dist/tools/read-tools.js +28 -7
  343. package/dist/tools/real-input-attempt.d.ts +10 -0
  344. package/dist/tools/real-input-attempt.js +59 -1
  345. package/dist/tools/reconcile-tools.js +4 -4
  346. package/dist/tools/tool-kit.d.ts +12 -0
  347. package/dist/tools/tool-kit.js +6 -1
  348. package/dist/tools/tool-names.d.ts +2 -0
  349. package/dist/tools/tool-names.js +2 -0
  350. package/dist/tools/tools-helpers.d.ts +19 -0
  351. package/dist/tools/tools-helpers.js +46 -0
  352. package/dist/tools/tools.d.ts +2 -0
  353. package/dist/tools/tools.js +56 -8
  354. package/dist/update/reticle-deps.d.ts +9 -0
  355. package/dist/update/reticle-deps.js +27 -0
  356. package/dist/update/sdk-sync.d.ts +1 -2
  357. package/dist/update/sdk-sync.js +1 -21
  358. package/dist/update/update-nudge.d.ts +18 -0
  359. package/dist/update/update-nudge.js +20 -0
  360. package/dist/version/peer-announce.js +4 -2
  361. package/dist/version/sdk-fix.d.ts +47 -0
  362. package/dist/version/sdk-fix.js +127 -0
  363. package/dist/version/version-nudge.d.ts +14 -0
  364. package/dist/version/version-nudge.js +21 -0
  365. package/dist/version/version-skew.d.ts +38 -3
  366. package/dist/version/version-skew.js +83 -7
  367. package/dist/visual/visual-store.d.ts +6 -6
  368. package/dist/visual/visual-store.js +18 -16
  369. package/dist/visual/visual-tools.js +57 -12
  370. package/docs/cli/doctor.mdx +1 -0
  371. package/docs/cli/serve.mdx +4 -2
  372. package/docs/cli.mdx +7 -3
  373. package/docs/desktop.mdx +1 -1
  374. package/docs/docs.json +1 -1
  375. package/docs/getting-started.md +19 -3
  376. package/docs/index.mdx +9 -6
  377. package/docs/install-agentic.mdx +38 -12
  378. package/docs/install-manual.mdx +1 -1
  379. package/docs/packages.mdx +1 -1
  380. package/docs/skill-file.mdx +3 -3
  381. package/docs/telemetry-contract.md +108 -4
  382. package/docs/tools-act-sequence.mdx +16 -0
  383. package/docs/usage.md +1 -1
  384. package/package.json +4 -3
  385. package/dist/init/cursor.d.ts +0 -24
  386. package/dist/init/cursor.js +0 -80
@@ -54,3 +54,17 @@ export declare function declaredInstallSource(env?: NodeJS.ProcessEnv): InstallS
54
54
  * data, and a hand-edited one must not be able to put an arbitrary string on the wire.
55
55
  */
56
56
  export declare function projectInstallSource(cwd: string, env?: NodeJS.ProcessEnv): InstallSource;
57
+ /**
58
+ * The same config with `installSource` added, or `undefined` when there is nothing to add.
59
+ *
60
+ * `installSource` is written only when `.reticle.json` is CREATED, and `init` reports that file as
61
+ * `already exists` on every re-run — so everyone who was already here when the marker shipped, and
62
+ * everyone who ran `init` once without one, keeps `unknown` forever with no way to fix it. That is
63
+ * most of the population the field exists to describe.
64
+ *
65
+ * An EXISTING value is never overwritten. The first channel is the one that brought the user in, and
66
+ * a later `init` through a different route must not take credit for an acquisition it did not make.
67
+ * Anything unparseable is left exactly as it is: a config is user data, and `init` rewriting a file
68
+ * it could not read would be a far worse failure than a missing dimension.
69
+ */
70
+ export declare function configWithInstallSource(source: string | null | undefined, declared: string | undefined): string | undefined;
@@ -72,3 +72,33 @@ export function projectInstallSource(cwd, env = process.env) {
72
72
  }
73
73
  return resolveInstallSource(env);
74
74
  }
75
+ /**
76
+ * The same config with `installSource` added, or `undefined` when there is nothing to add.
77
+ *
78
+ * `installSource` is written only when `.reticle.json` is CREATED, and `init` reports that file as
79
+ * `already exists` on every re-run — so everyone who was already here when the marker shipped, and
80
+ * everyone who ran `init` once without one, keeps `unknown` forever with no way to fix it. That is
81
+ * most of the population the field exists to describe.
82
+ *
83
+ * An EXISTING value is never overwritten. The first channel is the one that brought the user in, and
84
+ * a later `init` through a different route must not take credit for an acquisition it did not make.
85
+ * Anything unparseable is left exactly as it is: a config is user data, and `init` rewriting a file
86
+ * it could not read would be a far worse failure than a missing dimension.
87
+ */
88
+ export function configWithInstallSource(source, declared) {
89
+ if (undefined === declared || null === source || undefined === source)
90
+ return undefined;
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(source);
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ if (null === parsed || 'object' !== typeof parsed || Array.isArray(parsed))
99
+ return undefined;
100
+ const fields = parsed;
101
+ if (undefined !== fields['installSource'])
102
+ return undefined;
103
+ return `${JSON.stringify({ ...fields, installSource: declared }, null, 2)}\n`;
104
+ }
@@ -30,6 +30,12 @@ import { LicenseActivation } from '@reticlehq/core';
30
30
  export interface LicenseFacts {
31
31
  licenseId?: string;
32
32
  licenseStatus?: LicenseActivation;
33
+ /**
34
+ * A key was placed, whether or not this build could judge it. See LICENSE_KEY_PRESENT in core.
35
+ * Reported even in evaluation mode, which is the whole point: that is the case where every other
36
+ * licence property is deliberately silent.
37
+ */
38
+ licenseKeyPresent?: boolean;
33
39
  }
34
40
  /**
35
41
  * Resolve activation for one event. The clock is passed in (rule 7) and is the EVENT's clock, not a
@@ -45,4 +51,4 @@ export interface LicenseFacts {
45
51
  * Never throws. A telemetry property may not change behaviour (rule 5), and this one sits in front of
46
52
  * key parsing, which is the part most likely to be handed something malformed.
47
53
  */
48
- export declare function licenseFacts(now: number, env?: NodeJS.ProcessEnv): LicenseFacts;
54
+ export declare function licenseFacts(now: number, env?: NodeJS.ProcessEnv, baked?: string): LicenseFacts;
@@ -26,7 +26,7 @@
26
26
  * holds a customer list.
27
27
  */
28
28
  import { LicenseActivation } from '@reticlehq/core';
29
- import { describeLicense } from '../license/license.js';
29
+ import { describeLicense, LICENSE_KEY_ENV } from '../license/license.js';
30
30
  /**
31
31
  * Resolve activation for one event. The clock is passed in (rule 7) and is the EVENT's clock, not a
32
32
  * value captured at startup: sessions here run to eleven hours, so a key that expires mid-session must
@@ -41,13 +41,17 @@ import { describeLicense } from '../license/license.js';
41
41
  * Never throws. A telemetry property may not change behaviour (rule 5), and this one sits in front of
42
42
  * key parsing, which is the part most likely to be handed something malformed.
43
43
  */
44
- export function licenseFacts(now, env = process.env) {
44
+ export function licenseFacts(now, env = process.env, baked) {
45
45
  try {
46
- const report = describeLicense(now, env);
47
- if (LicenseActivation.EVAL === report.status)
48
- return {};
46
+ const report = baked === undefined ? describeLicense(now, env) : describeLicense(now, env, baked);
47
+ // Asked BEFORE the eval short-circuit, because eval is exactly the case that used to be silent.
48
+ const keyPresent = (env[LICENSE_KEY_ENV] ?? '').length > 0;
49
+ if (LicenseActivation.EVAL === report.status) {
50
+ return keyPresent ? { licenseKeyPresent: true } : {};
51
+ }
49
52
  return {
50
53
  licenseStatus: report.status,
54
+ ...(keyPresent ? { licenseKeyPresent: true } : {}),
51
55
  ...(report.licenseId !== undefined ? { licenseId: report.licenseId } : {}),
52
56
  };
53
57
  }
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { TelemetryEventKind } from '@reticlehq/core';
13
13
  import { getTelemetry } from './telemetry.js';
14
+ import { appEverConnected } from './app-instrumented.js';
14
15
  let connectsSeen = 0;
15
16
  /** Set when the daemon starts, so the FIRST connect can report how long Reticle sat unused. */
16
17
  let daemonStartedAt;
@@ -30,6 +31,12 @@ export function reportMcpConnected(client, now = () => Date.now()) {
30
31
  // sat there. On a reconnect it is just how long the daemon has been up.
31
32
  daemonAgeMs: daemonStartedAt === undefined ? 0 : Math.max(0, now() - daemonStartedAt),
32
33
  ...(client !== undefined ? { client } : {}),
34
+ // WHAT THE AGENT HAD TO LOOK AT. Most clients that attach never call a tool, and that cohort
35
+ // emits nothing at all — an agent that reads the instructions, learns nothing is wired and
36
+ // stops has refused nothing, so `tool_refused` cannot see it either. Read off the same flag
37
+ // `app_instrumented` sets, never a second counter, so the two halves of the funnel cannot
38
+ // disagree about one daemon run.
39
+ appConnected: appEverConnected(),
33
40
  },
34
41
  });
35
42
  }
@@ -32,5 +32,16 @@ type FileReader = (path: string) => string;
32
32
  * Build the full profile. Best-effort throughout: an unreadable project yields a profile of zeroes
33
33
  * rather than an exception, because a telemetry snapshot must never be able to fail a daemon start.
34
34
  */
35
- export declare function profileProject(cwd: string, now: number): ProjectProfile;
35
+ /**
36
+ * Facts about the install that the filesystem cannot answer.
37
+ *
38
+ * Passed in rather than read here for the same reason `previouslyConnected` is passed to the MCP
39
+ * server: it needs the daemon's port and the state home, and a profile that reached for those would
40
+ * stop being a pure function of a directory.
41
+ */
42
+ export interface InstallFacts {
43
+ /** Has an app for this project EVER connected to Reticle, from durable state. */
44
+ appConnectedBefore: boolean;
45
+ }
46
+ export declare function profileProject(cwd: string, now: number, install?: InstallFacts): ProjectProfile;
36
47
  export {};
@@ -15,6 +15,7 @@ import { join } from 'node:path';
15
15
  import { ProjectSize, ReticleDir } from '@reticlehq/core';
16
16
  import { gitFacts } from './git-facts.js';
17
17
  import { detectStack } from './feedback-context.js';
18
+ import { readProjectId } from '../cli/cli-port.js';
18
19
  /**
19
20
  * The feature FAMILIES, and the on-disk evidence that a project has adopted each. Named rather than
20
21
  * counted so the vocabulary stays stable as tools are added, renamed, or merged into action-dispatched
@@ -151,11 +152,7 @@ function exists(path) {
151
152
  return false;
152
153
  }
153
154
  }
154
- /**
155
- * Build the full profile. Best-effort throughout: an unreadable project yields a profile of zeroes
156
- * rather than an exception, because a telemetry snapshot must never be able to fail a daemon start.
157
- */
158
- export function profileProject(cwd, now) {
155
+ export function profileProject(cwd, now, install) {
159
156
  const reticleRoot = join(cwd, ReticleDir.ROOT);
160
157
  const flowCount = countIn(reticleRoot, ReticleDir.FLOWS_SUBDIR, /\.json$/);
161
158
  const baselineCount = countIn(reticleRoot, ReticleDir.BASELINES_SUBDIR);
@@ -180,7 +177,7 @@ export function profileProject(cwd, now) {
180
177
  featuresUsed.push(FeatureFamily.BUG_CAPSULES);
181
178
  if (hasHistory)
182
179
  featuresUsed.push(FeatureFamily.CROSS_RUN_HISTORY);
183
- const { stack, stackMajor, stackSource } = detectStack(cwd);
180
+ const { stack, stackMajor, stackSource, stackUnknownReason } = detectStack(cwd);
184
181
  const ageWeeks = projectAgeWeeks(cwd, now);
185
182
  const git = gitFacts(cwd);
186
183
  return {
@@ -189,6 +186,8 @@ export function profileProject(cwd, now) {
189
186
  ...(stack !== undefined ? { stack } : {}),
190
187
  ...(stackSource !== undefined ? { stackSource } : {}),
191
188
  ...(stackMajor !== undefined ? { stackMajor } : {}),
189
+ // Present only when `stack` is absent, so the field's presence marks the unknown bucket.
190
+ ...(stackUnknownReason !== undefined ? { stackUnknownReason } : {}),
192
191
  size: sizeBucket(countSourceFiles(cwd)),
193
192
  monorepo: isMonorepo(cwd),
194
193
  ...(ageWeeks !== undefined ? { ageWeeks } : {}),
@@ -202,5 +201,11 @@ export function profileProject(cwd, now) {
202
201
  // Rounded to 2dp: the exact ratio is a function of the counts above, and an unrounded float would
203
202
  // just be a higher-cardinality restatement of them.
204
203
  featureDepth: Math.round((featuresUsed.length / ALL_FAMILIES.length) * 100) / 100,
204
+ // WHERE the install stopped, for the daemons that never see an app. `daemon_started` minus
205
+ // `app_instrumented` says one happened and cannot say why; these two bits split that silence
206
+ // into "never ran init", "ran it and no page has ever reached us", and "works, just not up
207
+ // right now". See the fields on ProjectProfileSchema.
208
+ initialized: readProjectId(cwd) !== undefined,
209
+ ...(install === undefined ? {} : { appConnectedBefore: install.appConnectedBefore }),
205
210
  };
206
211
  }
@@ -0,0 +1,3 @@
1
+ import { type SessionMetrics } from './session-metrics.js';
2
+ import { type Telemetry } from './telemetry.js';
3
+ export declare function flushProxySessionMetrics(telemetry?: Telemetry, metrics?: SessionMetrics): Promise<void>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The MCP proxy's session rollup.
3
+ *
4
+ * POST socket failures (`ENOBUFS`, `EMFILE`, `ECONNREFUSED` before any bytes were sent) happen in
5
+ * this process, not the daemon's. `tool_refused` never fires (the handler never ran) and
6
+ * `mcp_connection_lost` never fires (the SSE stream is fine). Counts live on SessionMetrics here
7
+ * and leave as one `session_progress` — an existing kind, omitted when nothing happened.
8
+ *
9
+ * The send is awaited. Fire-and-forget microseconds before `process.exit` is how `daemon_stopped`
10
+ * never arrived: the POST was killed every time and nothing threw.
11
+ */
12
+ import { TelemetryEventKind } from '@reticlehq/core';
13
+ import { getSessionMetrics } from './session-metrics.js';
14
+ import { getTelemetry } from './telemetry.js';
15
+ export async function flushProxySessionMetrics(telemetry = getTelemetry(), metrics = getSessionMetrics()) {
16
+ if (metrics.empty)
17
+ return;
18
+ await telemetry.emit(TelemetryEventKind.SESSION_PROGRESS, {
19
+ session: metrics.summarize(true),
20
+ });
21
+ }
@@ -65,6 +65,18 @@ export declare class SessionMetrics {
65
65
  * one inside a much larger pile. Same treatment otherwise — fingerprinted, variables stripped.
66
66
  */
67
67
  recordSdkFailure(site: string, message: string): void;
68
+ /**
69
+ * The MCP proxy could not POST a tool call because the TCP socket itself failed (`ENOBUFS`,
70
+ * `EMFILE`, `ECONNREFUSED` before any bytes left). The SSE stream is still up, so this is
71
+ * invisible to `mcp_connection_lost`, and the handler never ran, so it is invisible to
72
+ * `tool_refused`. Counted so a dashboard can see it without a new event kind.
73
+ */
74
+ recordPostSocketFailure(): void;
75
+ /**
76
+ * A bounded retry of an unsent POST then delivered. The pair against `recordPostSocketFailure`:
77
+ * the retry that quietly saved a call is a different fact from the one that never ran.
78
+ */
79
+ recordPostRetrySaved(): void;
68
80
  /** The agent's recent approach run + the call in flight — context for a crash report. */
69
81
  get trail(): {
70
82
  breadcrumb: string[];
@@ -123,6 +123,12 @@ export class SessionMetrics {
123
123
  * call a tool, and of the sessions that made exactly one call, most bounced on this.
124
124
  */
125
125
  #noSessionErrors = 0;
126
+ /** Connection-level MCP POST failures the SSE stream cannot see. See SessionSummary. */
127
+ #postSocketFailures = 0;
128
+ #lifetimePostSocketFailures = 0;
129
+ /** Retries of those POSTs that then delivered. */
130
+ #postRetriesSaved = 0;
131
+ #lifetimePostRetriesSaved = 0;
126
132
  /** Longest back-to-back run per tool — a retry loop, which `toolCounts` cannot distinguish. */
127
133
  #repeatRuns = new Map();
128
134
  #lastTool;
@@ -325,6 +331,24 @@ export class SessionMetrics {
325
331
  tool: site,
326
332
  });
327
333
  }
334
+ /**
335
+ * The MCP proxy could not POST a tool call because the TCP socket itself failed (`ENOBUFS`,
336
+ * `EMFILE`, `ECONNREFUSED` before any bytes left). The SSE stream is still up, so this is
337
+ * invisible to `mcp_connection_lost`, and the handler never ran, so it is invisible to
338
+ * `tool_refused`. Counted so a dashboard can see it without a new event kind.
339
+ */
340
+ recordPostSocketFailure() {
341
+ this.#postSocketFailures += 1;
342
+ this.#lifetimePostSocketFailures += 1;
343
+ }
344
+ /**
345
+ * A bounded retry of an unsent POST then delivered. The pair against `recordPostSocketFailure`:
346
+ * the retry that quietly saved a call is a different fact from the one that never ran.
347
+ */
348
+ recordPostRetrySaved() {
349
+ this.#postRetriesSaved += 1;
350
+ this.#lifetimePostRetriesSaved += 1;
351
+ }
328
352
  /** The agent's recent approach run + the call in flight — context for a crash report. */
329
353
  get trail() {
330
354
  return { breadcrumb: [...this.#breadcrumb], inFlight: this.#inFlight };
@@ -469,12 +493,16 @@ export class SessionMetrics {
469
493
  toolErrors: this.#lifetimeToolErrors,
470
494
  verifications: this.#lifetimeVerifications,
471
495
  bugsFound: this.#lifetimeBugsFound,
496
+ postSocketFailures: this.#lifetimePostSocketFailures,
497
+ postRetriesSaved: this.#lifetimePostRetriesSaved,
472
498
  }
473
499
  : {
474
500
  toolCalls: this.#toolCalls,
475
501
  toolErrors: this.#toolErrors,
476
502
  verifications: this.#verifications,
477
503
  bugsFound: this.#bugsFound,
504
+ postSocketFailures: this.#postSocketFailures,
505
+ postRetriesSaved: this.#postRetriesSaved,
478
506
  };
479
507
  return {
480
508
  durationMs: Math.max(0, this.#now() - this.#startedAt),
@@ -488,6 +516,8 @@ export class SessionMetrics {
488
516
  bugsFound: scalars.bugsFound,
489
517
  ...(this.#bugKinds.size > 0 ? { bugKinds: Object.fromEntries(this.#bugKinds) } : {}),
490
518
  ...(this.#noSessionErrors > 0 ? { noSessionErrors: this.#noSessionErrors } : {}),
519
+ ...(scalars.postSocketFailures > 0 ? { postSocketFailures: scalars.postSocketFailures } : {}),
520
+ ...(scalars.postRetriesSaved > 0 ? { postRetriesSaved: scalars.postRetriesSaved } : {}),
491
521
  ...(this.#repeatRuns.size > 0
492
522
  ? { consecutiveRepeats: Object.fromEntries(this.#repeatRuns) }
493
523
  : {}),
@@ -588,7 +618,9 @@ export class SessionMetrics {
588
618
  0 === this.#verifications &&
589
619
  0 === this.#toolErrors &&
590
620
  0 === this.#bugsFound &&
591
- 0 === this.#sdkFailures);
621
+ 0 === this.#sdkFailures &&
622
+ 0 === this.#postSocketFailures &&
623
+ 0 === this.#postRetriesSaved);
592
624
  }
593
625
  /** Zero the counters after a non-final flush so the next flush reports the NEXT window, not a total. */
594
626
  reset() {
@@ -596,6 +628,8 @@ export class SessionMetrics {
596
628
  this.#toolErrors = 0;
597
629
  this.#verifications = 0;
598
630
  this.#noSessionErrors = 0;
631
+ this.#postSocketFailures = 0;
632
+ this.#postRetriesSaved = 0;
599
633
  this.#repeatRuns.clear();
600
634
  this.#lastTool = undefined;
601
635
  this.#currentRun = 0;
@@ -9,7 +9,7 @@
9
9
  * - it is best-effort and non-blocking: a send failure NEVER changes what the tool does.
10
10
  *
11
11
  * Events are `@reticlehq/core`'s `TelemetryEventSchema`, mapped at the wire into PostHog's capture
12
- * format (https://posthog.com/docs/api/capture) — PostHog is the analytics backend until Reticle Cloud
12
+ * format (https://posthog.com/docs/api/capture) — PostHog is the analytics backend until the hosted service
13
13
  * grows its own; a project API key is WRITE-ONLY by design, so embedding it in an OSS client is safe.
14
14
  * Everything here is wrapped so a telemetry bug can never surface to a user — a broken metric must not
15
15
  * break a verification.
@@ -76,8 +76,23 @@ export const FEEDBACK_RETRY_BACKOFF_MS = 750;
76
76
  * (`reticle version`/`gate`) ~800ms. Long-lived daemon events still send in-process: a spawn per tool
77
77
  * call would be far heavier than a fetch inside an already-running server.
78
78
  */
79
+ /**
80
+ * The budget the DISOWNED child gets, and why it is not `SEND_TIMEOUT_MS`.
81
+ *
82
+ * 2s is right for an in-process send: the daemon is waiting on it, so the budget is a latency cost
83
+ * somebody pays. A detached child pays nobody — the parent has already exited — so the only thing a
84
+ * short budget there buys is a lost event.
85
+ *
86
+ * And it loses the worst ones. Every detached send comes from a SHORT-LIVED CLI process, which pays
87
+ * cold DNS and a cold TLS route by definition; measured on the feedback path with a WARM cache it
88
+ * was already 0.694s before any payload moved. `reticle_installed` is the extreme case: it fires
89
+ * once per machine, on the first Reticle command that machine ever runs, which is the single coldest
90
+ * network call it will ever make to the collector — so the top of the funnel is the event most
91
+ * likely to be dropped, and it fails silently like everything else here.
92
+ */
93
+ const DETACHED_SEND_TIMEOUT_MS = 15_000;
79
94
  const DETACHED_SEND_SCRIPT = "fetch(process.argv[1],{method:'POST',headers:{'content-type':'application/json'}," +
80
- `body:process.argv[2],signal:AbortSignal.timeout(${SEND_TIMEOUT_MS})})` +
95
+ `body:process.argv[2],signal:AbortSignal.timeout(${DETACHED_SEND_TIMEOUT_MS})})` +
81
96
  '.catch(()=>{}).finally(()=>process.exit(0))';
82
97
  /** Env var names — mirror cloud-sync's `RETICLE_*` convention. */
83
98
  import { isReticleSourceCheckout } from './dev-repo.js';
@@ -10,7 +10,9 @@
10
10
  * Emitted from `runTool`, which is the one place both dispatch paths cross, so a tool added later is
11
11
  * covered without anybody remembering to cover it.
12
12
  */
13
- import { type RefusalReason } from '@reticlehq/core';
13
+ import { RefusalReason, type NoSessionReason } from '@reticlehq/core';
14
+ /** Record the branch code for the no-session error being thrown right now. */
15
+ export declare function notePendingNoSessionReason(reason: NoSessionReason | undefined): void;
14
16
  /** A call that was served. Breaks the retry chain: what follows it is not a retry of anything. */
15
17
  export declare function noteToolServed(): void;
16
18
  /** One refused call, reported with its cause and whether it was itself a retry. */
@@ -10,7 +10,7 @@
10
10
  * Emitted from `runTool`, which is the one place both dispatch paths cross, so a tool added later is
11
11
  * covered without anybody remembering to cover it.
12
12
  */
13
- import { TelemetryActor, TelemetryEventKind } from '@reticlehq/core';
13
+ import { RefusalReason, TelemetryActor, TelemetryEventKind, } from '@reticlehq/core';
14
14
  import { getTelemetry } from './telemetry.js';
15
15
  /**
16
16
  * Refusal events one daemon run will send.
@@ -32,6 +32,24 @@ let sent = 0;
32
32
  * describe — so the flag lands on the RETRY instead of on the first refusal.
33
33
  */
34
34
  let lastRefusedTool;
35
+ /**
36
+ * The no-session branch code for the refusal about to be reported, set by whoever threw it.
37
+ *
38
+ * `no_session` is the largest refusal cohort and on its own it is a set difference: nothing
39
+ * connected, and no word on which of several opposite situations that was. The diagnosis that CAN
40
+ * say is computed where the error is thrown, and the refusal is classified downstream from the
41
+ * message, so the two never met (#615).
42
+ *
43
+ * Module-level and consumed once, like `lastRefusedTool` above and for the same reason: the
44
+ * producer and the consumer are one hop apart on the same call, and threading a field through every
45
+ * refusal path to carry it would touch a dozen call sites that have nothing to do with sessions.
46
+ * Cleared on read so a later refusal cannot inherit a stale reason.
47
+ */
48
+ let pendingNoSessionReason;
49
+ /** Record the branch code for the no-session error being thrown right now. */
50
+ export function notePendingNoSessionReason(reason) {
51
+ pendingNoSessionReason = reason;
52
+ }
35
53
  /** A call that was served. Breaks the retry chain: what follows it is not a retry of anything. */
36
54
  export function noteToolServed() {
37
55
  lastRefusedTool = undefined;
@@ -40,16 +58,30 @@ export function noteToolServed() {
40
58
  export function reportToolRefused(tool, reason) {
41
59
  const retried = lastRefusedTool === tool;
42
60
  lastRefusedTool = tool;
61
+ // Read and clear whatever the throw left, whether or not this refusal can use it: a reason left
62
+ // sitting would attach itself to an unrelated refusal later in the session.
63
+ const noSessionReason = pendingNoSessionReason;
64
+ pendingNoSessionReason = undefined;
43
65
  if (sent >= MAX_REFUSALS_PER_SESSION)
44
66
  return;
45
67
  sent += 1;
46
68
  void getTelemetry().emit(TelemetryEventKind.TOOL_REFUSED, {
47
69
  actor: TelemetryActor.AGENT,
48
- refusal: { tool, reason, retried },
70
+ refusal: {
71
+ tool,
72
+ reason,
73
+ retried,
74
+ // Only where it means something. On any other refusal reason this field would be answering
75
+ // a question nobody asked of it.
76
+ ...(RefusalReason.NO_SESSION === reason && noSessionReason !== undefined
77
+ ? { noSessionReason }
78
+ : {}),
79
+ },
49
80
  });
50
81
  }
51
82
  /** Tests only — the counters are process-lifetime, so each case has to start from zero. */
52
83
  export function resetToolRefusals() {
53
84
  sent = 0;
54
85
  lastRefusedTool = undefined;
86
+ pendingNoSessionReason = undefined;
55
87
  }
@@ -17,6 +17,16 @@ interface CapsuleSaveInputs {
17
17
  file: string;
18
18
  line: number;
19
19
  };
20
+ /**
21
+ * The `.reticle` this capsule belongs in, from the session the act ACTUALLY drove.
22
+ *
23
+ * Passed in rather than re-resolved here, because re-resolving loses twice: `args.sessionId` is
24
+ * frequently absent, and `sessions.resolve(undefined)` throws whenever more than one tab is
25
+ * connected — which is precisely the multi-project case this routing exists for, so it failed in
26
+ * exactly the situation it was written to fix. The caller also knows about a session the act
27
+ * FOLLOWED through a navigation; this file cannot.
28
+ */
29
+ root?: string | undefined;
20
30
  }
21
31
  /** Returns the capsule id when one was written, or undefined when there was nothing to save. */
22
32
  export declare function saveFailedAssertCapsule(inputs: CapsuleSaveInputs): Promise<string | undefined>;
@@ -13,16 +13,22 @@ import { ActionType, AnchorKind } from '@reticlehq/core';
13
13
  import { CapsuleStore, capsuleId, CAPSULE_VERSION } from '../capsule/capsule-store.js';
14
14
  import { ReticleTool } from './tool-names.js';
15
15
  import { asRecord, asString } from './tools-helpers.js';
16
+ import { sessionRoot } from '../project/session-root.js';
16
17
  /** Returns the capsule id when one was written, or undefined when there was nothing to save. */
17
18
  export async function saveFailedAssertCapsule(inputs) {
18
- const { deps, verdict, capsule, links, args, actResult, actedSource } = inputs;
19
+ const { deps, verdict, capsule, links, args, actResult, actedSource, root: given } = inputs;
19
20
  if (verdict.pass || capsule === undefined)
20
21
  return undefined;
21
22
  const id = capsuleId(deps.now(), asString(args['ref']) ?? 'assert');
22
23
  const expectedText = links
23
24
  .map((l) => ('name' in l ? `${l.kind} ${String(l.name)}` : l.kind))
24
25
  .join(' AND ');
25
- const saved = await new CapsuleStore(deps.fs, deps.reticleRoot).save({
26
+ // The capsule belongs to the project whose app just failed, not to wherever the daemon stands.
27
+ // Same rule and same resolver as every other artifact: a capsule written into a sibling repo is
28
+ // evidence filed against a codebase that did not produce it, and it outlives the turn.
29
+ // The caller's answer wins; the resolver is the fallback for callers that have no session.
30
+ const root = given ?? sessionRoot(deps, asString(args['sessionId']));
31
+ const saved = await new CapsuleStore(deps.fs, root).save({
26
32
  version: CAPSULE_VERSION,
27
33
  id,
28
34
  createdAt: deps.now(),
@@ -2,7 +2,8 @@ import { ActionType } from '@reticlehq/core';
2
2
  export declare function assertNotDestructive(action: ActionType, innerArgs: Record<string, unknown>, inspected: unknown): void;
3
3
  /**
4
4
  * A drag is judged on BOTH ends. Dropping a harmless row onto "Delete" is destructive, and the
5
- * source alone never says so.
5
+ * source alone never says so. Each end is classified on its own text and role, so a Payment
6
+ * option dragged onto Save is not a payment.
6
7
  */
7
8
  export declare function assertDragNotDestructive(innerArgs: Record<string, unknown>, from: unknown, to: unknown): void;
8
9
  /**
@@ -22,23 +22,31 @@ function descriptorText(value) {
22
22
  asString(descriptor['formText']) ?? '',
23
23
  ].join(' ');
24
24
  }
25
+ function descriptorRole(value) {
26
+ const role = asString(asRecord(value)['role']);
27
+ return role !== undefined && role.length > 0 ? role : undefined;
28
+ }
29
+ function isDestructiveDescriptor(value) {
30
+ return isDangerousActionText(descriptorText(value), descriptorRole(value));
31
+ }
25
32
  export function assertNotDestructive(action, innerArgs, inspected) {
26
33
  if (action !== ActionType.CLICK && action !== ActionType.DBLCLICK)
27
34
  return;
28
35
  if (true === innerArgs[DANGEROUS_ACTION_CONFIRM_ARG])
29
36
  return;
30
- if (!isDangerousActionText(descriptorText(inspected)))
37
+ if (!isDestructiveDescriptor(inspected))
31
38
  return;
32
39
  throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
33
40
  }
34
41
  /**
35
42
  * A drag is judged on BOTH ends. Dropping a harmless row onto "Delete" is destructive, and the
36
- * source alone never says so.
43
+ * source alone never says so. Each end is classified on its own text and role, so a Payment
44
+ * option dragged onto Save is not a payment.
37
45
  */
38
46
  export function assertDragNotDestructive(innerArgs, from, to) {
39
47
  if (true === innerArgs[DANGEROUS_ACTION_CONFIRM_ARG])
40
48
  return;
41
- if (!isDangerousActionText(`${descriptorText(from)} ${descriptorText(to)}`))
49
+ if (!isDestructiveDescriptor(from) && !isDestructiveDescriptor(to))
42
50
  return;
43
51
  throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
44
52
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * What to do when the observer left in the middle of an act.
3
+ *
4
+ * A full-document navigation — often the very thing the act CAUSED — tears the SDK down. The page
5
+ * re-announces, and whatever this call still had in flight is gone. That is a fact about the
6
+ * observer, never about the application, and the difference matters: every honest verdict this
7
+ * engine can give depends on not converting "I stopped watching" into "your app is broken".
8
+ *
9
+ * Extracted from act-tools.ts, which had reached the file-size cap. It earns its own module because
10
+ * it is one idea — follow the document that took over, and re-ask there — used from the two points
11
+ * where observation can be lost: a write rejected at dispatch, and a wait cut off mid-predicate.
12
+ */
13
+ import type { Session } from '../session/session.js';
14
+ import { type SuccessorRegistry } from '../session/session-successor.js';
15
+ import type { EvalResult } from '../events/predicate-eval.js';
16
+ export interface FollowSuccessor {
17
+ sessions: SuccessorRegistry;
18
+ session: Session;
19
+ verdict: EvalResult;
20
+ /** Total budget the CALLER granted, in ms. */
21
+ timeout: number;
22
+ /** `session.elapsed()` at the moment the predicate started. */
23
+ predicateStarted: number;
24
+ /** Re-ask the declared consequence on `next`, with whatever budget is left. */
25
+ reevaluate: (next: Session, budgetMs: number) => Promise<EvalResult>;
26
+ }
27
+ export interface FollowedOutcome {
28
+ session: Session;
29
+ verdict: EvalResult;
30
+ /** True when the wait moved to a new document, so the caller must reset its event cursor. */
31
+ followed: boolean;
32
+ }
33
+ /**
34
+ * Follow the unique same-origin successor and re-ask there, or leave the verdict untouched.
35
+ *
36
+ * `navigate` already waits for the HELLO and returns the new id; this path used to grade
37
+ * `observation_lost` and leave the agent holding a DEAD id, so the next assert failed even though
38
+ * the new page had loaded perfectly. Two live tabs at that origin is still a guess, and
39
+ * `awaitDocumentSuccessor` refuses to guess — a wrong follow drives somebody else's app.
40
+ */
41
+ export declare function followLostObservation(input: FollowSuccessor): Promise<FollowedOutcome>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What to do when the observer left in the middle of an act.
3
+ *
4
+ * A full-document navigation — often the very thing the act CAUSED — tears the SDK down. The page
5
+ * re-announces, and whatever this call still had in flight is gone. That is a fact about the
6
+ * observer, never about the application, and the difference matters: every honest verdict this
7
+ * engine can give depends on not converting "I stopped watching" into "your app is broken".
8
+ *
9
+ * Extracted from act-tools.ts, which had reached the file-size cap. It earns its own module because
10
+ * it is one idea — follow the document that took over, and re-ask there — used from the two points
11
+ * where observation can be lost: a write rejected at dispatch, and a wait cut off mid-predicate.
12
+ */
13
+ import { awaitDocumentSuccessor } from '../session/session-successor.js';
14
+ /**
15
+ * Follow the unique same-origin successor and re-ask there, or leave the verdict untouched.
16
+ *
17
+ * `navigate` already waits for the HELLO and returns the new id; this path used to grade
18
+ * `observation_lost` and leave the agent holding a DEAD id, so the next assert failed even though
19
+ * the new page had loaded perfectly. Two live tabs at that origin is still a guess, and
20
+ * `awaitDocumentSuccessor` refuses to guess — a wrong follow drives somebody else's app.
21
+ */
22
+ export async function followLostObservation(input) {
23
+ const { sessions, session, verdict, timeout, predicateStarted, reevaluate } = input;
24
+ if (true !== verdict.observationLost || 0 >= timeout)
25
+ return { session, verdict, followed: false };
26
+ const remaining = timeout - (session.elapsed() - predicateStarted);
27
+ const next = remaining > 0 ? await awaitDocumentSuccessor(sessions, session, remaining) : null;
28
+ if (null === next)
29
+ return { session, verdict, followed: false };
30
+ // Computed on the DEPARTED session, before the caller points at the successor: the new session's
31
+ // elapsed() starts at 0 and would silently refund the whole budget the caller had already spent.
32
+ const leftover = timeout - (session.elapsed() - predicateStarted);
33
+ if (leftover <= 0)
34
+ return { session: next, verdict, followed: true };
35
+ return { session: next, verdict: await reevaluate(next, leftover), followed: true };
36
+ }
@@ -0,0 +1,20 @@
1
+ export declare function assertSequenceSteps(steps: readonly unknown[]): void;
2
+ /**
3
+ * Throws if this call cannot honestly be driven. Call it after the args are parsed and before the
4
+ * first dispatch — anything that depends on what is actually rendered belongs after the action.
5
+ */
6
+ export declare function preflightAct(actArgs: Record<string, unknown>, until: unknown): void;
7
+ /**
8
+ * Dispatch a write, or report that its outcome went unobserved.
9
+ *
10
+ * Returns null — never throws — when the transport was displaced by a newer connection claiming the
11
+ * same id. That happens on any full-document navigation the act itself causes: the page unloads,
12
+ * re-announces, and the write in flight is rejected by the handle that just died.
13
+ *
14
+ * The write is deliberately NOT re-sent. Re-asking a READ on the successor is free; re-sending a
15
+ * WRITE is not, because nothing can prove the first one did not reach the page — and a duplicated
16
+ * click on a button that charges a card is a far worse outcome than a verdict of "I could not see".
17
+ * Every other failure propagates unchanged: only a replacement is an observation problem, and
18
+ * treating a timeout or a refusal as one would launder a real failure into a shrug.
19
+ */
20
+ export declare function dispatchAct<T>(send: () => Promise<T>): Promise<T | null>;