@reticlehq/server 3.1.0 → 3.2.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 (228) hide show
  1. package/SKILL.md +17 -18
  2. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  3. package/dist/command/cli/cli-doctor.js +13 -1
  4. package/dist/command/cli/cli-flow-commands.d.ts +2 -1
  5. package/dist/command/cli/cli-kill.js +1 -1
  6. package/dist/command/cli/cli-parse.d.ts +2 -1
  7. package/dist/command/cli/cli-parse.js +5 -2
  8. package/dist/command/cli/cli-verify.js +2 -2
  9. package/dist/command/cli/cloud-cli.js +7 -6
  10. package/dist/command/cli/cloud-kit.js +2 -6
  11. package/dist/command/cli/config/config-discovery.d.ts +12 -0
  12. package/dist/command/cli/config/config-discovery.js +19 -0
  13. package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
  14. package/dist/command/cli/doctor/doctor-rows.js +12 -0
  15. package/dist/command/cli/drive/drive-command.js +1 -1
  16. package/dist/command/cli/launch/cli-launch.d.ts +10 -2
  17. package/dist/command/cli/launch/cli-launch.js +32 -39
  18. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
  19. package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
  20. package/dist/command/cli/mcp-command.js +5 -3
  21. package/dist/command/cli/ports/resolve/cli-port.d.ts +2 -1
  22. package/dist/command/cli/ports/resolve/cli-port.js +5 -1
  23. package/dist/command/cli/status/status-lines.d.ts +28 -0
  24. package/dist/command/cli/status/status-lines.js +117 -0
  25. package/dist/command/cli.d.ts +1 -1
  26. package/dist/command/cli.js +24 -177
  27. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  28. package/dist/command/daemon/binding/daemon-status-probe.js +48 -0
  29. package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
  30. package/dist/command/daemon/binding/loopback-reach.js +63 -0
  31. package/dist/command/daemon/daemon-resilience.d.ts +13 -1
  32. package/dist/command/daemon/daemon-resilience.js +12 -1
  33. package/dist/command/daemon/daemon.d.ts +2 -11
  34. package/dist/command/daemon/daemon.js +8 -11
  35. package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
  36. package/dist/command/daemon/lifetime/heartbeat.js +10 -0
  37. package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
  38. package/dist/command/daemon/lifetime/log-cap.js +115 -0
  39. package/dist/command/setup/agent-notice-memory.d.ts +16 -0
  40. package/dist/command/setup/agent-notice-memory.js +65 -0
  41. package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
  42. package/dist/command/setup/bringup/dev-server-wait.js +16 -0
  43. package/dist/command/setup/bringup/ensure-daemon.js +1 -1
  44. package/dist/command/setup/init/init-runtime.js +13 -1
  45. package/dist/command/setup/node-effects.js +1 -1
  46. package/dist/command/setup/probe/page-probe.js +9 -2
  47. package/dist/command/setup/run-setup.d.ts +25 -7
  48. package/dist/command/setup/run-setup.js +51 -9
  49. package/dist/command/setup/setup-command.js +31 -3
  50. package/dist/command/setup/terminal/confirm.js +2 -1
  51. package/dist/command/update/updater.d.ts +27 -0
  52. package/dist/command/update/updater.js +35 -2
  53. package/dist/features/crawl/crawl-tools.js +3 -1
  54. package/dist/features/crawl/crawl.js +2 -1
  55. package/dist/features/harness/drive-report.d.ts +73 -0
  56. package/dist/features/harness/drive-report.js +196 -0
  57. package/dist/features/harness/driver.js +29 -8
  58. package/dist/features/harness/drivers.d.ts +19 -0
  59. package/dist/features/harness/drivers.js +19 -0
  60. package/dist/features/harness/fill-values.d.ts +67 -0
  61. package/dist/features/harness/fill-values.js +153 -0
  62. package/dist/features/harness/jev-driver.d.ts +188 -0
  63. package/dist/features/harness/jev-driver.js +664 -0
  64. package/dist/features/harness/openai-driver.d.ts +46 -0
  65. package/dist/features/harness/openai-driver.js +197 -0
  66. package/dist/features/harness/platform-config.d.ts +52 -0
  67. package/dist/features/harness/platform-config.js +89 -0
  68. package/dist/features/visual/visual-tools.js +26 -2
  69. package/dist/index.js +60 -55
  70. package/dist/judgement/domain/domain-tools.js +4 -2
  71. package/dist/judgement/runs/verification-sync.d.ts +2 -2
  72. package/dist/judgement/runs/verification-sync.js +9 -2
  73. package/dist/language/flows/affected-tools.js +6 -3
  74. package/dist/language/flows/flow-replay-run.d.ts +25 -2
  75. package/dist/language/flows/flow-replay-run.js +73 -27
  76. package/dist/language/flows/flow-replay.d.ts +17 -0
  77. package/dist/language/flows/flow-replay.js +148 -100
  78. package/dist/language/flows/flow-result.d.ts +18 -1
  79. package/dist/language/flows/flow-result.js +18 -2
  80. package/dist/language/flows/flow-store-for-session.d.ts +2 -1
  81. package/dist/language/flows/flow-tools.js +8 -4
  82. package/dist/language/flows/flows.d.ts +8 -7
  83. package/dist/language/flows/flows.js +7 -3
  84. package/dist/language/flows/narrow-write.d.ts +4 -4
  85. package/dist/language/flows/replay.js +1 -1
  86. package/dist/language/flows/suite-selection.d.ts +15 -1
  87. package/dist/language/flows/suite-selection.js +1 -1
  88. package/dist/language/flows/verify-change-tools.js +7 -4
  89. package/dist/memory/cloud/account-state.d.ts +1 -1
  90. package/dist/memory/cloud/account-state.js +5 -4
  91. package/dist/memory/cloud/cloud-config.d.ts +9 -0
  92. package/dist/memory/cloud/cloud-config.js +9 -0
  93. package/dist/memory/cloud/cloud-sync.d.ts +7 -2
  94. package/dist/memory/cloud/cloud-sync.js +10 -5
  95. package/dist/memory/cloud/harness-config.d.ts +10 -0
  96. package/dist/memory/cloud/harness-config.js +58 -0
  97. package/dist/memory/cloud/harness-offer.d.ts +44 -0
  98. package/dist/memory/cloud/harness-offer.js +146 -0
  99. package/dist/memory/cloud/harness-switch.d.ts +11 -0
  100. package/dist/memory/cloud/harness-switch.js +53 -0
  101. package/dist/memory/cloud/sync-daemon.d.ts +22 -2
  102. package/dist/memory/cloud/sync-daemon.js +24 -0
  103. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  104. package/dist/memory/impact/impact-recorder.js +2 -1
  105. package/dist/memory/impact/impact-store.d.ts +5 -0
  106. package/dist/memory/impact/impact-store.js +19 -0
  107. package/dist/memory/intent/intent-shard-store.js +2 -2
  108. package/dist/memory/intent/intent-shard.d.ts +12 -12
  109. package/dist/memory/journal/attach-journal.d.ts +11 -0
  110. package/dist/memory/journal/attach-journal.js +1 -1
  111. package/dist/memory/journal/journal-query.d.ts +35 -1
  112. package/dist/memory/journal/journal-query.js +46 -0
  113. package/dist/memory/journal/journal-recorder.d.ts +50 -2
  114. package/dist/memory/journal/journal-recorder.js +45 -7
  115. package/dist/memory/journal/on-disk/retention.d.ts +87 -2
  116. package/dist/memory/journal/on-disk/retention.js +177 -8
  117. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +9 -0
  118. package/dist/memory/journal/on-disk/workspace-gitignore.js +9 -2
  119. package/dist/memory/journal/self-observation.d.ts +3 -0
  120. package/dist/memory/journal/self-observation.js +67 -0
  121. package/dist/memory/journal/session-end.d.ts +13 -3
  122. package/dist/memory/journal/session-end.js +24 -4
  123. package/dist/memory/journal/session-journal.d.ts +115 -5
  124. package/dist/memory/journal/session-journal.js +479 -32
  125. package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
  126. package/dist/memory/project/artifact-root-resolver.js +89 -0
  127. package/dist/memory/project/artifact-root.d.ts +39 -2
  128. package/dist/memory/project/artifact-root.js +58 -1
  129. package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
  130. package/dist/memory/project/dir/fill-value-store.js +88 -0
  131. package/dist/memory/project/dir/reticle-dir.d.ts +14 -4
  132. package/dist/memory/project/dir/reticle-dir.js +11 -0
  133. package/dist/memory/project/fs/fs-port.d.ts +22 -1
  134. package/dist/memory/project/fs/fs-port.js +33 -5
  135. package/dist/memory/project/learned-routes.d.ts +16 -2
  136. package/dist/memory/project/learned-routes.js +26 -10
  137. package/dist/memory/project/project-for-root.d.ts +30 -0
  138. package/dist/memory/project/project-for-root.js +43 -0
  139. package/dist/memory/project/project-tools.js +10 -5
  140. package/dist/memory/project/session-root.d.ts +35 -6
  141. package/dist/memory/project/session-root.js +70 -8
  142. package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
  143. package/dist/portal/bridge/auth-failure-reason.js +15 -1
  144. package/dist/portal/bridge/bridge.d.ts +6 -1
  145. package/dist/portal/bridge/bridge.js +36 -2
  146. package/dist/portal/session/facts/session-replaced.d.ts +19 -0
  147. package/dist/portal/session/facts/session-replaced.js +22 -0
  148. package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
  149. package/dist/portal/session/no-session-diagnosis.js +67 -60
  150. package/dist/portal/session/no-session-next-action.js +3 -2
  151. package/dist/portal/session/no-session-watch.js +14 -10
  152. package/dist/portal/session/rebind.d.ts +18 -0
  153. package/dist/portal/session/rebind.js +38 -0
  154. package/dist/portal/session/session-manager.d.ts +4 -0
  155. package/dist/portal/session/session-manager.js +19 -1
  156. package/dist/portal/session/session.d.ts +21 -10
  157. package/dist/portal/session/session.js +29 -42
  158. package/dist/status-payload.d.ts +12 -1
  159. package/dist/status-payload.js +4 -1
  160. package/dist/surface/http-server.js +3 -5
  161. package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
  162. package/dist/surface/mcp/faults/mcp-outage.js +20 -4
  163. package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
  164. package/dist/surface/mcp/mcp-proxy.js +20 -9
  165. package/dist/surface/mcp/mcp.d.ts +10 -1
  166. package/dist/surface/mcp/mcp.js +59 -29
  167. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
  168. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
  169. package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
  170. package/dist/surface/mcp/proxy/proxy-log.js +7 -22
  171. package/dist/surface/tools/act/act-preflight.js +6 -1
  172. package/dist/surface/tools/act/sequence-grade.js +13 -0
  173. package/dist/surface/tools/act-capsule.js +27 -1
  174. package/dist/surface/tools/assert/assert-verdict.js +44 -17
  175. package/dist/surface/tools/coverage-tools.js +5 -2
  176. package/dist/surface/tools/dynamic-tools.js +37 -46
  177. package/dist/surface/tools/error-recovery.d.ts +6 -0
  178. package/dist/surface/tools/error-recovery.js +15 -0
  179. package/dist/surface/tools/explore-tools.js +80 -7
  180. package/dist/surface/tools/feedback-tools.js +49 -2
  181. package/dist/surface/tools/first-sentence.d.ts +24 -0
  182. package/dist/surface/tools/first-sentence.js +0 -0
  183. package/dist/surface/tools/harness-explore.d.ts +125 -1
  184. package/dist/surface/tools/harness-explore.js +401 -15
  185. package/dist/surface/tools/harness-plan.d.ts +76 -0
  186. package/dist/surface/tools/harness-plan.js +156 -0
  187. package/dist/surface/tools/harness-toolset.js +12 -1
  188. package/dist/surface/tools/invoke-tool.js +65 -5
  189. package/dist/surface/tools/live-call-text.d.ts +6 -0
  190. package/dist/surface/tools/live-call-text.js +36 -2
  191. package/dist/surface/tools/observe-tools.js +55 -9
  192. package/dist/surface/tools/read/query-windowed.d.ts +1 -1
  193. package/dist/surface/tools/read/query-windowed.js +1 -1
  194. package/dist/surface/tools/read-tools.js +1 -2
  195. package/dist/surface/tools/tool-kit.d.ts +18 -3
  196. package/dist/surface/tools/tools-helpers.d.ts +8 -7
  197. package/dist/surface/tools/tools-helpers.js +11 -13
  198. package/dist/surface/tools/tools.js +6 -1
  199. package/dist/telemetry/bug-found.js +32 -2
  200. package/dist/telemetry/feedback-context.js +3 -3
  201. package/dist/telemetry/harness-drive.d.ts +6 -0
  202. package/dist/telemetry/harness-drive.js +39 -0
  203. package/dist/telemetry/mcp-connection.d.ts +14 -1
  204. package/dist/telemetry/mcp-connection.js +17 -2
  205. package/dist/telemetry/onboarding-firsts.d.ts +30 -1
  206. package/dist/telemetry/onboarding-firsts.js +54 -12
  207. package/dist/telemetry/verification-of.js +31 -3
  208. package/docs/agent-cheatsheet.md +7 -3
  209. package/docs/autodrive.md +42 -2
  210. package/docs/benchmarks.md +1 -1
  211. package/docs/capabilities.mdx +1 -1
  212. package/docs/cli/cloud.mdx +3 -3
  213. package/docs/cli/doctor.mdx +1 -0
  214. package/docs/cli/status.mdx +35 -20
  215. package/docs/cli.mdx +8 -18
  216. package/docs/desktop.mdx +1 -1
  217. package/docs/faq.mdx +1 -1
  218. package/docs/install-manual.mdx +1 -1
  219. package/docs/packages/tauri.mdx +1 -1
  220. package/docs/packages.mdx +1 -1
  221. package/docs/predicates.mdx +16 -0
  222. package/docs/system-map.md +6 -4
  223. package/docs/telemetry.md +2 -1
  224. package/docs/token-efficiency.md +2 -2
  225. package/docs/tools/overview.mdx +1 -1
  226. package/docs/troubleshooting.mdx +8 -2
  227. package/docs/usage.md +1 -1
  228. package/package.json +6 -6
