@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
@@ -3,14 +3,39 @@ import type { StartOptions } from './start-options.js';
3
3
  import type { Bridge } from './portal/bridge/bridge.js';
4
4
  import type { RealInputProvider } from './portal/input/real-input.js';
5
5
  import { BrowserPool } from './portal/pool/browser-pool.js';
6
+ import { type PageErrorJournal } from './portal/pool/page-error-journal.js';
7
+ import type { PooledPageError } from './portal/pool/pool-contract.js';
6
8
  import { type InjectedConnect } from './portal/pool/zero-install.js';
9
+ import type { NonceMinter } from './portal/production/production-activation.js';
7
10
  /**
8
11
  * Build the shared browser pool (one headless Chromium, N capped isolated leased contexts). Lazy —
9
12
  * no Chromium launches until the first lease — so creating it is free even when never used.
10
13
  */
11
14
  /** Where the zero-install reader connects: this daemon's own bridge, with its pairing token. */
12
15
  export declare function readerConnect(port: number, token: string | undefined): InjectedConnect;
13
- export declare function createBrowserPool(headless: boolean, reader: InjectedConnect): BrowserPool;
16
+ /**
17
+ * The zero-install script for pages that connect to `reader`, built on first use and kept: the
18
+ * page that needs it is one whose app never connected. Undefined when this install has no reader.
19
+ *
20
+ * A page that is not on loopback gets a nonce minted for its origin in place of the pairing token.
21
+ * The bridge admits a WebSocket from an origin only while a nonce names it, so the reader on a
22
+ * deployed page was refused with a 403 (#2016). The nonce also keeps the machine's pairing token
23
+ * out of a page that somebody else serves.
24
+ *
25
+ * ponytail: the nonce lives for one drive (SESSION_NONCE_TTL_MS). A lease reloaded after that
26
+ * dials with a dead nonce; mint per navigation if that is ever seen.
27
+ */
28
+ export declare function zeroInstallLoader(reader: InjectedConnect, nonces?: Pick<NonceMinter, 'mint'>): (url?: string) => string | undefined;
29
+ export declare function createBrowserPool(headless: boolean, reader: InjectedConnect, onPageError?: (leaseId: string, error: PooledPageError) => void, onLeaseClosed?: (leaseId: string) => void, nonces?: Pick<NonceMinter, 'mint'>): BrowserPool;
30
+ /**
31
+ * Journal the uncaught page errors the pool hears into the session that connects for that lease.
32
+ *
33
+ * The pool sees every throw its pages make from before the first navigation; the SDK only exists
34
+ * afterwards. Without this wiring an import-time crash — a blank page with no error text anywhere
35
+ * (#1966) — was invisible to the agent driving it. Returns the journal so the pool's sink can be
36
+ * handed over; the bridge hook is registered here.
37
+ */
38
+ export declare function attachPageErrorJournal(bridge: Bridge): PageErrorJournal;
14
39
  /**
15
40
  * Route CDP-authoritative network detail onto the driven session's journal.
16
41
  *
@@ -26,7 +51,9 @@ export declare function makeNetworkDetailRouter(bridge: Bridge, driveUrl: string
26
51
  * so a failed launch never leaks a WS port. A past divergence between the two paths let daemon mode
27
52
  * run with a different setup — this removes that risk.
28
53
  */
