@reticlehq/server 3.8.1 → 3.9.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 (231) hide show
  1. package/LICENSE +201 -104
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/bin/launch.js +13 -0
  5. package/bin/package.json +3 -0
  6. package/bin/reticle.js +66 -8
  7. package/dist/chromium-launch-options.d.ts +25 -0
  8. package/dist/chromium-launch-options.js +21 -1
  9. package/dist/command/cli/adhoc-verdict.js +34 -7
  10. package/dist/command/cli/answers/gate-exit.d.ts +6 -2
  11. package/dist/command/cli/answers/gate-exit.js +12 -2
  12. package/dist/command/cli/ci-hint.d.ts +44 -0
  13. package/dist/command/cli/ci-hint.js +72 -0
  14. package/dist/command/cli/cli-doctor.d.ts +6 -1
  15. package/dist/command/cli/cli-doctor.js +45 -4
  16. package/dist/command/cli/cli-flow-commands.d.ts +7 -1
  17. package/dist/command/cli/cli-flow-commands.js +39 -17
  18. package/dist/command/cli/cli-kill.d.ts +21 -0
  19. package/dist/command/cli/cli-kill.js +46 -3
  20. package/dist/command/cli/cli-parse.d.ts +5 -2
  21. package/dist/command/cli/cli-parse.js +10 -5
  22. package/dist/command/cli/cli-usage.js +3 -2
  23. package/dist/command/cli/cli-verify.d.ts +6 -1
  24. package/dist/command/cli/cli-verify.js +32 -13
  25. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
  26. package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
  27. package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
  28. package/dist/command/cli/doctor/doctor-fix.js +89 -0
  29. package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
  30. package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
  31. package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
  32. package/dist/command/cli/doctor/doctor-rows.js +4 -0
  33. package/dist/command/cli/doctor-repairs.d.ts +64 -0
  34. package/dist/command/cli/doctor-repairs.js +214 -0
  35. package/dist/command/cli/drive/drive-command.js +3 -7
  36. package/dist/command/cli/first-use-checks.d.ts +41 -0
  37. package/dist/command/cli/first-use-checks.js +127 -0
  38. package/dist/command/cli/first-verdict-node.d.ts +3 -0
  39. package/dist/command/cli/first-verdict-node.js +189 -0
  40. package/dist/command/cli/first-verdict.d.ts +126 -0
  41. package/dist/command/cli/first-verdict.js +267 -0
  42. package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
  43. package/dist/command/cli/launch/sdk-version-match.js +3 -2
  44. package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
  45. package/dist/command/cli/mcp-command.js +13 -4
  46. package/dist/command/cli/ports/port-holder.d.ts +2 -0
  47. package/dist/command/cli/ports/port-holder.js +11 -3
  48. package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
  49. package/dist/command/cli/setup-mcp-cli.js +98 -23
  50. package/dist/command/cli/status/status-lines.d.ts +1 -0
  51. package/dist/command/cli/status/status-lines.js +50 -17
  52. package/dist/command/cli/status-fields.js +17 -3
  53. package/dist/command/cli/try-command.d.ts +7 -0
  54. package/dist/command/cli/try-command.js +11 -3
  55. package/dist/command/cli.js +70 -33
  56. package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
  57. package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
  58. package/dist/command/daemon/daemon-argv.d.ts +9 -0
  59. package/dist/command/daemon/daemon-argv.js +11 -0
  60. package/dist/command/daemon/daemon.d.ts +10 -3
  61. package/dist/command/daemon/daemon.js +17 -6
  62. package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
  63. package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
  64. package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
  65. package/dist/command/daemon/lifetime/death-cause.js +130 -0
  66. package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
  67. package/dist/command/daemon/lifetime/heartbeat.js +4 -13
  68. package/dist/command/daemon/port-state.d.ts +14 -0
  69. package/dist/command/daemon/port-state.js +31 -0
  70. package/dist/command/setup/agent-configs.d.ts +2 -0
  71. package/dist/command/setup/agent-configs.js +42 -15
  72. package/dist/command/setup/agent-next-steps.d.ts +77 -0
  73. package/dist/command/setup/agent-next-steps.js +284 -0
  74. package/dist/command/setup/agent-writer.js +11 -6
  75. package/dist/command/setup/init/init-host.js +2 -0
  76. package/dist/command/setup/init/init-runtime.d.ts +6 -1
  77. package/dist/command/setup/init/init-runtime.js +57 -21
  78. package/dist/command/setup/mcp-launcher.d.ts +3 -0
  79. package/dist/command/setup/mcp-launcher.js +11 -0
  80. package/dist/command/setup/setup-command.js +30 -12
  81. package/dist/command/setup/setup-mcp.d.ts +8 -0
  82. package/dist/command/setup/setup-mcp.js +47 -20
  83. package/dist/command/setup/start-dev-server.d.ts +8 -0
  84. package/dist/command/setup/start-dev-server.js +43 -0
  85. package/dist/command/version/identity/server-version.d.ts +2 -0
  86. package/dist/command/version/identity/server-version.js +2 -0
  87. package/dist/command/version/version-skew.d.ts +7 -0
  88. package/dist/command/version/version-skew.js +13 -2
  89. package/dist/features/license/license.d.ts +11 -71
  90. package/dist/features/license/license.js +43 -136
  91. package/dist/features/visual/desktop-capture.d.ts +33 -0
  92. package/dist/features/visual/desktop-capture.js +94 -0
  93. package/dist/features/visual/step-shots.js +139 -35
  94. package/dist/features/visual/visual-tools.d.ts +0 -18
  95. package/dist/features/visual/visual-tools.js +2 -81
  96. package/dist/import-graph.js +5 -6
  97. package/dist/index.d.ts +3 -1
  98. package/dist/index.js +14 -5
  99. package/dist/judgement/runs/run-ledger.d.ts +7 -5
  100. package/dist/judgement/runs/run-ledger.js +98 -69
  101. package/dist/judgement/runs/verification-sync.d.ts +6 -3
  102. package/dist/judgement/runs/verification-sync.js +21 -10
  103. package/dist/language/flows/flow-store-for-session.js +7 -5
  104. package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
  105. package/dist/language/flows/recording/tape/recordings.js +41 -44
  106. package/dist/language/flows/replay.d.ts +17 -7
  107. package/dist/language/flows/replay.js +95 -44
  108. package/dist/launch-chromium.d.ts +1 -0
  109. package/dist/launch-chromium.js +49 -12
  110. package/dist/memory/cloud/sync-screenshots.js +6 -1
  111. package/dist/portal/bridge/bridge.d.ts +13 -4
  112. package/dist/portal/bridge/bridge.js +10 -9
  113. package/dist/portal/input/drive-error.d.ts +6 -0
  114. package/dist/portal/input/drive-error.js +9 -0
  115. package/dist/portal/input/network-mock.js +3 -2
  116. package/dist/portal/input/real-input-press.d.ts +34 -0
  117. package/dist/portal/input/real-input-press.js +150 -0
  118. package/dist/portal/input/real-input.d.ts +21 -23
  119. package/dist/portal/input/real-input.js +153 -194
  120. package/dist/portal/input/viewport-tools.js +6 -3
  121. package/dist/portal/pool/browser-pool.d.ts +5 -1
  122. package/dist/portal/pool/browser-pool.js +32 -38
  123. package/dist/portal/pool/context-permissions.d.ts +20 -17
  124. package/dist/portal/pool/context-permissions.js +63 -1
  125. package/dist/portal/pool/csp-widen.d.ts +62 -0
  126. package/dist/portal/pool/csp-widen.js +132 -0
  127. package/dist/portal/pool/lease-page-error.d.ts +29 -0
  128. package/dist/portal/pool/lease-page-error.js +18 -0
  129. package/dist/portal/pool/page-error-journal.d.ts +57 -0
  130. package/dist/portal/pool/page-error-journal.js +181 -0
  131. package/dist/portal/pool/playwright-launcher.js +21 -8
  132. package/dist/portal/pool/pool-contract.d.ts +22 -0
  133. package/dist/portal/pool/pool-lease.d.ts +35 -2
  134. package/dist/portal/pool/pool-lease.js +23 -0
  135. package/dist/portal/pool/zero-install.js +69 -1
  136. package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
  137. package/dist/portal/session/dev-server/dev-command.js +56 -8
  138. package/dist/portal/session/no-session-diagnosis.js +10 -3
  139. package/dist/portal/session/no-session-next-action.d.ts +12 -0
  140. package/dist/portal/session/no-session-next-action.js +44 -4
  141. package/dist/portal/session/no-session-watch.js +3 -1
  142. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  143. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  144. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  145. package/dist/portal/session/recorded-gaps.js +5 -2
  146. package/dist/portal/session/session-manager.js +40 -21
  147. package/dist/portal/session/session.js +4 -3
  148. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  149. package/dist/portal/session/zero-install-first-call.js +34 -2
  150. package/dist/start-wiring.d.ts +29 -2
  151. package/dist/start-wiring.js +73 -9
  152. package/dist/surface/mcp/mcp-proxy.js +10 -2
  153. package/dist/surface/mcp/mcp.js +5 -3
  154. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  155. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  156. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  157. package/dist/surface/tools/act/act-preflight.js +37 -17
  158. package/dist/surface/tools/act/act-target.js +3 -1
  159. package/dist/surface/tools/act/after-match.d.ts +7 -0
  160. package/dist/surface/tools/act/after-match.js +33 -0
  161. package/dist/surface/tools/act/resolve-target.d.ts +2 -0
  162. package/dist/surface/tools/act/resolve-target.js +35 -8
  163. package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
  164. package/dist/surface/tools/act/sequence-grade.js +8 -1
  165. package/dist/surface/tools/act-sequence-tool.js +3 -0
  166. package/dist/surface/tools/act-tools.js +12 -12
  167. package/dist/surface/tools/assert/assert-verdict.js +28 -1
  168. package/dist/surface/tools/browser-tools.js +75 -7
  169. package/dist/surface/tools/dynamic-tools.js +2 -1
  170. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  171. package/dist/surface/tools/empty-session-list.js +4 -2
  172. package/dist/surface/tools/error-recovery.js +43 -22
  173. package/dist/surface/tools/invoke-tool.js +27 -20
  174. package/dist/surface/tools/lease-hint.d.ts +7 -0
  175. package/dist/surface/tools/lease-hint.js +10 -3
  176. package/dist/surface/tools/lease-readiness.d.ts +21 -0
  177. package/dist/surface/tools/lease-readiness.js +47 -0
  178. package/dist/surface/tools/lease-seed.d.ts +2 -0
  179. package/dist/surface/tools/lease-seed.js +2 -1
  180. package/dist/surface/tools/lease-tools.js +43 -15
  181. package/dist/surface/tools/real-input-attempt.js +70 -31
  182. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  183. package/dist/surface/tools/repeat-hint.js +171 -0
  184. package/dist/surface/tools/run-report.d.ts +2 -2
  185. package/dist/surface/tools/run-report.js +27 -15
  186. package/dist/surface/tools/tool-kit.d.ts +12 -0
  187. package/dist/surface/tools/tool-kit.js +7 -0
  188. package/dist/surface/tools/tools.js +26 -13
  189. package/dist/telemetry/argument-shape.js +16 -7
  190. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  191. package/dist/telemetry/cli-telemetry.js +15 -0
  192. package/dist/telemetry/init-telemetry.d.ts +2 -1
  193. package/dist/telemetry/init-telemetry.js +47 -2
  194. package/dist/telemetry/install-trace.js +23 -4
  195. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  196. package/dist/telemetry/onboarding-funnel.js +25 -3
  197. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  198. package/dist/telemetry/proxy-telemetry.js +2 -2
  199. package/dist/telemetry/session-metrics.d.ts +1 -1
  200. package/dist/telemetry/session-metrics.js +67 -27
  201. package/dist/telemetry/telemetry.d.ts +3 -1
  202. package/dist/telemetry/telemetry.js +60 -28
  203. package/dist/telemetry/tool-refused.d.ts +2 -2
  204. package/dist/telemetry/tool-refused.js +9 -2
  205. package/docs/architecture.md +3 -6
  206. package/docs/cli/doctor.mdx +41 -2
  207. package/docs/cli/help.mdx +4 -2
  208. package/docs/cli/license.mdx +4 -5
  209. package/docs/cli/setup.mdx +13 -2
  210. package/docs/cli/status.mdx +4 -0
  211. package/docs/cli.mdx +1 -1
  212. package/docs/deploy-checks.md +25 -0
  213. package/docs/desktop.mdx +1 -1
  214. package/docs/enterprise.md +15 -20
  215. package/docs/environment.mdx +1 -0
  216. package/docs/first-drive.md +1 -1
  217. package/docs/install-manual.mdx +16 -1
  218. package/docs/packages/engine.mdx +1 -1
  219. package/docs/packages/react.mdx +14 -2
  220. package/docs/packages/server.mdx +1 -1
  221. package/docs/packages/tauri.mdx +1 -1
  222. package/docs/packages/test.mdx +1 -1
  223. package/docs/packages/vite-plugin.mdx +1 -0
  224. package/docs/packages.mdx +1 -1
  225. package/docs/platform-integration.md +1 -1
  226. package/docs/skill.md +1 -1
  227. package/docs/telemetry.md +4 -2
  228. package/package.json +14 -10
  229. package/LICENSE-ENTERPRISE +0 -37
  230. package/dist/features/ee/audit-log.d.ts +0 -14
  231. package/dist/features/ee/audit-log.js +0 -11