@@ -8,7 +8,6 @@ import { openFailureNote } from './cli/answers/open-note.js';
8
8
  import { realpathSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
10
  import { installCrashGuard } from './setup/setup-command.js';
11
- import { stateDirProblem } from './daemon/state-dir.js';
12
11
  import { readDevServers } from './daemon/dev-servers.js';
13
12
  import { hasAnyAppConnectedBefore, hasProjectConnectedBefore, } from '../memory/recall/prior/connection-memory.js';
14
13
  import { attachStatusFields } from '../surface/mcp/attach-memory.js';
@@ -16,7 +15,7 @@ import { splitBrainFields, withNextAction } from './cli/status-fields.js';
16
15
  import { reticleStateHome } from './daemon/daemon.js';
17
16
  import { handleMcp } from './cli/mcp-command.js';
18
17
  import { resolveDaemonForProject } from './daemon/daemon-resolve.js';
19
- import { daemonSpawnArgs, daemonStartOptions } from './cli/daemon-start-options.js';
18
+ import { daemonStartOptions } from './cli/daemon-start-options.js';
20
19
  import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
21
20
  import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv, devServersForProject, } from '@reticlehq/core';
22
21
  import { loadDotEnv } from '../telemetry/dev-repo.js';
@@ -26,20 +25,20 @@ import { createNodeFileSystem } from '../memory/project/fs/fs-port.js';
26
25
  import { affectedSavedFlows } from '../language/flows/change/flow-sources.js';
