@reticlehq/server 2.8.0 → 2.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 (292) hide show
  1. package/SKILL.md +130 -813
  2. package/dist/bridge/bridge.d.ts +23 -0
  3. package/dist/bridge/bridge.js +80 -2
  4. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  5. package/dist/bridge/bridge.test-harness.js +6 -4
  6. package/dist/capsule/causal-summary.d.ts +16 -1
  7. package/dist/capsule/causal-summary.js +2 -1
  8. package/dist/cli/cli-doctor.js +41 -13
  9. package/dist/cli/cli-flow-commands.js +8 -8
  10. package/dist/cli/cli-launch.d.ts +0 -17
  11. package/dist/cli/cli-launch.js +0 -28
  12. package/dist/cli/cli-port.d.ts +24 -0
  13. package/dist/cli/cli-port.js +66 -35
  14. package/dist/cli/cli-verify.d.ts +21 -0
  15. package/dist/cli/cli-verify.js +45 -1
  16. package/dist/cli/config-discovery.d.ts +35 -0
  17. package/dist/cli/config-discovery.js +163 -0
  18. package/dist/cli/doctor-daemon-line.js +4 -1
  19. package/dist/cli/doctor-project-line.d.ts +37 -0
  20. package/dist/cli/doctor-project-line.js +35 -0
  21. package/dist/cli/doctor-rows.d.ts +51 -0
  22. package/dist/cli/doctor-rows.js +54 -0
  23. package/dist/cli/doctor-sessions-line.js +6 -3
  24. package/dist/cli/drive-attach.d.ts +99 -0
  25. package/dist/cli/drive-attach.js +203 -0
  26. package/dist/cli/drive-command.d.ts +13 -0
  27. package/dist/cli/drive-command.js +103 -0
  28. package/dist/cli/open-note.d.ts +20 -0
  29. package/dist/cli/open-note.js +37 -0
  30. package/dist/cli/status-next-action.d.ts +37 -0
  31. package/dist/cli/status-next-action.js +61 -0
  32. package/dist/cli.js +65 -70
  33. package/dist/events/contradictions.d.ts +32 -0
  34. package/dist/events/contradictions.js +9 -5
  35. package/dist/events/declared.d.ts +45 -0
  36. package/dist/events/declared.js +73 -0
  37. package/dist/events/echo-mismatch.d.ts +8 -2
  38. package/dist/events/echo-mismatch.js +33 -5
  39. package/dist/events/event-filters.d.ts +1 -1
  40. package/dist/events/event-filters.js +7 -4
  41. package/dist/events/predicate-eval.d.ts +17 -91
  42. package/dist/events/predicate-eval.js +85 -326
  43. package/dist/events/predicate-parse.js +31 -3
  44. package/dist/events/predicate-route.d.ts +32 -0
  45. package/dist/events/predicate-route.js +102 -0
  46. package/dist/events/predicate-schema.d.ts +99 -0
  47. package/dist/events/predicate-schema.js +309 -0
  48. package/dist/events/predicate.d.ts +8 -0
  49. package/dist/events/predicate.js +127 -5
  50. package/dist/flows/annotate-tools.js +3 -0
  51. package/dist/flows/annotate.js +18 -4
  52. package/dist/flows/coverage.d.ts +38 -5
  53. package/dist/flows/coverage.js +30 -4
  54. package/dist/flows/decision.d.ts +15 -0
  55. package/dist/flows/decision.js +1 -1
  56. package/dist/flows/flow-replay-run.js +8 -1
  57. package/dist/flows/flow-tools.js +9 -1
  58. package/dist/flows/replay.js +3 -0
  59. package/dist/honesty/accepted-write.d.ts +3 -0
  60. package/dist/honesty/accepted-write.js +8 -3
  61. package/dist/honesty/already-true.js +15 -0
  62. package/dist/honesty/blind-spots.js +4 -0
  63. package/dist/honesty/honesty.d.ts +11 -0
  64. package/dist/honesty/honesty.js +1 -0
  65. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  66. package/dist/honesty/uncaptured-bodies.js +9 -2
  67. package/dist/honesty/unread-outcome.d.ts +8 -0
  68. package/dist/honesty/unread-outcome.js +36 -9
  69. package/dist/honesty/unsettled.d.ts +9 -0
  70. package/dist/honesty/unsettled.js +7 -3
  71. package/dist/honesty/verified.d.ts +29 -3
  72. package/dist/honesty/verified.js +51 -8
  73. package/dist/http-server.d.ts +19 -0
  74. package/dist/http-server.js +202 -4
  75. package/dist/impact/impact-recorder.d.ts +23 -0
  76. package/dist/impact/impact-recorder.js +76 -0
  77. package/dist/impact/impact-store.d.ts +47 -0
  78. package/dist/impact/impact-store.js +171 -0
  79. package/dist/index.d.ts +7 -0
  80. package/dist/index.js +44 -10
  81. package/dist/init/agent-rules.d.ts +20 -0
  82. package/dist/init/agent-rules.js +36 -0
  83. package/dist/init/astro-patch.js +27 -1
  84. package/dist/init/confirm.d.ts +21 -3
  85. package/dist/init/confirm.js +32 -5
  86. package/dist/init/connect-steps.d.ts +2 -0
  87. package/dist/init/connect-steps.js +50 -0
  88. package/dist/init/csp-check.d.ts +38 -0
  89. package/dist/init/csp-check.js +92 -0
  90. package/dist/init/csp-doctor.d.ts +25 -0
  91. package/dist/init/csp-doctor.js +49 -0
  92. package/dist/init/cursor.js +6 -9
  93. package/dist/init/detect.d.ts +8 -0
  94. package/dist/init/detect.js +33 -3
  95. package/dist/init/mcp-clients.d.ts +1 -0
  96. package/dist/init/mcp-clients.js +51 -7
  97. package/dist/init/mcp.d.ts +15 -0
  98. package/dist/init/mcp.js +19 -0
  99. package/dist/init/plan-framework.d.ts +10 -0
  100. package/dist/init/plan-framework.js +25 -1
  101. package/dist/init/plan.d.ts +7 -2
  102. package/dist/init/plan.js +66 -30
  103. package/dist/init/run.js +11 -5
  104. package/dist/init/slash-command.d.ts +1 -1
  105. package/dist/init/slash-command.js +12 -4
  106. package/dist/init/snippets.d.ts +1 -1
  107. package/dist/init/snippets.js +35 -7
  108. package/dist/init/vite-config.js +24 -2
  109. package/dist/input/network-mock-tools.js +8 -1
  110. package/dist/input/scroll-find.d.ts +6 -0
  111. package/dist/input/scroll-find.js +17 -2
  112. package/dist/input/scroll-tools.js +4 -0
  113. package/dist/journal/attach-journal.d.ts +9 -1
  114. package/dist/journal/attach-journal.js +13 -2
  115. package/dist/journal/journal-query.d.ts +13 -0
  116. package/dist/journal/journal-query.js +28 -3
  117. package/dist/journal/workspace-gitignore.d.ts +10 -0
  118. package/dist/journal/workspace-gitignore.js +59 -0
  119. package/dist/mcp/mcp-proxy.d.ts +5 -66
  120. package/dist/mcp/mcp-proxy.js +180 -163
  121. package/dist/mcp/mcp.d.ts +10 -1
  122. package/dist/mcp/mcp.js +47 -27
  123. package/dist/mcp/proxy-handshake.d.ts +82 -1
  124. package/dist/mcp/proxy-handshake.js +109 -2
  125. package/dist/mcp/proxy-lifecycle.d.ts +10 -1
  126. package/dist/mcp/proxy-lifecycle.js +11 -2
  127. package/dist/mcp/proxy-log.d.ts +63 -0
  128. package/dist/mcp/proxy-log.js +132 -0
  129. package/dist/mcp/server-instructions.d.ts +17 -0
  130. package/dist/mcp/server-instructions.js +56 -0
  131. package/dist/mcp/shared-params.d.ts +27 -0
  132. package/dist/mcp/shared-params.js +36 -0
  133. package/dist/project/reticle-dir.d.ts +2 -0
  134. package/dist/project/reticle-dir.js +1 -0
  135. package/dist/session/connection-memory.d.ts +59 -0
  136. package/dist/session/connection-memory.js +108 -0
  137. package/dist/session/dev-command.d.ts +26 -0
  138. package/dist/session/dev-command.js +89 -0
  139. package/dist/session/dev-server-probe.d.ts +68 -0
  140. package/dist/session/dev-server-probe.js +102 -7
  141. package/dist/session/last-act.d.ts +26 -0
  142. package/dist/session/last-act.js +35 -1
  143. package/dist/session/live-control-tools.js +42 -5
  144. package/dist/session/no-session-diagnosis.d.ts +48 -2
  145. package/dist/session/no-session-diagnosis.js +176 -20
  146. package/dist/session/no-session-next-action.d.ts +46 -0
  147. package/dist/session/no-session-next-action.js +85 -0
  148. package/dist/session/no-session-watch.d.ts +40 -1
  149. package/dist/session/no-session-watch.js +165 -11
  150. package/dist/session/pending-commands.d.ts +12 -0
  151. package/dist/session/pending-commands.js +13 -1
  152. package/dist/session/session-manager.d.ts +4 -0
  153. package/dist/session/session-manager.js +35 -5
  154. package/dist/session/session.d.ts +10 -1
  155. package/dist/session/session.js +146 -5
  156. package/dist/telemetry/app-instrumented.d.ts +8 -0
  157. package/dist/telemetry/app-instrumented.js +10 -0
  158. package/dist/telemetry/bug-found.d.ts +18 -0
  159. package/dist/telemetry/bug-found.js +14 -0
  160. package/dist/telemetry/daemon-telemetry.js +24 -0
  161. package/dist/telemetry/install-source.d.ts +20 -0
  162. package/dist/telemetry/install-source.js +34 -0
  163. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  164. package/dist/telemetry/instrumentation-stall.js +90 -0
  165. package/dist/telemetry/session-metrics.d.ts +8 -0
  166. package/dist/telemetry/session-metrics.js +10 -0
  167. package/dist/telemetry/telemetry.d.ts +3 -1
  168. package/dist/telemetry/telemetry.js +13 -2
  169. package/dist/tools/act-tools.js +166 -29
  170. package/dist/tools/assert-verdict.js +32 -7
  171. package/dist/tools/browser-tools.js +10 -0
  172. package/dist/tools/dynamic-tools.js +16 -7
  173. package/dist/tools/error-recovery.js +9 -5
  174. package/dist/tools/invoke-tool.js +20 -3
  175. package/dist/tools/lease-hint.d.ts +30 -8
  176. package/dist/tools/lease-hint.js +65 -14
  177. package/dist/tools/lease-tools.d.ts +11 -0
  178. package/dist/tools/lease-tools.js +141 -96
  179. package/dist/tools/observe-tools.js +9 -3
  180. package/dist/tools/react-grace.d.ts +4 -0
  181. package/dist/tools/react-grace.js +6 -3
  182. package/dist/tools/read-tools.js +47 -1
  183. package/dist/tools/resolve-target.d.ts +33 -0
  184. package/dist/tools/resolve-target.js +53 -0
  185. package/dist/tools/settle-in-flight.d.ts +15 -0
  186. package/dist/tools/settle-in-flight.js +42 -2
  187. package/dist/tools/snapshot-delta.d.ts +3 -0
  188. package/dist/tools/snapshot-delta.js +67 -6
  189. package/dist/tools/tool-kit.d.ts +8 -0
  190. package/dist/tools/tool-surface.d.ts +60 -0
  191. package/dist/tools/tool-surface.js +85 -19
  192. package/dist/tools/tools.js +45 -4
  193. package/dist/tools/unadvertised-help.js +9 -6
  194. package/docs/README.md +1 -2
  195. package/docs/actions.mdx +41 -8
  196. package/docs/agent-cheatsheet.md +4 -2
  197. package/docs/architecture.md +6 -0
  198. package/docs/benchmarks.md +60 -12
  199. package/docs/best-practices.mdx +11 -6
  200. package/docs/capabilities.mdx +201 -54
  201. package/docs/cli/affected.mdx +6 -4
  202. package/docs/cli/capsules.mdx +9 -8
  203. package/docs/cli/cloud.mdx +30 -4
  204. package/docs/cli/doctor.mdx +41 -8
  205. package/docs/cli/drive.mdx +34 -15
  206. package/docs/cli/gate.mdx +6 -6
  207. package/docs/cli/help.mdx +8 -6
  208. package/docs/cli/hunt.mdx +18 -2
  209. package/docs/cli/identify.mdx +16 -2
  210. package/docs/cli/init.mdx +35 -8
  211. package/docs/cli/kill.mdx +5 -5
  212. package/docs/cli/license.mdx +1 -1
  213. package/docs/cli/open.mdx +6 -0
  214. package/docs/cli/restart.mdx +6 -6
  215. package/docs/cli/serve.mdx +10 -3
  216. package/docs/cli/status.mdx +29 -5
  217. package/docs/cli/stop.mdx +8 -0
  218. package/docs/cli/verify.mdx +20 -1
  219. package/docs/cli/version.mdx +1 -1
  220. package/docs/cli/watch.mdx +9 -2
  221. package/docs/cli.mdx +44 -22
  222. package/docs/debugging.md +3 -1
  223. package/docs/deploy-checks.md +30 -1
  224. package/docs/desktop.mdx +276 -80
  225. package/docs/docs.json +13 -12
  226. package/docs/enterprise.md +2 -0
  227. package/docs/faq.mdx +49 -35
  228. package/docs/fixtures.md +2 -0
  229. package/docs/flows.md +3 -1
  230. package/docs/for-agents.md +3 -3
  231. package/docs/frameworks.mdx +46 -7
  232. package/docs/gate-plan.md +4 -2
  233. package/docs/gates.md +7 -5
  234. package/docs/getting-started.md +53 -12
  235. package/docs/human-control.md +2 -0
  236. package/docs/index.mdx +43 -1
  237. package/docs/install-agentic.mdx +13 -4
  238. package/docs/install-manual.mdx +6 -4
  239. package/docs/instrumentation.mdx +7 -1
  240. package/docs/integration-patterns.md +3 -1
  241. package/docs/local-registry.md +4 -2
  242. package/docs/multi-agent-testing.md +3 -1
  243. package/docs/packages/babel-plugin.mdx +15 -6
  244. package/docs/packages/browser.mdx +2 -2
  245. package/docs/packages/core.mdx +26 -16
  246. package/docs/packages/electron.mdx +5 -7
  247. package/docs/packages/eslint-plugin.mdx +5 -3
  248. package/docs/packages/next.mdx +3 -5
  249. package/docs/packages/react.mdx +3 -3
  250. package/docs/packages/server.mdx +2 -2
  251. package/docs/packages/tauri.mdx +6 -6
  252. package/docs/packages/test.mdx +18 -7
  253. package/docs/packages/vite-plugin.mdx +4 -6
  254. package/docs/packages.mdx +14 -13
  255. package/docs/platform-integration.md +2 -0
  256. package/docs/predicates.mdx +76 -7
  257. package/docs/quickstart.mdx +51 -5
  258. package/docs/recipes.mdx +62 -22
  259. package/docs/skill-file.mdx +3 -1
  260. package/docs/state-management.mdx +2 -0
  261. package/docs/system-map.md +3 -1
  262. package/docs/telemetry-contract.md +16 -5
  263. package/docs/telemetry-events.mdx +63 -18
  264. package/docs/telemetry.md +8 -5
  265. package/docs/testing.md +35 -1
  266. package/docs/token-efficiency.md +8 -2
  267. package/docs/tools-act-and-wait.mdx +64 -14
  268. package/docs/tools-act-sequence.mdx +34 -17
  269. package/docs/tools-act.mdx +24 -11
  270. package/docs/tools-assert.mdx +69 -10
  271. package/docs/tools-console.mdx +11 -7
  272. package/docs/tools-inspect.mdx +11 -8
  273. package/docs/tools-navigate.mdx +41 -5
  274. package/docs/tools-network.mdx +42 -9
  275. package/docs/tools-observe.mdx +66 -10
  276. package/docs/tools-overview.mdx +26 -23
  277. package/docs/tools-query.mdx +52 -8
  278. package/docs/tools-session-and-feedback.mdx +30 -9
  279. package/docs/tools-sessions.mdx +39 -10
  280. package/docs/tools-snapshot.mdx +57 -3
  281. package/docs/tools-state.mdx +24 -1
  282. package/docs/tools-tools-and-run.mdx +40 -9
  283. package/docs/tools-wait-for.mdx +34 -15
  284. package/docs/troubleshooting.mdx +80 -10
  285. package/docs/usage.md +43 -21
  286. package/docs/vs-chrome-devtools-mcp.mdx +41 -0
  287. package/docs/vs-playwright-mcp.mdx +42 -2
  288. package/docs/vs-screenshots.mdx +42 -1
  289. package/docs/what-is-reticle.mdx +1 -1
  290. package/docs/why-reticle.mdx +3 -1
  291. package/package.json +5 -5
  292. package/docs/desktop-apps.md +0 -265