29
- export declare function resolveRealInput(options: StartOptions, onNavigateError: () => Promise<void>, onNetworkDetail?: (detail: NetworkDetail) => void): Promise<{
54
+ export declare function resolveRealInput(options: StartOptions, onNavigateError: () => Promise<void>, onNetworkDetail?: (detail: NetworkDetail) => void,
55
+ /** Where a driven page with no SDK connects the zero-install reader. Absent: it gets none. */
56
+ reader?: InjectedConnect): Promise<{
30
57
  realInput?: RealInputProvider;
31
58
  owned?: {
32
59
  dispose: () => Promise<void>;
@@ -23,7 +23,10 @@ import { SERVER_VERSION } from './command/version/identity/server-version.js';
23
23
  import { CdpRealInputProvider, LaunchedRealInputProvider } from './portal/input/real-input.js';
24
24
  import { BrowserPool } from './portal/pool/browser-pool.js';
25
25
  import { playwrightLauncher, resolveMaxContexts } from './portal/pool/playwright-launcher.js';
26
+ import { makePageErrorJournal } from './portal/pool/page-error-journal.js';
27
+ import { resolveLeasedSessionId } from './surface/tools/lease-tools.js';
26
28
  import { readReaderBundle, zeroInstallScript as zeroInstallScriptFor, } from './portal/pool/zero-install.js';
29
+ import { offLoopbackOrigin } from './portal/pool/context-permissions.js';
27
30
  /**
28
31
  * Build the shared browser pool (one headless Chromium, N capped isolated leased contexts). Lazy —
29
32
  * no Chromium launches until the first lease — so creating it is free even when never used.
@@ -32,26 +35,84 @@ import { readReaderBundle, zeroInstallScript as zeroInstallScriptFor, } from './
32
35
  export function readerConnect(port, token) {
33
36
  return { url: bridgeWsUrl(port), ...(token === undefined ? {} : { token }) };
34
37
  }
35
- export function createBrowserPool(headless, reader) {
36
- const maxContexts = resolveMaxContexts(process.env[ReticleEnv.MAX_CONTEXTS], cpus().length);
37
- const genSessionId = () => `lease-${'function' === typeof globalThis.crypto?.randomUUID
38
- ? globalThis.crypto.randomUUID()
39
- : String(Date.now())}`;
40
- // Built on first use and kept: the page that needs it is one whose app never connected.
38
+ /**
39
+ * The zero-install script for pages that connect to `reader`, built on first use and kept: the
40
+ * page that needs it is one whose app never connected. Undefined when this install has no reader.
41
+ *
42
+ * A page that is not on loopback gets a nonce minted for its origin in place of the pairing token.
43
+ * The bridge admits a WebSocket from an origin only while a nonce names it, so the reader on a
44
+ * deployed page was refused with a 403 (#2016). The nonce also keeps the machine's pairing token
45
+ * out of a page that somebody else serves.
46
+ *
47
+ * ponytail: the nonce lives for one drive (SESSION_NONCE_TTL_MS). A lease reloaded after that
48
+ * dials with a dead nonce; mint per navigation if that is ever seen.
49
+ */
50
+ export function zeroInstallLoader(reader, nonces) {
41
51
  let script;
42
- const zeroInstallScript = () => {
52
+ return (url) => {
53
+ const origin = offLoopbackOrigin(url);
54
+ if (origin !== undefined && nonces !== undefined) {
55
+ const bundle = readReaderBundle();
56
+ if (bundle === undefined) {
57
+ return undefined;
58
+ }
59
+ return zeroInstallScriptFor(bundle, {
60
+ url: reader.url,
61
+ token: nonces.mint({ origin }).nonce,
62
+ });
63
+ }
43
64
  if (script === undefined) {
44
65
  const bundle = readReaderBundle();
45
66
  script = bundle === undefined ? null : zeroInstallScriptFor(bundle, reader);
46
67
  }
47
68
  return script ?? undefined;
48
69
  };
70
+ }
71
+ export function createBrowserPool(headless, reader, onPageError, onLeaseClosed, nonces) {
72
+ const maxContexts = resolveMaxContexts(process.env[ReticleEnv.MAX_CONTEXTS], cpus().length);
73
+ const genSessionId = () => `lease-${'function' === typeof globalThis.crypto?.randomUUID
74
+ ? globalThis.crypto.randomUUID()
75
+ : String(Date.now())}`;
49
76
  return new BrowserPool(playwrightLauncher({ headless }), {
50
77
  maxContexts,
51
78
  genSessionId,
52
- zeroInstallScript,
79
+ zeroInstallScript: zeroInstallLoader(reader, nonces),
80
+ // The reader dials this origin; a lease off loopback adds it to the page CSP (#2016).
81
+ daemonOrigin: new URL(reader.url).origin,
53
82
  launchHeaded: playwrightLauncher({ headless: false }),
83
+ ...(onPageError === undefined ? {} : { onPageError }),
84
+ ...(onLeaseClosed === undefined ? {} : { onLeaseClosed }),
85
+ });
86
+ }
87
+ /**
88
+ * Journal the uncaught page errors the pool hears into the session that connects for that lease.
89
+ *
90
+ * The pool sees every throw its pages make from before the first navigation; the SDK only exists
91
+ * afterwards. Without this wiring an import-time crash — a blank page with no error text anywhere
92
+ * (#1966) — was invisible to the agent driving it. Returns the journal so the pool's sink can be
93
+ * handed over; the bridge hook is registered here.
94
+ */
95
+ export function attachPageErrorJournal(bridge) {
96
+ const journal = makePageErrorJournal({
97
+ resolveSession: (leaseId) => {
98
+ // Same resolution the lease tool uses: the lease id, or the id of a session whose URL
99
+ // carries the lease marker — an app that registered under its own name still reaches its lease.
100
+ const id = resolveLeasedSessionId(bridge.sessions, leaseId);
101
+ if (id === undefined) {
102
+ return undefined;
103
+ }
104
+ const session = bridge.sessions.get(id);
105
+ return session === undefined ? undefined : session;
106
+ },
107
+ });
108
+ bridge.attachSessionCreate((session) => journal.onSessionCreate(session));
109
+ // The daemon runs for days; both of the journal's maps are keyed by things that end, so both
110
+ // endings are hooked. The bridge's end hook is additive — the durable journal's teardown runs too.
111
+ bridge.attachSessionEnd((session) => {
112
+ journal.onSessionEnd(session.id);
113
+ return Promise.resolve();
54
114
  });
115
+ return journal;
55
116
  }
56
117
  /**
57
118
  * Route CDP-authoritative network detail onto the driven session's journal.
@@ -98,7 +159,9 @@ export function makeNetworkDetailRouter(bridge, driveUrl) {
98
159
  * so a failed launch never leaks a WS port. A past divergence between the two paths let daemon mode
99
160
  * run with a different setup — this removes that risk.
100
161
  */
101
- export async function resolveRealInput(options, onNavigateError, onNetworkDetail) {
162
+ export async function resolveRealInput(options, onNavigateError, onNetworkDetail,
163
+ /** Where a driven page with no SDK connects the zero-install reader. Absent: it gets none. */
164
+ reader) {
102
165
  const driveUrl = options.driveUrl;
103
166
  if (driveUrl !== undefined && driveUrl.length > 0) {
104
167
  const headless = options.headless ?? true;
@@ -114,6 +177,7 @@ export async function resolveRealInput(options, onNavigateError, onNetworkDetail
114
177
  ...(injectConnect !== undefined ? { injectConnect } : {}),
115
178
  ...(storageState !== undefined ? { storageState } : {}),
116
179
  ...(onNetworkDetail !== undefined ? { onNetworkDetail } : {}),
180
+ ...(reader !== undefined ? { zeroInstallScript: zeroInstallLoader(reader) } : {}),
117
181
  }));
118
182
  const launched = factory({ driveUrl, headless });
119
183
  try {
@@ -5,6 +5,8 @@ import { localInitializeResponse, degradedInstructions, isHandshakeLine, drainLi
5
5
  export { HandshakeReplay, RECONNECT_INITIALIZE_ID } from './proxy/proxy-handshake.js';
6
6
  import { isToolCallRequest, isToolsListRequest, ToolCatalogCache } from './tool-catalog-cache.js';
7
7
  import { rememberEnumerated, rememberProxyStarted, rememberToolCalled } from './attach-memory.js';
8
+ import { initializeClientName, rememberAgentConnected } from './peer/agents-connected.js';
9
+ import { agentClientOf } from '../../hooks/coding-agents.js';
8
10
  import { toolsChangedNotification } from './proxy/proxy-handshake.js';
9
11
  import { LOOPBACK_HOST, MCP_SSE_PATH, MCP_SHUTDOWN_EVENT, ReticleEnv, CONTRACT_FINGERPRINT, ProxyExitReason, PROXY_ID_PARAM, } from '@reticlehq/core';
10
12
  import { SseFrameParser, buildSessionUrl } from './sse-frame-parser.js';
@@ -17,7 +19,8 @@ import { hasAnyProjectConnectedBefore } from '../../memory/recall/prior/connecti
17
19
  import { projectIdsAt } from '../../command/cli/ports/resolve/cli-port.js';
18
20
  import { PEER_VERSION_PARAM, PEER_CONTRACT_PARAM, agentSessionQuery, } from '../../command/version/peer-announce.js';
19
21
  import { onStreamDrop, onClientRequest, onReconnectBudgetSpent, onQueueExpired, OnDrop, OnRequest, } from './proxy/proxy-lifecycle.js';
20
- import { describePresence, probePresence } from '../../command/daemon/binding/port-presence.js';
22
+ import { probePresence } from '../../command/daemon/binding/port-presence.js';
23
+ import { describePortState } from '../../command/daemon/port-state.js';
21
24
  import { PROXY_ID, flushProxyExit, noteProxyDrop, noteProxyToolCall, } from '../../telemetry/proxy-telemetry.js';
22
25
  export { flushProxyExit, reportWakeRefused } from '../../telemetry/proxy-telemetry.js';
23
26
  import { detectStack } from '../../telemetry/feedback-context.js';
@@ -513,7 +516,7 @@ boot) {
513
516
  // The sentence `doctor` has always been able to say, in the log the agent's own process
514
517
  // writes. A FOREIGN holder used to be indistinguishable from a free port here, and the
515
518
  // proxy spun against it instead of saying which one it was.
516
- const sentence = describePresence(presence, port);
519
+ const sentence = describePortState(presence, port);
517
520
  proxyLog('reticle_mcp_proxy_dormant', { port, presence, note: sentence });
518
521
  // Nothing is coming that could answer the client's handshake through this port, and the
519
522
  // proxy now knows that rather than merely suspecting it. Waiting out LOCAL_HANDSHAKE_MS
@@ -792,6 +795,11 @@ boot) {
792
795
  if (isToolsListRequest(trimmed)) {
793
796
  rememberEnumerated(reticleStateHome(), port);
794
797
  }
798
+ // WHICH agent connected, so the installer and `status` can say "Connected: Cursor ✓".
799
+ const client = agentClientOf(initializeClientName(trimmed));
800
+ if (client !== undefined) {
801
+ rememberAgentConnected(reticleStateHome(), client);
802
+ }
795
803
  // The hop every other check misses. Recorded on the REQUEST rather than a reply, because
796
804
  // the question is whether anything ever left the agent — a call that leaves and gets no
797
805
  // answer is a different fault from one that was never made, and only this side sees it.
@@ -12,6 +12,7 @@ import { buildDynamicTools, descriptionOf } from '../tools/dynamic-tools.js';
12
12
  import { runTool } from '../tools/invoke-tool.js';
13
13
  import { sessionEnvelopeShape, newSnapshotCache } from '../tools/tool-kit.js';
14
14
  import { buildErrorPayload, codedRefusalOf } from '../tools/error-recovery.js';
15
+ import { withRepeatHint } from '../tools/repeat-hint.js';
15
16
  import { connectionSkew, takeVersionSkewOnto } from '../../command/version/version-nudge.js';
16
17
  import { resultIsError } from './faults/mcp-is-error.js';
17
18
  import { consumerVerdictRefusal, reservedVerdictKeysIn } from './consumer-verdict-guard.js';
@@ -149,8 +150,9 @@ function leanZodShape(shape, advertised, predicateAnchor) {
149
150
  let hintSpent = predicateAnchor !== undefined;
150
151
  for (const [key, schema] of Object.entries(shape)) {
151
152
  if (isPredicateParam(schema)) {
152
- // Every tool keeps the kinds; only the navigation hint is anchored.
153
- const compact = z.record(z.string(), z.unknown());
153
+ // Every tool keeps the kinds; only the navigation hint is anchored. A bare string is visible
154
+ // text (core's `PredicateSchema`), so the SDK must let it through to that parse.
155
+ const compact = z.union([z.string(), z.record(z.string(), z.unknown())]);
154
156
  out[key] = (schema.isOptional() ? compact.optional() : compact).describe(hintSpent ? PREDICATE_KINDS : COMPACT_PREDICATE_DESCRIPTION);
155
157
  hintSpent = true;
156
158
  continue;
@@ -769,7 +771,7 @@ tools = tableForSurface(profile)) {
769
771
  content: [
770
772
  {
771
773
  type: 'text',
772
- text: JSON.stringify(liveCallValues(takeVersionSkewOnto(buildErrorPayload(message, codedRefusalOf(error)), deps.peerSkew), advertisedNames)),
774
+ text: JSON.stringify(liveCallValues(takeVersionSkewOnto(withRepeatHint(buildErrorPayload(message, codedRefusalOf(error)), error), deps.peerSkew), advertisedNames)),
773
775
  },
774
776
  ],
775
777
  };
@@ -0,0 +1,6 @@
1
+ /** The `AgentClient` values of every agent that has connected. Empty on a missing or broken file. */
2
+ export declare function connectedAgentClients(stateDir: string): ReadonlySet<string>;
3
+ /** Record that `client` (an `AgentClient` value) connected. Written once per client. */
4
+ export declare function rememberAgentConnected(stateDir: string, client: string): void;
5
+ /** `clientInfo.name` of an MCP `initialize` request line, or undefined for any other line. */
6
+ export declare function initializeClientName(line: string): string | undefined;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Which coding agents have connected to Reticle on this machine, by name.
3
+ *
4
+ * `attach-memory.ts` records that SOME client attached on a port. It cannot say which one, so with
5
+ * two agents a healthy one hides a broken one. This record keeps the client that the MCP
6
+ * `initialize` handshake names, as its closed `AgentClient` value. It is machine wide, because the
7
+ * installer runs before there is a project and therefore before there is a port.
8
+ *
9
+ * The proxy writes it when the request arrives. It records a fact that happened, never an inference
10
+ * from a config file. Best-effort in both directions: a hint never breaks the client path.
11
+ */
12
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ const RECORD_FILE = 'agents-connected.json';
15
+ const INITIALIZE_METHOD = 'initialize';
16
+ /** The `AgentClient` values of every agent that has connected. Empty on a missing or broken file. */
17
+ export function connectedAgentClients(stateDir) {
18
+ try {
19
+ const parsed = JSON.parse(readFileSync(join(stateDir, RECORD_FILE), 'utf8'));
20
+ return new Set(Array.isArray(parsed) ? parsed.filter((v) => 'string' === typeof v) : []);
21
+ }
22
+ catch {
23
+ return new Set();
24
+ }
25
+ }
26
+ /** Record that `client` (an `AgentClient` value) connected. Written once per client. */
27
+ export function rememberAgentConnected(stateDir, client) {
28
+ const known = connectedAgentClients(stateDir);
29
+ if (known.has(client)) {
30
+ return;
31
+ }
32
+ try {
33
+ mkdirSync(stateDir, { recursive: true });
34
+ writeFileSync(join(stateDir, RECORD_FILE), JSON.stringify([...known, client]), 'utf8');
35
+ }
36
+ catch {
37
+ // An unwritable state home is reported elsewhere; it must not break the client path.
38
+ }
39
+ }
40
+ /** `clientInfo.name` of an MCP `initialize` request line, or undefined for any other line. */
41
+ export function initializeClientName(line) {
42
+ try {
43
+ const msg = JSON.parse(line);
44
+ if ('object' !== typeof msg || null === msg) {
45
+ return undefined;
46
+ }
47
+ const { method, params } = msg;
48
+ if (INITIALIZE_METHOD !== method || 'object' !== typeof params || null === params) {
49
+ return undefined;
50
+ }
51
+ const info = params.clientInfo;
52
+ const name = 'object' === typeof info && null !== info ? info.name : undefined;
53
+ return 'string' === typeof name && 0 < name.length ? name : undefined;
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
@@ -1,6 +1,7 @@
1
1
  import * as net from 'node:net';
2
2
  import { LOOPBACK_HOST } from '@reticlehq/core';
3
- import { describePresence, presenceIsUsable, probePresence, PortPresence, } from '../../../command/daemon/binding/port-presence.js';
3
+ import { presenceIsUsable, probePresence, PortPresence, } from '../../../command/daemon/binding/port-presence.js';
4
+ import { describePortState } from '../../../command/daemon/port-state.js';
4
5
  import { fetchStatus } from '../../../command/daemon/binding/daemon-status-probe.js';
5
6
  import { isAlive, readPid } from '../../../command/daemon/daemon.js';
6
7
  const DEFAULT_DAEMON_READY_TIMEOUT_MS = 10_000;
@@ -21,6 +22,16 @@ function delay(ms) {
21
22
  export function daemonPollDelayMs(attempt) {
22
23
  return Math.min(DAEMON_POLL_INTERVAL_MS * attempt, DAEMON_POLL_MAX_INTERVAL_MS);
23
24
  }
25
+ const CONNECTION_RESET = 'ECONNRESET';
26
+ /**
27
+ * A loopback connect to a port with no listener is refused at once. A timeout means a listener
28
+ * whose queue is full because it accepts nothing: a daemon stuck in its exit (#1937). Read as
29
+ * "free", that made `status` say the port was free while a process held it.
30
+ *
31
+ * Not on Windows: it retries a refused loopback connect for about two seconds, so there a timeout
32
+ * is also what a free port gives.
33
+ */
34
+ const TIMEOUT_MEANS_HELD = 'win32' !== process.platform;
24
35
  /**
25
36
  * Returns true if something is already listening on the reticle port.
26
37
  * Uses a plain TCP probe so we don't create a side-effectful SSE session
@@ -38,10 +49,11 @@ export function probeDaemon(port, host = LOOPBACK_HOST) {
38
49
  socket.destroy();
39
50
  resolve(true);
40
51
  });
41
- socket.on('error', () => resolve(false));
52
+ // A reset is an answer from a listener, not "nothing listens".
53
+ socket.on('error', (err) => resolve(CONNECTION_RESET === err.code));
42
54
  socket.on('timeout', () => {
43
55
  socket.destroy();
44
- resolve(false);
56
+ resolve(TIMEOUT_MEANS_HELD);
45
57
  });
46
58
  socket.connect(port, host);
47
59
  });
@@ -70,7 +82,7 @@ export async function waitForDaemon(port, probes = { tcpOpen: probeDaemon, statu
70
82
  return;
71
83
  if (false === probes.alive?.()) {
72
84
  throw new Error(`the reticle daemon on port ${String(port)} exited before it was ready — ` +
73
- describePresence(presence, port));
85
+ describePortState(presence, port));
74
86
  }
75
87
  attempt++;
76
88
  await delay(daemonPollDelayMs(attempt));
@@ -79,7 +91,7 @@ export async function waitForDaemon(port, probes = { tcpOpen: probeDaemon, statu
79
91
  // means something is squatting the port. Reporting only the timeout sent readers hunting the
80
92
  // wrong one.
81
93
  throw new Error(`reticle daemon did not become ready on port ${String(port)} within ` +
82
- `${String(DAEMON_READY_TIMEOUT_MS)}ms — ${describePresence(presence, port)}`);
94
+ `${String(DAEMON_READY_TIMEOUT_MS)}ms — ${describePortState(presence, port)}`);
83
95
  }
84
96
  /** True while the process in the daemon pid file for `port` runs. */
85
97
  export function daemonProcessAlive(port) {
@@ -11,6 +11,7 @@
11
11
  * outcome than "unknown".
12
12
  */
13
13
  import { PredicateSchema } from '@reticlehq/engine/question/predicate/predicate-schema.js';
14
+ import { parsePredicate } from '@reticlehq/engine/question/predicate/predicate-parse.js';
14
15
  import { SESSION_REF, isGlobalPressCall, unissuedRefRefusal } from '@reticlehq/core';
15
16
  import { isDocumentGoneError } from '../../../portal/session/facts/session-replaced.js';
16
17
  import { SessionReplacedError } from '../../../portal/session/page-commands/pending-commands.js';
@@ -77,8 +78,9 @@ const STEP_STRUCTURE_KEYS = new Set([
77
78
  export function sequenceStepArgs(step) {
78
79
  const flat = {};
79
80
  for (const [key, value] of Object.entries(step)) {
80
- if (!STEP_STRUCTURE_KEYS.has(key))
81
+ if (!STEP_STRUCTURE_KEYS.has(key)) {
81
82
  flat[key] = value;
83
+ }
82
84
  }
83
85
  const nested = step['args'];
84
86
  if ('string' === typeof nested) {
@@ -96,6 +98,22 @@ export function sequenceStepArgs(step) {
96
98
  }
97
99
  return { ...flat, ...(null !== nested && 'object' === typeof nested ? nested : {}) };
98
100
  }
101
+ /**
102
+ * Why `parsePredicate` refuses this `expect`, as the sentence `reticle_assert` gives.
103
+ *
104
+ * Called only after `PredicateSchema` has refused the value, so the parse throws. The schema stays
105
+ * the test for refusal: `parsePredicate` also refuses a value that the schema accepts, and this
106
+ * check must not refuse more than it did before.
107
+ */
108
+ function expectParseIssue(value) {
109
+ try {
110
+ parsePredicate(value);
111
+ return '';
112
+ }
113
+ catch (error) {
114
+ return error instanceof Error ? error.message : String(error);
115
+ }
116
+ }
99
117
  export function assertSequenceSteps(steps) {
100
118
  if (0 === steps.length) {
101
119
  throw new Error('reticle_act_sequence was given no steps. Pass steps: [{ ref, action, args? }] or [{ target, action, args? }] — nothing was acted on.');
@@ -118,18 +136,15 @@ export function assertSequenceSteps(steps) {
118
136
  * declared; nothing told it the declaration was discarded, and that sentence invites it to add
119
137
  * the very thing it just wrote. Same false green as an unreadable key, through a key this tool
120
138
  * does read.
139
+ *
140
+ * The reason is the sentence `reticle_assert` gives for the same predicate: the field that failed
141
+ * and the fields that work. A fixed sentence with generic examples did not say that `css` is not a
142
+ * `query` field, so the caller had to guess again. That sentence also carries the schema's own
143
+ * words, such as the keys it saw when it could not tell the kind.
121
144
  */
122
- const parsed = step['expect'] === undefined ? undefined : PredicateSchema.safeParse(step['expect']);
123
- if (parsed !== undefined && !parsed.success) {
124
- // The schema's own words, such as the keys it saw when it could not tell the kind.
125
- const said = parsed.error.issues
126
- .filter((issue) => 'custom' === issue.code)
127
- .map((issue) => ` ${issue.message}.`)
128
- .join('');
129
- throw new Error(`step ${String(i)} carries an \`expect\` this tool cannot parse.${said} ` +
130
- 'A consequence is a predicate object with a `kind` — e.g. { kind: "signal", name: ' +
131
- '"order:placed" }, { kind: "element", by: "testid", value: "receipt" }, or ' +
132
- '{ kind: "settled" } — the same shape reticle_act_and_wait takes as `until`. ' +
145
+ if (step['expect'] !== undefined && !PredicateSchema.safeParse(step['expect']).success) {
146
+ throw new Error(`step ${String(i)} carries an \`expect\` this tool cannot parse: ` +
147
+ `${expectParseIssue(step['expect'])} ` +
133
148
  'Refused rather than dropped, because a step whose declaration was discarded reports ' +
134
149
  'as a step that declared nothing. Nothing was acted on.');
135
150
  }
@@ -145,10 +160,12 @@ export function assertSequenceSteps(steps) {
145
160
  }
146
161
  return;
147
162
  }
148
- if (step['target'] !== undefined)
163
+ if (step['target'] !== undefined) {
149
164
  return;
150
- if (isGlobalPressCall(step))
165
+ }
166
+ if (isGlobalPressCall(step)) {
151
167
  return;
168
+ }
152
169
  throw new Error(`step ${String(i)} has no \`ref\` or \`target\`. ` +
153
170
  'Sequence steps take `ref` (from reticle_query/reticle_snapshot) or `target` ' +
154
171
  '(e.g. { testid } or { label }), the same locator reticle_act accepts. ' +
@@ -165,13 +182,15 @@ export function preflightAct(actArgs, until) {
165
182
  // the agent its trusted click had happened. See act-danger.
166
183
  assertNativeInputSupported(actArgs);
167
184
  const unevaluable = unevaluablePredicateReason(until);
168
- if (unevaluable !== undefined)
185
+ if (unevaluable !== undefined) {
169
186
  throw new Error(unevaluable);
187
+ }
170
188
  // A predicate that was already true when it was written answered `no-fault` / `already_true` —
171
189
  // verdict-shaped, and carrying no proof of anything the action did. See vacuousPredicateReason.
172
190
  const vacuous = vacuousPredicateReason(until);
173
- if (vacuous !== undefined)
191
+ if (vacuous !== undefined) {
174
192
  throw new Error(vacuous);
193
+ }
175
194
  }
176
195
  /**
177
196
  * Dispatch a write, or report that its outcome went unobserved.
@@ -191,8 +210,9 @@ export async function dispatchAct(send) {
191
210
  return await send();
192
211
  }
193
212
  catch (error) {
194
- if (error instanceof SessionReplacedError || isDocumentGoneError(error))
213
+ if (error instanceof SessionReplacedError || isDocumentGoneError(error)) {
195
214
  return null;
215
+ }
196
216
  throw error;
197
217
  }
198
218
  }
@@ -48,7 +48,9 @@ export async function resolveActTarget(session, args,
48
48
  * was ignored.
49
49
  */
50
50
  appearanceMs) {
51
- const ref = asString(args['ref']);
51
+ // A ref inside the target object (`target: { ref }`) is the same instruction as a top-level ref.
52
+ // Read as a query it named nothing, and the step failed with "Nothing was searched".
53
+ const ref = asString(args['ref']) ?? asString(asRecord(args['target'])['ref']);
52
54
  if (ref !== undefined && ref.length > 0) {
53
55
  // Refused here, at the boundary, and never re-read as a `target`: guessing what an arbitrary
54
56
  // string meant is the silent re-resolution the stale-ref refusal exists to prevent.
@@ -9,9 +9,16 @@
9
9
  * nothing on its own.
10
10
  */
11
11
  import type { Predicate } from '@reticlehq/core';
12
+ import { evaluatePredicate, type EvalResult } from '@reticlehq/engine/question/predicate/predicate.js';
12
13
  import { waitForInFlight } from './settle-in-flight.js';
13
14
  import { revertedAfterMatch } from './durable.js';
14
15
  type Waited = Parameters<typeof waitForInFlight>[0] & Parameters<typeof revertedAfterMatch>[0];
15
16
  /** Returns the sentence to fail with when the consequence reverted, else undefined. */
16
17
  export declare function finishAfterMatch(session: Waited, until: Predicate, since: number, baselines: Parameters<typeof revertedAfterMatch>[3], remaining: () => number): Promise<string | undefined>;
18
+ /**
19
+ * A green that rests only on hidden matches, read again until a match shows, for at most
20
+ * SHOWN_GRACE_MS. Returns the reading that shows one, or `verdict` unchanged. Nothing is predicted:
21
+ * the verdict is judged on what the page painted, and a node that stays hidden stays `hidden_match`.
22
+ */
23
+ export declare function awaitShownMatch(session: Parameters<typeof evaluatePredicate>[0], until: Predicate, since: number, baselines: Parameters<typeof evaluatePredicate>[4], verdict: EvalResult, remaining: () => number): Promise<EvalResult>;
17
24
  export {};
@@ -8,6 +8,8 @@
8
8
  * rest of the budget the caller already granted is spent here, in three steps, each deciding
9
9
  * nothing on its own.
10
10
  */
11
+ import { hiddenMatchNote } from '@reticlehq/engine/evidence/already-true.js';
12
+ import { evaluatePredicate, } from '@reticlehq/engine/question/predicate/predicate.js';
11
13
  import { waitForInFlight } from './settle-in-flight.js';
12
14
  import { waitForReaction } from './react-grace.js';
13
15
  import { revertedAfterMatch } from './durable.js';
@@ -28,3 +30,34 @@ export async function finishAfterMatch(session, until, since, baselines, remaini
28
30
  // 3. Then ask whether what held still holds — see revertedAfterMatch.
29
31
  return revertedAfterMatch(session, until, since, baselines);
30
32
  }
33
+ /**
34
+ * How long a match that holds only on hidden nodes gets to show. An entrance fade or a toast mounted
35
+ * at opacity 0 reads hidden for its first frame, and the wait matched on that frame. A node hidden
36
+ * by design pays this once, on a verdict that is `unknown` either way.
37
+ */
38
+ const SHOWN_GRACE_MS = 1000;
39
+ /** Poll cadence — the same as the reaction grace. */
40
+ const SHOWN_POLL_MS = 50;
41
+ /**
42
+ * A green that rests only on hidden matches, read again until a match shows, for at most
43
+ * SHOWN_GRACE_MS. Returns the reading that shows one, or `verdict` unchanged. Nothing is predicted:
44
+ * the verdict is judged on what the page painted, and a node that stays hidden stays `hidden_match`.
45
+ */
46
+ export async function awaitShownMatch(session, until, since, baselines, verdict, remaining) {
47
+ if (!verdict.pass || hiddenMatchNote(until, verdict.evidence) === undefined) {
48
+ return verdict;
49
+ }
50
+ // Counted in reads, not read off the session clock, so a clock that does not move still ends it.
51
+ const reads = Math.floor(Math.min(SHOWN_GRACE_MS, remaining()) / SHOWN_POLL_MS);
52
+ for (let read = 0; read < reads; read += 1) {
53
+ await new Promise((resolve) => setTimeout(resolve, SHOWN_POLL_MS));
54
+ const now = await evaluatePredicate(session, until, since, true, baselines);
55
+ if (!now.pass) {
56
+ return verdict;
57
+ }
58
+ if (hiddenMatchNote(until, now.evidence) === undefined) {
59
+ return now;
60
+ }
61
+ }
62
+ return verdict;
63
+ }
@@ -9,6 +9,8 @@ export type TargetResolution = {
9
9
  readonly kind: 'error';
10
10
  readonly message: string;
11
11
  };
12
+ /** The words before the nearest-target list in a no_match refusal. The repeat hint reads after it. */
13
+ export declare const NEAREST_TARGETS_LEAD = " The nearest targets on the page, most similar first: ";
12
14
  /**
13
15
  * Turn a target QUERY into the single ref an action can be performed on.
14
16
  *