@reticlehq/server 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/SKILL.md +30 -28
  2. package/dist/command/cli/adhoc-suite.d.ts +19 -0
  3. package/dist/command/cli/adhoc-suite.js +88 -0
  4. package/dist/command/cli/adhoc-verdict.d.ts +23 -2
  5. package/dist/command/cli/adhoc-verdict.js +18 -10
  6. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  7. package/dist/command/cli/cli-doctor.js +37 -5
  8. package/dist/command/cli/cli-flow-commands.d.ts +2 -1
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  11. package/dist/command/cli/cli-parse-grammar.js +7 -0
  12. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  13. package/dist/command/cli/cli-parse-verify.js +86 -13
  14. package/dist/command/cli/cli-parse.d.ts +8 -1
  15. package/dist/command/cli/cli-parse.js +38 -7
  16. package/dist/command/cli/cli-verify.d.ts +59 -0
  17. package/dist/command/cli/cli-verify.js +131 -19
  18. package/dist/command/cli/cloud-cli.js +7 -6
  19. package/dist/command/cli/cloud-kit.d.ts +0 -2
  20. package/dist/command/cli/cloud-kit.js +3 -7
  21. package/dist/command/cli/config/config-discovery.d.ts +12 -0
  22. package/dist/command/cli/config/config-discovery.js +19 -0
  23. package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
  24. package/dist/command/cli/doctor/doctor-rows.js +12 -0
  25. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  26. package/dist/command/cli/drive/drive-command.js +1 -1
  27. package/dist/command/cli/launch/cli-launch.d.ts +3 -23
  28. package/dist/command/cli/launch/cli-launch.js +29 -70
  29. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
  30. package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
  31. package/dist/command/cli/mcp-command.js +5 -3
  32. package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
  33. package/dist/command/cli/ports/resolve/cli-port.js +50 -2
  34. package/dist/command/cli/report-command.d.ts +16 -0
  35. package/dist/command/cli/report-command.js +97 -0
  36. package/dist/command/cli/status/status-lines.d.ts +28 -0
  37. package/dist/command/cli/status/status-lines.js +117 -0
  38. package/dist/command/cli.d.ts +1 -1
  39. package/dist/command/cli.js +29 -177
  40. package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
  41. package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
  42. package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
  43. package/dist/command/daemon/binding/loopback-reach.js +63 -0
  44. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  45. package/dist/command/daemon/daemon-resilience.d.ts +13 -1
  46. package/dist/command/daemon/daemon-resilience.js +12 -1
  47. package/dist/command/daemon/daemon.d.ts +2 -11
  48. package/dist/command/daemon/daemon.js +8 -11
  49. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  50. package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
  51. package/dist/command/daemon/lifetime/heartbeat.js +10 -0
  52. package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
  53. package/dist/command/daemon/lifetime/log-cap.js +115 -0
  54. package/dist/command/setup/agent-notice-memory.d.ts +16 -0
  55. package/dist/command/setup/agent-notice-memory.js +65 -0
  56. package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
  57. package/dist/command/setup/bringup/dev-server-wait.js +16 -0
  58. package/dist/command/setup/bringup/ensure-daemon.js +1 -1
  59. package/dist/command/setup/init/init-host.js +8 -0
  60. package/dist/command/setup/init/init-runtime.js +13 -1
  61. package/dist/command/setup/node-effects.js +1 -1
  62. package/dist/command/setup/probe/page-probe.js +9 -2
  63. package/dist/command/setup/run-setup.d.ts +25 -7
  64. package/dist/command/setup/run-setup.js +51 -9
  65. package/dist/command/setup/setup-command.js +31 -3
  66. package/dist/command/setup/terminal/confirm.js +2 -1
  67. package/dist/command/update/updater.d.ts +27 -0
  68. package/dist/command/update/updater.js +35 -2
  69. package/dist/features/crawl/crawl-tools.js +3 -1
  70. package/dist/features/crawl/crawl.d.ts +2 -0
  71. package/dist/features/crawl/crawl.js +3 -1
  72. package/dist/features/harness/drive-report.d.ts +73 -0
  73. package/dist/features/harness/drive-report.js +196 -0
  74. package/dist/features/harness/driver.js +29 -8
  75. package/dist/features/harness/drivers.d.ts +19 -0
  76. package/dist/features/harness/drivers.js +19 -0
  77. package/dist/features/harness/fill-values.d.ts +67 -0
  78. package/dist/features/harness/fill-values.js +153 -0
  79. package/dist/features/harness/jev-driver.d.ts +188 -0
  80. package/dist/features/harness/jev-driver.js +661 -0
  81. package/dist/features/harness/openai-driver.d.ts +46 -0
  82. package/dist/features/harness/openai-driver.js +197 -0
  83. package/dist/features/harness/platform-config.d.ts +62 -0
  84. package/dist/features/harness/platform-config.js +97 -0
  85. package/dist/features/visual/visual-tools.js +26 -2
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +80 -130
  88. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  89. package/dist/judgement/capsule/capsule-store.js +58 -6
  90. package/dist/judgement/capsule/capsule.js +5 -0
  91. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  92. package/dist/judgement/capsule/causal-summary.js +7 -0
  93. package/dist/judgement/domain/domain-model.js +38 -9
  94. package/dist/judgement/domain/domain-tools.js +4 -2
  95. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  96. package/dist/judgement/phenomena/phenomena.d.ts +2 -1
  97. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  98. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  99. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  100. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  101. package/dist/judgement/runs/artifact/render-report.js +4 -1
  102. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  103. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  104. package/dist/judgement/runs/context-tools.js +21 -3
  105. package/dist/judgement/runs/drive-run.js +7 -2
  106. package/dist/judgement/runs/verification-sync.d.ts +2 -2
  107. package/dist/judgement/runs/verification-sync.js +9 -2
  108. package/dist/language/flows/affected-tools.js +6 -3
  109. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  110. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  111. package/dist/language/flows/flow-anchor.d.ts +12 -0
  112. package/dist/language/flows/flow-anchor.js +20 -0
  113. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  114. package/dist/language/flows/flow-expect-grammar.js +92 -11
  115. package/dist/language/flows/flow-intent.js +1 -1
  116. package/dist/language/flows/flow-learning.d.ts +3 -0
  117. package/dist/language/flows/flow-learning.js +39 -8
  118. package/dist/language/flows/flow-replay-run.d.ts +55 -10
  119. package/dist/language/flows/flow-replay-run.js +217 -58
  120. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  121. package/dist/language/flows/flow-replay.d.ts +29 -11
  122. package/dist/language/flows/flow-replay.js +221 -203
  123. package/dist/language/flows/flow-result.d.ts +18 -1
  124. package/dist/language/flows/flow-result.js +18 -2
  125. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  126. package/dist/language/flows/flow-step-runners.js +20 -4
  127. package/dist/language/flows/flow-store-for-session.d.ts +2 -1
  128. package/dist/language/flows/flow-success.d.ts +21 -6
  129. package/dist/language/flows/flow-success.js +50 -138
  130. package/dist/language/flows/flow-tools.js +8 -4
  131. package/dist/language/flows/flows.d.ts +15 -10
  132. package/dist/language/flows/flows.js +17 -4
  133. package/dist/language/flows/heal.d.ts +9 -1
  134. package/dist/language/flows/heal.js +24 -2
  135. package/dist/language/flows/learned-sources.d.ts +21 -0
  136. package/dist/language/flows/learned-sources.js +84 -0
  137. package/dist/language/flows/narrow-write.d.ts +4 -4
  138. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  139. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  140. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  141. package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
  142. package/dist/language/flows/replay.js +13 -9
  143. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  144. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  145. package/dist/language/flows/stores/flake-store.js +2 -3
  146. package/dist/language/flows/suite/coverage.d.ts +1 -1
  147. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  148. package/dist/language/flows/suite-selection.d.ts +15 -1
  149. package/dist/language/flows/suite-selection.js +1 -1
  150. package/dist/language/flows/verify-change-tools.js +21 -4
  151. package/dist/memory/cloud/account-state.d.ts +1 -1
  152. package/dist/memory/cloud/account-state.js +5 -4
  153. package/dist/memory/cloud/cloud-config.d.ts +9 -0
  154. package/dist/memory/cloud/cloud-config.js +9 -0
  155. package/dist/memory/cloud/cloud-sync.d.ts +7 -4
  156. package/dist/memory/cloud/cloud-sync.js +11 -6
  157. package/dist/memory/cloud/harness-config.d.ts +12 -0
  158. package/dist/memory/cloud/harness-config.js +58 -0
  159. package/dist/memory/cloud/harness-offer.d.ts +44 -0
  160. package/dist/memory/cloud/harness-offer.js +146 -0
  161. package/dist/memory/cloud/harness-switch.d.ts +11 -0
  162. package/dist/memory/cloud/harness-switch.js +53 -0
  163. package/dist/memory/cloud/sync-cycle.d.ts +12 -0
  164. package/dist/memory/cloud/sync-cycle.js +29 -4
  165. package/dist/memory/cloud/sync-daemon.d.ts +35 -2
  166. package/dist/memory/cloud/sync-daemon.js +45 -0
  167. package/dist/memory/cloud/sync-disk.js +11 -6
  168. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  169. package/dist/memory/impact/impact-recorder.js +2 -1
  170. package/dist/memory/impact/impact-store.d.ts +5 -0
  171. package/dist/memory/impact/impact-store.js +25 -0
  172. package/dist/memory/intent/inline-intent.js +8 -3
  173. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  174. package/dist/memory/intent/intent-shard-store.js +253 -141
  175. package/dist/memory/intent/intent-shard.d.ts +22 -23
  176. package/dist/memory/intent/intent-shard.js +1 -1
  177. package/dist/memory/intent/intent-store.d.ts +13 -8
  178. package/dist/memory/intent/intent-store.js +46 -90
  179. package/dist/memory/intent/intent-tools.js +34 -23
  180. package/dist/memory/journal/ambient-store.js +2 -3
  181. package/dist/memory/journal/attach-journal.d.ts +11 -0
  182. package/dist/memory/journal/attach-journal.js +1 -1
  183. package/dist/memory/journal/deviation-report.js +4 -1
  184. package/dist/memory/journal/deviation-service.js +10 -2
  185. package/dist/memory/journal/drive-flow.d.ts +2 -2
  186. package/dist/memory/journal/drive-flow.js +21 -12
  187. package/dist/memory/journal/envelope-store.js +2 -3
  188. package/dist/memory/journal/journal-query.d.ts +35 -1
  189. package/dist/memory/journal/journal-query.js +46 -0
  190. package/dist/memory/journal/journal-recorder.d.ts +71 -2
  191. package/dist/memory/journal/journal-recorder.js +45 -7
  192. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  193. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  194. package/dist/memory/journal/on-disk/retention.d.ts +91 -2
  195. package/dist/memory/journal/on-disk/retention.js +181 -11
  196. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  197. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  198. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
  199. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
  200. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  201. package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
  202. package/dist/memory/journal/route-template.d.ts +22 -0
  203. package/dist/memory/journal/route-template.js +44 -0
  204. package/dist/memory/journal/self-observation.d.ts +3 -0
  205. package/dist/memory/journal/self-observation.js +67 -0
  206. package/dist/memory/journal/session-end.d.ts +17 -4
  207. package/dist/memory/journal/session-end.js +85 -7
  208. package/dist/memory/journal/session-journal.d.ts +122 -5
  209. package/dist/memory/journal/session-journal.js +489 -32
  210. package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
  211. package/dist/memory/project/artifact-root-resolver.js +89 -0
  212. package/dist/memory/project/artifact-root.d.ts +39 -2
  213. package/dist/memory/project/artifact-root.js +58 -1
  214. package/dist/memory/project/baselines.d.ts +1 -1
  215. package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
  216. package/dist/memory/project/dir/fill-value-store.js +88 -0
  217. package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
  218. package/dist/memory/project/dir/reticle-dir.js +11 -0
  219. package/dist/memory/project/fs/fs-port.d.ts +22 -1
  220. package/dist/memory/project/fs/fs-port.js +72 -5
  221. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  222. package/dist/memory/project/fs/write-atomic.js +48 -0
  223. package/dist/memory/project/learned-routes.d.ts +16 -2
  224. package/dist/memory/project/learned-routes.js +26 -10
  225. package/dist/memory/project/project-for-root.d.ts +30 -0
  226. package/dist/memory/project/project-for-root.js +43 -0
  227. package/dist/memory/project/project-tools.js +10 -5
  228. package/dist/memory/project/session-root.d.ts +35 -6
  229. package/dist/memory/project/session-root.js +70 -8
  230. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  231. package/dist/memory/recall/prior/connection-memory.js +8 -0
  232. package/dist/memory/recall/registered-projects.d.ts +2 -1
  233. package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
  234. package/dist/portal/bridge/auth-failure-reason.js +20 -3
  235. package/dist/portal/bridge/bridge.d.ts +6 -1
  236. package/dist/portal/bridge/bridge.js +36 -2
  237. package/dist/portal/input/real-input.js +2 -1
  238. package/dist/portal/pool/browser-pool.d.ts +10 -0
  239. package/dist/portal/pool/browser-pool.js +10 -0
  240. package/dist/portal/pool/playwright-launcher.js +1 -0
  241. package/dist/portal/pool/pool-contract.d.ts +2 -0
  242. package/dist/portal/pool/storage-seed.d.ts +2 -1
  243. package/dist/portal/pool/zero-install.d.ts +27 -0
  244. package/dist/portal/pool/zero-install.js +54 -0
  245. package/dist/portal/realm/web-realm.js +6 -0
  246. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  247. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  248. package/dist/portal/session/dev-server/served-document.js +57 -0
  249. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  250. package/dist/portal/session/facts/background-traffic.js +25 -0
  251. package/dist/portal/session/facts/session-replaced.d.ts +19 -0
  252. package/dist/portal/session/facts/session-replaced.js +22 -0
  253. package/dist/portal/session/live-control-tools.js +48 -3
  254. package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
  255. package/dist/portal/session/no-session-diagnosis.js +67 -60
  256. package/dist/portal/session/no-session-next-action.js +3 -2
  257. package/dist/portal/session/no-session-watch.js +14 -10
  258. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  259. package/dist/portal/session/rebind.d.ts +18 -0
  260. package/dist/portal/session/rebind.js +38 -0
  261. package/dist/portal/session/session-health.d.ts +61 -0
  262. package/dist/portal/session/session-health.js +40 -1
  263. package/dist/portal/session/session-info.d.ts +6 -0
  264. package/dist/portal/session/session-manager.d.ts +13 -0
  265. package/dist/portal/session/session-manager.js +42 -3
  266. package/dist/portal/session/session-successor.d.ts +11 -0
  267. package/dist/portal/session/session-successor.js +14 -1
  268. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  269. package/dist/portal/session/session-verdict-facts.js +15 -0
  270. package/dist/portal/session/session.d.ts +26 -10
  271. package/dist/portal/session/session.js +48 -49
  272. package/dist/status-payload.d.ts +12 -1
  273. package/dist/status-payload.js +4 -1
  274. package/dist/surface/http-server.js +3 -5
  275. package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
  276. package/dist/surface/mcp/faults/mcp-outage.js +20 -4
  277. package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
  278. package/dist/surface/mcp/mcp-proxy.js +23 -12
  279. package/dist/surface/mcp/mcp.d.ts +10 -1
  280. package/dist/surface/mcp/mcp.js +59 -29
  281. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
  282. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
  283. package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
  284. package/dist/surface/mcp/proxy/proxy-log.js +7 -22
  285. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  286. package/dist/surface/mcp/server-instructions.js +12 -1
  287. package/dist/surface/tools/act/act-preflight.js +6 -1
  288. package/dist/surface/tools/act/already-true.d.ts +40 -0
  289. package/dist/surface/tools/act/already-true.js +36 -0
  290. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
  291. package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
  292. package/dist/surface/tools/act/sequence-grade.js +13 -0
  293. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  294. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  295. package/dist/surface/tools/act-capsule.js +49 -8
  296. package/dist/surface/tools/act-tools.js +25 -24
  297. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  298. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  299. package/dist/surface/tools/assert/assert-grade.js +14 -2
  300. package/dist/surface/tools/assert/assert-verdict.js +74 -21
  301. package/dist/surface/tools/browser-tools.js +4 -3
  302. package/dist/surface/tools/coverage-tools.js +5 -2
  303. package/dist/surface/tools/dynamic-tools.js +43 -46
  304. package/dist/surface/tools/error-recovery.d.ts +6 -0
  305. package/dist/surface/tools/error-recovery.js +15 -0
  306. package/dist/surface/tools/explore-tools.js +80 -7
  307. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  308. package/dist/surface/tools/feedback-tools.js +50 -3
  309. package/dist/surface/tools/first-sentence.d.ts +24 -0
  310. package/dist/surface/tools/first-sentence.js +0 -0
  311. package/dist/surface/tools/harness-explore.d.ts +125 -1
  312. package/dist/surface/tools/harness-explore.js +401 -15
  313. package/dist/surface/tools/harness-plan.d.ts +76 -0
  314. package/dist/surface/tools/harness-plan.js +156 -0
  315. package/dist/surface/tools/harness-toolset.js +12 -1
  316. package/dist/surface/tools/invoke-tool.js +82 -9
  317. package/dist/surface/tools/lease-hint.d.ts +8 -0
  318. package/dist/surface/tools/lease-hint.js +10 -0
  319. package/dist/surface/tools/lease-tools.d.ts +16 -28
  320. package/dist/surface/tools/lease-tools.js +53 -5
  321. package/dist/surface/tools/lineage-tools.js +1 -1
  322. package/dist/surface/tools/live-call-text.d.ts +6 -0
  323. package/dist/surface/tools/live-call-text.js +36 -2
  324. package/dist/surface/tools/observe-tools.js +69 -15
  325. package/dist/surface/tools/read/query-windowed.d.ts +1 -1
  326. package/dist/surface/tools/read/query-windowed.js +1 -1
  327. package/dist/surface/tools/read-tools.js +1 -2
  328. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  329. package/dist/surface/tools/real-input-attempt.js +1 -1
  330. package/dist/surface/tools/tool-kit.d.ts +23 -3
  331. package/dist/surface/tools/tool-kit.js +6 -1
  332. package/dist/surface/tools/tool-surface.js +17 -5
  333. package/dist/surface/tools/tools-helpers.d.ts +8 -7
  334. package/dist/surface/tools/tools-helpers.js +11 -13
  335. package/dist/surface/tools/tools.js +70 -30
  336. package/dist/telemetry/bug-found.js +32 -2
  337. package/dist/telemetry/feedback-context.js +3 -3
  338. package/dist/telemetry/feedback-local.d.ts +1 -0
  339. package/dist/telemetry/feedback-local.js +1 -3
  340. package/dist/telemetry/feedback.d.ts +1 -1
  341. package/dist/telemetry/harness-drive.d.ts +6 -0
  342. package/dist/telemetry/harness-drive.js +39 -0
  343. package/dist/telemetry/hud-metrics.d.ts +32 -0
  344. package/dist/telemetry/hud-metrics.js +95 -0
  345. package/dist/telemetry/mcp-connection.d.ts +14 -1
  346. package/dist/telemetry/mcp-connection.js +17 -2
  347. package/dist/telemetry/onboarding-firsts.d.ts +30 -1
  348. package/dist/telemetry/onboarding-firsts.js +54 -12
  349. package/dist/telemetry/session-metrics.d.ts +5 -0
  350. package/dist/telemetry/session-metrics.js +15 -1
  351. package/dist/telemetry/verification-of.js +31 -3
  352. package/dist/wire-journal.d.ts +31 -0
  353. package/dist/wire-journal.js +100 -0
  354. package/docs/README.md +1 -0
  355. package/docs/agent-cheatsheet.md +7 -3
  356. package/docs/autodrive.md +42 -2
  357. package/docs/benchmarks.md +1 -1
  358. package/docs/capabilities.mdx +1 -1
  359. package/docs/cli/cloud.mdx +11 -9
  360. package/docs/cli/doctor.mdx +1 -0
  361. package/docs/cli/init.mdx +1 -0
  362. package/docs/cli/report.mdx +44 -0
  363. package/docs/cli/status.mdx +35 -20
  364. package/docs/cli.mdx +8 -18
  365. package/docs/desktop.mdx +1 -1
  366. package/docs/faq.mdx +1 -1
  367. package/docs/flows.md +11 -0
  368. package/docs/frameworks.mdx +113 -1
  369. package/docs/install-agentic.mdx +36 -0
  370. package/docs/install-manual.mdx +1 -1
  371. package/docs/packages/tauri.mdx +1 -1
  372. package/docs/packages.mdx +1 -1
  373. package/docs/predicates.mdx +144 -4
  374. package/docs/system-map.md +6 -4
  375. package/docs/telemetry.md +2 -1
  376. package/docs/token-efficiency.md +2 -2
  377. package/docs/tools/overview.mdx +1 -1
  378. package/docs/tools/query.mdx +10 -10
  379. package/docs/troubleshooting.mdx +8 -2
  380. package/docs/usage.md +2 -2
  381. package/docs/what-is-recorded.md +62 -0
  382. package/package.json +5 -5
  383. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  384. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