package/dist/cli.js CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { pathToFileURL } from 'node:url';
3
+ import { openFailureNote } from './cli/open-note.js';
3
4
  import { realpathSync } from 'node:fs';
4
5
  import { join } from 'node:path';
5
6
  import { stateDirProblem } from './daemon/state-dir.js';
7
+ import { statusNextAction } from './cli/status-next-action.js';
8
+ import { hasConnectedBefore } from './session/connection-memory.js';
6
9
  import { reticleStateHome } from './daemon/daemon.js';
7
10
  import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
8
11
  import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv } from '@reticlehq/core';
@@ -11,7 +14,7 @@ import { createNodeFileSystem } from './project/fs-port.js';
11
14
  import { affectedSavedFlows } from './flows/flow-sources.js';
12
15
  import { availableUpdate } from './update/update-nudge.js';
13
16
  import { handleUpdate, handleRollback } from './cli/cli-update-commands.js';
14
- import { start, startDaemon } from './index.js';
17
+ import { startDaemon } from './index.js';
15
18
  import { isCloudCommand, runCloudCommand } from './cli/cloud-cli.js';
16
19
  import { SERVER_VERSION } from './version/server-version.js';
17
20
  import { log } from './log.js';
@@ -22,7 +25,8 @@ import { installDaemonResilience, installProxyResilience, recordExitReason, Daem
22
25
  import { IdleShutdown, resolveIdleShutdownMs, resolveIdleCheckMs } from './daemon/idle-shutdown.js';
23
26
  import { DaemonHeartbeat, resolveHeartbeatMs } from './daemon/heartbeat.js';
24
27
  import { everServedToolCall } from './daemon/daemon-usefulness.js';
25
- import { fetchStatus, summarizeStatus, warnOnDaemonSkew, decideOpen, drivePortConflict, openInBrowser, openCommand, } from './cli/cli-launch.js';
28
+ import { fetchStatus, summarizeStatus, warnOnDaemonSkew, decideOpen, openInBrowser, openCommand, } from './cli/cli-launch.js';
29
+ import { handleDrive } from './cli/drive-command.js';
26
30
  import { handleVerify } from './cli/cli-verify.js';
27
31
  import { runKill } from './cli/cli-kill.js';
28
32
  import { summarizeHunt } from './hunt/hunt-report.js';
@@ -220,17 +224,34 @@ async function handleRestart(port, force) {
220
224
  // same lie `serve` already stopped telling.
221
225
  await serveWithHonestExit({ port, headless: true, http: false });
222
226
  }
227
+ /** `{ nextAction }` when there is one, `{}` when a session is connected — so the success case is silent. */
228
+ function withNextAction(facts) {
229
+ const next = statusNextAction(facts);
230
+ return next === undefined ? {} : { nextAction: next };
231
+ }
223
232
  function handleStatus(port) {
224
233
  const pid = readPid(port);
234
+ // Durable, so it survives the daemon idling out — which is the state `status` is most often run in.
235
+ const projectId = readProjectId(process.cwd());
236
+ const previouslyConnected = hasConnectedBefore(reticleStateHome(), port, projectId);
237
+ // Whether `init` has run HERE. Registering the MCP server does not wire the app, and more than one
238
+ // path does the first without the second — so this is the commonest reason `status` has nothing to
239
+ // report, and it was not among the facts this command could state.
240
+ const initialized = projectId !== undefined;
225
241
  if (null === pid || !isAlive(pid)) {
226
242
  // `running: false` on its own has been reported about a port that was demonstrably occupied,
227
243
  // because the pid file is not the port. Ask the port before answering.
228
244
  void probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).then((presence) => {
245
+ const running = presenceIsUsable(presence);
229
246
  log('reticle_status', {
230
247
  port,
231
- running: presenceIsUsable(presence),
248
+ running,
232
249
  presence,
233
250
  ...(presence === PortPresence.FOREIGN ? { reason: describePresence(presence, port) } : {}),
251
+ // `init` promises this command says why the app has not connected. Without it the answer was
252
+ // `running: false` and nothing else, which reads as "Reticle is broken" for what is usually
253
+ // just a daemon that has not been asked to do anything yet.
254
+ ...withNextAction({ running, sessionCount: 0, previouslyConnected, initialized }),
234
255
  });
235
256
  });
236
257
  return;
@@ -243,10 +264,23 @@ function handleStatus(port) {
243
264
  const update = availableUpdate();
244
265
  const nudge = update === undefined ? {} : { updateAvailable: update };
245
266
  if (payload === undefined) {
246
- log('reticle_status', { port, running: true, pid, ...nudge });
267
+ log('reticle_status', {
268
+ port,
269
+ running: true,
270
+ pid,
271
+ ...nudge,
272
+ ...withNextAction({ running: true, sessionCount: 0, previouslyConnected, initialized }),
273
+ });
247
274
  return;
248
275
  }
249
- log('reticle_status', { port, running: true, pid, ...summarizeStatus(payload), ...nudge });
276
+ const summary = summarizeStatus(payload);
277
+ // Only when the daemon did NOT already explain itself. It has the whole diagnosis in-process and
278
+ // puts it on the wire as `why`; printing a second, thinner opinion beside it risks two confident
279
+ // answers pointing different ways, which is worse than one.
280
+ const next = summary.why === undefined
281
+ ? withNextAction({ running: true, ...summary, previouslyConnected, initialized })
282
+ : {};
283
+ log('reticle_status', { port, running: true, pid, ...summary, ...next, ...nudge });
250
284
  });
251
285
  }