@@ -14,9 +14,10 @@
14
14
  * nothing at all, the next one is awaited, so the agent is told `{ error }` instead of a path to a
15
15
  * file that will never exist.
16
16
  *
17
- * ponytail: the desktop shell's own window capture is not a source here. It returns a device-pixel
18
- * PNG, and the daemon ships no JPEG encoder to shrink it to the step format; an Electron renderer
19
- * draws itself fine in-page. Add it when the shells can hand back a JPEG.
17
+ * A page SDK before 3.7 has no `render`. When such a page declares the desktop shell's `capture`,
18
+ * that command is asked instead. Its answer is a device-pixel PNG, and the daemon ships no JPEG
19
+ * encoder, so that picture is kept as a `.png` and stays on this machine (the platform takes only
20
+ * JPEG). A page that declares neither command is not asked: the result says to update its SDK.
20
21
  */
21
22
  import { createHash } from 'node:crypto';
22
23
  import { join } from 'node:path';
@@ -25,7 +26,12 @@ import { runIdOfCall } from '../../judgement/runs/drive-run.js';
25
26
  import { noteLanding } from '../../judgement/runs/artifact/run-store.js';
26
27
  import { isValidRunId, stepShotDir } from '../../memory/project/dir/reticle-dir.js';
27
28
  import { UPLOAD_TEXT_MAX } from '@reticlehq/core/artifacts';