27
26
  import { availableUpdate } from './update/update-nudge.js';
28
27
  import { handleUpdate, handleRollback } from './cli/cli-update-commands.js';
29
- import { startDaemon, RETICLE_VERIFY_DEFAULT_PORT } from '../index.js';
30
- import { verifyEndpointMismatch } from '../status-payload.js';
28
+ import { startDaemon } from '../index.js';
31
29
  import { isCloudCommand, runCloudCommand } from './cli/cloud-cli.js';
32
30
  import { SERVER_VERSION } from './version/identity/server-version.js';
33
31
  import { log } from '../log.js';
34
- import { readPid, isAlive, removePid, spawnDaemon, discoverDaemonPort, writeDaemonRegistry, logPath, } from './daemon/daemon.js';
32
+ import { readPid, isAlive, removePid, spawnDaemon, discoverDaemonPort, writeDaemonRegistry, } from './daemon/daemon.js';
35
33
  import { PortPresence, probePresence, presenceIsUsable, describePresence, } from './daemon/binding/port-presence.js';
36
34
  import { waitForDaemon, probeDaemon } from '../surface/mcp/mcp-proxy.js';
37
35
  import { installDaemonResilience, recordExitReason, DaemonExitReason, } from './daemon/daemon-resilience.js';
38
36
  import { IdleShutdown, resolveIdleShutdownMs, resolveIdleCheckMs } from './daemon/idle-shutdown.js';
39
37
  import { DaemonHeartbeat, resolveHeartbeatMs } from './daemon/lifetime/heartbeat.js';
40
38
  import { everServedToolCall } from './daemon/lifetime/daemon-usefulness.js';
41
- import { DAEMON_START_FAILED_EVENT, readDaemonStartupCause, } from './daemon/lifetime/startup-failure.js';
42
- import { fetchStatus, summarizeStatus, warnOnDaemonSkew, decideOpen, openInBrowser, openCommand, } from './cli/launch/cli-launch.js';
39
+ import { DAEMON_START_FAILED_EVENT } from './daemon/lifetime/startup-failure.js';
40
+ import { summarizeStatus, warnOnDaemonSkew, decideOpen, openInBrowser, openCommand, } from './cli/launch/cli-launch.js';
41
+ import { fetchStatus } from './daemon/binding/daemon-status-probe.js';
43
42
  import { handleDrive } from './cli/drive/drive-command.js';