@@ -1,5 +1,69 @@
1
- import { JournalActionSchema, ReticleEventSchema, } from '@reticlehq/core';
2
- import { isValidSessionId, journalActionsPath, journalEventsPath, sessionDirPath, } from '../project/dir/reticle-dir.js';
1
+ import { EventType, JOURNAL_FILE_VERSION, JournalActionSchema, JournalWriteLossSchema, ReticleEventSchema, TruncationChannel, } from '@reticlehq/core';
2
+ import { log } from '../../log.js';
3
+ import { isValidSessionId, journalActionsPath, journalClosedPath, journalEventsPath, sessionDirPath, } from '../project/dir/reticle-dir.js';
4
+ /**
5
+ * The two ceilings a durable read is held to.
6
+ *
7
+ * Neither is a tuning number. `events.jsonl` is append-only and nothing prunes a single session's
8
+ * file (`pruneSessions` caps how many session DIRECTORIES survive, never how large one grows), and
9
+ * the session id lives in the page's sessionStorage, so a reload reconnects under the same id and
10
+ * builds a fresh journal over the same file with its byte cursor back at zero. That first read
11
+ * asked for the whole accumulated file, and V8 cannot build a string longer than 0x1fffffe8
12
+ * characters: `Buffer.prototype.toString` throws rather than returning a shorter one. The throw came
13
+ * out of the daemon as a raw exception on `reticle_assert` and `reticle_act_and_wait` — the only
14
+ * tools that reach this fall-through, and the only two that produce a verdict at all.
15
+ *
16
+ * `MAX_READ_BYTES` bounds ONE read. That alone bounds nothing over the life of a session: reads are
17
+ * incremental, so a thousand reads each well under the ceiling still accumulate every record they
18
+ * parsed. `MAX_RETAINED_*` bound what the parse-cache keeps, which is the quantity that actually
19
+ * grows with session age.
20
+ *
21
+ * The numbers, and what they are measured against:
22
+ *
23
+ * - 32 MiB is four times the ring buffer's own byte budget (`RING_BUFFER_DEFAULTS.MAX_BYTES`), so
24
+ * the durable half still reaches meaningfully further back than the hot cache it backs, and
25
+ * roughly a sixteenth of the string ceiling above (0x1fffffe8 characters, about 512 MB of
26
+ * ASCII) — a margin, not an order of magnitude, and enough of one.
27
+ * - Retention is bounded in journal BYTES and in RECORDS, because neither alone bounds memory:
28
+ * bytes track the file, records track the parsed objects, and an object costs several times the
29
+ * JSONL it came from (see the parse-cache note below). 100k records is fifty times the ring
30
+ * buffer's event budget, so the fall-through is still the long-memory half by a wide margin.
31
+ */
32
+ export const JOURNAL_READ_LIMITS = {
33
+ /** What ONE read materialises as a single string. */
34
+ MAX_READ_BYTES: 32 * 1024 * 1024,
35
+ /** What the parse-cache keeps, in the journal bytes those records were parsed from. */
36
+ MAX_RETAINED_BYTES: 32 * 1024 * 1024,
37
+ /** What the parse-cache keeps, in records. Whichever bound binds first wins. */
38
+ MAX_RETAINED_EVENTS: 100_000,
39
+ };
40
+ /**
41
+ * Reclaim the evicted prefix once it dominates the cache — the same amortized-O(1) compaction the
42
+ * ring buffer uses, and for the same reason: shifting per evicted event is O(n) per read.
43
+ */
44
+ const CACHE_COMPACT_AT = 1024;
45
+ /**
46
+ * A ceiling is a whole number of bytes or records, or it is a bug upstream.
47
+ *
48
+ * Checked rather than coerced: `NaN` and `Infinity` both survive every arithmetic comparison a
49
+ * bound is used in and silently disable it, which is the one failure mode a bound must not have.
50
+ */
51
+ function boundedOption(value, fallback, name) {
52
+ if (value === undefined)
53
+ return fallback;
54
+ if (!Number.isSafeInteger(value) || value <= 0) {
55
+ throw new Error(`refusing a journal ${name} that is not a positive whole number: ${String(value)}`);
56
+ }
57
+ return value;
58
+ }
59
+ /**
60
+ * Default byte budget for one serialized writer's event ledger (#986).
61
+ *
62
+ * A ledger may exceed this by one bounded truncation marker. Existing oversized files are not
63
+ * deleted or rewritten; they stop accepting event batches. This is a per-event-file limit, not a
64
+ * workspace-wide quota, and does not bound action journals or concurrent independent writers.
65
+ */
66
+ export const JOURNAL_EVENT_BYTES_CAP = 64 * 1024 * 1024;
3
67
  /**
4
68
  * The durable per-session journal: append-only JSONL for events and actions, the ledger the ring
5
69
  * buffer becomes a hot cache over. Writes are batched (the caller flushes ring-buffer windows), so a
@@ -10,47 +74,312 @@ import { isValidSessionId, journalActionsPath, journalEventsPath, sessionDirPath
10
74
  * journal stores redacted payloads; the ledger is local-only. A server-side second-pass event redactor
11
75
  * is defense-in-depth for a later commit, not a correctness gate here.
12
76
  *
13
- * ponytail: append-per-batch, no retention yet. Bounded-disk pruning (cap session dirs / file size,
14
- * "pruned like runs/") is a dedicated follow-up. Perf ceiling: if main-thread overhead becomes
15
- * visible at high event rates, coalesce batches behind a flush timer.
77
+ * Event READS are BOUNDED at both ends — one read, and what the parse-cache keeps across reads.
78
+ * Both keep the NEWEST records and declare what they removed through `readLoss()`. Without the read
79
+ * ceiling a long session's file outgrew what a JavaScript string can hold and the read threw
80
+ * instead of answering; without the retention ceiling the cache grew for the life of the session.
81
+ * See `JOURNAL_READ_LIMITS` and `session-journal.lossy-conformance.test.ts`.
82
+ *
83
+ * The event ledger is also bounded on the WRITE side — see {@link JOURNAL_EVENT_BYTES_CAP} and
84
+ * `#closeAtCap`. Session directories are pruned by recency in `on-disk/retention.ts`; this is the
85
+ * per-file half of the same question, and the half a single runaway session needs. The two ceilings
86
+ * answer different questions and neither substitutes for the other: one stops a read from throwing,
87
+ * the other stops the file from growing without end.
88
+ *
89
+ * `readActions` is NOT bounded: it reads the whole action ledger as one string and would throw the
90
+ * same V8 string-length error on a large enough one. Left that way deliberately, and the reason is
91
+ * the loss channel rather than the read — the action ledger is one record per tool call, so it is
92
+ * orders of magnitude smaller than the event stream, and it is read by the run fold rather than by
93
+ * a verdict. Bounding it needs a loss report of its own: folding an action-ledger cut into
94
+ * `readLoss` would impeach page verdicts (through `Session.lostSince`) over a ledger that is not
95
+ * page evidence. Do that when an action ledger is measured large, not before.
96
+ *
97
+ * ponytail: append-per-batch. Perf ceiling: if main-thread overhead becomes visible at high event
98
+ * rates, coalesce batches behind a flush timer.
16
99
  */
