@reticlehq/server 3.1.0 → 3.3.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.
- package/SKILL.md +30 -28
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +37 -5
- package/dist/command/cli/cli-flow-commands.d.ts +2 -1
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +38 -7
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +131 -19
- package/dist/command/cli/cloud-cli.js +7 -6
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +3 -7
- package/dist/command/cli/config/config-discovery.d.ts +12 -0
- package/dist/command/cli/config/config-discovery.js +19 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
- package/dist/command/cli/doctor/doctor-rows.js +12 -0
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +1 -1
- package/dist/command/cli/launch/cli-launch.d.ts +3 -23
- package/dist/command/cli/launch/cli-launch.js +29 -70
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
- package/dist/command/cli/mcp-command.js +5 -3
- package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
- package/dist/command/cli/ports/resolve/cli-port.js +50 -2
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli/status/status-lines.d.ts +28 -0
- package/dist/command/cli/status/status-lines.js +117 -0
- package/dist/command/cli.d.ts +1 -1
- package/dist/command/cli.js +29 -177
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
- package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
- package/dist/command/daemon/binding/loopback-reach.js +63 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/daemon-resilience.d.ts +13 -1
- package/dist/command/daemon/daemon-resilience.js +12 -1
- package/dist/command/daemon/daemon.d.ts +2 -11
- package/dist/command/daemon/daemon.js +8 -11
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
- package/dist/command/daemon/lifetime/heartbeat.js +10 -0
- package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
- package/dist/command/daemon/lifetime/log-cap.js +115 -0
- package/dist/command/setup/agent-notice-memory.d.ts +16 -0
- package/dist/command/setup/agent-notice-memory.js +65 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
- package/dist/command/setup/bringup/dev-server-wait.js +16 -0
- package/dist/command/setup/bringup/ensure-daemon.js +1 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/command/setup/init/init-runtime.js +13 -1
- package/dist/command/setup/node-effects.js +1 -1
- package/dist/command/setup/probe/page-probe.js +9 -2
- package/dist/command/setup/run-setup.d.ts +25 -7
- package/dist/command/setup/run-setup.js +51 -9
- package/dist/command/setup/setup-command.js +31 -3
- package/dist/command/setup/terminal/confirm.js +2 -1
- package/dist/command/update/updater.d.ts +27 -0
- package/dist/command/update/updater.js +35 -2
- package/dist/features/crawl/crawl-tools.js +3 -1
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +3 -1
- package/dist/features/harness/drive-report.d.ts +73 -0
- package/dist/features/harness/drive-report.js +196 -0
- package/dist/features/harness/driver.js +29 -8
- package/dist/features/harness/drivers.d.ts +19 -0
- package/dist/features/harness/drivers.js +19 -0
- package/dist/features/harness/fill-values.d.ts +67 -0
- package/dist/features/harness/fill-values.js +153 -0
- package/dist/features/harness/jev-driver.d.ts +188 -0
- package/dist/features/harness/jev-driver.js +661 -0
- package/dist/features/harness/openai-driver.d.ts +46 -0
- package/dist/features/harness/openai-driver.js +197 -0
- package/dist/features/harness/platform-config.d.ts +62 -0
- package/dist/features/harness/platform-config.js +97 -0
- package/dist/features/visual/visual-tools.js +26 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +80 -130
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/judgement/runs/verification-sync.d.ts +2 -2
- package/dist/judgement/runs/verification-sync.js +9 -2
- package/dist/language/flows/affected-tools.js +6 -3
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +55 -10
- package/dist/language/flows/flow-replay-run.js +217 -58
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +29 -11
- package/dist/language/flows/flow-replay.js +221 -203
- package/dist/language/flows/flow-result.d.ts +18 -1
- package/dist/language/flows/flow-result.js +18 -2
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-store-for-session.d.ts +2 -1
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flow-tools.js +8 -4
- package/dist/language/flows/flows.d.ts +15 -10
- package/dist/language/flows/flows.js +17 -4
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/narrow-write.d.ts +4 -4
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +13 -9
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/suite-selection.d.ts +15 -1
- package/dist/language/flows/suite-selection.js +1 -1
- package/dist/language/flows/verify-change-tools.js +21 -4
- package/dist/memory/cloud/account-state.d.ts +1 -1
- package/dist/memory/cloud/account-state.js +5 -4
- package/dist/memory/cloud/cloud-config.d.ts +9 -0
- package/dist/memory/cloud/cloud-config.js +9 -0
- package/dist/memory/cloud/cloud-sync.d.ts +7 -4
- package/dist/memory/cloud/cloud-sync.js +11 -6
- package/dist/memory/cloud/harness-config.d.ts +12 -0
- package/dist/memory/cloud/harness-config.js +58 -0
- package/dist/memory/cloud/harness-offer.d.ts +44 -0
- package/dist/memory/cloud/harness-offer.js +146 -0
- package/dist/memory/cloud/harness-switch.d.ts +11 -0
- package/dist/memory/cloud/harness-switch.js +53 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +35 -2
- package/dist/memory/cloud/sync-daemon.js +45 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +2 -1
- package/dist/memory/impact/impact-store.d.ts +5 -0
- package/dist/memory/impact/impact-store.js +25 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +253 -141
- package/dist/memory/intent/intent-shard.d.ts +22 -23
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/attach-journal.d.ts +11 -0
- package/dist/memory/journal/attach-journal.js +1 -1
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-query.d.ts +35 -1
- package/dist/memory/journal/journal-query.js +46 -0
- package/dist/memory/journal/journal-recorder.d.ts +71 -2
- package/dist/memory/journal/journal-recorder.js +45 -7
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +91 -2
- package/dist/memory/journal/on-disk/retention.js +181 -11
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/self-observation.d.ts +3 -0
- package/dist/memory/journal/self-observation.js +67 -0
- package/dist/memory/journal/session-end.d.ts +17 -4
- package/dist/memory/journal/session-end.js +85 -7
- package/dist/memory/journal/session-journal.d.ts +122 -5
- package/dist/memory/journal/session-journal.js +489 -32
- package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
- package/dist/memory/project/artifact-root-resolver.js +89 -0
- package/dist/memory/project/artifact-root.d.ts +39 -2
- package/dist/memory/project/artifact-root.js +58 -1
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
- package/dist/memory/project/dir/fill-value-store.js +88 -0
- package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
- package/dist/memory/project/dir/reticle-dir.js +11 -0
- package/dist/memory/project/fs/fs-port.d.ts +22 -1
- package/dist/memory/project/fs/fs-port.js +72 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/project/learned-routes.d.ts +16 -2
- package/dist/memory/project/learned-routes.js +26 -10
- package/dist/memory/project/project-for-root.d.ts +30 -0
- package/dist/memory/project/project-for-root.js +43 -0
- package/dist/memory/project/project-tools.js +10 -5
- package/dist/memory/project/session-root.d.ts +35 -6
- package/dist/memory/project/session-root.js +70 -8
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
- package/dist/portal/bridge/auth-failure-reason.js +20 -3
- package/dist/portal/bridge/bridge.d.ts +6 -1
- package/dist/portal/bridge/bridge.js +36 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/facts/session-replaced.d.ts +19 -0
- package/dist/portal/session/facts/session-replaced.js +22 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -60
- package/dist/portal/session/no-session-next-action.js +3 -2
- package/dist/portal/session/no-session-watch.js +14 -10
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/rebind.d.ts +18 -0
- package/dist/portal/session/rebind.js +38 -0
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +13 -0
- package/dist/portal/session/session-manager.js +42 -3
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +26 -10
- package/dist/portal/session/session.js +48 -49
- package/dist/status-payload.d.ts +12 -1
- package/dist/status-payload.js +4 -1
- package/dist/surface/http-server.js +3 -5
- package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
- package/dist/surface/mcp/faults/mcp-outage.js +20 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
- package/dist/surface/mcp/mcp-proxy.js +23 -12
- package/dist/surface/mcp/mcp.d.ts +10 -1
- package/dist/surface/mcp/mcp.js +59 -29
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
- package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
- package/dist/surface/mcp/proxy/proxy-log.js +7 -22
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/act-preflight.js +6 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/sequence-grade.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +49 -8
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +74 -21
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/coverage-tools.js +5 -2
- package/dist/surface/tools/dynamic-tools.js +43 -46
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +15 -0
- package/dist/surface/tools/explore-tools.js +80 -7
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +50 -3
- package/dist/surface/tools/first-sentence.d.ts +24 -0
- package/dist/surface/tools/first-sentence.js +0 -0
- package/dist/surface/tools/harness-explore.d.ts +125 -1
- package/dist/surface/tools/harness-explore.js +401 -15
- package/dist/surface/tools/harness-plan.d.ts +76 -0
- package/dist/surface/tools/harness-plan.js +156 -0
- package/dist/surface/tools/harness-toolset.js +12 -1
- package/dist/surface/tools/invoke-tool.js +82 -9
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/live-call-text.d.ts +6 -0
- package/dist/surface/tools/live-call-text.js +36 -2
- package/dist/surface/tools/observe-tools.js +69 -15
- package/dist/surface/tools/read/query-windowed.d.ts +1 -1
- package/dist/surface/tools/read/query-windowed.js +1 -1
- package/dist/surface/tools/read-tools.js +1 -2
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +23 -3
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools-helpers.d.ts +8 -7
- package/dist/surface/tools/tools-helpers.js +11 -13
- package/dist/surface/tools/tools.js +70 -30
- package/dist/telemetry/bug-found.js +32 -2
- package/dist/telemetry/feedback-context.js +3 -3
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/harness-drive.d.ts +6 -0
- package/dist/telemetry/harness-drive.js +39 -0
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/mcp-connection.d.ts +14 -1
- package/dist/telemetry/mcp-connection.js +17 -2
- package/dist/telemetry/onboarding-firsts.d.ts +30 -1
- package/dist/telemetry/onboarding-firsts.js +54 -12
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/telemetry/verification-of.js +31 -3
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/agent-cheatsheet.md +7 -3
- package/docs/autodrive.md +42 -2
- package/docs/benchmarks.md +1 -1
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/cloud.mdx +11 -9
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/cli/status.mdx +35 -20
- package/docs/cli.mdx +8 -18
- package/docs/desktop.mdx +1 -1
- package/docs/faq.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +144 -4
- package/docs/system-map.md +6 -4
- package/docs/telemetry.md +2 -1
- package/docs/token-efficiency.md +2 -2
- package/docs/tools/overview.mdx +1 -1
- package/docs/tools/query.mdx +10 -10
- package/docs/troubleshooting.mdx +8 -2
- package/docs/usage.md +2 -2
- package/docs/what-is-recorded.md +62 -0
- package/package.json +5 -5
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -0,0 +1,32 @@
|
|
|
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>;
|
|
3
|
+
/** One connected tab as `reticle status` reports it — the at-a-glance health line. */
|
|
4
|
+
interface StatusSession {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
url: string;
|
|
7
|
+
projectId?: string;
|
|
8
|
+
throttled: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The tab is backgrounded. Reported by the daemon on every session; `false` on one too old to say,
|
|
11
|
+
* which reads as visible — the conservative answer, since it keeps the reuse this command has
|
|
12
|
+
* always done rather than opening a tab on a guess.
|
|
13
|
+
*/
|
|
14
|
+
hidden: boolean;
|
|
15
|
+
stale: boolean;
|
|
16
|
+
pendingMarks: number;
|
|
17
|
+
/** The tab is attached and answering nothing — see Session.unresponsive. Absent on an older daemon. */
|
|
18
|
+
unresponsive?: true;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
|
|
22
|
+
* untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
|
|
23
|
+
* degrades to "running, 0 sessions" instead of throwing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function summarizeStatus(payload: unknown): {
|
|
26
|
+
sessionCount: number;
|
|
27
|
+
sessions: StatusSession[];
|
|
28
|
+
why?: string;
|
|
29
|
+
/** The same diagnosis without the differential, for a surface a person reads. */
|
|
30
|
+
whyLead?: string;
|
|
31
|
+
};
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
|
|
51
|
+
* untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
|
|
52
|
+
* degrades to "running, 0 sessions" instead of throwing.
|
|
53
|
+
*/
|
|
54
|
+
export function summarizeStatus(payload) {
|
|
55
|
+
if (typeof payload !== 'object' || null === payload)
|
|
56
|
+
return { sessionCount: 0, sessions: [] };
|
|
57
|
+
const obj = payload;
|
|
58
|
+
// Carried through to the printed line: with no sessions this is the whole answer, and dropping it
|
|
59
|
+
// here would silently undo the reason it is on the wire.
|
|
60
|
+
const why = 'string' === typeof obj['why'] ? obj['why'] : undefined;
|
|
61
|
+
// Absent on a daemon older than this field, which is why every reader falls back to `why`.
|
|
62
|
+
const whyLead = 'string' === typeof obj['whyLead'] ? obj['whyLead'] : undefined;
|
|
63
|
+
const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
|
|
64
|
+
const sessions = raw
|
|
65
|
+
.map((s) => {
|
|
66
|
+
if (typeof s !== 'object' || null === s)
|
|
67
|
+
return null;
|
|
68
|
+
const r = s;
|
|
69
|
+
const sessionId = 'string' === typeof r['sessionId'] ? r['sessionId'] : '';
|
|
70
|
+
if ('' === sessionId)
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
sessionId,
|
|
74
|
+
url: 'string' === typeof r['url'] ? r['url'] : '',
|
|
75
|
+
...('string' === typeof r['projectId'] && 0 < r['projectId'].length
|
|
76
|
+
? { projectId: r['projectId'] }
|
|
77
|
+
: {}),
|
|
78
|
+
throttled: true === r['throttled'],
|
|
79
|
+
hidden: true === r['hidden'],
|
|
80
|
+
stale: true === r['stale'],
|
|
81
|
+
pendingMarks: 'number' === typeof r['pendingMarks'] ? r['pendingMarks'] : 0,
|
|
82
|
+
...(true === r['unresponsive'] ? { unresponsive: true } : {}),
|
|
83
|
+
};
|
|
84
|
+
})
|
|
85
|
+
.filter((s) => s !== null);
|
|
86
|
+
const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
|
|
87
|
+
return {
|
|
88
|
+
sessionCount,
|
|
89
|
+
sessions,
|
|
90
|
+
...(why === undefined ? {} : { why }),
|
|
91
|
+
...(whyLead === undefined ? {} : { whyLead }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -65,7 +65,7 @@ export declare function probePresence(port: number, probes: {
|
|
|
65
65
|
* these lets the sentence tell them apart when — and only when — the recorded pid for the port
|
|
66
66
|
* matches the process actually holding it.
|
|
67
67
|
*/
|
|
68
|
-
|
|
68
|
+
interface PortHolder {
|
|
69
69
|
/** The pid we recorded for this port, from the daemon pid file. `null` when there is none. */
|
|
70
70
|
ourPid: number | null;
|
|
71
71
|
/** The pid actually holding the port, when the caller could determine it. */
|
|
@@ -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):
|
|
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');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
interface IdleShutdownOptions {
|
|
2
2
|
/** True when nothing is using the daemon: no agent connected, no browser session, no pool lease. */
|
|
3
3
|
isIdle: () => boolean;
|
|
4
4
|
/** Clean teardown + process exit. Called at most once. */
|
|
@@ -52,3 +52,4 @@ export declare function resolveIdleCheckMs(raw: string | undefined): number;
|
|
|
52
52
|
* broken.
|
|
53
53
|
*/
|
|
54
54
|
export declare function resolveIdleShutdownMs(raw: string | undefined): number;
|
|
55
|
+
export {};
|
|
@@ -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,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How big the daemon's log may get, and the three things that keep it there.
|
|
3
|
+
*
|
|
4
|
+
* `rotateDaemonLog` runs at spawn. `guardDaemonLog` runs while the daemon lives, because a detached
|
|
5
|
+
* daemon writes for days — a beat every thirty seconds plus every session, tool call and reconnect —
|
|
6
|
+
* and a size checked once at start is a size nothing will ever check again. A log measured at 98% of
|
|
7
|
+
* the cap and still climbing had no remaining mechanism that would trim it, and the tracker carries
|
|
8
|
+
* the same failure with a bigger number: the daemon log filling a disk, and a user who left over it.
|
|
9
|
+
* `pruneOldDaemonLogs` is the other half of the disk — one file per port ever bound, forever.
|
|
10
|
+
*
|
|
11
|
+
* Here rather than in daemon.ts because the size check has to be called from the heartbeat, and
|
|
12
|
+
* `lifetime` is a sink: nothing in it may reach back into its parent. Same reason nothing here
|
|
13
|
+
* logs — the caller that has a logger emits `DAEMON_LOG_TRUNCATED_EVENT`.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* How large a daemon log may get before it is rolled over to `<name>.1`.
|
|
17
|
+
*
|
|
18
|
+
* It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
|
|
19
|
+
* is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
|
|
20
|
+
* nothing ever reclaims. One previous generation is kept, because the question people bring to this
|
|
21
|
+
* file ("what happened just now?") is answered by the current one and the question they bring next
|
|
22
|
+
* ("and just before that?") is answered by the other.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MAX_DAEMON_LOG_BYTES: number;
|
|
25
|
+
/**
|
|
26
|
+
* Reclaim a log that has already run past `max`, by TRUNCATING IT IN PLACE. Returns bytes reclaimed.
|
|
27
|
+
*
|
|
28
|
+
* The one definition behind both logs Reticle writes: the daemon log, and the MCP proxy log in
|
|
29
|
+
* surface/mcp/proxy/proxy-log.ts, which delegates to this. An uncapped proxy log has filled a disk
|
|
30
|
+
* and broken unrelated builds, Docker and ordinary shell commands with ENOSPC; the daemon log is
|
|
31
|
+
* larger, longer-lived and had the same hole, so it gets the same answer rather than a second one.
|
|
32
|
+
*
|
|
33
|
+
* In place, never renamed or unlinked: a rename moves the bytes without reclaiming a byte, and a
|
|
34
|
+
* file a running process still holds open keeps its blocks allocated after an unlink until that
|
|
35
|
+
* handle closes. `: > ~/.reticle/daemon-4400.log` is the operation being copied here.
|
|
36
|
+
*
|
|
37
|
+
* Best-effort, in the same spirit as `rotateDaemonLog`: dying over housekeeping is strictly worse
|
|
38
|
+
* than a large file.
|
|
39
|
+
*/
|
|
40
|
+
export declare function recoverOversizedLog(deps: {
|
|
41
|
+
size(): number;
|
|
42
|
+
truncate(): void;
|
|
43
|
+
}, max?: number): number;
|
|
44
|
+
/** Emitted into the fresh log immediately after a truncation, so the gap explains itself. */
|
|
45
|
+
export declare const DAEMON_LOG_TRUNCATED_EVENT = "reticle_daemon_log_truncated";
|
|
46
|
+
/** Keep a RUNNING daemon's log under the cap. Returns the bytes reclaimed, 0 when it was fine. */
|
|
47
|
+
export declare function guardDaemonLog(deps?: {
|
|
48
|
+
size(): number;
|
|
49
|
+
truncate(): void;
|
|
50
|
+
}): number;
|
|
51
|
+
/**
|
|
52
|
+
* How long a daemon log outlives its daemon.
|
|
53
|
+
*
|
|
54
|
+
* Every port ever bound leaves a `daemon-<port>.log` behind and nothing removed any of them — one
|
|
55
|
+
* machine held 128 files in `~/.reticle`, logs the large majority. Nothing enumerates them either:
|
|
56
|
+
* every reader (`doctor`, the startup-cause reader, the lifecycle messages) opens the log of the
|
|
57
|
+
* port it is asking about right now, so a log for a port that will never be bound again is read by
|
|
58
|
+
* nobody. A week is long enough to still have last Tuesday's crash when somebody asks about it.
|
|
59
|
+
*/
|
|
60
|
+
export declare const DAEMON_LOG_RETENTION_MS: number;
|
|
61
|
+
/**
|
|
62
|
+
* Delete daemon logs nothing has written to in the retention window. Returns the names removed.
|
|
63
|
+
*
|
|
64
|
+
* Age is the whole test, and it doubles as the liveness probe: a live daemon beats into its log
|
|
65
|
+
* every thirty seconds, so a log older than a week has no daemon behind it by construction.
|
|
66
|
+
* Best-effort throughout — a log that will not delete is not a reason to fail a daemon start.
|
|
67
|
+
*/
|
|
68
|
+
export declare function pruneOldDaemonLogs(home: string, nowMs: number): string[];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How big the daemon's log may get, and the three things that keep it there.
|
|
3
|
+
*
|
|
4
|
+
* `rotateDaemonLog` runs at spawn. `guardDaemonLog` runs while the daemon lives, because a detached
|
|
5
|
+
* daemon writes for days — a beat every thirty seconds plus every session, tool call and reconnect —
|
|
6
|
+
* and a size checked once at start is a size nothing will ever check again. A log measured at 98% of
|
|
7
|
+
* the cap and still climbing had no remaining mechanism that would trim it, and the tracker carries
|
|
8
|
+
* the same failure with a bigger number: the daemon log filling a disk, and a user who left over it.
|
|
9
|
+
* `pruneOldDaemonLogs` is the other half of the disk — one file per port ever bound, forever.
|
|
10
|
+
*
|
|
11
|
+
* Here rather than in daemon.ts because the size check has to be called from the heartbeat, and
|
|
12
|
+
* `lifetime` is a sink: nothing in it may reach back into its parent. Same reason nothing here
|
|
13
|
+
* logs — the caller that has a logger emits `DAEMON_LOG_TRUNCATED_EVENT`.
|
|
14
|
+
*/
|
|
15
|
+
import { fstatSync, ftruncateSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
/**
|
|
18
|
+
* How large a daemon log may get before it is rolled over to `<name>.1`.
|
|
19
|
+
*
|
|
20
|
+
* It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
|
|
21
|
+
* is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
|
|
22
|
+
* nothing ever reclaims. One previous generation is kept, because the question people bring to this
|
|
23
|
+
* file ("what happened just now?") is answered by the current one and the question they bring next
|
|
24
|
+
* ("and just before that?") is answered by the other.
|
|
25
|
+
*/
|
|
26
|
+
export const MAX_DAEMON_LOG_BYTES = 8 * 1024 * 1024;
|
|
27
|
+
/**
|
|
28
|
+
* Reclaim a log that has already run past `max`, by TRUNCATING IT IN PLACE. Returns bytes reclaimed.
|
|
29
|
+
*
|
|
30
|
+
* The one definition behind both logs Reticle writes: the daemon log, and the MCP proxy log in
|
|
31
|
+
* surface/mcp/proxy/proxy-log.ts, which delegates to this. An uncapped proxy log has filled a disk
|
|
32
|
+
* and broken unrelated builds, Docker and ordinary shell commands with ENOSPC; the daemon log is
|
|
33
|
+
* larger, longer-lived and had the same hole, so it gets the same answer rather than a second one.
|
|
34
|
+
*
|
|
35
|
+
* In place, never renamed or unlinked: a rename moves the bytes without reclaiming a byte, and a
|
|
36
|
+
* file a running process still holds open keeps its blocks allocated after an unlink until that
|
|
37
|
+
* handle closes. `: > ~/.reticle/daemon-4400.log` is the operation being copied here.
|
|
38
|
+
*
|
|
39
|
+
* Best-effort, in the same spirit as `rotateDaemonLog`: dying over housekeeping is strictly worse
|
|
40
|
+
* than a large file.
|
|
41
|
+
*/
|
|
42
|
+
export function recoverOversizedLog(deps, max = MAX_DAEMON_LOG_BYTES) {
|
|
43
|
+
try {
|
|
44
|
+
const size = deps.size();
|
|
45
|
+
if (max >= size)
|
|
46
|
+
return 0;
|
|
47
|
+
deps.truncate();
|
|
48
|
+
return size;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The daemon's log, as the daemon itself can address it: its own stderr.
|
|
56
|
+
*
|
|
57
|
+
* `spawnDaemon` hands the child `stdio: ['ignore', logFd, logFd]`, so inside a running daemon fd 2
|
|
58
|
+
* IS `daemon-<port>.log`, opened with O_APPEND — which is what makes truncating it safe: an
|
|
59
|
+
* appending write always lands at the new end of file, so there is no sparse hole and no lost
|
|
60
|
+
* handle. No path, no port and no second open are needed to reach it. When the daemon runs in the
|
|
61
|
+
* foreground fd 2 is a terminal, whose reported size is 0, so this is a no-op there.
|
|
62
|
+
*/
|
|
63
|
+
const DAEMON_LOG_FD = 2;
|
|
64
|
+
/** Emitted into the fresh log immediately after a truncation, so the gap explains itself. */
|
|
65
|
+
export const DAEMON_LOG_TRUNCATED_EVENT = 'reticle_daemon_log_truncated';
|
|
66
|
+
const daemonLogFdOps = {
|
|
67
|
+
size: () => fstatSync(DAEMON_LOG_FD).size,
|
|
68
|
+
truncate: () => ftruncateSync(DAEMON_LOG_FD, 0),
|
|
69
|
+
};
|
|
70
|
+
/** Keep a RUNNING daemon's log under the cap. Returns the bytes reclaimed, 0 when it was fine. */
|
|
71
|
+
export function guardDaemonLog(deps = daemonLogFdOps) {
|
|
72
|
+
return recoverOversizedLog(deps);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* How long a daemon log outlives its daemon.
|
|
76
|
+
*
|
|
77
|
+
* Every port ever bound leaves a `daemon-<port>.log` behind and nothing removed any of them — one
|
|
78
|
+
* machine held 128 files in `~/.reticle`, logs the large majority. Nothing enumerates them either:
|
|
79
|
+
* every reader (`doctor`, the startup-cause reader, the lifecycle messages) opens the log of the
|
|
80
|
+
* port it is asking about right now, so a log for a port that will never be bound again is read by
|
|
81
|
+
* nobody. A week is long enough to still have last Tuesday's crash when somebody asks about it.
|
|
82
|
+
*/
|
|
83
|
+
export const DAEMON_LOG_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
84
|
+
/**
|
|
85
|
+
* Delete daemon logs nothing has written to in the retention window. Returns the names removed.
|
|
86
|
+
*
|
|
87
|
+
* Age is the whole test, and it doubles as the liveness probe: a live daemon beats into its log
|
|
88
|
+
* every thirty seconds, so a log older than a week has no daemon behind it by construction.
|
|
89
|
+
* Best-effort throughout — a log that will not delete is not a reason to fail a daemon start.
|
|
90
|
+
*/
|
|
91
|
+
export function pruneOldDaemonLogs(home, nowMs) {
|
|
92
|
+
const pruned = [];
|
|
93
|
+
let files;
|
|
94
|
+
try {
|
|
95
|
+
files = readdirSync(home);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return pruned; // no ~/.reticle yet → nothing to prune
|
|
99
|
+
}
|
|
100
|
+
for (const file of files) {
|
|
101
|
+
if (null === /^daemon-\d+\.log(\.1)?$/.exec(file))
|
|
102
|
+
continue;
|
|
103
|
+
const path = join(home, file);
|
|
104
|
+
try {
|
|
105
|
+
if (DAEMON_LOG_RETENTION_MS > nowMs - statSync(path).mtimeMs)
|
|
106
|
+
continue;
|
|
107
|
+
unlinkSync(path);
|
|
108
|
+
pruned.push(file);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// unreadable or already gone — either way it is not this process's problem
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return pruned;
|
|
115
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** One client we could not write to, and why. */
|
|
2
|
+
export interface ManualNotice {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly why: string;
|
|
6
|
+
}
|
|
7
|
+
/** Where the stamp lives, under the state home the caller resolves. */
|
|
8
|
+
export declare const NOTICE_MEMORY_BASENAME = "agent-notices.json";
|
|
9
|
+
/** Identity of a notice: the same client, the same file, for the same reason. */
|
|
10
|
+
export declare function noticeKey(n: ManualNotice): string;
|
|
11
|
+
/** Pure. The notices `said` has not already recorded, in order, without repeats. */
|
|
12
|
+
export declare function unsaidNotices(said: readonly string[], notices: readonly ManualNotice[]): ManualNotice[];
|
|
13
|
+
/** Every key recorded before. An unreadable or malformed stamp reads as "nothing was said". */
|
|
14
|
+
export declare function readSaid(stateHome: string): string[];
|
|
15
|
+
/** Record these keys beside whatever was there. A failed write costs a repeated line, nothing more. */
|
|
16
|
+
export declare function rememberSaid(stateHome: string, keys: readonly string[]): void;
|