44
43
  import { handleVerify } from './cli/cli-verify.js';
45
44
  import { runKill } from './cli/cli-kill.js';
@@ -77,178 +76,26 @@ function handleInit(parsed) {
77
76
  }, io);
78
77
  void continueAfterInit(parsed, result, io, cwd);
79
78
  }
80
- function handleServe(parsed) {
81
- void serveWithHonestExit(parsed);
82
- }
83
- /**
84
- * Report the BIND, not the spawn.
85
- *
86
- * `serve` used to log `reticle_daemon_spawned` and exit 0 unconditionally, while the child died at
87
- * `reticle_daemon_start_failed` with an EADDRINUSE nothing joined back to the parent. Three surfaces
88
- * then disagreed about what was true and none of them named the port.
89
- *
90
- * Two changes, both about saying what is actually the case: refuse up front when the port is held by
91
- * something that is not a Reticle daemon (a spawn there cannot succeed), and after spawning, wait
92
- * for the daemon to actually answer before claiming it started.
93
- */
94
- async function serveWithHonestExit(parsed) {
95
- const presence = await probePresence(parsed.port, {
96
- tcpOpen: probeDaemon,
97
- status: fetchStatus,
98
- });
99
- if (presence === PortPresence.DAEMON) {
100
- // A daemon that is already up was started with ITS flags, not these — `serve` only attaches.
101
- // Exiting 0 here regardless is how `--http-port` came to be accepted and ignored (#687): the
102
- // running daemon kept serving whatever it was started with, and the flag vanished without a
103
- // word. Ask the daemon which verify port it actually serves and refuse when it is not the one
104
- // requested — a flag that cannot be honoured must say so, not report success.
105
- if (parsed.http) {
106
- const mismatch = verifyEndpointMismatch(await fetchStatus(parsed.port), parsed.httpPort ?? RETICLE_VERIFY_DEFAULT_PORT);
107
- if (mismatch !== undefined) {
108
- log('reticle_daemon_start_refused', { port: parsed.port, reason: mismatch });
109
- process.stderr.write(`${mismatch}\n`);
110
- process.exit(1);
111
- return;
112
- }
113
- }
114
- log('reticle_daemon_already_running', { port: parsed.port });
115
- return;
116
- }
117
- if (presence === PortPresence.FOREIGN) {
118
- log('reticle_daemon_start_refused', {
119
- port: parsed.port,
120
- presence,
121
- reason: describePresence(presence, parsed.port),
122
- });
123
- process.stderr.write(`${describePresence(presence, parsed.port)}\n`);
124
- process.exit(1);
125
- return;
126
- }
127
- const scriptPath = process.argv[1];
128
- if (scriptPath === undefined) {
129
- log('reticle_serve_no_script', {});
130
- process.exit(1);
131
- return;
132
- }
133
- const daemonArgs = daemonSpawnArgs(parsed);
134
- const startupStartedAt = Date.now();
135
- spawnDaemon(process.execPath, scriptPath, daemonArgs, parsed.port);
136
- // The child binds asynchronously and, when it cannot, exits 1 long after this process would have
137
- // reported success. Wait for it to ANSWER — `/status` responding is the only evidence a daemon
138
- // exists that does not come from the pid file the child may never have earned.
139
- const bound = await waitForPresence(parsed.port, PortPresence.DAEMON, SERVE_BIND_TIMEOUT_MS);
140
- if (!bound) {
141
- const settled = await probePresence(parsed.port, { tcpOpen: probeDaemon, status: fetchStatus });
142
- const startupCause = readDaemonStartupCause(logPath(parsed.port), startupStartedAt);
143
- log('reticle_daemon_start_failed_parent', {
144
- port: parsed.port,
145
- presence: settled,
146
- reason: describePresence(settled, parsed.port),
147
- log: logPath(parsed.port),
148
- stateDir: stateDirProblem(reticleStateHome()) === undefined ? 'writable' : 'unwritable',
149
- ...(startupCause === undefined ? {} : { cause: startupCause }),
150
- });
151
- // Name the CAUSE when we can see it. An unwritable state directory is a first-run failure mode
152
- // (locked-down home, read-only container mount, managed profile) that produced only "nothing is
153
- // listening" — the symptom restated — and then pointed at a log INSIDE that directory, which
154
- // cannot exist. Reproduced with `chmod 555 ~/.reticle`.
155
- const dirProblem = stateDirProblem(reticleStateHome());
156
- process.stderr.write(dirProblem === undefined
157
- ? startupCause === undefined
158
- ? `the daemon did not come up on :${String(parsed.port)} — ${describePresence(settled, parsed.port)}\n` +
159
- `see ${logPath(parsed.port)}\n`
160
- : `the daemon did not come up on :${String(parsed.port)}.\n${startupCause}\n` +
161
- `see ${logPath(parsed.port)}\n`
162
- : `the daemon did not come up on :${String(parsed.port)}.\n${dirProblem}\n`);
163
- process.exit(1);
164
- return;
165
- }
166
- log('reticle_daemon_spawned', { port: parsed.port, ...(parsed.http ? { http: true } : {}) });
167
- }
168
- /** How long `serve` waits for the child to bind. Generous: a cold daemon start is seconds. */
169
- const SERVE_BIND_TIMEOUT_MS = 15_000;
170
- const PRESENCE_POLL_MS = 150;
171
- /** How long a daemon gets to shut down on its own before `stop` stops asking politely. */
172
- const GRACEFUL_STOP_MS = 5000;
173
- /** How long after SIGKILL before we accept the process is not ours to kill and say so. */
174
- const FORCED_STOP_MS = 2000;
175
- async function waitForPresence(port, want, timeoutMs) {
176
- const deadline = Date.now() + timeoutMs;
177
- for (;;) {
178
- const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
179
- if (presence === want)
180
- return true;
181
- if (Date.now() >= deadline)
182
- return false;
183
- await new Promise((resolve) => setTimeout(resolve, PRESENCE_POLL_MS));
184
- }
185
- }
186
- function handleStop(port, quiet) {
187
- const pid = readPid(port);
188
- if (null === pid || !isAlive(pid)) {
189
- removePid(port);
190
- if (!quiet)
191
- log('reticle_daemon_not_running', { port });
192
- return;
193
- }
194
- process.kill(pid, 'SIGTERM');
195
- const started = Date.now();
196
- let escalated = false;
197
- const poll = setInterval(() => {
198
- if (!isAlive(pid)) {
199
- clearInterval(poll);
200
- removePid(port);
201
- if (!quiet)
202
- log('reticle_daemon_stopped', { port, pid, ...(escalated ? { escalated } : {}) });
203
- return;
204
- }
205
- const waited = Date.now() - started;
206
- // A daemon that ignores SIGTERM is WEDGED — which is the one situation `stop` exists for. Giving
207
- // up here left the port held, the pid file stale, and a human running `kill -9` by hand at the
208
- // first two minutes of setup. Escalate instead: graceful first, then unconditional.
209
- if (!escalated && waited > GRACEFUL_STOP_MS) {
210
- escalated = true;
211
- try {
212
- process.kill(pid, 'SIGKILL');
213
- }
214
- catch {
215
- // Already gone between the liveness check and here — the next tick reports it stopped.
216
- }
217
- return;
218
- }
219
- if (waited > GRACEFUL_STOP_MS + FORCED_STOP_MS) {
220
- clearInterval(poll);
221
- // Surviving SIGKILL means the pid is not ours to kill (permissions) or is an unkillable
222
- // zombie. Neither is retryable, and both need the pid named so a human can act on it.
223
- if (!quiet)
224
- log('reticle_daemon_stop_timeout', { port, pid, escalated });
225
- process.exit(1);
226
- }
227
- }, 100);
228
- }
79
+ // `serve`, `stop` and `restart` live in `cli/lifecycle/daemon-lifecycle.ts`: one idea, and the
80
+ // largest group this file could give up without splitting something that belongs together.
81
+ import { handleServe, handleStop, handleRestart } from './cli/lifecycle/daemon-lifecycle.js';
82
+ import { statusLines } from './cli/status/status-lines.js';
229
83
  /**
230
- * `reticle restart` — free the port, then put a daemon back on it and prove it bound.
84
+ * Log the status event AND print it for a person.
231
85
  *
232
- * The two halves are reported separately on purpose. A restart that killed the old daemon and then
233
- * could not start a new one leaves the user in a different place from one that could not kill
234
- * anything at all, and a single `ok: false` cannot tell them which happened.
86
+ * Both, not either: the JSON line is documented and is what a log is for, and the block is for
87
+ * whoever is reading the terminal — which, once an agent is running the command, is somebody who
88
+ * did not type it and cannot be assumed to parse it.
235
89
  */