17
100
  export class SessionJournal {
18
101
  #fs;
19
102
  #root;
20
103
  #sessionId;
104
+ #eventBytesCap;
21
105
  #dirEnsured = false;
106
+ /**
107
+ * Bytes of `events.jsonl` on disk, or undefined until the first append reads the file's real size.
108
+ *
109
+ * Seeded from `stat` rather than counted from zero, because a session id survives a reload (it
110
+ * lives in sessionStorage) and the next connection reopens the SAME file. A per-connection budget
111
+ * would let a reload loop grow the ledger by a whole cap each time, which is the bug with an extra
112
+ * step in it.
113
+ */
114
+ #eventBytes;
115
+ /**
116
+ * The ledger's closure report, once known — `undefined` means open, and `#lossLoaded` says which.
117
+ *
118
+ * Read from disk rather than remembered, because a fresh object is exactly what a reconnect
119
+ * produces. The first implementation of this cap held the closed state in a boolean field, so the
120
+ * ledger reopened on every connection: it re-appended a declaration line each time (a file that
121
+ * still grew without bound, slower, while claiming a hard ceiling), and where the refused batch
122
+ * had been large enough to leave room under the ceiling it also accepted the NEXT small batch —
123
+ * writing events after the marker that says the ledger stopped.
124
+ */
125
+ #writeLoss;
126
+ #lossLoaded = false;
22
127
  // Parse-cache for the append-only EVENTS journal. queryEvents falls through to readEvents on every
23
128
  // observe/network/console call once the ring buffer has evicted (permanent ~60s into any session), so
24
129
  // a naive readEvents re-read + re-JSON.parse + re-zod-validated the WHOLE file each time — measured at
25
130
  // a 1-hour 100 ev/s session as ~1.5s CPU + ~300MB transient heap PER tool call, growing with age. The
26
131
  // journal only ever grows and is always a run of complete '\n'-terminated lines, so we keep the parsed
27
132
  // events and the char count already consumed, and parse only the tail written since the last read.
28
- #eventCache;
133
+ #eventCache = [];
134
+ /** Journal bytes each cached event was parsed from, parallel to #eventCache — the retention cost. */
135
+ #eventCost = [];
136
+ /** Index of the first LIVE event; [0, #cacheHead) are evicted but not yet compacted out. */
137
+ #cacheHead = 0;
138
+ #retainedBytes = 0;
29
139
  #eventCharsConsumed = 0; // fallback (whole-file) path: UTF-16 code units consumed
30
140
  #eventBytesConsumed = 0; // fast (bounded-read) path: UTF-8 bytes consumed, always at a '\n' boundary
31
- constructor(fs, root, sessionId) {
141
+ #maxReadBytes;
142
+ #maxRetainedBytes;
143
+ #maxRetainedEvents;
144
+ /** Journal bytes not represented in the answer: skipped by a cut, or evicted after parsing. */
145
+ #droppedBytes = 0;
146
+ /** Parsed records evicted from the retained set. Exact — these were counted, not estimated. */
147
+ #droppedEvents = 0;
148
+ /**
149
+ * Latest parsed eviction timestamp, or +Infinity after unparsed bytes were skipped.
150
+ * -Infinity means no loss. Later intact reads cannot narrow an unknown discarded time range.
151
+ */
152
+ #lostThroughT = Number.NEGATIVE_INFINITY;
153
+ constructor(fs, root, sessionId, options = {}) {
32
154
  if (!isValidSessionId(sessionId)) {
33
155
  throw new Error(`refusing to journal an unsafe session id: ${sessionId}`);
34
156
  }
157
+ const cap = options.eventBytesCap ?? JOURNAL_EVENT_BYTES_CAP;
158
+ // Checked rather than trusted, because every way of getting this wrong fails SILENTLY as a
159
+ // journal that quietly stopped writing: zero and negatives refuse every batch, a fraction is a
160
+ // ceiling no byte count can ever equal, and NaN makes every comparison false so the bound is
161
+ // simply absent. Refusing at construction turns all four into a crash at the one place that
162
+ // names the value.
163
+ if (!Number.isSafeInteger(cap) || cap <= 0) {
164
+ throw new Error(`refusing to journal against a ${String(cap)}-byte event cap: expected a positive whole number of bytes`);
165
+ }
35
166
  this.#fs = fs;
36
167
  this.#root = root;
37
168
  this.#sessionId = sessionId;
169
+ this.#maxReadBytes = boundedOption(options.maxReadBytes, JOURNAL_READ_LIMITS.MAX_READ_BYTES, 'maxReadBytes');
170
+ this.#maxRetainedBytes = boundedOption(options.maxRetainedBytes, JOURNAL_READ_LIMITS.MAX_RETAINED_BYTES, 'maxRetainedBytes');
171
+ this.#maxRetainedEvents = boundedOption(options.maxRetainedEvents, JOURNAL_READ_LIMITS.MAX_RETAINED_EVENTS, 'maxRetainedEvents');
172
+ this.#eventBytesCap = cap;
38
173
  }
174
+ /**
175
+ * Append a batch, or refuse it at the byte cap and say so.
176
+ *
177
+ * A batch is accepted whole or not at all. This preserves the batch's ordering and avoids
178
+ * representing only part of a refused batch as successfully persisted evidence.
179
+ *
180
+ * REFUSAL, not rotation. Rotation keeps the newest evidence, which is the better policy in the
181
+ * abstract, but it shrinks the file — and `readEvents` tracks a byte offset into it, so a rotation
182
+ * resets that cursor and the reader silently returns FEWER events than it did a call earlier, with
183
+ * nothing to say why. Refusal keeps the file monotonic, so every cursor stays valid, no evidence
184
+ * already on disk is deleted, and the loss is at the end where it can be declared.
185
+ */
39
186
  async appendEvents(events) {
40
187
  if (0 === events.length)
41
188
  return;
189
+ // Durable, not a field: see #writeLoss. A closed ledger is closed for a one-event batch too.
190
+ if ((await this.#closureReport()) !== undefined)
191
+ return;
42
192
  await this.#ensureDir();
43
- const text = `${events.map((e) => JSON.stringify(e)).join('\n')}\n`;
44
- await this.#fs.appendFile(journalEventsPath(this.#root, this.#sessionId), text);
193
+ const path = journalEventsPath(this.#root, this.#sessionId);
194
+ const written = await this.#eventBytesOnDisk(path);
195
+ const budget = this.#eventBytesCap - written;
196
+ // Serialized INCREMENTALLY, stopping the moment the batch has outgrown what is left. Building
197
+ // the whole string first means the runaway session pays full serialization cost and peak memory
198
+ // for every batch it will never be allowed to write — the flood stops costing disk and keeps
199
+ // costing everything else. Nothing here touches the caller's events.
200
+ const lines = [];
201
+ let bytes = 0;
202
+ for (const event of events) {
203
+ const line = `${JSON.stringify(event)}\n`;
204
+ bytes += Buffer.byteLength(line, 'utf8');
205
+ if (bytes > budget) {
206
+ await this.#closeAtCap(path, written, events);
207
+ return;
208
+ }
209
+ lines.push(line);
210
+ }
211
+ // `#append`, not a bare appendFile: the cap decides WHETHER to write, and #append survives
212
+ // the directory vanishing under a live session. The two bounds are independent.
213
+ await this.#append(path, lines.join(''));
214
+ this.#eventBytes = written + bytes;
215
+ }
216
+ /**
217
+ * Bytes already in the ledger, read once from disk per instance then tracked in memory.
218
+ *
219
+ * Seeded from `stat` rather than counted from zero, because a session id survives a reload (it
220
+ * lives in sessionStorage) and the next connection reopens the SAME file.
221
+ */
222
+ async #eventBytesOnDisk(path) {
223
+ const known = this.#eventBytes;
224
+ if (known !== undefined)
225
+ return known;
226
+ let size;
227
+ try {
228
+ size = (await this.#fs.stat(path)).size;
229
+ }
230
+ catch (error) {
231
+ // ENOENT is the ONLY absence. A permission change, an IO error, a directory where the ledger
232
+ // belongs — each of those is a size nobody knows, and answering zero there hands the writer a
233
+ // full budget over a file that may already be at the ceiling, then CACHES that zero so every
234
+ // later batch on this connection is measured against an empty file. The cap would be disabled
235
+ // by exactly the conditions most likely to accompany a failing disk. Thrown before anything is
236
+ // appended; `JournalRecorder` swallows sink errors, so a live session is never taken down by
237
+ // one.
238
+ if (!this.#fs.isNotFound(error))
239
+ throw error;
240
+ size = 0;
241
+ }
242
+ this.#eventBytes = size;
243
+ return size;
244
+ }
245
+ /**
246
+ * The ledger's closure report, or `undefined` while it is still open. Read from disk once.
247
+ *
248
+ * A present-but-unparseable report still means CLOSED: its presence is the durable fact and its
249
+ * contents are the detail. Reading a corrupted note as "no closure" would reopen a ledger that
250
+ * was deliberately shut, which is the one answer that cannot be right. The details are reported
251
+ * absent rather than reconstructed from what is true now — this instance's ceiling is not
252
+ * necessarily the ceiling that closed the file.
253
+ */
254
+ async #closureReport() {
255
+ if (this.#lossLoaded)
256
+ return this.#writeLoss;
257
+ let text;
258
+ try {
259
+ text = await this.#fs.readFile(journalClosedPath(this.#root, this.#sessionId));
260
+ }
261
+ catch (error) {
262
+ // Same rule as the size read above, and for the same reason: only ENOENT is "never closed".
263
+ if (!this.#fs.isNotFound(error))
264
+ throw error;
265
+ this.#lossLoaded = true;
266
+ return undefined;
267
+ }
268
+ this.#lossLoaded = true;
269
+ this.#writeLoss = parseClosureReport(text);
270
+ return this.#writeLoss;
271
+ }
272
+ /**
273
+ * What this session's durable ledger lost on the WRITE side, or `undefined` if it lost nothing.
274
+ *
275
+ * The half the in-band marker cannot cover. That record carries the `t` of the last refused event
276
+ * and every journal-backed query filters on `t`, so a window opened after the ceiling was reached
277
+ * holds no marker and no events — which is indistinguishable from a complete window in which
278
+ * nothing happened. Named `readWriteLoss` because it is the WRITER's loss: what never reached the
279
+ * file, as opposed to what a bounded read declined to hand back.
280
+ */
281
+ async readWriteLoss() {
282
+ return this.#closureReport();
283
+ }
284
+ /**
285
+ * The ledger's size against its ceiling, once the size is known.
286
+ *
287
+ * `undefined` before the first append, because `#eventBytes` is seeded from `stat` at that point
288
+ * and a zero reported before it is measured would be a claim rather than a reading.
289
+ */
290
+ ledger() {
291
+ const bytes = this.#eventBytes;
292
+ return bytes === undefined ? undefined : { bytes, capBytes: this.#eventBytesCap };
293
+ }
294
+ /**
295
+ * Close the ledger for good, and declare the loss twice — durably, then in-band.
296
+ *
297
+ * The two declarations do different jobs and neither replaces the other. The sidecar report is the
298
+ * GATE: it is what makes the closure survive the reconnect that reopens this file with a fresh
299
+ * object, and it is the only declaration a time-windowed query cannot filter away. The in-band
300
+ * `TRUNCATED` record is the COURTESY: every reader of this file already reads `ReticleEvent`s, so
301
+ * it needs no new field, no new interface and no new allowlist, and `EventType.TRUNCATED` is the
302
+ * vocabulary the browser's own per-channel caps already use — this adds a channel, not a second
303
+ * mechanism.
304
+ *
305
+ * Written in that order deliberately. A failure between the two leaves a ledger that is closed
306
+ * with no in-band marker, which `readWriteLoss` still reports; the other order leaves a ledger
307
+ * that is marked and not closed, so the next connection would mark it again and the file would
308
+ * keep growing one line per reload. The failure that would hit here is a disk that cannot take
309
+ * another byte, which is precisely the condition this cap exists for, so the order is chosen for
310
+ * how it fails rather than for how it reads.
311
+ *
312
+ * `t` is the last record's timestamp in the first refused batch, not a measured start of loss.
313
+ * The sidecar reports closure independently of that timestamp so later queries cannot hide it.
314
+ *
315
+ * The marker line is allowed past the ceiling. That is the whole overrun: ONE line, once, for the
316
+ * life of the ledger — not one per connection, which was this method's first shape.
317
+ */
318
+ async #closeAtCap(path, written, refused) {
319
+ const last = refused[refused.length - 1];
320
+ const loss = {
321
+ v: JOURNAL_FILE_VERSION,
322
+ channel: TruncationChannel.JOURNAL,
323
+ capBytes: this.#eventBytesCap,
324
+ bytesOnDisk: written,
325
+ droppedInBatch: refused.length,
326
+ at: last?.t ?? 0,
327
+ };
328
+ // In memory first, so this instance refuses everything after this point even if a write below
329
+ // throws. A half-closed ledger must still be a closed one.
330
+ this.#writeLoss = loss;
331
+ this.#lossLoaded = true;
332
+ log('journal_events_cap_reached', {
333
+ sessionId: this.#sessionId,
334
+ capBytes: this.#eventBytesCap,
335
+ writtenBytes: written,
336
+ droppedEvents: refused.length,
337
+ });
338
+ await this.#fs.writeFile(journalClosedPath(this.#root, this.#sessionId), `${JSON.stringify(loss)}\n`);
339
+ const declaration = {
340
+ t: loss.at ?? 0,
341
+ type: EventType.TRUNCATED,
342
+ sessionId: this.#sessionId,
343
+ data: { channel: TruncationChannel.JOURNAL, dropped: refused.length },
344
+ };
345
+ const line = `${JSON.stringify(declaration)}\n`;
346
+ await this.#fs.appendFile(path, line);
347
+ this.#eventBytes = written + Buffer.byteLength(line, 'utf8');
45
348
  }
46
349
  async appendAction(action) {
350
+ await this.#append(journalActionsPath(this.#root, this.#sessionId), `${JSON.stringify(action)}\n`);
351
+ }
352
+ /**
353
+ * The one write path, so both ledgers heal the same way.
354
+ *
355
+ * The session directory can VANISH under a live session: retention's count bound and byte budget
356
+ * both used to be able to take it, because a directory's mtime is frozen at creation and a long
357
+ * drive therefore looks like the oldest thing on disk. Both bounds now skip open sessions, but the
358
+ * directory can also be removed by a user, by a cleanup script, or by a peer daemon sweeping the
359
+ * same `.reticle/`, and the journal must not be one `rm` away from silently writing nothing for
360
+ * the rest of the session.
361
+ *
362
+ * Retry on ENOENT rather than dropping `#dirEnsured` and calling `mkdir` before every append:
363
+ * dropping the latch would still leave a window between the `mkdir` and the `appendFile` — small
364
+ * is not zero, and the failure it produces is exactly the invisible one — while costing a syscall
365
+ * on every batch, which is the cost batching exists to avoid. This pays nothing on the path that
366
+ * works and recovers on the one that does not.
367
+ */
368
+ async #append(path, text) {
47
369
  await this.#ensureDir();
48
- await this.#fs.appendFile(journalActionsPath(this.#root, this.#sessionId), `${JSON.stringify(action)}\n`);
370
+ try {
371
+ await this.#fs.appendFile(path, text);
372
+ }
373
+ catch (error) {
374
+ if (!this.#fs.isNotFound(error))
375
+ throw error;
376
+ this.#dirEnsured = false;
377
+ await this.#ensureDir();
378
+ await this.#fs.appendFile(path, text);
379
+ }
49
380
  }
50
381
  async readEvents() {
51
382
  const path = journalEventsPath(this.#root, this.#sessionId);
52
- if (this.#eventCache === undefined)
53
- this.#eventCache = [];
54
383
  // Fast path: read only the BYTES appended since the last read, so cost tracks the tail, not the
55
384
  // whole (unboundedly growing) file. Falls back to a whole-file read for a FileSystemPort that omits
56
385
  // readFileFrom (test stubs).
@@ -58,22 +387,37 @@ export class SessionJournal {
58
387
  if (readFrom !== undefined) {
59
388
  let chunk;
60
389
  try {
61
- chunk = await readFrom(path, this.#eventBytesConsumed);
390
+ chunk = await readFrom(path, this.#eventBytesConsumed, this.#maxReadBytes);
62
391
  }
63
392
  catch (error) {
64
393
  if (this.#fs.isNotFound(error))
65
- return this.#eventCache.slice();
394
+ return this.#liveEvents();
66
395
  throw error;
67
396
  }
68
- // Shrink/rotation guard (not done today): if the file is smaller than what we consumed, the offset
69
- // is meaningless — reset and re-read from 0.
397
+ // Shrink guard: if the file is smaller than what we consumed, the offset is meaningless — reset
398
+ // and re-read from 0. The writer never rotates (see appendEvents), so this is defence against an
399
+ // outside hand on the file, not a path the journal itself takes. What was already declared lost
400
+ // stays lost: the records are no less absent for the file having been rewritten under us.
70
401
  if (chunk.size < this.#eventBytesConsumed) {
71
- this.#eventCache = [];
402
+ this.#resetCache();
72
403
  this.#eventBytesConsumed = 0;
73
- chunk = await readFrom(path, 0);
404
+ chunk = await readFrom(path, 0, this.#maxReadBytes);
74
405
  }
75
- this.#eventBytesConsumed += this.#ingestTail(chunk.text);
76
- return this.#eventCache.slice();
406
+ // A port that implements the ceiling says where the text really starts; one that does not
407
+ // never skips, so its start IS the offset we asked for.
408
+ const from = chunk.from ?? this.#eventBytesConsumed;
409
+ const skipped = Math.max(0, from - this.#eventBytesConsumed);
410
+ this.#ingestTail(chunk.text);
411
+ if (skipped > 0)
412
+ this.#noteCut(skipped);
413
+ // Resume at the end of the last COMPLETE line, counted forward over the bytes that actually
414
+ // ARRIVED. Not from `chunk.size`: that is the file's end as `fstat` reported it, and a read
415
+ // truncated or cut short returns less — a cursor there would skip every unread byte, silently
416
+ // and permanently. Counting forward is exact because the text starts at a record boundary, so
417
+ // its prefix up to the last newline is whole records and no replacement char can widen it.
418
+ this.#eventBytesConsumed =
419
+ from + Buffer.byteLength(chunk.text.slice(0, chunk.text.lastIndexOf('\n') + 1), 'utf8');
420
+ return this.#liveEvents();
77
421
  }
78
422
  let text;
79
423
  try {
@@ -81,14 +425,14 @@ export class SessionJournal {
81
425
  }
82
426
  catch (error) {
83
427
  if (this.#fs.isNotFound(error))
84
- return this.#eventCache.slice();
428
+ return this.#liveEvents();
85
429
  throw error;
86
430
  }
87
431
  // Fallback (whole-file) path: #eventCharsConsumed is a UTF-16 offset into the whole text. Shrink
88
432
  // guard, then ingest the un-consumed tail up to its last newline (see #ingestTail for the
89
433
  // partial-line rationale).
90
434
  if (text.length < this.#eventCharsConsumed) {
91
- this.#eventCache = [];
435
+ this.#resetCache();
92
436
  this.#eventCharsConsumed = 0;
93
437
  }
94
438
  const end = text.lastIndexOf('\n') + 1;
@@ -96,22 +440,36 @@ export class SessionJournal {
96
440
  this.#ingestTail(text.slice(this.#eventCharsConsumed, end));
97
441
  this.#eventCharsConsumed = end;
98
442
  }
99
- return this.#eventCache.slice();
443
+ return this.#liveEvents();
444
+ }
445
+ /** The retained events, as a copy — a caller mutating the answer must not corrupt the cache. */
446
+ #liveEvents() {
447
+ return this.#eventCache
448
+ .slice(this.#cacheHead)
449
+ .filter((event) => event !== undefined);
450
+ }
451
+ /** Drop everything parsed so far. Declared loss is NOT reset: those records are still absent. */
452
+ #resetCache() {
453
+ this.#eventCache = [];
454
+ this.#eventCost = [];
455
+ this.#cacheHead = 0;
456
+ this.#retainedBytes = 0;
100
457
  }
101
458
  /**
102
459
  * Parse complete ('\n'-terminated) JSON event lines from `tail` into #eventCache, stopping at the LAST
103
460
  * newline. A concurrent append can be observed mid-record (reads and writes run on separate libuv
104
461
  * threads), and consuming a partial trailing line would splice its tail onto the next read's head, fail
105
- * to parse, and drop that event forever — so the partial tail is left for the next read. Returns the
106
- * UTF-8 BYTES consumed, which the bounded-read path uses to advance its byte offset.
462
+ * to parse, and drop that event forever — so the partial tail is left for the next read.
463
+ *
464
+ * `tail` always begins at a record boundary: when a ceiling moves a read's start, `readFileFrom`
465
+ * drops the straddled record in BYTES before decoding, so nothing here has to guess whether a
466
+ * leading line is whole. Enforces the retention bound before returning, so no caller can forget to.
107
467
  */
108
468
  #ingestTail(tail) {
109
- const cache = this.#eventCache ?? (this.#eventCache = []);
110
469
  const end = tail.lastIndexOf('\n') + 1;
111
470
  if (0 === end)
112
- return 0; // no complete line yet
113
- const complete = tail.slice(0, end);
114
- for (const line of complete.split('\n')) {
471
+ return; // no complete line yet
472
+ for (const line of tail.slice(0, end).split('\n')) {
115
473
  if (0 === line.length)
116
474
  continue;
117
475
  let parsed;
@@ -122,11 +480,90 @@ export class SessionJournal {
122
480
  continue;
123
481
  }
124
482
  const result = ReticleEventSchema.safeParse(parsed);
125
- if (result.success)
126
- cache.push(result.data);
483
+ if (result.success) {
484
+ this.#eventCache.push(result.data);
485
+ // Its cost in the file, newline included — so the retained bound is in the same unit the
486
+ // read ceiling and the loss report are.
487
+ const cost = Buffer.byteLength(line, 'utf8') + 1;
488
+ this.#eventCost.push(cost);
489
+ this.#retainedBytes += cost;
490
+ }
127
491
  }
128
- return Buffer.byteLength(complete, 'utf8');
492
+ this.#evictToRetentionBound();
493
+ }
494
+ /**
495
+ * Hold the parse-cache to its bounds, oldest first, and count what that cost.
496
+ *
497
+ * Eviction advances a HEAD index rather than shifting, and the dead prefix is compacted away only
498
+ * once it dominates — the same amortized-O(1) shape the ring buffer uses. A `splice(0, n)` per
499
+ * read looks cheaper and is not: `queryEvents` re-reads on every observe/network/console call, so
500
+ * one O(n) memmove per read over a cache the size of the bound is quadratic across a session.
501
+ *
502
+ * Evicted payload references are cleared immediately. Empty slots can remain until compaction,
503
+ * but they must not keep the discarded snapshots reachable.
504
+ *
505
+ * The newest record always survives the BYTE bound, even if it alone exceeds it. Evicting it would
506
+ * empty the cache on every read and turn the fall-through into a permanent silent nothing.
507
+ */
508
+ #evictToRetentionBound() {
509
+ while (this.#eventCache.length - this.#cacheHead > this.#maxRetainedEvents ||
510
+ (this.#retainedBytes > this.#maxRetainedBytes &&
511
+ this.#eventCache.length - this.#cacheHead > 1)) {
512
+ const victim = this.#eventCache[this.#cacheHead];
513
+ const cost = this.#eventCost[this.#cacheHead] ?? 0;
514
+ this.#retainedBytes -= cost;
515
+ this.#droppedBytes += cost;
516
+ this.#droppedEvents += 1;
517
+ // Exact, unlike a byte cut: this record was parsed, so the millisecond it was lost at is known.
518
+ if (victim !== undefined)
519
+ this.#lostThroughT = Math.max(this.#lostThroughT, victim.t);
520
+ // Release the payload now. Waiting for slot compaction keeps evicted snapshots strongly
521
+ // reachable, so a short returned list alone would not bound retained evidence in memory.
522
+ this.#eventCache[this.#cacheHead] = undefined;
523
+ this.#cacheHead += 1;
524
+ }
525
+ if (this.#cacheHead > CACHE_COMPACT_AT && this.#cacheHead * 2 >= this.#eventCache.length) {
526
+ this.#eventCache = this.#eventCache.slice(this.#cacheHead);
527
+ this.#eventCost = this.#eventCost.slice(this.#cacheHead);
528
+ this.#cacheHead = 0;
529
+ }
530
+ }
531
+ /**
532
+ * Skipped bytes have unknown timestamps: page clocks can reset while the journal id survives.
533
+ * A surviving tail cannot bound records that were never parsed. Later intact reads cannot
534
+ * reconstruct that history; finite bounds apply only to parsed cache evictions.
535
+ */
536
+ #noteCut(skippedBytes) {
537
+ this.#droppedBytes += skippedBytes;
538
+ this.#lostThroughT = Number.POSITIVE_INFINITY;
129
539
  }
540
+ /**
541
+ * What these reads could not produce, or `undefined` when nothing was lost.
542
+ *
543
+ * A report beside the value, not inside it: the events keep the exact shape their consumers parse,
544
+ * and a caller holding a short list can still tell it apart from a complete one. `Session.lostSince`
545
+ * is what carries this to a verdict.
546
+ */
547
+ readLoss() {
548
+ if (0 === this.#droppedBytes && 0 === this.#droppedEvents)
549
+ return undefined;
550
+ const bounded = Number.isFinite(this.#lostThroughT);
551
+ return {
552
+ droppedBytes: this.#droppedBytes,
553
+ droppedEvents: this.#droppedEvents,
554
+ ...(bounded ? { lostThroughT: this.#lostThroughT } : {}),
555
+ note: `partial — this answer is missing at least ${String(this.#droppedBytes)} byte(s) of this session's durable journal, ` +
556
+ `including ${String(this.#droppedEvents)} record(s) evicted after parsing, ` +
557
+ (bounded
558
+ ? `so no window opened at or before t=${String(this.#lostThroughT)} can be answered from it; anything earlier survives only in the ring buffer.`
559
+ : 'and the skipped records have unknown timestamps, so no window this journal answers is known complete.'),
560
+ };
561
+ }
562
+ /**
563
+ * The action ledger, whole — the one read here that is still UNBOUNDED, on purpose. See the class
564
+ * note: it holds one record per tool call rather than one per observed event, and bounding it
565
+ * needs a loss channel that does not impeach page verdicts.
566
+ */
130
567
  async readActions() {
131
568
  return this.#readLines(journalActionsPath(this.#root, this.#sessionId), JournalActionSchema);
132
569
  }
@@ -165,3 +602,23 @@ export class SessionJournal {
165
602
  return out;
166
603
  }
167
604
  }
605
+ /**
606
+ * The closure report a session's sidecar holds — details when it can be read, the bare fact when not.
607
+ *
608
+ * Never `undefined`: this is only ever called on a file that EXISTS, and the existence is the claim.
609
+ * Unparseable bytes cost the numbers, not the closure, so the fallback states the channel and stops
610
+ * there. Guessing the rest from the current ceiling would put a value that was never measured into
611
+ * the one field a reader consults to decide whether evidence is complete.
612
+ */
613
+ function parseClosureReport(text) {
614
+ const closed = { v: JOURNAL_FILE_VERSION, channel: TruncationChannel.JOURNAL };
615
+ let parsed;
616
+ try {
617
+ parsed = JSON.parse(text);
618
+ }
619
+ catch {
620
+ return closed;
621
+ }
622
+ const result = JournalWriteLossSchema.safeParse(parsed);
623
+ return result.success ? result.data : closed;
624
+ }
@@ -0,0 +1,3 @@
1
+ import { type ProjectId } from '@reticlehq/core';
2
+ import { type ArtifactRoot } from './artifact-root.js';
3
+ export declare function artifactRootResolver(daemonRoot: string): (projectId: ProjectId | undefined, origin?: string) => ArtifactRoot;