@reticlehq/server 3.8.2 → 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 (224) 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/network-mock.js +3 -2
  114. package/dist/portal/input/real-input.d.ts +15 -0
  115. package/dist/portal/input/real-input.js +59 -3
  116. package/dist/portal/input/viewport-tools.js +6 -3
  117. package/dist/portal/pool/browser-pool.d.ts +5 -1
  118. package/dist/portal/pool/browser-pool.js +32 -38
  119. package/dist/portal/pool/context-permissions.d.ts +20 -17
  120. package/dist/portal/pool/context-permissions.js +63 -1
  121. package/dist/portal/pool/csp-widen.d.ts +62 -0
  122. package/dist/portal/pool/csp-widen.js +132 -0
  123. package/dist/portal/pool/lease-page-error.d.ts +29 -0
  124. package/dist/portal/pool/lease-page-error.js +18 -0
  125. package/dist/portal/pool/page-error-journal.d.ts +57 -0
  126. package/dist/portal/pool/page-error-journal.js +181 -0
  127. package/dist/portal/pool/playwright-launcher.js +21 -8
  128. package/dist/portal/pool/pool-contract.d.ts +22 -0
  129. package/dist/portal/pool/pool-lease.d.ts +35 -2
  130. package/dist/portal/pool/pool-lease.js +23 -0
  131. package/dist/portal/pool/zero-install.js +69 -1
  132. package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
  133. package/dist/portal/session/dev-server/dev-command.js +56 -8
  134. package/dist/portal/session/no-session-diagnosis.js +10 -3
  135. package/dist/portal/session/no-session-next-action.d.ts +12 -0
  136. package/dist/portal/session/no-session-next-action.js +44 -4
  137. package/dist/portal/session/no-session-watch.js +3 -1
  138. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  139. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  140. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  141. package/dist/portal/session/recorded-gaps.js +5 -2
  142. package/dist/portal/session/session-manager.js +40 -21
  143. package/dist/portal/session/session.js +4 -3
  144. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  145. package/dist/portal/session/zero-install-first-call.js +34 -2
  146. package/dist/start-wiring.d.ts +29 -2
  147. package/dist/start-wiring.js +73 -9
  148. package/dist/surface/mcp/mcp-proxy.js +10 -2
  149. package/dist/surface/mcp/mcp.js +5 -3
  150. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  151. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  152. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  153. package/dist/surface/tools/act/act-preflight.js +37 -17
  154. package/dist/surface/tools/act/act-target.js +3 -1
  155. package/dist/surface/tools/act/after-match.d.ts +7 -0
  156. package/dist/surface/tools/act/after-match.js +33 -0
  157. package/dist/surface/tools/act/resolve-target.d.ts +2 -0
  158. package/dist/surface/tools/act/resolve-target.js +35 -8
  159. package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
  160. package/dist/surface/tools/act/sequence-grade.js +8 -1
  161. package/dist/surface/tools/act-sequence-tool.js +3 -0
  162. package/dist/surface/tools/act-tools.js +10 -10
  163. package/dist/surface/tools/assert/assert-verdict.js +28 -1
  164. package/dist/surface/tools/browser-tools.js +75 -7
  165. package/dist/surface/tools/dynamic-tools.js +2 -1
  166. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  167. package/dist/surface/tools/empty-session-list.js +4 -2
  168. package/dist/surface/tools/error-recovery.js +43 -22
  169. package/dist/surface/tools/invoke-tool.js +27 -20
  170. package/dist/surface/tools/lease-hint.d.ts +7 -0
  171. package/dist/surface/tools/lease-hint.js +10 -3
  172. package/dist/surface/tools/lease-readiness.d.ts +21 -0
  173. package/dist/surface/tools/lease-readiness.js +47 -0
  174. package/dist/surface/tools/lease-seed.d.ts +2 -0
  175. package/dist/surface/tools/lease-seed.js +2 -1
  176. package/dist/surface/tools/lease-tools.js +43 -15
  177. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  178. package/dist/surface/tools/repeat-hint.js +171 -0
  179. package/dist/surface/tools/run-report.d.ts +2 -2
  180. package/dist/surface/tools/run-report.js +27 -15
  181. package/dist/surface/tools/tool-kit.d.ts +12 -0
  182. package/dist/surface/tools/tool-kit.js +7 -0
  183. package/dist/surface/tools/tools.js +26 -13
  184. package/dist/telemetry/argument-shape.js +16 -7
  185. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  186. package/dist/telemetry/cli-telemetry.js +15 -0
  187. package/dist/telemetry/init-telemetry.d.ts +2 -1
  188. package/dist/telemetry/init-telemetry.js +47 -2
  189. package/dist/telemetry/install-trace.js +23 -4
  190. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  191. package/dist/telemetry/onboarding-funnel.js +25 -3
  192. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  193. package/dist/telemetry/proxy-telemetry.js +2 -2
  194. package/dist/telemetry/session-metrics.d.ts +1 -1
  195. package/dist/telemetry/session-metrics.js +67 -27
  196. package/dist/telemetry/telemetry.d.ts +3 -1
  197. package/dist/telemetry/telemetry.js +60 -28
  198. package/dist/telemetry/tool-refused.d.ts +2 -2
  199. package/dist/telemetry/tool-refused.js +9 -2
  200. package/docs/architecture.md +3 -6
  201. package/docs/cli/doctor.mdx +41 -2
  202. package/docs/cli/help.mdx +4 -2
  203. package/docs/cli/license.mdx +4 -5
  204. package/docs/cli/setup.mdx +13 -2
  205. package/docs/cli/status.mdx +4 -0
  206. package/docs/cli.mdx +1 -1
  207. package/docs/deploy-checks.md +25 -0
  208. package/docs/desktop.mdx +1 -1
  209. package/docs/enterprise.md +15 -20
  210. package/docs/environment.mdx +1 -0
  211. package/docs/first-drive.md +1 -1
  212. package/docs/install-manual.mdx +16 -1
  213. package/docs/packages/engine.mdx +1 -1
  214. package/docs/packages/server.mdx +1 -1
  215. package/docs/packages/tauri.mdx +1 -1
  216. package/docs/packages/test.mdx +1 -1
  217. package/docs/packages.mdx +1 -1
  218. package/docs/platform-integration.md +1 -1
  219. package/docs/skill.md +1 -1
  220. package/docs/telemetry.md +4 -2
  221. package/package.json +13 -9
  222. package/LICENSE-ENTERPRISE +0 -37
  223. package/dist/features/ee/audit-log.d.ts +0 -14
  224. package/dist/features/ee/audit-log.js +0 -11
