@reticlehq/server 2.12.0 → 2.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +68 -0
  253. package/dist/setup/dev-server-wait.js +67 -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 +203 -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
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Resolve a session, or spend the caller's stated budget waiting for one to arrive.
3
+ *
4
+ * Every tool that takes a `timeout_ms` used to resolve the session as its FIRST statement, so an
5
+ * empty session map threw before the budget was ever consulted. A caller that said "wait up to
6
+ * thirty seconds for the app" was refused in under a millisecond, and the only way to actually wait
7
+ * was to poll `reticle_sessions` blind.
8
+ *
9
+ * That is the wrong answer to the commonest situation in the product. The moment an agent has run
10
+ * `init` and restarted the dev server is exactly the moment there is no session yet and one is about
11
+ * to appear — and `no_session` is the refusal agents hit most. An agent that gets refused rarely
12
+ * retries; it concludes and moves on. Spending the budget it already asked for turns the most common
13
+ * dead end into a wait that ends in a verdict.
14
+ *
15
+ * Deliberately narrow in two ways.
16
+ *
17
+ * Only the EMPTY case waits. A `sessionId` that names a session which is not connected while others
18
+ * are is a different error with a different fix, and waiting on it would hide a typo behind a
19
+ * timeout.
20
+ *
21
+ * The original error is what gets thrown on expiry, never a generic timeout message. That error
22
+ * carries the daemon's whole diagnosis — the port scan, the next action, the literal command — and
23
+ * it is the single most valuable string this product emits to an agent that has nothing connected.
24
+ * Replacing it with "timed out" would trade the answer for a stopwatch reading.
25
+ */
26
+ /** How often to re-ask. A session appearing is an out-of-band event; this is a plain poll. */
27
+ const POLL_MS = 250;
28
+ export async function resolveSessionWithin(sessions, sessionId, timeoutMs, deps) {
29
+ try {
30
+ return sessions.resolve(sessionId);
31
+ }
32
+ catch (first) {
33
+ // Nothing to wait FOR unless the map is empty, and nothing to wait WITH unless a budget was
34
+ // given. Either way the caller gets the same error it would have got before.
35
+ if (timeoutMs <= 0 || sessions.count() > 0)
36
+ throw first;
37
+ const deadline = deps.now() + timeoutMs;
38
+ for (;;) {
39
+ const remaining = deadline - deps.now();
40
+ if (remaining <= 0)
41
+ throw first;
42
+ await deps.sleep(Math.min(POLL_MS, remaining));
43
+ try {
44
+ return sessions.resolve(sessionId);
45
+ }
46
+ catch {
47
+ // Still nothing. Keep the FIRST error: it was raised when the daemon had most recently
48
+ // computed its diagnosis, and re-throwing the newest one would report the same fact with a
49
+ // staler explanation.
50
+ }
51
+ }
52
+ }
53
+ }
@@ -8,6 +8,15 @@ export interface SessionInfo {
8
8
  title?: string;
9
9
  adapters: string[];
10
10
  hasCapabilities: boolean;
11
+ /**
12
+ * Which shell answered: `web`, `electron` or `tauri`.
13
+ *
14
+ * Absent — never defaulted to `web` — on an SDK too old to report one, because the guess is wrong
15
+ * on exactly the machines this exists to tell apart. Two windows on the same url are otherwise
16
+ * indistinguishable, and a browser tab is not a desktop app: it has none of its IPC, and it does
17
+ * not render like it.
18
+ */
19
+ runtime?: string;
11
20
  /** Present only when the page's SDK version differs from the daemon's — see version-skew.ts. */
12
21
  versionSkew?: string;
13
22
  /** ms since the SDK last reported anything (silence ⇒ likely throttled). */
@@ -19,6 +28,13 @@ export interface SessionInfo {
19
28
  recommendation?: string;
20
29
  stale?: boolean;
21
30
  cleanup_suggestion?: string;
31
+ /**
32
+ * present ONLY when this tab has stopped answering commands — attached, streaming events, and
33
+ * missing every command budget. Never `false`: absence means "answering, or not asked yet".
34
+ */
35
+ unresponsive?: true;
36
+ /** present with `unresponsive` — the in-protocol way out of a wedged tab. */
37
+ unresponsive_suggestion?: string;
22
38
  /** present only when the human has flagged bugs on this tab — count of pending review marks. */
23
39
  pendingMarks?: number;
24
40
  /** present with pendingMarks — nudges the agent to drain them with reticle_review. */
@@ -47,11 +63,13 @@ interface SessionView {
47
63
  title: string;
48
64
  adapters: string[];
49
65
  hasCapabilities: boolean;
66
+ runtime: string | undefined;
50
67
  versionSkew: string | undefined;
51
68
  hidden: boolean;
52
69
  health: () => SessionHealth;
53
70
  staleMs: () => number;
54
71
  pendingMarkCount: () => number;
72
+ unresponsive: () => boolean;
55
73
  }
56
74
  /**
57
75
  * Project a live Session into the shape the tools return.
@@ -25,6 +25,8 @@ export function buildSessionInfo(session) {
25
25
  ...('' === session.title.trim() ? {} : { title: session.title }),
26
26
  adapters: session.adapters,
27
27
  hasCapabilities: session.hasCapabilities,
28
+ // Omitted when the page never said, so absence stays readable as "unknown" rather than "web".
29
+ ...(session.runtime === undefined ? {} : { runtime: session.runtime }),
28
30
  // On every listing, not buried in a log — skew explains failures that read as app bugs.
29
31
  ...(session.versionSkew === undefined ? {} : { versionSkew: session.versionSkew }),
30
32
  hidden: session.hidden,
@@ -35,6 +37,15 @@ export function buildSessionInfo(session) {
35
37
  base.cleanup_suggestion =
36
38
  'Call reticle_session{action:"end"} to free this session before starting new work.';
37
39
  }
40
+ // A tab that is attached and answering nothing. Said out loud because every health field beside it
41
+ // reads fine — that combination is exactly what made the wedge invisible.
42
+ if (session.unresponsive()) {
43
+ base.unresponsive = true;
44
+ base.unresponsive_suggestion =
45
+ 'This tab is connected but has stopped answering commands, so every call against it will time ' +
46
+ 'out. Ending the session does not revive the page. Reload the tab, or run `reticle open <url>` ' +
47
+ 'to get a fresh one — it will no longer hand this tab back.';
48
+ }
38
49
  // Surface human bug reports in reticle_sessions (only when > 0, so a clean session adds nothing).
39
50
  const marks = session.pendingMarkCount();
40
51
  if (marks > 0) {
@@ -1,3 +1,4 @@
1
+ import type { NoSessionReason } from '@reticlehq/core';
1
2
  import { Session, type SessionInfo } from './session.js';
2
3
  import { AttachmentHistory } from './attachment-history.js';
3
4
  import type { NoSessionNextAction } from './no-session-next-action.js';
@@ -37,6 +38,8 @@ export declare class SessionManager {
37
38
  * already computed for the error path; this exposes it to the read path too.
38
39
  */
39
40
  noSessionHint(): string | undefined;
41
+ setNoSessionReason(reason: (() => NoSessionReason | undefined) | undefined): void;
42
+ noSessionReason(): NoSessionReason | undefined;
40
43
  setNoSessionNextAction(next: (() => NoSessionNextAction | undefined) | undefined): void;
41
44
  noSessionNextAction(): NoSessionNextAction | undefined;
42
45
  everConnected(): boolean;
@@ -47,6 +50,17 @@ export declare class SessionManager {
47
50
  at: number;
48
51
  reason: string;
49
52
  } | undefined;
53
+ /**
54
+ * The id of the session that most recently went away, if any.
55
+ *
56
+ * Distinct from {@link lastClosure}, which only records closes the bridge itself initiated
57
+ * (handshake refusals, auth failures) — an ordinary disconnect never reaches it. This reads the
58
+ * tombstone ring `remove()` maintains, so it covers every departure.
59
+ *
60
+ * Read by the no-session diagnosis, which needs to say something about the session that actually
61
+ * vanished rather than about the daemon's lifetime history (#611).
62
+ */
63
+ lastDeparted(): string | undefined;
50
64
  resolve(sessionId?: string, scope?: ResolveScope): Session;
51
65
  }
52
66
  export {};
@@ -1,4 +1,5 @@
1
1
  import { NO_SESSION_CONNECTED_ERROR } from '@reticlehq/core';
2
+ import { notePendingNoSessionReason } from '../telemetry/tool-refused.js';
2
3
  import { declareDrivenRedactionKeys, forgetDrivenRedactionKeys, } from '../input/driven-redaction.js';
3
4
  import { Session } from './session.js';
4
5
  import { AttachmentHistory } from './attachment-history.js';
@@ -111,6 +112,11 @@ export class SessionManager {
111
112
  this.#recordConnection?.(session.projectId);
112
113
  const previous = this.#sessions.get(session.id);
113
114
  this.#sessions.set(session.id, session);
115
+ // Who can SEE this session's HUD feed. Wired here because `add` is the one method every path
116
+ // that registers a session goes through, and read lazily so a tab that opens later still counts.
117
+ // Optional-call for the same reason `pushImpact` is one at the dispatch chokepoint: a test
118
+ // double is a partial Session, and wiring a courtesy channel must never break registration.
119
+ session.setViewers?.(() => this.#viewersFor(session));
114
120
  // Publish what this app declared sensitive to the driven-path rule. Here rather than in the
115
121
  // bridge because EVERY path that registers a session goes through this method, and a declaration
116
122
  // that silently fails to register is a leak nothing would report.
@@ -119,6 +125,17 @@ export class SessionManager {
119
125
  this.#tombstones.delete(session.id);
120
126
  return previous;
121
127
  }
128
+ /**
129
+ * The other tabs of the same app as `driven`.
130
+ *
131
+ * Scoped the same way auto-selection is: by the stable projectId when the driven tab stamped one,
132
+ * else by origin. A drive session is frequently a headless pooled context (see Session.#viewers),
133
+ * so without this the human's own tab is the one place the report never reaches.
134
+ */
135
+ #viewersFor(driven) {
136
+ const scope = driven.projectId === undefined ? { url: driven.url } : { projectId: driven.projectId };
137
+ return scopeSessions([...this.#sessions.values()], scope).filter((s) => s !== driven);
138
+ }
122
139
  remove(session) {
123
140
  if (this.#sessions.get(session.id) !== session)
124
141
  return false;
@@ -218,6 +235,13 @@ export class SessionManager {
218
235
  * bridge constructed without a daemon (every unit test) keeps the plain message.
219
236
  */
220
237
  #noSessionHint;
238
+ /**
239
+ * The CODE for the same diagnosis `#noSessionHint` renders as prose.
240
+ *
241
+ * Registered from the same `explainNoSession` call as the hint, so the two cannot describe
242
+ * different branches. See no-session-watch.ts.
243
+ */
244
+ #noSessionReason;
221
245
  /** Wire the diagnosis provider (daemon boot). Absent ⇒ the plain, static message. */
222
246
  setNoSessionHint(hint) {
223
247
  this.#noSessionHint = hint;
@@ -233,6 +257,12 @@ export class SessionManager {
233
257
  noSessionHint() {
234
258
  return this.#noSessionHint?.();
235
259
  }
260
+ setNoSessionReason(reason) {
261
+ this.#noSessionReason = reason;
262
+ }
263
+ noSessionReason() {
264
+ return this.#noSessionReason?.();
265
+ }
236
266
  /**
237
267
  * The same diagnosis with the prose taken out — one action kind and one literal command.
238
268
  *
@@ -262,6 +292,22 @@ export class SessionManager {
262
292
  lastClosure() {
263
293
  return this.#recentClosures[this.#recentClosures.length - 1];
264
294
  }
295
+ /**
296
+ * The id of the session that most recently went away, if any.
297
+ *
298
+ * Distinct from {@link lastClosure}, which only records closes the bridge itself initiated
299
+ * (handshake refusals, auth failures) — an ordinary disconnect never reaches it. This reads the
300
+ * tombstone ring `remove()` maintains, so it covers every departure.
301
+ *
302
+ * Read by the no-session diagnosis, which needs to say something about the session that actually
303
+ * vanished rather than about the daemon's lifetime history (#611).
304
+ */
305
+ lastDeparted() {
306
+ let last;
307
+ for (const id of this.#tombstones.keys())
308
+ last = id;
309
+ return last;
310
+ }
265
311
  /**
266
312
  * A dead `sessionId`, answered with what the caller needs to recover — not with an errand.
267
313
  *
@@ -307,6 +353,10 @@ export class SessionManager {
307
353
  const refusal = closure === undefined
308
354
  ? ''
309
355
  : ` NOTE: the bridge REFUSED or closed a connection recently — "${closure.reason}". The app is probably still running and trying to connect: it was turned away, and the SDK does not retry after a policy close. The reason above names the fix — do not go looking for a stopped dev server.`;
356
+ // Hand the branch code to the refusal that is about to be reported for this throw. The
357
+ // diagnosis is computed HERE and the refusal is classified from the message downstream, so
358
+ // without this the largest refusal cohort stays one undifferentiated bucket (#615).
359
+ notePendingNoSessionReason(this.#noSessionReason?.());
310
360
  throw new Error(`${hint ?? NO_SESSION_CONNECTED_ERROR}${refusal}`);
311
361
  }
312
362
  // Scope to the agent's active project FIRST, so a stray tab from another app/origin (e.g. a
@@ -357,8 +407,15 @@ export class SessionManager {
357
407
  'several apps leaves it unable to tell them apart.');
358
408
  }
359
409
  // Multiple sessions: score each (lower = better candidate for auto-selection).
360
- // 0 = non-throttled (visible + recently-heard), 1 = throttled (hidden or stale heartbeat).
361
- const scored = all.map((s) => ({ s, score: s.throttled() ? 1 : 0, ms: s.lastSeenMs() }));
410
+ // 0 = non-throttled (visible + recently-heard), 1 = throttled (hidden or stale heartbeat),
411
+ // 2 = attached but not answering commands. A wedged tab keeps streaming events, so recency
412
+ // RATES IT HIGHEST — it was beating a healthy sibling on the one signal scoring looked at, and
413
+ // every call auto-targeted the one tab guaranteed to time out.
414
+ const scored = all.map((s) => ({
415
+ s,
416
+ score: s.unresponsive() ? 2 : s.throttled() ? 1 : 0,
417
+ ms: s.lastSeenMs(),
418
+ }));
362
419
  const bestScore = Math.min(...scored.map((x) => x.score));
363
420
  const candidates = scored.filter((x) => x.score === bestScore);
364
421
  // Sort candidates by recency (ascending lastSeenMs = most recently active first).
@@ -375,7 +432,8 @@ export class SessionManager {
375
432
  // desktop), the gap requirement is dropped: every session is already in "background" mode
376
433
  // so we just pick the one with the freshest heartbeat and let the agent proceed. Requiring
377
434
  // a gap here only produces spurious "ambiguous" errors while the user works elsewhere.
378
- const allThrottled = 1 === bestScore;
435
+ // Nothing healthy left to choose between (all throttled, or all wedged): drop the gap check.
436
+ const allThrottled = bestScore >= 1;
379
437
  const RECENCY_GAP_MS = allThrottled ? 0 : 1_000;
380
438
  const clearWinner = runnerUp === undefined || best.ms + RECENCY_GAP_MS < runnerUp.ms;
381
439
  if (!clearWinner) {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What a tab that is only WATCHING gets to see of a drive happening somewhere else.
3
+ *
4
+ * Its own module because it is its own question — the rest of session.ts is about a session Reticle
5
+ * is DRIVING, and this is about the one it is not. Split out when session.ts crossed the 1000-line
6
+ * cap; the cap did its job, because these two had no reason to share a file beyond history.
7
+ */
8
+ /**
9
+ * How a mirrored row is introduced on a tab that is only WATCHING.
10
+ *
11
+ * The row happened somewhere else, and a HUD that prints it unlabelled tells the reader their own
12
+ * tab was driven. Naming the session is also the handle they need to address it.
13
+ */
14
+ export declare function mirroredNarration(sessionId: string, text: string): string;
15
+ /** The pushes that are a REPORT of what happened, and so are worth showing to a watching tab. */
16
+ export declare const MIRRORED_COMMANDS: ReadonlySet<string>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * What a tab that is only WATCHING gets to see of a drive happening somewhere else.
3
+ *
4
+ * Its own module because it is its own question — the rest of session.ts is about a session Reticle
5
+ * is DRIVING, and this is about the one it is not. Split out when session.ts crossed the 1000-line
6
+ * cap; the cap did its job, because these two had no reason to share a file beyond history.
7
+ */
8
+ import { ReticleCommand } from '@reticlehq/core';
9
+ /**
10
+ * How a mirrored row is introduced on a tab that is only WATCHING.
11
+ *
12
+ * The row happened somewhere else, and a HUD that prints it unlabelled tells the reader their own
13
+ * tab was driven. Naming the session is also the handle they need to address it.
14
+ */
15
+ export function mirroredNarration(sessionId, text) {
16
+ return `${sessionId} \u2192 ${text}`;
17
+ }
18
+ /** The pushes that are a REPORT of what happened, and so are worth showing to a watching tab. */
19
+ export const MIRRORED_COMMANDS = new Set([
20
+ ReticleCommand.NARRATE,
21
+ ReticleCommand.IMPACT,
22
+ ]);
@@ -6,12 +6,26 @@ export interface RecommendationInputs {
6
6
  hidden: boolean;
7
7
  throttled: boolean;
8
8
  focused: boolean;
9
+ /**
10
+ * The shell this session runs in, as the SDK reported it at handshake. Undefined for an older SDK
11
+ * that does not report one — and an unknown runtime keeps the WEB advice, because a browser tab is
12
+ * what most sessions are and withholding a usable escape hatch is the worse error.
13
+ */
14
+ runtime?: string | undefined;
9
15
  }
10
16
  /**
11
- * A human-readable escape-hatch hint when a tab is hidden/throttled and may be
12
- * un-scriptable/un-focusable from here. Returns undefined for a healthy tab so the field stays
13
- * ABSENT (not empty). Keys on `hidden || throttled` — the same disjunction as Session.throttled —
14
- * so it never disagrees with the `throttled` flag the agent already sees. A merely-unfocused but
15
- * live tab is still scriptable, so blur alone does not trigger it. Pure.
17
+ * A human-readable hint when a tab is hidden or throttled. Returns undefined for a healthy tab so
18
+ * the field stays ABSENT (not empty). A merely-unfocused but live tab is still scriptable, so blur
19
+ * alone does not trigger it. Pure.
20
+ *
21
+ * HIDDEN and THROTTLED get DIFFERENT answers, which is the fix. They used to share one message that
22
+ * said "may be un-focusable; acquire a guaranteed scriptable context" — correct for a background
23
+ * tab, and actively harmful for a visible one. A visible throttled tab is driveable; measured in the
24
+ * field, one took a sign-in and two clean net-grade verdicts with no retries, while that message had
25
+ * already sent the agent into a lease the watching human could not see. One flag, advice that cost
26
+ * the product's main trust surface, and nothing bought.
27
+ *
28
+ * Hidden outranks throttled when both are set: a background tab is the stronger fact, and its
29
+ * failure mode (events landing on a page that never advances) is the one worth warning about.
16
30
  */
17
31
  export declare function buildSessionRecommendation(inputs: RecommendationInputs): string | undefined;
@@ -1,13 +1,29 @@
1
- import { UNSCRIPTABLE_TAB_RECOMMENDATION } from '@reticlehq/core';
1
+ import { AppRuntime, DESKTOP_WINDOW_BACKGROUNDED, HIDDEN_TAB_RECOMMENDATION, THROTTLED_TAB_RECOMMENDATION, } from '@reticlehq/core';
2
2
  /**
3
- * A human-readable escape-hatch hint when a tab is hidden/throttled and may be
4
- * un-scriptable/un-focusable from here. Returns undefined for a healthy tab so the field stays
5
- * ABSENT (not empty). Keys on `hidden || throttled` — the same disjunction as Session.throttled —
6
- * so it never disagrees with the `throttled` flag the agent already sees. A merely-unfocused but
7
- * live tab is still scriptable, so blur alone does not trigger it. Pure.
3
+ * A human-readable hint when a tab is hidden or throttled. Returns undefined for a healthy tab so
4
+ * the field stays ABSENT (not empty). A merely-unfocused but live tab is still scriptable, so blur
5
+ * alone does not trigger it. Pure.
6
+ *
7
+ * HIDDEN and THROTTLED get DIFFERENT answers, which is the fix. They used to share one message that
8
+ * said "may be un-focusable; acquire a guaranteed scriptable context" — correct for a background
9
+ * tab, and actively harmful for a visible one. A visible throttled tab is driveable; measured in the
10
+ * field, one took a sign-in and two clean net-grade verdicts with no retries, while that message had
11
+ * already sent the agent into a lease the watching human could not see. One flag, advice that cost
12
+ * the product's main trust surface, and nothing bought.
13
+ *
14
+ * Hidden outranks throttled when both are set: a background tab is the stronger fact, and its
15
+ * failure mode (events landing on a page that never advances) is the one worth warning about.
8
16
  */
9
17
  export function buildSessionRecommendation(inputs) {
10
- if (inputs.hidden || inputs.throttled)
11
- return UNSCRIPTABLE_TAB_RECOMMENDATION;
12
- return undefined;
18
+ if (!inputs.hidden && !inputs.throttled)
19
+ return undefined;
20
+ // A desktop window gets the one answer it can act on. Checked BEFORE hidden/throttled are told
21
+ // apart, because the distinction between them is about browser tab lifecycle and neither of its
22
+ // two answers exists for an app whose window is the client. See DESKTOP_WINDOW_BACKGROUNDED.
23
+ if (AppRuntime.ELECTRON === inputs.runtime || AppRuntime.TAURI === inputs.runtime) {
24
+ return DESKTOP_WINDOW_BACKGROUNDED;
25
+ }
26
+ if (inputs.hidden)
27
+ return HIDDEN_TAB_RECOMMENDATION;
28
+ return THROTTLED_TAB_RECOMMENDATION;
13
29
  }
@@ -9,6 +9,7 @@
9
9
  * Unique at the departed origin (and project, when known). Two live tabs at that origin is still a
10
10
  * guess, and a guess here would drive the wrong app.
11
11
  */
12
+ import { RETICLE_URL_PARAM } from '@reticlehq/core';
12
13
  const POLL_MS = 25;
13
14
  const REAL_CLOCK = {
14
15
  now: () => Date.now(),
@@ -22,6 +23,39 @@ function originOf(url) {
22
23
  return undefined;
23
24
  }
24
25
  }
26
+ /**
27
+ * The session a URL explicitly CLAIMS to be, via `__reticle_session`.
28
+ *
29
+ * A leased tab carries this marker so the pool can find the context it opened, and an app can set it
30
+ * deliberately so a battery addresses a known id. Either way it is a claim of identity, not a
31
+ * coincidence of origin — which is what makes it the right thing to gate succession on.
32
+ */
33
+ function claimedSessionOf(url) {
34
+ try {
35
+ return new URL(url).searchParams.get(RETICLE_URL_PARAM.SESSION) ?? undefined;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ /**
42
+ * Whether one session may inherit another's identity.
43
+ *
44
+ * Origin alone is not enough. A leased tab is an isolated context that an AGENT owns and no human
45
+ * can see; the developer's own tab at the same origin is a different trust and visibility domain
46
+ * entirely. Letting one succeed the other meant an expired lease silently redirected the next call
47
+ * into somebody's real browser tab — observed, twice, and reported as success.
48
+ *
49
+ * So a session that claims an explicit identity only ever succeeds to one claiming the SAME
50
+ * identity. A session that claims none is unaffected, which is the ordinary reload case this whole
51
+ * mechanism exists for.
52
+ */
53
+ function mayInherit(departed, candidate) {
54
+ const claimed = claimedSessionOf(departed.url);
55
+ if (claimed === undefined)
56
+ return true;
57
+ return claimedSessionOf(candidate.url) === claimed;
58
+ }
25
59
  function identityOf(session) {
26
60
  return {
27
61
  id: session.id,
@@ -44,7 +78,8 @@ export function pickDocumentSuccessor(live, departed) {
44
78
  if (sameId !== undefined)
45
79
  return sameId;
46
80
  const others = atOrigin.filter((s) => s.id !== departed.id &&
47
- (departed.projectId === undefined || s.projectId === departed.projectId));
81
+ (departed.projectId === undefined || s.projectId === departed.projectId) &&
82
+ mayInherit(departed, s));
48
83
  if (1 !== others.length)
49
84
  return undefined;
50
85
  return others[0];
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { ReticleCommand } from '@reticlehq/core';
3
3
  import { ReticleTool } from '../tools/tool-names.js';
4
4
  import { asNumber, asString } from '../tools/tools-helpers.js';
5
+ import { idleMsSchema } from '../tools/numeric-bounds.js';
5
6
  /**
6
7
  * Session lifecycle controls. The presenter session begins on the agent's first action and ends
7
8
  * either when the agent ends it (reticle_end_session) or after an idle window — and the human keeps the
@@ -13,8 +14,7 @@ export const SESSION_TOOLS = [
13
14
  name: ReticleTool.SESSION_TUNE,
14
15
  description: 'Tune the presenter session for this app. { idleEndMs } sets how long the session stays open after you go quiet before the panel shows the human you are WAITING (your turn). Default 5min — the SLOW backstop; signal handback IMMEDIATELY with reticle_session{action:"yield"} instead of waiting for this. Lower it for snappier auto-handback, raise it for a slow app where long gaps between your tool calls are normal. Enforced SERVER-SIDE (immune to background-tab throttling); it also fires if you (the MCP client) disconnect — so a forgotten or crashed session never leaves the HUD reading "live". Going quiet then acting again revives the session automatically. Returns { applied, idleEndMs }.',
15
16
  inputSchema: {
16
- idleEndMs: z
17
- .number()
17
+ idleEndMs: idleMsSchema
18
18
  .optional()
19
19
  .describe('Idle window in milliseconds after which the panel shows WAITING (your turn). Default: 300000 (5 min) — the slow backstop; prefer reticle_session{action:"yield"}. Raise for slow apps.'),
20
20
  sessionId: z
@@ -26,6 +26,17 @@ export declare class Session {
26
26
  readonly id: string;
27
27
  /** Stable build-stamped project identity; undefined for v1.0 SDKs that omit it. */
28
28
  readonly projectId: string | undefined;
29
+ /**
30
+ * The `.reticle` directory this session's evidence belongs in — impact counters AND capsules.
31
+ *
32
+ * Stamped once when the session is created, from the project it declared in HELLO, rather than
33
+ * resolved per call: the answer cannot change while the tab is connected, and resolving it once
34
+ * means every counter that fires for this session agrees about where it goes.
35
+ *
36
+ * Undefined when nothing could name a project — the caller then falls back to the daemon's own
37
+ * root, which is what every counter did unconditionally before this existed.
38
+ */
39
+ artifactRoot: string | undefined;
29
40
  url: string;
30
41
  title: string;
31
42
  adapters: string[];
@@ -62,6 +73,13 @@ export declare class Session {
62
73
  /** The attachable health block — single source of truth for the tools. */
63
74
  health(): SessionHealth;
64
75
  info(): SessionInfo;
76
+ /**
77
+ * Attached but not answering. A tab can be `connected` with a fresh `lastSeenMs` and still answer
78
+ * nothing: `lastSeenMs` measures the events the SDK streams, not the commands it replies to. Reuse
79
+ * and auto-selection read that as health, so `reticle open` handed a wedged tab back forever and
80
+ * `reticle_session{action:"end"}` — a flag on the record, not on the page — could not help.
81
+ */
82
+ unresponsive(): boolean;
65
83
  /** Wall-clock age of the session in milliseconds. */
66
84
  staleMs(): number;
67
85
  /** The document the most recent stamped event was observed under. See `#documentId`. */
@@ -187,10 +205,10 @@ export declare class Session {
187
205
  command(name: string, args?: Record<string, unknown>, timeoutMs?: number): Promise<CommandResult>;
188
206
  handleResult(result: CommandResult): void;
189
207
  /** Reject everything still in flight — used on disconnect. */
190
- rejectAll(reason: string): void;
208
+ rejectAll(reason: string, replaced?: boolean): void;
191
209
  succeededBy(next: Session): void;
192
210
  /** End this transport without letting a stale socket remove its replacement session. */
193
- disconnect(reason: string): void;
211
+ disconnect(reason: string, replaced?: boolean): void;
194
212
  getState(): SessionState;
195
213
  isPaused(): boolean;
196
214
  isEnded(): boolean;
@@ -243,6 +261,8 @@ export declare class Session {
243
261
  * without needing an explicit polling loop.
244
262
  */
245
263
  ageWarning(): string | undefined;
264
+ /** Wired by SessionManager.add, the one place every session registers. */
265
+ setViewers(read: () => Session[]): void;
246
266
  }
247
267
  /**
248
268
  * Re-exported from session-manager.ts so the public import path (`./session.js`) is unchanged for