29
+ import { sdkBehindNote } from '../../command/version/version-skew.js';
30
+ import { desktopCapture } from './desktop-capture.js';
28
31
  const JPEG_EXT = '.jpg';
32
+ const PNG_EXT = '.png';
33
+ /** Why a page that declared neither `render` nor `capture` was not asked for a picture. */
34
+ const NO_PAGE_CAPTURE = 'this page declared no command that draws it (neither "render" nor "capture"), so nothing was sent';
29
35
  const TOOL_PREFIX = /^reticle_/;
30
36
  const SEQ_WIDTH = 4;
31
37
  /** A prior capture on this tab produced nothing — see the header. */
@@ -34,19 +40,58 @@ const NOTHING = 'none';
34
40
  const CDP_QUALITY = Math.round(STEP_SCREENSHOT.JPEG_QUALITY * 100);
35
41
  /** What worked last on each tab: the next result names that source. */
36
42
  const lastSource = new Map();
43
+ /**
44
+ * The file type of the last picture of each tab: the next path is named with it.
45
+ *
46
+ * ponytail: a tab that changes type between two steps gets one predicted path with the old type;
47
+ * the run record names the real file. Wait for the capture if that ever matters.
48
+ */
49
+ const lastExt = new Map();
37
50
  /** The next sequence number per run directory, read from its record the first time. */
38
51
  const nextSeq = new Map();
52
+ /** The PNG signature's first four bytes, and where the IHDR chunk keeps the width and height. */
53
+ const PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47];
54
+ const PNG_WIDTH_AT = 16;
55
+ const PNG_HEIGHT_AT = 20;
56
+ /** Width and height from a PNG's header, or undefined when the bytes are not a PNG. */
57
+ function pngSize(bytes) {
58
+ if (bytes.length < PNG_HEIGHT_AT + 4 || PNG_MAGIC.some((b, i) => b !== bytes[i])) {
59
+ return undefined;
60
+ }
61
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
62
+ const width = view.getUint32(PNG_WIDTH_AT);
63
+ const height = view.getUint32(PNG_HEIGHT_AT);
64
+ return width > 0 && height > 0 ? { width, height } : undefined;
65
+ }
66
+ /**
67
+ * The command that asks this page for its own picture, or undefined when it declared none.
68
+ *
69
+ * `render` came in SDK 3.7. An older SDK declares only the desktop shell's `capture`. A page that
70
+ * declared nothing is too old to say, so it is asked for `render` as before.
71
+ */
72
+ function pageCaptureCommand(session) {
73
+ const declared = session.commands;
74
+ if (declared === undefined || declared.includes(ReticleCommand.RENDER)) {
75
+ return ReticleCommand.RENDER;
76
+ }
77
+ if (declared.includes(ReticleCommand.CAPTURE)) {
78
+ return ReticleCommand.CAPTURE;
79
+ }
80
+ return undefined;
81
+ }
39
82
  /**
40
83
  * Width and height from a JPEG's frame header, or undefined when the bytes are not a JPEG.
41
84
  * Read rather than trusted: the run record and the upload both state the size.
42
85
  */