252
286
  /** Print the running package version (resolved once in server-version.ts). */
@@ -318,13 +352,20 @@ async function handleHunt(dir) {
318
352
  }
319
353
  /** Ensure a daemon is reachable on `port` (probe the real port; spawn + wait only if nothing's there). */
320
354
  function ensureDaemon(port) {
321
- return probeDaemon(port).then(async (listening) => {
355
+ return probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).then(async (presence) => {
322
356
  // Attaching to whatever already owns the port is the whole point of a daemon — but it means an
323
357
  // upgrade does NOT take effect until that daemon dies, and nothing used to say so. Say it here,
324
358
  // where both versions are in hand, and keep attaching: killing another agent's daemon on a
325
359
  // version bump is worse than a loud warning.
326
- if (listening)
360
+ if (presenceIsUsable(presence))
327
361
  return warnOnDaemonSkew(port);
362
+ // Not free, not a daemon: a stranger holds the port. Spawning here is guaranteed to fail —
363
+ // the child cannot bind — and `waitForDaemon` would then report READY anyway, because its
364
+ // probe is the same bare TCP connect and the stranger accepts. That is the "serve reports
365
+ // success for a daemon that never bound" shape. Refuse instead, with the sentence `doctor`
366
+ // says.
367
+ if (PortPresence.FREE !== presence)
368
+ throw new Error(describePresence(presence, port));
328
369
  const scriptPath = process.argv[1];
329
370
  if (scriptPath === undefined)
330
371
  throw new Error('cannot locate the reticle daemon script');
@@ -410,6 +451,7 @@ function handleOpen(requestedPort, url) {
410
451
  const connected = await waitForNewSession(port, sessions.length);
411
452
  log('reticle_open', {
412
453
  port,
454
+ ...(port === requestedPort ? {} : { requestedPort }),
413
455
  opened: decision.url,
414
456
  connected,
415
457
  ...(connected
@@ -422,14 +464,7 @@ function handleOpen(requestedPort, url) {
422
464
  // about a browser THIS command never uses, so a missing Chromium reads as the
423
465
  // explanation for a session that is missing for an unrelated reason. Both misdirects
424
466
  // were reported from the field, each costing several calls of app-side wiring hunt.
425
- note: `the URL was handed to the system default browser (this command does not use ` +
426
- `Reticle's own Chromium, so a chromium warning from \`reticle doctor\` is unrelated ` +
427
- `to this). No Reticle session appeared. By far the likeliest cause is that the app ` +
428
- `carries no Reticle SDK, or dials a port other than ${String(port)} — run ` +
429
- `\`reticle init\` in the app's directory and restart its dev server. If the app IS ` +
430
- `wired, give the page a moment and check the browser console: the SDK announces its ` +
431
- `own connect failures there, including the one it refuses to make from a ` +
432
- `non-localhost host without allowNonLocalhost.`,
467
+ note: openFailureNote(port, requestedPort),
433
468
  }),
434
469
  });
435
470
  })
@@ -478,6 +513,12 @@ function handleDaemonInner(parsed) {
478
513
  // which is after everything below has run. Without it the exit line says `code: 0` and a
479
514
  // reader cannot tell a tidy stop from the bridge disappearing — see #123.
480
515
  recordExitReason(reason);
516
+ // Say so on the wire before anything closes. The proxy on the other end sees a clean stream
517
+ // end whether we retired on schedule or died under it, and it has no other way to tell —
518
+ // which is how a designed shutdown came to dominate the metric that means "the agent lost
519
+ // its tools". Told, the proxy classifies its own drop honestly. First, because the send has
520
+ // to reach a socket that is still open.
521
+ server.announceShutdown?.();
481
522
  // Awaited before the close/exit chain: `process.exit(0)` kills an in-flight POST, and this is
482
523
  // the one event carrying the whole session. A failed send resolves anyway (emit swallows its
483
524
  // own errors), so this can delay the exit by at most the send timeout, never prevent it.
@@ -572,8 +613,15 @@ function handleMcp(opts) {
572
613
  * disappeared mid-session with nothing said. Respawning here makes the reconnect self-healing.
573
614
  */
574
615
  const ensure = async () => {
575
- if (await probeDaemon(port))
616
+ // The same question every other surface asks. It used to be a bare TCP connect, so a stranger on
617
+ // the bridge port answered "a daemon is here" — the proxy connected, the stream ended, and each
618
+ // client request woke into the identical non-answer. Rejecting here is what puts the proxy
619
+ // dormant with a reason, instead of pretending the wake succeeded.
620
+ const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
621
+ if (presenceIsUsable(presence))
576
622
  return;
623
+ if (PortPresence.FREE !== presence)
624
+ throw new Error(describePresence(presence, port));
577
625
  const scriptPath = process.argv[1];
578
626
  if (scriptPath === undefined) {
579
627
  log('reticle_mcp_no_script', {});
@@ -640,59 +688,6 @@ function handleMcp(opts) {
640
688
  });
641
689
  });
642
690
  }
643
- function handleLegacyDrive(parsed) {
644
- void driveWithHonestConflict(parsed);
645
- }
646
- /**
647
- * Refuse the port BEFORE binding it, and name what is holding it.
648
- *
649
- * `drive` went straight to `start`, which binds. The listen error surfaces asynchronously on the
650
- * server object long after `start` has resolved, so the `.catch` on that promise could never see it
651
- * and the process died with the raw `node:net` EADDRINUSE stack — in the one situation this command
652
- * is most often run in, since `reticle_sessions` recommends `reticle drive` for a throttled tab and
653
- * a throttled tab nearly always coexists with the daemon holding the port.
654
- */
655
- async function driveWithHonestConflict(parsed) {
656
- const presence = await probePresence(parsed.port, { tcpOpen: probeDaemon, status: fetchStatus });
657
- const conflict = drivePortConflict(presence, parsed.port, { ourPid: readPid(parsed.port) });
658
- if (conflict !== undefined) {
659
- log('reticle_drive_port_conflict', { port: parsed.port, presence, reason: conflict });
660
- process.stderr.write(`${conflict}\n`);
661
- process.exit(1);
662
- return;
663
- }
664
- // The probe cannot close the race: something can take the port between here and the bind, and the
665
- // bind reports it on the server, outside every promise this function holds. Catching it at the
666
- // process is the only place that sees it at all — and only EADDRINUSE, so a genuine crash still
667
- // crashes rather than being dressed up as a port conflict. Who won the race is not knowable from
668
- // in here, so the sentence used is the one that is true of any holder.
669
- process.on('uncaughtException', (error) => {
670
- const code = error?.code;
671
- if ('EADDRINUSE' !== code)
672
- throw error;
673
- const reason = drivePortConflict(PortPresence.FOREIGN, parsed.port);
674
- log('reticle_drive_port_conflict', {
675
- port: parsed.port,
676
- presence: PortPresence.FOREIGN,
677
- reason,
678
- });
679
- process.stderr.write(`${String(reason)}\n`);
680
- process.exit(1);
681
- });
682
- const options = {
683
- port: parsed.port,
684
- driveUrl: parsed.driveUrl,
685
- headless: parsed.headless,
686
- };
687
- try {
688
- await start(options);
689
- log('reticle_started', { port: parsed.port });
690
- }
691
- catch (error) {
692
- log('reticle_start_failed', { error: error instanceof Error ? error.message : String(error) });
693
- process.exit(1);
694
- }
695
- }
696
691
  function main() {
697
692
  // Before anything reads process.env — notably the telemetry gate and the bridge's security
698
693
  // options — fold in a project-local `.env`. Values already in the environment always win.
@@ -782,7 +777,7 @@ function main() {
782
777
  handleOpen(parsed.port, parsed.url);
783
778
  break;
784
779
  case 'drive':
785
- handleLegacyDrive(parsed);
780
+ handleDrive(parsed);
786
781
  break;
787
782
  case 'verify':
788
783
  handleVerify(parsed);
@@ -1,4 +1,5 @@
1
1
  import { ContradictionKind, type ReticleEvent } from '@reticlehq/core';
2
+ import { type DeclaredNetFailure } from './declared.js';
2
3
  /**
3
4
  * The contradiction hunter.
4
5
  *
@@ -63,5 +64,36 @@ export interface ContradictionOptions {
63
64
  * test, which is weaker but never wrong in the direction of a false accusation.
64
65
  */
65
66
  mutatedWithin?: number | undefined;
67
+ /**
68
+ * Requests the CALLER declared would fail, read off the oracle it wrote before acting.
69
+ *
70
+ * `ui-advanced-request-failed` cannot tell "the app swallowed the error and carried on" from "the
71
+ * app rendered the error, which is the behaviour under test" — both are a moved DOM beside a
72
+ * failed request. `failureAcknowledged` recovers the first from the app's own state, and an app
73
+ * that renders its error straight into the DOM without touching a store defeats it.
74
+ *
75
+ * The declaration is the missing evidence, and it costs no new API: an agent verifying an error
76
+ * path already writes `{ net, POST, /api/login, status: 500 }` into the predicate. Measured in the
77
+ * field: every branch of a login error path (500, 401, 503) passed every declared clause and every
78
+ * run still returned `verified: "no" / contradicted`, so error handling — empty states, offline
79
+ * banners, 4xx/5xx messaging, lockouts — was the code least able to reach a green verdict and the
80
+ * most worth verifying.
81
+ *
82
+ * Scoped as narrowly as it can be: it suppresses ONLY the heuristic rule that cannot see the
83
+ * difference. A success signal fired over the declared failure still contradicts, and a server
84
+ * fault blamed on the user is still misattributed — see the negative controls in
85
+ * `contradictions.declared.test.ts`.
86
+ */
87
+ expectedFailures?: readonly DeclaredNetFailure[] | undefined;
88
+ /**
89
+ * The caller declared an on-screen consequence and it HELD in this window.
90
+ *
91
+ * `route-rendered-nothing` infers a blank destination from the absence of DOM events, and a
92
+ * verdict that names it beside an element match — heading found, with its source file and line —
93
+ * is a clause its own evidence disproves. Positive evidence outranks the absence it is inferred
94
+ * from. Undefined/false leaves the rule exactly as it was, which is the case that catches a route
95
+ * with no view.
96
+ */
97
+ renderProved?: boolean | undefined;
66
98
  }
67
99
  export declare function findContradictions(allEvents: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];
@@ -4,6 +4,7 @@ import { findBodyFailures } from './body-failures.js';
4
4
  import { findEchoMismatches } from './echo-mismatch.js';
5
5
  import { findUnitMismatches } from './unit-mismatch.js';
6
6
  import { asNumber, asString } from '../tools/tools-helpers.js';
7
+ import { matchesDeclaredFailure } from './declared.js';
7
8
  function netCall(e) {
8
9
  const status = asNumber(e.data['status']);
9
10
  return {
@@ -171,7 +172,7 @@ export function findContradictions(allEvents, options = {}) {
171
172
  // Needs body capture; silent without it, which is why the assert path also declares when bodies
172
173
  // were never recorded rather than letting an unread payload read as an empty one.
173
174
  found.push(...findBodyFailures(events));
174
- found.push(...findEchoMismatches(events));
175
+ found.push(...findEchoMismatches(events, options.actionSince));
175
176
  // ── A money value written back at the wrong SCALE ───────────────────────────────────────────
176
177
  found.push(...findUnitMismatches(events, options.prior ?? []));
177
178
  // ── The action landed on something that does not react ──────────────────────────────────────
@@ -192,6 +193,9 @@ export function findContradictions(allEvents, options = {}) {
192
193
  ];
193
194
  }
194
195
  const failed = settled.filter((c) => false === c.ok);
196
+ // The failures NOBODY declared — see ContradictionOptions.expectedFailures. Only the heuristic
197
+ // "the UI moved while a request failed" rule reads this; the sharp rules still read `failed`.
198
+ const unexpected = failed.filter((c) => !matchesDeclaredFailure(c, options.expectedFailures ?? []));
195
199
  const advanced = uiAdvanced(events);
196
200
  const signals = events
197
201
  .filter((e) => e.type === EventType.SIGNAL)
@@ -215,7 +219,7 @@ export function findContradictions(allEvents, options = {}) {
215
219
  e.type === EventType.DOM_REMOVED ||
216
220
  e.type === EventType.DOM_TEXT);
217
221
  const fetched = events.some((e) => e.type === EventType.NET_REQUEST || e.type === EventType.NET_PENDING);
218
- if (routed && !rendered && !fetched) {
222
+ if (routed && !rendered && !fetched && true !== options.renderProved) {
219
223
  found.push({
220
224
  kind: ContradictionKind.ROUTE_RENDERED_NOTHING,
221
225
  claim: 'the app navigated to a new route',
@@ -259,12 +263,12 @@ export function findContradictions(allEvents, options = {}) {
259
263
  detail: failed.map(describe).join('; '),
260
264
  });
261
265
  }
262
- else if (failed.length > 0 && advanced && !misattributed && !failureAcknowledged(events)) {
266
+ else if (unexpected.length > 0 && advanced && !misattributed && !failureAcknowledged(events)) {
263
267
  found.push({
264
268
  kind: ContradictionKind.UI_ADVANCED_REQUEST_FAILED,
265
269
  claim: 'the UI moved forward (DOM/store/route changed)',
266
- counter: `${String(failed.length)} request(s) in the same window failed`,
267
- detail: failed.map(describe).join('; '),
270
+ counter: `${String(unexpected.length)} request(s) in the same window failed`,
271
+ detail: unexpected.map(describe).join('; '),
268
272
  });
269
273
  }
270
274
  // ── A write succeeded and nothing on the client moved ───────────────────────────────────────
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What the caller DECLARED before acting — the oracle, read back.
3
+ *
4
+ * `act_and_wait` asks the agent to name the expected consequence BEFORE the action, and that
5
+ * declaration was then thrown away: everything downstream judged the window as if nobody had said
6
+ * what they were expecting. Two measured consequences, one bug:
7
+ *
8
+ * - An error path can be declared exactly — `{ net, POST, /api/login, status: 500 }` — and the
9
+ * contradiction hunter still reported `ui-advanced-request-failed`, because a failed request plus
10
+ * a moved DOM is all it can see. The failure was EXPECTED and said so in the predicate, so a
11
+ * verdict of "channels disagree" is a statement about a disagreement nobody had.
12
+ * - A destination whose content was asserted and FOUND was still reported as
13
+ * `route-rendered-nothing`, a clause the element evidence beside it disproves.
14
+ *
15
+ * Deliberately conservative about what counts as declared. Only what the caller REQUIRED is read:
16
+ * the top level and `allOf` chains. An `anyOf` branch may never have held, and a `not` declares the
17
+ * opposite of a consequence — honouring either would suppress a real finding on the strength of
18
+ * something that never happened. Pure: a predicate in, a description out.
19
+ */
20
+ import type { Predicate } from './predicate-eval.js';
21
+ /** A failing call the caller named in advance — matched against the window's real calls. */
22
+ export interface DeclaredNetFailure {
23
+ method?: string;
24
+ urlContains?: string;
25
+ status?: number;
26
+ }
27
+ export interface DeclaredExpectations {
28
+ /** Requests the caller declared would FAIL, so failing is the expected outcome, not a disagreement. */
29
+ netFailures: readonly DeclaredNetFailure[];
30
+ /** The caller required something to be ON SCREEN — an element or text, present rather than absent. */
31
+ rendersContent: boolean;
32
+ }
33
+ export declare function declaredExpectations(predicate: Predicate | undefined): DeclaredExpectations;
34
+ /**
35
+ * Does a call the window recorded match a failure the caller declared?
36
+ *
37
+ * Every field the caller named must agree — a declaration about `POST /api/login → 500` says nothing
38
+ * about `POST /api/orders`, and treating it as a blanket amnesty for failed requests would remove
39
+ * the check rather than inform it. Fields the caller left out are not constraints.
40
+ */
41
+ export declare function matchesDeclaredFailure(call: {
42
+ method: string;
43
+ url: string;
44
+ status: number | undefined;
45
+ }, declared: readonly DeclaredNetFailure[]): boolean;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * What the caller DECLARED before acting — the oracle, read back.
3
+ *
4
+ * `act_and_wait` asks the agent to name the expected consequence BEFORE the action, and that
5
+ * declaration was then thrown away: everything downstream judged the window as if nobody had said
6
+ * what they were expecting. Two measured consequences, one bug:
7
+ *
8
+ * - An error path can be declared exactly — `{ net, POST, /api/login, status: 500 }` — and the
9
+ * contradiction hunter still reported `ui-advanced-request-failed`, because a failed request plus
10
+ * a moved DOM is all it can see. The failure was EXPECTED and said so in the predicate, so a
11
+ * verdict of "channels disagree" is a statement about a disagreement nobody had.
12
+ * - A destination whose content was asserted and FOUND was still reported as
13
+ * `route-rendered-nothing`, a clause the element evidence beside it disproves.
14
+ *
15
+ * Deliberately conservative about what counts as declared. Only what the caller REQUIRED is read:
16
+ * the top level and `allOf` chains. An `anyOf` branch may never have held, and a `not` declares the
17
+ * opposite of a consequence — honouring either would suppress a real finding on the strength of
18
+ * something that never happened. Pure: a predicate in, a description out.
19
+ */
20
+ import { PredicateKind } from '@reticlehq/core';
21
+ /** Below this, a status is a success or a redirect: not a declared failure. */
22
+ const FAILURE_STATUS_MIN = 400;
23
+ export function declaredExpectations(predicate) {
24
+ const netFailures = [];
25
+ let rendersContent = false;
26
+ const walk = (p) => {
27
+ switch (p.kind) {
28
+ case PredicateKind.ALL_OF:
29
+ for (const child of p.predicates)
30
+ walk(child);
31
+ return;
32
+ case PredicateKind.NET: {
33
+ const declaredFailure = false === p.ok || (p.status !== undefined && p.status >= FAILURE_STATUS_MIN);
34
+ if (!declaredFailure)
35
+ return;
36
+ netFailures.push({
37
+ ...(p.method === undefined ? {} : { method: p.method }),
38
+ ...(p.urlContains === undefined ? {} : { urlContains: p.urlContains }),
39
+ ...(p.status === undefined ? {} : { status: p.status }),
40
+ });
41
+ return;
42
+ }
43
+ case PredicateKind.ELEMENT:
44
+ case PredicateKind.TEXT:
45
+ if (true !== p.absent)
46
+ rendersContent = true;
47
+ return;
48
+ default:
49
+ return;
50
+ }
51
+ };
52
+ if (predicate !== undefined)
53
+ walk(predicate);
54
+ return { netFailures, rendersContent };
55
+ }
56
+ /**
57
+ * Does a call the window recorded match a failure the caller declared?
58
+ *
59
+ * Every field the caller named must agree — a declaration about `POST /api/login → 500` says nothing
60
+ * about `POST /api/orders`, and treating it as a blanket amnesty for failed requests would remove
61
+ * the check rather than inform it. Fields the caller left out are not constraints.
62
+ */
63
+ export function matchesDeclaredFailure(call, declared) {
64
+ return declared.some((d) => {
65
+ if (d.method !== undefined && d.method.toUpperCase() !== call.method.toUpperCase())
66
+ return false;
67
+ if (d.urlContains !== undefined && !call.url.includes(d.urlContains))
68
+ return false;
69
+ if (d.status !== undefined && d.status !== call.status)
70
+ return false;
71
+ return true;
72
+ });
73
+ }
@@ -1,4 +1,10 @@
1
1
  import { type ReticleEvent } from '@reticlehq/core';
2
2
  import type { Contradiction } from './contradictions.js';
3
- /** Contradictions for writes whose response echoes a different value than the request asked for. */
4
- export declare function findEchoMismatches(events: readonly ReticleEvent[]): Contradiction[];
3
+ /**
4
+ * Contradictions for writes whose response echoes a different value than the request asked for.
5
+ *
6
+ * `actionSince` is the attribution floor `findContradictions` already keeps — the event time the
7
+ * action that opened this window was dispatched. A finding about a request that had already settled
8
+ * before the agent acted otherwise reads as a verdict on the change it just made.
9
+ */
10
+ export declare function findEchoMismatches(events: readonly ReticleEvent[], actionSince?: number): Contradiction[];
@@ -1,4 +1,4 @@
1
- import { ContradictionKind, EventType } from '@reticlehq/core';
1
+ import { ContradictionKind, EventType, MUTATING_METHODS } from '@reticlehq/core';
2
2
  /**
3
3
  * A write that returned OK and quietly did not apply part of what it was asked to.
4
4
  *
@@ -18,6 +18,11 @@ import { ContradictionKind, EventType } from '@reticlehq/core';
18
18
  * would fire on healthy APIs constantly, and a contradiction kind that cries wolf gets filtered out
19
19
  * by the agents reading it, taking the true positives with it. So:
20
20
  *
21
+ * - only requests that are WRITES are considered. Reported from the field: a lookup that sends its
22
+ * key in the body (`POST /get-branding` with `{workspace_id}`) and gets a record back was graded
23
+ * as a half-applied write. The gate was on the RESPONSE being a success and never on the REQUEST
24
+ * being a write, so every successful call carrying a body read as a save — and the same key name
25
+ * legitimately means different things on the two sides of a request/response pair;
21
26
  * - only keys the request actually SENT are considered;
22
27
  * - only scalars, since deep structural diffing is where the false positives live;
23
28
  * - values are compared NORMALISED (trimmed, case-folded, numbers as numbers), so `FR` vs `fr` and
@@ -89,12 +94,33 @@ function scalarsByKey(body, into = new Map(), depth = 0) {
89
94
  }
90
95
  const OK_MIN = 200;
91
96
  const OK_MAX = 300;
92
- /** Contradictions for writes whose response echoes a different value than the request asked for. */
93
- export function findEchoMismatches(events) {
97
+ /**
98
+ * Contradictions for writes whose response echoes a different value than the request asked for.
99
+ *
100
+ * `actionSince` is the attribution floor `findContradictions` already keeps — the event time the
101
+ * action that opened this window was dispatched. A finding about a request that had already settled
102
+ * before the agent acted otherwise reads as a verdict on the change it just made.
103
+ */
104
+ export function findEchoMismatches(events, actionSince) {
94
105
  const found = [];
95
106
  for (const event of events) {
96
107
  if (event.type !== EventType.NET_REQUEST)
97
108
  continue;
109
+ // `MUTATING_METHODS` is the one definition of "is this a write" in the codebase — the same list
110
+ // `contradictions.ts`'s `isMutating` reads. A read has no fields to half-apply.
111
+ //
112
+ // KNOWN LIMIT, stated because it is easy to read this fix as bigger than it is: POST is in that
113
+ // list, so a lookup that POSTs its key and gets a record back — the shape actually reported —
114
+ // still reaches the comparison below. Method is the only signal available here, and a read-shaped
115
+ // POST is genuinely indistinguishable from a write by method alone.
116
+ //
117
+ // The tempting next step is to skip a pair whose JSON types disagree (a string key sent, a number
118
+ // echoed). Deliberately not taken: that also silences `{"qty":"5"}` answered with `3`, which is a
119
+ // real dropped write. A false negative here is a missed bug in someone's app, and that is not a
120
+ // trade this file makes to quieten a false positive.
121
+ const method = 'string' === typeof event.data['method'] ? event.data['method'] : '';
122
+ if (!MUTATING_METHODS.includes(method.toUpperCase()))
123
+ continue;
98
124
  // IPC reports `ok` rather than a status; HTTP reports a status. Either must say success — a write
99
125
  // that already failed is a different (and louder) finding than one that half-applied.
100
126
  const status = event.data['status'];
@@ -126,12 +152,14 @@ export function findEchoMismatches(events) {
126
152
  if (0 === dropped.length)
127
153
  continue;
128
154
  const url = 'string' === typeof event.data['url'] ? event.data['url'] : '';
129
- const method = 'string' === typeof event.data['method'] ? event.data['method'] : '';
155
+ const attribution = actionSince !== undefined && event.t < actionSince
156
+ ? ' — this request completed before the action being verified, so the finding is not about that change'
157
+ : '';
130
158
  found.push({
131
159
  kind: ContradictionKind.WRITE_FIELD_IGNORED,
132
160
  claim: 'the write returned success and the page treated it as saved',
133
161
  counter: `its own echo shows ${String(dropped.length)} field(s) NOT applied — ${dropped.join('; ')}`,
134
- detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set, so the write half-applied; the UI has no way to know and will show the value the user typed`,
162
+ detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set, so the write half-applied; the UI has no way to know and will show the value the user typed${attribution}`,
135
163
  });
136
164
  }
137
165
  return found;
@@ -57,7 +57,7 @@ interface NetCallView {
57
57
  */
58
58
  oneWay?: boolean;
59
59
  }
60
- export declare function projectNetCall(e: ReticleEvent): NetCallView;
60
+ export declare function projectNetCall(e: ReticleEvent, includeBodies?: boolean): NetCallView;
61
61
  /**
62
62
  * Compact console-log summary for reticle_console output.
63
63
  *
@@ -86,7 +86,7 @@ export function reconcileNet(events) {
86
86
  .map((e) => ({ ...e, data: { ...e.data, status: 'pending', pending: true } }));
87
87
  return [...completed, ...unresolved].sort((a, b) => a.t - b.t);
88
88
  }
89
- export function projectNetCall(e) {
89
+ export function projectNetCall(e, includeBodies = true) {
90
90
  const status = e.data['status'];
91
91
  const ms = asNumber(e.data['durationMs']);
92
92
  const statusText = asString(e.data['statusText']);
@@ -106,11 +106,14 @@ export function projectNetCall(e) {
106
106
  view.contentType = contentType;
107
107
  if (responseSize !== undefined)
108
108
  view.responseSize = responseSize;
109
- if (requestBody !== undefined)
109
+ // Bodies dominate the payload of the most-called read tool, and are only occasionally the part
110
+ // anyone wanted — `bodies: false` on reticle_network drops them for a body-free listing (#401).
111
+ if (includeBodies && requestBody !== undefined)
110
112
  view.requestBody = requestBody;
111
- if (responseBody !== undefined)
113
+ if (includeBodies && responseBody !== undefined)
112
114
  view.responseBody = responseBody;
113
- if (true === e.data['requestBodyTruncated'] || true === e.data['responseBodyTruncated']) {
115
+ if (includeBodies &&
116
+ (true === e.data['requestBodyTruncated'] || true === e.data['responseBodyTruncated'])) {
114
117
  view.bodyTruncated = true;
115
118
  }
116
119
  if (ms !== undefined)