@@ -36,6 +36,7 @@ const YARN_DLX = 'yarn dlx';
36
36
  const INIT_VERB = 'init';
37
37
  const DEFAULT_PACKAGE_MANAGER = 'npm';
38
38
  const PACKAGE_JSON = 'package.json';
39
+ const PNPM_WORKSPACE = 'pnpm-workspace.yaml';
39
40
  /** `--port 4311` / `--port=4311`, the flag every dev server in this list accepts. */
40
41
  const PORT_FLAG = /--port[= ]\s*(\d+)/;
41
42
  /**
@@ -59,15 +60,17 @@ function readTextFile(path) {
59
60
  function pinnedPort(script) {
60
61
  const match = PORT_FLAG.exec(script) ?? PORT_SHORT_FLAG.exec(script) ?? PORT_ENV.exec(script);
61
62
  const digits = match?.[1];
62
- if (digits === undefined)
63
+ if (digits === undefined) {
63
64
  return undefined;
65
+ }
64
66
  const port = Number.parseInt(digits, 10);
65
67
  return Number.isSafeInteger(port) ? port : undefined;
66
68
  }
67
69
  /** The parsed package.json, or undefined for anything unreadable/unparseable. */
68
70
  function readManifest(raw) {
69
- if (raw === undefined)
71
+ if (raw === undefined) {
70
72
  return undefined;
73
+ }
71
74
  try {
72
75
  return JSON.parse(raw);
73
76
  }
@@ -77,11 +80,13 @@ function readManifest(raw) {
77
80
  }
78
81
  /** The scripts block of a parsed package.json, or an empty one. */
79
82
  function scriptsOf(manifest) {
80
- if (null === manifest || 'object' !== typeof manifest)
83
+ if (null === manifest || 'object' !== typeof manifest) {
81
84
  return {};
85
+ }
82
86
  const scripts = manifest.scripts;
83
- if (null === scripts || 'object' !== typeof scripts)
87
+ if (null === scripts || 'object' !== typeof scripts) {
84
88
  return {};
89
+ }
85
90
  return scripts;
86
91
  }
87
92
  /** The package manager the lockfiles under `directory` identify. Anything else is npm. */
@@ -98,8 +103,9 @@ function devCommandFor(directory, manager, read) {
98
103
  // spawns are the same.
99
104
  const desktop = desktopLaunch(manifest);
100
105
  if (desktop !== undefined) {
101
- if ('command' in desktop)
106
+ if ('command' in desktop) {
102
107
  return { command: desktop.command };
108
+ }
103
109
  const command = `${manager} run ${desktop.script}`;
104
110
  return {
105
111
  command: 0 < desktop.args.length ? `${command} ${desktop.args}` : command,
@@ -108,8 +114,9 @@ function devCommandFor(directory, manager, read) {
108
114
  }
109
115
  for (const script of DEV_SCRIPTS) {
110
116
  const body = scripts[script];
111
- if ('string' !== typeof body || 0 === body.trim().length)
117
+ if ('string' !== typeof body || 0 === body.trim().length) {
112
118
  continue;
119
+ }
113
120
  const port = pinnedPort(body);
114
121
  return {
115
122
  command: `${manager} run ${script}`,
@@ -119,6 +126,28 @@ function devCommandFor(directory, manager, read) {
119
126
  }
120
127
  return undefined;
121
128
  }
129
+ /** Folders never read as an app: dependencies, and anything hidden (`.git`, `.cache`). */
130
+ const NODE_MODULES = 'node_modules';
131
+ const HIDDEN_PREFIX = '.';
132
+ /**
133
+ * The direct child folders of `directory` that are apps with a dev command, sorted by name.
134
+ *
135
+ * Only for a folder with no package.json of its own. A task folder holds the app one level down, and
136
+ * no workspace file lists it, so `workspacePackageDirs` never finds it. Folders that are not apps
137
+ * (no package.json, or no dev script), `node_modules` and hidden folders are skipped.
138
+ */
139
+ export function childDevApps(directory, read = readTextFile, listDirs = readDirNames) {
140
+ if (read(join(directory, PACKAGE_JSON)) !== undefined) {
141
+ return [];
142
+ }
143
+ return listDirs(directory)
144
+ .filter((name) => NODE_MODULES !== name && !name.startsWith(HIDDEN_PREFIX))
145
+ .filter((name) => {
146
+ const child = join(directory, name);
147
+ return devCommandFor(child, packageManager(child, read), read) !== undefined;
148
+ })
149
+ .sort();
150
+ }
122
151
  /** Directory names under `path`, or none when it cannot be read. */
123
152
  function readDirNames(path) {
124
153
  try {
@@ -151,16 +180,35 @@ export function detectDevCommand(directory, read = readTextFile) {
151
180
  export function detectDevCommandInProject(directory, read = readTextFile, listDirs = readDirNames) {
152
181
  const manager = packageManager(directory, read);
153
182
  const here = devCommandFor(directory, manager, read);
154
- if (here !== undefined)
183
+ if (here !== undefined) {
155
184
  return here;
185
+ }
186
+ // No package.json and no pnpm workspace file: a task folder, with the app one level down. With two
187
+ // apps there, choosing the first would start the wrong one; the caller names them through
188
+ // `childDevApps`. With none, the workspace search below still looks a level deeper.
189
+ if (read(join(directory, PACKAGE_JSON)) === undefined &&
190
+ read(join(directory, PNPM_WORKSPACE)) === undefined) {
191
+ const [only, ...others] = childDevApps(directory, read, listDirs);
192
+ if (0 < others.length) {
193
+ return undefined;
194
+ }
195
+ if (only !== undefined) {
196
+ const child = join(directory, only);
197
+ const found = devCommandFor(child, packageManager(child, read), read);
198
+ if (found !== undefined) {
199
+ return { ...found, directory: only };
200
+ }
201
+ }
202
+ }
156
203
  const packages = workspacePackageDirs({
157
204
  readFile: (path) => read(join(directory, path)) ?? null,
158
205
  listDirs: (path) => listDirs(join(directory, path)),
159
206
  });
160
207
  for (const sub of packages) {
161
208
  const found = devCommandFor(join(directory, sub), manager, read);
162
- if (found !== undefined)
209
+ if (found !== undefined) {
163
210
  return { ...found, directory: sub };
211
+ }
164
212
  }
165
213
  return undefined;
166
214
  }
@@ -178,7 +178,7 @@ const INIT_CMD = '`npx @reticlehq/server init`';
178
178
  * against it, and it has to be read before the "nothing is listening" sentence rather than after —
179
179
  * the reader acts on the first claim.
180
180
  */
181
- function slowListenerClause(facts) {
181
+ function slowListenerClause(facts, after = '') {
182
182
  const slow = facts.slowListeners ?? [];
183
183
  if (0 === slow.length)
184
184
  return '';
@@ -186,7 +186,7 @@ function slowListenerClause(facts) {
186
186
  const subject = 1 === slow.length ? `Port ${ports} ACCEPTED` : `Ports ${ports} ACCEPTED`;
187
187
  return (`${subject} a connection but did not answer in time, which is what a server-rendered dev ` +
188
188
  'server compiling its first route looks like — so something IS running there and you should ' +
189
- 'open it rather than start it. Setting that aside: ');
189
+ `open it rather than start it.${after}`);
190
190
  }
191
191
  /**
192
192
  * The commonest first-run state there is (#320).
@@ -481,7 +481,7 @@ export function explainNoSession(facts) {
481
481
  // is expected rather than diagnostic.
482
482
  if (!initialized) {
483
483
  return reason(NoSessionReason.NO_LISTENER_NO_CONFIG, 'no browser session connected. Two things to weigh, and neither of them is proof. ' +
484
- `(1) ${slowListenerClause(facts)}Nothing is listening on the ports Reticle scans, so the ` +
484
+ `(1) ${slowListenerClause(facts, ' Setting that aside: ')}Nothing is listening on the ports Reticle scans, so the ` +
485
485
  'dev server may not be running: START IT YOURSELF, in the background, using the command ' +
486
486
  'in `next_action`, and tell the human in one line that it is running. ' +
487
487
  `(2) There is no \`.reticle.json\` in ${where}, which is the file ${INIT_CMD} writes, so ` +
@@ -493,6 +493,13 @@ export function explainNoSession(facts) {
493
493
  'the Vite or Babel plugin carries the SDK without that file at all.' +
494
494
  `${searchedClause(facts)}${siblingListenerClause(facts)} ${leaseAdvice(URL_THEN_LEASE, facts)}`);
495
495
  }
496
+ // A wired project whose own port accepted a connection and answered nothing yet is compiling,
497
+ // not down. "Start it yourself" here starts a second dev server.
498
+ if ((facts.slowListeners ?? []).length > 0) {
499
+ return reason(NoSessionReason.NO_LISTENER, `${stallClause(facts)}no browser session connected, and this daemon has never seen one. ` +
500
+ `${slowListenerClause(facts)} Do not start a second dev server. ` +
501
+ `Wait for the first compile to finish, then open the app (the command is in \`next_action\`). ${RETRY}`, `${leaseAdvice(URL_THEN_LEASE, facts)}${siblingListenerClause(facts)}`);
502
+ }
496
503
  return reason(NoSessionReason.NO_LISTENER, `${stallClause(facts)}no browser session connected, and this daemon has never seen one. ${openTheApp(facts)} ` +
497
504
  'Nothing is listening on the ports Reticle scans either, and the usual reason is a dev ' +
498
505
  'server that is not running: start it yourself in the background with the command in ' +
@@ -102,6 +102,11 @@ interface NextActionFacts {
102
102
  * project's own runner (see `initCommandIn`). Absent means the generic npx spelling.
103
103
  */
104
104
  init?: string;
105
+ /**
106
+ * Several direct child folders are apps with a dev script, and this folder has no package.json.
107
+ * Absent or short means there is no choice to hand the agent. See `childDevApps`.
108
+ */
109
+ childApps?: readonly string[];
105
110
  }
106
111
  /** The port a tombstone URL names, when it names one — anything unparseable names no port. */
107
112
  export declare function portOfUrl(url: string): number | undefined;
@@ -123,6 +128,13 @@ export declare const INIT_COMMAND = "npx @reticlehq/server init";
123
128
  * agent asks first. The lease changes nothing, so it needs no question.
124
129
  */
125
130
  export declare function wiringConsent(init?: string): string;
131
+ /**
132
+ * The offer for the first session in an unwired web app: check it now with the lease, then ask
133
+ * before `init`. The one text for the plugin's SessionStart hook and the first tool call of each
134
+ * agent connection. The hook cannot import this, so it keeps a copy, and
135
+ * `plugin-init-nudge.test.ts` fails when the two are not the same.
136
+ */
137
+ export declare function firstSessionOffer(init?: string): string;
126
138
  /** The zero-install call, with the URL only when it can be named without guessing. */
127
139
  export declare function zeroInstallLease(url: string | undefined): string;
128
140
  export declare function nextActionFor(facts: NextActionFacts): NoSessionNextAction;
@@ -18,8 +18,9 @@ import { detectNonJsEcosystem, noPackageJsonMessage } from '@reticlehq/init';
18
18
  export function portOfUrl(url) {
19
19
  try {
20
20
  const raw = new URL(url).port;
21
- if ('' === raw)
21
+ if ('' === raw) {
22
22
  return undefined;
23
+ }
23
24
  const port = Number(raw);
24
25
  return Number.isSafeInteger(port) && port > 0 ? port : undefined;
25
26
  }
@@ -45,8 +46,9 @@ export function departedPortGone(facts) {
45
46
  * for a url that does not parse, or one with no port to name (a desktop webview's opaque origin).
46
47
  */
47
48
  function reopenableUrl(raw) {
48
- if (raw === undefined)
49
+ if (raw === undefined) {
49
50
  return undefined;
51
+ }
50
52
  let url;
51
53
  try {
52
54
  url = new URL(raw);
@@ -54,10 +56,12 @@ function reopenableUrl(raw) {
54
56
  catch {
55
57
  return undefined;
56
58
  }
57
- if ('http:' !== url.protocol && 'https:' !== url.protocol)
59
+ if ('http:' !== url.protocol && 'https:' !== url.protocol) {
58
60
  return undefined;
59
- for (const param of Object.values(RETICLE_URL_PARAM))
61
+ }
62
+ for (const param of Object.values(RETICLE_URL_PARAM)) {
60
63
  url.searchParams.delete(param);
64
+ }
61
65
  const port = '' === url.port ? ('https:' === url.protocol ? 443 : 80) : Number(url.port);
62
66
  return { url: url.toString(), port };
63
67
  }
@@ -78,6 +82,17 @@ export function wiringConsent(init = INIT_COMMAND) {
78
82
  'build config and starts the dev server. Ask the user before you wire the app. If they ' +
79
83
  `agree, run \`${init}\` in the app's folder. If they decline, do not ask again.`);
80
84
  }
85
+ /**
86
+ * The offer for the first session in an unwired web app: check it now with the lease, then ask
87
+ * before `init`. The one text for the plugin's SessionStart hook and the first tool call of each
88
+ * agent connection. The hook cannot import this, so it keeps a copy, and
89
+ * `plugin-init-nudge.test.ts` fails when the two are not the same.
90
+ */
91
+ export function firstSessionOffer(init = INIT_COMMAND) {
92
+ return (`This web app can be checked now with no install: \`${zeroInstallLease(undefined)}\` opens ` +
93
+ "the running app in Reticle's own browser and changes no file. To keep Reticle wired to this " +
94
+ `app: ${wiringConsent(init)}`);
95
+ }
81
96
  const OPEN_COMMAND = `${CLI} open`;
82
97
  const LOCALHOST = 'http://localhost';
83
98
  /**
@@ -243,6 +258,19 @@ export function nextActionFor(facts) {
243
258
  'args: { action: "acquire", url, projectId } }, or restart the daemon from the app\'s own directory.',
244
259
  };
245
260
  }
261
+ // A wired project whose only port accepted a connection and answered nothing yet: a dev server
262
+ // compiling its first route. It is up, so a start command would start a second one.
263
+ const compiling = facts.slowListeners?.[0];
264
+ if (true === facts.initialized && 0 === facts.listening.length && compiling !== undefined) {
265
+ return {
266
+ action: NoSessionAction.OPEN_APP,
267
+ command: `${OPEN_COMMAND} ${LOCALHOST}:${String(compiling)}`,
268
+ port: compiling,
269
+ reason: `port ${String(compiling)} accepted a connection but did not answer in time, which is a dev ` +
270
+ 'server compiling its first route. The app IS running. Do not start a second dev server. ' +
271
+ 'Wait for the compile to finish, then open it.',
272
+ };
273
+ }
246
274
  if (0 === facts.listening.length || departedPortIsGone) {
247
275
  const dev = facts.dev;
248
276
  // The start advice below follows a departed session whose port is gone. Name it, so the restart
@@ -255,6 +283,18 @@ export function nextActionFor(facts) {
255
283
  ? "nothing answers on the port this project's app was last on, so the app is probably not running — "
256
284
  : notThisProject(facts.otherListeners);
257
285
  if (dev === undefined) {
286
+ const childApps = facts.childApps ?? [];
287
+ if (1 < childApps.length) {
288
+ return {
289
+ action: NoSessionAction.START_DEV_SERVER,
290
+ reason: notRunning +
291
+ `but this folder has no package.json, and ${childApps.length} child folders are apps ` +
292
+ `with a dev script: ${childApps.join(', ')}. Reticle cannot tell which one you mean, so ` +
293
+ 'there is no command to hand you. Pick one, then run Reticle from that folder or ask ' +
294
+ 'the human which app to start.' +
295
+ stoppedNote,
296
+ };
297
+ }
258
298
  if (facts.exists !== undefined && !facts.exists('package.json')) {
259
299
  const ecosystem = detectNonJsEcosystem(facts.exists);
260
300
  if (ecosystem !== undefined) {
@@ -17,7 +17,7 @@ import { join, resolve } from 'node:path';
17
17
  import { callingClientDirectory } from '../../hooks/client-directory.js';
18
18
  import { registeredElsewhere } from '../../memory/recall/registered-projects.js';
19
19
  import { explainNoSession } from './no-session-diagnosis.js';
20
- import { detectDevCommandInProject, initCommandIn } from './dev-server/dev-command.js';
20
+ import { childDevApps, detectDevCommandInProject, initCommandIn, } from './dev-server/dev-command.js';
21
21
  import { nextActionFor, portOfUrl, renderNextAction } from './no-session-next-action.js';
22
22
  import { DEV_SERVER_PORTS, readProjectFramework, readProjectId, readProjectIsDesktop, readProjectPort, } from '../../command/cli/ports/resolve/cli-port.js';
23
23
  import { discoverProjectConfigs } from '../../command/cli/config/config-discovery.js';
@@ -480,6 +480,7 @@ export function startNoSessionWatch(options) {
480
480
  ...(known.departedTo === undefined ? {} : { departedTo: known.departedTo }),
481
481
  }),
482
482
  dev: detectDevCommandInProject(dir),
483
+ childApps: childDevApps(dir),
483
484
  init: initCommandIn(dir),
484
485
  });
485
486
  return { facts, next };
@@ -540,6 +541,7 @@ export function startNoSessionWatch(options) {
540
541
  // Read when asked, like everything else here: a `package.json` can gain a dev script, and a
541
542
  // daemon that cached "there is none" at boot would keep saying so for the rest of the day.
542
543
  dev: detectDevCommandInProject(directory),
544
+ childApps: childDevApps(directory),
543
545
  // This project's own runner and app folder, so the agent can wire it itself.
544
546
  init: initCommandIn(directory),
545
547
  });
@@ -18,8 +18,14 @@
18
18
  * answers rather than a set lookup at the call site.
19
19
  */
20
20
  import { type CommandResult } from '@reticlehq/core';
21
- /** Why nothing was sent, in a sentence, or undefined when the command should go. */
22
- export declare function refusalForUndeclared(name: string, declared: readonly string[] | undefined): string | undefined;
21
+ /**
22
+ * Why nothing was sent, in a sentence, or undefined when the command should go.
23
+ *
24
+ * `behind` is the caller's sentence for a page SDK older than this daemon. A newer command
25
+ * (`render`, `match`) is then missing because of the version, and the list of commands alone
26
+ * does not say that.
27
+ */
28
+ export declare function refusalForUndeclared(name: string, declared: readonly string[] | undefined, behind?: string): string | undefined;
23
29
  /**
24
30
  * The refusal as a finished result, or undefined when the command should be sent.
25
31
  *
@@ -27,4 +33,4 @@ export declare function refusalForUndeclared(name: string, declared: readonly st
27
33
  * cannot get the envelope subtly wrong -- an `ok: true` refusal would be read as a command that
28
34
  * worked, which is the one shape this must never produce.
29
35
  */
30
- export declare function refusedResult(name: string, declared: readonly string[] | undefined, id: string): CommandResult | undefined;
36
+ export declare function refusedResult(name: string, declared: readonly string[] | undefined, id: string, behind?: string): CommandResult | undefined;
@@ -18,16 +18,25 @@
18
18
  * answers rather than a set lookup at the call site.
19
19
  */
20
20
  import { MessageKind } from '@reticlehq/core';
21
- /** Why nothing was sent, in a sentence, or undefined when the command should go. */
22
- export function refusalForUndeclared(name, declared) {
21
+ /**
22
+ * Why nothing was sent, in a sentence, or undefined when the command should go.
23
+ *
24
+ * `behind` is the caller's sentence for a page SDK older than this daemon. A newer command
25
+ * (`render`, `match`) is then missing because of the version, and the list of commands alone
26
+ * does not say that.
27
+ */
28
+ export function refusalForUndeclared(name, declared, behind) {
23
29
  // Too old to say. Unknown, never "nothing".
24
- if (declared === undefined)
30
+ if (declared === undefined) {
25
31
  return undefined;
26
- if (declared.includes(name))
32
+ }
33
+ if (declared.includes(name)) {
27
34
  return undefined;
35
+ }
28
36
  return (`this page does not serve "${name}". It declared ${String(declared.length)} command(s) when ` +
29
37
  `it connected, and this is not one of them: ${declared.join(', ')}. Nothing was sent, so ` +
30
- 'nothing happened -- this is a refusal, not a failure of the app.');
38
+ 'nothing happened -- this is a refusal, not a failure of the app.' +
39
+ (behind === undefined ? '' : ` ${behind}`));
31
40
  }
32
41
  /**
33
42
  * The refusal as a finished result, or undefined when the command should be sent.
@@ -36,8 +45,8 @@ export function refusalForUndeclared(name, declared) {
36
45
  * cannot get the envelope subtly wrong -- an `ok: true` refusal would be read as a command that
37
46
  * worked, which is the one shape this must never produce.
38
47
  */
39
- export function refusedResult(name, declared, id) {
40
- const error = refusalForUndeclared(name, declared);
48
+ export function refusedResult(name, declared, id, behind) {
49
+ const error = refusalForUndeclared(name, declared, behind);
41
50
  return error === undefined
42
51
  ? undefined
43
52
  : { kind: MessageKind.COMMAND_RESULT, id, ok: false, error };
@@ -49,7 +49,7 @@ export declare function probeMarkers(tabs: readonly ProbedTab[]): Promise<boolea
49
49
  /**
50
50
  * The `engineBlindSpots` field for a verdict result: what this engine could not observe in the
51
51
  * session so far. Notes this verdict's own comparison first, so a `compare` clears the arithmetic
52
- * spot on the result that ran it. `claims`: the inline intent, then the run's declared intents.
52
+ * spot on the result that ran it. `claims`: the inline intent, or else the run's declared intents.
53
53
  */
54
54
  export declare function engineBlindSpotsField(sessionId: string, predicate: Predicate | undefined, intent: unknown, declared: readonly {
55
55
  statement: string;
@@ -162,12 +162,15 @@ const comparedSeen = new Set();
162
162
  /**
163
163
  * The `engineBlindSpots` field for a verdict result: what this engine could not observe in the
164
164
  * session so far. Notes this verdict's own comparison first, so a `compare` clears the arithmetic
165
- * spot on the result that ran it. `claims`: the inline intent, then the run's declared intents.
165
+ * spot on the result that ran it. `claims`: the inline intent, or else the run's declared intents.
166
166
  */
167
167
  export function engineBlindSpotsField(sessionId, predicate, intent, declared) {
168
168
  if (computesComparison(predicate))
169
169
  comparedSeen.add(sessionId);
170
- const claims = [text(intent), ...declared.map((each) => each.statement)];
170
+ // A call that names its own intent is judged on that intent alone. The run's open intents can be
171
+ // an earlier task's, and judging them reported that task's claim on this one (#1938).
172
+ const own = text(intent);
173
+ const claims = own === undefined ? declared.map((each) => each.statement) : [own];
171
174
  return {
172
175
  [ENGINE_BLIND_SPOTS_FIELD]: engineBlindSpots({
173
176
  canvasSeen: canvasSeen.has(sessionId),
@@ -1,4 +1,4 @@
1
- import { NO_SESSION_CONNECTED_ERROR, originOf, samePath } from '@reticlehq/core';
1
+ import { NO_SESSION_CONNECTED_ERROR, NO_SESSION_NAVIGATE_FIX, originOf, samePath, } from '@reticlehq/core';
2
2
  import { notePendingNoSessionReason } from '../../telemetry/tool-refused.js';
3
3
  import { declareDrivenRedactionKeys, forgetDrivenRedactionKeys, } from '../input/driven-redaction.js';
4
4
  import { Session } from './session.js';
@@ -14,8 +14,9 @@ export { originOf };
14
14
  * no scope, return the list unchanged (legacy single-/multi-session behavior).
15
15
  */
16
16
  function scopeSessions(sessions, scope) {
17
- if (scope === undefined)
17
+ if (scope === undefined) {
18
18
  return sessions;
19
+ }
19
20
  if (scope.projectId !== undefined) {
20
21
  return sessions.filter((s) => s.projectId === scope.projectId);
21
22
  }
@@ -124,8 +125,9 @@ export class SessionManager {
124
125
  }
125
126
  add(session) {
126
127
  this.#everConnected = true;
127
- if (session.projectId !== undefined)
128
+ if (session.projectId !== undefined) {
128
129
  this.#connectedProjects.add(session.projectId);
130
+ }
129
131
  // Ordering, not lifetime: this is what makes a remembered refusal distinguishable from a live
130
132
  // one. Here for the same reason `#everConnected` is — `add` is the one method every path that
131
133
  // registers a session goes through.
@@ -177,8 +179,9 @@ export class SessionManager {
177
179
  }
178
180
  #parked = new Set();
179
181
  remove(session) {
180
- if (this.#sessions.get(session.id) !== session)
182
+ if (this.#sessions.get(session.id) !== session) {
181
183
  return false;
184
+ }
182
185
  session.rejectAll(SESSION_DISCONNECTED_REASON);
183
186
  forgetDrivenRedactionKeys(session.id);
184
187
  // Recorded, not forgotten: a session that comes back needs its gap measured, and a listing after
@@ -189,10 +192,12 @@ export class SessionManager {
189
192
  }
190
193
  #rememberTombstone(session) {
191
194
  const origin = originOf(session.url);
192
- if (origin === undefined)
195
+ if (origin === undefined) {
193
196
  return;
194
- if (this.#tombstones.has(session.id))
197
+ }
198
+ if (this.#tombstones.has(session.id)) {
195
199
  this.#tombstones.delete(session.id);
200
+ }
196
201
  this.#siblingsAtDeparture.set(session.id, new Set([...this.#sessions.values()]
197
202
  .filter((s) => s !== session && openedBeforeDeparture(s, session))
198
203
  .map((s) => s.id)));
@@ -206,16 +211,18 @@ export class SessionManager {
206
211
  });
207
212
  while (this.#tombstones.size > MAX_TOMBSTONES) {
208
213
  const oldest = this.#tombstones.keys().next().value;
209
- if (oldest === undefined)
214
+ if (oldest === undefined) {
210
215
  break;
216
+ }
211
217
  this.#tombstones.delete(oldest);
212
218
  this.#siblingsAtDeparture.delete(oldest);
213
219
  }
214
220
  }
215
221
  #successorOf(sessionId) {
216
222
  const departed = this.#tombstones.get(sessionId);
217
- if (departed === undefined)
223
+ if (departed === undefined) {
218
224
  return undefined;
225
+ }
219
226
  const siblings = this.#siblingsAtDeparture.get(sessionId);
220
227
  const picked = pickDocumentSuccessor(this.all()
221
228
  .filter((s) => true !== siblings?.has(s.id))
@@ -224,8 +231,9 @@ export class SessionManager {
224
231
  url: s.url,
225
232
  ...(s.projectId === undefined ? {} : { projectId: s.projectId }),
226
233
  })), departed);
227
- if (picked === undefined)
234
+ if (picked === undefined) {
228
235
  return undefined;
236
+ }
229
237
  return this.#sessions.get(picked.id);
230
238
  }
231
239
  get(sessionId) {
@@ -396,8 +404,9 @@ export class SessionManager {
396
404
  */
397
405
  noteClosure(reason, at, page) {
398
406
  this.#recentClosures.push({ at, reason, ...(page === undefined ? {} : { page }) });
399
- if (this.#recentClosures.length > MAX_REMEMBERED_CLOSURES)
407
+ if (this.#recentClosures.length > MAX_REMEMBERED_CLOSURES) {
400
408
  this.#recentClosures.shift();
409
+ }
401
410
  this.#connectedSinceLastClosure = false;
402
411
  }
403
412
  /** The most recent bridge-initiated close, if any. */
@@ -443,8 +452,9 @@ export class SessionManager {
443
452
  */
444
453
  lastKnown() {
445
454
  let last;
446
- for (const record of this.#tombstones.values())
455
+ for (const record of this.#tombstones.values()) {
447
456
  last = record;
457
+ }
448
458
  return last;
449
459
  }
450
460
  /**
@@ -456,8 +466,9 @@ export class SessionManager {
456
466
  lastKnownFor(projectId) {
457
467
  let last;
458
468
  for (const record of this.#tombstones.values()) {
459
- if (record.projectId === projectId)
469
+ if (record.projectId === projectId) {
460
470
  last = record;
471
+ }
461
472
  }
462
473
  return last;
463
474
  }
@@ -496,9 +507,10 @@ export class SessionManager {
496
507
  const ended = sessionId.startsWith(LEASE_ID_PREFIX)
497
508
  ? ` Lease '${sessionId}' has ended (it expired or was released); lease a new tab to go on.`
498
509
  : '';
499
- if (0 === own.length)
510
+ if (0 === own.length) {
500
511
  return (`no connected session with id '${sessionId}'.${ended} ` +
501
512
  'Omit sessionId to let Reticle scope to your project, or lease a tab of your own.');
513
+ }
502
514
  const named = own.map((s) => `'${s.id}' (${s.url})`).join(', ');
503
515
  return (`no connected session with id '${sessionId}'.${ended} Your app's tabs: ${named}. ` +
504
516
  'Retry with one of those, or omit sessionId entirely and let Reticle scope to your project.');
@@ -528,7 +540,7 @@ export class SessionManager {
528
540
  // diagnosis is computed HERE and the refusal is classified from the message downstream, so
529
541
  // without this every no-session refusal lands in one undifferentiated bucket (#615).
530
542
  notePendingNoSessionReason(this.#noSessionReason?.());
531
- throw new Error(`${hint ?? NO_SESSION_CONNECTED_ERROR}${refusal}`);
543
+ throw new Error(`${hint ?? NO_SESSION_CONNECTED_ERROR}${refusal}${NO_SESSION_NAVIGATE_FIX}`);
532
544
  }
533
545
  // Scope to the agent's active project FIRST, so a stray tab from another app/origin (e.g. a
534
546
  // leftover dashboard on a different port) is structurally unselectable — it never enters the
@@ -541,8 +553,9 @@ export class SessionManager {
541
553
  const byCheckout = this.#checkoutScope === undefined
542
554
  ? { sessions: inProject }
543
555
  : scopeToCallerCheckout(inProject, this.#checkoutScope.caller(), this.#checkoutScope.ofTab);
544
- if (byCheckout.refusal !== undefined)
556
+ if (byCheckout.refusal !== undefined) {
545
557
  throw new Error(byCheckout.refusal);
558
+ }
546
559
  const all = byCheckout.sessions;
547
560
  if (0 === all.length) {
548
561
  // Sessions exist, but none belong to the scoped project — never fall back to a foreign tab.
@@ -553,8 +566,9 @@ export class SessionManager {
553
566
  }
554
567
  if (1 === all.length) {
555
568
  const [only] = all;
556
- if (only === undefined)
569
+ if (only === undefined) {
557
570
  throw new Error('session lookup failed');
571
+ }
558
572
  only.markAgentActivity();
559
573
  return only;
560
574
  }
@@ -601,8 +615,9 @@ export class SessionManager {
601
615
  // Sort candidates by recency (ascending lastSeenMs = most recently active first).
602
616
  candidates.sort((a, b) => a.ms - b.ms);
603
617
  const [best, runnerUp] = candidates;
604
- if (best === undefined)
618
+ if (best === undefined) {
605
619
  throw new Error('session lookup failed');
620
+ }
606
621
  // Only auto-select if there is a clear winner.
607
622
  //
608
623
  // When at least one non-throttled (focused/visible) session exists, require a >1 s recency
@@ -649,18 +664,21 @@ export class SessionManager {
649
664
  * dropped. A tab whose checkout cannot be read stays, because it can be the agent's own.
650
665
  */
651
666
  export function scopeToCallerCheckout(sessions, caller, checkoutOfTab) {
652
- if (caller === undefined)
667
+ if (caller === undefined) {
653
668
  return { sessions: [...sessions] };
669
+ }
654
670
  const placed = sessions.map((session) => ({ session, checkout: checkoutOfTab(session) }));
655
671
  const mine = placed.filter((p) => isSame(p.checkout?.root, caller.root)).map((p) => p.session);
656
- if (mine.length > 0)
672
+ if (mine.length > 0) {
657
673
  return { sessions: mine };
674
+ }
658
675
  const siblings = placed.filter((p) => isSame(p.checkout?.commonDir, caller.commonDir));
659
676
  if (siblings.length !== placed.length) {
660
677
  return { sessions: placed.filter((p) => !siblings.includes(p)).map((p) => p.session) };
661
678
  }
662
- if (0 === siblings.length)
679
+ if (0 === siblings.length) {
663
680
  return { sessions: [] };
681
+ }
664
682
  const listed = siblings
665
683
  .map((p) => `${describeCheckout(p.checkout)} (sessionId '${p.session.id}', ${p.session.url})`)
666
684
  .join(', ');
@@ -675,8 +693,9 @@ export function scopeToCallerCheckout(sessions, caller, checkoutOfTab) {
675
693
  const isSame = (a, b) => a !== undefined && samePath(a, b);
676
694
  /** `/wt/a (branch feat/a)`, or the folder alone on a detached HEAD. */
677
695
  export function describeCheckout(checkout) {
678
- if (checkout === undefined)
696
+ if (checkout === undefined) {
679
697
  return 'an unknown folder';
698
+ }
680
699
  return checkout.branch === undefined
681
700
  ? checkout.root
682
701
  : `${checkout.root} (branch ${checkout.branch})`;
@@ -1,6 +1,7 @@
1
1
  import { noteOwnReload, notePageLoaded } from '../../features/harness/drive-runs.js';
2
2
  import { asProjectId } from '@reticlehq/core';
3
3
  import { narrationArgs } from './narration-args.js';
4
+ import { sdkBehindNote } from '../../command/version/version-skew.js';
4
5
  import { refusedResult } from './page-commands/undeclared-command.js';
5
6
  import { impactSnapshot, recordImpact } from '../../memory/impact/impact-recorder.js';
6
7
  import { callingAgent } from '../../hooks/coding-agents.js';
@@ -580,9 +581,9 @@ export class Session {
580
581
  this.recordActedRef(ref);
581
582
  if (name === ReticleCommand.REFRESH)
582
583
  noteOwnReload(this.id, this.#clock());
583
- // Refused before it is sent, when the page said it does not serve this. The rule, and why
584
- // an absent declaration is not an empty list, live in `undeclared-command.ts`.
585
- const refused = refusedResult(name, this.commands, `${COMMAND_ID_PREFIX}refused`);
584
+ // Refused before it is sent when the page did not declare it. See `undeclared-command.ts`.
585
+ const behind = sdkBehindNote(this.sdkVersion);
586
+ const refused = refusedResult(name, this.commands, `${COMMAND_ID_PREFIX}refused`, behind);
586
587
  if (refused !== undefined)
587
588
  return Promise.resolve(refused);
588
589
  // The page already re-dialled under this same id. Nothing has been sent yet, so handing the