43
86
  export function jpegSize(bytes) {
44
- if (bytes.length < 4 || 0xff !== bytes[0] || 0xd8 !== bytes[1])
87
+ if (bytes.length < 4 || 0xff !== bytes[0] || 0xd8 !== bytes[1]) {
45
88
  return undefined;
89
+ }
46
90
  let i = 2;
47
91
  while (i + 9 < bytes.length) {
48
- if (0xff !== bytes[i])
92
+ if (0xff !== bytes[i]) {
49
93
  return undefined;
94
+ }
50
95
  const marker = bytes[i + 1] ?? 0;
51
96
  const length = ((bytes[i + 2] ?? 0) << 8) | (bytes[i + 3] ?? 0);
52
97
  // The start-of-frame markers carry the size; 0xC4, 0xC8 and 0xCC share the range and are not frames.
@@ -63,22 +108,32 @@ export function jpegSize(bytes) {
63
108
  async function browserPixels(deps, session) {
64
109
  const opts = { jpegQuality: CDP_QUALITY, cssScale: true };
65
110
  const leased = await deps.pool?.screenshotLease(session.id, opts);
66
- if (leased !== undefined)
111
+ if (leased !== undefined) {
67
112
  return leased;
113
+ }
114
+ // A lease capture that failed still has a reason. Leaving it on the floor made the only text
115
+ // the page's "I cannot draw", which says nothing about the browser that was asked first.
116
+ const failed = deps.pool?.leaseCaptureError(session.id);
117
+ if (failed !== undefined) {
118
+ throw new Error(failed);
119
+ }
68
120
  // Only a provider driving THIS tab: a launched provider photographs the one page it owns whatever
69
121
  // url it is handed, which is how a screenshot of one tab was once filed under another (#1407).
70
- if (true !== (await deps.realInput?.isAvailableFor(session.url)))
122
+ if (true !== (await deps.realInput?.isAvailableFor(session.url))) {
71
123
  return undefined;
124
+ }
72
125
  return deps.realInput?.screenshot?.(session.url, opts);
73
126
  }
74
127
  /** The page drawn by the SDK. The answer is base64 in chunks — see `ReticleCommand.RENDER`. */
75
128
  async function renderedPixels(session) {
76
129
  const res = await session.command(ReticleCommand.RENDER, {});
77
- if (!res.ok)
130
+ if (!res.ok) {
78
131
  return res.error ?? 'the page did not answer';
132
+ }
79
133
  const r = asRecord(res.result);
80
- if (true !== r['ok'])
134
+ if (true !== r['ok']) {
81
135
  return 'string' === typeof r['reason'] ? r['reason'] : 'render failed';
136
+ }
82
137
  const chunks = Array.isArray(r['chunks']) ? r['chunks'] : [];
83
138
  return new Uint8Array(Buffer.from(chunks.filter((c) => 'string' === typeof c).join(''), 'base64'));
84
139
  }
@@ -88,20 +143,47 @@ async function capture(deps, session) {
88
143
  try {
89
144
  const real = await browserPixels(deps, session);
90
145
  const size = real === undefined ? undefined : jpegSize(real);
91
- if (real !== undefined && size !== undefined)
92
- return { bytes: real, source: ScreenshotSource.BROWSER, ...size };
146
+ if (real !== undefined && size !== undefined) {
147
+ return { bytes: real, source: ScreenshotSource.BROWSER, ext: JPEG_EXT, ...size };
148
+ }
93
149
  }
94
150
  catch (error) {
95
151
  reasons.push(error instanceof Error ? error.message : String(error));
96
152
  }
153
+ const command = pageCaptureCommand(session);
154
+ if (command === undefined) {
155
+ // Nothing is sent: the page said it has no command for this.
156
+ reasons.push(NO_PAGE_CAPTURE);
157
+ }
158
+ if (command !== ReticleCommand.RENDER) {
159
+ if (command === ReticleCommand.CAPTURE) {
160
+ const png = (await desktopCapture(deps, session.id, false).catch(() => undefined))?.png;
161
+ const size = png === undefined ? undefined : pngSize(png);
162
+ if (png !== undefined && size !== undefined) {
163
+ return { bytes: png, source: ScreenshotSource.BROWSER, ext: PNG_EXT, ...size };
164
+ }
165
+ reasons.push('the desktop shell did not capture its window');
166
+ }
167
+ const behind = sdkBehindNote(session.sdkVersion);
168
+ if (behind !== undefined) {
169
+ reasons.push(behind);
170
+ }
171
+ return `no screenshot: ${reasons.join('; ')}`;
172
+ }
97
173
  try {
98
174
  const drawn = await renderedPixels(session);
99
- if ('string' === typeof drawn)
175
+ if ('string' === typeof drawn) {
100
176
  reasons.push(`the page could not draw itself: ${drawn}`);
177
+ const behind = sdkBehindNote(session.sdkVersion);
178
+ if (behind !== undefined) {
179
+ reasons.push(behind);
180
+ }
181
+ }
101
182
  else {
102
183
  const size = jpegSize(drawn);
103
- if (size !== undefined)
104
- return { bytes: drawn, source: ScreenshotSource.RENDERED, ...size };
184
+ if (size !== undefined) {
185
+ return { bytes: drawn, source: ScreenshotSource.RENDERED, ext: JPEG_EXT, ...size };
186
+ }
105
187
  reasons.push('the page drew something that is not a JPEG');
106
188
  }
107
189
  }
@@ -113,12 +195,15 @@ async function capture(deps, session) {
113
195
  /** Which source this tab's next result names, or undefined when the last capture got nothing. */
114
196
  async function predict(deps, session) {
115
197
  const last = lastSource.get(session.id);
116
- if (NOTHING === last)
198
+ if (NOTHING === last) {
117
199
  return undefined;
118
- if (last !== undefined)
200
+ }
201
+ if (last !== undefined) {
119
202
  return last;
120
- if (true === deps.pool?.leasedSessionIds().includes(session.id))
203
+ }
204
+ if (true === deps.pool?.leasedSessionIds().includes(session.id)) {
121
205
  return ScreenshotSource.BROWSER;
206
+ }
122
207
  const driven = deps.realInput?.screenshot !== undefined &&
123
208
  true === (await deps.realInput.isAvailableFor(session.url));
124
209
  return driven ? ScreenshotSource.BROWSER : ScreenshotSource.RENDERED;
@@ -168,8 +253,9 @@ async function shoot(deps, session, tool, { step, verdict }) {
168
253
  // Read before anything is awaited: the action this call's verdict was recorded on.
169
254
  const actionId = true === verdict ? session.lastActionId : undefined;
170
255
  const runId = runIdOfCall(session.id);
171
- if (!isValidRunId(runId))
256
+ if (!isValidRunId(runId)) {
172
257
  return { error: `no screenshot: unusable run id ${runId}` };
258
+ }
173
259
  const dir = stepShotDir(root, runId);
174
260
  void filmTab(deps, session, dir);
175
261
  const seq = await allocate(deps.fs, dir);
@@ -185,14 +271,17 @@ async function shoot(deps, session, tool, { step, verdict }) {
185
271
  // Started before anything else is awaited: the picture is of THIS moment.
186
272
  const taken = capture(deps, session).then((pixels) => fresh && 'string' === typeof pixels ? capture(deps, session) : pixels);
187
273
  const predicted = fresh ? undefined : await predict(deps, session);
188
- const file = `${String(seq).padStart(SEQ_WIDTH, '0')}-${tool.replace(TOOL_PREFIX, '')}${JPEG_EXT}`;
189
- const path = join(dir, file);
274
+ const name = (ext) => `${String(seq).padStart(SEQ_WIDTH, '0')}-${tool.replace(TOOL_PREFIX, '')}${ext}`;
275
+ const predictedPath = join(dir, name(lastExt.get(session.id) ?? JPEG_EXT));
190
276
  const land = taken.then(async (pixels) => {
191
277
  if ('string' === typeof pixels) {
192
278
  lastSource.set(session.id, NOTHING);
193
279
  return;
194
280
  }
195
281
  lastSource.set(session.id, pixels.source);
282
+ lastExt.set(session.id, pixels.ext);
283
+ const file = name(pixels.ext);
284
+ const path = join(dir, file);
196
285
  try {
197
286
  await deps.fs.mkdir(dir);
198
287
  await deps.fs.writeFileBytes(path, pixels.bytes);
@@ -215,12 +304,15 @@ async function shoot(deps, session, tool, { step, verdict }) {
215
304
  }
216
305
  });
217
306
  noteLanding(dir, land);
218
- if (predicted !== undefined)
219
- return { path, source: predicted, pending: true };
307
+ if (predicted !== undefined) {
308
+ return { path: predictedPath, source: predicted, pending: true };
309
+ }
220
310
  // Nothing worked last time: wait, and tell the truth either way.
221
311
  const pixels = await taken;
222
312
  await land;
223
- return 'string' === typeof pixels ? { error: pixels } : { path, source: pixels.source };
313
+ return 'string' === typeof pixels
314
+ ? { error: pixels }
315
+ : { path: join(dir, name(pixels.ext)), source: pixels.source };
224
316
  }
225
317
  /** The replay hook that gives every reported flow step its screenshot, filed as `<flow>#<step>`. */
226
318
  export function replayStepShooter(deps, sessionId) {
@@ -267,8 +359,9 @@ const films = new Map();
267
359
  const framesIn = new Map();
268
360
  /** Is this a tab Reticle drives, with a real browser behind it? */
269
361
  async function driven(deps, session) {
270
- if (true === deps.pool?.leasedSessionIds().includes(session.id))
362
+ if (true === deps.pool?.leasedSessionIds().includes(session.id)) {
271
363
  return true;
364
+ }
272
365
  return (deps.realInput?.screenshot !== undefined &&
273
366
  true === (await deps.realInput.isAvailableFor(session.url)));
274
367
  }
@@ -281,10 +374,12 @@ async function filmTab(deps, session, dir) {
281
374
  film.still = 1;
282
375
  return;
283
376
  }
284
- if (!(await driven(deps, session).catch(() => false)))
377
+ if (!(await driven(deps, session).catch(() => false))) {
285
378
  return;
286
- if (films.has(session.id))
379
+ }
380
+ if (films.has(session.id)) {
287
381
  return;
382
+ }
288
383
  const timer = setInterval(() => {
289
384
  void filmNow(session.id);
290
385
  }, FRAME.EVERY_MS);
@@ -295,16 +390,18 @@ async function filmTab(deps, session, dir) {
295
390
  /** Stop filming a tab. Safe to call for a tab that is not filmed. */
296
391
  export function stopFilming(sessionId) {
297
392
  const film = films.get(sessionId);
298
- if (film === undefined)
393
+ if (film === undefined) {
299
394
  return;
395
+ }
300
396
  clearInterval(film.timer);
301
397
  films.delete(sessionId);
302
398
  }
303
399
  /** One tick of a tab's film: one frame, when the budget, the gap and the page allow. Never throws. */
304
400
  export async function filmNow(sessionId) {
305
401
  const film = films.get(sessionId);
306
- if (film === undefined || film.busy !== undefined)
402
+ if (film === undefined || film.busy !== undefined) {
307
403
  return;
404
+ }
308
405
  film.busy = frame(sessionId, film).finally(() => {
309
406
  film.busy = undefined;
310
407
  });
@@ -315,8 +412,9 @@ async function frame(sessionId, film) {
315
412
  try {
316
413
  session = film.deps.sessions.resolve(sessionId);
317
414
  // An ended tab has nothing more to show.
318
- if (session.isEnded())
415
+ if (session.isEnded()) {
319
416
  session = undefined;
417
+ }
320
418
  }
321
419
  catch {
322
420
  session = undefined;
@@ -327,18 +425,21 @@ async function frame(sessionId, film) {
327
425
  }
328
426
  const dir = film.dir;
329
427
  const kept = framesIn.get(dir) ?? 0;
330
- if (kept >= FRAME.MAX_PER_RUN)
428
+ if (kept >= FRAME.MAX_PER_RUN) {
331
429
  return;
430
+ }
332
431
  film.idle += 1;
333
432
  const gap = 2 ** Math.floor(kept / (FRAME.MAX_PER_RUN / FRAME_STAGES));
334
- if (film.idle < Math.max(gap, film.still))
433
+ if (film.idle < Math.max(gap, film.still)) {
335
434
  return;
435
+ }
336
436
  film.idle = 0;
337
437
  try {
338
438
  const bytes = await framePixels(film.deps, session);
339
439
  const size = bytes === undefined ? undefined : jpegSize(bytes);
340
- if (bytes === undefined || size === undefined)
440
+ if (bytes === undefined || size === undefined) {
341
441
  return;
442
+ }
342
443
  const sha256 = createHash('sha256').update(bytes).digest('hex');
343
444
  if (film.recent.includes(sha256)) {
344
445
  film.still = Math.min(film.still * 2, MAX_STILL_TICKS);
@@ -348,8 +449,9 @@ async function frame(sessionId, film) {
348
449
  film.still = 1;
349
450
  const seq = await allocate(film.deps.fs, dir);
350
451
  // The step shots' own limit is the platform's: a frame never takes the last of it.
351
- if (seq > STEP_SCREENSHOT.MAX_PER_RUN)
452
+ if (seq > STEP_SCREENSHOT.MAX_PER_RUN) {
352
453
  return;
454
+ }
353
455
  framesIn.set(dir, kept + 1);
354
456
  const file = `${String(seq).padStart(SEQ_WIDTH, '0')}-${FRAME.TOOL}${JPEG_EXT}`;
355
457
  const record = {
@@ -377,9 +479,11 @@ async function frame(sessionId, film) {
377
479
  async function framePixels(deps, session) {
378
480
  const opts = { jpegQuality: FRAME.JPEG_QUALITY, cssScale: true };
379
481
  const leased = await deps.pool?.screenshotLease(session.id, opts);
380
- if (leased !== undefined)
482
+ if (leased !== undefined) {
381
483
  return leased;
382
- if (true !== (await deps.realInput?.isAvailableFor(session.url)))
484
+ }
485
+ if (true !== (await deps.realInput?.isAvailableFor(session.url))) {
383
486
  return undefined;
487
+ }
384
488
  return deps.realInput?.screenshot?.(session.url, opts);
385
489
  }
@@ -8,24 +8,6 @@ import type { ToolDef, ToolDeps } from '../../surface/tools/tool-kit.js';
8
8
  * gone mid-drive must not break the drive.
9
9
  */
10
10
  export declare function driveFrame(deps: ToolDeps, sessionId: string, jpegQuality: number): Promise<Uint8Array | undefined>;
11
- /**
12
- * Only read a capture the shell wrote. The path arrives from the PAGE, and the daemon must not
13
- * become a file-read oracle for whatever a compromised renderer names.
14
- *
15
- * Two layouts are accepted, and the pair is the whole subtlety:
16
- *
17
- * - `<tmp>/<prefix><dir>/<prefix><n>.png` — the shell's own PRIVATE capture directory, created 0700
18
- * so no other local user can read the screenshot or pre-place a symlink at the path we write.
19
- * - `<tmp>/<prefix><pid>-<n>.png` — the legacy flat layout. Kept because the shell ships as a
20
- * SEPARATE package on the user's own upgrade schedule: an app still on the older
21
- * `@reticlehq/electron` talking to a newer daemon must keep getting screenshots, not silently
22
- * start returning no image. It grants the daemon nothing it did not already have.
23
- *
24
- * Every comparison is an exact match against an UNRESOLVED path, which is what refuses traversal:
25
- * `<tmp>/<prefix>x/../../etc/<prefix>passwd.png` has a grandparent of `<tmp>/<prefix>x/..`, not
26
- * `<tmp>`. Exactly one directory level is allowed, so a deeper nesting is refused too.
27
- */
28
- export declare function isCapturePath(path: string): boolean;
29
11
  /**
30
12
  * The opt-in pixel layer. Both tools require a DRIVEN browser (reticle drive /
31
13
  * RETICLE_CDP_URL) — the always-on SDK ships no screenshotter — and return a structured NO_PROVIDER
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { AppRuntime, ReticleCommand, RETICLE_CAPTURE_FILE_PREFIX, VISUAL_NO_PROVIDER_RECOMMENDATION, RETICLE_TAURI_INERT_MARKER, TAURI_INERT_RECOMMENDATION, VisualReason, } from '@reticlehq/core';
2
+ import { AppRuntime, ReticleCommand, VISUAL_NO_PROVIDER_RECOMMENDATION, VisualReason, } from '@reticlehq/core';
3
3
  import { ReticleTool } from '@reticlehq/core';
4
4
  import { sessionIdShape } from '../../surface/tools/tool-kit.js';
5
5
  import { ratioSchema } from '../../surface/tools/args/numeric-bounds.js';
@@ -21,10 +21,7 @@ function captureRoot(deps, sessionId) {
21
21
  return deps.reticleRoot;
22
22
  }
23
23
  }
24
- import { trackCaptureDirectory } from './capture-cleanup.js';
25
- import { readFile, unlink } from 'node:fs/promises';
26
- import { tmpdir } from 'node:os';
27
- import { basename, dirname } from 'node:path';
24
+ import { desktopCapture } from './desktop-capture.js';
28
25
  const rectShape = z.object({
29
26
  x: z.number(),
30
27
  y: z.number(),
@@ -80,46 +77,6 @@ async function buildOpts(deps, sessionId, args) {
80
77
  }
81
78
  return true === args['fullPage'] ? { fullPage: true } : {};
82
79
  }
83
- /**
84
- * Ask the desktop shell to photograph its own window.
85
- *
86
- * Returns undefined for a plain web page (no capture helper) and for a desktop app that did not
87
- * install `@reticlehq/electron/main`. Deliberately NOT a screen-region capture: photographing
88
- * the glass returns whatever window is on top, which would save a picture of the user's editor as a
89
- * visual baseline — the exact false green Reticle exists to eliminate.
90
- */
91
- async function desktopCapture(deps, sessionId, fullPage) {
92
- const session = deps.sessions.resolve(sessionId);
93
- // A session that cannot take commands (no live browser behind it) simply has no pixels to give.
94
- if (typeof session.command !== 'function')
95
- return {};
96
- const res = await session.command(ReticleCommand.CAPTURE, { fullPage });
97
- if (!res.ok)
98
- return {};
99
- const r = asRecord(res.result);
100
- if (r['ok'] !== true) {
101
- // The shell refused a full-page capture it cannot do. That is a different answer from "capture
102
- // broke", and the caller has to hear it — otherwise they read `fullPage` as honoured.
103
- if (r['reason'] === VisualReason.FULL_PAGE_UNSUPPORTED)
104
- return { reason: VisualReason.FULL_PAGE_UNSUPPORTED };
105
- // A Tauri release build with the capture compiled out: say that, never the web answer.
106
- return true === asString(r['reason'])?.includes(RETICLE_TAURI_INERT_MARKER)
107
- ? { reason: VisualReason.NO_PROVIDER, recommendation: TAURI_INERT_RECOMMENDATION }
108
- : {};
109
- }
110
- const path = asString(r['path']);
111
- if (path === undefined || !isCapturePath(path))
112
- return {};
113
- try {
114
- const bytes = await readFile(path);
115
- await unlink(path).catch(() => undefined);
116
- trackCaptureDirectory(path);
117
- return isCompletePng(bytes) ? { png: new Uint8Array(bytes) } : {};
118
- }
119
- catch {
120
- return {};
121
- }
122
- }
123
80
  /**
124
81
  * One picture of the tab a chat-requested drive is driving, or undefined when there is none to take.
125
82
  *
@@ -146,42 +103,6 @@ export async function driveFrame(deps, sessionId, jpegQuality) {
146
103
  return undefined;
147
104
  }
148
105
  }
149
- /**
150
- * Only read a capture the shell wrote. The path arrives from the PAGE, and the daemon must not
151
- * become a file-read oracle for whatever a compromised renderer names.
152
- *
153
- * Two layouts are accepted, and the pair is the whole subtlety:
154
- *
155
- * - `<tmp>/<prefix><dir>/<prefix><n>.png` — the shell's own PRIVATE capture directory, created 0700
156
- * so no other local user can read the screenshot or pre-place a symlink at the path we write.
157
- * - `<tmp>/<prefix><pid>-<n>.png` — the legacy flat layout. Kept because the shell ships as a
158
- * SEPARATE package on the user's own upgrade schedule: an app still on the older
159
- * `@reticlehq/electron` talking to a newer daemon must keep getting screenshots, not silently
160
- * start returning no image. It grants the daemon nothing it did not already have.
161
- *
162
- * Every comparison is an exact match against an UNRESOLVED path, which is what refuses traversal:
163
- * `<tmp>/<prefix>x/../../etc/<prefix>passwd.png` has a grandparent of `<tmp>/<prefix>x/..`, not
164
- * `<tmp>`. Exactly one directory level is allowed, so a deeper nesting is refused too.
165
- */
166
- export function isCapturePath(path) {
167
- if (!basename(path).startsWith(RETICLE_CAPTURE_FILE_PREFIX))
168
- return false;
169
- const parent = dirname(path);
170
- if (parent === tmpdir())
171
- return true;
172
- return dirname(parent) === tmpdir() && basename(parent).startsWith(RETICLE_CAPTURE_FILE_PREFIX);
173
- }
174
- /**
175
- * A PNG is complete only if it ends with IEND. This check exists because a truncated image once
176
- * sailed through as a saved baseline: the tool answered `saved: true` with a path, and the file was
177
- * undecodable. A screenshot that cannot be read must fail loudly, never bank a corrupt baseline that
178
- * a later visual_diff will compare against.
179
- */
180
- function isCompletePng(bytes) {
181
- if (bytes.byteLength < 12)
182
- return false;
183
- return 'IEND' === bytes.subarray(bytes.byteLength - 8, bytes.byteLength - 4).toString('ascii');
184
- }
185
106
  /**
186
107
  * Which runtime this session is, for scoping a visual artifact.
187
108
  *
@@ -75,12 +75,11 @@ export function importsOf(sourceRoot, file) {
75
75
  export function reachableFrom(sourceRoot, entry) {
76
76
  /*
77
77
  * The entry is normalised the same way `resolveImport` normalises everything this walker
78
- * DISCOVERS, because callers build it with `join()` and on Windows that yields `features\ee\x.ts`
79
- * while every other key in the map is `features/ee/x.ts`. A caller then filters the result with
80
- * `startsWith('features/ee/')` and the seed — the one path it did not walk to — silently drops
81
- * out. That is how `ee-boundary`, the guard that says the free build cannot reach the
82
- * separately-licensed code, passed on Windows while checking nothing at all: both of its real
83
- * assertions expect an EMPTY list, so a walker that finds nothing agrees with them. Only its
78
+ * DISCOVERS, because callers build it with `join()` and on Windows that yields `features\x\y.ts`
79
+ * while every other key in the map is `features/x/y.ts`. A caller then filters the result with
80
+ * `startsWith('features/x/')` and the seed — the one path it did not walk to — silently drops
81
+ * out. That is how a boundary guard passed on Windows while checking nothing at all: its real
82
+ * assertions expected an EMPTY list, so a walker that finds nothing agrees with them. Only its
84
83
  * negative control could see it, and that is the whole reason the negative control exists.
85
84
  *
86
85
  * Both separators, not `sep`: on a POSIX machine `sep` is `/`, so splitting on it leaves a
package/dist/index.d.ts CHANGED
@@ -102,8 +102,10 @@ export { registerContradictionFold, registeredContradictionFolds, } from '@retic
102
102
  export type { ContradictionFold } from '@reticlehq/engine/disagreement/contradiction-folds.js';
103
103
  export { MCP_SSE_PATH, MCP_MESSAGE_PATH } from '@reticlehq/core';
104
104
  export { BrowserPool, DEFAULT_LEASE_TTL_MS } from './portal/pool/browser-pool.js';
105
- export type { Lease, Launcher, PooledBrowser } from './portal/pool/browser-pool.js';
105
+ export type { Lease, Launcher, PooledBrowser, PooledPageError, } from './portal/pool/browser-pool.js';
106
106
  export { playwrightLauncher, resolveMaxContexts } from './portal/pool/playwright-launcher.js';
107
+ export { makePageErrorJournal, uncaughtIdentity } from './portal/pool/page-error-journal.js';
108
+ export type { JournalableSession, PageErrorJournal } from './portal/pool/page-error-journal.js';
107
109
  export { appendReticleParams } from './surface/tools/lease-tools.js';
108
110
  export { writePid, removePid, isRunning, logPath, readPid, isAlive, } from './command/daemon/daemon.js';
109
111
  export { classifyDaemonLife, DaemonEnd, DAEMON_HEARTBEAT_EVENT, DAEMON_HEARTBEAT_MS, type DaemonLife, } from './command/daemon/lifetime/heartbeat.js';
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ import { attachJournal } from './wire-journal.js';
58
58
  import { reportOnboardingStep } from './telemetry/onboarding-funnel.js';
59
59
  import { readRetainPolicy } from './memory/journal/on-disk/retain-policy.js';
60
60
  import { log } from './log.js';
61
- import { createBrowserPool, knownProjectRoots, loadHarnessConfig, makeNetworkDetailRouter, platformEnvFor, readerConnect, resolveRealInput, sdkFixForProject, } from './start-wiring.js';
61
+ import { attachPageErrorJournal, createBrowserPool, knownProjectRoots, loadHarnessConfig, makeNetworkDetailRouter, platformEnvFor, readerConnect, resolveRealInput, sdkFixForProject, } from './start-wiring.js';
62
62
  export { replayVerdictLine } from './language/flows/replay-from-hud.js';
63
63
  // Re-exported from the contract, where the names now live: a consumer importing the server should
64
64
  // not have to know that the vocabulary moved.
@@ -156,7 +156,11 @@ export async function start(options = {}) {
156
156
  // Route CDP-authoritative network detail (drive path only) onto the driven session's journal: the
157
157
  // page and the SDK session share an origin, so a NET_DETAIL is pushed to the matching connected session.
158
158
  const routeNetworkDetail = makeNetworkDetailRouter(bridge, options.driveUrl);
159
- const { realInput, owned } = await resolveRealInput(options, () => bridge.close(), routeNetworkDetail);
159
+ // Journal the uncaught page errors the pool hears — an import-time throw lands before any SDK
160
+ // exists to see it, and it was previously invisible to the agent (#1966). The bridge hook is
161
+ // registered here; the pool gets the sink at its creation below.
162
+ const pageErrorJournal = attachPageErrorJournal(bridge);
163
+ const { realInput, owned } = await resolveRealInput(options, () => bridge.close(), routeNetworkDetail, readerConnect(port, security.token));
160
164
  let chatDrives;
161
165
  if (options.mcp !== false) {
162
166
  // cwd/Date.now are confined to start — never inside reticle-dir.ts's pure logic (rule 7).
@@ -184,7 +188,7 @@ export async function start(options = {}) {
184
188
  const project = new ProjectStore(fs, reticleRoot, { now });
185
189
  attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
186
190
  const annotations = new AnnotationStore();
187
- pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token));
191
+ pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token), (leaseId, error) => pageErrorJournal.onPoolPageError(leaseId, error), (leaseId) => pageErrorJournal.onLeaseClosed(leaseId), bridge.nonces);
188
192
  leaseReaper = new LeaseReaper(pool);
189
193
  leaseReaper.start();
190
194
  /*
@@ -346,7 +350,11 @@ export async function startDaemon(options = {}) {
346
350
  }, AGENT_DETACH_GRACE_MS));
347
351
  const reaper = new SessionReaper(bridge.sessions);
348
352
  reaper.start();
349
- const { realInput, owned } = await resolveRealInput(options, () => shared.close(), makeNetworkDetailRouter(bridge, options.driveUrl));
353
+ // Same page-error journal the `start` path wires: an import-time crash reaches the agent through
354
+ // it instead of reading as a blank page (#1966). Kept beside the other shared wiring so the two
355
+ // entry points cannot drift.
356
+ const pageErrorJournal = attachPageErrorJournal(bridge);
357
+ const { realInput, owned } = await resolveRealInput(options, () => shared.close(), makeNetworkDetailRouter(bridge, options.driveUrl), readerConnect(port, security.token));
350
358
  const fs = createNodeFileSystem();
351
359
  const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
352
360
  const now = options.now ?? (() => Date.now());
@@ -380,7 +388,7 @@ export async function startDaemon(options = {}) {
380
388
  const project = new ProjectStore(fs, reticleRoot, { now });
381
389
  attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
382
390
  const annotations = new AnnotationStore();
383
- const pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token));
391
+ const pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token), (leaseId, error) => pageErrorJournal.onPoolPageError(leaseId, error), (leaseId) => pageErrorJournal.onLeaseClosed(leaseId), bridge.nonces);
384
392
  const leaseReaper = new LeaseReaper(pool);
385
393
  leaseReaper.start();
386
394
  /*
@@ -587,6 +595,7 @@ export { registerContradictionFold, registeredContradictionFolds, } from '@retic
587
595
  export { MCP_SSE_PATH, MCP_MESSAGE_PATH } from '@reticlehq/core';
588
596
  export { BrowserPool, DEFAULT_LEASE_TTL_MS } from './portal/pool/browser-pool.js';
589
597
  export { playwrightLauncher, resolveMaxContexts } from './portal/pool/playwright-launcher.js';
598
+ export { makePageErrorJournal, uncaughtIdentity } from './portal/pool/page-error-journal.js';
590
599
  export { appendReticleParams } from './surface/tools/lease-tools.js';
591
600
  export { writePid, removePid, isRunning, logPath, readPid, isAlive, } from './command/daemon/daemon.js';
592
601
  // The daemon's own liveness vocabulary, exported so a GATE can read a daemon log back and say how
@@ -1,10 +1,12 @@
1
1
  import { Verified, type JournalAction, type RunReport } from '@reticlehq/core';
2
+ /** A run with no agent call for this long is dropped. A working agent calls much more often. */
3
+ export declare const RUN_IDLE_LIMIT_MS: number;
2
4
  export declare class RunLedger {
3
5
  #private;
4
- /** An agent call starts. The first one starts the run, and a call from another agent starts a new one. */
6
+ /** An agent call starts. The agent's first call starts its run. A run idle too long is dropped. */
5
7
  begin(now: number, owner: string): void;
6
8
  /** An agent call ended. The first one that left a verdict sets the time to the first verdict. */
7
- after(now: number): void;
9
+ after(now: number, owner?: string): void;
8
10
  /** A journal record from the agent's own call. A record with no verdict is not a claim. */
9
11
  verdict(action: JournalAction): void;
10
12
  flowSaved(name: string): void;
@@ -13,9 +15,9 @@ export declare class RunLedger {
13
15
  memoryWritten(count?: number): void;
14
16
  /** Notes given to the agent. Each one counts once, however often it is shown. */
15
17
  memoryRead(notes: readonly string[]): void;
16
- isEmpty(): boolean;
17
- /** The report, without the cloud line (the caller knows the project). Empties the ledger. */
18
- take(now: number): RunReport;
18
+ isEmpty(owner?: string): boolean;
19
+ /** The owner's report, without the cloud line (the caller knows the project). Ends that run. */
20
+ take(now: number, owner?: string): RunReport;
19
21
  }
20
22
  /** The daemon's ledger. */
21
23
  export declare const runLedger: RunLedger;