236
- async function handleRestart(port, force) {
237
- const freed = await runKill(port, force);
238
- if (!freed) {
239
- log('reticle_restart_aborted', {
240
- port,
241
- reason: 'the port was not freed, so nothing was started — the old holder is still there',
242
- });
243
- process.exit(1);
90
+ function reportStatus(fields, json) {
91
+ if (json) {
92
+ log('reticle_status', fields);
244
93
  return;
245
94
  }
246
- // Reuses `serve`'s path, which waits for the daemon to ANSWER before claiming it started and exits
247
- // non-zero when it does not. A restart that reports success for a daemon that never bound is the
248
- // same lie `serve` already stopped telling.
249
- await serveWithHonestExit({ port, headless: true, http: false });
95
+ process.stdout.write(`${statusLines(fields).join('\n')}\n`);
250
96
  }
251
- export async function handleStatus(port) {
97
+ export async function handleStatus(port, json = false) {
98
+ const report = (fields) => reportStatus(fields, json);
252
99
  const pid = readPid(port);
253
100
  // Durable, so it survives the daemon idling out — which is the state `status` is most often run in.
254
101
  const projectId = readProjectId(process.cwd());
@@ -281,7 +128,7 @@ export async function handleStatus(port) {
281
128
  // process exists; a Reticle daemon is running only when the shared port probe reaches /status.
282
129
  const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
283
130
  if (!presenceIsUsable(presence)) {
284
- log('reticle_status', {
131
+ report({
285
132
  port,
286
133
  running: false,
287
134
  presence,
@@ -310,7 +157,7 @@ export async function handleStatus(port) {
310
157
  const update = availableUpdate();
311
158
  const nudge = update === undefined ? {} : { updateAvailable: update };
312
159
  if (payload === undefined) {
313
- log('reticle_status', {
160
+ report({
314
161
  port,
315
162
  running: true,
316
163
  pid,
@@ -342,7 +189,7 @@ export async function handleStatus(port) {
342
189
  devServerPorts,
343
190
  })
344
191
  : {};
345
- log('reticle_status', {
192
+ report({
346
193
  port,
347
194
  running: true,
348
195
  pid,
@@ -752,7 +599,7 @@ export function main() {
752
599
  void handleRestart(parsed.port, parsed.force);
753
600
  break;
754
601
  case 'status':
755
- void handleStatus(parsed.port);
602
+ void handleStatus(parsed.port, parsed.json);
756
603
  break;
757
604
  case 'license':
758
605
  handleLicense();
@@ -0,0 +1,2 @@
1
+ /** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
2
+ export declare function fetchStatus(port: number): Promise<unknown>;
@@ -0,0 +1,48 @@
1
+ import * as http from 'node:http';
2
+ import { LOOPBACK_HOST, STATUS_PATH } from '@reticlehq/core';
3
+ import { loopbackAgent } from '../../../surface/loopback-agent.js';
4
+ /**
5
+ * The impure half of `probePresence`: ask the port whether a Reticle daemon is answering.
6
+ *
7
+ * It lives beside `port-presence.ts` rather than in `launch/` because every caller of
8
+ * `probePresence` passes it — status, doctor, kill, verify, drive, the daemon lifecycle and the
9
+ * MCP wake path all spell `probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus })`.
10
+ * Filed under `launch/` it read as a launcher helper, and it is not one; the proxy could not reach
11
+ * it there without inventing a dependency on a directory it has no other business with.
12
+ *
13
+ * The rule it feeds stays pure and socket-free in `port-presence.ts`. That split is deliberate: a
14
+ * probe that starts lying is a different failure from a rule that does.
15
+ */
16
+ /** How long the daemon /status probe waits before giving up — a local loopback call is near-instant. */
17
+ const STATUS_PROBE_TIMEOUT_MS = 1000;
18
+ /** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
19
+ export function fetchStatus(port) {
20
+ return new Promise((resolve) => {
21
+ const req = http.get({
22
+ host: LOOPBACK_HOST,
23
+ port,
24
+ path: STATUS_PATH,
25
+ timeout: STATUS_PROBE_TIMEOUT_MS,
26
+ // Shares the proxy's keep-alive agent: the proxy calls this on every reconnect, and a socket
27
+ // per probe is the same churn the POST leg was fixed for.
28
+ agent: loopbackAgent,
29
+ }, (res) => {
30
+ let body = '';
31
+ res.setEncoding('utf8');
32
+ res.on('data', (chunk) => (body += chunk));
33
+ res.on('end', () => {
34
+ try {
35
+ resolve(JSON.parse(body));
36
+ }
37
+ catch {
38
+ resolve(undefined);
39
+ }
40
+ });
41
+ });
42
+ req.on('error', () => resolve(undefined));
43
+ req.on('timeout', () => {
44
+ req.destroy();
45
+ resolve(undefined);
46
+ });
47
+ });
48
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Which of `localhost`'s two answers can actually reach the bridge.
3
+ *
4
+ * `localhost` is a name with two answers, and Windows Chrome tries the IPv6 one first. The daemon
5
+ * binds `127.0.0.1`, so `loopback-alias.ts` forwards `[::1]:port` to it — best-effort by contract,
6
+ * because a machine with IPv6 off or something already holding `[::1]:port` must not stop the daemon
7
+ * serving. When that alias does not open, the daemon is serving perfectly on IPv4 and the page's
8
+ * documented default URL cannot reach it. The daemon's own view is "the bridge was up for the whole
9
+ * window", and it is right.
10
+ *
11
+ * Nothing said which half was missing. The one record was a field on the `mcp_daemon_started` log
12
+ * line, so the condition that makes `localhost` unreachable on the platform with most of our
13
+ * installs was invisible unless somebody read the daemon's JSON. The install gate's Windows cell
14
+ * reports exactly this shape — `init` exits 1 with "the SDK IS in the page and never dialled", while
15
+ * the page's own console says it could not open a websocket to `ws://localhost:<port>` after three
16
+ * attempts, and the very next boot of the same scaffold connects — and none of the three causes that
17
+ * diagnosis offers is this one.
18
+ *
19
+ * This answers it by connecting, at the moment of the diagnosis, rather than by remembering what
20
+ * happened at startup: a probe is true about now, and a startup flag is true about startup.
21
+ */
22
+ /** Which loopback addresses answered on the bridge port. */
23
+ export interface LoopbackReach {
24
+ /** `127.0.0.1` answered — what the daemon binds. */
25
+ readonly v4: boolean;
26
+ /** `[::1]` answered — what `localhost` resolves to first on Windows. */
27
+ readonly v6: boolean;
28
+ }
29
+ /**
30
+ * Probe both halves of loopback on the bridge port.
31
+ *
32
+ * Both are attempted even when the first answers: the interesting result is the DISAGREEMENT, and
33
+ * learning that v4 works tells you nothing about the address the browser will actually pick.
34
+ */
35
+ export declare function probeLoopbackReach(port: number): Promise<LoopbackReach>;
36
+ /**
37
+ * True when the bridge is reachable as an IPv4 address and NOT as the name the SDK is told to dial.
38
+ *
39
+ * This is the whole condition worth naming. Both up is healthy; both down is an ordinary "the daemon
40
+ * is not there" that every other check already reports; v6-only cannot happen on a `127.0.0.1` bind.
41
+ */
42
+ export declare function isLocalhostSplit(reach: LoopbackReach): boolean;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Which of `localhost`'s two answers can actually reach the bridge.
3
+ *
4
+ * `localhost` is a name with two answers, and Windows Chrome tries the IPv6 one first. The daemon
5
+ * binds `127.0.0.1`, so `loopback-alias.ts` forwards `[::1]:port` to it — best-effort by contract,
6
+ * because a machine with IPv6 off or something already holding `[::1]:port` must not stop the daemon
7
+ * serving. When that alias does not open, the daemon is serving perfectly on IPv4 and the page's
8
+ * documented default URL cannot reach it. The daemon's own view is "the bridge was up for the whole
9
+ * window", and it is right.
10
+ *
11
+ * Nothing said which half was missing. The one record was a field on the `mcp_daemon_started` log
12
+ * line, so the condition that makes `localhost` unreachable on the platform with most of our
13
+ * installs was invisible unless somebody read the daemon's JSON. The install gate's Windows cell
14
+ * reports exactly this shape — `init` exits 1 with "the SDK IS in the page and never dialled", while
15
+ * the page's own console says it could not open a websocket to `ws://localhost:<port>` after three
16
+ * attempts, and the very next boot of the same scaffold connects — and none of the three causes that
17
+ * diagnosis offers is this one.
18
+ *
19
+ * This answers it by connecting, at the moment of the diagnosis, rather than by remembering what
20
+ * happened at startup: a probe is true about now, and a startup flag is true about startup.
21
+ */
22
+ import net from 'node:net';
23
+ import { LOOPBACK_HOST } from '@reticlehq/core';
24
+ import { IPV6_LOOPBACK } from './loopback-alias.js';
25
+ /** Long enough for a loopback TCP handshake, short enough to sit inside a diagnosis. */
26
+ const REACH_TIMEOUT_MS = 500;
27
+ /** Can a TCP connection be opened to this address and port at all. */
28
+ function canConnect(host, port) {
29
+ return new Promise((resolve) => {
30
+ const socket = net.connect({ host, port });
31
+ const settle = (answer) => {
32
+ socket.destroy();
33
+ resolve(answer);
34
+ };
35
+ socket.setTimeout(REACH_TIMEOUT_MS, () => settle(false));
36
+ socket.once('connect', () => settle(true));
37
+ // Refused, unreachable, IPv6 disabled entirely — all of them are "this address does not answer",
38
+ // which is the only question being asked. An unhandled 'error' here would take down the caller.
39
+ socket.once('error', () => settle(false));
40
+ });
41
+ }
42
+ /**
43
+ * Probe both halves of loopback on the bridge port.
44
+ *
45
+ * Both are attempted even when the first answers: the interesting result is the DISAGREEMENT, and
46
+ * learning that v4 works tells you nothing about the address the browser will actually pick.
47
+ */
48
+ export async function probeLoopbackReach(port) {
49
+ const [v4, v6] = await Promise.all([
50
+ canConnect(LOOPBACK_HOST, port),
51
+ canConnect(IPV6_LOOPBACK, port),
52
+ ]);
53
+ return { v4, v6 };
54
+ }
55
+ /**
56
+ * True when the bridge is reachable as an IPv4 address and NOT as the name the SDK is told to dial.
57
+ *
58
+ * This is the whole condition worth naming. Both up is healthy; both down is an ordinary "the daemon
59
+ * is not there" that every other check already reports; v6-only cannot happen on a `127.0.0.1` bind.
60
+ */
61
+ export function isLocalhostSplit(reach) {
62
+ return reach.v4 && !reach.v6;
63
+ }
@@ -57,6 +57,18 @@ export type DaemonExitReason = (typeof DaemonExitReason)[keyof typeof DaemonExit
57
57
  export declare function recordExitReason(reason: DaemonExitReason): void;
58
58
  export declare function installExitTrace(proc: ProcessLike, log: LogFn): void;
59
59
  export declare function installDaemonResilience(proc: ProcessLike, log: LogFn, onFatal: () => void): void;
60
+ /** The handle the proxy keeps, so it can say when a session proved itself usable. */
61
+ export interface ProxyResilience {
62
+ /**
63
+ * A stream reached an `endpoint` frame: there is a working session, so whatever disconnects came
64
+ * before it were reconnects rather than a runaway.
65
+ *
66
+ * The same signal that already earns a fresh RETRY budget in `mcp-proxy.ts`, and chosen there for
67
+ * the same reason: response headers are produced by a daemon that accepts SSE and drops it, and an
68
+ * `endpoint` frame is not.
69
+ */
70
+ noteUsableSession(): void;
71
+ }
60
72
  /**
61
73
  * Process-level resilience for the MCP PROXY, whose rule is the opposite of the daemon's.
62
74
  *
@@ -70,5 +82,5 @@ export declare function installDaemonResilience(proc: ProcessLike, log: LogFn, o
70
82
  * `onCrash` is for reporting only — it must not end the process. `onGone` is the ONE exception, and
71
83
  * it is the subject of DISCONNECT_STORM below.
72
84
  */
73
- export declare function installProxyResilience(proc: ProcessLike, log: LogFn, onCrash?: (kind: CrashKind, cause: unknown) => void, onGone?: () => void): void;
85
+ export declare function installProxyResilience(proc: ProcessLike, log: LogFn, onCrash?: (kind: CrashKind, cause: unknown) => void, onGone?: () => void): ProxyResilience;
74
86
  export {};
@@ -290,7 +290,10 @@ export function installProxyResilience(proc, log, onCrash = reportCrash, onGone
290
290
  unreachable: 'reticle_mcp_proxy_daemon_unreachable',
291
291
  };
292
292
  // "Absorb the disconnect and keep serving" is right for ONE disconnect and catastrophic for a
293
- // stream of them — see DISCONNECT_STORM.
293
+ // stream of them — see DISCONNECT_STORM. Counted since the last USABLE SESSION rather than since
294
+ // this process started: a lifetime tally reaches the threshold on a working week, because the
295
+ // classifier reads `ECONNRESET` as "the peer vanished" and on this leg the peer is the DAEMON,
296
+ // which restarts routinely and is the thing the proxy exists to survive.
294
297
  let disconnects = 0;
295
298
  let gone = false;
296
299
  const absorb = (value) => {
@@ -309,6 +312,13 @@ export function installProxyResilience(proc, log, onCrash = reportCrash, onGone
309
312
  }
310
313
  return true;
311
314
  };
315
+ const noteUsableSession = () => {
316
+ // Not after `gone`: the process is already leaving, and a late frame must not look like a
317
+ // reason to stay. Nothing brings the proxy back from `onGone` by design.
318
+ if (gone)
319
+ return;
320
+ disconnects = 0;
321
+ };
312
322
  proc.on('unhandledRejection', (reason) => {
313
323
  if (absorb(reason))
314
324
  return;
@@ -325,4 +335,5 @@ export function installProxyResilience(proc, log, onCrash = reportCrash, onGone
325
335
  });
326
336
  onCrash(CrashKind.UNCAUGHT_EXCEPTION, err);
327
337
  });
338
+ return { noteUsableSession };
328
339
  }
@@ -1,3 +1,4 @@
1
+ export { MAX_DAEMON_LOG_BYTES, recoverOversizedLog } from './lifetime/log-cap.js';
1
2
  /** Env override for the whole state directory — see ReticleEnv.STATE_DIR. */
2
3
  export declare const STATE_DIR_ENV: "RETICLE_STATE_DIR";
3
4
  /**
@@ -10,20 +11,10 @@ export declare const STATE_DIR_ENV: "RETICLE_STATE_DIR";
10
11
  */
11
12
  export declare function reticleStateHome(): string;
12
13
  export declare function logPath(port: number): string;
13
- /**
14
- * How large a daemon log may get before it is rolled over to `<name>.1`.
15
- *
16
- * It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
17
- * is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
18
- * nothing ever reclaims. One previous generation is kept, because the question people bring to this
19
- * file ("what happened just now?") is answered by the current one and the question they bring next
20
- * ("and just before that?") is answered by the other.
21
- */
22
- export declare const MAX_DAEMON_LOG_BYTES: number;
23
14
  /**
24
15
  * Roll the log over if it has grown past the cap. Best-effort: a daemon must still start when the
25
16
  * log cannot be rotated, because failing to launch over housekeeping is strictly worse than a large
26
- * file.
17
+ * file. The cap itself, the mid-run check and the prune live in lifetime/log-cap.ts.
27
18
  */
28
19
  export declare function rotateDaemonLog(path: string, deps: {
29
20
  fileSize(p: string): number;
@@ -5,6 +5,10 @@ import { spawn } from 'node:child_process';
5
5
  import { daemonRegistryFileName, ReticleEnv } from '@reticlehq/core';
6
6
  import { log } from '../../log.js';
7
7
  import { uncleanPredecessor } from './lifetime/unclean-predecessor.js';
8
+ import { MAX_DAEMON_LOG_BYTES, pruneOldDaemonLogs } from './lifetime/log-cap.js';
9
+ // Re-exported from their owner so callers outside this directory — the proxy log, the rotation
10
+ // tests — keep reaching `daemon` rather than `lifetime`, which is a sink. See lifetime/log-cap.ts.
11
+ export { MAX_DAEMON_LOG_BYTES, recoverOversizedLog } from './lifetime/log-cap.js';
8
12
  /** Env override for the whole state directory — see ReticleEnv.STATE_DIR. */
9
13
  export const STATE_DIR_ENV = ReticleEnv.STATE_DIR;
10
14
  /**
@@ -30,20 +34,10 @@ function registryPath(port) {
30
34
  export function logPath(port) {
31
35
  return join(reticleStateHome(), `daemon-${port}.log`);
32
36
  }
33
- /**
34
- * How large a daemon log may get before it is rolled over to `<name>.1`.
35
- *
36
- * It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
37
- * is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
38
- * nothing ever reclaims. One previous generation is kept, because the question people bring to this
39
- * file ("what happened just now?") is answered by the current one and the question they bring next
40
- * ("and just before that?") is answered by the other.
41
- */
42
- export const MAX_DAEMON_LOG_BYTES = 8 * 1024 * 1024;
43
37
  /**
44
38
  * Roll the log over if it has grown past the cap. Best-effort: a daemon must still start when the
45
39
  * log cannot be rotated, because failing to launch over housekeeping is strictly worse than a large
46
- * file.
40
+ * file. The cap itself, the mid-run check and the prune live in lifetime/log-cap.ts.
47
41
  */
48
42
  export function rotateDaemonLog(path, deps, max = MAX_DAEMON_LOG_BYTES) {
49
43
  try {
@@ -259,6 +253,9 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
259
253
  // Roll the log BEFORE opening the append handle, or the daemon writes into the renamed file
260
254
  // through an fd that no longer matches any name anyone can find.
261
255
  rotateDaemonLog(logFilePath, deps);
256
+ // Every OTHER port's leftovers, while we are already here and holding the directory. The clock is
257
+ // read at this I/O boundary rather than injected, same as the timestamp written below.
258
+ pruneOldDaemonLogs(deps.home, Date.now());
262
259
  let logFd;
263
260
  try {
264
261
  logFd = deps.openFile(logFilePath, 'a');
@@ -51,6 +51,14 @@ interface DaemonHeartbeatOptions {
51
51
  intervalMs?: number;
52
52
  /** Injected, per the repo rule: never call Date.now() inside logic. */
53
53
  clock?: () => number;
54
+ /**
55
+ * The log file this beat writes into, for the size check that rides along with it. Injected only
56
+ * by tests; the default is the daemon's own stderr. See guardDaemonLog.
57
+ */
58
+ logGuard?: {
59
+ size(): number;
60
+ truncate(): void;
61
+ };
54
62
  }
55
63
  export declare class DaemonHeartbeat {
56
64
  #private;
@@ -17,6 +17,7 @@
17
17
  *
18
18
  * A heartbeat nobody interprets is just log volume.
19
19
  */
20
+ import { DAEMON_LOG_TRUNCATED_EVENT, guardDaemonLog } from './log-cap.js';
20
21
  /** The event name a beat carries. One `grep` separates liveness from everything else in the file. */
21
22
  export const DAEMON_HEARTBEAT_EVENT = 'reticle_daemon_alive';
22
23
  /** Written by installExitTrace when the process exits through Node. */
@@ -55,6 +56,7 @@ export class DaemonHeartbeat {
55
56
  #facts;
56
57
  #intervalMs;
57
58
  #clock;
59
+ #logGuard;
58
60
  #startedAt;
59
61
  #timer;
60
62
  constructor(opts) {
@@ -62,6 +64,7 @@ export class DaemonHeartbeat {
62
64
  this.#facts = opts.facts;
63
65
  this.#intervalMs = opts.intervalMs ?? DAEMON_HEARTBEAT_MS;
64
66
  this.#clock = opts.clock ?? (() => Date.now());
67
+ this.#logGuard = opts.logGuard;
65
68
  this.#startedAt = this.#clock();
66
69
  }
67
70
  /**
@@ -72,6 +75,13 @@ export class DaemonHeartbeat {
72
75
  * ambiguity this exists to remove.
73
76
  */
74
77
  beat() {
78
+ // The one thing a running daemon does on a timer, so it is also where the log it is filling gets
79
+ // looked at. `rotateDaemonLog` runs at spawn and never again: a detached daemon then writes for
80
+ // days with nothing checking the size, which is how a log reaches its cap with no mechanism left
81
+ // that would ever trim it. The beat is itself one of those writes.
82
+ const reclaimed = guardDaemonLog(this.#logGuard);
83
+ if (0 < reclaimed)
84
+ this.#log(DAEMON_LOG_TRUNCATED_EVENT, { reclaimedBytes: reclaimed });
75
85
  const facts = this.#facts();
76
86
  this.#log(DAEMON_HEARTBEAT_EVENT, {
77
87
  uptimeMs: this.#clock() - this.#startedAt,